欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

mysql InnoDB引擎的事务与锁分析

程序员文章站 2022-03-04 23:34:16
InnoDB引擎的事务与锁一. 背景:事务和事务引发的问题1. ACID原子性:表示整个事务是不可分割的,要么都执行成功,要么都执行失败。一致性:保证完整性约束没有被破坏。隔离性: 事务不可见行,事务与事务之间分离不可见。持久性:事务一旦提交,其结果就是永久性的,即使发生宕机,数据也是可以恢复的。2. 事务的分类1. 扁平事务扁平事务是事务中最简单的一种,也是使用最频繁的,在扁平事务中,所有操作都处于同一层次,由BEGIN开始,COMMIT 或者ROLLBACK结束,其操作都是原子性的。...

InnoDB引擎的事务与锁

一. 背景:事务和事务引发的问题

1. ACID

原子性:表示整个事务是不可分割的,要么都执行成功,要么都执行失败。

一致性:保证完整性约束没有被破坏。

隔离性: 事务不可见行,事务与事务之间分离不可见。

持久性:事务一旦提交,其结果就是永久性的,即使发生宕机,数据也是可以恢复的。

2. 事务的分类

1. 扁平事务

扁平事务是事务中最简单的一种,也是使用最频繁的,在扁平事务中,所有操作都处于同一层次,由BEGIN开始,COMMIT 或者ROLLBACK结束,其操作都是原子性的。

2. 带有保存点的扁平事务

在扁平事务的基础上,增加了保存点, 允许回滚到同一事务中较早的一个状态,因为在某些场景,放弃整个事务会浪费不必要的开销,对于扁平事务来说,隐式的增加了一个保存点,保存点用SAVE WORK函数建立,然后在事务中 只有这一个保存点, 保存点是一次递增的。

3. 链事务

可以看作是保存点模式的变种,当系统发生崩溃的时候,所有的保存点都会消失,因为保存点是**易失(volatile)的和非持久(persistent)**的,链事务的思想是:提交当前事务和开始下一个事务操作合并为一个原子操作,这意味这下一个事务是能看到上一个事务的处理结果的,如图 1-1:

mysql InnoDB引擎的事务与锁分析
图 1-1

4. 嵌套事务

嵌套事务是一个层次结构框架,InnoDB本事并不能很好的支持,因此需要依据前面三种事务,自己实现。

嵌套事务由一个顶层事务控制着各个层次的事务,被嵌套的事务被称为子事务,如图1-2:

mysql InnoDB引擎的事务与锁分析
图 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多版本并发控制(一致性非锁定读)

目的 : 解决一致性的非锁定读 成为快照读

mysql InnoDB引擎的事务与锁分析
图 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:

mysql InnoDB引擎的事务与锁分析
图4-1

五. 总结:

1. InnoDB 通过 MVCCNEXT-KEY Locks,解决了在可重复读的事务隔离级别下出现幻读的问题。

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

本文地址:https://blog.csdn.net/shuchuntang2729/article/details/107922232