前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >万级TPS优惠券系统设计与实践

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

作者头像
腾讯云开发者
发布2024-11-13 10:01:52
1740
发布2024-11-13 10:01:52
举报
文章被收录于专栏:【腾讯云开发者】
优惠券是电商常见的营销手段,是营销平台中的一个重要组成部分,既可以作为促销活动的载体,也是重要的引流入口。在刚刚过去的电商大促周期内,各大电商平台都有配置不同类目、价位的优惠券,吸引用户下单购买。

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

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

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 脚本示意(示意代码仅供学习参考):

代码语言:javascript
复制
--批次的HashKey
local stockKey = KEYS[1];

--Argv 参数
local stockId = ARGV[1];
local couponId = ARGV[2];
local uid = ARGV[3];
--该批次当天最大发放量
local maxByDay = ARGV[4];
-- 每人限领
local maxByUser = ARGV[5];
--当前时间Str
local crtDateStr = ARGV[6];
-- 每人每日限领
local dailyMaxByUser = ARGV[7];

stockId = tonumber(stockId);
maxByUser = tonumber(maxByUser);
maxByDay = tonumber(maxByDay);
dailyMaxByUser = tonumber(dailyMaxByUser);

--StockKey nil
if not stockKey then
    return '-4'
end
--Argv nil
if 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

-- 该批次每人每日领取数量HashKey
local 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'
  end
end

--该批次用户领取数量HashKey
local userAcquireNumKey = stockKey .. ":user:acquire";
local usrAcquireNum = redis.call("HGET", userAcquireNumKey, uid);
usrAcquireNum = tonumber(usrAcquireNum);

--达到用户领取上限
if usrAcquireNum and usrAcquireNum >= maxByUser then
    return '-2'
end

--扣减库存-1
local leftAmountAfterOp = redis.call("HINCRBY", stockKey, leftAmountField, -1);
--当天发放量+1
local crtDispatchAmountAfterOp = redis.call("HINCRBY", stockKey, crtDateStr, 1);
--当前用户发放量+1
local usrAcquireNumAfterOp = redis.call("HINCRBY", userAcquireNumKey, uid, 1);
-- 当前用户当天发放量+1
local 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-

原创作者|管振盼

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-11-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯云开发者 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 01、背景介绍
  • 02、什么是优惠券系统?
  • 03、优惠券创建
  • 04、优惠券派发
  • 05、后续优化
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档