一、锁
1 介绍
行级锁不一定会增加开销。InnoDB存储引擎不需要锁升级,因为一个锁和多个锁的开销是相同的。
位图存储,所以相同
InnoDB提供一致性非锁定读、行级锁支持。
lock与latch
- latch一般被称为闩锁,是轻量级锁,要求锁定时间非常短,分为mutex互斥量、rwlock读写锁,保证并发线程操作临界资源的正确性,通常无死锁检测机制。我理解是对数据库本身各种线程请求资源加的锁
- lock的对象是事务,锁定数据库中的对象,如表、页、行,有死锁机制。
lock | latch | |
---|---|---|
对象 | 事务 | 线程 |
保护 | 数据库内容 | 内存数据结构 |
持续时间 | 整个事务过程 | 临界资源 |
模式 | 行锁、表锁、意向锁 | 读写锁、互斥量 |
死锁 | 通过waits-for graph、time out等机制进行死锁检测和处理 | 无死锁检测和处理机制。仅通过应用程序加锁的顺序保证无死锁情况发生 |
模式 | Lock Manager的哈希表中 | 每个数据结构的对象中 |
2. InnoDB存储引擎中的一致性读
2.2 一致性非锁定读
一致性非锁定读是指InnoDB通过行多版本控制读取数据发现读取的行正在执行DELETE或UPDATE操作,则不会等待锁释放,而是读取快照数据。
特点:
- 不需要等待锁定行的锁释放,也不会对历史数据上锁,提高并发度
- 通过undo段(用户事务回滚)完成,因此快照数据无额外开销
在事务隔离级别READ COMMITTED和REPEATABLE READ下,InnoDB使用一致性非锁定读。
- READ COMMITTED读取最新的快照数据
- REPEATABLE READ读取事务开始时的行数据版本
2.3 一致性锁定读
由上节可知,某些隔离级别下的普通select语句都是一致性非锁定读,但是某些情况下需要对select操作进行一致性锁定读,InnoDB支持两种锁定读操作:
- select … for update (悲观锁,X写锁)
- select … lock in share mode (乐观锁,S读锁)
在串行化级别下,快照读会加上S锁。在其他级别下,快照读不加锁
3 锁的算法
3.1 行锁的三种算法
- Record Lock: 单行记录锁,锁某一行
- Gap Lock:间隙锁,锁一个范围,不包含记录本身
- Next-Key Lock:Record Lock+Gap Lock,锁定范围加本身,左开右闭
Gap Lock是为了阻止多个事务将记录插到同一范围内,能解决不可重复读的问题
读已提交隔离级别下,行级锁的种类只有记录锁
可重复读隔离级别下,行级锁的种类有记录锁(Record-Lock)、间隙锁(Gap-Lock)、临键锁(Next-Key Lock)
加锁的对象是索引,加锁的基本单位是 next-key lock,但在能使用记录锁或者间隙锁就能避免幻读现象的场景下会退化成记录锁或间隙锁
非唯一索引,相同数据可能有多条。用行锁无法阻塞新数据插入。所以必须用gap-lock或者next-key lock实现。
t_lock表,id为主键,age上加了非聚集索引,后文全都以该表数据进行实验
X 排它锁 | 唯一索引 | 非唯一索引 | |
---|---|---|---|
等值查询-记录存在 | 在索引树上定位到这一条记录后, 临键锁退化成【记录锁】 select * from t_lock where id = 5 for update; 记录锁5 | select age from t_lock where age = 12 for update; 主键索引-记录锁-5 二级索引-间隙锁-(12,33) 二级索引-临键锁-(0,12] | |
等值查询-记录不存在 | 在索引树找到第一条大于该查询记录,临键锁会退化成【间隙锁】 select * from t_lock where id = 6 for update; 间隙锁(5,10) | select age from t_lock where age = 10 for update; 二级索引-间隙锁-(0,12) | |
范围查询-大于 | 全都是临键锁 select * from t_lock where id > 5 for update; 临键锁 (5,10],(10,+oo] | ||
范围查询-大于等于 | 加锁效果等同于拆成两个语句:大于和等于。等于语句根据记录是否存在来退化。大于语句全都是临键锁 select * from t_lock where id >=5 for update; 记录锁5,临键锁(5,10],(10,+oo] | ||
范围查询-小于/小于等于且等于出记录不存在 | 除了边界处为间隙锁,其余左侧数据上都是临键锁 select * from t_lock where id <=6 for update; 间隙锁(5,10),临键锁 (-oo,1] (1,5] | ||
范围查询-小于等于且等于处记录存在 | 左侧数据全是临键锁 select * from t_lock where id <=5 for update; 临键锁 (-oo,1] (1,5] |
3.2 锁兼容性
a. X/S/IX/IS
InnoDB存储引擎实现了两种标准行级锁(S/X)
- 共享锁(S Lock),允许事务读一行数据
- 排它锁(X Lock),允许事务删除或更新一行数据
两种锁的兼容性
X | S | |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
不兼容是指a事务已经获取某行的X锁,b事务获取该行的X锁时会被阻塞
InnoDB支持意向锁(Intention Lock)
意向锁将锁定对象分为多个层次,意味着事务希望在更细粒度的对象上加锁。比如对象层次分为库->表->页->行,想对行上加锁,会先对库表页上加意向锁。
X | S | IS | IX | |
---|---|---|---|---|
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 | 兼容 | 不兼容 |
IS | 不兼容 | 兼容 | 兼容 | 兼容 |
IX | 不兼容 | 不兼容 | 兼容 | 兼容 |
意向锁之间是相互兼容的,因为他们的下一层(实际锁对象)之间没有冲突。
b. X排它锁下,Record/GAP/Next-key/II GAP兼容性
间隙锁与记录锁/临建锁都是兼容的。
记录锁(Record Lock) | 间隙锁(Gap Lock) | 临键锁(Next-Key Lock) | |
---|---|---|---|
记录锁(Record Lock) | 不兼容 | 兼容 | 不兼容 |
间隙锁(Gap Lock) | 兼容 | 兼容 | 兼容 |
临键锁(Next-Key Lock) | 不兼容 | 兼容 | 不兼容 |
插入意向锁(Insert Intention Gap Lock)是一种间隙锁,但是它与间隙锁、临键锁都不兼容
3.2 举例
查询加锁信息的SQL如下。
select * from performance_schema.data_locks;
a. 在非唯一索引上加X锁,不管是否回表,都会导致查询到的数据行主键加记录锁
-
开启事务加锁:
begin; select age from t_lock where age = 12 for update;
-
查询加锁信息可以看到除了在表上加了意向排它锁(IX)外,分别在二级索引加了临键锁(X)、间隙锁(X,GAP),和主键索引上的记录锁(X,REC_NOT_GAP)
b. 在非唯一索引上加S锁,只有回表情况下才会对数据行主键加锁(todo 原因?)
回表查询主键索引情况:
- 开启事务加锁
begin;
select name from t_lock where age = 12 lock in share mode;
- 查询回表,导致主键索引加锁。主键索引的记录锁5+二级索引的间隙锁(12,33)+二级索引的临键锁 (0,12]
不回表情况
- 开启事务加锁
select id from t_lock where age = 12 lock in share mode;
- 只在二级索引上就查询到了结果,因此只有二级索引的间隙锁(12,33)+二级索引的临键锁 (0,12]
参考
4 锁升级
InnoDB不存在锁升级的问题,其不是根据记录产生行锁,而是对每个页进行锁管理,采用位图的方式。因此锁页还是锁行开销是相同的。
5 死锁
概念:两个或两个以上的事务在执行过程中,因争抢锁资源造成互相等待导致无法前进。
死锁的两种解决方式:超时、死锁检测
5.1 超时
设置某事务等待超过某时间后,进行回滚,释放资源。通过innodb_lock_wait_timeout设置超时时间
- FIFO顺序回滚
- 更新行数较多,占用undo log较大,回滚花费时间可能较长
5.2 死锁检测
wait-for graph 等待图,一种主动的死锁检测机制,通过锁信息链表和事务等待链表构造图。
5.3 两种方式优缺点
- 超时:
- 缺点:超时时间难以把控,时间过长系统难以接受,时间过短容易误伤正常的事务。回滚事务undo log日志可能较多。
- 优点:底层逻辑简单,保底手段
- 死锁检测:
- 缺点:一旦阻塞就会检测,大量事务更新同key可能会浪费大量CPU资源进行死锁检测
- 优点:存在死锁时回滚undo log少的事务
热点更新的问题:
热点更新会造成死锁检测花费大量CPU时间进行死锁检测,降低事务并发度。
解决方式:
- 关闭死锁检测,依靠超时解决:可能大量超时,影响系统性能
- 控制并发度,在应用层或中间件控制同key串行更新
二、MVCC多版本并发控制
MVCC是多版本并发控制的意思,主要是通过一致性非锁定读用于不加锁读提高数据库的并发性能。解决了读已提交和可重复读两种事务隔离级别下的并发读写冲突。通过三个隐式字段(最近一次修改该数据的事务id、隐藏的自增id、指向上一个数据版本的回滚指针)undo log和read view实现。
读未提交、串行化未使用MVCC
对于undo log、readview的讲解可以看这篇
无论是读已提交、可重复读,在使用undo log和read view的可见性判断流程上是一样的,唯一区别是两种隔离级别下read view的生成时机不同。
可见性判断: read view里包括 创建时活跃(未提交)事务集合、当前事务ID、最大事务ID、最小活跃事务ID。活跃事务和超过最大事务ID(创建ReadView时还没有这个事务)都不可见该条数据。
RC每次Select都会创建ReadView。RR只在第一次Select创建ReadView
三、事务
1. 事务四大特性
ACID。原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(duration)
简单解释:
- 原子性:事务的一系列操作,要么全成功,要么全失败
- 一致性:事务执行前和执行后,数据库应处于一致性的状态。
- 隔离性:并发事务相互隔离,不能相互干扰
- 持久性:事务一旦完成,数据就会被永久地保存下来。
2. 事务类型
- 扁平事务 flat transactions
- 带有保存点的扁平事务 flat transactions with savepoints
- 链事务 chained transactions
- 嵌套事务 nested transactions
- 分布式事务 distributed transactions
3. 事务的实现
redo
事务的持久性是通过重做日志实现的。内存中的重做日志缓冲(redo log buffer)是容易丢失的。磁盘中的重做日志文件(redo log file)是持久的。
事务提交时,必须先将事务的所有日志写入到重做日志文件进行持久化。重做日志文件由两部分组成:redo log 和undo log
4. 四种隔离级别
解决的问题 | 有什么问题 | 理解 | |
---|---|---|---|
读未提交(Read Uncommitted) | 更新丢失(两次自增更新后却结果只自增更新一次) | 脏读(事务A读取到了事务B未提交的数据,并且事务B发生了回滚,因此事务A读到的数据是不应存在的数据) | 在这条数据上写写互斥,写读兼容。快照读是不加锁的当前读 |
读已提交(Read Committed) | 解决脏读 | 不可重复读(事务开启到结束的过程中,对同一条数据的多次查询结果可能不同) | 使用MVCC,同一事务每次快照读都会创建read view |
可重复读(Repeatable Read)(InnoDB默认的隔离级别) | 不可重复读 | 幻读(一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行) | 使用MVCC,同一事务,多次快照读都会使用第一次快照读产生的read view |
串行化(Serializable) | 幻读 | 锁表,性能差 | 快照读默认加上S锁 |
5. 脏读、不可重复读、幻读
脏读
含义: A事务读到了B事务未提交的数据,若是B回滚,最终A读到了不存在的数据。
解决方法:
- 快照读时:利用MVCC机制,每次快照读都创建read view,读取版本链上最近的已提交的数据或者当前事务操作过的数据。
- 当前读时:利用锁机制,将当前事务阻塞直至另一个事务提交
不可重复读
含义: 同一事务,同一条数据多次查询的结果不同。这是因为其他事务在当前事务两次查询期间,对数据进行了更改并提交。
解决方法:
- 快照读时:利用MVCC机制,在一次事务过程中,只在第一次查询时创建read view,后面每次查询都会使用第一次的read view。根据这个read view判断版本链上的数据是否可见(已提交或者当前事务操作过的数据)。由于每次查询都是采用第一次的read view,因此每次查询结果都是相同的。
- 当前读时:利用锁机制,将当前事务阻塞直至另一个事务提交
幻读
含义: 同一事务两次范围查询的结果集不同。这是因为存在其他事务增加或者删除了当前查询范围下的数据。
可重复读隔离级别下
- 解决了部分幻读场景:
-
普通情况下(后面有个特例),两次快照读查询到的结果集是一致的。在解决不可重复读时便解决了这个问题。
-
两次当前读查询到的结果集是一致的。利用Next-Key Lock(间隙锁+单行锁),保证其他事务无法插入当前查询范围。
-
- 未解决的幻读场景:
- A事务快照读,B事务插入并提交,A事务当前读,就会读到B插入的数据,从而两次查询结果集不一致。因为当前读会加锁并读取最新的数据,所以跟快照读读到的数据不一样。
- A事务快照读,B事务插入并提交,A事务更新B插入的数据(虽然在A事务中,见不到这条数据),A再次快照读,这时A会查询到B插入的数据。
关于未解决的第二点,这里举一个例子。
在空表table,
A事务执行
begin;
select * from table; 此时查询结果为0条数据
B事务执行
insert into table values(‘x’);
A事务执行
select * from table; 此时查询结果为0条数据。使用第一次快照读的read view,可重复读,看不到B插入的数据。
update table set name = ‘y’ where name = ‘x’; update操作是当前读并加锁操作,能看到B事务插入的数据,此时数据x的版本链上新增了个数据y,事务id为当前A事务的id,对于下次A快照读该数据,y是可见的。
select * from table; 此时查询结果为1条数据
commit;
四、日志
七种日志
日志类型 | 解释 |
---|---|
redo log 重做日志 | 用于恢复因宕机丢失未刷入磁盘的内存的数据 |
undo log 回滚日志 | 保证数据的原子性,保存事务发生前版本的数据,用于回滚 |
bin log 二进制日志 | 记录增删改时的日志,用于主从复制和数据库恢复 |
relay log 中继日志 | 主从复制过程中,从结点会将主节点的数据写到relaylog,然后 |
error log 错误日志 | 记录mysql启动、停止、运行过程中错误相关信息 |
slow query log 慢查询日志 | 记录执行成功的执行时间过长和没有使用索引的语句 |
general log 普通日志 | 记录服务器收到的命令 |
参考
- 《MySQL技术内幕》 第六章
- MVCC详解,深入浅出简单易懂
- https://www.cnblogs.com/qdhxhz/p/15750866.html
- https://www.jianshu.com/p/bdae82978b4d
- Mysql InnoDB存储引擎的锁相关
- 大白话讲解脏写、脏读、不可重复读和幻读
什么是幻读?以及如何解决幻读问题?- MySQL 幻读
- 看一遍就懂:MVCC原理详解
- 如何理解MySQL中间隙锁可以避免幻读的问题?
- 强烈推荐:MySQL MVCC底层原理详解
- MySQL七种日志介绍