事务管理简介
事务(Transaction
) 通俗的讲就是一件事件,要么做完,要么不做,不要做一半留一半。也就是说,事务必须是一个不可分割的整体。
1. 事务特性
1.1 原子性(Atomicity
)
事务必须是一个不可分割的整体,要么全部执行,要么不执行。
1.2. 一致性(Consistency
)
事务开始前和结束后,数据库的完整性约束没有被破坏 。比如 A 向 B 转账,不可能 A 扣了钱,B 却没收到。
1.3. 隔离性(Isolation
)
同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任务干扰。
1.4. 持久性(Durability
)
事务完成后,事务对数据库的所有更改将被保存到数据库中。
2. 事务隔离级别
数据库定义了4个隔离的级别:
READ_UNCOMMITTED
读未提交READ_COMMITTED
读提交REPEATABLE_READ
重复读SERIALIZABLE
序列化
从上往下,级别越来越高,并发性越来越差,一致性越来越高。
3. 事务面临的问题
数据库在高并发下会产生下列问题:
Dirty Read
脏读Unrepeatable Read
不可重复读Phantom Read
幻读
3.1 脏读
脏读可以理解为读到 “垃圾数据”了。比如有2个事务同时对数据库进行操作,会发生竞争,如下表所示:
时间 | 事务A(存款) | 事务B(取款) |
---|---|---|
T1 | 开始事务 | – |
T2 | – | 开始事务 |
T3 | – | 查询余额(1000元) |
T4 | – | 取出1000(余额0元) |
T5 | 查询余额(0元) | – |
T6 | – | 撤销事务(余额恢复为1000元) |
T7 | 存入500元(余额500元) | – |
T8 | 提交事务 | – |
余额应该为1500元,但是T5时间点,事务A查询余额为0元,这个数据就是脏数据,它是事务B造成的。所以脏数据是非常要不得的。
3.2 不可重复读
时间 | 事务A(存款) | 事务B(取款) |
---|---|---|
T1 | 开始事务 | – |
T2 | – | 开始事务 |
T3 | – | 查询余额(1000元) |
T4 | 查询余额(1000元) | – |
T5 | – | 取出1000元(余额0元) |
T6 | – | 提交事务 |
T7 | 查询余额(0元) | – |
从上述表格中看到,事务A除了查询操作2次以外,其他什么事情都没有做,结果钱就从1000变为0了,这就是重复读。
3.3 幻读
时间 | 事务A(统计) | 事务B(存款) |
---|---|---|
T1 | 开始事务 | – |
T2 | – | 开始事务 |
T3 | 统计总存款(1000元) | – |
T4 | – | 存入100元 |
T5 | – | 提交事务 |
T6 | 统计总存款(10100元) | – |
事务A每次统计的结果都是不一样的,就好像产生了幻觉一样。
小结:
不可重复读和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需要锁住满足条件的行,解决幻读需要锁表。
不同的隔离级别解决并发问题:
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ_UNCOMMITTED | 允许 | 允许 | 允许 |
READ_COMMITTED | 禁止 | 允许 | 允许 |
REPEATABLE_READ | 禁止 | 禁止 | 允许 |
SERIALIZABLE | 禁止 | 禁止 | 禁止 |
MySQL 默认的隔离级别是:REPEATABLE_READ(重复读)
4. Spring 的事务传播行为
前提条件:
- 方法A有事务,方法B也有事务
- 方法A有事务,方法B没有事务
- 方法A没有事务,方法B有事务
- 方法A没有事务,方法B也没有事务
如上图所示:FooService
的业务方法的传播行为被我们指定为 Required
,表示如果当前存在事务的话,则加入当前事务。
-
FoobarService
在调用FooService
业务方法的时候已经启动了一个事务,所以,FooService
的业务方法会直接加入FooBarService
启动的事务1中。 -
BarService
的业务方法的传播行为被指定为RequiredNew
,表示无论当前是否存在事务,都需要为其重新启动一个事务,所以它使用的是自己启动的事务2.
Spring 提供了7种事务传播行为:
-
PROPAGATION_REQUIRED
(默认)如果没有,就新建一个事务;如果有,就加入当前事务。总之,要至少保证在一个事务中运行。
-
PROPAGATION_REQUIRES_NEW
如果没有,就新建一个事务;如果有,就将当前事务挂起。意思就是创建一个事务,和原来的事务没有关系了。场景:如果某个业务对象所做的事情不想影响到外层事务,REQUIRES_NEW是合适的选择。
-
PROPAGATION_NESTED
如果没有,就新建一个事务;如果有,就在当前事务中嵌套其他事务。所嵌套的子事务和主事务之间是有关联的(当主事务提交或回滚,子事务也会提交或回滚)。
-
PROPAGATION_SUPPORTS
如果没有,就以非事务方式执行;如果有,就使用当前事务。
对于一些查询方法来说,SUPPORTS 通常是比较合适的传播行为。如果当前方法直接执行,那么不需要事务的支持;如果当前方法被其他方法调用,而其他方法启动了一个事务,使用 SUPPORTS 可以保证当前方法能够加入当前事务,并且洞察当前事务对数据资源所做的更新。
-
PROPAGATION_NOT_SUPPORTS
如果没有,就以非事务方式执行;如果有,就将当前事务挂起(但也要看对应的事务管理器实现类是否支持事务的挂起)。
-
PROPAGATION_NEVER
如果没有,就以非事务方式执行;如果有,就抛出异常。
-
PROPAGATION_MANDATORY
如果没有,就抛出异常;如果有,就使用当前事务。
Spring 提供了一些小的附加功能:
- 事务超时(
Transaction Timeout
) 为了解决事务时间太长,消耗太多资源的问题,所以故意给事务设置一个最大时长,如果超时了,就回滚事务。 - 只读事务(
Readonly Transaction
) 为了忽略哪些不需要事务的方法,比如读取数据,就样可以有效地提高一些性能。