InnoDB引擎的事务与锁
一. 背景:事务和事务引发的问题
1. ACID
原子性:表示整个事务是不可分割的,要么都执行成功,要么都执行失败。
一致性:保证完整性约束没有被破坏。
隔离性: 事务不可见行,事务与事务之间分离不可见。
持久性:事务一旦提交,其结果就是永久性的,即使发生宕机,数据也是可以恢复的。
2. 事务的分类
1. 扁平事务
扁平事务是事务中最简单的一种,也是使用最频繁的,在扁平事务中,所有操作都处于同一层次,由BEGIN开始,COMMIT 或者ROLLBACK结束,其操作都是原子性的。
2. 带有保存点的扁平事务
在扁平事务的基础上,增加了保存点, 允许回滚到同一事务中较早的一个状态,因为在某些场景,放弃整个事务会浪费不必要的开销,对于扁平事务来说,隐式的增加了一个保存点,保存点用SAVE WORK函数建立,然后在事务中 只有这一个保存点, 保存点是一次递增的。
3. 链事务
可以看作是保存点模式的变种,当系统发生崩溃的时候,所有的保存点都会消失,因为保存点是**易失(volatile)的和非持久(persistent)**的,链事务的思想是:提交当前事务和开始下一个事务操作合并为一个原子操作,这意味这下一个事务是能看到上一个事务的处理结果的,如图 1-1:
图 1-1
4. 嵌套事务
嵌套事务是一个层次结构框架,InnoDB本事并不能很好的支持,因此需要依据前面三种事务,自己实现。
嵌套事务由一个顶层事务控制着各个层次的事务,被嵌套的事务被称为子事务,如图1-2:
图 1-2
(1). 嵌套事务是事务组成的一个树,子树既可以是嵌套事务,也可以是扁平事务。
(2). 处在叶子节点的事务是扁平事务。
(3). 叶子节点的深度可以不同。
(4). 子事务既可以提交也可以回滚,但不会立马生效,要等父事务提交,因此顶层事务是所有子事务的前置事务。
(5). 树中的任意个事务的回滚会引起它的子事务的回滚,故子事务仅保留A,C,I特性,不具有D特性。
分布式事务
通常是一个在分布式环境下运行的扁平事务,一般分为强一致性事务和柔一致性事务,比如基于XA的二阶段提交就属于强一致性事务,而基于MQ或者补偿机制的分布式事务属于柔一致性事务,基于BASE理论,强一致性事务要保证强一致性,而柔一致性事务保证的数据的最终一致性,这里不展开讨论分布式事务,在之后的文章会展开讨论分布式事务的常见实现和原理。
3. 事务的实现
事务的实现 一般依赖于Redo log 和 Undo Log。
1.Redo Log :
重做日志是用来实现事务的持久性,即事务ACID中的D。其由两部分组成:一个是内存中的重做日志缓存(redo log buffer),它是易失的;二是重做日志文件(redo log file),是持久的。
当事务提交的时候,必须先将该事务的所有日志写入到重做日志文件进行持久化,待提交的事务COMMIT才算完成。重做日志格式是基于页的,文件记录的是每一个事务操作的物理地址和偏移量,并不是记录的数据本身,当数据库宕机重启,就是依赖于重做日志恢复的。
这里需要注意的是 数据库的页一般都是16K大小,而计算机内存的页,一般是4K大小,为了保证Redo log在数据库和机器内存同步的时候,保证数据不会丢失和错误,会采用Double Write的思想,感兴趣的小伙伴可以去了解以下,这里不展开讨论。
2.Undo Log :
undo log就是帮我们解决事务回滚的功能,与redo log不同的是, undo log存放在数据库内部的一个特殊段中,位于共享表空间。
undo log分为 insert undo log 和 update undo log,insert 就是在插入数据的时候产生的 undo log ,只对自身事务可见,对其他事物不可见;而 update undo log记录的是对update 和 delete操作产生的 undo log,该log可能需要提供MVCC机制(下面会说到),因此不能在事务提交的时候就删除,提交的时候放到 undo log链表,等待purge线程进行最后的删除。
3.事务引发的问题:
1.脏读:
指一个事务读取了另外一个事务未提交的数据。
事务一 | 事务二 |
---|---|
select * from user; //查出id为1的数据 | |
insert into user(id,name) values(2,‘coco’); | |
select * from user; //查出id为1 和 2 的数据 | |
ROLLBACK | |
select * from user; //查出id为1 |
2. 不可重复读:
在一个事务内读取表中的某一行数据,多次读取结果不同。(这个不一定是错误,只是某些场合不对)
事务一 | 事务二 |
---|---|
select * from user; //查出id=1,name=’leeco’的数据 | |
update user set name=‘leeco2’ where id = 1; | |
COMMIT | |
select * from user; //查出id=1,name=’leeco’的数据 |
3.幻读:
是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致(更偏向于数量)
事务一 | 事务二 |
---|---|
select * from user; //查出id为1的数据 | |
insert into user(id,name) values(2,‘coco’); | |
COMMIT | |
select * from user; //查出id为1 和 2 的数据 |
二. MVCC多版本并发控制(一致性非锁定读)
目的 : 解决一致性的非锁定读 成为快照读
图 2-1
该图直观的展示了非锁定读,之所以称为不锁定读,是因为不需要等待行X(排他)锁的释放。快照数据是指该行的之前的版本的数据,该实现是由undo段来完成的。而undo用来在事务中回滚数据,因为快照数据本身并没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。
因此,非锁定读大大提高了并发性。但是在不同的事务隔离级别下,读取的方式是不同的,在默认的可重复读(Repeatable Read)级别下,总是读取快照的开始时候的版本,而在读已提交(Read Committed)的级别下,总是读取最新的快照版本,因此读已提交会出现幻读的问题。
**MVCC解决快照读的幻读问题 : **针对与MVCC是否能解决幻读的问题,是存在争议的,绝大多数人认为是可以解决幻读的,少数人认为无法解决幻读,下例引发思考:
假设 :现在user表有1条数据
id | name |
---|---|
1 | leeco |
现在做如下操作:
事务一 | 事务二 |
---|---|
select * from user; //查出id为1的数据 | |
insert into user(id,name) values(2,‘coco’); | |
select * from user; //查出id为1的数据 | |
update user set name=‘coco’ where id=2; | |
select * from user; //查出id为1 和 2 的数据 |
思考:这种情况到底算不算幻读问题?
三. 行锁算法(一致性锁定读)
锁算法都是基于索引的,且锁的就是索引本身,而InnoDB默认使用的是next-key Lock
以下内容 非特殊情况 都是基于默认的可重复读的隔离级别展开说明!
共享锁(S) | 排他锁(X) | |
---|---|---|
共享锁(S) | 兼容 | 不兼容 |
排他锁(X) | 不兼容 | 不兼容 |
一致性锁定读,是显式在SELECT的时候加锁以保证数据逻辑的一致性,而这要求对操作行进行加锁语法
SELECT … FOR UPDATE; 加一个X锁,此时其他事务不能做任何操作
SELECT … LOCK IN SHARE MODE;加一个S锁,此时其他事务可以加S锁,但是加X锁会被阻塞
1. 间隙锁(Gap Lcok)
锁定一定的范围,但不包含本身 (左开右开)
2. 临键锁(Next-Key Lock)
锁定一定的范围,并且锁住本身 即GapLock + Record Lock (左开右闭)
3. 记录锁(Record Lock)
锁定单行记录
比如数据记录 1,5,9,Record Lock锁住的就是1,5,9,Gap Lcok锁住的是(-∞,1),(1,5),(5,9),(9,+∞),
Next-Key Lock锁住的是 (-∞,1], (1,5], (5,9], (9,+∞]
注意了,这里仅针对RR隔离级别,对于RC隔离级除了外键约束和唯一性约束会加间隙锁,没有间隙锁,自然也就没有了临键锁,所以RC级别下加的行锁都是记录锁,没有命中记录则不加锁,所以RC级别是没有解决幻读问题的。
那么这三种锁分别在什么时候生效呢,首先,行锁是基于索引的,InnoDB默认是的采用Next-Key Lock锁算法, 例如上例,
当 SELECT … WHERE ID = 5; 的时候,若ID非索引,则退化成表锁;如果ID是辅助索引,则会对前一个区域使用临键锁,即锁住了 (1,5] ,然后对下一个区域使用间隙锁,即锁住了(5,9),总结就是锁住了(1,9); 只有当ID是非空唯一索引的时候,会升级为记录锁(Record Lock), 只锁住ID=5的这一行记录的索引。
注意:虽然ID是非空唯一索引,但是当查询条件是范围查询的时候 也会退化为临键锁。
例如上例中,select * from id > 6; 此时 只能查出来ID=9的数据,然后添加一条ID=10的数据,如果没有临键锁,则下次查询会查出来ID=9和ID=10两条数据,就出现了幻读。而真是情况是:因为是范围查找,InnoDB采用临键锁,此时会对(6,9]和(9,+∞)范围进行加锁, 此时插入ID=10的数据会被阻塞,所以不会出现幻读.
因此 在当前读的环境下 临键锁解决了幻读问题。
四. 死锁问题
死锁 是指两个或两个以上的事务在执行过程中,因为抢夺资源而造成的一种互相等待的现象。
解决死锁的方式最简单的是超时,当超过等待时间 则进行回滚;
还有一种普遍的方式就是采用**wait-for graph(**等待图),要求数据库保存两种信息: 锁的信息链表,事务等待链表,
通过上述链表可以构造出一张图,如存在回路,则表示存在死锁问题,如图4-1:
图4-1
五. 总结:
1. InnoDB 通过 MVCC
和 NEXT-KEY Locks
,解决了在可重复读的事务隔离级别下出现幻读的问题。
2. 即使InnoDB默认是采用可重复读的事务隔离级别,但是正是由于MVCC和临键锁的存在,解决了幻读的问题,因此已经达到了串行化
的隔离级别。
本文地址:https://blog.csdn.net/shuchuntang2729/article/details/107922232