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

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

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

  • 网络波动: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. 重试策略:失败请求通过动态调整辅助键,安全绕过唯一索引限制

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

相关推荐
烛阴3 小时前
bignumber.js深度解析:驾驭任意精度计算的终极武器
前端·javascript·后端
你的人类朋友3 小时前
✍️Node.js CMS框架概述:Directus与Strapi详解
javascript·后端·node.js
面朝大海,春不暖,花不开4 小时前
自定义Spring Boot Starter的全面指南
java·spring boot·后端
钡铼技术ARM工业边缘计算机4 小时前
【成本降40%·性能翻倍】RK3588边缘控制器在安防联动系统的升级路径
后端
CryptoPP5 小时前
使用WebSocket实时获取印度股票数据源(无调用次数限制)实战
后端·python·websocket·网络协议·区块链
白宇横流学长5 小时前
基于SpringBoot实现的大创管理系统设计与实现【源码+文档】
java·spring boot·后端
草捏子6 小时前
状态机设计:比if-else优雅100倍的设计
后端
考虑考虑7 小时前
Springboot3.5.x结构化日志新属性
spring boot·后端·spring
涡能增压发动积7 小时前
一起来学 Langgraph [第三节]
后端