InnoDB引擎的事务与锁

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 logupdate 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 通过 MVCCNEXT-KEY Locks,解决了在可重复读的事务隔离级别下出现幻读的问题。

2. 即使InnoDB默认是采用可重复读的事务隔离级别,但是正是由于MVCC和临键锁的存在,解决了幻读的问题,因此已经达到了串行化的隔离级别。

猜你喜欢

转载自blog.csdn.net/shuchuntang2729/article/details/107922232