vivo 全球商城:优惠券系统架构设计与实践( 二 )


用户优惠券与用户id关联 , 并且用户id是贯穿整个系统的重要字段 , 因此使用用户id作为分库分表的路由因子 。这样可以保证同一个用户路由至相同的库表 , 既有利于数据的聚合 , 也方便用户数据的查询 。
假设共分N个库M个表 , 分库分表的路由策略为:

库后缀databaseSuffix = hash(userId) / M %N
表后缀tableSuffix = hash(userId) % M
 

vivo 全球商城:优惠券系统架构设计与实践

文章插图
 
3.2 优惠券发放方式设计
为满足各种不同场景的发券需求 , 优惠券系统提供三种发券方式:统一领券接口、后台定向发券、券码兑换发放 。
3.2.1 统一领券接口
保证领券校验的准确性
领券时 , 需要严格校验优惠券的各种属性是否满足:比如领取对象、各种限制条件等 。其中 , 比较关键的是库存和领取数量的校验 。因为在高并发的情况下 , 需保证数量校验的准确性 , 不然很容易造成用户超领 。
存在这样的场景:A用户连续发起两次领取券C的请求 , 券C限制每个用户领取一张 。第一次请求通过了领券数量的校验 , 在用户优惠券未落库的情况下 , 如果不做限制 , 第二次请求也会通过领券数量的校验 。这样A用户会成功领取两张券C , 造成超领 。
为了解决这个问题 , 优惠券采用的是分布式锁方案 , 分布式锁的实现依赖于redis 。在校验用户领券数量前先尝试获取分布式锁 , 优惠券发放成功后释放锁 , 保证用户领取同一张券时不会出现超领 。上面这种场景 , 用户第一次请求成功获取分布式锁后 , 直至第一次请求成功释放已获取的分布式锁或超时释放 , 不然用户第二次请求会获取分布式锁失败 , 这样保证A用户只会成功领取一张 。
库存扣减
领券要进行库存扣减 , 常见库存扣减方案有两种:
方案一:数据库扣减 。
扣减库存时 , 直接更新数据库中库存字段 。
 
【vivo 全球商城:优惠券系统架构设计与实践】该方案的优点是简单便捷 , 查验库存时直接查库即可获取到实时库存 。且有数据库事务保证 , 不用考虑数据丢失和不一致的问题 。
 
缺点也很明显 , 主要有两点:
1)库存是数据库中的单个字段 , 在更新库存时 , 所有的请求需要等待行锁 。一旦并发量大了 , 就会有很多请求阻塞在这里 , 导致请求超时 , 进而系统雪崩 。
2)频繁请求数据库 , 比较耗时 , 且会大量占用数据库连接资源 。
方案二:基于redis实现库存扣减操作 。
将库存放到缓存中 , 利用redis的incrby特性来扣减库存 。
 
该方案的优点是突破数据库的瓶颈 , 速度快 , 性能高 。
 
缺点是系统流程会比较复杂 , 而且需要考虑缓存丢失或宕机数据恢复的问题 , 容易造成库存数据不一致 。
 
从优惠券系统当前及可预见未来的流量峰值、系统维护性、实用性上综合考虑 , 优惠券系统采用了方案一的改进方案 。改进方案是将单库存字段分散成多库存字段 , 分散数据库的行锁 , 减少并发量大的情况数据库的行锁瓶颈 。
vivo 全球商城:优惠券系统架构设计与实践

文章插图
 
库存数更新后 , 会将库存平均分配成M份 , 初始化更新到库存记录表中 。用户领券 , 随机选取库存记录表中已分配的某一库存字段(共M个)进行更新 , 更新成功即为库存扣减成功 。同时 , 定时任务会定期同步已领取的库存数 。相比方案一 , 该方案突破了数据库单行锁的瓶颈限制 , 且实现简单 , 不用考虑数据丢失和不一致的问题 。
一键领取多张券
在对接的业务方的领券场景中 , 存在用户一键领取多张券的情形 。因此统一领券接口需要支持用户一键领券 , 除了领取同一券模板的多张 , 也支持领取不同券模板的多张 。一般来说 , 一键领取多张券指领取不同券模板的多张 。在实现过程中 , 需要注意以下几点:


推荐阅读