领红包场景的数据一致性解决方案

原创
2018/01/31 19:07
阅读数 754

 

需求背景

红包系统作为理财运营系统的重要基础服务支持了众多的运营活动,发放的红包也特别多。当用户在“我的红包”里领取红包时本质上相当于发起来一起"货币基金"购买行为,只是这个钱不是用户自己出的,而是从理财的公共运营账户出的。

整体业务流程大致如下:

红包系统

1、红包维度加全局锁,确保并发安全

2、判断红包的状态是否可以领取

3、如果可以领取更新数据库中该红包的状态为“领取中”

4、发送易钱袋充值消息

5、消费易钱袋充值成功消息,更新红包状态为领取成功

易钱袋系统

1、消费易钱袋充值消息

2、为指定用户充值易钱袋金额

3、充值成功发送易钱袋充值成功消息

 

这里存在一个问题:是先修改红包状态?还是先发送充值消息?

问题解析

场景一

先发消息,后修改红包状态

考虑如下时序图:

显然当充值消息发送成功后如果红包系统出现宕机或者数据库出现异常更新操作失败,那么此时消息已经被易钱袋系统消费,用户充值成功,造成自损。明显次方案不行。

场景二

先修改红包状态,再发送消息,这是我们平时使用最多的方式,伪代码如下:

try {
    boolean result = dao.upate(model);// 更新数据库
    if (result) {
        producer.send(msg);//发送消息
    }
    commit();//提交本地事务
} catch (Exception ex) {
    rollback();//回滚事务
}

可能出现的情况如下:

情况

合法

状态更新失败抛出异常,消息未发送

状态更新成功,发送消息失败抛出异常,事务回滚

状态更新成功,发送消息正常,事务提交

备注:此处假设采用带ACK机制的消息系统我们采用的是RabbitMQ可以保证投递的可靠性

这种写法看似完美,好像并不会出现问题,我们平时基本都是这么使用。

事实的确如此吗?请考虑如下时序图

在投递消息后到数据库commit操作之间如果红包系统出现宕机或网络故障数据库事务会因为连接异常关闭而被回滚。最终结果和场景一相同,造成自损情况。

虽然红包系统故障情况概率较小,但是在交易频繁,集群数量众多的应用中出现故障的概率会被放大,而红包系统基本符合这些条件,所以有必要采用更加稳健的方式支持红包的领取,避免自损的发生,毕竟地主家的余粮也不多啦。


解决方案

在和同事讨论了理财的其他业务场景后发现此需求具有一定的共性,所以我们在现有的消息中转系统Medusa中添加“外部事件表”的支持,原理类似于“二阶段提交“。

当业务进行本地事务操作之前先调用Medusa暴露的eventPrepare接口,接口返回后再进行本地事务操作,再根据本地事务操作结果发布commitEvent或cancelEvent消息。

Medusa会消费commitEvent和cancelEvent消息,如果是commit就发布消息并将事件状态设置为“已投递”,cancel就更新事件状态为“已取消”,保留一段时间后删除。

时序图如下:

事件补偿

当调用Medusa eventPrepare接口时,Medusa首先会将事件入库此时消息的的状态为“未投递”。

本地会有一个后台线程定时从数据库拉去已经过期的“未投递”状态事件,然后生成回查消息并投递到指定的回查队列中(业务方在调用eventPrepare通过参数指定),业务方会订阅该回查队列,根据业务的执行情况再次发送commitEvent/cancelEvnet消息。

时序图如下:

异常处理

场景一

调用Medusa接口提交事件失败,流程退出,用户重试。

场景二

事件提交成功,本地事务未执行,此时事件的状态为“未投递”,由事件补偿机制负责状态回查

场景三

事件提交成功、本地事务执行成功,发布commitEvent/cancelEvnet消息失败,情况类似于场景二由事件补偿机制负责状态回查

 

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部