事务与锁
上一篇文章MySQL查询篇,熟悉了连接查询、查询排序等原理,这篇文章来总结事务与锁相关的知识,主要有事务ACID原则、事务实现的redo log、undo log、MVCC、隔离性问题、锁等
事务特性ACID
- 原子性:一组事务要么都成功要么都失败
- 隔离性:多用户并发读写操作相同数据时,可能会出现隔离性问题;基本的隔离性需要使用户感知不到其他用户的操作
- 一致性:数据库中数据满足约束,通常这种约束在业务层面约束
- 持久性:事务提交成功就永久保存在磁盘上
事务的实现
持久化的实现
redo log
如果在内存中修改完记录,就立马将记录写回磁盘持久化,这样可能会产生随机写IO
为了防止随机写IO开销大的情况,当发生修改等操作时会使用redo log来记录对某页上记录的物理操作,并将redo log持久化到磁盘,等到合适的情况下再将真正的脏页刷入磁盘,由于redo log持久化到磁盘使用顺序写入,因此开销小
在合适的时机刷入磁盘后,redo log记录那些刷入磁盘的操作就没有用了,因此redo log的物理文件被设计为可覆盖的环形文件;redo log由redo log block 管理,redo log buffer由redo log block组成
事务中的每一条语句对应一组MTR(Mini-Transaction),MTR是对底层的原子访问,不同事务的MTR可以被交替的写入buffer中,默认策略下当事务提交时会将redo log刷入磁盘,因为MTR交替写入buffer中,所以可能存在当前事务还未提交但是redo log已经被其他事务刷入磁盘;恢复数据时由bin log引导,就算该事务最后未提交也不会导致这些redo log用来恢复数据时引起数据不一致
使用升序的lsn日志序列号来记录日志量,lsn为redo log总日志量,flush to disk lsn为刷入磁盘的redo log日志量,check point lsn表示check point指到的日志量;checkpoint lsn表示可以覆盖的redo log位置,在check point lsn前的redo log都可以覆盖,由于redo log是环形文件,如果脏页不刷进磁盘,那么redo log文件可能会满从而导致影响用户线程,check point操作是将脏页和redo log刷到磁盘并更新check point lsn
double write
如果在此期间数据库宕机,那么下次启动时可以根据已经持久化的redo log对数据进行恢复,需要注意的是redo log是在已有的页上进行物理操作进行恢复,如果页没保存下来就不能使用redo log恢复,因此innodb使用double write两次写来保证页的完整,double write 也是使用顺序IO代替随机IO
doublewrite是将脏页复制到doublewrite buffer中,先将脏页顺序写入磁盘(顺序IO),最后才将脏页写入对应表空间文件(随机IO)
而redo log刷入磁盘的单位为block以此来保证完整,因此redo log刷盘不需要double write
bin log、redo log两阶段提交
bin log是server层逻辑恢复数据的日志,redo log是innodb层面物理操作页恢复数据的日志
当事务提交时,如果bin log和redo log不一起持久化,那可能会出现数据一致性问题;如果写完redolog断电,那么binlog没记录,下次恢复时,会导致少更新一次(虽然redolog记录了,但是要按照binlog记录的来恢复);如果写完binlog断电,redolog没记录,下次恢复时,会导致无法恢复(按照binlog记录的恢复,但是redoLog没记录到如何恢复)
- 两阶段提交过程(默认策略情况下)
- redo log prepare write:redo log 从 redo log buffer 写入page cache
- bin log write : bin log 从 bin log buffer 写入 page cache
- redo log prepare fsync : redo log 从 page cache 刷入磁盘
- bin log fsync :bin log 从 page cache 刷入磁盘
- redo log commit write
原子性的实现
undo log
在一次事务中,可能进行写操作,如果事务失败就需要回滚,将修改的值变为事务启动前的值
undo log使用回滚段管理,存储在共享表空间,实际类型存储在回滚页上,使用链表来管理不同类型(update、delete、insert等)的undo log页
undo log则是用来记录一个反向操作,记录上的回滚指针指向上次修改的undo log,undo log的回滚指针又指向上次修改的undo log,事务回滚时可以通过undo log回滚达到数据一致性
MVCC
MVCC 多版本并发控制,利用版本链、读快照等控制读数据能够读到版本链上的具体哪条数据,使用MVCC无锁并发控制解决隔离性问题(部分幻读不能解决)
事务id会全局维护自增,如果是查询语句则为0
行记录有隐藏列回滚指针指向上一次修改的undo log,undo log又指向上一次修改的undo log,从而形成版本链;使用读视图 read view 来判断此次读能够读取版本链上的哪条数据
read view会维护活跃事务id列表、当前最小事务id、当前最大事务id等信息,通过读视图上的事务id与版本链上不同版本的事务id对比来判断是否能读,如果不能读则遍历版本链上的下一个版本判断
- 判断规则
- 如果版本上的事务id小于最小事务id则说明事务已提交可以访问
- 如果版本上的事务id大于最大事务id则说明版本是在读视图生成后执行的,不能访问
- 如果版本上的事务id在最大、最小事务id之间,则判断事务id是否存在活跃列表中,存在则可以访问否则不能
- 如果版本上的事务id 等于 当前读视图的事务id 说明是当前事务修改的,可以访问 (如果事务A修改了事务B新增的记录这种情况会出现幻读)
purge
删除记录实际不是立马删除,为了满足MVCC,先标记删除,等到MVCC版本链过期使用不到了再由后台线程purge最终清理
为了避免清理时发生随机读,purge时从链表尾部获取最旧的undo log,再去对应的页清除,顺便将页中其他事务undo log清理
锁的简介
innodb中的行锁可以类比JUC下的读写锁,X锁排他锁就是写锁,S锁共享锁就是读锁,除了读读的情况其他情况都会阻塞(读写、写写)
innodb中的表锁有意向锁,表示这个表中某记录存在X或S锁;IX表示意向排他锁表中存在X锁,IS表示意向共享锁表中存在S锁
默认情况在并发读写时,不加锁的情况下使用MVCC来读取版本链上的记录
如果使用锁定读的情况下,lock in share mode
加S锁、for update
加X锁,修改等操作可以看成加X锁;
具体锁的算法不同的隔离级别不同,在读已提交的情况下使用record记录锁,在可重复读的情况下使用next key锁
record记录锁就是锁住某条记录,next key锁则是record记录锁 + gap间隙锁,间隙锁锁住当前记录与上一条记录之间的间隙,不允许插入,以此来解决幻读问题
隔离性问题
- 可能出现的隔离性问题:脏写、脏读、不可重复读、幻读
- 脏写:事务A在修改了事务B修改未提交的数据(写写并发太严重了,不允许出现)
- 脏读:事务A读到了事务B未提交的修改,事务B后续回滚,事务A就会读到脏数据
- 不可重复读:事务B修改了事务A读过的数据,事务A在同次事务中再读,发现两次读取数据不一致
- 幻读:事务B新增数据,事务A在事务B操作前后读取数据,最后的读取发现数据增加
- 不同隔离级别(按顺序隔离级别限制越严格性能就越不好)
- 读未提交:允许读到其他事务未提交的数据,在此隔离级别下可能发生:脏读、不可重复读、幻读
- 读已提交:只允许读到其他事务已提交的数据,在此隔离级别下可能发生:不可重复读、幻读;读时使用MVCC,写时使用record锁
- 可重复读:innodb默认隔离级别,可能发生特殊幻读;读时使用MVCC,写时使用next key
- 串行化:读写加S/X锁串行化,避免隔离性问题
读已提交隔离级别情况下,读时使用MVCC,每次读产生读快照,以此来读到已提交的记录
可重复读隔离级别情况下,只在事务的第一次读产生快照,因此事务中每次读都是第一次读能够看到记录版本,以此来避免幻读,注意如果事务中修改了其他事务新增的记录会导致该快照对这些数据可见,从而导致幻读发生
加锁规则
使用哪些索引,才对哪些索引加锁;读已提交使用record锁,在遍历完索引后对不需要的记录释放锁;可重复读使用next key锁,在遍历完索引后不对不需要的记录释放锁(防止新插入),在事务提交或回滚才释放
增删改操作当成锁定读,加X锁处理;X锁只使用二级索引时,会把聚簇索引对应记录也加锁,而S锁不会
原则 1:加锁的基本单位是 next-key lock。希望你还记得,next-key lock 是前开后闭区间。
原则 2:查找过程中访问到的对象才会加锁。
优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
死锁
当多个线程抢占同一行记录时可能发生死锁(资源互斥、占有资源、不释放资源、成环),innodb使用wait for graph来判断是否死锁(事务需要等待其他事务时就指向其他事务,当构成的图成环时则发生死锁),检测死锁有性能开销
排查方案:
- show engine innodb status 查看最近一次死锁
- 联表查询information_schema库中的innodb_trx、innodb_locks、innodb_lock_waits表等查看相关SQL、事务、锁、等待时间等信息
- 再去业务层排查代码写法问题,更改顺序等手段阻止死锁发生
总结
innodb 中使用 double write 保证页的完整(redo log基于block为单位刷入磁盘,因此不需要double write),使用redo log 记录页中物理的操作(顺序写取代持久化的随机写)
在发生宕机可以通过bin log与redo log恢复数据(redo log完整恢复,不完整判断是否有bin log),并且bin log与redo log需要两阶段提交来满足数据一致性
因为脏页刷入磁盘后redo log就没有作用了,所以redo log设计为环形可覆盖文件,如果脏页不刷盘可能导致redo log占满,这可能导致用户线程主动触发脏页刷盘从而影响用户线程
innodb 中使用undo log记录回滚操作,行记录中的隐藏列回滚指针指向上一次修改的undo log,而undo log的回滚指针又指向上次修改的undo log,从而形成版本链,事务失败需要回滚时可以通过undo log回滚到某个版本
MVCC通过版本链 + read view 读视图的方式实现无锁的并发读,read view根据生成时的事务id和维护的事务id来判断当前事务是否能够读到版本链上的记录,不能则判断版本链上的下一条
读已提交隔离级别下每次读生成read view因此能够看到已经提交事务的数据,可重复读隔离级别只在第一次读生成read view因此多次读到的数据都是相同的
MVCC 能够避免大部分幻读(特殊情况下,事务B新增数据,事务A修改了新增数据导致read view对数据可见从而产生幻读),而可重复读级别下使用next key算法防止幻读
非锁定读的情况下能够使用mvcc解决隔离性问题,锁定读的情况下使用S/X锁来解决隔离性问题;读已提交级别下使用record lock,可重复读级别下使用next key锁,使用到哪些索引就对哪些索引加锁,因此加锁不使用索引时,会给聚簇索引加锁
当满足一定条件时会发生死锁,死锁检测使用wait for groph依赖图成环就死锁,可以查询死锁相关信息,排查业务层分析代码写法问题,破坏死锁生成条件,阻止死锁发生