电商系统的促销手段:优惠券,拼团,砍价,老带新。
优惠券种类
满减券,直减券,折扣券。
优惠券系统的核心流程
发券,领券,用券。
优惠券分库分表
用户优惠券与用户ID关联,并且用户ID是贯穿整个系统的重要字段,因此使用用户ID作为分库分表的路由因子。
这样可以保证同一个用户路由至相同的库表,既有利于数据的聚合,也方便用户数据的查询。
保证领券校验的准确性
领券时,需要严格校验优惠券的各种属性是否满足:
- 比如领取对象、各种限制条件等。
- 其中,比较关键的是库存和领取数量的校验。
- 因为在高并发的情况下,需保证数量校验的准确性,不然很容易造成用户超领。
存在这样的场景:
- A用户连续发起两次领取券C的请求,券C限制每个用户领取一张。
- 第一次请求通过了领券数量的校验,在用户优惠券未落库的情况下,如果不做限制,第二次请求也会通过领券数量的校验。
- 这样A用户会成功领取两张券C,造成超领。
采用分布式锁方案:
在校验用户领券数量前先尝试获取分布式锁,优惠券发放成功后释放锁,保证用户领取同一张券时不会出现超领。
库存扣减
领券要进行库存扣减,常见库存扣减方案有两种:
方案一:
- 数据库扣减。
缺点主要有两点:
库存是数据库中的单个字段,在更新库存时,所有的请求需要等待行锁。
- 一旦并发量大了,就会有很多请求阻塞在这里,导致请求超时,进而系统雪崩。
频繁请求数据库,比较耗时,且会大量占用数据库连接资源。
方案二:
- 基于
Redis
实现库存扣减操作。将库存放到缓存中,利用
Redis
的Incrby
特性来扣减库存。缺点是:系统流程会比较复杂,而且需要考虑缓存丢失或宕机数据恢复的问题,容易造成库存数据不一致。
库存拆分:
将单库存字段分散成多库存字段,分散数据库的行锁,减少并发量大的情况数据库的行锁瓶颈。
库存数更新后,会将库存平均分配成M份,初始化更新到库存记录表中。
用户领券,随机选取库存记录表中已分配的某一库存字段(共M个)进行更新,更新成功即为库存扣减成功。
- 同时,定时任务会定期同步已领取的库存数。
相比方案一,该方案突破了数据库单行锁的瓶颈限制,且实现简单,不用考虑数据丢失和不一致的问题。
一键领取多张券
在对接的业务方的领券场景中,存在用户一键领取多张券的情形。
因此统一领券接口需要支持用户一键领券,除了领取同一券模板的多张,也支持领取不同券模板的多张。
- 一般来说,一键领取多张券指领取不同券模板的多张。
在实现过程中,需要注意以下几点:
如何保证性能
领取多张券,如果每张券分别进行校验、库存扣减、入库,那么接口性能的瓶颈卡在券的数量上,数量越多,性能直线下降。
两个措施:
批量操作
- 从发券流程来看,瓶颈在于券的入库。
- 领券是实时的(异步的话,不能实时将券发到用户账户下,影响到用户的体验还有券的转化率)。
- 券越多,入库时与数据库的IO次数越多,性能越差。
- 批量入库可以保证与数据库的IO的次数只有一次,不受券的数量影响。
- 如上所述,用户优惠券数据做了分库分表,同一用户的优惠券资产保存在同一库表中,因此同一用户可实现批量入库。
限制单次领券数量
- 设置阀值,超出数量后,直接返回,保证系统在安全范围内。
保证高并发情况下,用户不会超领:
假如用户发起请求一键领取A/B/C/D四张券,同时活动系统给用户发放券A,这两个领券请求是同时的。
- 其中,券A限制了每个用户只能领取一张。
按照前述采用分布式锁保证校验的准确性,两次请求的分布式锁的
Key
分别为:用户
id+A_id+B_id+C_id+D_id
用户
id+A_id
这种情况下,两次请求的分布式锁并没有发挥作用,因为锁
Key
是不同,数量校验依旧存在错误的可能性。为避免批量领券过程中用户超领现象的发生,在批量领券过程中,对分布锁的获取进行了改造。
上例一键领取A/B/C/D四张券,需要批量获取4个分布式锁,锁
Key
为:用户
id+A_id
用户
id+B_id
用户
id+C_id
用户
id+D_id
获取其中任何一个锁失败,即表明此时该用户正在领取其中某一张券,需要自旋等待(在超时时间内)。
获取所有的分布式锁成功,才可以进行下一步。
领券接口幂等性
在网络超时、异常情况下,领券结果没有及时返回,业务方会进行领券重试。
- 如果接口不保证幂等性,会造成超发。
幂等性的实现有多种方案,可以利用数据库的唯一索引来保证幂等。
如何给大量用户发券?
异步发送。
券过期
券过期是一个状态推进的过程,使用
RocketMQ
来实现。由于
RocketMQ
支持的延时消息有最大限制,而卡券的有效期不固定,有可能会超过限制。
- 所以将卡券过期消息循环处理,直到卡券过期。