手动后置提交——多数据源事务的实现

1 前言

在日常开发中,一个方法中可能涉及到多个数据库的写入操作,这个时候再用Spring tx为我们提供的@Transactional注解就显得有些力不从心了,此时可以用第三方框架atomikos,atomikos是一个非常有名的分布式事务开源框架. 它有JTA/XA规范的实现, 也有TCC机制的实现方案, 前者是免费开源的, 后者是商业付费版的。本文对atomikos框架不做讨论,主要讨论如何在代码层面实现后置提交

2 原理

我们知道"ACID"是数据库事务的四个基本特点,分别代表原子性、一致性、隔离性和持久性。spring 为我们提供的@Transactional注解可以定义隔离级别、传播行为等属性,spring通过AOP拦截带有@Transactional注解的方法,为其生成代理对象,将该方法的执行交由事务管理器PlatformTransactionManager完成事务的开始、提交和回滚。具体的实现原理还是比较复杂的,这里不再展开,有兴趣的童鞋可以查阅源码。

在了解了spring事务的实现方式后,我们可以借用其思想,利用aop切面拦截自定义注解,将事务管理器信息定义在注解中,把涉及到多数据源写入的方法的所有事务捆绑到一起遍历提交,做到“一荣俱荣,一损俱损”。

3 代码

3.1 定义注解

自定义注解类MultiTransactional,字段是事务管理器的对象名

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MultiTransactional {
    String[] transactionManagers();
}

3.2 aop切面

这里我用了aspectJ,在切面中定义了一个holder存放事务管理器和事务状态的键值对。

@Aspect
@Component
public class TransactionAspect {
    private static final ThreadLocal<Deque<Pair<DataSourceTransactionManager, TransactionStatus>>> STACK_THREAD_LOCAL = new ThreadLocal<>();

    @Resource
    private ApplicationContext applicationContext;

    @Pointcut("@annotation(com.zhaobo.multidstx.annotation.MultiTransactional)")
    public void txPointCut(){}

    /**
     * 声明事务
     * @param transactional
     */
    @Before(value = "txPointCut() && @annotation(transactional)")
    public void before(MultiTransactional transactional){
        // 根据设置的事务名称按顺序声明,并放到ThreadLocal里
        Deque<Pair<DataSourceTransactionManager, TransactionStatus>> stack = new LinkedList<>();
        // 先获取所有的txManager的name
        String[] transactionManagerNames = transactional.transactionManagers();
        for (String transactionManagerName : transactionManagerNames) {
            // 获取bean,并创建BeanDefinition
            DataSourceTransactionManager transactionManager = applicationContext
                    .getBean(transactionManagerName, DataSourceTransactionManager.class);
            DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
            // 设置非只读、隔离级别、传播行为
            definition.setReadOnly(false);
            definition.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
            definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
            // 获取事务状态
            TransactionStatus transactionStatus = transactionManager.getTransaction(definition);
            // 入栈
            stack.push(new Pair<>(transactionManager, transactionStatus));
        }
        // 加入到LoaclThread
        STACK_THREAD_LOCAL.set(stack);
    }

    /**
     * 提交事务
     */
    @AfterReturning(value = "txPointCut()")
    public void afterReturning(){
        // 弹栈,事务后进先出
        Deque<Pair<DataSourceTransactionManager, TransactionStatus>> pairStack = STACK_THREAD_LOCAL.get();
        // 从pair中取出 全部提交
        while (!pairStack.isEmpty()){
            Pair<DataSourceTransactionManager, TransactionStatus> pair = pairStack.pop();
            TransactionStatus transactionStatus = pair.getValue();
            DataSourceTransactionManager transactionManager = pair.getKey();
            transactionManager.commit(transactionStatus);
        }
        // 清空本地变量
        STACK_THREAD_LOCAL.remove();
    }

    @AfterThrowing(value = "txPointCut()")
    public void afterThrowing(){
        Deque<Pair<DataSourceTransactionManager, TransactionStatus>> pairStack = STACK_THREAD_LOCAL.get();
        // 全部弹栈
        while (!pairStack.isEmpty()){
            // 声明事务和提交事务或者回滚事务的顺序应该相反的,就是先进后出
            Pair<DataSourceTransactionManager, TransactionStatus> pair = pairStack.pop();
            DataSourceTransactionManager transactionManager = pair.getKey();
            TransactionStatus transactionStatus = pair.getValue();
            transactionManager.rollback(transactionStatus);
        }
        STACK_THREAD_LOCAL.remove();
    }
}

3.3 Service类

这里模拟异常抛出,注意在注解中加入方法中涉及到的事务管理器,并且多个事务管理器和SqlSessionTemplate要分别定义,在@MapperScan注解中指定不同的mapper扫描路径和sqlSessionTemplateRef引用,否则mybatis会报错。

@Service
public class BussinessService {

    @Resource
    private MessageMapper messageMapper;

    @Resource
    private UserMapper userMapper;

    @MultiTransactional(transactionManagers = {"transactionManager1", "transactionManager2"})
    public Boolean sendMessage(Message message, User user) {
        int ret1 = messageMapper.insertSelective(message);
//        int i = 1 / 0;
        int ret2 = userMapper.insertSelective(user);
//        int j = 1/0;

        // other http invoking method
        return ret1 + ret2 == 2;
    }
}

3.4 测试

@SpringBootTest
public class MultiDsTxApplicationTests {

    @Resource
    private BussinessService service;

    @Test
    public void test1() {
        Message message = new Message();
        message.setId(UUID.randomUUID().toString());
        message.setTitle("message.");
        message.setContent("this is a message");
        User user = new User();
        user.setAge(19);
        user.setId(UUID.randomUUID().toString());
        user.setName("lisi");
        Boolean aBoolean = service.sendMessage(message,user);
        System.out.println(aBoolean);
    }
}

当没有异常时可以正常插入:

user表:

image-20240114215620121

message表:

image-20240114215655249

当抛出异常时:

image-20240114215836354

此时两个数据库均没有插入。

4 结语

以上代码是本人在工作中碰到并使用过的,虽然最后换成了 baomidou 的 DynamicDatasource 的@DS+@DSTransactional注解,但是也具有一定借鉴价值。
代码已上传至GitHub:https://github.com/zhaobo97/multi-datasource.git

本博客内容仅供个人学习使用,禁止用于商业用途。转载需注明出处并链接至原文。

posted @ 2024-01-14 22:21  爱吃麦辣鸡翅  阅读(18)  评论(0编辑  收藏  举报