一、ACID原则
事务需要满足ACID原则。
A–原子性
事务要么就成功提交,要么就全部失败,然后回滚。
例如,银行扣款200,微信增加200,只进行扣款是不行滴。
C–一致性
数据库在事务执行前后都保持一致的状态。只有一致性得到保证,数据库才是正确的。
I–隔离性
在一个事务完成以前,它对于其他事务是不可见的。
比如A事务银行扣款了200元,提交事务之前其他事务无法知道。
D–持久性
一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。
例如:
操作前A:800,B:200
操作后A:600,B:400
如果在操作前(事务还没有提交)服务器宕机或者断电,那么重启数据库以后,数据状态应该为
A:800,B:200
如果在操作后(事务已经提交)服务器宕机或者断电,那么重启数据库以后,数据状态应该为
A:600,B:400
二、并发一致性问题
在并发情况下,数据库会出现种种问题
1、丢失修改
两个事务对同一个数据进行修改,那么其中一个事务修改的结果就会被替换,从而造成丢失。
就比如说上面的图中,-1张这个修改就被丢失了,从而导致了错误的结果(正确应该是15,13)
2、脏读
指一个事务读取了另外一个事务未提交的数据。注意,主体是外部的另一个事务,而不是正在进行的这个未提交的事务。
3、不可重复读
在一个事务内读取某一数据,多次读取结果不同。主体是没有提交的这个事务
4、幻读
幻读本质上也属于不可重复读的情况
是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。(一般是行影响,多了一行)
突然多了个东西,让人感觉出现了幻觉
三、锁
1、粒度
“行级锁”:行级锁是 MySQL 中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。
“表级锁”:表级锁是 MySQL 中锁定粒度最大的一种锁,表示对当前操作的整张表加锁。
“页级锁”:页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。
一般来说,粒度越小,发生锁争用的可能就越小,并发程度就越高;但同时,系统开销就越大
2、读写锁
-
独占锁—X锁,写锁
对象A只能被一个事务获取,获得X锁的事务即能读取又能修改数据。
其它事务不能对 A 加任何锁
-
共享锁–S锁,读锁
对象A可以被多个事务读,但是谁也不能修改它。
其它事务能对 A 加 S 锁,但是不能加 X 锁。
3、意向锁
在存在行级锁和表级锁的情况下,事务 T 想要对表 A 加 X 锁,就需要先检测是否有其它事务对表 A 或者表 A 中的任意一行加了锁,那么就需要对表 A 的每一行都检测一次,这是非常耗时的。
因此,在原来的 X/S 锁之上引入了意向锁 IX/IS,IX/IS 都是表锁,用来表示一个事务有意向在表中的某个数据行上加 X 锁或 S 锁
规定:
- 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁;
- 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。
它们之间的兼容性矩阵是酱紫的:
- 任意 IS/IX 锁之间都是兼容的,因为它们只表示想要对表加锁,而不是真正加锁
- IX和X的关系等同于X和X之间的关系,为什么呢?因为事务获得了IX锁,接下来就有权利获取X锁,这样就会出现两个事务都获取X锁的情况,这和我们已知的X锁和X锁之间互斥是矛盾的。S和IS、X和IS、IX和IS也可以由此推导出来
- 注意,这里兼容关系针对的是表级锁
有趣的问题:
IX 与 X冲突,那岂不是任意两个写操作,即使写不同行也会造成死锁
- Session A 请求 IX–成功,Session B请求 IX–成功
- Session A 请求 X,发现已经有其他session有IX,因此冲突
- 同理SessionB请求X也会是这种情况。那这row lock还有什么用?
答案:
- IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突
- 行级别的X和S按照普通的共享、排他规则即可。所以之前的示例中第2步不会冲突,只要写操作不是同一行,就不会发生冲突。
4、封锁协议
-
一级封锁协议
事务 T 要修改数据 A 时必须加 X 锁,直到 T 结束才释放锁。
可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么事务的修改就不会被覆盖。
但是不能解决脏读问题等。一个事务正在修改着,另一个进来读,照样脏锁
-
二级封锁协议
在一级的基础上,要求读取数据 A 时必须加 S 锁,读取完马上释放 S 锁。
可以解决脏读问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,这在存在 S 锁的情况下是不允许的
但是不能解决不可重复读问题等。一个事务读完了释放S锁,然后另一个事务修改了值,那再读就会出现不可重复读。
-
三级封锁协议
在二级的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束了才能释放 S 锁。
可以解决不可重复读的问题,因为读 A 时直到事务结束,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。
-
两段锁协议(2PL)
加锁和解锁分为两个阶段进行:加锁和解锁。
1.所有的读写操作之前均需加锁;
2.解锁操作后不允许再出现加锁操作;
可以证明,若并发执行的所有事务均遵守两段锁协议,则对这些事务的任何并发调度策略都是可串行化的。
四、隔离级别
1、未提交读
事务中的修改,即使没有提交,对其它事务也是可见的。
不能防脏读。
2、提交读
一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。
可防脏读,不能防不可重复读。
3、可重复读
保证在同一个事务中多次读取同一数据的结果是一样的。
可防不可重复读,不可防幻读。
4、可串行化
强制事务串行执行,这样多个事务互不干扰,不会出现并发一致性问题。
该隔离级别需要加锁实现,因为要使用加锁机制保证同一时间只有一个事务执行,也就是保证事务串行执行。
可防幻读。
五、多版本并发控制(MVCC)
多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。
主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
MVCC没有用锁,除非在select语句中强制加锁
而未提交读隔离级别总是读取最新的数据行,要求很低,无需使用 MVCC。
可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。
1、当前读,快照读与MVCC
-
当前读:读取的是记录的最新版本,会对读取的记录进行加锁
当前读包含:update、insert、delete情况,
以及select * from table where ? lock in share mode; (加共享锁)
和select * from table where ? for update; (加排它锁)这两种特殊情况
-
快照读:读到的并不一定是数据的最新版本,有可能是之前的历史版本,不加锁
快照读包含:select的一般情况
MVCC的理念是维持一个数据的多个版本,使得读写操作没有冲突。快照读就是MySQL为我们实现MVCC的理想模型。MVCC模型在MySQL中的具体实现则是由 3个隐式字段
,undo日志
,Read View
等去完成的。
2、MVCC的实现原理
1、隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID
,DB_ROLL_PTR
,DB_ROW_ID
等字段
-
DB_TRX_ID
最近修改事务ID -
DB_ROLL_PTR
这条记录的上一个版本 -
DB_ROW_ID
隐含的自增ID(隐藏主键)实际还有一个删除flag隐藏字段, 即记录被更新或删除并不代表真的删除,而是删除flag变了
2、undo日志
undo log
是一条记录版本线性表,即链表,undo log
的链首就是最新的旧记录,链尾就是最早的旧记录
undo log的节点可能会被purge线程清除掉。
purge线程:
为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。
3、Read View
Read View
主要是用来做可见性判断的。
当我们某个事务执行快照读的时候,对该记录创建一个Read View
读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据
trx_list
一个列表,维护Read View生成时刻系统正活跃的事务IDup_limit_id
记录trx_list列表中最小的事务ID(最早的)low_limit_id
ReadView生成时系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1(最近的)
然后,用该条记录的DB_TRX_ID和这些值做比较
-
首先比较
DB_TRX_ID < up_limit_id
, 如果小于,则当前事务能看到DB_TRX_ID
所在的记录,如果大于等于进入下一个判断 -
接下来判断
DB_TRX_ID 大于等于 low_limit_id
, 如果大于等于则代表DB_TRX_ID
所在的记录在Read View
生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断 -
判断
DB_TRX_ID
是否在活跃事务之中,trx_list.contains(DB_TRX_ID)
,如果在,则代表我Read View
生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在Read View
生成之前就已经Commit了,你修改的结果,我当前事务是能看见的
我想吐槽一下up和low的命名,对应最小和最大,让人感觉弄反了,但源码里就是这么写的
3、RC和RR级别下的快照读
RC:提交读
RR:可重复读
InnoDB默认的隔离级别是RR(可重复读)
但是呢,InnoDB默认还禁用了innodb_locks_unsafe_for_binlog系统变量。 在这种情况下,InnoDB使用next-key locks进行搜索和索引扫描,从而避免幻读。
意思是说,隔离级别是RR的,但是由于禁用系统变量的缘故,导致达成了串行化的效果。
在RC隔离级别下,同一个事务中每个快照读都会生成并获取最新的Read View;
而在RR隔离级别下,同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View
4、MVCC+next-key locks解决幻读
Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。
MVCC 不能解决幻影读问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。
1、Record Locks
锁定一个记录上的索引,而不是记录本身。
如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。
2、Gap Locks
锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。
SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
3、Next-Key Locks
它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。它锁定一个前开后闭区间,例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间:
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)
参考资料: