MVCC与锁
锁基本原理
当事务想要改动记录时,会查看内存中有没有跟该记录相关联的锁结构
- 没有的话就生成一个is_waiting为false的锁结构与之关联,代表获取锁成功;
- 如果发现该记录已经有锁关联了,会生成一个is_waiting为true的锁结构,代表获取锁失败,进入等待状态;
- 如果加锁的事务结束,将释放锁结构,查看是否有其他事务正在等待,若有则将其锁的is_waiting改为false,并唤醒其事务对应线程唤醒
锁定读的语句:
SELECT ... LOCK IN SHARE MODE
:事务执行该语句,则为读取到的记录加S锁SELECT ... FOR UPDATE
:事务执行该语句,则为读取到的记录加X锁
不同类型的锁
-
共享锁:S锁,读取记录前需要获取s锁
-
独占锁:X锁,改动记录前需要获取X锁
-
意向共享锁:当事务准备在某条记录加上S锁,需要在表级别加一个IS锁
-
意向独占锁:当事务准备在某条记录加上X锁,需要在表级别加一个IX锁
意向锁让表快速判断表中记录是否被加行锁,为表是否能加表锁提供依据,所以IX和IX,IX和IS锁都是兼容的,因为它们并不用作互斥
不同粒度的锁
全局锁:对整个数据库实例加锁
典型应用场景是:做全库逻辑备份,目的是让备份系统备份得到的库和原库是保持逻辑一致性的,代价是如果在主库上备份,期间不能做数据更新,业务停摆;如果在从库上备份,备份期间从库不能进行主从同步,导致延迟
如果引擎(innoDB)支持一致性读,推荐使用single-transaction方法
但如果有的表使用了不支持事务的引擎,就需要对全库加读锁,有以下两种方式:
- FTWRL:
flush table with read lock
set global readonly = true
使用FTWRL更好,一是修改global的方式影响面更大;二是如果执行FTWRL之后客户端异常断开,则mysql会自动释放全局锁,整个库回到可以正常更新的状态,但readonly而不会因为异常而取消readonly状态
表级锁:针对表加锁
分为两种:表锁 和 元数据锁MDL
表锁:lock tables .. read/write;
一般在数据引擎不支持行锁的时候才会用到
- 可以使用
unlock tables
释放锁,或在客户端断开时自动释放 - 会限制其他线程 和 本线程 的读写
MDL:不需要显式使用,在访问一个表的时候会被自动加上
- 作用:保证读写的正确性,比如禁止一个查询正在遍历表中数据,而执行期间另一个线程修改了表结构这种操作
- MySQL5.5引入MDL,对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁
- 读锁不互斥,允许多个线程同时对一张表增删改查
- 但读写锁与写锁间互斥,保证变更表结构操作的安全性
自增锁:用于AUTO_INCREATMENT修饰的列自动递增,作用范围是单个插入语句,执行插入语句时加一个AUTO_INC锁,语句结束后释放
行锁
在innoDB事务中,行锁在需要的时候加上,但等到事务结束时才释放
所以要把最可能造成锁冲突,最可能影响并发度的锁尽量往后放
Record Locks,类型为LOCK_REC_NOT_GAP,有X锁和S锁,作用就是锁一条记录
Gap Locks间隙锁:类型为LOCK_GAP,为解决幻读问题发明的,X锁和S锁没有差别,给一条记录加gap锁:其他事务不能在这条记录前面的间隙插入新纪录
可以通过给最后一条记录A所在页面的supremum记录(该页面中最大的记录)加gap锁,来阻止在A之后的间隙插入新记录
幻读:一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行(可重复读隔离级别下,幻读在“当前读”下才会出现)
产生幻读的一种原因:行锁只能锁住行,但新插入记录这个动作,更新的是记录的”间隙“,所以只好引入新的锁解决这个问题:间隙锁,它在可重复读隔离级别下才有效
跟间隙锁存在冲突关系的,是往这个间隙中插入一个记录这个操作,而间隙锁之间不存在冲突关系
间隙锁+行锁,合称为next-key lock,每个next-key lock都是前开后闭区间(间隙锁是开区间,加上一个行锁后 就变成前开后闭)
但间隙锁的引入,可能会导致同样的语句锁住更大的范围,影响并发度
间隙锁的加锁规则:
- 原则 1:加锁的基本单位是前开后闭区间的 next-key lock。
- 原则 2:查找过程中访问到的对象都会加锁。(范围查询会继续往后访问,访问到哪加锁到哪)
- 优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
- 优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
- 一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止
插入意向锁:如果在被gap锁锁住的区域想插入记录,该事务就会为gap锁锁住的记录加上插入意向锁,等待gap锁释放,它的作用仅限于此,不会阻止其他事务获取任何类型的锁
隐式锁:INSERT语句一般不加锁,但可以通过事务id,为新插入的记录加隐式锁
死锁和死锁检测
死锁:并发系统中不同线程出现循环资源依赖,导致这几个线程都进入无限等待的状态
出现死锁后,有两种策略:
-
直接进入等待,直到超时,超时时间由:innodb_lock_wait_timeout决定,默认是50s,即当出现死锁后,第一个锁住的线程要过50s才会超时退出,后续的线程才有可能执行
-
发起死锁检测,发现死锁后主动回滚死锁链条中的某个事务,让其他事务得以继续执行,将innodb_deadlock_detect设置为on开启
- 负担:每当一个事务锁住时,都需要判断会不会由于自己的加入导致死锁,这是一个O(N)的操作
- 如果能确保业务一定不会出现死锁,可以临时关闭死锁检测;
查看死锁:show engine innodb status
里的LATESTDETECTED DEADLOCK
记录最后一次死锁信息
MVCC版本控制
当我们在改动一条记录时,该记录的隐藏列roll_pointer指向undo日志版本链的头节点,trx_id记录该版本链对应的事务id,这个版本链在MVCC多版本并发控制中发挥了很大的作用
对于READ UNCOMMITTED来说,脏读是允许发生的,所以每次读取数据时直接读取最新版本即可
而对于READ COMMITTED和REPEATABLE READ来说,需要用到Readview来帮助进行版本控制,保证每次满足不脏读或可重复读的需求
Readview:获取当前系统活跃(尚未commit)的事务id列表、min_trx_id(最小的事务id),max_trx_id(系统应该分配给下一个事务的id),creator_trx_id(生成该Readview事务的id)
不脏读即事务不能读取到其他未提交事务修改的数据,观察被访问记录当前版本的trx_id
- trx_id == creator_trx_id:当前版本记录就是当前事务,可以访问该版本
- trx_id < min_trx_id:小于当前活跃事务的最小id,说明该版本已经commit,可以访问
- trx_id > max_trx_id:说明当前事务执行时,该版本还没有commit,不能访问
- 如果max_trx_id > trx_id > min_trx_id:查看事务id列表里有没有当前记录版本事务id,没有就说明当前事务已经commit
就这样顺着版本链以此判断当前版本是否对当前事务可见,就像是在生成 ReadView 的那个时刻做了一 次时间静止(就像用相机拍了一个快照),查询语句只能读到在生成 ReadView 之前已提交事务所做的更改
而READ COMMITTED和REPEATABLE READ区别在于
- 后者只会在第一次读取数据时生成Readview,这就使得它在后续判断版本可见性时用的都是最开始的Readview数据,所以即使在两次读取数据之间,有其他事务commit了,对于当前事务来说,那些事务commit的数据版本依旧是不可见的,这就实现了可重复读的需求;
- 前者则会每次读取数据时,都生成一个新的Readview
事务利用MVCC进行的读取操作叫做:一致性读、一致性无锁读、快照读
所有普通的select语句在READ COMMITTED和REPEATABLE READ下都是一致性读,不会对记录做任何加锁操作,其他事务可以自由改动
标签:事务,加锁,记录,trx,死锁,MVCC,id From: https://www.cnblogs.com/pinoky/p/18414843