在Spring中,经常使用@Transactional注解来声明方法需要事务支持。然而,当一个类中的方法自调用(即一个方法调用同类中的另一个方法)时,@Transactional注解可能会失效。这是因为Spring的事务管理是基于代理机制实现的,而自调用无法触发代理逻辑。

示例代码

以下是遇到的一个事务失效场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class XXServiceImpl implements XXService {

@Override
public void acquireItem(Long uuid, Long iId, IdptEnum idptEnum, String affairId) {
String idptKey = getIdptKey(iId, idptEnum, affairId);
doAcquireItem(uuid, iId, idptKey); // 自调用导致事务失效
}

@Transactional
public void doAcquireItem(Long uuid, Long iId, String idptKey) {
// 数据库操作
}

private String getIdptKey(Long iId, IdptEnum idptEnum, String affairId) {
return String.format("%d_%d_%s", iId, idptEnum.getType(), affairId);
}
}

在上述代码中,调用了同类中的doAcquireItem方法,但由于是自调用,@Transactional注解没有生效。


原理分析

通过分析,了解到Spring的事务管理是通过AOP(面向切面编程)实现的:

  1. Spring会为带有@Transactional注解的方法生成代理对象。
  2. 当外部调用代理对象的方法时,代理会拦截调用并添加事务逻辑。
  3. 自调用时,调用的是目标对象本身,而不是代理对象,因此事务逻辑不会被触发。

解决方案

方法一:通过代理对象调用

将自调用改为通过代理对象调用,确保事务逻辑生效。

修改后的代码

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
28
29
30
31
32
33
34
35
36
@Service
public class XXServiceImpl implements XXService {
@Autowired
private LockService lockService;
@Autowired
private XXDao XXDao;
@Autowired
@Lazy
private XXServiceImpl XXServiceImp; // 启用代理对象

@Override
public void acquireItem(Long uuid, Long iId, IdptEnum idptEnum, String affairId) {
String idptKey = getIdptKey(iId, idptEnum, affairId);
XXServiceImp.doAcquireItem(uuid, iId, idptKey); // 通过代理对象调用
}

@Transactional
@RedisLock(key = "#idptKey", waitTime = 3000)
public void doAcquireItem(Long uuid, Long iId, String idptKey) {
XX XX = XXDao.getByIdpt(idptKey);
if (Objects.nonNull(XX)) {
return;
}
XX insert = XX.builder()
.uuid(uuid)
.iId(iId)
.status(YesOrNoEnum.NO.getStatus())
.idpt(idptKey)
.build();
XXDao.save(insert);
}

private String getIdptKey(Long iId, IdptEnum idptEnum, String affairId) {
return String.format("%d_%d_%s", iId, idptEnum.getType(), affairId);
}
}

关键点

  • 使用了@Lazy注解延迟注入代理对象,避免循环依赖。
  • 通过代理对象调用doAcquireItem方法,确保事务逻辑生效。

方法二:提取方法到另一个类

还尝试将需要事务支持的方法提取到另一个类中,由外部类调用。

修改后的代码

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Service
public class XXServiceImpl implements XXService {
@Autowired
private LockService lockService;
@Autowired
private XXDao XXDao;
@Autowired
private TransactionalService transactionalService;

@Override
public void acquireItem(Long uuid, Long iId, IdptEnum idptEnum, String affairId) {
String idptKey = getIdptKey(iId, idptEnum, affairId);
transactionalService.doAcquireItem(uuid, iId, idptKey); // 调用外部类方法
}

private String getIdptKey(Long iId, IdptEnum idptEnum, String affairId) {
return String.format("%d_%d_%s", iId, idptEnum.getType(), affairId);
}
}

@Service
public class TransactionalService {
@Autowired
private XXDao XXDao;

@Transactional
@RedisLock(key = "#idptKey", waitTime = 3000)
public void doAcquireItem(Long uuid, Long iId, String idptKey) {
XX XX = XXDao.getByIdpt(idptKey);
if (Objects.nonNull(XX)) {
return;
}
XX insert = XX.builder()
.uuid(uuid)
.iId(iId)
.status(YesOrNoEnum.NO.getStatus())
.idpt(idptKey)
.build();
XXDao.save(insert);
}
}

关键点

  • 将事务逻辑提取到TransactionalService类中,避免自调用。
  • 原类通过@Autowired注入TransactionalService,调用其方法。

方法三:使用AopContext.currentProxy

最后,尝试通过AopContext.currentProxy()获取当前代理对象,并通过代理对象调用方法。

修改后的代码

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
28
29
30
31
32
33
@Service
public class XXServiceImpl implements XXService {
@Autowired
private LockService lockService;
@Autowired
private XXDao XXDao;

@Override
public void acquireItem(Long uuid, Long iId, IdptEnum idptEnum, String affairId) {
String idptKey = getIdptKey(iId, idptEnum, affairId);
((XXServiceImpl) AopContext.currentProxy()).doAcquireItem(uuid, iId, idptKey); // 使用代理对象调用
}

@Transactional
@RedisLock(key = "#idptKey", waitTime = 3000)
public void doAcquireItem(Long uuid, Long iId, String idptKey) {
XX XX = XXDao.getByIdpt(idptKey);
if (Objects.nonNull(XX)) {
return;
}
XX insert = XX.builder()
.uuid(uuid)
.iId(iId)
.status(YesOrNoEnum.NO.getStatus())
.idpt(idptKey)
.build();
XXDao.save(insert);
}

private String getIdptKey(Long iId, IdptEnum idptEnum, String affairId) {
return String.format("%d_%d_%s", iId, idptEnum.getType(), affairId);
}
}

关键点

  • 通过AopContext.currentProxy()获取当前代理对象。
  • 确保在Spring配置中启用了exposeProxy属性:
1
2
3
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
<property name="exposeProxy" value="true" />
</bean>

事务传播行为

在解决问题的过程中,还学习了Spring事务的多种传播行为(Propagation):

  • REQUIRED(默认):如果当前存在事务,则加入;否则创建新事务。
  • REQUIRES_NEW:总是创建新事务,暂停当前事务。
  • NESTED:嵌套事务,支持回滚到子事务。

事务失效的其他场景

  1. public方法:Spring AOP仅支持public方法。
  2. 异常未被捕获:事务仅在未捕获的RuntimeExceptionError时回滚。
  3. 多线程调用:事务上下文无法跨线程传播。

通过这些方法,成功解决了Spring事务注解自调用失效的问题,并加深了对Spring事务机制的理解。