昨晚某技术群里你们热火的在讨论分布式事务的问题,想起了本身前几年因为技术太渣也犯过不少相关错误,现结合本身以前一次BUG案例由感而写此文,但愿对看到文章的同窗们多少有些帮助(若是发现错误之处,欢迎交流)。mysql
一个注册业务,用户注册成功后,后台调用另一个服务同步完成开通资金帐户,后来加了一个需求同时还要把注册用户数据同步到另外一个业务系统中。sql
真实状况逻辑更复杂,如今简化方便描述后相关伪代码以下:服务器
@Transactional public void register(){ //保存用户 saveUser(); //初始化帐户 initAccount(); //推送用户信息 pushUser(); }
后面两个方法能够理解成是其余业务系统的远程服务;异步
代码上线后一段时间内倒也平稳没出什么问题(用户数少),然而是bug迟早会复现的,最终在一个周末问题触发了,接到领导通知,网站不能注册,赶忙连上服务器看日志,发现日志中大量超时及mysql死锁异常,而后瞬间明白了问题缘由,毕竟锅是本身引发的。分布式
pushUser()里面是一个请求其余业务系统的HTTP接口,此处当时没加connectTimeout超时限制,那天此业务系统接口异常,请求了60多秒还没响应,这个总体方法上还有一个大事务,接着就形成了大量死锁,再以后网站就不能注册。优化
上面的这个示例能够说是分布式事务中常见的一类问题;一个业务的完成,要依赖于其余项目的多个远程服务;但上面的那种写法明显问题很大,极易引起各类BUG,至少存在如下问题:网站
事务范围过大spa
注册业务严重耦合其余业务系统接口日志
数据不一致性问题code
如今回想起来,当时果真是不知者无畏,啥代码都敢写。
如今根据目前的认知水平针对上面问题,从新提供一个优化思路,使用MQ来解耦下面两个远程服务。
用户信息保存成功后,插一条记录到user_task_record表(user_id,account_flag,push_flag,create_time,update_time等字段,两个状态字段初始值默认为0);这两个操做在同一个事务内;
扔一条消息到MQ中;
消费者接受到广播消息后分别再处理initAccount、pushUser逻辑;相应消费者接收到消息处理成功后,修改user_task_record表对应状态字段为1;
失败重试机制,由于接口可能会有调用失败的状况,新增一个5分钟一次的定时任务,扫user_task_record表状态为0的记录,扔消息到MQ中;
若是业务重要还能够加入监控预警,设定一个阀值,若是发现user_task_record表中create_time大于阀值而且状态一直是0的记录,能够给相关人员短信,邮件预警。
注意:因为有失败重试机制,因此业务系统的相关接口必须是幂等的(幂等很重要),即我能够调用屡次,不会产生重复数据。在本例中咱们的消费者接受到消息后,能够先从user_task表中查取下对应状态是否为1,若是是1,说明业务逻辑已经执行成功,只用确认下消息扔给下一个消费者处理;不等于1的就执行相应业务逻辑。
相关伪代码以下:
public void register(){ //保存用户 @Transactional saveUser(); //生成一条消息 producer.send(message); }
简易流程图以下:
安利下mq,mq在实际开发中能帮咱们不少忙:
在传统的事务处理中,多个系统之间的交互耦合到一个事务中,响应时间长,影响系统可用性。引入分布式事务消息,业务系统和消息队列之间,组成一个事务处理,能保证分布式系统之间数据的最终一致;下游业务系统(订单交易、购物车、积分、其余)相互隔离,并行处理。
常见使用场景:
异步解耦
削峰填谷
异步通知
分布式事务处理
......
最后总结:
咱们把2个同步远程调用方法改为异步了
事务范围变小了
引入MQ,把业务解耦了,保证了分布式系统事务的最终一致性
项目更高可用了,不会由于其余业务系统引发项目宕机不可用