万级TPS优惠券系统设计与实践

共 4899字,需浏览 10分钟

 ·

2024-11-13 08:45


👉目录


1 背景介绍

2 什么是优惠券系统?

3 优惠券创建

4 优惠券派发

5 后续优化




优惠券是电商常见的营销手段,是营销平台中的一个重要组成部分,既可以作为促销活动的载体,也是重要的引流入口。在刚刚过去的电商大促周期内,各大电商平台都有配置不同类目、价位的优惠券,吸引用户下单购买。

优惠券系统主要涵盖四个核心能力:创建、派发、使用、统计。本篇主要针对派发这部分,在系统设计和落地过程中遇到和解决的一些问题做一个简单记录,以便后来补缺。

关注腾讯云开发者,一手技术干货提前解锁👇





01



背景介绍

优惠券是电商常见的营销手段,是营销平台中的一个重要组成部分,腾讯云 MALL 也需要搭建优惠券相关的平台能力来更好的助力赋能商家的各种促销场景。




02



什么是优惠券系统?

这里找了几个电商平台的优惠券相关页面:



依次是某东、某宝、腾讯云 MALL ,这里各式各样的优惠券背后涉及的相关系统,可以统称为优惠券系统。所以单说优惠券系统是一个很庞大的系统,这里收敛一下讲其中主要有四大核心能力:创建、派发、使用、统计。


   2.1 系统架构



本篇主要介绍的是平台如何创建和派发优惠券到用户账户的券包里,即上面提到的四大核心能力中的创建和派发。




03



优惠券创建

   3.1 核心概念


先简单了解一下两个概念:优惠券批次、优惠券。

  1. 优惠券批次:一批相同优惠券的生成模版。

  2. 优惠券:根据批次信息生成,优惠券与批次的对应关系是 N:1。



   3.2 批次表核心字段


  1. 批次 ID ;

  2. 优惠券名称;

  3. 优惠券类型;

  4. 库存数量;

  5. 优惠规则如:满减,满折等;

  6. 生效规则:固定生效时间、领取后生效时间等;

  7. 领取规则:批次每天限领数量、用户每天限领数量、用户总限领数量等;

  8. 使用规则:指定商家、指定商品、指定类目、指定场景等。


   3.3 优惠券表核心字段


  1. 优惠券 ID:分布式 ID 全局唯一

  2. 批次 ID;

  3. 用户 ID;

  4. 优惠券状态;

  5. 上下文信息。


批次表的数据写入主要是 B 端后台管理来操作,这里不多赘述。


优惠券表数据主要通过派发动作与用户关联后写入,后面会展开介绍。


   3.4 B 端配置效果





04



优惠券派发

   4.1 两大主要问题


  1. 库存管理,如何防止超发,保障库存安全。

  2. 场景复杂,如何支持高并发及瞬时高流量毛刺场景。


流量毛刺示意:



   4.2 主流程拆解


  1. 库存扣减;

  2. 生成优惠券。


   4.2.1 库存扣减


  1. 直接用数据库做库存管理,面临问题:高并发导致数据库崩溃、性能瓶颈明显。

  2. 缓存做库存管理:数据不一致、穿透、击穿、雪崩等问题。


最终方案:


Redis+Lua+库存异步分段增补:

  1. Redis+Lua:支持高并发库存扣减。

  2. 库存异步分段增补:支持高并发的前提下灵活分配库存。


Lua 脚本示意(示意代码仅供学习参考):


--批次的HashKeylocal stockKey = KEYS[1];
--Argv 参数local stockId = ARGV[1];local couponId = ARGV[2];local uid = ARGV[3];--该批次当天最大发放量local maxByDay = ARGV[4];-- 每人限领local maxByUser = ARGV[5];--当前时间Strlocal crtDateStr = ARGV[6];-- 每人每日限领local dailyMaxByUser = ARGV[7];
stockId = tonumber(stockId);maxByUser = tonumber(maxByUser);maxByDay = tonumber(maxByDay);dailyMaxByUser = tonumber(dailyMaxByUser);
--StockKey nilif not stockKey then return '-4'end--Argv nilif not stockId or not couponId or not uid or not maxByUser or not maxByDay or not crtDateStr or not dailyMaxByUser then return '-5'end
local leftAmountField = 'left_amount';local res = redis.call("HMGET", stockKey, leftAmountField, crtDateStr);local leftAmount = tonumber(res[1]);local crtDispatchAmount = tonumber(res[2]);local couponIdSetKey = stockKey .. ':coupon:zset';
--优惠券ID是否已经分配库存local score = redis.call("ZSCORE", couponIdSetKey, couponId);-- couponId 已经存在if score then return '-6';end
-- 库存不足if not leftAmount or leftAmount <= 0 then return '-3';end
--达到当天发放上限if crtDispatchAmount and crtDispatchAmount >= maxByDay then return '-1';end
-- 该批次每人每日领取数量HashKeylocal dailyUserAcquireNumKey = stockKey .. ":user:acquire:" .. crtDateStr;if dailyMaxByUser > 0 then local dailyUserAcquireNum = redis.call("HGET", dailyUserAcquireNumKey, uid); dailyUserAcquireNum = tonumber(dailyUserAcquireNum); -- 达到每人每日领取上限 if dailyUserAcquireNum and dailyUserAcquireNum >= dailyMaxByUser then return '-7' endend
--该批次用户领取数量HashKeylocal userAcquireNumKey = stockKey .. ":user:acquire";local usrAcquireNum = redis.call("HGET", userAcquireNumKey, uid);usrAcquireNum = tonumber(usrAcquireNum);
--达到用户领取上限if usrAcquireNum and usrAcquireNum >= maxByUser then return '-2'end
--扣减库存-1local leftAmountAfterOp = redis.call("HINCRBY", stockKey, leftAmountField, -1);--当天发放量+1local crtDispatchAmountAfterOp = redis.call("HINCRBY", stockKey, crtDateStr, 1);--当前用户发放量+1local usrAcquireNumAfterOp = redis.call("HINCRBY", userAcquireNumKey, uid, 1);-- 当前用户当天发放量+1local dailyUserAcquireNumAfterOp = redis.call("HINCRBY", dailyUserAcquireNumKey, uid, 1);
redis.call("ZADD", couponIdSetKey, uid, couponId);
--返回操作之后的上下文,缓存中剩余量,当天已经发放量,用户已经领取量,用户当天已经领取量return '0|' .. leftAmountAfterOp .. '|' .. crtDispatchAmountAfterOp .. '|' .. usrAcquireNumAfterOp .. '|' .. dailyUserAcquireNumAfterOp


分段增补示意:



介绍:


每当 Redis 剩余库存小于 M 个时,异步从数据库增补 N 个库存到 Redis 里,保证 Redis 库存数量一直小于等于数据库。


  1. 屏蔽流量直接打到数据库,减轻数据库压力。

  2. Redis+数据库控制,双重保证不超发。

  3. 库存增补的 M 和 N 可以根据实际业务需要灵活调配。

    1. M 可以理解为业务发券速率兜底。比如:发快补慢提示无库存等。

    2. N 可以理解为极端情况下最大允许丢失的库存数量。


主流程如图:



   4.2.2 生成优惠劵


  1. 扣减库存成功同步生成优惠券信息写入数据库,同样会面临高并发导致数据库崩溃的问题,系统瓶颈明显不可取。

  2. 这里再加缓存的话,解决缓存问题会让业务变得更复杂,结合第二个主要问题:瞬时高流量毛刺。


最终方案:


  1. 库存扣减成功后异步生成优惠券,达到整体流程支持高并发,且可以解决流量毛刺的问题。PS:分布式事务问题。

  2. 结合自身业务场景,对比权衡了多种分布式事务解决方案,最终选用本地事务表+最大努力通知来解决分布式事务问题。



介绍:


通过消息异步生成优惠券落库处理来支持高并发,引入一张本地事务表达成数据的最终一致性。


主流程如图:



数据参考:


  1. 结合自身实际业务测试环境压测目标 1W/TPS 示意(系统整体支持横向扩容进一步提升性能)。


示意:




05



后续优化

   5.1 热点问题


回顾整体方案,同批次场景仍存在热点问题,针对这里可以做一些优化来提升系统性能,如:资源分桶,聚合扣减,热点更新技术等。如何解决热点问题?下面结合发券场景列举几种方案做一下对比介绍,可供参考。


热点示意:



   5.1.1 资源分桶


简介同一个批次的库存分成多份,通过分散库存扣减请求提升性能。



优势:水平扩展能力强。


重点关注:

  1. 分桶 Key 路由倾斜问题,理想情况是所有 Key 平均对应分桶。

  2. 各桶之间库存倾斜与性能权衡的问题,理想情况是所有分桶消耗速率一致。


   5.1.2 聚合扣减


简介:聚合相同批次的请求统一扣减,通过聚合请求量来提升服务整体性能。



优势:前置聚合请求利于提高服务稳定性。


重点关注:

  1. 聚合策略的设计需要在系统稳定性和性能上做取舍。

  2. 临界库存如何与聚合策略适配的问题。


   5.1.3 热点更新


简介:热点更新技术详细介绍见腾讯云文档:

https://cloud.tencent.com/document/product/237/13402


优势:适用数据库锁层面的热点优化。


重点关注:

  1. 依赖数据库适用场景较单一。


小结:每种方案的实现均有利有弊,最后都需要在系统性能和复杂度上做权衡取舍,最终选出契合自身实际业务的才是最好的方案。


-End-
原创作者|管振盼


 


关于优惠券系统的设计,你还有哪些心得体会?欢迎评论留言。我们将选取1则优质的评论,送出腾讯Q哥公仔1个(见下图)。11月20日中午12点开奖。


📢📢欢迎加入腾讯云开发者社群,享前沿资讯、大咖干货,找兴趣搭子,交同城好友,更有鹅厂招聘机会、限量周边好礼等你来~


(长按图片立即扫码)


浏览 1028
12点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
12点赞
评论
收藏
分享

手机扫一扫分享

分享
举报