数据库事务的ACID和Spring事务传播

1.1 ACID
数据库事务正确执行的四个基本要素的缩写,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)
由于一项操作通常会包含许多子操作,而这些子操作可能会因为硬件的损坏或其他因素产生问题,要正确实现ACID并不容易。ACID建议数据库将所有需要更新以及修改的资料一次操作完毕,但实际上并不可行。
目前主要有两种方式实现ACID:
第一种是Write ahead logging,也就是日志式的方式。
第二种是Shadow paging。
1.1.1 原子性(Atomicity)
整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样
1.1.2 一致性(Consistency)
在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏
1.1.3 隔离性(Isolation)
隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。
1.1.4 持久性(Durability)
就是说任何执行成功的事务修改后的数据都要保证永久的保存在数据库系统中,能够时刻记录系统的状态,并且保证发生故障后能够恢复
write ahead logging:SQL Server中使用了WAL(Write-Ahead Logging)技术来保证事务日志的ACID特性,在数据写入到数据库之前,先写入到日志,再将日志记录变更到存储器中
1.2 四个事务隔离级别
隔离级别对比

| 隔离级别 | 脏读 | 可重复读 | 幻读 |
|---|---|---|---|
| 未提交读取 | 存在 | 不可以 | 存在 |
| 提交读取 | 不存在 | 不可以 | 存在 |
| 可重复读取 | 不存在 | 可以 | 存在 |
| 串行化 | 不存在 | 可以 | 不存在 |
事务隔离级别越高,就越能保证数据的完整性和一致性,但是同时对并发性能的影响也越大
DEFAULT:采用 DB 默认的事务隔离级别:
MySql 默认为: 可重复读
Oracle 默认为: 读已提交
1.2.1 未授权读
未授权读也被称为对未提交(Read Uncommitted),该隔离级别允许脏读,这个隔离级别最低。就是说,如果一个事务正在处理某一个数据,并对其进行了更新,但是还没完成事务,事务还没有提交;而此时,允许另一个事务也能访问到该数据。例子:事务A和事务B同时进行,事务A在整个执行阶段,会将数据项的值从1开始,做一系列的加法动作(比如说加1操作)直到变成10后再进行事务的提交,此时,事务B能够看到这个数据项在事务A操作中的所有中间值(如1变成2,2变成3等),而对这一系列的中间值的读取就是未授权读取。
1.2.2 授权读取
授权读取也被称为读已提交(Read Committed),它和未授权读的区别是授权读取只允许已经被提交的数据。例子:事务B无法看到这个数据项在事务A操作过程中的所有中间值,只能看到最终的10。另外,如果有一个事务C和事务A进行非常相似的操作,只是事务C是将数据项从10加到20,此时事务B也同样可以读到20,即授权读取允许不可重复读。
1.2.3 可重复读
可重复读取(Repeatable Read),简单地说,就是保证在事务处理过程中,多次读取同一个数据时,其值和事务开始的时候是一致的。因此该事务级别禁止了不可重复读取和脏读,但是有可能出现幻影数据。就是指同样的事务操作,由于别的事务增加了记录,致使两次看到的数量不一样。
1.2.4 串行化
串行化(Serializable)是最严格的事务隔离级别。要求所有事务都被串行执行,即事务只能一个接一个地进行处理,不能并发执行。
1.3 Spring 事务的传播属性
所谓事务传播行为是指,处于不同事务中的方法在相互调用时,执行期间事务的维护情况。如,A 事务中的方法 a() 调用 B 事务中的方法 b(),在调用执行期间事务的维护情况,就称为事务传播行为。事务传播行为是加在方法上的。
定义在存在多个事务同时存在的时候,spring应该如何处理这些事务的行为。这些属性在TransactionDefinition中定义,具体常量的解释见下表:
| 常量名称 | 常量解释 |
|---|---|
| PROPAGATION_REQUIRED | 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择,也是 Spring 默认的事务的传播。 |
| PROPAGATION_REQUIRES_NEW | 总是新建事务,如果当前存在事务,把当前事务挂起。新建的事务将和被挂起的事务没有任何关系,是两个独立的事务,外层事务失败回滚之后,不能回滚内层事务执行的结果,内层事务失败抛出异常,外层事务捕获,也可以不处理回滚操作 |
| PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
| PROPAGATION_MANDATORY | 支持当前事务,如果当前没有事务,就抛出异常。 |
| PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
| PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
| PROPAGATION_NESTED | 如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按REQUIRED属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效 |
短文版:
REQUIRED:指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事务。这种传播行为是最常见的选择,也是 Spring 默认的事务传播行为。
SUPPORTS:指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。
MANDATORY:指定的方法必须在当前事务内执行,若当前没有事务,则直接抛出异常。
REQUIRES_NEW:总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。
NOT_SUPPORTED:指定的方法不能在事务环境中执行,若当前存在事务,就将当前事务挂起。
NEVER:指定的方法不能在事务环境下执行,若当前存在事务,就直接抛出异常。
NESTED:指定的方法必须在事务内执行。若当前存在事务,则在嵌套事务内执行;若当前没有事务,则创建一个新事务。
2.1 Spring 事务概述
事务原本是数据库中的概念,用于数据访问层。但一般情况下,需要将事务提升到业务层,即 Service 层。这样做是为了能够使用事务的特性来管理具体的业务。
在 Spring 中通常可以通过以下三种方式来实现对事务的管理:
使用 Spring 的事务代理工厂管理事务(已过时)
使用 Spring 的事务注解管理事务
使用 AspectJ 的 AOP 配置管理事务
2.2 Spring 事务管理 API
Spring 的事务管理,主要用到两个事务相关的接口。
2.2.1 事务管理器接口
事务管理器是 PlatformTransactionManager 接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息。
该接口定义了 3 个事务方法:
void commit(TransactionStatus status):事务的提交
TransactionStatus getTransaction(TransactionDefinition definition):获取事务的状态
void rollback(TranscationStatus status):事务的回滚
(1)常用的两个实现类
PlatformTransactionManager 接口有两个常用的实现类:
DataSourceTransactionManager:使用 JDBC 或 MyBatis 进行持久化数据时使用。
HibernateTransactionManager:使用 Hibernate 进行持久化数据时使用。
(2)Spring 的回滚方式 Spring 事务的默认回滚方式是:发生运行时异常回滚
2.2.2 事务定义接口
事务定义接口 TransactionDefinition 中定义了事务描述相关的三类常量:事务隔离级别、事务传播行为、事务默认超时时限,及对它们的操作
3.1 Spring 注解管理事务概述
通过 @Transactional
注解方式,也可将事务织入到相应方法中。而使用注解方式,只需在配置文件中加入一个 tx 标签,以告诉 Spring 使用注解来完成事务的织入。该标签只需指定一个属性,事务管理器。
<!-- 开启事务注解驱动 -->
<tx:annotation-driven transaction-manager="transactionManager" />
3.2 @Transactional 注解简介
@Transactional
的所有可选属性:
propagation:用于设置事务传播属性。该属性类型为 Propagation 枚举,默认值为 Propagation.REQUIRED。
isolation:用于设置事务的隔离级别。该属性类型为 Isolation 枚举 ,默认值为 Isolation.DEFAULT。
readOnly:用于设置该方法对数据库的操作是否是只读的。该属性为 boolean,默认值为 false。
timeout:用于设置本操作与数据库连接的超时时限。单位为秒,类型为 int,默认值为 -1,即没有时限。
rollbackFor:指定需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
rollbackForClassName:指定需要回滚的异常类类名。类型为 String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
noRollbackFor:指定不需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
noRollbackForClassName:指定不需要回滚的异常类类名。类型为 String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
需要注意的是,@Transactional
若用在方法上,只能用于 public 方法上。对于其他非 public
方法,如果加上了注解 @Transactional
,虽然 Spring 不会报错,但不会将指定事务织入到该方法中。因为 Spring 会忽略掉所有非 public 方法上的 @Transaction
注解。
若 @Transaction
注解在类上,则表示该类上所有的方法均将在执行时织入事务。
3.3 使用 @Transaction 注解
使用起来很简单,只需要在需要增加事务的业务类上增加 @Transaction 注解即可,案例代码如下:
package com.hello.spring.transaction.aspectsj.aop.service.impl;
import com.hello.spring.transaction.aspectsj.aop.dao.TbContentCategoryDao;
import com.hello.spring.transaction.aspectsj.aop.domain.TbContent;
import com.hello.spring.transaction.aspectsj.aop.domain.TbContentCategory;
import com.hello.spring.transaction.aspectsj.aop.service.TbContentCategoryService;
import com.hello.spring.transaction.aspectsj.aop.service.TbContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Transactional
@Service(value = "tbContentCategoryService")
public class TbContentCategoryServiceImpl implements TbContentCategoryService {
@Autowired
private TbContentCategoryDao tbContentCategoryDao;
@Autowired
private TbContentService tbContentService;
public void save(TbContentCategory tbContentCategory, TbContent tbContent) {
tbContentCategoryDao.insert(tbContentCategory);
tbContentService.save(tbContent);
}
}
4.1 spring事务
spring事务本质上使用数据库事务,而数据库事务本质上使用数据库锁,所以spring事务本质上使用数据库锁,开启spring事务意味着使用数据库锁;
当将数据库的隔离级别定义为某一级别后如仍不能满足要求,可以自定义 sql 的锁来覆盖事务隔离级别默认的锁机制。
spring事务实际使用AOP拦截注解方法,然后使用动态代理处理事务方法,捕获处理过程中的异常,spring事务其实是把异常交给spring处理;
spring事务只有捕获到异常才会终止或回滚,如果你在程序中try/catch后自己处理异常而没有throw,那么事务将不会终止或回滚,失去事务本来的作用;
spring事务会捕获所有的异常,但只会回滚数据库相关的操作,并且只有在声明了rollbackForClassName="Exception"之类的配置才会回滚;
spring事务会回滚同一事务中的所有数据库操作,本质上是回滚同一数据库连接上的数据库操作;
4.2 spring事务与数据库锁总结
spring事务本质上使用数据库锁:
spring事务只有在方法执行过程中出现异常才会回滚,并且只回滚数据库相关的操作
对象锁和spring事务的对比:
对象锁可以保证数据一致性和业务逻辑正确性,但不能保证并发性;
spring事务不能严格保证数据一致性和业务逻辑正确性,但具有较好的并发性,因为只锁数据库行数据;
建议:
如果只有insert操作,可以使用事务
如果涉及update操作但不涉及其他业务逻辑,可以保守使用事务
如果涉及update操作及其他业务逻辑,慎用事务,并且数据库查询跟数据库更新之间尽量间隔较短,中间不宜插入太多其他逻辑,减少数据一致性的风险
对数据一致性要求不高的情况下可以使用事务结合乐观锁,否则建议用锁
spring事务不能保证数据一致性和业务逻辑正确性:
如果事务方法抛异常,此时会回滚数据库操作,但已经执行的其他方法不会回滚,因此无法保证业务逻辑正确性
即使事务方法不抛异常,也不能保证数据一致性(因为事务接口里的数据库操作在整个接口逻辑执行结束后才提交到数据库,在接口最后提交到数据库的前后很有可能带来数据一致性的问题),从而不能保证业务逻辑正确性
参考文档:
[1] zjj972326230.CSDN: https://blog.csdn.net/zjj972326230/article/details/79003318 , 2018-01-08.




