万级TPS优惠券系统设计与实践
共 4899字,需浏览 10分钟
·
2024-11-13 08:45
👉目录
1 背景介绍
2 什么是优惠券系统?
3 优惠券创建
4 优惠券派发
5 后续优化
01
优惠券是电商常见的营销手段,是营销平台中的一个重要组成部分,腾讯云 MALL 也需要搭建优惠券相关的平台能力来更好的助力赋能商家的各种促销场景。
02
这里找了几个电商平台的优惠券相关页面:
依次是某东、某宝、腾讯云 MALL ,这里各式各样的优惠券背后涉及的相关系统,可以统称为优惠券系统。所以单说优惠券系统是一个很庞大的系统,这里收敛一下讲其中主要有四大核心能力:创建、派发、使用、统计。
本篇主要介绍的是平台如何创建和派发优惠券到用户账户的券包里,即上面提到的四大核心能力中的创建和派发。
03
先简单了解一下两个概念:优惠券批次、优惠券。
优惠券批次:一批相同优惠券的生成模版。
优惠券:根据批次信息生成,优惠券与批次的对应关系是 N:1。
批次 ID ;
优惠券名称;
优惠券类型;
库存数量;
优惠规则如:满减,满折等;
生效规则:固定生效时间、领取后生效时间等;
领取规则:批次每天限领数量、用户每天限领数量、用户总限领数量等;
使用规则:指定商家、指定商品、指定类目、指定场景等。
优惠券 ID:分布式 ID 全局唯一;
批次 ID;
用户 ID;
优惠券状态;
上下文信息。
批次表的数据写入主要是 B 端后台管理来操作,这里不多赘述。
优惠券表数据主要通过派发动作与用户关联后写入,后面会展开介绍。
04
库存管理,如何防止超发,保障库存安全。
场景复杂,如何支持高并发及瞬时高流量毛刺场景。
流量毛刺示意:
库存扣减;
生成优惠券。
直接用数据库做库存管理,面临问题:高并发导致数据库崩溃、性能瓶颈明显。
缓存做库存管理:数据不一致、穿透、击穿、雪崩等问题。
最终方案:
Redis+Lua+库存异步分段增补:
Redis+Lua:支持高并发库存扣减。
库存异步分段增补:支持高并发的前提下灵活分配库存。
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 thenreturn '-4'end--Argv nilif not stockId or not couponId or not uid or not maxByUser or not maxByDay or not crtDateStr or not dailyMaxByUser thenreturn '-5'endlocal 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 thenreturn '-6';end-- 库存不足if not leftAmount or leftAmount <= 0 thenreturn '-3';end--达到当天发放上限if crtDispatchAmount and crtDispatchAmount >= maxByDay thenreturn '-1';end-- 该批次每人每日领取数量HashKeylocal dailyUserAcquireNumKey = stockKey .. ":user:acquire:" .. crtDateStr;if dailyMaxByUser > 0 thenlocal dailyUserAcquireNum = redis.call("HGET", dailyUserAcquireNumKey, uid);dailyUserAcquireNum = tonumber(dailyUserAcquireNum);-- 达到每人每日领取上限if dailyUserAcquireNum and dailyUserAcquireNum >= dailyMaxByUser thenreturn '-7'endend--该批次用户领取数量HashKeylocal userAcquireNumKey = stockKey .. ":user:acquire";local usrAcquireNum = redis.call("HGET", userAcquireNumKey, uid);usrAcquireNum = tonumber(usrAcquireNum);--达到用户领取上限if usrAcquireNum and usrAcquireNum >= maxByUser thenreturn '-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 库存数量一直小于等于数据库。
屏蔽流量直接打到数据库,减轻数据库压力。
Redis+数据库控制,双重保证不超发。
库存增补的 M 和 N 可以根据实际业务需要灵活调配。
M 可以理解为业务发券速率兜底。比如:发快补慢提示无库存等。
N 可以理解为极端情况下最大允许丢失的库存数量。
主流程如图:
扣减库存成功同步生成优惠券信息写入数据库,同样会面临高并发导致数据库崩溃的问题,系统瓶颈明显不可取。
这里再加缓存的话,解决缓存问题会让业务变得更复杂,结合第二个主要问题:瞬时高流量毛刺。
最终方案:
库存扣减成功后异步生成优惠券,达到整体流程支持高并发,且可以解决流量毛刺的问题。PS:分布式事务问题。
结合自身业务场景,对比权衡了多种分布式事务解决方案,最终选用本地事务表+最大努力通知来解决分布式事务问题。
介绍:
通过消息异步生成优惠券落库处理来支持高并发,引入一张本地事务表达成数据的最终一致性。
主流程如图:
数据参考:
结合自身实际业务测试环境压测目标 1W/TPS 示意(系统整体支持横向扩容进一步提升性能)。
示意:
05
回顾整体方案,同批次场景仍存在热点问题,针对这里可以做一些优化来提升系统性能,如:资源分桶,聚合扣减,热点更新技术等。如何解决热点问题?下面结合发券场景列举几种方案做一下对比介绍,可供参考。
热点示意:
简介:同一个批次的库存分成多份,通过分散库存扣减请求提升性能。
优势:水平扩展能力强。
重点关注:
分桶 Key 路由倾斜问题,理想情况是所有 Key 平均对应分桶。
各桶之间库存倾斜与性能权衡的问题,理想情况是所有分桶消耗速率一致。
简介:聚合相同批次的请求统一扣减,通过聚合请求量来提升服务整体性能。
优势:前置聚合请求利于提高服务稳定性。
重点关注:
聚合策略的设计需要在系统稳定性和性能上做取舍。
临界库存如何与聚合策略适配的问题。
简介:热点更新技术详细介绍见腾讯云文档:
https://cloud.tencent.com/document/product/237/13402
优势:适用数据库锁层面的热点优化。
重点关注:
依赖数据库适用场景较单一。
小结:每种方案的实现均有利有弊,最后都需要在系统性能和复杂度上做权衡取舍,最终选出契合自身实际业务的才是最好的方案。
📢📢欢迎加入腾讯云开发者社群,享前沿资讯、大咖干货,找兴趣搭子,交同城好友,更有鹅厂招聘机会、限量周边好礼等你来~
(长按图片立即扫码)
