电商营销系统中的幂等性设计:从抽奖积分发放谈起

后台业务开发多年发现我们设计开发各种的业务功能,里面很多基础功能其实是有共性的,后序将分析通用基础能力的设计思路,将工作中经常使用的比较通用优雅的设计分享出来,供大家一起学习讨论。本文分享幂等设计方案,以玩法抽奖后积分发放为案例。

在电商营销活动中,抽奖是常见的用户增长手段。用户参与抽奖后,玩法系统需要调用积分系统发放奖励积分。这个看似简单的过程,在分布式系统中却面临诸多挑战:

  • 网络波动:RPC 调用可能超时,但实际操作已在积分系统执行
  • 用户重试:用户未收到结果反馈时,可能重复点击抽奖按钮
  • 系统重试:中间件或框架自动重试超时请求

这些场景都可能导致积分重复发放 ,给公司带来损失。因此,我们需要一套机制确保:同一请求无论调用多少次,对系统产生的影响都和执行一次时相同------ 这就是幂等性设计的核心目标。

二、玩法系统 A 的幂等实现:状态流转与重试机制

2.1 幂等表设计

sql 复制代码
CREATE TABLE `play_idempotent` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `biz_type` varchar(64) NOT NULL COMMENT '业务类型(如抽奖活动ID)',
  `req_id` varchar(64) NOT NULL COMMENT '业务唯一标识',
  `assist_id` varchar(64) DEFAULT NULL COMMENT '辅助键',
  `status` tinyint NOT NULL DEFAULT '0' COMMENT '0=初始化, 1=成功, 2=处理中, 3=失败',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_req_assist` (`user_id`,`req_id`,`assist_id`)
) ENGINE=InnoDB COMMENT='玩法系统幂等表';

关键设计点

  • assist_id初始值为userId,失败后更新为记录主键id
  • 唯一索引user_id+req_id+assist_id确保同一请求不会重复执行

2.2 状态流转逻辑

2.3 核心代码实现

java 复制代码
public Result processLottery(Long userId, String reqId) {
    // 1. 尝试插入初始化记录(assist_id默认为userId)
    try {
        idempotentDao.insert(userId, "LOTTERY", reqId, userId, 0);
    } catch (DuplicateKeyException e) {
        // 唯一索引冲突,查询最新状态
        PlayIdempotentDO record = idempotentDao.select(userId, reqId);
        if (record.getStatus() == 1) {
            return Result.success("积分已发放");
        } else if (record.getStatus() == 2) {
            // 处理中状态:重新调用积分发放
            return retryGrantPoints(userId, reqId);
        } else {
            // 失败状态:重置assist_id为userId,重新插入
            idempotentDao.resetAssistId(userId, reqId, userId);
            return processLottery(userId, reqId);
        }
    }

    // 2. 调用积分系统发放积分
    try {
        Result积分结果 = pointService.grantPoints(userId, 100, reqId);
        if (积分结果.isSuccess()) {
            idempotentDao.updateStatus(reqId, 1); // 更新为成功
            return Result.success("抽奖成功,积分已发放");
        } else {
            // 明确失败:更新状态为失败,assist_id设为记录主键ID
            Long id = idempotentDao.selectId(userId, reqId);
            idempotentDao.updateFailed(reqId, id);
            return Result.fail("抽奖失败:" + 积分结果.getMsg());
        }
    } catch (Exception e) {
        // 未知错误:更新为处理中状态
        idempotentDao.updateStatus(reqId, 2);
        return Result.processing("系统处理中,请稍后查询");
    }
}

三、积分系统 B 的幂等实现:数据库事务保障

3.1 幂等表设计

sql 复制代码
CREATE TABLE `point_idempotent` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `req_id` varchar(64) NOT NULL COMMENT '业务唯一标识',
  `source` varchar(64) NOT NULL COMMENT '积分来源',
  `points` int NOT NULL COMMENT '积分数量',
  `operate_type` tinyint NOT NULL COMMENT '操作类型:1=发放,2=扣减',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_req_operate` (`user_id`,`req_id`,`operate_type`)
) ENGINE=InnoDB COMMENT='积分系统幂等表';

3.2 积分总账户表

3.3 核心逻辑:事务中的原子操作

sql 复制代码
CREATE TABLE `user_points` (
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `total_points` int NOT NULL DEFAULT '0' COMMENT '总积分',
  `update_time` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB COMMENT='用户积分总账户';

核心问题与设计目标

java 复制代码
@Transactional
public Result grantPoints(Long userId, Integer points, String reqId) {
    try {
        // 1. 插入幂等表记录(利用唯一索引防重)
        pointIdempotentDao.insert(userId, reqId, "lottery", points, 1);
        
        // 2. 更新用户积分账户(原子操作)
        userPointsDao.increment(userId, points);
        
        return Result.success("积分发放成功");
    } catch (DuplicateKeyException e) {
        // 主键冲突,说明已经处理过,直接返回成功
        return Result.success("积分已发放");
    } catch (Exception e) {
        // 其他异常,回滚事务
        throw new RuntimeException("积分发放失败", e);
    }
}

四、跨系统协作的幂等保障

4.1 整体调用流程

4.2 异常处理机制

  • 超时重试:玩法系统 A 在遇到超时等未知错误时,将记录标记为 "处理中",后续定时任务会扫描此类记录并自动重试
  • 失败恢复 :积分系统 B 失败时,玩法系统 A 将记录标记为 "失败",并将assist_id设为记录主键 ID,允许用户重新发起请求
  • 最终一致性:通过定期对账和人工干预,确保极端情况下的数据一致性

五、方案优势与注意事项

5.1 主要优势

  • 简单可靠:利用数据库唯一索引和事务,无需复杂分布式锁
  • 动态重试 :通过assist_id的动态调整,实现失败请求的安全重试
  • 分层保障:玩法系统和积分系统各自实现幂等,形成双重防护

5.2 注意事项

  • req_id 生成规则:必须保证全局唯一,需要根据实际业务规则确定,建议使用 "业务类型 + 时间戳 + 随机数" 组合
  • 事务边界:积分系统的插入幂等表和更新积分账户必须在同一事务
  • 状态监控:定期清理长时间处于 "处理中" 状态的记录,防止资源堆积

六、总结:幂等性设计的核心思路

在电商营销系统中,幂等性设计的关键在于:

  1. 唯一标识 :为每个请求分配唯一的req_id,作为幂等校验的基础
  2. 状态流转:通过状态机控制请求生命周期,明确每个状态的处理逻辑
  3. 数据库保障:利用唯一索引和事务,实现低成本、高可靠的幂等控制
  4. 重试策略:失败请求通过动态调整辅助键,安全绕过唯一索引限制

通过玩法系统和积分系统的双重幂等设计,我们成功解决了抽奖活动中积分发放的重复问题,确保了系统的稳定性和数据的准确性。这套方案不仅适用于积分发放,也可推广到优惠券发放、订单支付等多种需要幂等保障的场景。

相关推荐
开心猴爷11 小时前
iOS App 性能测试中常被忽略的运行期问题
后端
SHERlocked9312 小时前
摄像头 RTSP 流视频多路实时监控解决方案实践
c++·后端·音视频开发
AutoMQ12 小时前
How does AutoMQ implement a sub-10ms latency Diskless Kafka?
后端·架构
Rover.x12 小时前
Netty基于SpringBoot实现WebSocket
spring boot·后端·websocket
疯狂的程序猴12 小时前
用 HBuilder 上架 iOS 应用时如何管理Bundle ID、证书与描述文件
后端
ShaneD77113 小时前
Redis 实战:从零手写分布式锁(误删问题与 Lua 脚本优化)
后端
我命由我1234513 小时前
Python Flask 开发问题:ImportError: cannot import name ‘Markup‘ from ‘flask‘
开发语言·后端·python·学习·flask·学习方法·python3.11
無量13 小时前
Java并发编程基础:从线程到锁
后端
小信啊啊13 小时前
Go语言数组与切片的区别
开发语言·后端·golang
计算机学姐13 小时前
基于php的摄影网站系统
开发语言·vue.js·后端·mysql·php·phpstorm