今天继续谈一下业务层改造相关的一个场景:领取红包。红包,是这几年最火的一类营销手段,从当年微信红包走红后,无论是电商还是互金,都开始加入了“红包”。红包场景本身并不复杂,但由于涉及到了资金的转移,就会引入事务一致性的问题,而且从我了解到的情况看,很多同学在处理这块业务的时候,刚开始都一不小心犯了错,这也是为什么我要选“红包”场景来展开谈的原因。
为了尽可能简化问题,以“小明领取现金红包20元”为例,整个事务包含如下操作:
检查20元红包是否有效
设置红包状态为“已领取”
小明账户余额加上20元
很明显,A,B,C3个操作,要么全部成功,要么全部失败。但在这个场景下,会有什么潜在的问题呢?
一, 1包多领,多个线程同时发送领取同一个红包的操作,红包金额重复添加。这个问题常见外部恶意攻击API,由于缺乏必要的数据一致性保护措施,读取脏数据,导致多领。
二,丢失更新,领取红包的同时,同时进行了购买操作,线程1的事务覆盖线程2事务已经提交的数据,造成线程2事务所做操作丢失:
在提出解决方案前,我们将后端系统架构分为两类:
单机版 Standalone,数据库相同,所有的业务操作在一个容器下。
微服务 红包操作,个人账户操作不在同一个容器内进行,分属不同的服务,拥有各自的数据库。
单机版的解决方案
如下图所示,在应用代码中,对红包与账户余额都上锁,确保红包对象同时只会被一个线程所操作。对红包加锁,如果有多余线程想操作红包,一定需要等待线程1被执行结束,这个时候红包的状态已经被更新到数据库中,根本上杜绝一包多领的情况。对账户余额加锁,如果余额账户在扣款的时候,确保领取红包的操作,不会读取脏数据,造成更新丢失。
微服务下的解决方案
微服务的解决方案引入了消息队列来进行处理。如果红包状态正常,并成功将状态至为“已领取”,且消息已经发送成功,用户服务端开始消费这条消息,如果这个时候出现消费失败或者消费超时,利用消息队列进行重试,直到用户端执行成功,考虑到“一包多领”的问题,整个过程中有可能会出现消息重复的问题。所以我们在微服务中,需要做到以下两点:
1. 消费端处理消息的业务逻辑保持幂等性。
2. 保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现。
在使用MQ过程中,需要注意以下几点:
一个应用尽可能用一个 Topic,消息子类型用 tags 来标识。只有发送消息设置了tags,消费方在订阅消息时,才可以利用 tags 在 broker 做消息过滤。
每个消息在业务层面的唯一标识码,要设置到 keys 字段,方便将来定位消息丢失问题。
消息发送成功或者失败,要打印消息日志,务必要打印 sendresult 和 key 字段。
对于消息不可丢失应用,务必要有消息重发机制。
总结
在OLTP系统领域,我们在很多业务场景下都会面临事务一致性方面的需求,一个看起来简单的功能,内部可能需要调用多个“服务”并操作多个数据库来实现,特别是高并发的情况下,更是考验我们架构中细节处理的时候。这里,没有一个标准的万能答案,不过只要我们能遵循基本的设计原则,都可以在场景下找到解决方案。