手动后置提交——多数据源事务的实现
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表:
message表:
当抛出异常时:
此时两个数据库均没有插入。
4 结语
以上代码是本人在工作中碰到并使用过的,虽然最后换成了 baomidou 的 DynamicDatasource 的@DS
+@DSTransactional
注解,但是也具有一定借鉴价值。
代码已上传至GitHub:https://github.com/zhaobo97/multi-datasource.git
本博客内容仅供个人学习使用,禁止用于商业用途。转载需注明出处并链接至原文。