Spring事务
事务是一组操作的集合,是一个不可分割的操作。事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。所以这组操作要么同时成功,要么同时失败。我们在进行程序开发时,也会有事务的需求。
参考文章:
Spring事务基本信息
Spring事务在解决什么问题
- 数据一致性:在分布式系统中,多个服务或组件可能需要同时操作同一数据,事务确保这些操作要么全部成功,要么全部失败,从而保持数据的一致性。
 - 并发控制:在多用户并发访问的情况下,事务可以控制对共享资源的访问,避免数据冲突和损坏。
 - 故障恢复:当操作过程中发生错误时,事务可以回滚到操作前的状态,确保系统不会处于不一致的状态。
 
Spring事务的实现原理
Spring事务的实现基于 AOP (面向切面编程)和 动态代理 。Spring通过代理机制对目标对象进行包装,插入事务管理的逻辑,包括开启事务、提交事务和回滚事务等。具体来说:
- 动态代理:Spring使用 
JDK动态代理或CGLIB动态代理来创建代理对象。 - AOP:通过切面编程,在方法调用前后插入事务管理的逻辑。
 
Spring事务的配置和使用
在Spring中配置事务主要通过注解@Transactional来实现。该注解可以应用于类或方法上,指示该方法需要事务管理。
@Transactional
基础信息
@Transactional是Spring框架中用于声明式事务管理的关键注解,其核心功能是确保标注的方法或类在数据库操作中遵循ACID原则(原子性、一致性、隔离性、持久性),实现要么全部成功提交,要么全部回滚的机制。通过AOP(面向切面编程)实现,在方法执行前开启事务,执行后根据结果提交或回滚。
Spring 事务默认的回滚规则
只有未捕获的 RuntimeException(运行时异常)或 Error 才会触发回滚,而普通的 Exception(检查异常)不会触发回滚。
若你希望所有异常都能回滚,可加上 rollbackFor = Exception.class,避免出现“事务看起来生效了,但并没有真正回滚”的情况。
常见属性配置
| 属性 | 作用 | 示例 | 
|---|---|---|
| propagation | 控制事务传播行为(如REQUIRED、REQUIRES_NEW) | @Transactional(propagation = Propagation.REQUIRED) | 
| isolation | 设置事务隔离级别(如READ_COMMITTED) | @Transactional(isolation = Isolation.READ_COMMITTED) | 
| rollbackFor | 指定触发回滚的异常类型 | @Transactional(rollbackFor = Exception.class) | 
| timeout | 定义事务超时时间(秒) | @Transactional(timeout = 30) | 
Spring事务为什么会失效
try-catch导致的事务失效
Spring 只有在方法抛出异常时,才会触发回滚。如果你在 catch 里吞掉了异常,那事务也就不会回滚了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void deletePersonById(Long id){
try {
// 业务处理
int x = 1 / 0; // 触发异常
} catch (Exception e) { // catch 里吞掉了异常,Spring 感知不到异常的发生
System.out.println("发生异常,但事务未回滚");
}
}
// 手动抛出异常
public void deletePersonById(Long id){
try {
// 业务处理
int x = 1 / 0; // 触发异常
} catch (Exception e) {
throw new RuntimeException("手动抛出异常,确保事务回滚", e);
}
}
// 异常自然传播
public void deletePersonById(Long id) throws Exception {
// 业务处理
int x = 1 / 0; // 事务会回滚
}方法不是public
@Transactional只会作用于 public 方法,如果你加在private或protected方法上,事务不会生效。因为Spring 事务是通过 代理机制 实现的,而 JDK 动态代理只能代理public方法,所以其他访问级别的方法都不行。1
2
3
4
private void deleteDept(Long id) { } // 事务不会生效
protected void deleteDept(Long id) { } // 事务不会生效同一类里,方法互相调用
这里
deleteDept方法调用了deleteEmp,但deleteEmp上的@Transactional不会生效!原因是:Spring 的事务是基于代理的,this.deleteEmp(id)直接调用了本类的方法,没有经过 Spring 代理,所以事务不会生效。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DeptServiceImpl {
private DeptMapper deptMapper;
public void deleteDept(Long id) {
this.deleteEmp(id); // 事务不会生效!
}
public void deleteEmp(Long id) {
empMapper.delByDeptId(id);
}
}正确的处理方法应是通过 Spring 管理的 Bean 调用(如下),或者使用
ApplicationContext获取代理对象,再调用方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DeptServiceImpl {
private DeptServiceImpl self;
public void deleteDept(Long id) {
self.deleteEmp(id); // 事务生效!
}
public void deleteEmp(Long id) {
empMapper.delByDeptId(id);
}
}数据库引擎不支持事务
如果你用的 MySQL 表引擎是 MyISAM,事务是不可能生效的,因为 MyISAM 根本不支持事务!要确保你的表是 InnoDB:
1
2SHOW TABLE STATUS WHERE Name = 'dept';
ALTER TABLE dept ENGINE = InnoDB;
@Transactional的传播机制
@Transactional注解支持多种事务传播机制,这些机制定义了事务的行为方式。通过Propagation配置,主要的传播机制及含义如下: 
| 传播类型 | 含义 | 
|---|---|
| Propagation.REQUIRED | 如果当前已有事务则加入当前事务,否则开启新的事务 | 
| Propagation.REQUIRED_NEW | 无论当前是否有事务都开启新的事务;如果当前存在事务,则把当前事务挂起。 | 
| Propagation.SUPPORTED | 如果当前事务存在就加入事务,否则以非事务运行 | 
| Propagation.NOT_SUPPORTED | 始终以非事务方式执行;如果当前存在事务,则挂起当前事务 | 
| Propagation.NEVER | 不使用事务,如果当前事务存在,则抛出异常 | 
| Propagation.MANDATORY | 当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。 | 
| Propagation.NESTED | 父子(嵌套)事务,父提交子提交,父回滚全回滚,子回滚不影响父事务 | 
这些传播机制提供了灵活的事务控制选项,可以根据不同的业务需求选择合适的事务行为,避免事务的方法过于长,一个事务里面调用的库表越多,就越有可能造成死锁,所以我们要根据具体的需要拆分使用。例如:
- 在需要确保操作完全独立于其他事务时,可以使用REQUIRES_NEW;
 - 在不需要事务时,可以使用NOT_SUPPORTED或NEVER;
 - 在需要嵌套事务时,可以使用NESTED等
 
避免事务的方法过于长,一个事务里面调用的库表越多,就越有可能造成死锁,所以我们要根据具体的需要拆分使用
高吞吐量下使用@Transactional注解导致性能降低
批量处理
尽可能将多个数据库操作合并到单个事务中。例如,可以使用JDBC的
batch updates或者在ORM框架(如Spring Data JPA, Hibernate)中利用其批量操作功能。 示例(Spring Data JPA):1
2
3
4
public void batchSave(List<Entity> entities) {
entityRepository.saveAll(entities);
}减少事务范围
尽量减小@Transactional注解覆盖的方法范围。只在确实需要事务控制的代码块上使用@Transactional。例如,只在服务层而非控制器层使用事务。
异步事务处理
   对于非阻塞操作,可以考虑将事务逻辑移至异步处理中。例如,使用@Async注解来异步执行事务操作,但这通常需要额外的配置来确保事务的正确管理。示例(Spring @Async):
1  | 
  | 
优化数据库配置
索引优化:确保数据库表上有适当的索引,以加快查询和插入速度。
连接池优化:使用高效的数据库连接池(如
HikariCP),并合理配置其参数(如最大连接数、连接超时时间等)。读写分离:对于读多写少的应用,可以考虑实现数据库的读写分离。
避免大事务
- 避免在单个事务中处理大量数据。如果可能,将大事务拆分成多个小事务。
 
使用乐观锁或悲观锁
- 在高并发场景下,使用乐观锁或悲观锁可以减少锁的竞争,提高并发性能。乐观锁通常用于写操作较少的情况,而悲观锁则适用于写操作较多的情况。
 
@Transactional大事务处理
什么是大事务
总体任务对应的事务运行时间比较长,长时间未提交的事务
大事务的危害
- 并发情况下,数据库连接池资源占满。大事务提交不及时,导致连接资源释放缓慢。
 - 数据库死锁和锁等待。
innodb引擎背景下,事务如果占用了排他锁,会容易导致并发情况下数据死锁或者锁等待。 - 大事务Rt时间长,容易导致接口超时。
 - 大事务回滚时间长。
 - 数据库主从架构下,数据同步延迟
 
解决方案
将声明式事务的@Transactional方式 合理的替换为 编程式事务TransactionTemplate 的方式
声明式事务的粒度最小是整个方法,可能会导致业务里不必要的逻辑都加了事务。编程式事务细化需要加事务的逻辑上,形成实际有用的事务块。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private TransactionTemplate transactionTemplate;
public void testTransaction() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
try {
// .... 业务代码
} catch (Exception e){
//回滚
transactionStatus.setRollbackOnly();
}
}
});
}将查询放在事务方法外
使用@Transactional 又想避免产生大事务,需对方法进行拆分,将不需要事务管理的逻辑与事务操作分开1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TransactionTestService{
// 避免同一个类内部方法相互调用,实例方法调用代理方法而导致事务失效
private TransactionTestService service;
public void create(ParamDto dto){
queryData1();
queryData2();
service.save(dto);
}
//事务操作
public void save(ParamDto dto){
paramDao.insert(dto);
}
}避免跨服务间的远程调用
服务间的通讯及服务之间的调用时间 受网络环境和远端接口Rt时间的影响,可能会比较耗时。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// 反例:
// 事务操作
public void save(ParamDto dto){
// 调用了其他服务
otherRemoteApi();
paramDao.insert(dto);
}
// 修改为:
private TransactionTemplate transactionTemplate;
public void save(ParamDto dto){
// 调用了其他服务
otherRemoteApi();
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
try {
paramDao.insert(dto);
} catch (Exception e){
//回滚
transactionStatus.setRollbackOnly();
}
}
});
}事务中不应该一次性处理太多的数据,可以使用分批执行
事务中的方法可以根据业务使用异步执行




