很高兴你能来阅读,这里我会陆续总结自己的项目经验,编程学习思路即可。首先是对自己编程经验反思,其次希望我的分享对大家有帮助!
在分布式系统消息传递场景中,幂等性是保障业务数据一致性的核心基础。先明确核心概念,再展开具体解决方案:
什么是幂等性? 指同一操作(或同一条消息)被重复执行多次后,最终得到的业务结果与执行一次完全一致,不会因重复执行导致数据异常(如重复创建订单、库存超额扣减、资金重复划转等)。
方案1:数据库唯一索引
核心原理:利用数据库唯一索引的唯一性约束防止重复插入,结合数据库事务保障幂等标记与业务操作的原子性,避免重复处理。
适配业务场景:
- 订单创建场景:用户下单时,以订单号作为唯一标识,避免重复创建订单;
- 库存扣减场景:扣减库存时,以订单号+商品SKU作为联合唯一标识,防止重复扣减导致负库存。
1. 核心设计:唯一索引表设计(通用去重表,适配多场景)
CREATE TABLE `msg_idempotent` (
`id` bigint NOT NULL AUTO_INCREMENT,
`biz_no` varchar(64) NOT NULL COMMENT '业务唯一标识(如支付流水号)',
`biz_type` varchar(32) NOT NULL COMMENT '业务类型(pay_callback/order_create)',
`status` tinyint NOT NULL COMMENT '处理状态:0-处理中,1-处理成功,2-处理失败',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_biz_no_type` (`biz_no`, `biz_type`) COMMENT '唯一索引:业务号+类型,防止重复'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
幂等性就是 不要消费端重复消费,我们设计一个表,用唯一键,让用户的数据只能进入一次即可。
方案2:Redisson分布式锁(高并发/跨服务场景首选)
核心原理:Redisson封装了Redis分布式锁的完整实现,解决了原生RedisTemplate实现的“锁超时、误删锁”等问题,适用于秒杀、高并发下单、跨服务幂等控制。
1. 核心代码实现(秒杀场景为例)
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class SeckillService {
private final RedissonClient redissonClient;
private final OrderService orderService;
// 构造器注入
public SeckillService(RedissonClient redissonClient, OrderService orderService) {
this.redissonClient = redissonClient;
this.orderService = orderService;
}
/**
* 处理秒杀消息(Redisson分布式锁实现幂等)
* @param userId 用户ID
* @param skuId 商品SKU
*/
public void handleSeckillMsg(String userId, String skuId) {
// 1. 生成唯一锁Key(用户ID+商品ID,确保同一用户同一商品只处理一次)
String lockKey = "idempotent:seckill:" + userId + "_" + skuId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 2. 获取分布式锁:最多等待5秒,锁自动过期时间30秒(Redisson会自动续期)
boolean lockAcquired = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (!lockAcquired) {
// 未获取到锁 → 重复消息/并发处理中,直接返回
log.info("重复处理秒杀消息,userId:{}, skuId:{}", userId, skuId);
return;
}
// 3. 二次校验(防止锁释放后重复执行)
if (orderService.checkSeckillOrderExist(userId, skuId)) {
log.info("用户已秒杀过该商品,userId:{}, skuId:{}", userId, skuId);
return;
}
// 4. 执行业务逻辑(创建秒杀订单)
orderService.createSeckillOrder(userId, skuId);
} catch (InterruptedException e) {
log.error("获取分布式锁中断", e);
Thread.currentThread().interrupt();
} catch (Exception e) {
log.error("处理秒杀消息失败", e);
throw new RuntimeException("秒杀处理失败", e);
} finally {
// 5. 释放锁(仅当前线程持有锁时才释放,避免误删)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
2. 关键优化点(Redisson核心优势)
-
自动续期:Redisson的
RLock会在业务逻辑执行超时前(默认每10秒)自动续期锁的过期时间,避免“业务没执行完,锁先过期”导致的重复处理; -
安全释放:通过
isHeldByCurrentThread()校验,确保只有持有锁的线程才能释放,避免误删其他线程的锁; -
可配置性:支持公平锁、非公平锁、读写锁等,可根据场景调整(秒杀场景用非公平锁即可)。
方案3:Redis缓存标记(轻量场景)
核心原理:将已处理的消息唯一标识存入Redis,处理前先检查是否存在,适合通知推送、日志记录等非核心场景,基于Redisson简化实现。
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class NotifyService {
private final RedissonClient redissonClient;
public NotifyService(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
/**
* 处理通知消息(轻量级幂等)
* @param msgId 消息唯一ID
*/
public void handleNotifyMsg(String msgId) {
// 1. 生成缓存Key
String cacheKey = "idempotent:notify:" + msgId;
RBucket<String> bucket = redissonClient.getBucket(cacheKey);
// 2. 检查是否已处理
if (bucket.isExists()) {
log.info("通知消息已处理,msgId:{}", msgId);
return;
}
// 3. 执行业务逻辑(发送短信/推送)
sendSms(msgId);
// 4. 存入Redis,设置24小时过期(避免缓存膨胀)
bucket.set("1", 24, TimeUnit.HOURS);
}
private void sendSms(String msgId) {
// 发送短信逻辑
}
}
方案4:状态机控制(订单/工单场景)
场景:这里适用于状态严格流程的场景,必须A->B->C 每次都会新增校验判断,避免重复更新!
核心原理:严格限定业务状态流转规则,结合数据库唯一索引,杜绝重复更新,适用于订单状态流转(待支付→已支付→已发货)。
// 订单状态枚举
public enum OrderStatus {
UNPAID(0, "待支付"),
PAID(1, "已支付"),
SHIPPED(2, "已发货"),
FINISHED(3, "已完成");
private final int code;
private final String desc;
OrderStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
public int getCode() {
return code;
}
}
@Service
@Transactional(rollbackFor = Exception.class)
public class OrderStatusService {
private final OrderMapper orderMapper;
public OrderStatusService(OrderMapper orderMapper) {
this.orderMapper = orderMapper;
}
/**
* 更新订单状态(状态机控制幂等)
* @param orderId 订单ID
* @param targetStatus 目标状态
* @return 是否更新成功
*/
public boolean updateOrderStatus(Long orderId, OrderStatus targetStatus) {
// 1. 查询当前订单状态
OrderDO order = orderMapper.getById(orderId);
if (order == null) {
log.warn("订单不存在,orderId:{}", orderId);
return false;
}
// 2. 状态机校验:仅允许待支付→已支付的合法流转
if (OrderStatus.UNPAID.getCode() != order.getStatus() || OrderStatus.PAID != targetStatus) {
log.info("订单状态不允许更新,orderId:{}, 当前状态:{}, 目标状态:{}",
orderId, order.getStatus(), targetStatus.getCode());
return false;
}
// 3. 执行状态更新(结合订单表唯一索引保障幂等)
return orderMapper.updateStatus(orderId, targetStatus.getCode()) == 1;
}
}
二、不同场景的方案选型建议
幂等性问题核心是同一操作重复触发/执行,主要分两类原因:
- 客观上,网络异常(响应或ACK丢失)、系统组件重试(网关、消息队列等)、分布式一致性保障(事务补偿等)导致重复,难以完全避免;
- 主观上,用户误操作(如重复提交)、人工运维/运营误操作引发重复,可通过产品设计和权限控制减少。
| 场景 | 推荐方案 | 核心优势 |
|---|---|---|
| 中低并发写库 | 数据库唯一索引 | 简单、事务一致性保障 |
| 高并发/跨服务/秒杀 | Redisson分布式锁 | 自动续期、安全释放、高可用 |
| 轻量通知/日志 | Redis缓存标记(Redisson) | 实现简单、性能最优 |
| 订单/工单状态流转 | 状态机+数据库唯一索引 | 贴合业务、逻辑严谨 |
补充说明:
最常见的场景按钮的重复点击导致的幂等性问题,我们可以新增前端限制!
这里关于按钮重复点击的问题我补充一点:
-
前端:这里也可以补充控制,如点击一次后按钮置灰,直到响应返回。
- 很多新手前端这里容易忘记,比如我们提交订单这里就可以设计,避免用户手抖重复提交!
-
后端:幂等性校验
📣非常感谢你阅读到这里,如果这篇文章对你有帮助,希望能留下你的点赞👍 关注❤️ 分享👥 留言💬thanks!!!