1、 抛出检查异常导致事务不能正确回滚
@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);
}
}
}
-
原因:Spring 默认只会回滚非检查异常
-
解法:配置 rollbackFor 属性
-
@Transactional(rollbackFor = Exception.class)
-
2、业务方法内自己 try-catch 异常导致事务不能正确回滚
@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();
}
}
}
-
原因:事务通知只有捉到了目标抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉异常,事务通知无法知悉
-
解法1:异常原样抛出
-
在 catch 块添加
throw new RuntimeException(e);
-
-
解法2:手动设置 TransactionStatus.setRollbackOnly()
-
在 catch 块添加
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
-
3、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);
}
}
}
@Aspect
public 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;
}
}
}
-
原因:事务切面优先级最低,但如果自定义的切面优先级和他一样,则还是自定义切面在内层,这时若自定义切面没有正确抛出异常…
-
解法1、2:同情况2 中的解法:1、2
-
解法3:调整切面顺序,在 MyAspect 上添加
@Order(Ordered.LOWEST_PRECEDENCE - 1)
(不推荐)数值越小优先级越高
4、非 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);
}
}
}
- 原因:Spring 为方法创建代理、添加事务通知、前提条件都是该方法是 public 的
- 解法1:改为 public 方法
- 解法2:添加 bean 配置如下(不推荐)
@Bean
public TransactionAttributeSource transactionAttributeSource() {
return new AnnotationTransactionAttributeSource(false);
}
5、父子容器导致的事务失效
package day04.tx.app.service;
import day04.tx.app.mapper.AccountMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.FileNotFoundException;
@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);
}
}
}
控制器类:
package day04.tx.app.controller;
import day04.tx.app.service.Service5;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import java.io.FileNotFoundException;
@Controller
public class AccountController {
@Autowired
public Service5 service;
public void transfer(int from, int to, int amount) throws FileNotFoundException {
service.transfer(from, to, amount);
}
}
App 配置类
package day04.tx;
import com.zaxxer.hikari.HikariDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.*;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.DatabasePopulator;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.interceptor.TransactionAttributeSource;
import javax.sql.DataSource;
@Configuration
@PropertySource("classpath:jdbc.properties")
@EnableTransactionManagement
@EnableAspectJAutoProxy(exposeProxy = true)
@ComponentScan("day04.tx.app.service")
@MapperScan("day04.tx.app.mapper")
public class AppConfig {
@ConfigurationProperties("jdbc")
@Bean
public DataSource dataSource() {
return new HikariDataSource();
}
@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"));
}
@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);
}
@Bean
// 只要 beanFactory 的 allowBeanDefinitionOverriding==true, 即使系统的 @Bean 定义没有 @ConditionalOnMissingBean 条件,也会被我们的同名 @Bean 覆盖掉
public TransactionAttributeSource transactionAttributeSource() {
return new AnnotationTransactionAttributeSource(false);
}
}
Web 配置类
package day04.tx;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan("day04.tx.app.controller")
public class WebConfig {
}
测试类:
package day04.tx.app.service;
import day04.tx.AppConfig;
import day04.tx.WebConfig;
import day04.tx.app.controller.AccountController;
import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.support.GenericApplicationContext;
import java.io.FileNotFoundException;
public class TestService5 {
public static void main(String[] args) throws FileNotFoundException {
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);
}
}
现在配置了父子容器,WebConfig 对应子容器,AppConfig 对应父容器,发现事务依然失效
-
原因:子容器扫描范围过大,把未加事务配置的 service 扫描进来
-
解法1:各扫描各的,不要图简便
-
解法2:不要用父子容器,所有 bean 放在同一容器
6、调用本类方法导致传播行为失效
@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");
}
}
-
原因:本类方法调用不经过代理,因此无法增强
-
解法1:依赖注入自己(代理)来调用
-
解法2:通过 AopContext 拿到代理对象,来调用
-
解法3:通过 CTW,LTW 实现功能增强(修改字节码,来进行的功能增强,CTW是在编译时期,LTW是在加载时期)
解法1
@Service
public class Service6 {
@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(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void bar() throws FileNotFoundException {
LoggerUtils.get().debug("bar");
}
}
解法2,还需要在 AppConfig 上添加 @EnableAspectJAutoProxy(exposeProxy = true)
@Service
public class Service6 {
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void foo() throws FileNotFoundException {
LoggerUtils.get().debug("foo");
((Service6) AopContext.currentProxy()).bar();
}
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void bar() throws FileNotFoundException {
LoggerUtils.get().debug("bar");
}
}
7、@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);
}
}
测试代码:
package day04.tx.app.service;
import day04.tx.AppConfig;
import org.slf4j.MDC;
import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.support.GenericApplicationContext;
import java.util.concurrent.CountDownLatch;
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);
// 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));
}
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");
}
}
}
上面的代码实际上是有 bug 的,假设 from 余额为 1000,两个线程都来转账 1000,可能会出现扣减为负数的情况
-
原因:事务的原子性仅涵盖 insert、update、delete、select … for update 语句,select 方法并不阻塞
-
如上图所示,红色线程和蓝色线程的查询都发生在扣减之前,都以为自己有足够的余额做扣减
8、@Transactional 方法导致的 synchronized 失效
针对上面的问题,能否在方法上加 synchronized 锁来解决呢?
@Service
public class Service7 {
private static final Logger logger = LoggerFactory.getLogger(Service7.class);
@Autowired
private AccountMapper accountMapper;
@Transactional(rollbackFor = Exception.class)
public synchronized 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);
}
}
答案是不行,原因如下:
-
synchronized 保证的仅是目标方法的原子性,环绕目标方法的还有 commit 等操作,它们并未处于 sync 块内
-
可以参考下图发现,蓝色线程的查询只要在红色线程提交之前执行,那么依然会查询到有 1000 足够余额来转账
-
解法1:synchronized 范围应扩大至代理方法调用
-
解法2:使用 select … for update 替换 select
package day04.tx.app.mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
public interface AccountMapper {
@Update("update account set balance=balance+#{balance} where accountNo=#{accountNo}")
void update(@Param("accountNo") int accountNo, @Param("balance") int balance);
@Select("select balance from account where accountNo=#{accountNo} for update")
int findBalanceBy(int accountNo);
}