1. 数据库事务特征
1.1 ACID特性
事务 (Transaction) 是数据库系统中一系列操作的一个逻辑单元,所有操作要么全部成功要么全部失败
。
事务是区分文件存储系统
与 Nosql
数据库重要特性之一,其存在的意义是为了保证即使在并发情况下也能挣钱的执行crud操作。怎样才算是正确的呢?这时提出了事务需要保证的四个特性即ACID
-
A:原子性(atomicity)
一个事务 (transaction) 中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚 (Rollback) 到事务开始前的状态,就像这个事务从来没有执行过一样。原子性表现为操作不能被分割。
C:一致性(consistency)
在事务开始之前和事务结束以后,数据库的完整性没有被破坏。数据库要一直处于一致的状态,事务开始前是一个一致状态,事务结束后是另一个一致状态,事务将数据库从一个一致状态转移到另一个一致状态
。I:隔离性(isolation)
数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致
。事务隔离分为不同级别,包括读未提交 (read uncommitted)、读已提交(read committed)、可重复读 (repeatable read) 和串行化 (Serializable)。D:持久性(durability)
事务外理结束后,对数据的修改就是永久的
,即便系统故障也不会丢失。
扩展
WAL原则
InnoDB 的 ARIES 三原则 Write Ahead Logging (WAL)
- 日志成功写入后事务就不会丢失,后续由checkpoint机制来保证磁盘物理文件与redo log达到一致性;
- 利用redo log来记录变更后的数据,即redo里记录事务数据变更后的值;
- 利用undo log来记录变更前的数据,即undo里记录事务数据变更前的值,用于回滚和其他事务多版本读。
并发事务控制
-
单版本控制-锁
锁用独占的方式来保证在只有一个版本的情况下事务之间相互隔离。在MySQL事务中,锁的实现与隔离级别有关系,在RR (Repeatable Read) 隔离级别下,MySQL为了解
决幻读的问题,以牺牲并行度为代价,通过 Gap 锁来防止数据的写入,而这种锁,因为其并行度不够,
冲突很多,经常会引起死锁。现在流行的 Row 模式可以避免很多冲突甚至死锁问题,所以推荐默认使用Row+
RC (Read Committed) 模式的隔离级别,可以很大程度上提高数据库的读写并行度。
-
多版本控制 - MVCC
在数据库中,为了实现高并发的数据访问,对数据进待多版本处理,并通过事务的可见性来保证事务能看到自己应该看到的数据版本。
MVCC最大的好处是读不加锁,读写不冲突
。每一次对数据库的修改,都会在 Undo 日志中记录当前修改记录的事务号及修改前数据状态的存储地址(即ROLL_PTR),以便在必要的时候可以回滚到老的数据版本。例如,一个读事务查询到当前记录,而最新的事务还未提交,根据原子性,读事务看不到最新数据。
1.2 事务隔离级别
在高并发的情况下,要完全保证其ACID特性是非常困难的,除非把所有的事务串行化执行,但带来的负面的影响将是性能大打折扣。很多时候我们有些业务对事务的要求是不一样的,所以数据库中设计了四种隔离级别,供用户基于业务进行选择。
数据库默认隔离级别:
- Oracle中默认级别:Read committed
- MySQL中默认级别:Repeatable read(在数据库内均表现为大写)
# 查看 mysql 的默认隔离级别
SELECT @@tx_isolation
# 设置为读未提交
SET tx_isolation='read-uncommitted';
# 设置为读已提交
SET tx_isolation='read-committed';
# 设置为可重复读
SET tx_isolation='REPEATABLE-READ';
# 设置为串行化
SET tx_isolation='SERIALIZABLE'
隔离级别 | 脏读(Dirty Read) | 不可重复读(NonRepeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
未提交读(Read uncommitted) | 可能 | 可能 | 可能 |
已提交读(Read committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable) | 不可能 | 不可能 | 不可能 |
脏读
一个事务读取到另一个事务未提交的更新数据
session_1
# 设置为读未提交
SET tx_isolation='read-uncommitted';
BEGIN;
INSERT INTO `tbl_employee` (`last_name`,`email`, `gender`)
VALUES ( '2222', '[email protected]', '1' );
# 1. 执行到此处,执行session_2
ROLLBACK;
# 3. 执行到此处,再执行session_2
COMMIT; -- 无用
session_2
# 设置为读未提交
SET tx_isolation='read-uncommitted';
SELECT * FROM `tbl_employee`;
# 2. 执行完成,session_1 事务未提交,却读出了数据;回到session_1执行回滚
# 4. 此时查询不到数据,说明第2步出现脏读
不可重复读
在同一事务中,多次读取同一数据返回的结果有所不同,换句话说,后续读取可以读到另一事务已提交的更新数据。相反,“可重复读”在同一事务中多次读取数据时,能够保证所读数据一样,也就是后续读取不能读到另一事务已提交的更新数据。
事务B修改数据导致当前事务A前后读取数据不一致,侧重点在于事务B的修改
当前事务读到了其他事务修改的数据
session_1
# 设置为读已提交
SET tx_isolation='read-committed';
BEGIN;
SELECT * FROM `tbl_employee`;
# 1. 执行session_2中内容
SELECT * FROM `tbl_employee`;
# 3. 和前面查询的数据不一致
COMMIT;
session_2
# 设置为读已提交
SET tx_isolation='read-committed';
UPDATE `tbl_employee` SET email = "[email protected]" WHERE id = 5;
# 2. 执行session_1中内容
幻读
查询表中一条数据,如果不存在就插入一条,并发的时候却发现,里面居然有两条相同的数据。
事务A修改表中数据,此时事务B插入一条新数据,事务A查询发现表中还有没修改的数据,像是出现幻觉
事务A读到了事务B新增的数据,导致结果不一致,侧重点在于事务B新增数据
session_1
# 设置为可重复读
SET tx_isolation='REPEATABLE-READ';
BEGIN;
SELECT * FROM `tbl_employee` WHERE last_name = "jack";
# 1. 此时在session_2事务插入数据
SELECT * FROM `tbl_employee` WHERE last_name = "jack";
INSERT INTO `tbl_employee` (`last_name`,`email`, `gender`)
VALUES ( 'jack', '[email protected]', '1' );
SELECT * FROM `tbl_employee` WHERE last_name = "jack";
UPDATE `tbl_employee` SET email = "[email protected]" WHERE last_name = "jack";
SELECT * FROM `tbl_employee` WHERE last_name = "jack";
COMMIT;
session_2
# 设置为可重复读
SET tx_isolation='REPEATABLE-READ';
INSERT INTO `tbl_employee` (`last_name`,`email`, `gender`)
VALUES ( 'jack', '[email protected]', '1' );
2. Spring事务应用及源码分析
2.1 Spring事务相关API
Spring 事务是在数据库事务的基础上进行封装扩展,其主要特性如下:
- 支持原有的数据库事务的隔离级别,加入了
事务传播
的概念 - 提供多个事务的合并或隔离的功能
- 提供声明式事务,让业务代码与事务分离,事务变得更易用(AOP)
Spring 提供了事务相关接口:
-
TransactionDefinition
事务定义:事务的隔离级别和事务的传播行为
-
TransactionAttribute
事务属性,实现了对回滚规则的扩展(外理异常)
-
PlatformTransactionManager
事务管理器
-
TransactionStatus
事务运行时状态
相关实现类
-
TransactionIntercenter
事务拦截器,实现了 MethodInterceptor
-
TransactionAspectSupport
事务切面支持,内部类Transactionlnfo封装了事务相关属性
TransactionAspectSupport.Transactionlnfo
2.2 编程式事务
public class SpringTransactionExample {
private static String url = "jdbc:mysql://127.0.0.1:3306/test";
private static String user = "root";
private static String password = "root";
public static void main(string[] args) {
// 获取数据源
final Datasource ds = new DriverManagerDatasource(url, user, password);
//编程式事务
final TransactionTemplate template = new TransactionTemplate();
// 设置事务管理器
template.setTransactionManager(new DataSourceTransactionManager(ds));
template.execute(new TransactionCa11back<0bject>() {
@override
public object doInTransaction(Transactionstatus status) {
Connection conn = DatasourceUtils.getConnection(ds);
object savePoint = null;
try {
{
// 插入
PreparedStatement prepare = conn.preparestatement("insert INTO account (accountName,user,money) VALUES (?, ?, ?)");
preparp.setstring(1, "111");
prepare.setstring(2, "aaa");
prepare.setInt(3, 10000);
prepare.executeUpdate():;
}
//设置保存点
savepoint = status.createSavepoint();
{
//捕入
PreparedStatement prepare = conn.preparestatement("insert INTO account (accountName.user,money) VALUES (?, ?, ?)");
prepare.setstring(1,"222");
prepare.setstring(2, "bbb");
prepare.setInt(3, 10000);
prepare.executeupdate() ;
}
{
//更新
Preparedstatement prepare = conn.preparestatement("UPDATE account SET money= money+100 where user=?");
prepare.setstring(1, "aaa");
prepare.executeUpdate();
//int i=1/0;
}
} catch (SQLException e) {
e.printstackTrace();
} catch (Exception e) {
System.out.print1n("更新失败");
if (savePoint != null) [
status.ro11backTosavepoint(savePoint);
} else {
status.setRollbackOnly();
}
}
return null;
}
});
}
2.3 声明式事务
Xml配置
添加tx名字空间
xmlns:tx="http://www.springframework.org/schema/tx"
配置事务管理器和数据源
事务切面配置
@Transactional
事务主机配置,作用于类,方法上
属性名 | 说明 |
---|---|
name | 当在配置文件中有多个TransactionManager,可以用该属性指定选择哪个事务管理器 |
propagation | 事务的传播行为,默认值为REOUIRED |
isolation | 事务的隔离度,默认值采用DEFAULT |
timeout | 事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务 |
read-only | 指定事务是否为只读事务,默认值为false;为了忽略那些不需要事务的方法,比如读取数 据,可以设置 read-only 为 true。 |
rollback-for | 用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通 过逗号分隔 |
no-rollback-for | 抛出 no-rollback-for 指定的异常类型,不回滚事务 |
Java Configuration
@EnableTransactionManagement
利用 TransactionManagementConfigurationSelector 向容器中注册两个组件
-
AutoProxyRegistrar
给容器中注册一个 InfrastructureAdvisorAutoProxyCreator 的后置处理器,返回一个代理对象 (增强器),代理对象执行方法利用拦截器链进行调用
-
ProxyTransactionManagementConfiguration 是一个 @Configuration
- 给容器中注册事务增强器 transactionAcvisor
- AnnotationTransactionAttributesource 解析事务注解
- 事务栏截器 transactionInterceptor
Spring的事务传播机制
多个事务方法相互调用时,事务如何在这些方法之间进行传播,Spring中提供了 `7种` 不同的传播特性,来保证事务的正常执行
- REQUIRED:默认的传播特性,如果当前没有事务,则新建一个事务,如果当前存在事务,则加入这个事务
- SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,则以非事务的方式执行
- MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常
- REQUIRED_NEW :创建一个新事务,如果存在当前事务,则挂起该事务
- NOT_SUPPORTED:以非事务方式执行,如果存在当前事务,则挂起当前事务
- NEVER:不使用事务,如果当前事务存在,则抛出异常
- NESTED:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样
NESTED 和 REQUIRED_NEW 的区别:
REQUIRED_NEW 是新建一个事务并且新开始的这个事务与原有事务无关,而 NESTED 则是当前存在事务时会开启一个嵌套事务,在 NESTED 情况下,父事务回滚时,子事务也会回滚,而 REQUIRED_NEW 情况下,原有事务回滚,不会影响新开启的事务
NESTED 和 REQUIRED 的区别:
REQUIRED 情况下,调用方存在事务时,则被调用方和调用方使用同一个事务,那么被调用方出现异常时,由于共用一个事务,所以无论是否 catch异常,事务都会回滚,而在 NESTED 情况下,被调用方发生异常时,调用方可以 catch其异常,这样只有子事务回滚,父事务不会回滚
Spring事务的实现原理
在使用Spring框架的时候,可以有两种事务的实现方式,一种是编程式事务,有用内自己通过代码来撑制事务的处理逻辑,还有一种是声明式事务,通过 `@Transactional` 注解来实现。
其实事务的操作本来应该是由数据库来进行控制,但是为了方便用户进行业务逻辑的操作,Spring对事务功能进行了扩展实现,一般我们很少会用编程式事务,更多的是通过添加 @Transactional 注解来进行实现,当添加此注解之后事务的自动功能就会关闭,有Spring框架来帮助进行控制。
其实事务操作是AOP的一个核心体现,当一个方法添加 @Transactional 注解之后,Spring会基于这个类生成个代理对象,会将这个代理对象作为bean,当使用这个代理对象的方法的时候,如果有事务处理,那么会先把事务的自动提交给关系,然后去执行具体的业务逻辑,如果执行逻辑没有出现异常,那么代理逻辑就会直接提交如果出现任何异常情况,那么直接进行回滚操作,当然用户可以控制对哪些异常进行回滚操作。
Spring事务失效场景
bean对象没有被Spring容器管理
方法的访间修饰符不是public
切点是否配置正确
-
自身调用问题
因为this不是代理对象,可以配置
expose-proxy="true"
,就可以通过AopContext.currentProx()
获取
到当前类的代理对象@EnableAspectJAutoProxy(exposeProxy = true)
也可以注入当前bean
数据源没有配置事务管理器
数据库不支持事务
导常被捕获
-
异常类型错误或者配置错误
默认只支持RuntimeException 和 Error,不支持检查异常
想要支持检查一异常需配置rollbackFor
@Transactional(rollbackFor = Exception.class)