Spring事务失效的场景很多,这里总结了8种
初始化环境
配置类
@Configuration
@PropertySource("classpath:jdbc.properties")
@EnableTransactionManagement //启用申明式事务管理
@EnableAspectJAutoProxy //启用面向切面编程
@ComponentScan("tx.app.service") //扫描servcie包
@MapperScan("tx.app.mapper")
public class AppConfig {
//配置数据源
@ConfigurationProperties("jdbc")
@Bean
public DataSource dataSource() {
return new HikariDataSource();
}
//数据源初始化器 执行SQL脚本
@Bean
public DataSourceInitializer dataSourceInitializer(DataSource dataSource, DatabasePopulator populator) {
DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
dataSourceInitializer.setDataSource(dataSource);
dataSourceInitializer.setDatabasePopulator(populator);
return dataSourceInitializer;
}
@Bean
public DatabasePopulator databasePopulator() {
return new ResourceDatabasePopulator(new ClassPathResource("account.sql"));
}
//session工厂
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
return factoryBean;
}
//事务管理器
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
未抛出运行时异常或Error
@Service
public class Service1 {
@Autowired
private AccountMapper accountMapper;
@Transactional
public void transfer(int from, int to, int amount) throws FileNotFoundException {
int fromBalance = accountMapper.findBalanceBy(from);
if (fromBalance - amount >= 0) {
accountMapper.update(from, -1 * amount);
new FileInputStream("aaa");
accountMapper.update(to, amount);
}
}
}
运行时异常与检查型异常
- 运行时异常是RuntimeException类及其子类,是非检查性异常,如空指针异常、数组下标越界异常、类型转换异常、算术异常。运行时异常与检查性异常最大的区别在于运行时异常不用对其捕获,JVM会自行处理,会自动catch运行时异常并停止线程,打印异常。如果产生运行时异常,相当于bug了,则需要修改代码避免异常,当然也可以手动抛出或者捕捉异常。
- 检查性异常是Exception类本身及其子类中除运行时异常之外的其他异常,检查性异常必须通过throws进行申明抛出,或者通过try-catch进行捕捉处理,否则不能通过编译,如IO异常,SQL异常等。
问题原因:查看日志可以发现并没有回滚异常,这是因为Spring事务只会回滚运行时异常和Error异常
解决方法:指定回滚的异常
@Transactional(rollbackFor = Exception.class)
捕获了异常未抛出
@Service
public class Service2 {
@Autowired
private AccountMapper accountMapper;
@Transactional(rollbackFor = Exception.class)
public void transfer(int from, int to, int amount) {
try {
int fromBalance = accountMapper.findBalanceBy(from);
if (fromBalance - amount >= 0) {
accountMapper.update(from, -1 * amount);
new FileInputStream("aaa");
accountMapper.update(to, amount);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
public class TestService2 {
public static void main(String[] args) throws FileNotFoundException {
GenericApplicationContext context = new GenericApplicationContext();
AnnotationConfigUtils.registerAnnotationConfigProcessors(context.getDefaultListableBeanFactory());
ConfigurationPropertiesBindingPostProcessor.register(context.getDefaultListableBeanFactory());
context.registerBean(AppConfig.class);
context.refresh();
Service2 bean = context.getBean(Service2.class);
bean.transfer(1, 2, 500);
}
}
问题原因:
声明式事物下拿到的对象是代理对象:Service2 bean = context.getBean(Service2.class); 代理对象加了事务控制,原始目标加了try catch方法对外层的事务通知拿不到异常
解决思路:
- 在catch中抛出异常:throw new RuntimeException(e);
- 手动设置回滚:TransactionInterceptor.currentTransactionStatus().setRollbackOnly(); 拿到当前事务状态对象告诉当前调用者回滚异常,这个信息是设置在当前线程的ThreadLocal中
AOP切面顺序
@Service
public class Service3 {
@Autowired
private AccountMapper accountMapper;
@Transactional(rollbackFor = Exception.class)
public void transfer(int from, int to, int amount) throws FileNotFoundException {
int fromBalance = accountMapper.findBalanceBy(from);
if (fromBalance - amount >= 0) {
accountMapper.update(from, -1 * amount);
new FileInputStream("aaa");
accountMapper.update(to, amount);
}
}
}
public class TestService3 {
public static void main(String[] args) throws FileNotFoundException {
GenericApplicationContext context = new GenericApplicationContext();
AnnotationConfigUtils.registerAnnotationConfigProcessors(context.getDefaultListableBeanFactory());
ConfigurationPropertiesBindingPostProcessor.register(context.getDefaultListableBeanFactory());
context.registerBean(MyAspect.class);
context.registerBean(AppConfig.class);
context.refresh();
Service3 bean = context.getBean(Service3.class);
bean.transfer(1, 2, 500);
}
@Aspect
static class MyAspect {
@Around("execution(* transfer(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
LoggerUtils.get().debug("log:{}", pjp.getTarget());
try {
return pjp.proceed();
} catch (Throwable e) {
e.printStackTrace();
return null;
}
}
}
}
场景构造:添加了切面类MyAspect,查看日志发现最外层是事务切面,中间是自定义切面,最里面才是目标方法
问题原因:异常被自定义切面捕获没有抛出,最外层事务切面拿不到异常信息无法回滚
解决思路:推荐使用第一种正常抛出异常
- 正常抛出异常
- 手动设置异常
- 调整事务切面和自定义切面的优先级:
查看EnableTransactionManagement的Order , 自定义切面如果未指定优先级也是最低的。Order值越小执行优先级越高,所有我们要调整自定义切面比事务切面的值小
@Order(Ordered.LOWEST_PRECEDENCE - 1)
非Public方法
@Service
public class Service4 {
@Autowired
private AccountMapper accountMapper;
@Transactional
void transfer(int from, int to, int amount) throws FileNotFoundException {
int fromBalance = accountMapper.findBalanceBy(from);
if (fromBalance - amount >= 0) {
accountMapper.update(from, -1 * amount);
accountMapper.update(to, amount);
}
}
}
问题分析:
解决思路:
- 添加public修饰符
- 修改事务默认行为:
//不推荐使用
@Bean
public TransactionAttributeSource transactionAttributeSource() {
return new AnnotationTransactionAttributeSource(false);
}
父子容器
@Service
public class Service5 {
@Autowired
private AccountMapper accountMapper;
@Transactional(rollbackFor = Exception.class)
public void transfer(int from, int to, int amount) throws FileNotFoundException {
int fromBalance = accountMapper.findBalanceBy(from);
if (fromBalance - amount >= 0) {
accountMapper.update(from, -1 * amount);
accountMapper.update(to, amount);
}
}
}
场景构造:
- 在controller中调用Service方法
- 定义WebConfig 扫描controller包和service包
- 使用子容器注册WebConfig
- 查看日志发现金额变成负数
@Controller
public class AccountController {
@Autowired
public Service5 service;
public void transfer(int from, int to, int amount) throws FileNotFoundException {
service.transfer(from, to, amount);
}
}
@ComponentScan("tx.app")
public class WebConfig {
}
public class TestService5 {
public static void main(String[] args) throws FileNotFoundException {
//父容器扫描Service包、mapper包
GenericApplicationContext parent = new GenericApplicationContext();
AnnotationConfigUtils.registerAnnotationConfigProcessors(parent.getDefaultListableBeanFactory());
ConfigurationPropertiesBindingPostProcessor.register(parent.getDefaultListableBeanFactory());
parent.registerBean(AppConfig.class);
parent.refresh();
//子容器扫描整个项目包
GenericApplicationContext child = new GenericApplicationContext();
AnnotationConfigUtils.registerAnnotationConfigProcessors(child.getDefaultListableBeanFactory());
child.setParent(parent);
child.registerBean(WebConfig.class);
child.refresh();
AccountController bean = child.getBean(AccountController.class);
bean.transfer(1, 2, 500);
}
}
问题分析:controller中注入的是子容器的Service, 可以和父容器的Service同时存在。子容器优先在自己的容器中查找注入的Service, 而子容器是没有加上事务控制的配置类
解决思路:
- 缩小子容器的扫描范围:@ComponentScan(“tx.app.controller”)
- SpringBoot没有父子容器,在传统SpringMVC整合Spring时会有父子容器的问题
调用本类方法导致传播行为失效
场景构造:在Service方法中调用另外一个方法
@Service
public class Service6 {
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void foo() throws FileNotFoundException {
LoggerUtils.get().debug("foo");
bar();
}
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void bar() throws FileNotFoundException {
LoggerUtils.get().debug("bar");
}
}
public class TestService6 {
public static void main(String[] args) throws FileNotFoundException {
GenericApplicationContext context = new GenericApplicationContext();
AnnotationConfigUtils.registerAnnotationConfigProcessors(context.getDefaultListableBeanFactory());
ConfigurationPropertiesBindingPostProcessor.register(context.getDefaultListableBeanFactory());
context.registerBean(AppConfig.class);
context.refresh();
Service6 bean = context.getBean(Service6.class);
System.out.println(bean.getClass());
bean.foo();
}
}
问题原因:调用bar方法的是this对象,并没有经过代理,不会进行事务的功能增强
解决思路:
- 使用代理对象调用方法,在Service注入自己,Spring能解决Set方式的循环依赖。查看日志发现调用bar方式时暂停的事务1,开启了新的事务
- 使用Aop上下文获取代理当前代理对象: ((Service6)AopContext.currentProxy()).bar();
注意这里要修改配置:@EnableAspectJAutoProxy(exposeProxy = true) ,
@Autowired
private Service6 proxy;
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void foo() throws FileNotFoundException {
LoggerUtils.get().debug("foo");
System.out.println(proxy.getClass());
proxy.bar();
}
Transactional没有保证原子行为
场景构造:模拟多线程调用 对一个账户同时转账
@Service
public class Service7 {
private static final Logger logger = LoggerFactory.getLogger(Service7.class);
@Autowired
private AccountMapper accountMapper;
@Transactional(rollbackFor = Exception.class)
public void transfer(int from, int to, int amount) {
int fromBalance = accountMapper.findBalanceBy(from);
logger.debug("更新前查询余额为: {}", fromBalance);
if (fromBalance - amount >= 0) {
accountMapper.update(from, -1 * amount);
accountMapper.update(to, amount);
}
}
public int findBalance(int accountNo) {
return accountMapper.findBalanceBy(accountNo);
}
}
public class TestService7 {
public static void main(String[] args) throws InterruptedException {
GenericApplicationContext context = new GenericApplicationContext();
AnnotationConfigUtils.registerAnnotationConfigProcessors(context.getDefaultListableBeanFactory());
ConfigurationPropertiesBindingPostProcessor.register(context.getDefaultListableBeanFactory());
context.registerBean(AppConfig.class);
context.refresh();
Service7 bean = context.getBean(Service7.class);
CountDownLatch latch = new CountDownLatch(2);
new MyThread(() -> {
bean.transfer(1, 2, 1000);
latch.countDown();
}, "t1", "boldMagenta").start();
new MyThread(() -> {
bean.transfer(1, 2, 1000);
latch.countDown();
}, "t2", "boldBlue").start();
latch.await();
//查询结果 如果等于负数说明出现问题
System.out.println(bean.findBalance(1));
}
//控制打印信息的颜色
static class MyThread extends Thread {
private String color;
public MyThread(Runnable target, String name, String color) {
super(target, name);
this.color = color;
}
@Override
public void run() {
MDC.put("thread", color);
super.run();
MDC.remove("thread");
}
}
}
问题分析:查看日志可以看到发生了指令交错的情况,两个线程都查询到了未更新之前的数据
解决思路:
- 加上synchronized保证方法的原子性
- 查询语句使用for update增加行锁,推荐使用第二种,降低锁的粒度
Transactional方法导致的synchronized失效
思考: transfer方法加上synchronized是否有用?
查看日志会发现还是产生了负数的情况 (如果一直没有复现,可以在DataSourceTransactionManager提交事务时增加断点)
问题原因:锁加在transfer上只保护了更新数据的方法,但是事务提交的逻辑并没有被原子性保护
解决思路:扩大锁的范围
Service7 bean = context.getBean(Service7.class);
Object lock = new Object();
CountDownLatch latch = new CountDownLatch(2);
new MyThread(() -> {
synchronized (lock) {
bean.transfer(1, 2, 1000);
}
latch.countDown();
}, "t1", "boldMagenta").start();
new MyThread(() -> {
synchronized (lock) {
bean.transfer(1, 2, 1000);
}
latch.countDown();
}, "t2", "boldBlue").start();
latch.await();
System.out.println(bean.findBalance(1));