分布式事务的一种解决思路

  昨晚某技术群里大家热火的在讨论分布式事务的问题,想起了自己前几年由于技术太渣也犯过很多相关错误,现结合自己之前一次BUG案例由感而写此文,希望对看到文章的同学们多少有些帮助(如果发现错误之处,欢迎交流)。

  一个注册业务,用户注册成功后,后台调用另外一个服务同步完成开通资金账户,后来加了一个需求同时还要把注册用户数据同步到另一个业务系统中。

  真实情况逻辑更复杂,现在简化方便描述后相关伪代码如下:

@Transactional
public void register(){
    //保存用户
    saveUser();
    //初始化账户
    initAccount();
    //推送用户信息
    pushUser();
}

  后面两个方法可以理解成是其他业务系统的远程服务;

  代码上线后一段时间内倒也平稳没出什么问题(用户数少),然而是bug早晚会复现的,最终在一个周末问题触发了,接到领导通知,网站不能注册,赶紧连上服务器看日志,发现日志中大量超时及mysql死锁异常,然后瞬间明白了问题原因,毕竟锅是自己引起的。

  pushUser()里面是一个请求其他业务系统的HTTP接口,此处当时没加connectTimeout超时限制,那天此业务系统接口异常,请求了60多秒还没响应,这个整体方法上还有一个大事务,接着就造成了大量死锁,再之后网站就不能注册。

  上面的这个示例可以说是分布式事务中常见的一类问题;一个业务的完成,要依赖于其他项目的多个远程服务;但上面的那种写法明显问题很大,极易引发各种BUG,至少存在以下问题:

  1. 事务范围过大

  2. 注册业务严重耦合其他业务系统接口

  3. 数据不一致性问题

 现在回想起来,当时果然是不知者无畏,啥代码都敢写。


 现在根据目前的认知水平针对上面问题,重新提供一个优化思路,使用MQ来解耦下面两个远程服务。

  1. 用户信息保存成功后,插一条记录到user_task_record表(user_id,account_flag,push_flag,create_time,update_time等字段,两个状态字段初始值默认为0);这两个操作在同一个事务内;

  2. 扔一条消息到MQ中;

  3. 消费者接受到广播消息后分别再处理initAccount、pushUser逻辑;相应消费者接收到消息处理成功后,修改user_task_record表对应状态字段为1;

  4. 失败重试机制,因为接口可能会有调用失败的情况,新增一个5分钟一次的定时任务,扫user_task_record表状态为0的记录,扔消息到MQ中;

  5. 如果业务重要还可以加入监控预警,设定一个阀值,如果发现user_task_record表中create_time大于阀值并且状态一直是0的记录,可以给相关人员短信,邮件预警。

 注意:由于有失败重试机制,所以业务系统的相关接口必须是幂等的(幂等很重要),即我可以调用多次,不会产生重复数据。在本例中我们的消费者接受到消息后,可以先从user_task表中查取下对应状态是否为1,如果是1,说明业务逻辑已经执行成功,只用确认下消息扔给下一个消费者处理;不等于1的就执行相应业务逻辑。

 

相关伪代码如下:

public void register(){
    //保存用户
    @Transactional
    saveUser();
        //生成一条消息
    producer.send(message);
}

简易流程图如下:

安利下mq,mq在实际开发中能帮我们很多忙:

  在传统的事务处理中,多个系统之间的交互耦合到一个事务中,响应时间长,影响系统可用性。引入分布式事务消息,业务系统和消息队列之间,组成一个事务处理,能保证分布式系统之间数据的最终一致;下游业务系统(订单交易、购物车、积分、其他)相互隔离,并行处理。

常见使用场景:

  1. 异步解耦

  2. 削峰填谷

  3. 异步通知

  4. 分布式事务处理

  5. ......

 

最后总结:

  1. 我们把2个同步远程调用方法改成异步了

  2. 事务范围变小了

  3. 引入MQ,把业务解耦了,保证了分布式系统事务的最终一致性

  4. 项目更高可用了,不会因为其他业务系统引起项目宕机不可用

 

posted @ 2019-05-27 22:41  myseries  阅读(742)  评论(2编辑  收藏  举报