Mysql事务和锁
前言
我理解的事务最直白就是为了一个动作如果涉及到两个地方的操作的话,这两个操作要么全部成功要么全部失败,如果一个成功了,另外一个没成功则会出现数据不一致
事务的前世今生,包括以后的发展,比如分布式事务了解了一下,但是不在这里展开讲了
典型的场景
转账:A给B转账,先扣A的余额,然后增加B的余额,再生成一条转账记录,如果中间有任何一个步骤出现错误,都会导致数据的不一致性,那么中间的操作可能出现什么样的错误呢?
- 如果扣减完成A的余额后,数据库宕机了,就会发生数据不一致性的情况
- 转账过程中涉及到远程调用其它微服务,服务调用失败也会出现数据的不一致
工作中的事务
创建用户:初始化用户=>初始化角色=>初始化部门=>发送邮件通知,任何一个环节都可能出现异常,要通过事务管理来保障数据的一致性
事务特性
事务必须满足ACID四个特性,这四个特性通过redo log、undo log、binlog、多版本并发控制(MVCC)、WAL技术和锁等技术进行保障
原子性
原子性(Atomicity):对数据的修改要么全部成功要么全部失败,通过redo log、undo log实现
一致性
一致性(Consistency):事务操作前和操作后,数据最终是一致的
持久性
持久性(Durability):事务结束后,对修改是持久的,不会受数据库宕机和其他操作的影响,通过redo log、undo log实现
隔离性
隔离性(Isolation):不同的事务之间是隔离的,因为会用并发的操作
并发事务控制演进过程
读读不互斥:引入读写锁
读写不互斥:引入了MVCC来保障,就是一行的数据可以有多个版本来保障读写不互斥
写写不互斥:乐观锁和悲观锁(行锁、间隙锁)
事务隔离级别经典问题分析
脏读是什么 ,会带来什么问题,如何解决?
- 是什么
当前事务读到别的事务修改但事务还没有提交的数据
- 问题
因为别的事务修改完成后可能会回滚,可以说没有读到完整的数据,会造成数据的不一致性
- 解决方法
- 设置数据库的隔离级别为读已提交及以上隔离级别,通过
MVCC+Read View
解决,主要思想就是MVCC通过undo log
实现了数据的多个版本,Read View
规定了当前事务可以读到哪些数据
- 设置数据库的隔离级别为读已提交及以上隔离级别,通过
不可重复读是什么,会带来什么问题,如何解决?
是什么
- 一个事务中两次读取某一行数据结果不一致
问题
首先思考一个问题,可重复读解决的是两次读取的记录是一致的,那么实际业务场景中哪里会存在两次读取的需求呢,也就是说可重复读到底解决的是什么样的问题?
我刚开始觉得是如果存在双重校验的情况,也就是存在两次读的情况,具体如下:
事务A 事务B begin; select balance from t where name = ‘X’ if 余额>=转账金额 console.log begin; update t set balance=balance-100 where name=’X’ select balance from t where name = ‘X’ if 100>=100 console.log select balance from t where name = ‘X’(更新前双重校验) if 100>=100 update t set balance=balance-100 where name=’X’ select balance from t where name = ‘X’(更新前双重校验) commit; if 100>=100 balance=100(结果) update t set balance=balance-100 where name=’X’ commit; balance=-100(结果) 结果发现可重复读解决不了这个问题,所以查找了相关的资料,发现可重复读隔离级别其实解决的是快照读的幻读问题(下文展开讲),还有在进行数据库的主备同步,为了保障在数据库同步的过程中,不至于系统停机更新,同步的时候开启事务并设置可重复读隔离级别然后进行同步
解决方法
设置数据库的隔离级别为可重复读及以上,同样是通过
MVCC+Read View
来进行解决,但是跟读已提交的区别是可重复读会在事务开始的时候创建Read View
,读已提交是每次快照读之前都会创建一个Read View
。如果是当前读则需要结合锁来解决幻读的(下文展开讲)事务开始:执行begin后;不会立即开启事务,会等待执行第一条语句的时候才会创建
read view
,可通过查询select * from information_schema.innodb_trx
进行查询
可重复读是如何工作的?
理论
- 事务开始时,会创建整个库的快照
- 一个事务只需要在启动的时候声明说:以启动的时刻为准
- 如果一个数据是在我启动之前生成的,就认;
- 如果是在启动之后生成的,就不认;就会找它的上一个版本,如果上一版本页不可见,那就继续往前找,当然,当前修改的还是要可见的
实现
事务启动的时候会创建一个数组,数组为【当前数据库中所有“活跃”的事务ID】,当前数据库中活跃的事务ID的最小值作为低水位,事务Id最大值+1作为高水位
视图:数组+高水位
这个视图数组把所有的row_trx_id分成了几种不同的情况
1 |
|
可重复读隔离级别和读已提交隔离级别区别
都是基于MVCC+Read View实现的
可重读:开始事务,建立Read View
读已提交:每个语句执行前都会建立Read View
幻读是什么,会带来什么问题,如何解决?
- 是什么
- 一个事务在前后两次查询时,后一次查询看到了前一次查询没有看到的行
- 问题
在这里引出《Mysql45讲中:20.幻读是什么,幻读有什么问题?》这篇文章针对幻读存在的问题基于binlog分析举了一个很好的例子,主要问题会出现在主从同步的过程中,binlog导致了从库数据不一致的情况,这是很严重的情况
解决
当前读:通过间隙锁、记录锁(分为(S)读锁和(X)写锁)、net-key lock结合起来进行解决
快照读:通过
MVCC+Read View
来解决幻读
脏读和幻读的区别?
脏读:针对的是读到了其他事务修改但是未提交的数据
幻读:针对的是如果一个事务在不同的时刻查询时,后一时刻查询结果出现了前一时刻没有看到的行
可重复读隔离级别,是如何解决幻读的?完全解决幻读了吗?
首先我们想一下为什么会出现间隙锁
1 |
|
在行锁的基础上,事务A执行select * from t where c=5 for update
只会锁住id=5
这行数据,此时如果事务B执行update t set c=5 where id=0
,然后提交(0,5,0)之后事务A执行update t set d=100 where c=5
(0,5,100),(5,5,100),这个不是幻读,但不是原本的语义
如果在事务A执行完当前读查询后,对所有扫描的记录进行加锁,但是如果事务C新插入了一条语句,事务A的更新也还是会影响新插入的记录,因为当时加锁的时候,新插入的记录是不在的,所以没法加锁,基于以上背景出现了间隙锁(gap lock)
间隙锁的规则,可以总结为“两个原则,两个优化”
- 原则一:加锁的基本单位是next-key lock。next-key lock是前开后闭区间
- 原则二:查找过程中访问到的对象都加锁
- 优化一:索引上的等值查询,给唯一索引加锁的时候,next-key lock会退化为record lock
- 优化二:索引上的等值查询,向右遍历且最后一个不满足等值条件的时候,next-key lock退化为gap lock
注意
- 锁是加载索引上的:lock in share mode 只锁覆盖索引加锁在索引上加锁、for update会锁对应记录的主键索引
- 虽然间隙锁的分析可以基于以上规则来分析,但是加锁的过程是先加间隙锁,再加记录锁,加记录锁的时候会被其他事务的next-key lock锁住
- 对于非唯一索引的范围查找,会在匹配到记录的下一条记录并加next-key lock
- 对于唯一索引的范围查找,会在匹配到记录的下一条记录并加 record lock
- limit加锁会进行锁的优化
select * from t where c=10 limit 2 for update
,会找到limit范围内的值然后停止,因此跟不加limit会加不同的锁 - 加索引是在索引页上加的,索引页是有顺序的,可参考
MySQL 中关于gap lock / next-key lock 的一个问题
锁的问题
乐观锁和悲观锁
悲观锁:上文提到所有的锁都是悲观锁
乐观锁:通过在行记录上添加version或时间戳来实现
表锁
表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作
参考资料:
《Mysql实战45讲》
《小林coding数据库篇》
《MySQL技术内幕:InnoDB存储引擎》