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

在电商营销活动中,抽奖是常见的用户增长手段。用户参与抽奖后,玩法系统需要调用积分系统发放奖励积分。这个看似简单的过程,在分布式系统中却面临诸多挑战:
- 网络波动: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 生成规则:必须保证全局唯一,需要根据实际业务规则确定,建议使用 "业务类型 + 时间戳 + 随机数" 组合
- 事务边界:积分系统的插入幂等表和更新积分账户必须在同一事务
- 状态监控:定期清理长时间处于 "处理中" 状态的记录,防止资源堆积
六、总结:幂等性设计的核心思路
在电商营销系统中,幂等性设计的关键在于:
- 唯一标识 :为每个请求分配唯一的
req_id
,作为幂等校验的基础 - 状态流转:通过状态机控制请求生命周期,明确每个状态的处理逻辑
- 数据库保障:利用唯一索引和事务,实现低成本、高可靠的幂等控制
- 重试策略:失败请求通过动态调整辅助键,安全绕过唯一索引限制
通过玩法系统和积分系统的双重幂等设计,我们成功解决了抽奖活动中积分发放的重复问题,确保了系统的稳定性和数据的准确性。这套方案不仅适用于积分发放,也可推广到优惠券发放、订单支付等多种需要幂等保障的场景。