分布式事务TCC实现思路
A{
B{远程方法} 成功
c{本地方法} 失败, 如何回滚远程方法B的操作
}
这里涉及到两个系统的事务,我们来用Redis 设计一套TCC回滚方案。
@自定义分布式事务注解(ID=null)
@Transactional
A{
创建唯一标识也好,假设20190503 ,并动态修改注解内的值ID= 20190503
---执行其他业务逻辑
方法B开始往redis 写入预写日志:
接着调用方法B();
方法B结束 往redis 写入写结束日志:
---执行其他业务逻辑
}
B开始写入
Key=20190503 ,value=[
{"统一事务ID”: "20190503",“方法标识”:“ beanName.类名.方法名B”,“事务开始标识”:“1”,“事务结束标识”:“0” , “参数”:“Object”}]
B成功后修改
Key=20190503 ,value=[
{"统一事务ID”: "20190503",“方法标识”:“ beanName.类名.方法名B”,“事务开始标识”:“1”,“事务结束标识”:“1” , “参数”:“Object”}]
如果出现B成功,A最后失败了,结果就是
A 失败的结果就没来的及修改事务结束标识,结果就是如下
Key=20190503 ,value=[
{"统一事务ID”: "20190503",“方法标识”:“ beanName.类名.方法名B”,“事务开始标识”:“1”,“事务结束标识”:“0” , “参数”:“Object”}]
接下来就是失败回滚逻辑
写一个对@自定义分布式事务注解(ID=null) 的AOP 切面,在这里做异常捕抓,
通过注解中的ID 获取Redis 中的
Key=20190503 ,value=[
{"统一事务ID”: "20190503",“方法标识”:“ beanName.类名.方法名B”,“事务开始标识”:“1”,“事务结束标识”:“1”, “参数”:“Object”}]
这个时候找到这个list中“事务结束标识”:“1”中的对象,通过反射加spring容器中加载对对应的bean来执行, 类名.方法名B Cancel(Object)方法,这个方法执行回滚逻辑,具体实现由程序员实现。
具体执行代码如下:
Object beanObject = SpringContextHolder.getBean("beanName");只能从容器中获取,不能用反射获取对象,因为是代理接口。
Class<?> clazz = Class.forName("类全名");
// 获取本类的所有方法,存放入数组
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.getName().equals("方法名"+“Cancel”)) {
Object resul = method.invoke(beanObject,"参数");
}
}
已测可行。
要是B操作是一个更新操作或者删除操作,只需要更新之前将可以从数据库中加载这条数据出来缓存到Redis几秒钟即可,xxxCancel(), 知道了之前的数据是怎么样的就可以更新了。
现实中我们有另外一种场景,就是本地方法A执行完了发送消息,让消费者B执行另外一个远程服务,阿里给出的解决方案是
用RocketMQ 来保证 A本地事务和发送消息的一致性,不保证A和B的一致性,换句话就是 它能保证 A成功了消息也一定成功,B能不能成功消费它就不保证了,如果A成功,B失败了,你只能人工解决了,记录日志,发送通知啥的,自行脑补。
它的大概原理是这样的:
方法A开始执行, 发送Prepared消息,可以理解为发送了一个状态为0 的消息,这时消费者是不会消费这条消息的。
接着执行方法A的其他逻辑,执行完了再次发送状态为1的确认消息。这个时候消费者可以消费这条消息了。
如果这条状态为1的消息发送失败,或者根本没就运行到这里A就出异常了,那么mq服务会按一分钟检测一次,发现状态为0 的消息后,向生产者询问,如果没人回答,则mq认为这条消息无效,如果回答OK,则mq会修改这条消息的状态为1.大概就是这样一个原理。接下来就是B能不能消费的问题了,如果是网络问题就重试,反正B能不能成功无法保证。
要保证回滚A太复杂了,得不偿失,这是阿里给的答案,人工处理。