分布式事务之本地消息表实践

摘要

分布式系统之前除了同步方式通过RPC框架调用接口,还有很多通讯是异步的方式通过MQ中间件完成的。本文主要介绍后者实现分布式事务的一种方案,核心思想是通过记录本地消息日志表的方式,借助本地事务和MQ消息中间件的可靠性来实现分布式系统中多个节点间状态的最终一致。

方案设计

消息发布方
发布方的数据库中增加消息日志表,将业务操作和插入消息日志放在同一个数据库事务中。通过后台线程的定时任务不断从消息日志表中读取消息,并投递到MQ中间件。
消息订阅方
为了防止重复消费,订阅方数据库需要增加消息接收表来记录已消费的消息。将业务操和插入消息接收记录放在同一个数据库事务中。事务成功则调用发布方的回调,更新发布方消息日志表中的消息状态。
分布式事务之本地消息表实践_第1张图片

业务场景

现在有电话营销系统(后面简称电销系统)和调度平台两个系统。电销系统处理具体电销案件作业逻辑,依赖于调度平台将案件分配给人处理。电销系统中有marketing_task表维护电销案件,调度平台有dispatching_task表维护包括电销系统、催收系统、审批系统等业务系统在内的多种业务案件。
电销系统生成电销案件后,是通过MQ中间件将案件信息推送调度平台,后者异步将案件保存并完成后续分配。

下面我们就来看下如何通过本地消息表方案来保证电销系统和调度平台之间,案件状态的最终一致。

方案实践

通过上面描述,很明显电销系统是消息生产方。在电销数据库中业务表marketing_task对应的bean是Task,我们创建再一个本地消息表trans_message,对应bean是TransMessage。下面代码中我们定义一个推送电销任务(案件)的本地事务:将电销案件表中指定ID的案件状态更新为“处理中”,同时记录一条消息日志,将本地事务ID,电销案件ID,消息状态插入trans_message表。

[spring+mybatis]
@Transactional
public void pushTask(String caseNo){
    // 生成本地事务uuid
    String xid = getUUID();
    // 更新电销案件表,将案件设为处理中
    Task task = new Task();
    task.setId(caseNo);
    task.setStatus(TaskStatus.HANDLING);
    taskMapper.updateByPrimaryKeySelective(task); 
    // 插入消息日志     
    TransMessage tMsg = new TransMessage();
    tMsg.setXid(xid);
    tMsg.setCaseNo(caseNo);
    tMsg.setStatus(1);//1表示未消费
    transMessageMapper.insertSelective(tMsg); 
}  

电销后台定时任务,每10分钟查询trans_message表,将status等于1的消息内容投递到MQ中间件,如果投递失败就重新发送。允许重复发送,但不允许漏发。

再来看看作为消费方的调度平台。调度数据库中保存各业务系统案件的表是dispatching_task,对应bean是DispatchingTask,我们再创建一个消息接收记录表trans_recv_log,对应bean是TransRecvLog。调度平台消费消息后获取消息内容,定义创建调度案件事务: 插入一个调度任务,对应业务案件号为消息中传来的电销案件号,然后将案件号和电销的事务ID插入到消息接收表中,表示已成功消费了这消息。

@Transactional
public void createDispatchingTask(String xid, String caseNo){
    //trans_recv_log中查询如果有记录则跳过
    List<TransRecvLog> recvlogs = queryTransRecvLog(xid);
    if(recvlogs !=null && recvlogs.length>0){
        return;
    }
    
    // 生成调度案件
    DispatchingTask task = new DispachingTask();
    task.setCaseNo(caseNo);
    dispachingTaskMapper.insertSelective(task);
   // 保存记录到trans_recv_log
   TransRecvLog recvLog = new TransRecvLog();
   recvLog.setXid(xid);
   recvLog.caseNo(caseNo);
   transRecvLogMapper.insertSelective(recvLog);
}

调度平台成功创建了调度任务后,需要电销系统更新tran_message表中对应的消息状态,这个怎么实现呢?

优化下电销后台定时任务就可以了,在trans_message表中查询到status等于1的消息后,先调用调度接口查询下trans_recv_log表中有无记录, 如果已经有了就将trans_message表的消息日志状态更新为2(已消费);否则再投递消息到MQ中间件

总结

本地消息表的方案借助本地事务和消息中间件的可靠性来保证分布式系统中两个异步通讯的节点状态的最终一致性。本文通过一个具体的业务场景展示如果实现本地消息表的方案,重点描述了本地事务。基于的假设是消息中间件是高可用,关于如何保证消息中间件的高可用就是另一个话题了。

参考资料
聊聊分布式事务,再说说解决方案

你可能感兴趣的:(后端技术,学习笔记)