arrayList的底层原理
- ArrayList是个动态数组,实现List接口,主要用来存储数据,如果存储基本类型的数据,如int,long,boolean,short,byte,那只存储它们对应的包装类。
它的特点是:
删除慢:每次删除元素,都需要更改数组长度、拷贝以及移动元素位置。
查询快:由于数组在内存中是一块连续空间,因此可以根据地址+索引的方式快速获取对应位置上的元素。
当开启多个线程操作List集合,向ArrayList中增加元素,同时去除元素。最后输出list中的所有数据,会出现几种情况:
①有些元素输出为Null;②数组下标越界异常
使用无参构造器创建ArrayList对象时,默认容量是0
当往ArrayList中添加了一个元素后,默认容量自动扩充成10
ArrayList默认数组大小是10。
ArrayList的扩容主要发生在向ArrayList集合中添加元素的时候,通过add()方法添加单个元素时,会先检查容量,看是否需要扩容。如果容量不足需要扩容则调用扩容方法,扩容后的大小等于扩容前大小的1.5倍。比如说超过10个元素时,会重新定义一个长度为15的数组。然后把原数组的数据,原封不动的复制到新数组中,这个时候再把指向原数组的地址换到新数组。
arrayList与LinkedList的区别
ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
1. ArrayList基于数组的存储结构,查询效率快,删除和添加数据比较慢,线程不安全
2. LinkedList基于链式存储结构,查询效率慢,删除和添加效率快,线程不安全
ArrayList优势:元素随机访问速度快
ArrayList的底层数据结构是数组,这也就使得ArrayList特性上有了数组的的特征,对于数组来说,我们可以通过元素下标很轻松的一次性定位到元素,因此,这就使得ArrayList在元素的访问上具有大的优势。
ArrayList劣势:插入、删除元素速度慢
由于其底层是数组,数组存入内存中是连续的存储空间,因此在进行插入和删除操作的时候,在插入位置,添加插入元素后,后面的位置下标需要向后面移动,删除操作时,删除元素位置后的下标都需要向前移动,这样执行的复杂程度随元素增多而增多,运行速度自然相对较慢
LinkedList的优势:插入、删除操作速度快
Linkedlist的底层数据结构是双向链表,由于链表以结点作为存储单元,这些存储单元可以是不连续的。每个结点由两部分组成:存储的数值+前序结点和后序结点的指针,在数据删除的时候,只需要将这个元素的前后指针删除,前一个元素的指针直接指向后一个数据就行,更改的只是指针地址,并不影响其他,因此,LinkedList在数据插入和删除时具有绝对优势。
LinkedList的劣势:随机访问速度慢同样,也是由于指针是需要一步一步向后移动的,直到找到需操作的节点,因此,在随机访问时,只能进行遍历,随着元素增多,遍历数据量越来越大,因此随机访问速度慢;
hashMap的底层原理
- HashMap 底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap 的时候,并不会马上创建一个数组,而是在我们第一次使用hashmap自己的put方法的时候,创建一个长度为16的数组。
在调用 put(key,value)向hashmap 中存值得时候,会根据 key 的 hashcode 得到一个数值,用这个值和数组长度取模求余数,得到要存放到数组中得下标位置,然后看这个位置上是否为空,如果为空,则直接将这个键值对存储到这个位置,如果不为空,则判断两个数据的hash值是否相同,如果两个数据的hash值不相同,那我们这个时候就会在这个桶下生成一个链表,用来存储数据,如果两个数据的hash值相同的话则会对两个数据进行一次判断
Key值相同:直接覆盖
Key值不同:从这个桶位的链表开始,一直往下比,如果key值一直不同,就存在链表尾部,如果这个时候链表长度超过了8,那么链表就会转化成红黑树
这样就解决了 hash 冲突的问题。
在调用 get(key)来获取值得时候,同样计算 key 的hashcode 值,将得到的值和数组的长度取模求余数,从而得到下标,根据下标,找到数组中具体的位置,然后再调用 equals 方法,将 key 和该位置的 key 进行对比,如果相同,就获取该位置的值,如果不同,则去该位置后面的链表中一个个的进行对比,如果找到相同的 Key,就返回这个位置的值,如果还找不到则返回空。为了提高查询的速度,在 Jdk1.8 引入了红黑树,目的就是为了避免单条链表过长而影响查询效率
hashMap如何扩容
HashMap的负载因子为 0.75。也就是说,默认情况下,数组大小为16,那么当 HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知 HashMap 中元素的个数,那么预设元素的个数能够有效的提高 HashMap 的性能。
HashMap 和 HashTable 有什么区别?
①、HashMap 是线程不安全的,HashTable 是线程安全的;
②、由于线程安全,所以 HashTable 的效率比不上 HashMap;
③、HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而 HashTable不允许;
④、HashMap 默认初始化数组的大小为16,HashTable 为 11,前者扩容时,扩大两倍,后者扩大两倍+1;
⑤、HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode
mybatis
(1)Mybatis常用标签
(1)执行sql的标签
select,update,insert,delete:进行增删改查。
标签的常用属性有
id和方法名一致,
parameterType 用来接收接口层过来的参数,
resultType,resultMap用来处理查询出来的返回值放到java的对象中。
resultType处理的的是java类型,resultMap要和resultMap标签配合使用
动态常用标签有
if:if是判断标签主要用于动态条件拼接,里边有test属性可用来进行是否要拼接这个条件的判断。
foreach:动态循环标签,主要用来进行批量操作,比如动态新增修改删除,排序等。
里边的属性collection声明集合的类型常用值有array,list,map,也可以填写对象中的属性名,会自动识对应的数据类型。
open 和close是进行格式化的,进行一次循环开始和结束的符号设置
speator:用某种符号分隔开
index:循环的下表
item:循环取值的对象
格式化标签:
where:格式化查询条件,会默认将条件中的第一个and剔除
sql:用于格式化常用的sql语句.
include标签:用于引用定义的常量
(2)#和$的区别,如何解决sql注入
两者在 MyBatis 中都可以作为 SQL 的参数占位符,在处理方式上不同
#{}:在解析 SQL 的时候会将其替换成 ? 占位符,然后通过 JDBC 的 PreparedStatement (跑佩尔他斯d他门他)对象添加参数值,这里会进行预编译处理,可以有效地防止 SQL 注入,提高系统的安全性
${}:在 MyBatis 中带有该占位符的 SQL 片段会被解析成动态 SQL 语句,根据入参直接替换掉这个值,然后执行数据库相关操作,存在 SQL注入 的安全性问题
解决sql注入:
对前台用户的输入进行过滤。具体实现是把正确的字段或者排序规则放到枚举类中,在枚举类中提供static工具方法,根据传过来的字段字符串,判断该字符串是否在枚举类中,判断使用枚举类.valueof(column),如果存在返回true说明该字段符合要求,不存在就不拼接该字段,这样就可以过滤一些恶意输入会引起sql注入的输入值.
(3)mybatis的一二级缓存
一级缓存:
一级缓存作用在同一个SqlSession中,Mybatis默认开启。在同一个sqlSession中两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。当一个sqlSession结束后该sqlSession中的一级缓存也就不存在了。数据库更新操作也会清空一级缓存。
二级缓存:
二级缓存作用在同一个 Mapper 中,二级缓存开启需要在application.properties配置文件中增加开启配置:在写sql的xml文件中增加<cache>,同时在返回值对应的对象上增加序列化。执行数据库查询操作前,如果在二级缓存中有对应的缓存数据,则直接返回,没有的话则去一级缓存中获取,如果有对应的缓存数据,则直接返回,不会去访问数据库,
二级缓存的更新:在当前的namespace中执行了增删改的任何一种操作,都会清空改namespace对应的二级缓存,下次查询就需要访问数据库。
mysql的概念
(1)mysql的架构
客户端请求--->连接器(验证用户身份,给予权限)--->查询缓存(存在则直接返回,不存在则执行后续操作 --->分析器(对SQL进行词法分析和语法分析操作) --->优化器(主要对执行的sql优化选择最优的执行方案方法)--->执行器(执行时会先看用户是否有执行权限,有才去使用这个引擎提供的接口)--->去引擎层获取数据返回(如果开启查询缓存则会缓存查询结果)
(2)mysql有哪些引擎
mysql支持的存储引擎有八种,我们常用的两种InnoDB和MyISAM。这两者的区别:
1.InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
2.InnoDB 支持外键,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;
3.InnoDB 是聚簇索引,MyISAM 是非聚簇索引。聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先通过辅助索引查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
4.InnoDB 不保存表的具体行数,执行select count(*) from table 时需要全表扫描。而 MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
5.InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
(3)mysql的索引 index
索引(Index)是帮助MySQL高效获取数据的,索引的本质是:有序的数据结构
索引的优缺点:
减少查询需要检索的行数,加快查询速度,避免进行全表扫描,这也是创建索引的最主要的原因。如果索引的数据结构是B+树,在使用分组和排序时,可以显著减少查询中分组和排序的时间。通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
当对表中的数据进行增加、删除和修改时,索引也要进行更新,维护的耗时随着数据量的增加而增加。索引需要占用物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
索引常用命令:
创建索引: CREATE [UNIQUE] INDEX indexName ON mytable(username(length)); |
实例: CREATE INDEX name_index ON tb_teacher(t_name(20)); ALTER table tb_teacher ADD INDEX phone_index(t_phone); |
删除索引: DROP INDEX [indexName] ON mytable; |
查看表中的索引 SHOW INDEX FROM tb_teacher; |
索引的类型:
普通索引:仅加速查询
唯一索引:加速查询 + 列值唯一(可以有null)
主键索引:加速查询 + 列值唯一 + 表中只有一个(不可以有null)
组合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并 减少回表查询 遵循最左匹配原则
维护成本较高
(4)如何判断索引是否生效?
在查询语句前加上explain,查看sql语句的执行计划。type:这是重要的列,显示连接使用了何种类型。它在 SQL优化中是一个非常重要的指标。如果type的类型是ALL的话代表语句没有走索引,走了全表扫描。
(5)为什么Mysql用B+树做索引而不用B-树?
因为B+树的data只存储在叶子节点上,B树的所有节点都存储了key和data,B+树的非叶节点不存储data,这样一个节点就可以存储更多的索引值,可以使得树更矮(高度更小),减少I/O次数,磁盘读写代价更低,I/O读写次数是影响索引检索效率的最大因素;
B+树的所有叶结点构成一个有序链表,顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历,相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
(6)索引失效?
(1)使用组合索引没有满足最左匹配原则,也就是最左边的字段一定要在查询条件中。
(2)where 后面有不是索引的字段,但是查询的结果集,超过了总行数 25%,优化器就会认为没有必要走索引了。
(3)加了索引的字段在查询条件中参与运算或者使用了函数
(4)字段的类型不同
mysql发现如果是int类型字段作为查询条件时,它会自动将该字段的传参进行隐式转换, 把字符串转换成int类型。
(5)模糊查询%放前边会造成索引失效
(6)列对比也会造成索引失效
(7)查询条件用or时,如果or的字段中有没有索引的字段那么索引就会失效
解决办法把两个条件分开查询,在使用UNION 进行连接,这样有索引的字段就会走索引。
(7)sql优化
1、查询SQL尽量不要使用select *,而是select具体字段。
理由:只取需要的字段,节省资源、减少网络开销。
select * 进行查询时,很可能就不会使用到覆盖索引了,就会造成回表查询。
2、如果知道查询结果只有一条或者只要最大/最小一条记录,建议用limit 1
加上limit 1后,只要找到了对应的一条记录,就不会继续向下扫描了,效率将会大大提高。
- 应尽量避免在where子句中使用or来连接条件,如果or的字段中有没有索引的字段那么索引就会失效。解决办法把两个条件分开查询,在使用UNION 进行连接,这样有索引的字段就会走索引。
- 优化你的like语句
日常开发中,如果用到模糊关键字查询,很容易想到like,但是like很可能让你的索引失效。把%放前面,并不走索引。
5.尽量避免在索引列上使用mysql的内置函数,查询的字段不能使用内置函数否则索引失效
6.应尽量避免在where子句中对字段进行表达式操作,这将导致系统放弃使用索引而进行全表扫描.
7、Inner join 、left join、right join,优先使用Inner join,如果是left join,左边表结果尽量小表
8、应尽量避免在where子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
9、使用联合索引时,注意索引列的顺序,一般遵循最左匹配原则
10.对查询进行优化,应考虑在where及order by涉及的列上建立索引,尽量避免全表扫描。
(8)SQL执行顺序
from on join where group by(开始使用select中的别名,后面的语句中都可以使用) avg,sum.... having select distinct
order by limit
事务
做事情所投入的资源.
(1)事务的ACID
1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务是一个不可分割的整体。
2、一致性(Consistency):事务从一个一致性状态到另一个一致性状态。
3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。
4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
(2)事务的并发问题
1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
3、幻读:幻读是指当事务不是独立执行时发生的一种现象。事务A读取与搜索条件相匹配的若干行。事务B以插入或删除行等方式来修改事务A的结果集,提交后导致事物A前后读取不一致,出现幻读。
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
(3)事务隔离级别
mysql默认的事务隔离级别为repeatable-read(可重复读)
查看数据库隔离级别的命令:select @@tx_isolation ;
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交(read-uncommitted) | 是 | 是 | 是 |
不可重复读(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read)瑞裴他报 | 否 | 否 | 否 |
串行化(serializable) | 否 | 否 | 否 |
(4)事务的传播特性
Propagation (事务的传播属性)
Propagation :key属性确定代理应该给哪个方法增加事务行为。这样的属性最重要的部份是传播行为。有以下选项可供使用:
_REQUIRED--支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。propagation_required
_SUPPORTS--支持当前事务,如果当前没有事务,就以非事务方式执行。supports
_MANDATORY(慢的他瑞)--支持当前事务,如果当前没有事务,就抛出异常。mandatory
_REQUIRES_NEW--新建事务,如果当前存在事务,把当前事务挂起。requires_new
_NOT_SUPPORTED--以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。not_supported
_NEVER(绝不)--以非事务方式执行,如果当前存在事务,则抛出异常。never
(5)springboot框架配置事务
@Transaction这个注解的使用
需要在启动类上加上 @EnableTransactionManagement
常用参数:propagation:用法propagation = Propagation.REQUIRED 事务的传播特性
REQUIRED:支持当前事务,如果当前没有事务,就新建一个事务.这是最常见的选择。 SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。 MANDATORY:支持当前事务,如果当前没有事务,就抛出异常。
REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。
NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
NESTED:支持当前事务,如果当前事务存在,则执行一个嵌套事务,如果当前没有事务,就新建一个事务。
参数 :事务的隔离级别:isolation(哎斯嘞神)
使用:isolation = Isolation.DEFAULT
Isolation.READ_UNCOMMITTED : 读取未提交数据(会出现脏读, 不可重复读) 基本不使用 Isolation.READ_COMMITTED : 读取已提交数据(会出现不可重复读和幻读) Isolation.REPEATABLE_READ:可重复读(会出现幻读)
Isolation.SERIALIZABLE:串行化
参数:timeout:
使用:timeout = -1 单位是秒
timeout = -1 没有时间限制
参数: readOnly 属性用于设置当前事务是否为只读事务,设置为true表示只读, false则表示可读写,默认值为false。
参数:rollbackFor,该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。
参数:noRollbackFor,该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。
(6)哪种情况下配置的事务会失效?(做过那些代码中的优化?)
(1)事务要想发挥作用就必须抛出异常,如果用try catch代码进行扑捉异常但是没有抛出异常,就会失效。
(2)如果Transactional注解应用在非 public 修饰的方法上,Transactional将会失效。
idea直接会给出提示Methods annotated with ‘@Transactional’ must be overridable ,原理很简单,private修饰的方式, spring无法生成动态代理。
(3)@Transactional 注解属性 propagation 设置错误
TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
(4)@Transactional 注解属性 rollbackFor 设置错误。rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException的异常)或者 Error才回滚事务,其他异常不会触发回滚事务。
(5)同一个类中方法调用,导致@Transactional失效。开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B,但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。
(6)数据库本身不支持。mysql数据库,必须设置数据库引擎为InnoDB。
springboot
(1)为啥使用springboot项目?
Spring Boot 以约定大于配置核心思想开展工作,相比Spring具有如下优势:
(1)Spring Boot可以快速创建独立的Spring应用程序。
(2)Spring Boot内嵌了如Tomcat,也就是说可以直接跑起来,用不着再做部署工作了。
(3)Spring Boot无需再像Spring一样使用一堆繁琐的xml文件配置。
(4)Spring Boot可以自动配置(核心)Spring。SpringBoot将原有的XML配置改为Java配置,将bean注入改为使用注解注入的方式(@Autowire),并将多个xml、properties配置浓缩在一个appliaction.yml配置文件中。
(5)Spring Boot提供了一些现有的功能,如量度工具,表单数据验证以及一些外部配置这样的一些第三方功能。
(6)Spring Boot 可以快速整合常用依赖(开发库,例如spring-webmvc、jackson-json、validation-api和tomcat等),提供的POM可以简化Maven的配置。当我们引入核心依赖时,SpringBoot会自引入其他依赖。
(2)springboot的核心注解
启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:是实现自动配置的入口,该注解又通过 @Import 注解导入了AutoConfigurationImportSelector,在该类中加载 META-INF/spring.factories 的配置信息。然后筛选出以 EnableAutoConfiguration 为 key 的数据,加载到 IOC 容器中,实现自动配置功能,解决了配置大量的参数。
@ComponentScan:Spring组件扫描。
(3)springboot多数据源配置?
SpringBoot配置多数据源主要是通过springboot的dynamic-datasource;dynamic-datasource是一个基于springboot的快速集成多数据源的启动器。
在pom中导入dynamic-datasource的jar包。然后在配置文件中配置多个数据源,通过spring.datasource.dynamic.primary指定默认的数据源,strict严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源。datasource配置具体数据源的连接地址和用户,密码。
使用 @DS 配置要使用哪个数据源进行数据源的切换。@DS 可以注解在方法上和类上。如果同时存在,方法注解优先于类上注解。一般是在service实现类的方法上加上@DS注解。不加注解指定数据源,会使用默认的数据源。
Spring自带事务@Transactional的实现在一个事务里,只能有一个数据库。所以要想实现动态多数据源下的统一提交和回滚,要使用@DSTransactional注解。
mysql的主从,分库分表
1、MySql主库在事务提交时会把数据变更作为事件记录在二进制日志Binlog中;
2、slave服务器会在一定时间间隔内对master二进制日志进行探测其是否发生改变,如果发生改变,则开始一个I/O线程请求master二进制事件。
3、主库随后创建Binlog Dump线程读取数据库事件并发送给I/O线程,I/O线程获取到事件数据后更新到从库的中继日志Relay Log中去,之后从库上的SQL线程读取中继日志Relay Log中更新的数据库事件并应用.
搭建过程
搭建主从首先创建两个数据库,一个当作主库,另一个当从库。修改主库的my.cnf 配置文件,开启二进制日志功能,设置二进制日志使用内存大小 、日志格式 、以及过期清理时间 。同时指定不需要同步的数据库名称 ,和要跳过主从复制中遇到的所有错误或指定类型的错误,避免slave端复制中断。比如:1062错误是指一些主键重复。
进入master容器中:连接到客户端:创建数据同步用户:并赋予从节点同步主节点数据库的权限。
- 修改从库的my.cnf 配置文件,除了要像主库一样配置以外,还需要配置中继日志、开启slave将复制事件写进自己的二进制日志以及将从库设置为只读。修改完配置文件后将主、从实例重启。进入从实例的容器,连接客户端,配置主从复制。执行主从复制的命令,命令中要指定主库的IP地址和端口号,用于同步数据的账号和密码,要复制的文件以及从文件的哪个位置开始复制,还有连接失败重试的时间间隔。然后使用start slave; 开启主从同步。
MyCat配置主从
- 配置主从首先要搭建一个MySQL的 主从。然后修改Mycat的配置文件,在serve.xml中增加一个用户设置密码,自定义逻辑库名称。
- 修改schema.xml,创建一个schema标签,name属性对应serve.xml中自定义的逻辑库名称,schema标签中有一个table标签,table标签的name属性对应数据库中的表名,多个表用逗号分开,primaryKey指明表中的主键,dataNode指向我们定义的dataNode标签,dataNode标签中配置数据库信息,name属性和table标签中dataNode保持一直,作为标识,让其能够找到对应的标签。database声明了数据库的名称,和真实数据库名称对应。dataHost 对应dataHost 标签,dataHost 标签中声明了主库和从库,分别是writeHost和readHost标签。writeHost包着readHost。这两个标签里分别配置了主库和从库的IP地址端口号和账号以及密码。
- 如果要开启读写分离,只需将dataHost 中的balance属性值修改为3,代表开启读写分离。我们搭建主从并没有进行分表,所以不配置分片规则,这个时候就要修改autopartition-long.txt文件,文件默认分了3个片,将后两个删掉就可以了。
- myCat配置分库分表
- 分库分表与主从的配置相差不大,首先修改Mycat的配置文件,在serve.xml中增加一个用户设置密码,自定义逻辑库名称。
- 修改schema.xml,创建一个schema标签,name属性对应serve.xml中自定义的逻辑库名称,schema标签中有一个table标签,table标签的name属性对应数据库中的表名,多个表用逗号分开,primaryKey指明表中的主键,rule指明分片的规则,dataNode中配置多个数据库信息,多个之间使用逗号分割,指向我们定义的dataNode标签。如果配置3个库,就写3个dataNode标签,dataNode标签中配置数据库信息,name属性和table标签中dataNode保持一直,作为标识,让其能够找到对应的标签。database声明了数据库的名称,和真实数据库名称对应。dataHost 对应dataHost 标签,dataHost 标签也配3个,dataHost 标签中声明开启读写分离,只需将dataHost 中的balance属性值修改为3,同时声明了主库和从库,分别是writeHost和readHost标签。writeHost包着readHost。这两个标签里分别配置了主库和从库的IP地址端口号和账号以及密码。
schema标签中配置了分片规则,如果按照auto-sharding-long进行分片就需要在autopartition-long.txt文件中声明具体存放规则。
线程
线程是指进程中的一个执行流程,一个进程中可以运行多个线程。线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。
在 Java 中实现线程的方式,一般有三种
继承Thread
实现Runnable接口,重写run方法
实现Callable接口,重写call方法(带返回值和可以抛出异常)
实际上Thread 类也是 Runnable 接口的子类,但在Thread类中并没有完全实现 Runnable 接口中的 run() 方法,
从代码扩展来说实现Runnable 接口比较好,可以方便的实现资源的共享。因为java是单继承,多实现。
Runnable和Callable的区别
(1) Callable规定的方法是 call(), Runnable规定的方法是 run()。
(2) Callable的任务执行后可返回值,而 Runnable的任务是不能返回值。
(4)运行 Callable任务可以拿到一个 Future(菲浅)对象
线程一般具有5种状态
也叫做线程的生命周期,即创建,就绪,运行,阻塞,终止。
创建(new)状态: 创建了一个线程的对象,即执行了new Thread(); 创建完成后就需要为线程分配内存
就绪(runnable)状态: 调用了start()方法, 等待CPU进行调度
运行(running)状态: 执行run()方法
阻塞(blocked)状态: 暂时停止执行线程,将线程挂起(sleep()、wait()、join()、没有获取到锁都会使线程阻塞), 可能将资源交给其它线程使用
死亡(terminated)状态: 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程的常用方法有:
start()方法:启动一个线程,使线程进入就绪状态。
run()方法:线程的执行体,具体实现在这个方法中。
sleep:休眠(Thread.sleep() 即可实现休眠。)不会释放对象锁。 sleep() 使当前线程进入阻塞状态,在指定时间内不会执行。
wait:Object 类的方法,对此对象调用 wait 方法导致本线程放弃对象锁,进入等待。可以由其他线程通过调用notify()或者notifyAll()方法,来唤醒此线程。
yield()方法:暂停当前线程的执行,让其他具有相同优先级的线程有机会继续执行。
join 线程的强制运行:在线程操作中,可以使用 join() 方法让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。
interrupt()方法:中断线程的执行,通常用于终止一个正在运行的线程。
isAlive()方法:判断一个线程是否还活着,即线程是否已经启动且尚未终止。
sleep和wait的区别?
wait 方法必须配合 synchronized一起使用,不然在运行时就会抛出异常,而sleep可以单独使用,无需配合synchronized一起使用。
wait 方法属于 Object 类的方法,而sleep属于 Thread 类的方法
sleep 方法具有主动唤醒功能,而wait 方法只能被动的被唤醒。
wait 方法会主动的释放锁,而 sleep 方法则不会。
调用 sleep 方法线程会进入有时限等待状态,而调用无参数的 wait 方法,线程会进入 无时限等待状态。
锁
Synchronized:
(1)java提供关键字,提供给对象的加锁功能
(2)synchronized自动的添加和释放锁
(3)他是一个互斥锁.只有获取锁的线程才能执行,其他线程只能等待该线程释放锁后,再获取锁才能执行。
(4)synchronized是非公平锁,它无法保证等待的线程获取锁的顺序
(5)如果加在方法上,分两种情况。这个是非静态方法:就是对对象加锁,每次创建对象都会创建一把新锁,锁得是该对象。
这个方法是静态方法时:项目启动时类进行加载然后创建锁,这个锁只会被创建一次,锁的是类对象。
(6)synchronized加在代码块中,小括号中的对象会被上锁。
Lock:
Lock接口提供了与synchronized相似的同步功能,Lock在使用的时候是显式的获取和释放锁。虽然Lock接口缺少了synchronized隐式获取释放锁的便捷性,但是对于锁的操作具有更强的可操作性、可控制性以及提供可中断操作和超时获取锁等机制。
Lock锁(CAS比较并置换)和synchronized(AQS)锁的区别
1、Synchronized 是Java的一个关键字,而Lock是一个接口
2、Synchronized 使用过后,会自动释放锁,而Lock需要手动上锁、手动释放锁。
3、Lock提供了更多的实现方法,而且可响应中断,可定时,而synchronized 关键字不能响应中断;
4、synchronized关键字是非公平锁,即,不能保证等待锁的那些线程们的顺序,而Lock的子类ReentrantLock默认是非公平锁,但是可通过一个布尔参数的构造方法实例化出一个公平锁;
5、synchronized无法判断是否已经获取到锁,而Lock通过tryLock()方法可以判断,是否已获取到锁
6.Lock可以通过分别定义读写锁提高多个线程读操作的效率。
7.二者的底层实现不一样:synchronized是同步阻塞,采用的是悲观并发策略;Lock是同步非阻塞,采用的是乐观并发策略(底层基于volatile关键字和CAS算法实现)
synchronized 锁升级:
在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁(只要有另一个竞争线程就升级),通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
常见的锁
悲观锁:当前线程去操作数据的时候,总是认为别的线程会去修改数据,所以每次操作数据的时候都会上锁,别的线程去操作数据的时候就会阻塞,比如synchronized;
乐观锁:当前线程每次去操作数据的时候都认为别人不会修改,更新的时候会判断别人是否会去更新数据,通过版本来判断,如果数据被修改了就拒绝更新,
总结:悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,乐观锁的吞吐量会比悲观锁高
公平锁:有多个线程按照申请锁的顺序来获取锁,就是说,如果一个线程组里面,能够保证每个线程都能拿到锁
非公平锁:获取锁的方式是随机的,保证不了每个线程都能拿到锁,会存在有的线程饿死,一直拿不到锁
总结:非公平锁性能高于公平锁,更以重复利用CPU的时间
3、可重入锁和不可重入锁
可重入锁:也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不会产生死锁
不可重入锁:在当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞
总结:可重入锁能一定程度的避免死锁,例如:synchronized,ReentrantLock
4、自旋锁
自旋锁:一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有一个执行单元获得锁
总结:不会发生线程状态的切换,一直处于用户态,减少了线程上下文切换的消耗,缺点是循环会消耗CPU
5、共享锁和独享锁
共享锁:也叫读锁,可以查看数据,但是不能修改和删除的一种数据锁,加锁后其他的用户可以并发读取,但不能修改、增加、删除数据,该锁可被多个线程持有,用于资源数据共享
独享锁:也叫排它锁、写锁、独占锁、独享锁,该锁每一次只能被一个线程所持有,加锁后任何线程试图再次加锁都会被阻塞,直到当前线程解锁。例如:线程A对data加上排它锁后,则其他线程不能再对data加任何类型的锁,获得互斥锁的线程既能读数据又能修改数据
线程池单一线程池,同步变异步,redis采用单一线程
性能损耗加锁
Java线程池
(1) 什么是线程池
线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源
(2) 为什么要有线程池
在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或"切换过度"而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。
(3) 线程池可以干什么
线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销,就被分摊到了多个任务上了,而且由于在请求到达时,线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快;另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。
(4) 线程池框架Executor
java中的线程池是通过Executor框架实现的,Executor 框架包括类:Executor,Executors,ExecutorService,ThreadPoolExecutor ,Callable和Future、FutureTask的使用等。
Executor: 所有线程池的接口,只有一个方法。
ExecutorService: 增加Executor的行为,是Executor实现类的最直接接口。
Executors: 提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService 接口。
ThreadPoolExecutor:线程池的具体实现类,一般用的各种线程池都是基于这个类实现的。
(1)corePoolSize(酷儿噗赛):线程池核心线程数量,核心线程不会被回收,即使没有任务执行,也会保持空闲状态,如果线程池中的线程少于此数目,则在执行任务时创建。
(2)maximumPoolSize(麦克岁木噗赛):线程池最大线程数,表示在线程池中最多能创建多少个线程。 当线程数量达到corePoolSize,且workQueue队列塞满任务了之后,继续创建线程 ,当线程池中的线程数量到达这个数字时,新来的任务会执行拒绝策略。
(3)keepAliveTime(k噗额来太木):表示线程没有任务执行时最多能保持多少时间会被回收,注意,这个参数控制的是超过corePoolSize之后的“临时线程”的存活时间。
(4)unit(由内他):参数 keepAliveTime 的时间单位。
(5)workQueue(喔q):工作队列,存放提交的等待任务,其中有队列大小的限制。
(6)threadFactory(思瑞嘚):创建线程的工厂类,通常我们会自定义一个threadFactory设置线程的名称,这样我们就可以知道线程是由哪个工厂类创建的,可以快速定位排查问题。
(7)handler(汉得l):如果线程池已满,新的任务进来时的拒绝策略。
(6) jdk中提供了四种工作队列:
①ArrayBlockingQueue:基于数组的有界阻塞队列,按先进先出排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene:基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照先进先出排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene:一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue (课后任务):具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
(7) 拒绝策略有那些?
jdk中提供了四种线程池拒绝策略(handler)
AbortPolicy(默认,额报一他跑来岁):丢弃任务并抛出 RejectedExecutionException 异常。
CallerRunsPolicy(扣嘞软子):由调用线程处理该任务。
DiscardPolicy(迪斯卡的):丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
DiscardOldestPolicy(迪斯卡的偶带斯特):丢弃队列最早的未处理任务,然后重新尝试执行任务。
(8) 线程池的工作过程?
线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
当调用 execute() 方法添加一个任务时,线程池会做如下判断:如果正在运行的线程数量小于核心线程数,那么马上创建线程运行这个任务;如果正在运行的线程数量大于或等于核心线程数,那么将这个任务放入队列;如果这时候队列满了,而且正在运行的线程数量小于 最大线程数,那么还是要创建非核心线程立刻运行这个任务;如果队列满了,而且正在运行的线程数量等于最大线程数,那么线程池会抛出异常RejectExecutionException。当一个线程完成任务时,它会从队列中取下一个任务来执行。当一个线程无事可做,超过一定的时间时,线程池会判断,如果当前运行的线程数大于核心线程数,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到核心线程数的大小。
(1)newCachedThreadPool :创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
特点:初始化线程池不创建线程,当有线程访问时创建一个线程,默认最大可以创建Intege最大值个线程。已经执行完任务的线程,会在线程池存活一分钟,如果有新的任务,就会获得这个空闲线程去执行任务。
(2)newFixedThreadPool:创建一个固定数目的、可重用的线程池。
特点:就是最大线程数和核心线程数是相等的,里边的线程没有超时时间,多出来的线程是加在一个没有边界的队列中。
(3)newScheduledThreadPool:创建一个定时任务线程池,支持定时及周期性任务执行。
(4)newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
特点:运行过程中只有一个线程。
线程的参数设置的参考
一、聊聊线程池的参数配置规则
1、CPU密集型、IO密集型、混合型任务
2、任务执行时间
3、任务是否有依赖----比如其他系统资源(数据库,第三方接口等)
这里讲解,根据CPU密集型、IO密集型、任务执行时间来决定如何配置:核心线程数、最大线程数、等待队列数
1、CPU密集型:一般使用较小的线程池---》 CPU核心数+1
2、IO密集型:2*CUP核心数+1
submit和execute的区别
1. execute只能提交Runnable类型的任务,没有返回值,而submit既能提交Runnable类型任务也能提交Callable类型任务,返回Future类型。
2. execute方法提交的任务异常是直接抛出的,而submit方法是是捕获了异常的,当调用FutureTask的get方法时,才会抛出异常。
分布式锁
可以基于数据库和redis 提供的很多原子操作,redission看门狗机制
(1) 什么是分布式锁
分布式锁是指分布式环境下,系统部署在多个机器中,实现多进程分布式互斥的一种锁。为了保证多个进程能看到锁,锁被存在公共存储(比如 Redis、数据库等三方存储中),以实现多个进程并发访问同一个临界资源,同一时刻只有一个进程可访问共享资源,确保数据的一致性。 例如:有两个服务器,其中都有查询数据库分类数据的方法,同一时间,只能够有一个项目中的一个线程能够查询成功过。
(2) 为啥使用分布式锁?
为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题
针对分布式锁的实现,目前比较常用的有以下几种方案:
1、基于数据库实现分布式锁
2、基于缓存(redis)实现分布式锁
3、基于注册中心实现分布式锁
基于数据库实现分布式锁:主要是利用数据库的唯一索引来实现,唯一索引天然具有排他性,这刚好符合我们对锁的要求:同一时刻只能允许一个竞争者获取锁。加锁时我们在数据库中插入一条锁记录,利用业务id进行防重。当第一个竞争者加锁成功后,第二个竞争者再来加锁就会抛出唯一索引冲突,如果抛出这个异常,我们就判定当前竞争者加锁失败。防重业务id需要我们自己来定义,例如我们的锁对象是一个方法,则我们的业务防重id就是这个方法的名字,如果锁定的对象是一个类,则业务防重id就是这个类名。
基于缓存(redis)实现分布式锁:
通过将数据id存入redis缓存中同时设置失效时间,避免程序中断导致线程占用,如果新增时已经存在则表示当前线程被占用,进入自旋;如果不存在则可执行后续操作,通过try--finally语句块,在finally中使用del(key)释放锁。设置redis锁,首先要设置一个唯一的key,之后使用redisTemplate.opsForValue().setIfAbsent()比较并替换,如果返回false,表示redis锁已经存在,进入自旋锁;如果返回true,将数据存入redis缓存中同时设置失效时间,避免程序中断导致线程占用,之后通过try-catch-finally语句块,在try中写入守护线程,如果说代码执行时间过长自动续签,业务执行完毕后,在finally中使用redisTemplate.delete(key)释放锁。
还可以集成Redisson,使用Redisson分布式锁时,第一部也要设置一个唯一的key,与redis不同的是使用redissonClient.getLock(stockKey)加锁,通过try-catch-finally语句块,在try中使用lock.lock()方法进行上锁,拿锁失败时会不停的重试,不设置超时时间,看门狗自动延期机制默认续30s,每隔30/3=10 秒续到30s ,在实例被关闭前,可以自动续期最终在finally中关闭锁。
如果设置超时时间,就没有看门狗 ,超时后会自动释放 。
Nacos的Cap原理
默认是AP原子一致性,高可用,分区容错性
分布式事物
Seata的分布式事务解决方案是业务层面的解决方案,只依赖于单台数据库的事务能力。Seata框架中一个分布式事务包含3中角色:
Transaction Coordinator (TC):seata 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
Transaction Manager (TM):注解 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager (RM):seata客户端 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
其中,TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。
分布式事务在Seata中的执行流程:
TM (注解) 向 TC (seata)申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
XID 在微服务调用链路的上下文中传播。
RM (seata客户端) 向 TC 注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC。
TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。
TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
为什么Seata在第一阶段就直接提交了分支事务?
Seata能够在第一阶段直接提交事务,是因为Seata框架为每一个RM维护了一张UNDO_LOG表(这张表需要客户端自行创建),其中保存了每一次本地事务的回滚数据。因此,二阶段的回滚并不依赖于本地数据库事务的回滚,而是RM直接读取这张UNDO_LOG表,并将数据库中的数据更新为UNDO_LOG中存储的历史数据。
如果第二阶段是提交命令,那么RM事实上并不会对数据进行提交(因为一阶段已经提交了),而实发起一个异步请求删除UNDO_LOG中关于本事务的记录。
Seata执行流程
下面是一个Seata中一个分布式事务执行的详细过程:
1.首先TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
2.XID 在微服务调用链路的上下文中传播。
3.RM 开始执行这个分支事务,RM首先解析这条SQL语句,生成对应的UNDO_LOG记录。下面是一条UNDO_LOG中的记录:
4.RM在同一个本地事务中执行业务SQL和UNDO_LOG数据的插入。在提交这个本地事务前,RM会向TC申请关于这条记录的全局锁。如果申请不到,则说明有其他事务也在对这条记录进行操作,因此它会在一段时间内重试,重试失败则回滚本地事务,并向TC汇报本地事务执行失败。
5.RM在事务提交前,申请到了相关记录的全局锁,因此直接提交本地事务,并向TC汇报本地事务执行成功。此时全局锁并没有释放,全局锁的释放取决于二阶段是提交命令还是回滚命令。
6.TC根据所有的分支事务执行结果,向RM下发提交或回滚命令。
7.RM如果收到TC的提交命令,首先立即释放相关记录的全局锁,然后把提交请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。异步队列中的提交请求真正执行时,只是删除相应 UNDO LOG 记录而已。
8.RM如果收到TC的回滚命令,则会开启一个本地事务,通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。将 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理。否则,根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句并执行,然后提交本地事务达到回滚的目的,最后释放相关记录的全局锁。
AT 模式
一阶段:
在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段提交:
二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段回滚:
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写“业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。
Jvm(垃圾回收机制,内存区)
JVM是Java Virtual Machine(Java虚拟机)的缩写,是Java编程语言的核心组成部分之一。它是一个虚拟计算机,可以在不同的操作系统上运行Java程序。
JVM的主要功能是将Java字节码(即Java源代码编译后的中间代码)解释成本地机器码,并在计算机上执行这些指令。JVM还负责管理Java程序运行时的内存,包括堆内存和栈内存,以及协调Java程序与操作系统的交互。
JVM具有平台无关性,这意味着编写的Java程序可以在不同的操作系统上运行,只要这些系统上有安装了相应版本的JVM。这种跨平台的能力是由JVM中的字节码解释器实现的。
另外,JVM还具有一些高级功能,例如自动垃圾回收、即时编译和运行时类型检查。这些功能使得Java程序开发更加方便和安全。
JVM的内存模型
JVM的内存模型主要分为以下几个部分:
程序计数器(Program Counter Register):每个线程都有一个程序计数器,用于记录当前线程执行的字节码指令地址。程序计数器是线程私有的,它在线程切换时被保存和恢复,因此能够确保线程能够从正确的地方恢复执行。
Java虚拟机栈(Java Virtual Machine Stacks):每个线程都有一个私有的栈,用于存储局部变量、方法参数、方法返回值和操作数栈。Java虚拟机栈的内存空间在编译时就被确定了,因此在运行时不能改变。如果线程请求的栈深度大于虚拟机所允许的深度,就会抛出StackOverflowError堆栈溢出异常;如果虚拟机栈无法动态扩展,则会抛出OutOfMemoryError内存不足异常。
本地方法栈(Native Method Stack):与Java虚拟机栈类似,用于存储本地方法的局部变量和操作数栈。本地方法栈也是线程私有的 。
堆(Heap):所有线程共享的内存区域,用于存储对象实例和数组。Java虚拟机启动时会初始化一个堆区域,堆区域的大小可以通过-Xms和-Xmx等命令行参数进行调整。当堆中的对象没有被任何引用指向时,就可以被垃圾回收器回收。
方法区(Method Area):所有线程共享的内存区域,用于存储类信息、常量、静态变量等数据。方法区也被称为永久代,但在JDK8之后,永久代被移除,被Metaspace取代。
运行时常量池(Runtime Constant Pool):每个类都有一个运行时常量池,用于存储编译时期生成的各种字面量和符号引用。这些数据在类加载时被放入常量池中,供类的方法调用时使用。
JVM的垃圾回收机制
JVM的垃圾回收机制是Java的一个重要特性,其作用是在程序运行时自动管理和回收不再使用的对象和内存空间,避免出现内存泄漏和内存溢出等问题。
JVM的垃圾回收机制分为两个阶段:标记阶段和清除阶段。
标记阶段:在这个阶段,垃圾回收器会遍历所有的对象,标记出哪些对象仍然在被使用,哪些对象已经可以被回收。一般情况下,垃圾回收器使用可达性分析算法来进行标记。如果一个对象不再被任何其他对象引用,那么这个对象就可以被回收。
清除阶段:在这个阶段,垃圾回收器会清除所有已经被标记为不再使用的对象,并回收它们所占用的内存空间。这个阶段的实现方式有多种,其中最常见的方式是使用分代收集算法。根据这个算法,对象可以被分为新生代和老年代,然后使用不同的垃圾回收算法分别处理它们。
JVM的垃圾回收机制使用不同的垃圾回收算法来处理不同类型的对象。下面是一些常见的垃圾回收算法:
标记-清除算法:这个算法是最早的垃圾回收算法之一。它的主要思想是先标记所有需要回收的对象,然后再清除这些对象所占用的内存空间。但这个算法有一个明显的缺点,就是清除之后会留下一些内存碎片,导致内存空间的利用率变低。
复制算法:这个算法是将可用内存分成两个相等的区域,每次只使用其中一个区域。当一个区域的内存用完之后,就将这个区域中所有的存活对象复制到另一个区域中,然后清除这个区域中所有不再使用的对象。这个算法的优点是可以避免内存碎片的产生,但缺点是会浪费一半的内存空间。
标记-整理算法:这个算法是在标记-清除算法的基础上进行改进的。它的主要思想是在标记对象的同时,将所有存活的对象向一端移动,然后清除掉所有不再使用的对象。这个算法可以避免内存碎片的产生,并且不会浪费内存空间。
分代收集算法:这个算法是将对象按
CMS垃圾回收器
CMS是老年代回收器,只能回收老年代的对象,在收集过程中可以与用户线程并发操作。CMS牺牲了系统的吞吐量来追求收集速度,适合追求垃圾收集速度的服务器上。CMS收集器可以通过参数:-XX:+UseConcMarkSweepGC启用。
2. 收集过程
CMS收集器是基于算法标记-清除来实现的,整个过程分为5步:
初始标记
记录能被GC Root直接引用的对象,触发一次STW,但是这次STW很快,因为在标记的过程中不会标记一整条引用链的对象,如图所示,只记录红色箭头关联到的对象,不记录黑色箭头。
并发标记
从GC Roots的直接引用对象开始依次扫描(对上面的黑色箭头的链路做扫描),这个过程需要比较多的时间,用户线程和GC线程同时执行,不会产生STW,因为在扫描的过程中用户线程还在不断的执行所以可能会出现标记过的对象又变成了垃圾。
重新标记
重新标记需要Stop The World,这个阶段是为了修正在并发标记阶段产生的浮动垃圾,对标记过的对象进行。
并发清除
GC线程和用户线程同时进行,开始正式清除未被标记的垃圾,在此阶段也会产生垃圾(浮动垃圾),产生垃圾后无法清除,只能留待下一次GC。
在清除完后会重置标记信息。
收集过程总结:
在初始标记和重新标记有两次stop the world的标记操作
初始标记只标记GC Root关联的对象,速度很快
在初始标记触发STW的时候,它的标记方式还是原始的更改对象头MarkWord的GC标记字段,但是在并发标记阶段,因为是用户线程和GC线程同时在跑,所以采用的是三色标记的方式进行垃圾标记。
4. CMS三色标记
三色标记将对象的标记过程分为三种颜色:白色、灰色、黑色
白色:对象的默认颜色,从GC Root开始扫描,如果对象是不可达对象的话就是白色,也就是垃圾对象,在并发清理的时候会清理掉。
灰色:当前对象已经被扫描过,但是当前对象所依赖的其他对象还没有被扫描。
黑色:当前对象和它所依赖的对象都已经被扫描过,不会对黑色对象和引用的对象再次进行扫描。
三色标记使用在并发标记阶段,使用三色标记会导致两个问题,一个是漏标,一个是多标。
漏标
比如现在有ABCD四个对象,A依赖了B和C,B依赖了D,初始标记之后A对象已经被扫描过了所以是灰色,其他对象是白色:
继续往下执行扫描B和C,当B和C扫描完之后,A变成了黑色,B变成了灰色,C是黑色,D还是白色:
此时如果用户线程把B和D的引用去掉,让C依赖D,建立起C和D的关系之后B变成了黑色:
那么问题来了,C已经是黑色就不会再对其依赖的对象扫描了,但事实上C还有一个依赖对象D没有被扫描。此时如果进行垃圾回收的话D就会被回收掉,这就是所谓的漏标问题。
多标
还是用上面的例子进行说明,比如现在AB是黑色,C是灰色,D是白色,当GC正在扫描D的时候,B被置空了,从逻辑上讲B是垃圾,理应被回收,但是因为GC不会对黑色对象做重复扫描所以B还是黑色,在进行垃圾清理的时候不会被回收,只能等下次GC的时候再进行重新标记扫描。这种情况相对于漏标来说还行,起码不会导致系统出BUG。
漏标的解决方案
CMS使用增量更新的方式解决三色标记漏标问题。
增量更新:
将新增的引用维护到一个集合里面,将引用的源头变成灰色,等待重新标记阶段再重新进行一次扫描。比如:当D的引用指向了C,则会将C变成灰色,并将C放在一个新增引用的集合里面;在重新标记阶段会将C作为根节点继续向下扫描。
5. 为什么CMS要用标记清除法收集垃圾?
CMS的垃圾回收阶段是并发回收的,如果使用标记整理法收集的话,对象的内存地址会进行移动,因为用户线程还在执行,为了避免因内存地址移动带来的bug,还需要对用户线程的对象指针进行维护,在这个过程中肯定会STW,这样做就提高了垃圾清理的时长,停顿时间也变长了,不符合CMS一获取最短回收停顿时间为目的设计的初衷。
三、 七种经典垃圾回收器
JVM的垃圾回收器大体上的分类主要包括四种:串行、并行、并发(CMS)和G1
串行垃圾回收器(Serial):它为单线程环境设计并且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境
并行垃圾回收器(Parallel):多个垃圾回收线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理等弱交互场景
并发垃圾回收器(CMS):用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程。互联网公司多用它,适用于对响应时间有要求的场景
G1垃圾回收器:G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收
(一)新生代回收器:Serial、ParNew、Parallel Scavenge
1、Serial 垃圾回收器
Serial收集器是最基本的、发展历史最悠久的收集器。俗称为:串行回收器,采用复制算法进行垃圾回收
特点
串行回收器是指使用单线程进行垃圾回收的回收器。每次回收时,串行回收器只有一个工作线程。
对于并行能力较弱的单CPU计算机来说,串行回收器的专注性和独占性往往有更好的性能表现。
它存在Stop The World问题,即垃圾回收时,要停止程序的运行。
使用-XX:+UseSerialGC参数可以设置新生代使用这个串行回收器
2、ParNew 垃圾回收器
ParNew其实就是Serial的多线程版本,除了使用多线程之外,其余参数和Serial一模一样。俗称:并行垃圾回收器,采用复制算法进行垃圾回收
特点
ParNew默认开启的线程数与CPU数量相同,在CPU核数很多的机器上,可以通过参数-XX:ParallelGCThreads来设置线程数
它是目前新生代首选的垃圾回收器,因为除了ParNew之外,它是唯一一个能与老年代CMS配合工作的。
它同样存在Stop The World问题
使用-XX:+UseParNewGC参数可以设置新生代使用这个并行回收器
3、ParallelGC 回收器
ParallelGC使用复制算法回收垃圾,也是多线程的
特点
ParallelGC是非常关注系统的吞吐量,吞吐量=代码运行时间/(代码运行时间+垃圾收集时间)
-XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间,可用把虚拟机在GC停顿的时间控制在MaxGCPauseMillis范围内,如果希望减少GC停顿时间可以将MaxGCPauseMillis设置的很小,但是会导致GC频繁,从而增加了GC的总时间,降低了吞吐量。所以需要根据实际情况设置该值。
-Xx:GCTimeRatio:设置吞吐量大小,它是一个0到100之间的整数,默认情况下他的取值是99,那么系统将花费不超过1/(1+n)的时间用于垃圾回收,也就是1/(1+99)=1%的时间。
另外还可以指定-XX:+UseAdaptiveSizePolicy打开自适应模式,在这种模式下,新生代的大小、eden、from/to的比例,以及晋升老年代的对象年龄参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。
使用-XX:+UseParallelGC参数可以设置新生代使用这个并行回收器
(二)老年代回收器:CMS、Serial Old、Parallel Old
1、SerialOld 垃圾回收器
SerialOld是Serial回收器的老年代回收器版本,它同样是一个单线程回收器。
用途
一个是在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用,
另一个就是作为CMS收集器的后备预案,如果CMS出现Concurrent Mode Failure,则SerialOld将作为后备收集器。
使用算法:标记 - 整理算法
2、ParallelOldGC 回收器
老年代ParallelOldGC回收器也是一种多线程的回收器,和新生代的ParallelGC回收器一样,也是一种关注吞吐量的回收器,他使用了标记- 整理/压缩算法进行实现。
-XX:+UseParallelOldGc进行设置老年代使用该回收器
-XX:+ParallelGCThreads也可以设置垃圾收集时的线程数量。
3、CMS 回收器
CMS全称为:Concurrent Mark Sweep意为并发标记清除,他使用的是标记清除算法。主要关注系统停顿时间。
使用-XX:+UseConcMarkSweepGC进行设置老年代使用该回收器。
使用-XX:ConcGCThreads设置并发线程数量。
特点
CMS并不是独占的回收器,也就说CMS回收的过程中,应用程序仍然在不停的工作,又会有新的垃圾不断的产生,所以在使用CMS的过程中应该确保应用程序的内存足够可用。
CMS不会等到应用程序饱和的时候才去回收垃圾,而是在某一阀值的时候开始回收,回收阀值可用指定的参数进行配置:-XX:CMSInitiatingoccupancyFraction来指定,默认为68,也就是说当老年代的空间使用率达到68%的时候,会执行CMS回收
如果内存使用率增长的很快,在CMS执行的过程中,已经出现了内存不足的情况,此时CMS回收就会失败,虚拟机将启动老年代串行回收器;SerialOldGC进行垃圾回收,这会导致应用程序中断,直到垃圾回收完成后才会正常工作。
这个过程GC的停顿时间可能较长,所以-XX:CMSInitiatingoccupancyFraction的设置要根据实际的情况。
标记清除算法有个缺点就是存在内存碎片的问题,那么CMS有个参数设置-XX:+UseCMSCompactAtFullCollecion可以使CMS回收完成之后进行一次碎片整理
-XX:CMSFullGCsBeforeCompaction参数可以设置进行多少次CMS回收之后,对内存进行一次压缩
(三)全堆:G1
G1(Garbage-First)是一款面向服务器的垃圾收集器,支持新生代和老年代空间的垃圾收集,主要针对配备多核处理器及大容量内存的机器,G1最主要的设计目标是:实现可预期及可配置的STW停顿时间
1、G1分区
G1将Java堆划分为多个大小相等的独立区域(Region),每一个小方格代表一个Region,JVM最多可以有2048个Region
一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数-XX:G1HeapRegionSize手动指定Region大小,但是推荐默认的计算方式
1、Region
为实现大内存空间的低停顿时间的回收,将划分为多个大小相等的Region。每个小堆区都可能是 Eden区,Survivor区或者Old区,但是在同一时刻只能属于某个代
在逻辑上, 所有的Eden区和Survivor区合起来就是新生代,所有的Old区合起来就是老年代,且新生代和老年代各自的内存Region区域由G1自动控制,不断变动
2、巨型对象(Humongous区)
G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如每个Region是2M,只要一个对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。
作用:Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销
Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收
2、G1 垃圾回收器工作流程
1、初始标记(Initial Marking)
这阶段仅仅只是标记GC Roots能直接关联到的对象并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的可用的Region中创建新对象,这阶段需要停顿线程,但是耗时很短。而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿
2、并发标记(Concurrent Marking)
从GC Roots开始对堆的对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象,这阶段耗时较长,但是可以与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
3、最终标记(Final Marking)
对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录
4、筛选回收(Live Data Counting and Evacuation)
负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。可以自由选择多个Region来构成会收集,然后把回收的那一部分Region中的存活对象==复制==到空的Region中,在对那些Region进行清空
除了并发标记外,其余过程都要 STW
stop-the-world(STW)
stop-the-world会在任何一种GC算法中发生。stop-the-world 意味着JVM因为需要执行GC而停止应用程序的执行。
当stop-the-world 发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成。GC优化很多时候就是减少stop-the-world 的发生
以下是一些常见的JVM参数:
-Xms: 指定JVM堆的初始大小
-Xmx: 指定JVM堆的最大大小
-Xss: 指定每个线程栈的大小
-XX:MaxPermSize: 指定永久代(Permanent Generation)的最大大小
-XX:PermSize: 指定永久代的初始大小
-XX:NewSize: 指定新生代(Young Generation)的初始大小
-XX:MaxNewSize: 指定新生代的最大大小
-XX:SurvivorRatio: 指定Eden区和Survivor区的大小比例
-XX:MaxTenuringThreshold: 指定对象经过多少次GC后进入老年代
-XX:+UseConcMarkSweepGC: 启用CMS垃圾回收器
-XX:+UseParallelGC: 启用并行垃圾回收器
-XX:+UseG1GC: 启用G1垃圾回收器
-XX:MaxGCPauseMillis: 指定垃圾回收的最大暂停时间
-XX:+HeapDumpOnOutOfMemoryError: 在内存溢出时生成堆转储文件
以上只是一些常见的JVM参数,实际上还有很多其他的参数可以使用,可以根据需要进行设置。
我们项目用的是maven项目,使用maven的 package命令可以对项目进行打包。这安装包可以在windows平台运行,也可以在linux运行,这就叫跨屏条运行。关键点是在这些服务安装了jdk,然后自带jvm虚拟机,这个虚拟机就是来实现java的跨平台运行。
线程私有 3 程序计数器
虚拟机栈:描述的是方法执行时的内存模型,是线程私有的,生命周期与线程相同,每个方法被执行的同时会创建栈桢(下文会看到),主要保存执行方法时的局部变量表、操作数栈、动态连接和方法返回地址等信息,方法执行时入栈,方法执行完出栈,出栈就相当于清空了数据,入栈出栈的时机很明确,所以这块区域不需要进行 GC。
本地方法栈:与虚拟机栈功能非常类似,主要区别在于虚拟机栈为虚拟机执行 Java 方法时服务,而本地方法栈为虚拟机执行本地方法时服务的。这块区域也不需要进行 GC。
程序计数器:线程独有的, 可以把它看作是当前线程执行的字节码的行号指示器,比如如下字节码内容,在每个字节码`前面都有一个数字(行号),我们可以认为它就是程序计数器存储的内容
线程共有的 2 堆
垃圾回收算法
标记清除算法
步骤很简单
先根据可达性算法标记出相应的可回收对象。对可回收的对象进行回收。操作起来确实很简单,也不用做移动数据的操作,那有啥问题呢?仔细看上图,没错,内存碎片!
复制算法
把堆等分成两块区域, A 和 B,区域 A 负责分配对象,区域 B 不分配, 对区域 A 使用以上所说的标记法把存活的对象标记出来,然后把区域 A 中存活的对象都复制到区域 B最后把 A 区对象全部清理掉释放出空间,这样就解决了内存碎片的问题了。不过复制算法的缺点很明显,比如给堆分配了 500M 内存,结果只有 250M 可用,空间平白无故减少了一半!这肯定是不能接受的!另外每次回收也要把存活对象移动到另一半,效率低下
标记整理法
前面两步和标记清除法一样,不同的是它在标记清除法的基础上添加了一个整理的过程 ,即将所有的存活对象都往一端移动,紧邻排列,再清理掉另一端的所有区域,这样的话就解决了内存碎片的问题。但是缺点也很明显:每进一次垃圾清除都要频繁地移动存活的对象,效率十分低下。
重写与重载
重载在同一个类中
比如说你声明了几个属性,然后再来这个类去给他提供构造函数,他的这个参数就是会有多种体现
重写体现在父子类之间,子类重写父类的方法