Spring/SpringBoot实现编程式事务

首先需要了解的是为什么需要编程式事务?声明式事务(@Transactional)明明更简洁好用

声明式事务的缺陷

声明式事务表面上简洁好用,但是实际上是牺牲了一些灵活性的,@Transactional注解并不是用在哪里都能生效,@Transactional生效的条件:

  • @Transactional注释的方法,不能是private修饰
  • @Transactional注释的方法,必须是有接口的方法实现(通用的Spring面向接口编程的套路)
  • @Transactional注释的方法,必须要通过接口的方式调用,才能生效(我们知道,注解的本质也是代理,同一个类中直接调用本类的方法,是不会产生代理的,所以注解就都不会生效)

同时满足以上三个条件,@Transactional注解才会生效。但是他的局限性也因此出现,并不是所有带数据库操作的接口公开方法都能放在同一个事务中,如下的例子:

    @Autowired
    private DeptMapper deptMapper;

    @Test
    @Transactional(rollbackFor = Exception.class)
    public void testTransaction() {
    
    
        Dept dept = deptMapper.queryDeptById(4L);
        System.out.println(dept);
        dept.setDname("处理中");
        // 先改为处理中状态
        int i = deptMapper.updateDept(dept);
        if (i == 0) {
    
    
            throw new RuntimeException("Dept数据更新为处理中失败!");
        }
        // 模拟分布式接口调用
        dept.setDname(invoke());
        int count = deptMapper.updateDept(dept);
        if (count == 0) {
    
    
            throw new RuntimeException("Dept数据更新失败!");
        }
    }
     /**
     * 模拟网络接口调用
     * @return dname
     */
    private String invoke() {
    
    
        try {
    
    
            System.out.println("----------------远程接口调用---------------");
            Thread.sleep(2000L); // 休眠2秒,模拟网络调用花费的时间
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("----------------远程接口成功---------------");
        return "处理成功";
    }

上述代码逻辑很简单,在分布式系统中很常见,就是在通过网络接口调用其他系统之前,先将本地的数据置为处理中,待网络结果返回再将结果更新。比如说商城订单的发货流程,先将订单置为发货中,然后提起发货,等到发货成功后再将订单状态改为发货成功。invoke方法是通过线程休眠模拟了网络的调用过程。
上述代码将整个处理逻辑处于一个事务中,这虽然保持了操作的原子性(一致性),但是却牺牲了系统的可靠性。因为一个事务开启会持续占用数据库连接数,而数据库的连接数是非常有限的资源,网络调用耗时比较长,若是高并发的场景下,很轻易地就会将所有的连接数占用,导致系统不可用。
所以这样的代码在企业级系统中是个BUG级的代码。
如何优化?
若是在网络接口调用的之前和之后分别开启两个事务,数据操作完成后立即释放数据库连接,这样虽然牺牲了一致性,但是确实提高了可靠性,这是大多数企业想要的效果。

编程式事务

这时我们用更为灵活的编程式事务重写这段逻辑:

    @Autowired
    private TransactionTemplate transactionTemplate;
    @Test
    public void testCodeTransaction() {
    
    
        final Dept dept = deptMapper.queryDeptById(5L);
        System.out.println(dept);
        // 开始编程式事务1
        Integer i = transactionTemplate.execute(new TransactionCallback<Integer>() {
    
    
            @Override
            public Integer doInTransaction(TransactionStatus transactionStatus) {
    
    
                dept.setDname("处理中");
                // 先改为处理中状态
                return deptMapper.updateDept(dept);
            }
        });
        if (i == null || i == 0) {
    
    
            throw new RuntimeException("Dept数据更新为处理中失败!");
        }
        // 模拟分布式接口调用
        dept.setDname(invoke());
        // 开始编程式事务2 (函数式写法)
        Integer count = transactionTemplate.execute((transactionStatus) -> deptMapper.updateDept(dept));
        if (count == null || count == 0) {
    
    
            throw new RuntimeException("Dept数据更新失败!");
        }
    }

上述写法是Spring自带的编程式事务操作,可以实现一个方法内开启两个互不相关的事务,从而将耗时比较长的网络操作进行隔离。
固定写法建议记下来。

上述代码中事务1采用的是匿名内部类的写法,事务2采用的是函数式写法,实际效果一样,请按需取用。

测试一下:
将上述代码做一个小更改,使事务回滚:

 @Test
    public void testCodeTransaction() {
    
    
        final Dept dept = deptMapper.queryDeptById(5L);
        System.out.println(dept);
        // 开始编程式事务1
        Integer i = transactionTemplate.execute(new TransactionCallback<Integer>() {
    
    
            @Override
            public Integer doInTransaction(TransactionStatus transactionStatus) {
    
    
                dept.setDname("处理中");
                // 先改为处理中状态
                return deptMapper.updateDept(dept);
            }
        });
        if (i == null || i == 0) {
    
    
            throw new RuntimeException("Dept数据更新为处理中失败!");
        }
        // 模拟分布式接口调用
        dept.setDname(invoke());
        // 开始编程式事务2 (函数式写法)
        Integer count = transactionTemplate.execute((transactionStatus) -> {
    
    
            int j = deptMapper.updateDept(dept);
            if (j == 1) {
    
    
                throw new RuntimeException("测试事务回滚!");
            }
            return j;
        });
        if (count == null || count == 0) {
    
    
            throw new RuntimeException("Dept数据更新失败!");
        }
    }

运行结果如下:
运行结果
数据库:deptno为5的数据停留在了处理中,说明第二次的update语句进行了回滚,但是并没有影响第一次update的结果,说明确实是开启了两个事务。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_41674401/article/details/120614639