目录
概述
一、事务的概念
数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作(对数据库的相关增删改查的操作),要么完全地执行,要么完全地不执行。
二、为什么需要数据库事务
银行应用是解释事务必要性的一个经典例子。假设一个银行的数据库有两张表
<1> 支票表(checking)
<2> 储蓄表(savings)
现在要从用户jane的支票账户转移200美元到她的储蓄账户,那么需要至少三个步骤
- 检查支票账户的余额高于200美元。
- 从支票账户余额中减去200美元。
- 在储蓄账户余额中增加200美元。
上述三个步骤的操作必须打包在一个事务中, 任何一个步骤失败, 则必须回滚所有的步骤。
可以用START TRANSACTION语句开启一个事务,然后要么使用COMMIT提交事务将修改的数据持久保留, 要么使用ROLLBACK撤销所有的修改。
事务SQL的样本如下:
START TRANSACTION;
SELECT balance FROM checking WHERE customer_id = 1;
UPDATE checking SET balance = balance - 200 WHERE customer_id = 1;
UPDATE savings SET balance = balance + 200 WHERE customer_id = 1;
COMMIT;
单纯的事务概念并不是故事的全部。试想一下,如果执行到第四条语句时服务器崩溃了,会发生什么? 天知道,用户可能会损失200美元。再假如,在执行到第三句语句和第四条语句之间时,另外一个进程要删除支票账户的所有余额,那么结果可能就是银行在不知道这个逻辑的情况下白白给Jane 200美元。
除非系统通过严格的ACID测试,否则空谈事务的概念是不够的。ACID表示原子性(Atomicity),一致性(Consistency),隔离性(Isolation)和持久性(Durability)。一个运行良好的事务系统,必须具备这些标准特征。
事务ACID四大原则
一、原子性(Atomicity)
事务中的所有操作作为一个整体像原子一样不可分割,要么全部成功,要么全部失败。
二、一致性(Consistency)
事务的执行结果必须使数据库从一个一致性状态到另一个一致性状态。一致性状态是指:
- 系统的状态满足数据的完整性约束(主码,参照完整性,check约束等)
- 系统的状态反应数据库本应描述的现实世界的真实状态,比如拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
三、隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。比如多个用户同时往一个账户转账,最后账户的结果应该和他们按先后次序转账的结果一样。
四、持久性(Durability)
事务一旦提交,其对数据库的更新就是持久的。任何事务或系统故障都不会导致数据丢失。
以上介绍完事务的四大特性(简称ACID),现在重点来说明下事务的隔离性,当多个线程都开启事务操作数据库中的数据时,数据库系统要能进行隔离操作,以保证各个线程获取数据的准确性,在介绍数据库提供的各种隔离级别之前,我们先看看如果不考虑事务的隔离性,会发生什么样的问题。
并发事务带来的问题
一、更新丢失(Lost Update)
更新丢失的含义如下
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题 --最后的更新覆盖了由其他事务所做的更新。
更新丢失示例
两个编辑人员制作了同一 文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同 一文件,则可避免此问题。
二、脏读(Dirty Read)
脏读的含义如下
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。例如:用户A向用户B转账100元。
SQL测试案例如下
-- 设置当前会话事务的隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
-- 开启一个事务
START TRANSACTION;
-- 事务体 用户A向用户B转账100元
UPDATE ACCOUNT SET MONEY = MONEY + 100 WHERE NAME = 'B'; -- (此时A通知B)
UPDATE ACCOUNT SET MONEY = MONEY - 100 WHERE NAME = 'A';
-- 提交一个事务
COMMIT;
当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账(此时即发生了脏读),而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转。
脏读示例
在事务A和事务B同时执行时可能会出现如下场景:
时间轴 | 事务A(存款) | 事务B(取款) |
T1 | 开始事务 | —— |
T2 | —— | 开始事务 |
T3 | —— | 查询余额(1000元) |
T4 | —— | 取出1000元(余额0元) |
T5 | 查询余额(0元) | —— |
T6 | —— | 撤销事务(余额恢复1000元) |
T7 | 存入500元(余额500元) | —— |
T8 | 提交事务 | —— |
余额应该为1500元才对。请看T5时间点,事务A此时查询的余额为0,这个数据就是脏数据,他是事务B造成的,很明显是事务没有进行隔离造成的。
三、不可重复读(Non-Repeatable Read)
不可重复读的含义如下
不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发送了不可重复读。
SQL测试案例如下
-- 设置当前会话事务的隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
-- A用户
-- 开启一个事务
START TRANSACTION
-- 事务体
-- 第一次读取,读取到A账户1000元
SELECT * FROM ACCOUNT WHERE NAME = 'A'
-- 第二次读取,读取到A账户100元
SELECT * FROM ACCOUNT WHERE NAME = 'A';
-- 提交一个事务
COMMIT;
```
```
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
START TRANSACTION
-- 更新A用户的钱
UPDATE ACCOUNT SET MONEY = 100 WHERE NAME = 'A';
COMMIT;
不可重复读示例
时间轴 | 事务A(存款) | 事务B(取款) |
T1 | 开始事务 | —— |
T2 | —— | 开始事务 |
T3 | —— | 查询余额(1000元) |
T4 | 查询余额(1000元) | —— |
T5 | —— | 取出1000元(余额0元) |
T6 | —— | 提交事务 |
T7 | 查询余额(0元) | —— |
T8 | 提交事务 | —— |
事务A其实除了查询两次以外,其它什么事情都没做,结果钱就从1000变成0了,这就是不可重复读的问题。
四、幻读(Phantom)
幻读的含义如下
幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。
SQL测试案例如下
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
CREATE TABLE ACCOUNT(name varchar(100) PRIMARY KEY , MONEY int)
INSERT INTO ACCOUNT VALUES('A', 1000);
INSERT INTO ACCOUNT VALUES('B', 1000);
-- 客户端1
-- 开启一个事务
START TRANSACTION
-- 事务体
-- Step1: 查看是否有用户C的信息
SELECT * FROM ACCOUNT WHERE NAME = 'C'
-- Step2: 如果没有用户C的信息则添加用户信息
INSERT INTO ACCOUNT VALUES('C', 1000);
-- 提交一个事务
COMMIT;
-- 客户端2
-- Step1: 插入用户C的信息
INSERT INTO ACCOUNT VALUES('C', 1000);
幻读示例
幻读就是指同样的事务操作,在前后两个时间段内执行对同一个数据项的读取,可能出现不一致的结果。
时间轴 | 事务A(统计总存款) | 事务B(存款) |
T1 | 开始事务 | —— |
T2 | —— | 开始事务 |
T3 | 统计总存款(1000元) | —— |
T4 | —— | 存入100元 |
T5 | —— | 提交事务 |
T6 | 提交总存款(10100) | —— |
T7 | 提交事 | —— |
幻读和不可重复读的区别
不可重复读的重点是修改:在同一事务中,同样的条件,第一次读的数据和第二次读的数据不一样。(因为中间有其他事务提交了修改)
幻读的重点在于新增或者删除:在同一事务中,同样的条件,,第一次和第二次读出来的记录数不一样。(因为中间有其他事务提交了插入/删除)
五、读偏差(Read Skew)
读偏差的含义如下
Skew可以理解为不一致,因此读偏差可以理解为读结果违反业务一致性,比如X、Y两个账户余额都为50,他们总和为100,事务A读X余额为50,然后事务B从X转账50到Y然后提交,事务A在B提交后读Y发现余额为100,那么它们总和变成了150,此时违反业务一致性。
读偏差示例(Read Skew)
时间轴 | 事务A | 事务B |
T1 | 开始事务 | 开始事务 |
T2 | 读取X账户的值,值为50 | X账户被转账走50 |
T3 | —— | Y账户增加了50 |
T4 | —— | 提交事务 |
T5 | —— | —— |
T6 | 读取Y账户的值,值为100 | —— |
T7 | 提交事务 | —— |
六、写偏差(Write Skew)
写偏差的含义如下
写偏差可以理解为事务commit之前写前提被破坏,导致写入了违反业务一致性的数据,网上有个很好的简称为写前提困境,也就是读出某些数据,作为另一些写入的前提条件,但是在提交前,读入的数据就已被别的事务修改并提交,这个事务并不知道,然后commit了自己的另一些写入,写前提在commit前就被修改,导致写入结果违反业务一致性。
写偏差发生在写前提与写入目标不相同的情境下。
这是业务开发中最容易出错地方,如果开发者不太理解隔离级别,也不知道目前使用的是哪个隔离级别,很可能写出有写偏差的代码,造成业务不一致。
写偏差示例(Write Skew)
信用卡系统对不同等级的会员有积分加成,3级会员则每次都3倍积分,同时,会有定时任务检查当积分不满足要求时,就会降级。
首先,会员进行了刷卡消费,此时要计算积分,开启了事务A,读到会员等级为3,与此同时定时任务也开始了,读到会员积分为2800,已经不满足3000分应该降级为2级,然后将会员等级降级为2并且commit,由于事务A读到的等级为3,它还是按照3倍积分为会员增加了积分,会员赚了,多亏那个程序员不理解他使用的事务隔离级别,出现了业务不一致。
时间轴 | 事务A | 事务B |
T1 | 开始事务 | 开始事务 |
T2 | 读到会员等级为3 | 读到会员积分为2800 |
T3 | —— | 写(等级) = 2 |
T4 | —— | 提交事务 |
T5 | 写(积分) += 普通积分 * 3 | —— |
T6 | 提交事务 | —— |
并发事务处理带来的问题的解决办法
“更新丢失”通常是应该完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。
脏读” 、 “不可重复读”和“幻读” ,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决:
- 一种是加锁:在读取数据前,对其加锁,阻止其他事务对数据进行修改。
- 另一种是数据多版本并发控制(MultiVersion Concurrency Control,简称 MVCC 或 MCC),也称为多版本数据库:不用加任何锁, 通过一定机制生成一个数据请求时间点的一致性数据快照 (Snapshot), 并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本。
隔离级别
在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。我们的数据库锁,也是为了构建这些隔离级别存在的。
隔离级别 | 脏读(Dirty Read) | 不可重复读(NonRepeatable Read) | 幻读(Phantom Read) |
未提交读(Read uncommitted) | 可能 | 可能 | 可能 |
已提交读(Read committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable ) | 不可能 | 不可能 | 不可能 |
- 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
- 提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
- 可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读
- 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞
Read Uncommitted这种级别,数据库一般都不会用。
一、Read Uncommitted(读取未提交内容)
- 所有事务都可以看到其他未提交事务的执行结果
- 本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少
- 该级别引发的问题是——脏读(Dirty Read):读取到了未提交的数据
二、Read Committed(读取提交内容)
- 这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)
- 它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变
- 这种隔离级别出现的问题是——不可重复读(Nonrepeatable Read):不可重复读意味着我们在同一个事务中执行完全相同的select语句时可能看到不一样的结果。导致这种情况的原因可能有:
- 有一个交叉的事务有新的commit,导致了数据的改变;
- 一个数据库被多个实例操作时,同一事务的其他实例在该实例处理其间可能会有新的commit
三、Repeatable Read(可重读)
- 这是MySQL的默认事务隔离级别
- 它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行
- 此级别可能出现的问题——幻读(Phantom Read):当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行
- InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决幻读问题;InnoDB还通过间隙锁解决幻读问题
四、Serializable(可串行化)
- 这是最高的隔离级别
- 它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。MySQL锁总结
- 在这个级别,可能导致大量的超时现象和锁竞争
结论
本文主要介绍了数据库的事务的ACID原则、并发事务带来的问题、事务的隔离级别。事务是数据库非常重要的核心功能。在数据安全方面有着非常重要的作用。计划和事物是数据库的倚天剑和屠龙刀,可想而知事物在数据库中起着何等重要的作用。
参考资料
1、《高性能MySQL》

微信公众号名称:技术茶馆
微信公众号ID : Night_ZW