活动营销系统
活动营销系统:玩法、权益发放、限量风控与高并发
为什么需要活动营销系统
互联网产品的增长靠三件事:拉新、促活、转化。获客成本越来越高,"花一笔营销预算,换用户完成一个关键行为" 是最直接、最可度量的增长手段:新人红包换注册,首单返现换第一笔交易,签到积分换每日打开,邀请奖励换社交裂变。 活动营销系统就是把这件事做成可规模化的能力。它要解决四个问题:
| 问题 | 没有统一系统时的表现 | 系统提供的能力 |
|---|---|---|
| 上线慢 | 每个活动从零开发,一个节日活动排期数周,错过营销窗口 | 活动类型模板化,常见玩法由运营配置上线,开发只做新玩法 |
| 资金风险 | 各业务自建发奖逻辑,限量、风控、对账能力参差不齐;配置多写一个 0、被羊毛党批量刷、并发超发,都会直接造成资金损失 | 统一的限量、预算、风控、熔断、对账,所有发奖经过同一套资金防线 |
| 扛不住峰值 | 节日红包、周年庆开始的几秒内流量是平时的几十到上百倍,自建系统容易雪崩 | 配置本地化、KV 主存储、异步化、过载保护、库存热点优化 |
| 效果说不清 | 奖发出去了,带来多少新用户、多少交易、花了多少钱,各算各的 | 统一的参与、发奖、到账数据和对账报表,按活动核算投入产出 |
这个系统的重要性来自一个事实:它直接花钱。活动玩法出错,损失的是用户体验;权益发放出错,损失的是真金白银, 而且往往在几分钟内就被刷空、难以追回。因此设计重心依次是:限量与预算的正确性、防重复发放、风控、熔断和对账, 其次是大促时的高并发,最后才是玩法的丰富程度。
系统范围
活动营销系统负责"用户参与一个活动,满足条件后拿到奖励"这件事:签到领积分、抽奖、邀请好友得红包、 组团瓜分奖金、完成首单返现、积分兑换商品。它由两部分组成:
- 活动:玩法和规则。决定谁能参加、怎么参加、参加后该得什么
- 权益:真正发出去的东西。红包、优惠券、积分、会员、实物、第三方卡券,以及它们的库存、预算、到账和对账
本文给出一套通用设计。表结构、接口字段、代码均为示意;文中出现的超时、间隔、比例等数值是示例取值,按业务调整。
1. 业务全景
1.1 活动玩法
| 玩法 | 用户动作 | 核心机制 | 关键难点 |
|---|---|---|---|
| 直接领取 | 点击领取 | 按用户分层映射奖品 | 限量、防重复领取 |
| 抽奖 | 消耗抽奖机会抽一次 | 奖池概率、保底、抽奖机会账户 | 概率正确、售罄降级、防刷 |
| 任务 | 完成指定行为(交易、浏览、分享) | 任务进度、完成判定、任务 → 奖励映射 | 行为来源分散,完成判定的时效和准确 |
| 签到 | 每日签到 | 连续天数、断签重置、累计奖励 | 跨天边界、补签 |
| 邀请 / 分享 / 助力 | 分享链接,好友点击或注册 | 分享码、邀请关系绑定、助力次数 | 防自助、防刷号、关系归属 |
| 组团 / 拼团 | 创建或加入团,满员或达标成团 | 团状态机、团积分 | 防超员、一人一团、成团后瓜分 |
| 排行榜 / 竞赛 | 累计行为得分 | 实时排名、按名次发奖 | 同分排名、去重加分、热点 |
| 答题 / 游戏 | 答题、PK、小游戏 | 题库、匹配、得分 | 防作弊、匹配超时 |
| 阶梯 / 多选一 | 达到不同门槛选择奖品 | 阶梯配置、互斥选择 | 选择后不可反悔的一致性 |
| 交易激励 | 完成首单、定投、续存等交易 | 交易消息驱动资格判定和到账 | 撤单 / 退款时的冲正 |
| 积分兑换 | 用积分兑换商品 | 积分账户、兑换订单 | 扣积分与发货的分布式一致性 |
1.2 权益类型
| 权益 | 发放方式 | 到账特点 |
|---|---|---|
| 现金红包 | 转账到用户零钱或账户 | 真实出款,需要出款商户、资金单号、对账 |
| 优惠券 / 抵扣券 | 发券到券账户,交易时核销 | 有有效期,核销、退回、过期回收都要处理 |
| 积分 | 加到积分账户 | 有余额、流水、批次、过期 |
| 虚拟物品 | 会员、话费、游戏道具 | 调用第三方发货接口 |
| 第三方卡券 / CDKey | 发码或调用合作方接口 | 合作方回调核销,需要对账 |
| 实物 | 登记收货地址,线下发货 | 物流状态回传 |
| 抽奖机会 / 任务机会 | 加到机会账户 | 作为另一个活动的"货币" |
| 行业特有权益 | 金融场景有加息券、体验金、交易返现 | 往往依赖交易事件才能到账 |
1.3 两个关键区分
资格与到账。很多权益不是领取即到账:
- 满减券领了之后要在下单时核销,才产生实际价值
- "首单返 10 元"要等交易确认才到账,交易撤单还要收回
- 实物领取后要走线下发货
所以发放分两个阶段:发资格(生成一条奖品记录,用户"拥有"了它)和发货 / 到账(真正把钱、券、物给到用户)。 两个阶段各自有状态、有失败和重试(第 6 节)。
活动与权益平台。活动玩法变化快、形态多,每个业务都在做;权益发放的限量、风控、对账要求高、逻辑稳定。 因此常见的分工是:各业务的活动系统负责玩法,统一的权益平台(也叫营销中台)负责发货、限量、预算、风控、熔断、对账。 活动系统调用权益平台发奖。本文两部分都覆盖。
2. 概念模型
erDiagram
BUDGET ||--o{ ACTIVITY : 授权
ACTIVITY ||--o{ PRIZE : 包含
PRIZE }o--|| ITEM : 发放
ITEM }o--|| DELIVERY_CHANNEL : 发货方式
ACTIVITY ||--o{ LIMIT_RULE : 限量
PRIZE ||--o{ LIMIT_RULE : 限量
USER ||--o{ PARTICIPATION : 参与
PARTICIPATION }o--|| ACTIVITY : 属于
USER ||--o{ PRIZE_RECORD : 获得
PRIZE_RECORD }o--|| PRIZE : 实例
PRIZE_RECORD ||--o| DELIVERY_ORDER : 发货
| 概念 | 说明 |
|---|---|
| 预算 | 一笔立项资金,有金额、有效期、审批人。活动和奖品的花费不能超过预算 |
| 活动 | 玩法 + 时间 + 资格 + 奖品集合。活动类型决定玩法 |
| 奖品 | 活动里的一个奖项。一个奖品对应一种物品,带发放规则:数量、金额计算方式、有效期、到账方式 |
| 物品 | 真正发出去的东西(某面额红包、某批次券、某会员卡),带单价和发货方式 |
| 限量规则 | 某个维度(活动、奖品、用户、自然人)在某个周期(日、周、月、总)内的上限 |
| 参与记录 | 用户参加活动的记录,用于次数限制和查询 |
| 奖品记录 | 用户获得的一个奖品实例,由全局唯一的奖品码标识,有独立的状态机 |
| 发货单 | 一次真实出款或发货,有发货单号,用于下游幂等和对账 |
"活动 → 奖品 → 物品"三层的好处:同一个物品(如 5 元红包)可以被多个活动的奖品复用;物品层对接发货,奖品层管发放规则, 活动层管玩法。新增一种物品只需要实现发货,不影响活动逻辑。
3. 整体架构
flowchart TB
subgraph 前端
H5[活动页 H5 / App]
ADMIN[运营管理端]
end
subgraph 接入层
GW[接入网关<br/>登录态 / 签名 / CSRF<br/>命令字路由 / 过载保护]
MQIN[消息接入<br/>交易、支付、任务事件]
end
subgraph 活动层
ENGINE[活动引擎<br/>通用校验 + 玩法插件]
LOTTERY[抽奖机会账户]
TASK[任务服务]
RANK[排行榜服务]
GROUP[组团服务]
SHOW[活动展示 / 查询]
end
subgraph 权益层["权益层(营销中台)"]
PGW[权益网关<br/>业务接入鉴权]
PRIZE[发奖服务<br/>发资格 / 发货]
LIMIT[限量与预算服务]
RISK[风控代理]
FUSE[熔断服务 旁路]
DELIVER[发货适配器<br/>红包 / 券 / 积分 / 虚拟物品 / 第三方]
RESEND[补发服务]
end
subgraph 支撑
RULE[规则引擎 / 标签服务<br/>资格与人群]
MSG[消息通知<br/>站内信 / Push / 短信]
CFG[配置中心 / 本地配置缓存]
end
subgraph 存储
KV[(KV 存储<br/>奖品记录 / 计数 / 机会<br/>支持 CAS 与原子 incr)]
ZSET[(Redis ZSet<br/>排行榜)]
DB[(分布式 MySQL<br/>参与、奖品、账单、补发、配置)]
ASYNC[异步写库服务]
end
H5 --> GW --> ENGINE
MQIN --> ENGINE
ENGINE --> LOTTERY & TASK & RANK & GROUP
ENGINE --> RULE
ENGINE --> PGW --> PRIZE
PRIZE --> LIMIT & RISK
PRIZE --> DELIVER
PRIZE --> KV
PRIZE --> ASYNC --> DB
FUSE -. 读发奖记录 .-> DB
PRIZE -. 读熔断状态 .-> FUSE
RESEND --> DELIVER
PRIZE --> MSG
RANK --> ZSET
LOTTERY & TASK & LIMIT --> KV
ADMIN --> CFG --> ENGINE & PRIZE
ADMIN --> RESEND
| 层 | 职责 |
|---|---|
| 接入层 | 统一入口:登录态校验、CSRF、签名、命令字路由、过载保护;另有消息接入,把交易、支付等事件转成活动请求 |
| 活动层 | 活动引擎做通用校验,按活动类型分派给玩法插件;抽奖机会、任务、排行榜、组团作为可复用的周边服务 |
| 权益层 | 发资格、发货、限量、预算、风控、熔断、补发;按物品类型接入各发货渠道 |
| 支撑 | 规则引擎和标签服务判断资格,消息服务发通知,配置服务管理活动配置 |
| 存储 | KV 作为奖品记录和计数的主存储(高并发、支持 CAS),MySQL 做持久化、查询和对账,写库异步化 |
KV 为主、DB 为辅是高并发活动系统的常见选择:参与路径上的读写(限量计数、奖品记录、机会账户)都在 KV 上完成, 用 CAS 或原子自增保证并发正确;DB 通过异步写库服务持久化,用于查询、对账和修复。代价是 KV 和 DB 之间存在短暂不一致, 需要对账和"以 DB 修复 KV"的工具(第 10 节)。
4. 活动配置与生命周期
4.1 配置模型:通用字段 + 按类型扩展
活动类型有几十种,每种的配置都不同。把所有字段平铺到一张表里不可维护,常见做法是:
| 部分 | 内容 | 存储 |
|---|---|---|
| 通用字段 | 活动 ID、类型、名称、起止时间、状态、渠道、访问 token、预算、限量、资格与风控开关、功能开关位 | 活动表的普通列 |
| 类型配置 | 该类型特有的逻辑:抽奖的奖池和概率、任务的任务列表、阶梯的门槛 | 活动表的一个 JSON 列,按类型反序列化成对应结构 |
通用字段示例:
| 分组 | 字段 |
|---|---|
| 基础 | act_id、act_type、name、channel(App / 小程序 / 全部)、token |
| 时间 | start_time、end_time |
| 状态 | state(见 4.2) |
| 次数限制 | 单用户总次数、日次数、月次数;是否按自然人(身份证 / 手机号 / 银行卡)限制 |
| 预算 | budget_fee(分) |
| 资格 | 用户分层方式、人群包、客群规则、是否组团活动 |
| 风控 | 是否校验黑名单、羊毛党、风控评分;命中后的处理方式(拒绝 / 发指定奖品) |
| 功能开关 | 位掩码:分享码当日有效、生日限制、展示中奖跑马灯、不记录参与列表…… |
奖品配置同样是"通用字段 + 按物品类型扩展":
| 通用字段 | 说明 |
|---|---|
prize_id、act_id、item_id |
奖品、所属活动、发放的物品 |
| 总量、单用户上限、日上限 | 限量 |
| 金额计算方式 | 固定、区间随机、按交易金额比例 |
| 到账方式 | 直接发放、交易确认后到账、设置计划后到账 |
| 有效期 | 奖品(券)的使用期限 |
| 预算 | 奖品级预算 |
活动与奖品的关系写在类型配置里,例如直接领取活动配置"用户分层 → 奖品",抽奖活动配置"用户分层 → 奖池 [奖品, 概率]"。
token 是活动级的访问令牌,请求必须带上。活动 ID 通常是自增的,没有 token 时攻击者可以遍历活动 ID 直接调用领取接口。
4.2 状态与发布
stateDiagram-v2
[*] --> 待审核: 运营创建
待审核 --> 产品审核通过: 产品审核
待审核 --> 产品驳回
产品审核通过 --> 技术审核通过: 技术审核(正式生效)
产品审核通过 --> 技术驳回
产品审核通过 --> 灰度: 白名单灰度
灰度 --> 技术审核通过: 全量
技术审核通过 --> 下架
灰度 --> 下架
下架 --> [*]
- 双重审核:产品审核看玩法和文案,技术审核看配置(金额、概率、限量、预算)是否合理。技术审核通过才正式生效
- 灰度态:只对白名单用户生效,上线前在真实环境走通全流程
- 测试库 → 生产库:运营在测试环境配置和验证,审核通过后由管理端把配置从测试库同步到生产库;只同步"已生效"或"灰度"状态的配置
- 直接发钱类活动双人确认:直接给用户发现金的活动,除活动配置外,还要求在另一个系统中单独登记,两处都配置了才允许发放,防止单人误配置直接放钱
4.3 配置加载
活动引擎和发奖服务每次请求都要读配置,配置必须在本地内存里:
| 机制 | 做法 |
|---|---|
| 全量加载 | 进程启动时把所有生效活动、奖品、限量、消息模板配置加载到内存 |
| 双缓冲 | 两份配置对象,后台线程加载到备用那份,完成后原子切换下标;读路径无锁 |
| 定时重载 | 定时(如每几分钟)全量重载;紧急修改时由管理端触发立即重载 |
| 错峰 | 多台机器加载时刻加随机偏移,避免同时打数据库 |
| 预计算 | 加载时算好读路径需要的派生结构:抽奖的前缀和概率表、按活动组装好的限量规则 |
| 共享内存快照 | 配置同时写一份到共享内存,进程重启时先从快照恢复,缩短启动时间 |
4.4 活动玩法插件
活动类型多,参与流程的骨架却相同:通用校验 → 资格 → 风控 → 选奖 → 限量 → 发奖。用"基类 + 工厂 + 模板方法"组织:
// 示意代码,未实测
;
std::unique_ptr<BaseActivity>
新增一种玩法:新增一个子类和一个类型配置结构,在工厂注册。通用校验、风控、限量不用重写。 奖品侧用同样的结构:发奖基类定义"发资格 / 校验 / 发货 / 补发"等方法,每种物品一个子类(第 6.4 节)。
5. 参与流程
5.1 主流程
sequenceDiagram
participant U as 用户
participant GW as 接入网关
participant ACT as 活动引擎
participant RULE as 规则 / 标签
participant RISK as 风控
participant LIM as 限量
participant PRZ as 发奖服务
participant DLV as 发货渠道
participant KV as KV
participant DB as DB(异步)
U->>GW: 参与活动(act_id、token)
GW->>GW: 登录态、CSRF、过载保护
GW->>ACT: 转发
ACT->>ACT: 通用校验:状态 / 灰度 / 时间 / token / 渠道
ACT->>RULE: 资格:人群包、客群规则、前置条件
ACT->>RISK: 黑名单、羊毛党、风险评分
ACT->>LIM: 扣活动次数(日 / 总)
ACT->>ACT: 选奖(分层映射 / 抽奖 / 阶梯)
ACT->>PRZ: 发奖(user、act、prize、业务单号)
PRZ->>LIM: 奖品总量、单用户、自然人、预算
PRZ->>KV: 写奖品记录(初始态)+ CAS 追加用户奖品列表
PRZ->>DB: 异步落库
PRZ->>DLV: 发货(直接到账类)
PRZ->>KV: 更新奖品状态
PRZ-->>ACT: 奖品记录
ACT-->>U: 结果
PRZ--)U: 异步通知(站内信 / Push)
5.2 各步骤要点
| 步骤 | 要点 |
|---|---|
| 通用校验 | 纯本地判断:配置在内存,灰度白名单也在内存 |
| 资格 | 人群包、BI 客群、规则引擎可能都要调外部服务。同一请求内多次判断的结果做请求级缓存 |
| 风控 | 优先级从高到低:黑名单 > 行业从业者等特殊人群 > 羊毛党 > 非白名单。结果不只是"拒绝",还可以是"放行但发指定奖品"(如给疑似羊毛党发最低档奖品),减少误伤投诉 |
| 用户分层 | 按标签把用户分为新客、老客、高价值、沉睡等,不同层映射不同奖品或奖池 |
| 次数限制 | 活动维度的日 / 总次数,按用户、按自然人 |
| 选奖 | 由玩法插件实现 |
| 发奖 | 调权益平台,带上业务单号作为幂等键 |
5.3 交易驱动的活动
"首单返现""定投满 3 期送券""续存加息"这类活动不由用户点击触发,而是由交易事件触发:
flowchart LR
TRADE[交易系统] -- 申购成功 / 确认 / 撤单 / 到期 --> MQ[[消息队列]]
MQ --> IN[消息接入]
IN --> MATCH[匹配用户已获得的待到账奖品<br/>或判断是否满足活动条件]
MATCH --> DEDUP{按交易单号<br/>是否处理过}
DEDUP -- 否 --> PROVIDE[到账:计算金额、发货]
DEDUP -- 是 --> SKIP[跳过]
IN -- 撤单 / 退款 --> REVERSE[冲正:收回奖励、回退限量]
| 要点 | 说明 |
|---|---|
| 两阶段 | 用户先在活动页领到资格(奖品记录状态为"待到账"),交易成功的消息到来后再到账 |
| 消息去重 | 同一交易消息可能重复投递。以"交易单号 + 奖品码"判断是否已处理,已处理的交易单号记录在奖品记录里 |
| 冲正 | 交易撤单或退款时,收回已发奖励(扣回金额、作废券),回退限量计数 |
| 首单判定 | "首单"需要查询用户历史交易,结果写入标记防止重复判定 |
6. 权益发放
6.1 奖品记录状态机
stateDiagram-v2
[*] --> 初始: 发资格
初始 --> 待到账: 需交易等外部条件
初始 --> 发奖中: 发货(CAS 抢占)
待到账 --> 发奖中: 条件满足
发奖中 --> 成功: 发货成功
发奖中 --> 失败: 不可重试的错误
发奖中 --> 延迟补发: 可重试的错误 / 频控
发奖中 --> 待外部发货: 由合作方异步发货
待外部发货 --> 成功: 合作方回调
延迟补发 --> 发奖中: 补发任务
初始 --> 无效: 发货前校验不通过
待到账 --> 过期: 超过有效期
成功 --> 已冲正: 撤单 / 退款
成功 --> [*]
失败 --> [*]
无效 --> [*]
过期 --> [*]
6.2 发资格
sequenceDiagram
participant C as 调用方
participant P as 发奖服务
participant L as 限量
participant KV as KV
participant A as 异步写库
C->>P: 发资格(user、act、prize、业务单号)
P->>P: 校验:活动时间、奖品时间、前置条件
P->>KV: 读用户奖品列表(带 CAS 版本)
P->>L: 扣限量:奖品总量、单用户、自然人、预算
P->>P: 生成奖品码;子类计算金额等
P->>KV: 写奖品记录(初始态)
P->>KV: CAS 追加奖品码到用户奖品列表
alt CAS 冲突
P->>KV: 奖品记录置为清理态
P-->>C: 失败,稍后重试
end
P->>A: 异步写奖品表
P-->>C: 奖品码
| 要点 | 说明 |
|---|---|
| 奖品码 | 全局唯一:活动 ID + 机器标识 + 时间 + 线程号 + 序号 拼接,本地生成,不依赖中心发号器 |
| 先记录后发货 | 奖品记录写成功后才发货。发货失败时记录还在,可以补发;反过来先发货再记录,记录失败就无法追溯 |
| 用户奖品列表 | KV 中按"用户 + 活动"存一个奖品码列表,用于"我的奖品"查询,也用于次数限制。用 CAS 追加,同一用户的并发请求只有一个能写成功 |
| 业务单号幂等 | 调用方传入业务单号。重复请求时返回已有奖品码,不重复扣限量 |
6.3 发货
sequenceDiagram
participant P as 发奖服务
participant KV as KV
participant S as 发货渠道
participant DB as 账单表
P->>KV: 读奖品记录(带 CAS 版本)
P->>P: 状态必须是 初始 / 待到账
P->>KV: CAS:状态改为 发奖中(抢占,防并发重复发货)
P->>P: 生成发货单号,先写回奖品记录
P->>S: 发货(带发货单号)
alt 成功
P->>KV: 状态 → 成功
else 可重试错误 / 频控
P->>KV: 状态 → 延迟补发
P->>DB: 写补发表
else 不可重试错误
P->>KV: 状态 → 失败
end
P->>DB: 写账单
防重复发货的三道保险:
- 状态 CAS 抢占:只有把状态从"初始"成功改成"发奖中"的那个请求才能发货,并发请求在 CAS 上失败
- 发货单号先落盘:发货单号在调用下游之前写入奖品记录,补发时复用同一个单号
- 下游按单号幂等:出款、发券接口以发货单号去重,同一单号重复请求只执行一次
只有下游明确返回"该单号已作废,需换新单号"这类错误时,才生成新单号。
超时怎么办:调用下游超时,结果未知。此时不能认为失败而用新单号重发(可能重复打款), 应进入补发,用原单号重试或查询,由下游幂等保证最多执行一次。
6.4 物品与发货适配
发奖服务用"基类 + 每种物品一个子类"组织发货逻辑:
| 方法 | 作用 |
|---|---|
OnTicket |
发资格阶段的物品特有逻辑,如计算随机金额 |
Check |
发货前校验,如交易金额是否满足门槛 |
Provide |
真正发货 |
RetryProvide |
补发 |
OnReverse |
冲正(可选) |
接入新物品有两种方式:
| 方式 | 说明 | 适用 |
|---|---|---|
| 标准化发货 | 平台定义标准发货协议,物品提供方实现。请求包含 seq、billno(需支持重复请求)、user_id、item_id、amount,响应包含 ret 和 errmsg;签名和加密使用业务接入时分配的密钥 |
新接入的物品。平台新增物品类型不需要开发 |
| 适配发货 | 平台为已有的发货接口写适配代码 | 存量接口、定制化多的接口 |
物品表记录物品类型(实物 / 虚拟 / 内部券)、单价、所属业务、发货方式和发货接口信息(地址、是否签名加密),并有审核状态。
6.5 优惠券
券是最复杂的一类权益:有批次、有核销、有资金流、有过期回收。
券批次
| 字段 | 说明 |
|---|---|
| 批次 ID、名称、使用说明 | |
| 面额、最低使用金额 | 单张券的金额(分)和使用门槛 |
| 总数量、总预算 | 发券上限 |
| 已发 / 已绑定 / 已核销 / 已过期数量与金额 | 统计与对账 |
| 有效期 | 开始、结束时间 |
| 券分类 | 领取即核销、领取后核销等(见下表) |
| 使用类型 | 下单抵扣、提现抵扣、赠送现金、加息等 |
| 预算立项信息 | 立项单号、立项金额 |
| 状态 | 创建 → 提交审核 → 审核中 → 已审核 → 过期处理中 → 已过期;作废 |
券分类
| 分类 | 说明 | 过期回收 |
|---|---|---|
| 领取即核销 | 领取时直接到账,如现金红包 | 不需要 |
| 领取后核销 | 领取后在交易中使用:兑换券(满足资格后兑换)、交易后一步核销(交易完成后自动核销)、交易中两步核销(下单时锁券、支付成功后确认) | 需要,由券所属系统负责 |
| 任务积分券 | 状态由活动侧维护 | 由活动侧发起 |
| 平台托管券 | 状态由公司统一的券托管平台维护 | 在托管平台发起 |
| 合作方券 | 用户跳转外部系统兑现,或由合作方线下发货 | 不需要 |
券的状态机
stateDiagram-v2
[*] --> 待领取
待领取 --> 已领取: 领取成功
待领取 --> 领取失败
已领取 --> 已绑定: 下单锁券
已绑定 --> 已领取: 解绑(订单取消)
已绑定 --> 使用请求: 支付
使用请求 --> 已扣款: 资金划转
已扣款 --> 使用成功: 交易确认
使用请求 --> 使用失败
已扣款 --> 资金退回: 交易失败 / 退款
资金退回 --> 退回成功
已领取 --> 过期请求: 到期
过期请求 --> 已过期
领取失败 --> [*]
使用成功 --> [*]
使用失败 --> [*]
退回成功 --> [*]
已过期 --> [*]
资金流
flowchart LR
BUDGET[活动资金账户<br/>预算充值] -- 批次审核通过 --> COUPON[券资金账户]
COUPON -- 核销:抵扣部分 --> MCH[收款方 / 商户]
MCH -- 退款 / 交易失败 --> COUPON
COUPON -- 批次过期:未使用金额 --> BUDGET
- 批次审核通过时,从活动资金账户把"面额 × 数量"转入券资金账户;每笔转账有单号,记录在批次上
- 核销时从券资金账户付给收款方;交易撤销时退回券资金账户
- 批次过期后,未使用部分退回活动资金账户
- 每张券的交易记录保存资金单号、退回单号、关联交易单号,用于对账
券的日常处理
| 处理 | 说明 |
|---|---|
| 核销触发 | 订阅交易结果消息,找出用户待核销的券,调用核销 |
| 过期回收 | 定时扫描到期未使用的券,置为过期,回收资金 |
| 中间态补单 | 已领取未过期、却长时间停在中间态(如"使用请求")的券,定时查询下游结果并推进到终态 |
| 日终对账 | 与权益平台对账:已领取、已核销、过期回收三类数量与金额 |
| 历史迁移 | 券交易表按用户分库分表,定期把终态的历史数据迁出 |
券交易表的唯一键是"发券方单号"(发券方按规则生成,保证不重复),用于发券幂等。
6.6 补发
发货失败后有三条补发链路:
| 链路 | 触发 | 实现 |
|---|---|---|
| 自动补发 | 发货返回可重试错误、超时、或被本机频控拦截 | 写补发表(按"用户 + 奖品码"去重,设过期时间如 5 天);补发线程定时扫描;指数退避重试(如从 60 秒开始翻倍,上限 1 小时);过期未成功转人工 |
| 延迟发送 | 需要在指定时间发放的奖品 | 延迟发送表 + 扫描线程,状态 BEGIN → DOING → SUCC |
| 人工批量补发 | 运营发现问题后批量补发 | 管理端生成补发配置和补发单,审核通过后执行 |
补发任务的抢占:多台机器同时扫描补发表,用条件更新抢占:
UPDATE resend_bill SET status = 1, owner = ?
WHERE id = ? AND status = 0; -- 影响行数为 1 才处理
批量补发任务用分布式锁(SET key value NX PX ttl,释放时用 Lua 脚本比较值后再删除),保证同一个补发批次只有一台机器执行。
补发前再次读取奖品记录,只有处于"发奖中 / 失败 / 延迟补发"的记录才补发,避免已经成功的奖品被重复发放。
7. 玩法实现
7.1 抽奖
选奖
配置:奖池 = [(奖品 A, 概率 100), (奖品 B, 概率 2000), (谢谢参与, 概率 99997900)],基数 1e8
加载时:前缀和表 = [100, 2100, 100000000]
抽奖时:r = rand() % 1e8 + 1;找到第一个前缀和 ≥ r 的奖品
| 要点 | 说明 |
|---|---|
| 概率基数 | 用整数基数(如 1e8)表示概率,避免浮点误差,支持万分之一以下的小概率 |
| 预计算 | 前缀和表在配置加载时计算,抽奖时二分或线性查找 |
| 随机数 | 每线程独立种子的随机数生成器,避免全局锁 |
| 保底 | 首抽、第二抽固定奖品;第 N 抽固定奖品;按顺序先判断保底再走概率 |
| 分层奖池 | 按用户分层选择不同奖池;风控命中用户使用专用奖池 |
| 售罄降级 | 抽中的奖品因总量或单用户上限发不出时,降级发"末等奖",用户体验上仍然"中奖" |
| 互斥 | 某些奖品不能同时持有(如同类翻倍卡),抽中时检查,冲突则降级 |
概率不等于实际发放比例:概率抽奖在库存有限时,头部奖品可能在活动开始几分钟就被抽完。需要均匀放量时, 按时间段给奖品设置库存(如每小时放 100 个),或按剩余库存和剩余时间动态调整概率。
抽奖机会账户
key = lottery_chance:{act_id}:{user_id}
value = { 总获得, 总消耗, 当日消耗, 当日日期, 本周消耗, 本周编号 }
- 获得机会:参与任务、分享、交易等,由其他活动或奖品"加机会"
- 消耗机会:检查总、日、周三个维度的剩余次数,CAS 写回
- 跨天、跨周:读取时比较日期,自然重置当日计数,不需要定时任务清零
7.2 任务
任务系统有两种模型:
| 模型 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 拉模式 | 用户点击领奖时,实时查询业务系统判断任务是否完成 | 简单,不需要接入事件 | 每次领奖都要查业务系统;只能判断当前状态,不适合累计型任务 |
| 推模式(事件驱动) | 业务系统发布行为事件,任务服务订阅,按事件更新任务进度,达成后自动发奖或点亮领取按钮 | 支持累计、连续、组合任务;实时 | 需要事件接入和进度存储;事件乱序、重复需要处理 |
事件驱动的任务服务:
flowchart LR
EV[行为事件<br/>交易 / 浏览 / 分享 / 签到] --> MQ[[消息队列]]
MQ --> TS[任务服务]
TS --> MATCH[按事件类型找到<br/>用户参与中的任务]
MATCH --> DEDUP{事件 ID<br/>已处理?}
DEDUP -- 否 --> PROG[更新进度 KV<br/>CAS]
PROG --> DONE{达成?}
DONE -- 是 --> REWARD[发奖或标记可领取]
| 要点 | 说明 |
|---|---|
| 任务定义 | 事件类型、条件(金额 ≥ X、指定商品)、目标值、周期(一次性 / 每日 / 每周)、奖励 |
| 进度存储 | task_progress:{task_id}:{user_id} → 当前值、周期编号、已处理事件 ID 集合 |
| 幂等 | 同一事件只计一次进度(事件 ID 去重) |
| 任务组 | 规则引擎按用户画像下发一组任务,组内按取模或随机分配;达到上限的任务从组中剔除 |
| 做任务机会 | 另一种简化形态:任务完成后获得"机会",用机会换奖励;机会账户与抽奖机会同构 |
7.3 邀请、分享、助力
| 要点 | 做法 |
|---|---|
| 分享码 | 服务端生成加密分享码,明文包含 `邀请人 |
| 两级限量 | 单个好友对同一分享码的助力次数;同一分享码的总助力次数 |
| 防自助 | 解析分享码后判断是否是自己的码 |
| 关系绑定 | 邀请关系首次绑定后不可更改(先到先得),存"被邀请人 → 邀请人" |
| 奖励时机 | 被邀请人完成关键行为(注册、首单)后才给邀请人奖励,防止刷号 |
| 反作弊 | 设备指纹、同 IP / 同设备大量新号、被邀请人质量分,命中则不给奖励 |
7.4 组团
stateDiagram-v2
[*] --> 组团中: 团长创建
组团中 --> 已成团未满员: 达到最少人数
组团中 --> 已满员: 达到最大人数
已成团未满员 --> 已满员: 继续加入
组团中 --> 系统成团: 到期由系统补足
组团中 --> 失败: 到期未达最少人数
| 要点 | 做法 |
|---|---|
| 防超员 | 条件更新:UPDATE team SET member_num = member_num + 1 WHERE team_id = ? AND member_num < max,看影响行数 |
| 一人一团 | 成员表以"活动 + 用户"为唯一键 |
| 原子性 | "人数 + 1"和"插入成员记录"必须在同一个数据库事务里;分两步执行时,插入失败会导致人数多算,出现"显示满员实际没满" |
| 团积分 | 团内成员行为(分享、交易)折算成团积分,写入团排行榜 |
| 邀请有效期 | 邀请链接带过期时间 |
| 团名 | 唯一、过敏感词检测 |
7.5 排行榜
| 要点 | 做法 |
|---|---|
| 存储 | Redis ZSet:rank:{act_id},member 为用户,score 为分数 |
| 加分 | ZINCRBY |
| 排名 | 普通排名 ZREVRANK + 1;同分同名次时名次 = ZCOUNT(key, (score, +inf) + 1,即分数严格大于自己的人数加 1((score 表示开区间) |
| 去重 | 按交易单号加分,已处理的单号要记录 |
| 原子性 | "记录单号已处理"与"加分"必须原子。先记单号再加分时,加分失败后重试会被判为重复,分数永久丢失。用 Lua 脚本把"检查单号 + 加分 + 记录单号"合成一个原子操作 |
| 同分先后 | 需要"先达到者排前"时,score 编码为 分数 × 10^k + (MAX_TS − 时间戳) |
| 读一致性 | 读写分离时刚写完读从库可能拿到旧排名;用户查自己名次时读主库 |
| TopN 缓存 | 榜单 TopN 在进程内存缓存几分钟,个人名次实时查 |
| 大榜 | 参与人数极多时只维护前 N 名的精确排名,其余给出区间(如"前 10%") |
7.6 积分与积分商城
积分账户
| 表 | 内容 |
|---|---|
| 账户汇总 | 余额、累计获得、累计消费、累计过期、最后检查时间 |
| 积分批次 | 每次获得一条:数量、剩余、过期时间、状态(可用 / 用完 / 过期) |
| 流水 | 每次变动一条:变动类型、数量、变动前后余额、业务单号、业务类型 |
| 操作 | 做法 |
|---|---|
| 加积分 | 按业务单号查重 → 开事务 → SELECT ... FOR UPDATE 锁住用户账户 → 日限量和预算检查 → 插入批次和流水 → 更新余额 → 提交 |
| 扣积分 | 同样加锁;按过期时间先到先扣(FIFO)扣减各批次,扣减明细记在流水里,退款时按明细退回原批次 |
| 过期 | 查询余额时惰性检查:把已过期批次置为过期,写过期流水。不需要在过期时刻跑大批量任务 |
| 退还 | 兑换失败时按原扣减明细退回 |
| 一致性校验 | 每日校验:流水的前后余额是否首尾相接;余额是否等于可用批次剩余之和。不一致时以批次为准校准并告警 |
| 预算 | 按业务累计发放的积分,超过预算拒绝发放 |
积分兑换
兑换涉及库存、积分、发货三个系统,采用"预留 + 确认 + 补偿":
sequenceDiagram
participant U as 用户
participant O as 订单服务
participant S as 库存
participant P as 积分账户
participant D as 发货
U->>O: 兑换商品
O->>O: 建单(初始)
O->>S: 预扣库存(条件更新 + 预扣记录,同一事务)
O->>P: 扣积分(业务单号 = 订单号)
O->>D: 发货(发货单号 = 订单号)
O->>O: 订单 → 成功
Note over O: 异常订单扫描:<br/>无预扣记录 → 作废<br/>扣积分失败 → 回滚预扣、作废<br/>扣积分成功未发货 → 补发货<br/>重试超限 → 告警转人工
库存预扣:
UPDATE goods_stock SET used = used + 1
WHERE goods_id = ? AND used < total; -- 影响行数为 1 才成功
INSERT INTO stock_prelock (order_id, goods_id, ...) VALUES (...); -- 同一事务
单用户兑换频次限制:对"用户锁"行 SELECT ... FOR UPDATE 使同一用户的兑换串行化,再统计已有订单数。
8. 限量与预算
8.1 限量维度
| 维度 | 例子 |
|---|---|
| 活动 × 用户 | 每人每天参与 3 次,总计 10 次 |
| 奖品总量 | 一等奖共 100 个 |
| 奖品 × 用户 | 每人最多获得 1 张券;日、周、月上限 |
| 自然人 | 同一身份证 / 手机号 / 银行卡只能领 1 次(防多账号) |
| 分享码 | 单个好友助力次数、分享码总助力次数 |
| 群组互斥 | 同一组内的多个活动,用户只能参加其中一个 |
| 活动预算 | 活动累计发放金额不超过预算 |
| 业务预算 | 某业务线累计发放的积分 / 金额不超过预算 |
8.2 实现方式
| 方式 | 做法 | 一致性 | 性能 | 适用 |
|---|---|---|---|---|
| KV CAS 计数节点 | 读节点(含日 / 月重置时间戳)→ 判断 → 带版本写回 | 并发冲突时失败,不会超 | 好 | 用户维度的日 / 月 / 总限量 |
| KV 原子自增 | INCR 后判断是否超限;超限则不发(可选择回退计数) |
不会超;不回退时计数偏大,效果"宁少发不超发" | 优 | 奖品总量、全局计数 |
| 记录计数 | 统计用户在该活动下已有奖品码数量,不单独维护计数器 | 依赖奖品列表的 CAS 写回兜底 | 好 | 避免计数器与记录不一致 |
| DB 条件更新 | UPDATE ... SET cnt = cnt + 1 WHERE id = ? AND cnt < limit,看影响行数 |
强一致,支持跨城容灾 | 差(单行热点写串行) | 高价值奖品 |
| 分批预算授权 | 预算按批次授权(如每批 50 万),消耗到 80% 告警提醒追加;未追加则耗尽后停止发放;调低预算不能低于已消耗额度 | 控制单次失控的上限 | — | 活动总预算 |
按奖品价值选择:低价值、大量的奖品用 KV(性能优先);高价值奖品用 DB 条件更新(一致性优先,KV 异步复制在故障时可能丢更新)。
多层兜底:活动系统的限量 → 权益平台的奖品限量 → 预算限量 → 业务自身的限量。任何一层都不应被当成唯一保证: 权益平台做不到跨城容灾时,业务侧必须保留自己的限量。
8.3 回退与冲正
| 场景 | 处理 |
|---|---|
| 扣完限量后发奖失败 | 需要回退限量计数,否则库存泄漏(计数多扣,实际少发) |
| 交易撤单、退款 | 回退限量、收回奖励 |
| 券核销后退款 | 券回到可用状态或作废,按规则回退 |
限量扣减和发奖分属不同存储,无法放进一个事务。做法是:扣减成功后再发奖;发奖确定失败时调用回退接口; 回退失败记录下来,由对账修正。库存泄漏的方向是"少发",比"超发"安全。
8.4 热点库存
一个爆款奖品的总量计数器是单个热点 key,大促时所有请求都打到它:
| 手段 | 做法 |
|---|---|
| 库存分桶 | 总库存拆成 N 个子 key,请求随机或按用户哈希选择一个桶扣减;某个桶扣完再尝试其他桶 |
| 本地预取 | 每台机器从中心库存批量领取一段(如 100 个)到本地,本地扣减;用完再领。机器宕机会损失本地剩余,需要回收机制 |
| 前置过滤 | 库存耗尽后在本地内存标记"已售罄",后续请求直接返回,不再访问存储 |
| 展示与扣减分离 | 活动页展示的剩余库存从本地缓存读(定时如每 10 秒刷新),只有真正领取才访问库存存储 |
9. 风控
9.1 分层
| 阶段 | 手段 |
|---|---|
| 事前 | 资格(人群包、客群规则、新老用户)、黑名单、羊毛党识别、设备风险、行业从业者等特殊人群限制、活动 token |
| 事中 | 防并发(按用户、身份证、手机号加短时锁)、频控、自然人限量、分享码校验 |
| 事后 | 熔断(发放异常时自动停止)、对账、离线识别作弊并追回 |
9.2 风控代理
风控规则通常由专门的风控团队或 BI 维护(羊毛党、防水墙、社交平台黑名单等),活动系统只做一个代理服务调用统一接口:
- 输入:用户、业务方、要查的风控项列表
- 输出:每个风控项的结果
- 风控项可能增加,接口耗时要有上限(如 200ms),超时按配置降级
命中风控后的处理方式可配置:
| 处理 | 适用 |
|---|---|
| 拒绝参与 | 高风险用户 |
| 发指定奖品 | 疑似风险用户:给最低档奖品,既控制损失又减少误伤投诉 |
| 放行 | 风控服务故障时,结合白名单兜底放行(业务可用性优先),或全部拒绝(资金安全优先),按活动配置 |
9.3 防并发
同一用户的并发请求(连点、客户端重试)要在限量和发奖之前拦住:
SET lock:{user_id}:{act_id}:{prize_id} 1 EX 3 NX
成功 → 继续流程,结束后删除
失败 → 已有相同请求在处理,直接返回
开启自然人防刷时,对身份证号和手机号各加一次锁。锁的过期时间是异常时的兜底,正常流程结束时主动删除。
9.4 自然人防刷
一个人可以有多个账号。按身份证号或手机号识别自然人:
- 预先把存量用户的"账号 → 身份证 / 手机号"映射导入 KV(量级估算:10 亿用户 × 100 字节 ≈ 100GB),设置有效期;未命中时实时查询补全
- 发奖前按"身份证或手机号 + 活动 + 奖品"查计数,≥ 1 则拦截
- 发奖成功后计数加一
这是"尽量保证"的方案:检查和记录之间有并发窗口,记录失败也会漏拦;严格的自然人限量需要以身份证为 key 做限量扣减。
9.5 熔断
熔断是营销事故的最后一道防线:配置错误(面额多写一个 0)、被刷、逻辑漏洞,都会表现为某个奖品的发放量异常。
sequenceDiagram
participant ADM as 管理端
participant PRZ as 发奖服务
participant DB as 数据库
participant F as 熔断服务
ADM->>DB: 配置规则(奖品、周期、统计方式、阈值)
loop 每次发奖
PRZ->>DB: 写发奖记录
end
loop 每个周期(如 5 分钟)
F->>DB: 统计周期内各奖品发放量
F->>F: 与阈值、上周期(环比)、昨天同时段(同比)比较
F->>DB: 超限:写熔断记录(批次号)并告警
end
PRZ->>DB: 定时加载熔断记录
PRZ->>PRZ: 命中熔断的奖品停止发放<br/>请求上下文写入待补发表
ADM->>DB: 人工确认:误判则触发补发
| 设计 | 原因 |
|---|---|
| 旁路系统,只通过 DB 交互 | 不改动发奖主流程;熔断服务故障不影响发奖 |
| 分钟级统计 | 运营熔断能接受一定时间误差,晚一分钟熔断可以接受,换来实现简单 |
| 同比 / 环比 / 绝对值 | 绝对值防总量失控;环比防突增;同比排除每天固定的高峰 |
| 批次号 + 补发 | 熔断可能误判,被拦下的请求保存上下文,人工确认后补发,用户不受损失 |
10. 一致性、幂等与对账
10.1 幂等键
| 环节 | 幂等键 | 作用 |
|---|---|---|
| 发资格 | 业务单号 | 调用方重试不重复发资格 |
| 奖品记录 | 奖品码 | KV 主键、DB 唯一键 |
| 发货 | 发货单号(先落盘,补发复用) | 下游出款 / 发券去重 |
| 账单 | 账单号 | 账单表唯一键;重复写入时容忍唯一键冲突 |
| 交易消息 | 交易单号 + 奖品码 | 消息重复投递不重复到账 |
| 补发 | 用户 + 奖品码 | 补发表去重 |
| 积分变动 | 业务单号 | 积分流水去重 |
| 券发放 | 发券方单号 | 券交易表唯一键 |
| 批量任务 | 批次状态条件更新(旧状态 → 新状态) | 批次不被重复执行 |
10.2 KV 与 DB 的一致性
| 问题 | 做法 |
|---|---|
| KV 写成功、异步写 DB 失败 | 异步写库服务重试;对账发现 DB 缺失时从 KV 补 |
| DB 有、KV 丢失(KV 故障) | 管理端提供"以 DB 修复 KV"的工具 |
| 高价值奖品 | 发资格时同步写 DB(先写 DB 再发货),牺牲一些性能换可靠性 |
| 同一用户并发 | 用户奖品列表 CAS 写回;CAS 失败时把刚写入的奖品记录置为清理态 |
10.3 对账
| 对账 | 内容 |
|---|---|
| 发放流水 ↔ 出款流水 | 奖品核销流水与商户出款流水按流水号双向关联,一一对应 |
| 预算 | 按预算立项单号汇总已核销金额,不超过立项金额 |
| 活动 | 按活动汇总花费,不超过活动预算 |
| 商户账户 | 入账总额 = 余额 + 各活动花费总额 |
| 券 | 日终与券系统核对已领取、已核销、过期回收的数量和金额 |
| 积分 | 流水连续性、余额 = 批次之和 |
| 预算缓存 | 预算计数缓存在 KV,定期与 DB 实际发放合计比对:间隔读两次缓存,两次一致时取 DB 在该时刻的合计,不一致则以 DB 为准修正缓存 |
| 补发 | 补发单是否生成完整;单笔金额是否超过上限 |
| 按机构记账 | 每笔成功发放按出资机构记账,便于结算 |
11. 高并发与高可用
大促活动(节日红包、周年庆)的流量特征是瞬时峰值:活动开始的几秒内请求量是平时的几十到上百倍。
11.1 手段
| 手段 | 做法 |
|---|---|
| 配置本地化 | 活动、奖品、限量配置全在进程内存,读路径不访问配置库 |
| KV 为主存储 | 参与路径的读写都在 KV 上,DB 异步写 |
| 按用途拆分 KV 集群 | 可过期数据(限量计数、防并发锁)和不过期数据(奖品记录)分开存放,分散压力,也便于设置不同的淘汰策略 |
| 异步化 | 接入层异步 IO;写库异步;消息通知异步、失败不影响主流程;到账由消息驱动 |
| 削峰 | 发货侧按出款渠道做本机频控(如每秒 120 笔),超出部分写入补发表,由补发线程平滑处理 |
| 库存优化 | 分桶、本地预取、售罄标记(第 8.4 节) |
| 降级 | 发不出的奖品降级为末等奖;非核心展示(跑马灯、排行榜 TopN)读缓存;风控超时按配置降级 |
| 预热 | 活动开始前加载配置、预建连接、预热缓存 |
| 路由 | 按用户 ID 一致性哈希路由,同一用户的请求落到同一实例,提高本地缓存命中,降低同用户并发冲突 |
11.2 接入层过载保护
接入层按排队时间判断过载,按优先级丢请求:
过载判定:统计周期内平均排队时间超过阈值(排队时间 = 开始处理时间 − 收到请求时间)
或出现队列超时、队列溢出;下游回包带回自己的排队时间,用于判断下游过载
请求优先级 = 业务优先级(命令字配置,如 3 档)× 1024 + 用户优先级
用户优先级 = hash(user_id + 当前小时) & 1023 每小时轮换,避免总是同一批用户被拒
未登录用户使用最低优先级
过载时:按优先级直方图从低到高丢弃,直到放行量不超过期望容量
恢复时:逐步放开;容量估算以过载周期内的正常回包数为候选值,恢复时从估算容量的 80% 开始加量
| 设计 | 原因 |
|---|---|
| 看排队时间而不是耗时 | 请求耗时长不一定是过载(有的请求本来就慢),排队说明处理不过来 |
| 按优先级丢而不是随机丢 | 核心命令字(领取、发奖)优先保证;被拒的是固定的一部分用户,同一用户的请求要么都成功要么都失败,而不是所有用户"时好时坏" |
| 用户优先级按小时轮换 | 保证公平,不让同一批用户一直被拒 |
12. 管理端与运营工具
| 功能 | 说明 |
|---|---|
| 活动配置 | 创建活动和奖品、配置玩法、预览、灰度白名单 |
| 审核 | 产品审核、技术审核;直接发钱类活动双人确认 |
| 配置同步 | 测试库配置审核后同步到生产库,服务定时或立即重载 |
| 预算管理 | 预算立项、分批授权、消耗告警 |
| 查询 | 按用户查参与记录、奖品记录、限量计数节点、黑名单状态 |
| 补发 | 单笔补发、批量补发(导入名单 → 审核 → 执行,批次状态条件更新) |
| 名单直发 | 按导入的名单批量直接发奖,每个用户的发放状态独立记录 |
| 修复 | 以 DB 修复 KV、修改奖品记录状态、重新发货 |
| 熔断 | 配置规则、查看熔断记录、确认补发 |
| 对账报表 | 每日对账结果、差异明细、按机构统计 |
| 监控告警 | 发放量、失败率、补发积压、预算消耗、商户账户余额 |
13. 常见缺陷与设计要点
| 缺陷 | 后果 | 正确做法 |
|---|---|---|
| 先扣限量再发奖,发奖失败不回退 | 库存泄漏,实际发放少于配置 | 发奖确定失败时回退;回退失败由对账修正 |
| 先读后判再写(检查与写入分离) | 并发时超发 | CAS、原子自增或条件更新,判断和扣减在一个原子操作里 |
| CAS 冲突直接返回失败 | 高并发下正常用户大量失败 | 对幂等的操作做有限次重试;或把冲突转化为"系统繁忙,请重试" |
| 去重记录与业务操作非原子(先记单号再加分) | 加分失败后重试被判重复,永久丢失 | 用 Lua 脚本或事务把"检查 + 操作 + 记录"合成原子操作 |
| 多步数据库操作不在事务里(先加人数再插成员) | 计数与记录不一致 | 同一个库内的多步修改放进一个事务 |
| 发货超时后用新单号重发 | 重复打款 | 复用原发货单号,由下游幂等 |
| 读写分离下写后立即读从库 | 读到旧数据(如刚加的分没有体现在排名上) | 需要强一致的读走主库 |
| 发资格只写 KV、DB 异步 | KV 故障时丢记录 | 高价值奖品同步写 DB;异步写库有重试和对账 |
| 参与请求没有幂等键 | 重复点击只能靠限量兜底 | 由前端生成请求 ID,或由服务端按"用户 + 活动 + 时间窗"去重 |
| 配置、脚本里写明文数据库账号密码 | 泄露风险 | 使用密钥管理服务,配置文件只存引用 |
| 活动 ID 可遍历、接口无 token | 被直接调用领取 | 活动 token、奖品 token、请求签名 |
暂无评论,欢迎留下第一条评论。