https://tech.meituan.com/2014/08/20/innodb-lock.html
基本认识
事务(Transaction)在数据库中一般是指包含了一组原子性操作的步骤组合,这些操作要么都成功,要么都失败,事务一般包含了ACID四大特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。这也是数据库区别于一般文件系统的重要特性之一。
事务ACID的概念
原子性(Atomicity):原子性是指一个事务中的操作,要么全部成功,要么全部失败,如果失败,就回滚到事务开始前的状态。
一致性(Consistency):一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。那转账举栗子,A账户和B账户之间相互转账,无论如何操作,A、B账户的总金额都必须是不变的。
隔离性(Isolation):隔离性是当多个用户 并发的 访问数据库时,如果操作同一张表,数据库则为每一个用户都开启一个事务,且事务之间互不干扰,也就是说事务之间的并发是隔离的。再举个栗子,现有两个并发的事务T1和T2,T1要么在T2开始前执行,要么在T2结束后执行,如果T1先执行,那T2就在T1结束后在执行。关于数据的隔离性级别,将在后文讲到。
持久性(Durability):持久性就是指如果事务一旦被提交,数据库中数据的改变就是永久性的,即使断电或者宕机的情况下,也不会丢失提交的事务操作。
事务分类
https://www.cnblogs.com/starsray/p/16456569.html
事务按照类别可以划分为以下几种类型:
扁平事务(Flat Transactions)
事务类型中最为简单的一种,也是使用最频繁的一种,通常伴随着BEGIN…COMMIT/ROLLBACK语句,所有操作都是原子性的。
带有保存点的扁平事务(Flat Transactions with Savepoints)
相比于扁平事务,带有保存点的扁平事务再事务内部维护了一个可以递增的保存点,在事务进行回滚时可以指定回滚到某个保存点,如果需要回滚到最初状态,需要再次执行ROLLBACK语句。
说明:执行过程类似于START TRANSACTION…SAVEPOINT a…ROLLBACK TO SAVEPOINT a…ROLLBACK /COMMIT
链事务(Chained Transactions)
链事务可以看作为带有保存点的扁平事务的一种变种。链事务将带有保存点的扁平事务中一个较长的操作链路转化为较短链路的事务链,这样一定程度保障了数据丢失的可能性。链事务同带有保存点的扁平事务类型相比,前者只能回滚到最近一个操作事务内的保存点,而后者可以回滚到任意保存点,并且链事务在COMMIT后就释放了当前事务所拥有的锁。
嵌套事务(Nested Transactions)
嵌套事务是一个多层次架构的事务树,由顶层事务控制子事务,子事务既可以是嵌套事务也可以是扁平事务,且叶子节点只能是扁平事务,每个子事务从根到叶节点的距离可以是不同的。
子事务既可以提交也可以回滚。但是它的提交操作并不马上生效,除非其父事务已经提交。因此可以推论出,任何子事物都在顶层事务提交后才真正的提交。树中的任意一个事务的回滚会引起它的所有子事务一同回滚,故子事务仅保留A、C、I特性,不具有D的特性。
分布式事务(Distributed Transactions)
分布式事务通常指在分布式环境中运行的扁平事务,一般出现在跨数据库实例的访问链路中。分布式事务同样要求满足ACID特性。单依靠数据库特性并不一定可以完美解决,一般需要引入第三方工具进行中间协调,如Seata。
MySQL中对于InnoDB存储引擎来说,其支持扁平事务、带有保存点的事务、链事务、分布式事务。对于嵌套事务,其并不原生支持,但用户仍可以通过带有保存点的事务来模拟串行的嵌套事务。
四种隔离级别
SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
mysql的4种事务隔离级别,如下所示:
1、未提交读(Read Uncommitted):
允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
这种事务隔离级别下,select语句不加锁,也不是快照读。
--原数据
--id name
--1 lisi
--事务1
START TRANSACTION;
updata t_table set name = 'wangwu' where id = 1; --此时事务2查询id = 1
ROLLBACK;
--事务2
select * from t_table where id = 1; --查询到 id = 1, name = 'wangwu'
2、提交读(不可重复读)(Read Committed):
--原数据
--id name
--1 lisi
--事务1
select * from t_table where id = 1; -- 查询到 id = 1, name = list, 事务2在此时提交
select * from t_table where id = 1; -- 查询到 id = 1, name = wangwu
--事务2
start transaction;
update t_table set name = 'wangwu' where id = 1;
COMMIT;
能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
该级别下是通过快照读来防止读脏的。因为在该级别下的快照读总是能读到最新的行数据快照,当然,必须是已提交事务写入的,所以可能产生不可重复读。
而且如果是不上锁的select,可能产生不可重复读。
会产生幻读, 每次select都生成一个快照读。
3、可重复读(Repeated Read)(解决大部分幻读):
可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读,但是innoDB解决了幻读
在该级别下
- 通过快照读以及锁定区间来实现避免产生幻读和不可重复读;
- 某个事务首次read记录的时间为T,未来不会读取到T时间之后已提交事务写入的记录,以保证连续相同的read读到相同的结果集,这可以防止不可重复读;
- RR下是通过间隙锁,临键锁来解决幻影读问题;
快照读没有完全解决幻读?
是的,MySQL没有完全解决快照读下的幻读问题。
可以做这个实验:
1.当前DB已有id 5, 10, 15三条数据。
2.事务A查询id < 10的数据,可以查出一行记录id = 5
3.事务B插入id = 6的数据
4.事务A再查询id < 10的数据,可以查出一行记录id = 5,查不出id = 6的数据(读场景,解决了幻读)
5.事务A可以更新/删除id = 6的数据,不能插入id = 6的数据(写场景,幻读不彻底)
这个很好理解,MySQL虽然通过MVCC的版本号来解决了读场景下的幻读,但对于上面第5步那种写场景的情况,其实是无能为力的,因为MVCC毕竟是无锁实现。
首先,没有彻底解决幻读。各自解决了一半
- RR隔离级别,如果事务中都是快照读,或者全都是当前读,都不会产生幻读。只有当前读和快照读混用,才会产生幻读。
- MVCC保证快照读不会幻读
- next-key lock保证当前读不会产生幻读
案例
-
在 RR 隔离级别下,插入新数据不会受到锁的限制,因此在事务中进行插入操作可能导致幻读。
-
如果使用非标准的语句或查询方式,例如使用不稳定的函数或不加锁的查询语句,也可能导致幻读。
-
如果在高并发环境下,有多个事务同时读取相同的数据行,而其中一个事务在读取之后修改或删除了该行数据,此时其他事务再次读取该行数据时可能会出现幻读。
MySQL innodb 在 RR 隔离下一样会出现幻读,next-key lock 和 MVCC 只解决了部分幻读的场景。
4、串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞
数据库事物隔离级别解决的问题
隔离性1-4 由低到高, 而并发性相反
- 脏读、不可重复读、幻读:
也许有很多读者会对上述隔离级别中提及到的 脏读、不可重复读、幻读 的理解有点吃力,我在这里尝试使用通俗的方式来解释这三种语义:
脏读:一个事务读取了另一个事务未提交的数据。
-
不可重复读:事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了,就是所谓的不可重复读了。
也就是说,当前事务先进行了一次数据读取,然后再次读取到的数据是别的事务修改成功的数据,导致两次读取到的数据不匹配,也就照应了不可重复读的语义。
幻读:一个事务读取到了别的事务插入的数据。
行锁防止别的事务修改或删除,GAP锁防止别的事务新增,行锁和GAP锁结合形成的的Next-Key锁共同解决了RR级别在写数据时的一部分幻读问题。
https://opensource.actionsky.com/20210818-mysql/
可重复读是如何避免幻读的?
MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象解决的方案有两种:
针对快照读(普通 select 语句)
- 快照读情况下
在可重复读隔离级别下是通过MVCC来避免幻读的,具体的实现方式在事务开启后的第一条select语句生成一张Read View(数据库系统当前的一个快照),之后的每一次快照读都会读取这个Read View。
即在第②时刻生成一张Read View,所以在第⑤时刻时读取到数据和第②时刻相同,避免了幻读。
当前读(select … for update 等语句)
-
当前读情况下
当前读:像select lock in share mode(共享锁), select for update ; update, insert ,delete这些操作都是一种当前读,读取的是记录的最新版本。
在当前读情况下是通过next-key lock来避免幻读的,即加锁阻塞其他事务的当前读。如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。事务A在第②时刻执行了select for update当前读,会对id=1和2加记录锁,以及(2,+∞)这个区间加间隙锁,两个都是排它锁,会阻塞其他事务的当前读,所以在第③时刻事务B更新时阻塞了,从而避免了当前读情况下的幻读。
不可重复读和幻读的区别
很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert。
如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。
所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。
上文说的,是使用悲观锁机制来处理这两种问题,但是MySQL、ORACLE、PostgreSQL等成熟的数据库,出于性能考虑,都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来避免这两种问题。
RC、RR区别
RC 与 RR 在锁方面的区别
-
RR 支持 gap lock(next-key lock),而RC则没有gap lock。因为MySQL的RR需要gap lock来解决幻读问题。而RC隔离级别则是允许存在不可重复读和幻读的。所以RC的并发一般要好于RR;
-
RC 隔离级别,通过 where 条件过滤之后,不符合条件的记录上的行锁,会释放掉(虽然这里破坏了“两阶段加锁原则”);但是RR隔离级别,即使不符合where条件的记录,也不会释放行锁和gap lock;所以从锁方面来看,RC的并发应该要好于RR;另外 insert into t select … from s where 语句在s表上的锁也是不一样的:
UPDATE 语句加锁分析
下面,我们先来分析 UPDATE 相关 SQL 在使用较为简单 where 从句情况下加锁情况。其中的分析原则也适用于 UPDATE,DELETE 和 SELECT … FOR UPDATE等当前读的语句。
聚簇索引(主键)
UPDATE book SET score = 9.2 WHERE ID = 10
-
命中:
命中的情况下在 RC 和 RR 隔离等级下的加锁,都是对 ID = 10 聚簇索引加排他记录锁(X锁)。 -
未命中:
在 RC 隔离等级下,不需要加锁;
在 RR 隔离级别会在 ID = 16 前后两个索引之间加上间隙锁。值得注意的是,间隙锁和间隙锁之间是互不冲突的,间隙锁唯一的作用就是为了防止其他事务的插入新行,导致幻读,所以加间隙 S 锁和加间隙 X 锁没有任何区别。
唯一索引
UPDATE students SET score = 100 WHERE no = 'S0003';
-
命中
在 InnoDB 存储引擎中,二级索引的叶子节点保存着主键索引的值,然后再拿主键索引去获取真正的数据行,所以在这种情况下,二级索引和主键索引都会加排他记录锁。
RC、RR会对二级和聚簇索引都加X锁(防止其他事务通过聚簇改数据) -
未命中
RC不加锁
RR只在二级索引(唯一索引)加GAP
二级非唯一索引
UPDATE students SET score = 100 WHERE name = 'Tom';
-
命中
RC对二级和聚簇加X锁;
RR对二级加X锁和Gap, 对聚簇加X锁。 -
未命中 二级非唯一索引(查询未命中)
RC不加锁;
RR只在二级索引加GAP;
无索引
当 Where 从句的条件并不使用索引时,则会对全表进行扫描,
在 RC 隔离等级下对所有的数据加排他记录锁。
在RR 隔离等级下,除了给记录加锁,还会对记录和记录之间加间隙锁。和上边一样,间隙锁会和左侧的记录锁合并成 Next-Key 。
下图就是 UPDATE book SET score = 9.2 WHERE score = 22
语句在两种隔离等级下的加锁情况。
RC 与 RR 在复制方面的区别
RC 隔离级别不支持 statement 格式的bin log,因为该格式的复制,会导致主从数据的不一致;只能使用 mixed 或者 row 格式的bin log; 这也是为什么MySQL默认使用RR隔离级别的原因。复制时,我们最好使用:binlog_format=row
SQL标准的RR并不要求避免幻象读,而InnoDB通过gap锁来避免幻读,从而实现SQL的可串行化,保证Binlog的一致性。
RC 与 RR 在一致性读方面的区别
RC隔离级别时,事务中的每一条select语句会读取到他自己执行时已经提交了的记录,也就是每一条select都有自己的一致性读ReadView;
而RR隔离级别时,事务中的一致性读的ReadView是以第一条select语句的运行时,作为本事务的一致性读snapshot的建立时间点的。只能读取该时间点之前已经提交的数据。
锁
悲观锁和乐观锁
悲观锁
正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。
乐观锁
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
要说明的是,MVCC的实现没有固定的规范,每个数据库都会有不同的实现方式,这里讨论的是InnoDB的MVCC。
Next-Key锁
Next-Key锁是行锁和GAP(间隙锁)的合并,行锁上文已经介绍了,接下来说下GAP间隙锁。
意向锁
六.事务是如何实现?
前面讲的重做日志,回滚日志以及锁技术就是实现事务的基础。
原子性 undo log
- 事务的原子性是通过 undo log 来实现的,(undo log 保存了事物发生之前的数据的一个版本,同时提供多版本并发控制下的读(mvcc 非锁定读))
为了做到同时成功或者失败,当系统发生错误或者执行rollback操作时需要根据undo log 进行回滚。
持久性 redo log
- 事务的持久性性是通过 redo log 来实现的,redolog是在事务开始与提交前就产生了
- MySQL的redo log是在事务提交前写入,并且在提交事务时进行刷盘操作。
这是因为redo log的目的是为了保证数据的持久性,即在数据库崩溃或意外关机等异常情况下,可以通过redo log来恢复数据库中未持久化的修改。因此,为了保证持久性,redo log必须先写入到磁盘中,等待确认后才能提交事务。在确认之前,redo log中的数据可以被视为“暂存”,尚未完全融合到数据库中。
既然redo log也需要存储,也涉及磁盘IO为啥还用它?
(1)redo log 的存储是顺序存储,而缓存同步是随机操作。
(2)缓存同步是以数据页为单位的,每次传输的数据大小大于redo log。
隔离性(读写锁+mvcc)
- 事务的隔离性是通过 (读写锁+MVCC)来实现的
一致性
而事务的终极大 boss 一致性是通过原子性,持久性,隔离性来实现的。
原子性,持久性,隔离性折腾半天的目的也是为了保障数据的一致性!
总之,ACID只是个概念,事务最终目的是要保障数据的可靠性,一致性。
MVCC
MVCC概念
MVCC(Multi-Version Concurrency Control)即多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。
MVCC使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能。
在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
MVCC基本特征
-
每行数据都存在一个版本,每次数据更新时都更新该版本。
-
修改时Copy出当前版本随意修改,各个事务之间无干扰。
-
保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)。
补充
1.MVCC手段只适用于Msyql隔离级别中的读已提交(Read committed)和可重复读(Repeatable Read);
2.Read uncimmitted由于存在脏读,即能读到未提交事务的数据行,所以不适用MVCC; 原因是MVCC的创建版本和删除版本只要在事务提交后才会产生。
3.串行化由于是会对所涉及到的表加锁,并非行锁,自然也就不存在行的版本控制问题;
4.通过以上总结,可知,MVCC主要作用于事务性的,有行锁控制的数据库模型。
MVCC 实现原理
MVCC 的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View(读视图) 来实现的。
3个隐式字段
每开启一个新事务,事务的版本号就会递增
undo log 数据链表记录
Undo log 的用途
(1)保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log的数据进行恢复。
(2)用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。
不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log 的链首就是最新的旧记录,链尾就是最早的旧记录.
Read View(读视图)
准确的说,MVCC多版本并发控制指的是 “维持一个数据的多个版本,使得读写操作没有冲突” 这么一个概念。仅仅是一个理想概念
-
在RC隔离级别下,是每个快照读都会生成井获取最新的Read View
-
而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View,之后的快照读获取的都是同一个Read View.
MVCC 在可重读Repeatable reads事务隔离级别下:
- SELECT时,读取创建版本号<=当前事务版本号, 删除版本号为空或>当前事务版本号。
- INSERT时,保存当前事务版本号为行的创建版本号
- DELETE时,保存当前事务版本号为行的删除版本号
- UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的
通过MVCC,虽然每行记录都需要额外的存储空间,更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用,大多数读操作都不用加锁,读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,也只锁住必要行。
MVCC解决了什么问题
MVCC不存在幻读问题(RR级别的情况下)
当前读
当前读的实现方式:next-key锁(行记录锁+Gap间隙锁)
select...lock in share mode (共享读锁)
select...for update
update , delete , insert
当前读, 读取的是最新版本, 并且对读取的记录加锁, 阻塞其他事务同时改动相同记录,避免出现安全问题。
例如,假设要update一条记录,但是另一个事务已经delete这条数据并且commit了,如果不加锁就会产生冲突。所以update的时候肯定要是当前读,得到最新的信息并且锁定相应的记录。
快照读
像不加锁的select * from 操作就是快照读,即不加锁的非阻塞读,不涉及其他锁之间的冲突;
快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;
既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
快照读的实现方式:undolog和多版本并发控制MVCC
对于快照读,InnoDB 使用 MVCC 解决幻读,
单纯的select操作,不包括上述 select … lock in share mode, select … for update。
-
Read Committed隔离级别:每次select都生成一个快照读。
-
Read Repeatable隔离级别:开启事务后第一个select语句才是快照读的地方,而不是一开启事务就快照读。
说白了MVCC就是为了实现读(select)-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现。