Java八股文-Spring事务失效场景

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);
        }
    }
}

运行时异常与检查型异常

  1. 运行时异常是RuntimeException类及其子类,是非检查性异常,如空指针异常、数组下标越界异常、类型转换异常、算术异常。运行时异常与检查性异常最大的区别在于运行时异常不用对其捕获,JVM会自行处理,会自动catch运行时异常并停止线程,打印异常。如果产生运行时异常,相当于bug了,则需要修改代码避免异常,当然也可以手动抛出或者捕捉异常。
  2. 检查性异常是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方法对外层的事务通知拿不到异常
解决思路:

  1. 在catch中抛出异常:throw new RuntimeException(e);
  2. 手动设置回滚: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,查看日志发现最外层是事务切面,中间是自定义切面,最里面才是目标方法
问题原因:异常被自定义切面捕获没有抛出,最外层事务切面拿不到异常信息无法回滚
解决思路:推荐使用第一种正常抛出异常

  1. 正常抛出异常
  2. 手动设置异常
  3. 调整事务切面和自定义切面的优先级:
    查看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);
        }
    }
}

问题分析:
解决思路:

  1. 添加public修饰符
  2. 修改事务默认行为:
//不推荐使用
@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);
        }
    }
}

场景构造:

  1. 在controller中调用Service方法
  2. 定义WebConfig 扫描controller包和service包
  3. 使用子容器注册WebConfig
  4. 查看日志发现金额变成负数
@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, 而子容器是没有加上事务控制的配置类
解决思路:

  1. 缩小子容器的扫描范围:@ComponentScan(“tx.app.controller”)
  2. 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对象,并没有经过代理,不会进行事务的功能增强
解决思路:

  1. 使用代理对象调用方法,在Service注入自己,Spring能解决Set方式的循环依赖。查看日志发现调用bar方式时暂停的事务1,开启了新的事务
  2. 使用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");
        }
    }
}

问题分析:查看日志可以看到发生了指令交错的情况,两个线程都查询到了未更新之前的数据
解决思路:

  1. 加上synchronized保证方法的原子性
  2. 查询语句使用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));
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值