现在各公司的数据库基本上都是mysql,只有银行和国企见过oracle,对他的锁机制比较感兴趣,学习了几天写个博客记录一下

        首先,现在基本上都是innodb引擎,事务模式只见过RR模式和RC模式,所以本次测试只针对innodb引擎下RR模式和RC模式下各种情况的差别;

       首先理论知识1: 事务的并发问题分三种,脏读,不可重复读和幻读,前两种很容易理解,第三种幻读网上说法很多,这里说下自己的理解:

  首先mysql的RR模式是部分解决了幻读的(为什么部分解决,最后可以分析一下),解决幻读的方式是MVCC(版本控制)和使用gap锁方式,而gap锁更多的是和插入时的插入意向锁冲突的,

      结合这个个人理解幻读更多说的是插入事务造成的前后不一致的问题:

  在RC模式下事务A执行sql:update XXX set a = 1 where b = 1; 事务B插入插入b=1的语句,结果A事务执行完发现有一行a不等于1,   当然网上关于幻读有很多种说法,不过侧重点基本都是在insert上,

 一个事务A在执行sql的时候,被另一个事务B的insert语句打断了事务A的执行逻辑;

  其次是理论知识2:mysql的隔离级别: RU(读未提交,基本不用),RC(不可重复读),RR(可重复读),串行化(基本不用),他们对应的关系如下:

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

  理论知识3: mysql的锁机制,mysql的隔离级别是主要通过锁实现的,mysql的锁分几种:

解释:X表示排他锁,可以认为是读锁,S是共享锁,IX是意向排他锁,意向锁是mysql自动加的,是表锁,目的是为了较快得检测冲突,比如:事务A在某行上加了X锁,那么mysql就会自动在表上加IX锁,

事务B如果需要在表上加排他锁或者共享锁就不会逐行检测是否有冲突,只要检测表上是不是有意向锁即可,提高了检测效率

gap锁:因为innodb引擎的锁是根据索引进行实现的,那么如果在索引1和索引100之间我想锁怎么办?   一种办法是锁整个表,但是这样显然效率不高,mysql的方式是使用索引1和索引100
实现锁住1和100之间的区域(1,100),这就是gap锁,也叫区间锁;
insert Intention: 插入意向锁,唯一冲突的就是gap锁,主要用于实现在RR模式下的幻读解决,解决插入和更新时候的冲突问题(个人理解,也是为了效率,因为如果没有插入意向锁也是可以
用插入之后的行排他锁进行判断冲突的,但是因为行锁锁的是具体的索引,
所以在索引没有生成之前是不存在行锁的,如果是行锁和gap锁冲突,那么在数据插入之前没有行锁,必须数据插入
之后才知道是否冲突,这显然不合理,所以加入一个插入意向锁来进行冲突判断,猜测插入意向锁是行锁的一个简易实现)
Record: 行锁
Next-key: record锁+Gap锁

理论知识准备完了,接下来就是直接测试了:
首先准备数据:users表,里面有普通索引列,唯一索引列和没有索引的列:
测试情况1:a事务update没有索引的name(只有一行),然后等30秒,b事务update其他name
ab事务都是RR模式下测试结果:
事务B必须要等事务A方法结束(事务结束)之后才会执行完毕,
而且如果事务B抛错回滚了,也是回滚到事务B的初始状态,即使事务B回滚事务A的修改也会被覆盖掉
表明在RR模式下,如果修改没有索引的表数据,即使只修改一条,也会锁住整个表
ab事务都是RC模式下测试结果:
如果修改的是同一行,那么会竞争锁,如果修改的不是同一行,那么事务B很快执行完毕,不存在锁竞争
并且得出在Rc模式下如果两个事务只锁一行数据,那么就只是行锁
思考: 既然RR模式下表锁,Rc模式下行锁,那么事务A是rc模式,事务brr模式,会发生什么呢? 行锁跟表锁应该是互斥的,得出结论应该是两个事务互斥
结果:符合预期


测试情况2: a事务update没有索引的name(不止一行),然后等3秒,b事务update其他name
因为RR模式下的事务在查询没有索引行的时候锁整个表,所以情况2主要测试Rc模式下会不会锁表
测试得出,即使10000条数据里事务A查询9999条,剩下的一条事务B也可以update。
所有网上资料都显示: RC模式下只能走全表扫描,所以会在所有记录上加锁,然后由MySQL Server层进行过滤,将不符合条件的记录释放锁
天下文章一大抄。。 能找到的所有文章都这么说,而且大多数文章都一模一样
但是这样的结果明显不符合测试:事务A锁住某行数据的时候,事务B如果试图在所有记录上加X锁,一定会被阻塞的
所以猜测应该是这样:全表扫描,然后有Server层过滤,符合条件的进行加锁操作,暂未找到支持的文章,不是专业dba也没法去db层验证。。


测试情况3:主要测试情况,测试gap锁和插入意向锁,这类测试的情况很多,不可能覆盖所有情况,有兴趣的可以稍微修改代码自测
 事务A和事务B都对普通索引age进行操作,且都只操作一行
RR模式下,事务A更新age=10000的记录,事务B更新age=100001的记录(数据库里有一条10000的记录,一条10003的记录) 事务B没有阻塞
但是如果事务A更新age=10000的记录,事务B插入10001的记录,事务会阻塞
事务A更新10001的记录(不存在), 事务B也更新10001(不存在),不阻塞
事务A更新10001的记录(不存在), 事务B插入10001数据(不存在),阻塞 **
事务A更新10002的记录(不存在), 事务B插入10001数据(不存在),阻塞 **
以上实例,联系网上和书上的理论知识,得出一个结论:事务对普通索引update的时候,会在当前行上加上行排他锁,在该行的左右(
左是左开右闭,右是左闭右开,比如9999,10000, 10003,10000的左右gap锁就是(9999, 10000]和[10000, 10003)),
而gap锁是可兼容的,所以两个update可以都获取到相同的gap锁,不阻塞,
而由于gap锁和insert要插入的行锁是排斥的(insert的插锁操作是先插入当前行的插入意向锁(),再插入要插入行的排他锁)
由此可模拟一个死锁: 10000到10003之间没有其他数据,两个事务分别获取gap锁之后插入10001和10002,其中一个事务会回滚,发生Deadlock found
when trying to get lock; try restarting transaction错误
另:如果两个事务分别向表中插入10001和10002,不会发生冲突,也不会死锁,https://juejin.im/post/5b865859e51d4538e331ae9a
因为insert的时候用的是插入意向锁,和普通意向锁不同,插入意向锁和具体的行排他锁不互斥

试验证明:
插入意向锁不和其他锁冲突,先插入之后,可以在相同的区间内插入其他数据(只要行锁不冲突)
通过查询资料和个人理解:插入意向锁仅和gap锁半冲突,当有gap锁的时候,不允许申请插入意向锁,而如果有插入意向锁,可以有gap锁(
所以相同区间下,insert之后可以update,但是update之后不允许insert)

即:事务A插入10002的记录(不存在), 事务B更新10001数据(不存在),不阻塞
所以以上对**的解释不对,插入阻塞的原因不是因为要插入的行锁和gap锁冲突,而是因为要申请的插入意向锁和gap锁冲突
至于为什么,个人理解和从网上博客分析,因为行锁锁的是具体的索引,所以在索引没有生成之前是不存在行锁的,
而数据没插入的话,是没有行锁的,如果是行锁和gap锁冲突,那么在数据插入之前没有行锁,必须数据插入之后才知道是否冲突,这显然不合理
,所以加入一个插入意向锁来进行冲突判断

其实插入意向锁了解就可以了,没有他的情况下是可以用行排它锁进行分析的(因为插入意向锁不会阻止任何其他锁,只是为了判断要插入的行锁是否和其他锁冲突)
RC模式很简单了就,因为不存在gap锁,插哪行锁哪行

测试情况3主要测试了gap锁,插入意向锁和行记录锁的情况,gap锁和其他所有锁都不冲突,插入意向锁只和gap锁冲突,这种设计想来很巧妙,如果gap锁和行记录锁冲突,
那么事务A update和事务B的update即使不是同一行也很可能会冲突,如果gap锁和其他gap锁冲突,
那么两个事务的update也很容易冲突,当前这种冲突模式,
可以很好的兼容和修改不同行的多线程情况,又可以避免两个事务之间的幻读问题(比如事务A update users where age > 2 and age < 5,如果2和5之间没有数据,
那么事务B也可以进行相同的update,因为2和5之间只有gap锁没有
行锁,兼容的,但是如果事务B想要在2和5之间插入数据就会冲突,因为事务A已经申请了2和5之间的gap锁了,
这也符合我们的预期),但是之前我写到只是解决了部分幻读,这个我们结尾的时候讨论;



测试情况4:事务A和事务B都对普通索引age进行操作,且都操作不止一行
RR模式下,A更新5000行,B更新最后一行,且两个更新中间相隔N行数据,不存在gap锁的问题
按照情况3的逻辑,两个事务应该互不影响
结果: 两个事务阻塞,说明事务B和事务A锁冲突了,结合网上资料,应该是发生了锁升级,即行锁升级成了表锁
掘金上有个理论,说值重复率会影响锁升级https://juejin.im/post/58f04e6b61ff4b0058e33d77
查了下,age这个索引的重复率大概10%,也就是相同的数据会有10行,也许有道理,那么把age这行所有的数据都改成唯一的,执行update users set age = id;
再执行情况4,发现还是会阻塞。。
依次修改RRModeTest4_A中的值,发现:
查询<= 11758的数据不会阻塞,查询<=11759的数据会阻塞,这时候总的数据量是10002条,age小于等于11758的数据量为1744条
这时候有两个猜测,一个是是否表升级跟锁的绝对行数有关,一个是跟锁的行数在总行数的比例有关,进行如下的例子:
创建两个临时表,users_temp1和users_temp2,表结构和users相同,其中temp1里面只存100条数据,事务A锁定前60条数据,事务Bupdate最后一条数据
结果: 事务A在锁定18条数据的时候阻塞,锁定17条数据的时候不会阻塞,所以猜测应该和锁定的行数在总行数里的比例有关
temp2放入65000条数据,锁定5000条,没有阻塞,所以应该和锁定的具体行数无关
当前能想到的比例: 大于0.174365127 小于0.175464907 应该是0.175,但是没有找到相关的理论依据
那为什么https://juejin.im/post/58f04e6b61ff4b0058e33d77 这个例子会阻塞? 参考测试情况3,应该是gap锁的问题,或者是比例达到17.5了
RC模式下,不会锁升级(类似情况3)
经过测试发现,当锁升级的情况下,如果我查询某一个不存在的数据(通过别的索引查)不会阻塞,所以锁升级应该不是升级为表锁,而是升级为(-无穷,+无穷)的记录锁和gap锁

 

测试情况5,测试唯一索引和主键索引的锁情况和普通索引是否一致,这里不测试速度,针对索引速度的测试放到后边做
情况1:只锁一行,看唯一索引和主键索引在RR模式下是否存在gap锁
测试数据1: card_id存在9966和9969,事务A update一条9966 和9967,事务b insert一条9968
由测试情况3可知,如果是普通索引应该是会阻塞的,但是唯一索引不会阻塞,说明唯一索引如果只查一行是没有gap锁的
测试数据2:card_id存在9966和9969,事务A update大于等于9965小于等于9966,事务b插入9968,阻塞
主键索引同唯一索引,而且在主键gap锁定的时候,插入的唯一索引冲突是在获取到锁之后才会报错,而主键冲突会第一时间报错,即使他没有获取到插入的锁在阻塞状态
分析: gap锁存在的目的主要是为了消除幻读情况,猜测innodb锁的级别有行锁和表锁,在不是唯一索引的情况下没办法锁定所有的索引项(比如by name = 3,查出来的
可能是1项也可能是10项,行锁没办法满足要求所以采用gap锁,把name=3的上项和下项都锁起来使不能插入,而唯一索引是可以进行行锁的,所以不需要gap锁)
情况2: 测试唯一索引和主键索引在RR模式下,会不会锁升级
发现有升级

 

以下测试,主要测试索引对速度的影响,所以用users_big表,这个表里有千万条数据,用来测速度很理想

1000w条数据,在更新1条数据的情况下,使用主键索引,唯一索引和普通索引以及不用索引的效率差
第一次执行:耗时统计:主键修改耗时:234,唯一索引耗时:29,普通索引耗时:8,不用索引耗时:20741
感觉不太对,因为主键耗时比预想高太多,猜测是第一次执行的时候有个数据库连接(所以现在都用数据库连接池了)
在计时之前先进行Update,然后再次执行:
耗时统计:主键修改耗时:16,唯一索引耗时:23,普通索引耗时:13(不用索引不测了,耗时完全不是一个量级)

更新10000条不同的数据:
耗时统计:主键修改耗时:156852,唯一索引耗时:271752,普通索引耗时:104730
更新10000条相同的数据:
耗时统计:主键修改耗时:110033,唯一索引耗时:227537,普通索引耗时:111300
可以看到,不管是更新相同的还是不同的数据,普通索引的耗时是最短的, 主键索引次之,唯一索引的耗时最长
这是因为普通索引和唯一索引都是二级索引,他们要先定位到一级索引的位置,相当于进行了两次定位
但是因为普通索引有更新缓存操作,所以在更新的时候较快

 

以上是我能想到的针对mysql的两种隔离模式和锁的测试,下面聊一下个人的想法:

1.目前mysql的默认模式是RR,但是个人觉得这种模式是不如RC模式的,而且大多数业务上RC已经完全符合要求了,所以我在写代码的时候喜欢手动将隔离级别设置成RC模式

2.为什么上面说RR模式实现了部分情况下的解决幻读? 下面分析一下RR模式和Ser模式下的不同:

网上没找到资料,但是试验发现Ser模式下应该也是有gap锁的(比如ser模式下,事务A update2011记录(没有2012记录,但是有2013记录,事务B插入2012失败,但是我Update2010记录事务B就可以插入2012))

但是Ser模式下,select是会申请全表共享锁的,所以只要事务A进行了查询操作,事务B就不可以进行更新和插入操作了,但是RR模式下普通读是快照读,是不上锁的

所以在RR模式下进行如下操作: 事务A读2011到2013之间的数据,发现是没有的,这时候事务B进行插入2012记录,因为没有上锁,所以事务B插入是成功的,并且在2012上申请了插入意向锁和行排他锁,

这时候事务A进行update操作在2012上,因为2012没有对应记录,所以按照事务A的想法,他只要申请gap锁即可,跟别人也不会冲突,但是他失败了,因为事务B插入了2012,

所以事务A必须申请2012的排他锁,和事务B冲突,但是事务A查询的时候是没有2012的(在同一个事务中,自己的查询不能支撑自己的业务逻辑,因为其他事务的插入操作导致自己认为自己出现了幻觉)

 

3.关于gap锁:

innodb的行记录锁是根据索引实现的,gap锁也是,所以innodb如果没有gap锁,他只能锁已经存在的索引行,

如下测试情况: RC模式下是没有gap锁的,所以RC模式下,事务A先对age = 20013的行记录执行update,事务B再插入age = 20013的行记录,事务B是不会阻塞的,因为事务A只能锁住已经存在的行索引,

所以事务A进行update之后发现有一行没有update成功(这也是幻读,由于其他事务的插入操作导致当前事务的操作逻辑出现幻觉,RR模式可以依靠gap锁解决这种情况下的幻读,所以叫部分解决幻读);

所以,在RR模式下,如果只update一行普通索引,也会有gap锁,但是update一行唯一索引就不会有gap锁,这也是因为行记录锁只能锁具体存在的行,所以如果是唯一索引,只能通过gap锁进行

当前索引的所有可能记录进行加锁,但是唯一索引就不用gap锁,因为唯一索引当前记录的所有可能已经被唯一索引的特性保证;

 

4. 以上所有的测试和结论都是个人理解,不是专业dba,如果有人有不同看法欢迎讨论,代码地址:  上传中,   Mysql个人搭的,里面表和数据还在,ip和端口以及用户名密码都没改,有兴趣的同学可以直接执行测试,

也可以修改部分语句之后进行其他情况的测试,也可以修改配置文件去链接自己的表

 

 

          

posted on 2020-08-01 15:34  野原小心  阅读(904)  评论(1编辑  收藏  举报