如果你觉得内容对你有帮助的话,不如给个赞,鼓励一下更新。
本文内容总结自极客时间《MySQL实战45讲》专栏
基于锁的并发控制,这种方案比较简单粗暴,就是一个事务去读取一条数据的时候,就上锁,不允许其他事务来操作(当然这个锁的实现也比较重要,如果我们只锁定当前一条数据依然无法解决幻读问题)。
在 MySQL 事务中,锁的实现与隔离级别有关系,在 RR(Repeatable Read)隔离级别下,MySQL 为了解决幻读的问题,以牺牲并行度为代价,通过 Gap 锁来防止数据的写入,而这种锁,因为其并行度不够,冲突很多,经常会引起死锁。现在流行的 Row 模式可以避免很多冲突甚至死锁问题,所以推荐默认使用 Row + RC(Read Committed)模式的隔离级别,可以很大程度上提高数据库的读写并行度。
根据加锁的范围,MySQL 里面的锁大致可以分成全局锁、表级锁和行锁三类。
全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)
。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
全局锁的典型使用场景是,做全库逻辑备份。 也就是把整库每个表都 select 出来存成文本。
开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。 会发生在MyISAM、memory、InnoDB、BDB 等存储引擎中。
MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。
表锁的语法是 lock tables … read/write。 与 FTWRL 类似,可以用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。需要注意,lock tables 语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。
MDL 不需要显式使用,在访问一个表的时候会被自动加上。MDL 的作用是防止DDL和DML并发的冲突,保证读写的正确性。在 MySQL 5.5 版本中引入了 MDL,当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。
注意:MDL 会直到事务提交才释放,在做表结构变更的时候,你一定要小心不要导致锁住线上查询和更新。
Online DDL的过程是这样的:
1、2、4、5如果没有锁冲突,执行时间非常短。第3步占用了DDL绝大部分时间,这期间这个表可以正常读写数据,是因此称为“online ”
锁定粒度最小,发生锁冲突的概率最低,并发度最高。会发生在InnoDB 存储引擎。
数据的操作其实只有两种,也就是读和写,而数据库在实现锁时,也会对这两种操作使用不同的锁;InnoDB 实现了标准的行级锁,也就是共享锁(Shared Lock)和排他锁(Exclusive Lock)。
FOR UPDATE
才可以锁定行,若开启自动提交,则匹配的行不会被锁定。set [global|session] innodb_lock_wait_timeout = 10;
来设置锁超时时间。InnoDB 行锁是通过对索引数据页上的记录(record)加锁实现的,所以InnoDB只有在通过索引条件检索数据时使用行级锁,否则使用表锁。
主要实现算法有 3 种:Record Lock、Gap Lock 和 Next-key Lock。
比如当你执行 select * from t where d=5 for update
的时候,就不止是给数据库中已有的记录加上了行锁,还同时记录之间加了间隙锁。这样就确保了无法再插入新的记录。
也就是说这时候,在一行行扫描的过程中,不仅将给行加上了行锁,还给行两边的空隙,也加上了间隙锁。
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。需要注意的是,每个间隙锁都是前开后开区间。
跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作,也即给相同区间加间隙锁的时候是不冲突的。间隙锁引入,帮我们解决了幻读的问题,但同时也带来了一些“困扰”。当互相持有想插入记录的间隙锁时,会发生死锁。并且间隙锁可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。
加锁规则里面,包含了两个“原则”、两个“优化”和一个“bug”:
开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。会发生在:BDB 存储引擎。
死锁产生的四个条件是什么呢?
如何避免死锁的产生呢?这里给出一些建议:
两阶段锁协议规定所有的事务应遵守的规则:
即事务的执行分为两个阶段:
首先,两阶段锁强调的是“加锁(增长阶段,growing phase)和解锁(缩减阶段,shrinking phase)这两项操作,且每项操作各自为一个阶段,这就是说不管同一个事务内需要在多少个数据项上加锁,那么所有的加锁操作都只能在同一个阶段完成,在这个阶段内,不允许对对已经加锁的数据项进行解锁操作,即加锁和解锁操作不能交叉执行(同一个事务内)。这一条是说在同一个事务内部的事情。 在InnoDB事务中,行锁在需要的时候才加上,但是并不是不需要了就立马释放,而是要等到事务结束才会释放。
通过乐观锁的方式来解决不可重复读和幻读问题,实际上 MVCC 机制它可以在大多数情况下替代行级锁,降低系统的开销。
注意:MVCC 只在 Read Commited(读已提交) 和 Repeatable Read(可重复读) 两种隔离级别下工作。
MVCC 的英文全称是 Multiversion Concurrency Control,中文翻译过来就是多版本并发控制技术。 从名字中也能看出来,MVCC 是通过数据行的多个版本管理来实现数据库的并发控制,简单来说它的思想就是保存数据的历史版本。这样我们就可以通过比较版本号决定数据是否显示出来。
通过 MVCC 我们可以解决以下几个问题:
在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现。
在两次并发的会话中,session B 的修改结果,被 session A 之后的 select 语句用“当前读”看到,不能称为幻读。幻读仅专指“新插入的行”。
在 MVCC 并发控制中,读操作可以分为两类:快照读(Snapshot Read)与当前读 (Current Read)。其中LBCC解决的是当前读情况下的幻读,MVCC解决的是普通读(快照读)的幻读。
简单的 select 操作,读取的是记录的可见版本(有可能是历史版本),不用加锁。
特殊的读操作,插入/更新/删除操作,读取的是记录的最新版本,并且当前读返回的记录,都会加锁,保证其他事务不会再并发修改这条记录。