有道无术,术尚可求,有术无道,止于术。
本系列Spring Boot 版本 3.1.0
本系列Seata 版本 2.0.0
源码地址:https://gitee.com/pearl-organization/study-seata-demo
文章目录
- 1. 概述
- 2. ACID 模型
- 2.1 原子性
- 2.2 一致性
- 2.3 隔离性
- 2.3.1 事务并发异常
- 2.3.1.1 第一类更新丢失(回滚丢失)
- 2.3.1.2 第二类更新丢失(覆盖丢失)
- 2.3.1.3 脏读
- 2.3.1.4 不可重复读
- 2.3.1.5 幻读
- 2.3.2 隔离级别
- 2.3.2.1 读未提交 Read uncommitted
- 2.3.2.2 读已提交 Read committed
- 2.3.2.3 可重复度 Repeatable read
- 2.3.2.4 序列化 Serializable
- 2.4 持久性
1. 概述
在计算机术语中,事务(Transaction)是指多个数据库操作组成一个不可分割的执行单元,这些操作要么全部成功,要么全部失败,不会结束在中间的某个环节,同时在这些操作执行后,需要保证数据可靠且一致。
例如在MySql中使用事务:
-- 开启事务
START TRANSACTION;
-- 一些列操作
INSERT INTO user(username,password) VALUES ('root','123456'); -- 插入
UPDATE t_user SET username=’cangJJ’ WHERE id=1; -- 更新-- 提交事务
COMMIT;
2. ACID 模型
ACID模型是指数据库中的事务必须满足原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)四大特性,是数据库设计的基本原则,以保证数据的可靠一致,即便在软件崩溃、硬件故障的情况下,数据也不会被破坏。
其中原子性、隔离性、持久性是为一致性服务的,即AID是手段,C是最终实现的目标。
2.1 原子性
原子性也可称为不可分割性,是说事务操作必须是原子工作单元,一个事务中的操作要么全部都执行成功,要么全部都回滚撤销执行,不会存在部分成功或失败的情况。
例如经典的银行转账场景中,转账操作可以分为以下两步:
- 从
A账户中扣除金额 - 将扣除金额存入到
B账户

整个转账操作过程可以看作是一个事务,原子性要求成功则全部成功,失败则需要全部撤消。如果第一步成功,第二步失败,整个操作应该取消,若不取消第一步的操作,则会发生丢失款项的问题。
2.2 一致性
一致性是指一个事务执行之前和执行之后都必须处于一致性状态。
2.3 隔离性
隔离性是指当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
2.3.1 事务并发异常
数据库是要被共享访问的,那么在并发操作数据库过程中很可能出现以下很多异常情况。
2.3.1.1 第一类更新丢失(回滚丢失)
比如下图中,T1、T2同时执行,由于没有事务隔离,虽然T1更新成功,但是T2进行了回滚,余额却没有变化,这种因为回滚导致另外一个事务操作丢失的情况叫做回滚丢失。

2.3.1.2 第二类更新丢失(覆盖丢失)
比如下图中,T1、T2同时执行,由于没有事务隔离,T1、T2都提交了事务,但是由于T2后执行,将T1事务的操作覆盖了,导致更新丢失问题。

2.3.1.3 脏读
脏读是因为一个事务读取了另一个事务修改了但是未提交的数据。
比如下图中,T1修改余额为1100,此时T2开始事务查询到的结果为1100,由于读取到了未提交的事务,当T1回滚时,T2还在脏数据上操作,会导致结果错误。

2.3.1.4 不可重复读
不可重复读是指一个事务对同一行数据执行了两次或更多次查询,但是却得到了不同的结果。
比如下图中,在T2中,多次查询的不一致,如果这个时候修改,就会造成数据错误。

2.3.1.5 幻读
幻读和不可重复读有点像,两者都表现为两次读取的结果不一致,只是针对的不是数据的值而是数据的数量。不可重复读重点在于update和delete,而幻读的重点在于insert。
如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会 发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。

2.3.2 隔离级别
数据并发访问所产生的问题,在有些场景下可能是允许的,但是有些场景下可能就是致命的,数据库通常会通过锁机制来解决数据并发访问问题,
直接使用锁是非常麻烦的,为此数据库为用户提供了自动锁机制,只要用户指定会话的事务隔离级别,数据库就会通过分析SQL语句然后为事务访问的资源加上合适的锁,此外,数据库还会维护这些锁通过各种手段提高系统的性能,这些对用户来说都是透明的。ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别,如下表所示:

事务隔离级别和数据访问的并发性是对立的,事务隔离级别越高并发性就越差。所以要根据具体的应用来确定合适的事务隔离级别,这个地方没有万能的原则。
可以通过以下SQL 查询和修改会话级别:
-- 查询数据库版本
SELECT VERSION();
-- 查询当前会话事务界别
SELECT @@session.tx_isolation;
-- 设置当前会话事务隔离级别,仅仅该窗口会话有效
set session tx_isolation='read-uncommitted';
2.3.2.1 读未提交 Read uncommitted
读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据,这个级别,几乎没有任何隔离性,一般不会采用这个隔离级别。
2.3.2.2 读已提交 Read committed
读提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数据。另外一个事务不能读取该事务未提交的数据。
在Navicat 中,打开两个会话窗口,修改隔离级别为读已提交:
-- 设置当前会话事务隔离级别,仅仅该窗口会话有效
set session tx_isolation='Read-committed';
开启事务,将当前账户余额扣掉100,不提交事务:
START TRANSACTION;
UPDATE account_tbl
SET money = money - 100
WHEREid = "11111111";
在另外一个窗口,查询当余额:
SELECT*
FROMaccount_tbl
WHEREid = "11111111";
可以看到读取到的余额仍为1000,说明该隔离级别下,事务读取不到另外一个事务未提交的数据。

分析:这就是读已提交,若有事务对数据进行更新(UPDATE)操作时,读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题。但是存在不可重复读问题,一旦事务提交后,另外的事务再次查询,查询结果会不一致。
2.3.2.3 可重复度 Repeatable read
可重复读,就是在开始读取数据(事务开启)时,多次读取到的结果是一致的,这是Mysql 默认的事务隔离级别。
比如我们先开启一个事务,读取数据,此时查询结果为1000:

另外一个事务,修改当前数据,并提交事务:

查询事务中,再次查询,发现余额没有变,这就是可重复读,Repeatable Read的确可以解决“不可重复读”的问题。
2.3.2.4 序列化 Serializable
Serializable是最严格的隔离级别。在Serializable隔离级别下,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。
虽然Serializable隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用Serializable隔离级别。
2.4 持久性
持久性是指事务一旦被提交,对数据库中的数据的改变就是永久性的,不会因为任何情况回到原来的状态。