我一个朋友的公司基于实际业务的考虑,选择了多个单体项目来组建成一个分布式系统。(对于目前来说分布式的系统最好采用微服务的架构来实现项目搭建。但基于许多客户只能采用内网的使用,微服务反而会影响项目的复杂度,技术只是为业务服务,适合自己的才是最好的。所以就最终选择了多个SpringBoot项目使用(http请求+mq请求)来组件成分布式系统)。
举例:在一个场景下,一个客户冲了10块钱,奖励给10个积分。然后获取用户的积分数量来扣减本次的支付金额。
成功的流程图如下:
在上图中可以看到,每一个步骤都有可能修改了自己系统的数据,如果此时一个系统出现了故障,那么基于单体SpringBoot的事务就只能回滚自己的系统数据。另一个系统无法感知事务异常。所以就会导致数据的错误。
支付异常的流程图如下:
上文说了,由于客户原因,微服务架构不适用与本系统。所以采用了自己编写回滚方案来解决两个分布式系统的事务回滚问题。本方案采用MQ请求+全局事务id+事务日志表来实现数据回滚。
答:属于BASE理论。ACID 和 BASE 是分布式系统中两种不同级别的一致性理论,在分布式系统中,ACID有更强的一致性,但可伸缩性非常差,仅在必要时使用;BASE的一致性较弱,但有很好的可伸缩性,还可以异步批量处理;大多数分布式事务适合 BASE。众多业务中一般不会要求强一致性,只要保证最终一致性就可以了!
详细过程图如下:
表结构:
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`xid` varchar(100) NOT NULL COMMENT '全局事务ID',
`object_json` text NOT NULL COMMENT '对象json',
`object_name` varchar(128) NOT NULL COMMENT '对象名',
`rollback_info` text DEFAULT NULL COMMENT '回滚信息',
`log_status` int(11) NOT NULL DEFAULT 0 COMMENT '状态,0正常,1全局已完成, 2回滚失败',
`log_created` datetime(6) NOT NULL COMMENT '创建时间',
`log_modified` datetime(6) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `ux_undo_log` (`xid`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COMMENT='全局事务回滚日志表';
1:先通过uuid生成全局事务id。
String xid = UUID.randomUUID().toString();
2:调用积分系统时将积分对象josn化,然后插入到事务日志表。
SimpleUndoLog simpleUndoLog=new SimpleUndoLog();
simpleUndoLog.setXid(xid); //全局事务id
simpleUndoLog.setObjectJson(JSON.toJSONString(integralDO)); //将积分对象json化。
simpleUndoLog.setObjectName(integralDO.getClass().getName()); //获取积分对象的类名
simpleUndoLog.setLogCreated(new Date());
simpleUndoLog.setLogModified(new Date());
simpleUndoLogMapper.insertSelective(simpleUndoLog); //插入事务日志表
3:支付系统出现异常,在catch代码块中发送mq请求。
try {
支付失败
}catch (Exception e){
//发送带全局事务id的MQ请求
rocketMQTemplate.convertAndSend("undo_log",event.getXid());
throw new RuntimeException(e.getMessage());
}
4:在积分系统中接收到回滚mq,通过全局事务id查找出对应的json数据,恢复成对象,然后回滚。
@Slf4j
@Service
@RocketMQMessageListener(
topic = "undo_log", //接收mq消息的分组
consumerGroup = "group_undo_log",
consumeMode = ConsumeMode.ORDERLY
)
@Resource
private SimpleUndoLogMapper simpleUndoLogMapper;
public class UndoLogConsumer implements RocketMQListener {
//创建事务日志对象
SimpleUndoLog undoResult=new SimpleUndoLog();
//MQ传过来的全局事务id
undoResult.setXid(xid);
try {
//通过事务id查找事务日志表,获取积分快照
List simpleUndoLogs = simpleUndoLogMapper.selectByXid(xid);
for (SimpleUndoLog simpleUndoLog : simpleUndoLogs) {
//获取事务回滚前的快照,进行更新
TntegralDO updateDO= JSON.parseObject(simpleUndoLog.getObjectJson(), new TypeReference() {});
updateMapper.update(updateDO);
}
}catch (Exception e){
undoResult.setRollbackInfo(e.getMessage());
undoResult.setLogStatus(UndoEnum.FAIL.getStatus());
//回滚失败
simpleUndoLogMapper.updateByXid(undoResult);
}
undoResult.setLogStatus(UndoEnum.SUCCESS.getStatus());
//事务回滚完成
simpleUndoLogMapper.updateByXid(undoResult);
}
此时就完成了一个完整的分布式系统的异常回滚。
优点:该方案实现简单,只需一张快照日志表即可实现通用的各种事务回滚。后续如果出现回滚失败还能通过表状态方便手动处理。
缺点:该方案无法做到分布式事务的高可用,只能勉强达到最终一致性。还有一种可能如果短时间其他操作影响了快照数据也会导致类似CAS中的ABA问题。