MVCC
MVCC,即Multi-Version Concurrency Control, 多版本并发控制,是伴随着事务的需求而产生的设计思想,主要用来支持事务的并发操作、事务回滚等特性。
innoDB的MVCC实现是基于undoLog,通过表记录的隐藏字段实现多版本控制。
1)表隐藏字段:
3部分,最后事务id(DB_TRX_ID)6字节,回滚段(DB_ROLL_PTR)7字节,递增记录id(DB_ROW_ID)6字节,再加上一个记录删除delete标记。
2)undoLog
insert类型的操作undoLog会记录undo_no,table id,数据唯一键的信息,事务id。
update类型的操作undoLog会记录undo_no,table id,数据唯一键的信息,字段修改前的值,旧记录事务id,事务id。
mysql的锁
mysql的锁分为行级锁和表级锁。
- 行级锁:innoDB引擎实现。分为共享锁(读锁,S锁)和排它锁(写锁,X锁,独占锁)。
共享锁的特点是多个事务可以同时加读锁,但是不能加写锁。
显示加读锁:
select ... lock in share mode;隐式加读锁:下面这种情况比较隐蔽,table_b会被加读锁,确切的说是间隙锁,所谓间隙锁就是锁住id为0到当前查询的最后一条记录的id的范围,不管这个范围中实际上有没有记录。由于select * 没有具体条件,这里innoDB会认为范围无限大,就变成table_b整个表都锁住了。
insert into table_a select * from table_b;
排他锁的特点是其他事务不能读也不能写。
update/delete/select ... for update;
关于间隙锁和单记录锁
单记录锁就是锁定一行数据的锁。
innoDB对间隙锁的具体实现是使用了next-key锁
next-key锁是单记录锁和间隙锁的组合用法,锁定的是一个范围。具体会锁住查询过程中所有遍历过的数据和数据之间的间隙。
next-key的查询规则具体会一直遍历找到需要查询的数据,如果查询条件是未加索引的字段,则会全表扫描,导致全表被被锁。
select * from table_a where pay = 3 for update; // pay没加索引导致全表被加了锁
特例:
1)使用唯一特性的字段查询不会使用间隙锁。插入的时候包含唯一字段也会转为单记录锁。
2)如果事务隔离级别降级为读已提交(read commited,RC)则间隙锁失效。插入意向锁
insert操作的时候加的锁。插入意向锁也是一种间隙锁,具体是指在插入之前先设置间隙锁。
一个典型的例子就是当前一个事务使用非主键作为条件update操作的时候,后一个事务insert会被阻塞,影响到了并发效率。如果两个事务都先不通过索引update然后insert就会出现死锁。总之,innoDB加锁默认都是使用next-key锁,锁定的是一个范围,如果sql写法不注意,就会锁住意料之外的行,影响到并发事务效率,甚至死锁。所以我们更新和查询都要尽量使用主键和索引。
Predicate Lock
因为next-key锁对多维空间列的支持不好,innoDB又提供了专门的Predicate Lock。大致思想就是通过锁查询条件而不是锁记录来达到目的。
- 表级锁:表级锁是由mysql服务实现的。
读锁(LOCK_S锁):
lock table tablename read;
写锁(LOCK_X锁):
lock table tablename write;
通常上述方式一般不会在生产环境使用。因为加上去了除了使用写琐(手动释放),加了LOCK_X锁,表数据读写操作就全部禁止了。加表锁的时候也并不知道有没有行级锁的事务操作。
为了解决这种场景,让表级锁和行级锁共存,mysql还特意提供了意向共享锁(IS)和意向互斥锁(IX),类似于注册,这样加表锁的时候就不用全表扫描是否当前表有事务加行级锁了。
意向共享锁(IS)
事务想获得行级共享锁,需要先在表上加意向共享锁。
意向互斥锁(IX)
事务想获得行级互斥锁,需要先在表上加意向互斥锁。
表级加锁总的来看还是比较繁琐,表最好还是设计的时候索引字段什么的考虑的全面点。
表的自增锁
这是一种特殊的表级锁,自动模式(innoDB默认选项)中,自增序列在执行sql的时候就计算好了行数(如果是未知行数,则自动转为传统模式--加表级锁,insert结束后释放),然后释放掉,自增过程并不受事务控制,2个并发的事务中自增序列也会依次排列,不会出现冲突,但是如果前一个事务rollback,则那个事务加的数据对应自增序列就会丢掉,导致整个表的自增序列出现不连续。
innoDB对事务模型的支持
读未提交:直接读行记录,不管隐藏字段有什么信息。
读已提交:读行记录,如果发现最新记录的事务id未提交,则根据回滚段找到undo日志,去读取上一个版本的数据。
可重复读(mysql默认事务隔离级别):只关心自己事务id对应的数据,如果发现当前记录的事务版本不对,则通过回滚段找到undoLog去读取跟自己事务版本一致的数据。
select读取的时候会先生成一个read-view快照,这个快照只存放当前事务id版本和以前版本的记录,事务过程中就算有别的事务update提交了,也不会影响到快照。通过这种方式实现了可重复读。
快照只适用于select读的事务,如果是update操作则会实时读最新记录,从而数据结果集不一致,解决办法是先加间隙锁select...for update,这样事务结束之前其他事务想操作是成功不了的。通常幻读并不会影响数据update数据,所以我们通常可以忍受,这时候就要考虑一定要利用id或者索引,减少不必要的阻塞。
串行化:串行化的原理其实还是利用了间隙锁,默认加了读锁,自然写锁就申请不到只能等着。所谓串行化并不是严格事务部分情况必须一个一个串行执行。
几个案例
- 多事务并发insert导致的死锁现象
1)3事务同时insert记录的时候,假设都包含一个唯一字段并且同名。
2)因为唯一键,只会使用单记录锁,并且先加读锁去查询有没有,然后才能加写锁插入。
3)第一个事务执行中途callback了,此时事务2和事务3都加了读锁(读锁不互斥),但是没办法加写锁,因为需互相等对方释放读锁,但是读锁的释放需要写操作完成即写锁释放。死锁产生。
如果第一个事务成功提交,则不会出现死锁,因为后面的事务读操作就会发现唯一键冲突。
mysql针对这种情况会主动让一个事务报错,退出事务,这时另一个事务就能操作成功。
网友评论