Spring事务总结
1.定义与特性
定义:逻辑上的一组操作,要么全做,要么全部做。
四大特性(简称ACID):
原子性(Atomicity)
事务是不可分割工作单元
一致性(Consistency)
事务前后数据的完整性必须保持一致
隔离型(Isolation)
多个用户并发访问时,多个并发事务之间的数据是相互隔离的
持久性(Durability)
事务一旦被提交,它对数据库中数据的改变是永久性的
2.三个主要接口
事务管理器 PlatformTransactionManager
事务定义信息(隔离、传播、超时、只读) TransactionDefinition
public interface TransactionDefinition {
// 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
int PROPAGATION_REQUIRED = 0;
// 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行
int PROPAGATION_SUPPORTS = 1;
// 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常
int PROPAGATION_MANDATORY = 2;
// 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。
int PROPAGATION_REQUIRES_NEW = 3;
// 总是非事务地执行,并挂起任何存在的事务
int PROPAGATION_NOT_SUPPORTED = 4;
// 总是非事务地执行,如果存在一个活动事务,则抛出异常
int PROPAGATION_NEVER = 5;
// 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作
int PROPAGATION_NESTED = 6;
// 使用后端数据库默认的隔离级别
int ISOLATION_DEFAULT = -1;
// 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;
// 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;
// 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;
// 最高的隔离级别,完全服从ACID的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的
int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;
// 默认超时时间
int TIMEOUT_DEFAULT = -1;
// 返回事务的传播行为
int getPropagationBehavior();
// 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据。
int getIsolationLevel();
// 返回事务必须在多少秒内完成。
int getTimeout();
// 事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的(若增删改会异常)。
boolean isReadOnly();
// 返回事务的名称
@Nullable
String getName();
}
事务运行状态 TransactionStatus
public interface TransactionStatus extends SavepointManager, Flushable {
// 是否是新的事物
boolean isNewTransaction();
// 是否有保存点
boolean hasSavepoint();
// 设置为只回滚
void setRollbackOnly();
// 是否为只回滚
boolean isRollbackOnly();
// 通过将任何缓冲的输出写入基础流来刷新此流。
@Override
void flush();
// 是否已完成
boolean isCompleted();
}
3.事务隔离级别
如果不考虑隔离性,会引发安全问题:
1.脏读:一个事务读取了另一个事务改写但还未提交的数据,如果这些数据被回滚,则读到的数据是无效的。
2.不可重复读:在同一事务中,多次读取同一数据返回的结果有所不同。
3.幻读(虚读):一个事务读取几行记录后,另一个事务插入一些记录,幻读就发生了。在后来的查询中,第一个事务就会发现有些原来没有的记录。
补充:
(1)Mysql 默认采用的 REPEATABLE_READ隔离级别
(2)Oracle 默认采用的 READ_COMMITTED隔离级别
4.事务传播行为
img
补充:
第一类共同点:如果 A 方法中有事务,则调用 B 方法时就用该事务,即:A和B方法在同一个事务中。PROPAGATION_REQUIRED:如果 A 方法中没有事务,则调用 B 方法时就创建一个新的事务,即:A和B方法在同一个事务中。PROPAGATION_SUPPORTS:如果 A 方法中没有事务,则调用 B 方法时就不使用该事务。PROPAGATION_MANDATORY:如果 A 方法中没有事务,则调用 B 方法时就抛出异常。
第二类共同点:A方法和B方法没有在同一个事务里面。PROPAGATION_REQUIRES_NEW:如果 A 方法中有事务,则挂起并新建一个事务给 B 方法。PROPAGATION_NOT_SUPPORTED:如果 A 方法中有事务,则挂起。PROPAGATION_NEVER:如果 A 方法中有事务,则报异常。
第三类:如果 A 方法有的事务执行完,设置一个保存点,如果 B 方法中事务执行失败,可以滚回保存点或初始状态。
重点的三种:PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW, PROPAGATION_NESTED。
5.事务的实现方式
编程式事务
实际中很少使用
通过TransactionTemplate手动管理事务
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {
@Autowired
private TransactionTemplate transactionTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 结果返回
*/
@Test
public void hasReturnTx() {
List<Map<String, Object>> result = transactionTemplate.execute(status -> {
String sql = "select id, create_time, username, amount from t_account";
return jdbcTemplate.queryForList(sql);
});
log.info("结果返回:{}", result);
}
/**
* 无结果返回
*/
@Test
public void noneReturnTx() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
String sql = "INSERT INTO t_account (create_time, username, amount) VALUE (?, ?, ?)";
Object[] args = {LocalDateTime.now(), "xiaohei", 1000L};
int rows = jdbcTemplate.update(sql, args);
log.info("影响行数:{}", rows);
}
});
}
}
声明式事务
推荐使用
通过使用AOP实现在配置文件XML中
注解
注意事项:
在非注入的方法上使用 @Transactional 无效 ,必须是 @Autowired 注入的 bean 且由 public 修饰,因为事务是代理执行的
在使用 @Transactional(propagation= Propagation.REQUIRES_NEW) 时,不需要单独配置事务管理器
如果mysql 事务出现问题,记得去查看数据引擎是否是 InnoDB,因为 MyISAM 引擎不支持事务;附上一个修改命令 alter table table_name engine=InnoDB
AspectJ
基于TransactionProxyFactoryBean方式
6.基于注解 @Transactional 声明事务失效分析
在开发过程中,可能会遇到使用 @Transactional 进行事务管理时出现失效的情况。这里我们的讨论是基于事务的默认传播行为是 REQUIRED。
常见的失效场景
如果使用 MySQL 且引擎是 MyISAM,则事务会不起作用,原因是 MyISAM 不支持事务,改成 InnoDB 引擎则支持事务。
注解 @Trasactional 只能加在 public 修饰的方法上事务才起效。如果加在 protect、private 等非 public 修饰的方法上,事务将失效。
如果在开启了事务的方法内,使用了 try-catch 语句块对异常进行了捕获,而没有将异常抛到外层,事务将不起效。
在不同类之间的方法调用中,如果 A 方法开启了事务,B 方法没有开启事务,B 方法调用了 A 方法。如果 B 方法中发生异常,但不是调用的 A 方法产生的,则异常不会使 A 方法的事务回滚,此时事务无效。如果 B 方法中发生异常,异常是调用的 A 方法产生的,则 A 方法的事务回滚,此时事务有效。在 B 方法上加上注解 @Trasactional,这样 A 和 B 方法就在同一个事务里了,不管异常产生在哪里,事务都是有效的。
简单地说,不同类之间方法调用时,异常发生在无事务的方法中,但不是被调用的方法产生的,被调用的方法的事务无效。只有异常发生在开启事务的方法内,事务才有效。
在同一个类的方法之间调用中,如果 A 方法调用了 B 方法,不管 A 方法有没有开启事务, B 方法的事务是无效的。
如果使用了Spring + MVC,则 context:component-scan 重复扫描问题可能会引起事务失效。
原因分析
在应用系统调用声明 @Transactional 的目标方法时,Spring Framework 默认使用 AOP 代理,在代码运行时生成一个代理对象,再由这个代理对象来统一管理。
Spring 事务是使用 AOP 环绕通知和异常通知,就是对方法进行拦截,在方法执行前开启事务,在捕获到异常时进行事务回滚,在方法执行完成后提交事务。
最后
Spring 团队建议在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。在接口上使用 @Transactional 注解,只能当你设置了基于接口的代理时它才生效。因为注解是不能继承的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。
Spring 文档中写到:Spring AOP 部分使用 JDK 动态代理或者 CGLIB 来为目标对象创建代理,如果被代理的目标对象实现了至少一个接口,则会使用 JDK 动态代理。所有该目标类型实现的接口都将被代理。若该目标对象没有实现任何接口,则创建一个CGLIB代理。