一、前言
MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说:银行系统,一个人转钱给另外一个人,这个过程应该放到一个事务中,不能因为其中一个环节失败,而导致钱的丢失,要么全部成功,要么全部失败。
下面带大家了解下数据库事务的特点、事务的隔离级别以及并发事务所带来的问题等等。
二、正文
1.事务的四个特性?
从业务角度出发,对数据库的一组操作要求保持4个特征:
- Atomicity(原子性):一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作。
- Consistency(一致性):数据库总是从一个一致性状态转换到另一个一致状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。下面的银行列子会说到。
- Isolation(隔离性):通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。注意这里的“通常来说”,后面的事务隔离级级别会说到。
- Durability(持久性):一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。(持久性的安全性与刷新日志级别也存在一定关系,不同的级别对应不同的数据安全级别。)
为了更好地理解ACID,以银行账户转账为例:
START TRANSACTION;
SELECT balance FROM checking WHERE customer_id = 10233276;
UPDATE checking SET balance = balance - 200.00 WHERE customer_id = 10233276;
UPDATE savings SET balance = balance + 200.00 WHERE customer_id = 10233276;
COMMIT;
- 原子性:要么完全提交(10233276的checking余额减少200,savings 的余额增加200),要么完全回滚(两个表的余额都不发生变化)
- 一致性:这个例子的一致性体现在 200元不会因为数据库系统运行到第3行之后,第4行之前时崩溃而不翼而飞,因为事务还没有提交。
- 隔离性:允许在一个事务中的操作语句会与其他事务的语句隔离开,比如事务A运行到第3行之后,第4行之前,此时事务B去查询checking余额时,它仍然能够看到在事务A中被减去的200元(账户钱不变),因为事务A和B是彼此隔离的。在事务A提交之前,事务B观察不到数据的改变。
- 持久性:这个很好理解。
- 事务的隔离性是通过锁、MVCC等实现
- 事务的原子性、一致性和持久性则是通过事务日志实现(见下)
ACID 之间的关系
事务的 ACID 特性概念很简单,但不好理解,主要是因为这几个特性不是一种平级关系:
- 只有满足一致性,事务的结果才是正确的。
- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
- 事务满足持久化是为了能应对数据库崩溃的情况。
2.并发事务带来的问题
- 更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题 ,最后的更新覆盖了由其他事务所做的更新。例如,两个编辑人员制作了同一 文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。 最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同 一文件,则可避免此问题。
例如:
T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。
- 脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务完成并提交前, 这条记录的数据就处于不一致状态; 这时, 另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做"脏读"。
列如:
T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。
-
不可重复读(Non-Repeatable Reads):一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读” 。
注:在 InnoDB 存储引擎中,SELECT 操作的不可重复读问题通过 MVCC 得到了解决,而 UPDATE、DELETE 的不可重复读问题是通过 Record Lock (行锁) 解决的,INSERT 的不可重复读问题是通过 Next-Key Lock(Record Lock + Gap Lock)解决的。
例如:
T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
- 幻读 (Phantom Reads): 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读” 。
幻读和不可重复读的区别:
- 不可重复读的重点是修改:在同一事务中,同样的条件,第一次读的数据和第二次读的数据不一样。(因为中间有其他事务提交了修改)
- 幻读的重点在于新增或者删除:在同一事务中,同样的条件,,第一次和第二次读出来的记录数不一样。(因为中间有其他事务提交了插入/删除)
并发事务处理带来的问题的解决办法:
-
“更新丢失”通常是应该完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。
-
“脏读” 、 “不可重复读”和“幻读” ,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决:
(1)一种是加锁:在读取数据前,对其加锁,阻止其他事务对数据进行修改。
(2)另一种是数据多版本并发控制(MultiVersion Concurrency Control,简称 MVCC 或 MCC),也称为多版本数据库:不用加任何锁, 通过一定机制生成一个数据请求时间点的一致性数据快照 (Snapshot), 并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本。
更详细的关于MVCC讲解请看:MVCC详解
SQL标准定义了4类隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
3.事务的隔离级别
第1级别:Read Uncommitted(未提交读)
-
所有事务都可以看到其他未提交事务的执行结果
-
本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少
-
该级别引发的问题是:
(1)脏读(Dirty Read):读取到了未提交的数据。
(2)不可重复读
(3)幻影读
第2级别:Read Committed(提交读)
-
这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)
-
它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变
-
这种隔离级别出现的问题是:
(1)不可重复读(Nonrepeatable Read):不可重复读意味着我们在同一个事务中执行完全相同的select语句时可能看到不一样的结果。导致这种情况的原因可能有:
(1)有一个交叉的事务有新的commit,导致了数据的改变;
(2)一个数据库被多个实例操作时,同一事务的其他实例在该实例处理其间可能会有新的commit
(2)幻影读
第3级别:Repeatable Read(可重复读)
- 这是MySQL的默认事务隔离级别
- 它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行
- 此级别可能出现的问题——幻读(Phantom Read):当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行
- InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决不可重复读问题;InnoDB还通过间隙锁解决幻读问题
第4级别:Serializable(可串行化)
- 这是最高的隔离级别
- 它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。
- 在这个级别,可能导致大量的超时现象和锁竞争
隔离级别和会导致的问题的比较
各具体数据库并不一定完全实现了上述 4 个隔离级别,例如:
-
Oracle 只提供 Read committed 和 Serializable 两个标准隔离级别,另外还提供自己定义的 Read only 隔离级别;
-
SQL Server 除支持上述 ISO/ANSI SQL92 定义的 4 个隔离级别外,还支持一个叫做“快照”的隔离级别,但严格来说它是一个用 MVCC 实现的 Serializable 隔离级别。
-
MySQL 支持全部 4 个隔离级别,但在具体实现时,有一些特点,比如在一些隔离级别下是采用 MVCC一致性读,但某些情况下又不是。
-
Mysql可以通过执行 set transaction isolation level命令来设置隔离级别,新的隔离级别会在下一个事务开始的时候生效。 例如: set session transaction isolation level read committed
上面提到了提到了行锁(Record Lock),间隙锁(Gap Lock)等,下面来详细说下。
4.锁算法
Record Lock(行锁)
锁定一个记录上的索引,而不是记录本身。
如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。
Gap Lock(间隙锁)
锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。
SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
Next-Key Lock
它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间:
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)
在 InnoDB 存储引擎中,SELECT 操作的不可重复读问题通过 MVCC 得到了解决,而 UPDATE、DELETE 的不可重复读问题通过 Record Lock 解决,INSERT 的不可重复读问题是通过 Next-Key Lock(Record Lock + Gap Lock)解决的。
5.事务日志?
事务日志可以帮助提高事务效率:
- 使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘。
- 事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快得多。
- 事务日志持久以后,内存中被修改的数据在后台可以慢慢刷回到磁盘。
- 如果数据的修改已经记录到事务日志并持久化,但数据本身没有写回到磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这一部分修改的数据。
目前来说,大多数存储引擎都是这样实现的,我们通常称之为预写式日志(Write-Ahead Logging),修改数据需要写两次磁盘。
Mysql中的事务实现原理
事务的实现是基于数据库的存储引擎。不同的存储引擎对事务的支持程度不一样。mysql中支持事务的存储引擎有innoDB和NDB。
innoDB是mysql默认的存储引擎,默认的隔离级别是RR(Repeatable Read),并且在RR的隔离级别下更进一步,通过多版本并发控制(MVCC,Multiversion Concurrency Control )解决不可重复读问题,加上间隙锁(也就是并发控制)解决幻读问题。因此innoDB的RR隔离级别其实实现了串行化级别的效果,而且保留了比较好的并发性能。
事务的隔离性是通过锁实现,而事务的原子性、一致性和持久性则是通过事务日志实现。说到事务日志,不得不说的就是redo和undo。
1.redo log
在innoDB的存储引擎中,事务日志通过重做(redo)日志和innoDB存储引擎的日志缓冲(InnoDB Log Buffer)实现。事务开启时,事务中的操作,都会先写入存储引擎的日志缓冲中,在事务提交之前,这些缓冲的日志都需要提前刷新到磁盘上持久化,这就是DBA们口中常说的“日志先行”(Write-Ahead Logging)。当事务提交之后,在Buffer Pool中映射的数据文件才会慢慢刷新到磁盘。此时如果数据库崩溃或者宕机,那么当系统重启进行恢复时,就可以根据redo log中记录的日志,把数据库恢复到崩溃前的一个状态。未完成的事务,可以继续提交,也可以选择回滚,这基于恢复的策略而定。
在系统启动的时候,就已经为redo log分配了一块连续的存储空间,以顺序追加的方式记录Redo Log,通过顺序IO来改善性能。所有的事务共享redo log的存储空间,它们的Redo Log按语句的执行顺序,依次交替的记录在一起。如下一个简单示例:
记录1:<trx1, insert…>
记录2:<trx2, delete…>
记录3:<trx3, update…>
记录4:<trx1, update…>
记录5:<trx3, insert…>
2.undo log
undo log主要为事务的回滚服务。在事务执行的过程中,除了记录redo log,还会记录一定量的undo log。undo log记录了数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据undo log进行回滚操作。单个事务的回滚,只会回滚当前事务做的操作,并不会影响到其他的事务做的操作。
以下是undo+redo事务的简化过程
假设有2个数值,分别为A和B,值为1,2
-
start transaction;
-
记录 A=1 到undo log;
-
update A = 3;
-
记录 A=3 到redo log;
-
记录 B=2 到undo log;
-
update B = 4;
-
记录B = 4 到redo log;
-
将redo log刷新到磁盘
-
commit
在1-8的任意一步系统宕机,事务未提交,该事务就不会对磁盘上的数据做任何影响。如果在8-9之间宕机,恢复之后可以选择回滚,也可以选择继续完成事务提交,因为此时redo log已经持久化。若在9之后系统宕机,内存映射中变更的数据还来不及刷回磁盘,那么系统恢复之后,可以根据redo log把数据刷回磁盘。
所以,redo log其实保障的是事务的持久性和一致性,而undo log则保障了事务的原子性。
5.Mysql中的事务使用
MySQL的服务层不管理事务,而是由下层的存储引擎实现。比如InnoDB。
MySQL支持本地事务的语句:
START TRANSACTION | BEGIN [WORK]
COMMIT [WORK] [AND [NO] CHAIN] [[NO] RELEASE]
ROLLBACK [WORK] [AND [NO] CHAIN] [[NO] RELEASE]
SET AUTOCOMMIT = {0 | 1}
- START TRANSACTION 或 BEGIN 语句:开始一项新的事务。
- COMMIT 和 ROLLBACK:用来提交或者回滚事务。
- CHAIN 和 RELEASE 子句:分别用来定义在事务提交或者回滚之后的操作,CHAIN 会立即启动一个新事物,并且和刚才的事务具有相同的隔离级别,RELEASE 则会断开和客户端的连接。
- SET AUTOCOMMIT 可以修改当前连接的提交方式, 如果设置了 SET AUTOCOMMIT=0,则设置之后的所有事务都需要通过明确的命令进行提交或者回滚
事务使用注意点:
- 如果在锁表期间,用 start transaction 命令开始一个新事务,会造成一个隐含的 unlock tables 被执行。
- 在同一个事务中,最好不使用不同存储引擎的表,否则 ROLLBACK 时需要对非事
务类型的表进行特别的处理,因为 COMMIT、ROLLBACK 只能对事务类型的表进行提交和回滚。 - 和 Oracle 的事务管理相同,所有的 DDL 语句是不能回滚的,并且部分的 DDL 语句会造成隐式的提交。
- 在事务中可以通过定义 SAVEPOINT(例如:mysql> savepoint test; 定义 savepoint,名称为 test),指定回滚事务的一个部分,但是不能指定提交事务的一个部分。对于复杂的应用,可以定义多个不同的 SAVEPOINT,满足不同的条件时,回滚
不同的 SAVEPOINT。需要注意的是,如果定义了相同名字的 SAVEPOINT,则后面定义的SAVEPOINT 会覆盖之前的定义。对于不再需要使用的 SAVEPOINT,可以通过 RELEASE SAVEPOINT 命令删除 SAVEPOINT, 删除后的 SAVEPOINT, 不能再执行 ROLLBACK TO SAVEPOINT命令。
自动提交(autocommit):
Mysql默认采用自动提交模式,可以通过设置autocommit变量来启用或禁用自动提交模式
- 隐式锁定
InnoDB在事务执行过程中,使用两阶段锁协议:
随时都可以执行锁定,InnoDB会根据隔离级别在需要的时候自动加锁;
锁只有在执行commit或者rollback的时候才会释放,并且所有的锁都是在同一时刻被释放。
- 显式锁定
InnoDB也支持通过特定的语句进行显示锁定(存储引擎层):
select ... lock in share mode //共享锁
select ... for update //排他锁
MySQL Server层的显示锁定:
lock table和unlock table
三、总结
本章首先数据库事务的4个特性,然后讲解了并发事务带来的问题,事务的隔离级别以及每种级别下所产生的问题,最后介绍了事务日志和事务的实现原理。
最后引用我很佩服的一个人经常说的话:你知道的越多,你不知道的越多!
文章参考:
https://zhuanlan.zhihu.com/p/29166694
https://mp.weixin.qq.com/s/J3kCOJwyv2nzvI0_X0tlnA