MVCC与锁

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. 原则 1:加锁的基本单位是前开后闭区间的 next-key lock。
  2. 原则 2:查找过程中访问到的对象都会加锁。(范围查询会继续往后访问,访问到哪加锁到哪)
  3. 优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
  4. 优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
  5. 一个 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下都是一致性读,不会对记录做任何加锁操作,其他事务可以自由改动

posted @ 2024-09-14 23:31  pinoky  阅读(3)  评论(0编辑  收藏  举报