幂等怎么做?有哪些常见坑?(深入实战)

1. 概述

1.1 幂等性的实战重要性

幂等性在真实项目中是必须考虑的核心问题,特别是在高并发、分布式、消息队列等场景下,幂等性设计直接影响系统的正确性和稳定性。

真实项目中的挑战

  • 高并发场景:大量并发请求可能导致重复操作
  • 网络重试:网络不稳定导致自动重试
  • 消息重复:消息队列可能重复投递
  • 分布式环境:多节点环境下保证幂等性
  • 性能影响:幂等性实现不能影响系统性能

1.2 本文重点

本文将从实战角度深入解析幂等性:

  1. 复杂场景处理:高并发、分布式、消息队列等场景
  2. 性能优化:如何在不影响性能的前提下实现幂等性
  3. 常见坑深度分析:真实项目中遇到的坑和解决方案
  4. 真实项目案例:从实际项目中总结的经验
  5. 最佳实践:经过验证的最佳实践方案

2. 幂等性实现方案深入

2.1 唯一索引方案(深入)

2.1.1 原理深入

唯一索引方案:利用数据库唯一索引保证幂等性。

适用场景

  • 创建操作(INSERT)
  • 有唯一业务标识的场景

实现要点

  • 唯一索引必须包含业务唯一标识
  • 需要处理唯一索引冲突异常
  • 需要考虑索引性能影响

2.1.2 高并发场景实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Service
public class OrderService {

@Autowired
private OrderMapper orderMapper;

/**
* 创建订单(高并发场景)
* 使用唯一索引 + 异常处理保证幂等性
*/
public OrderResult createOrder(OrderRequest request) {
// 1. 生成唯一订单号(业务唯一标识)
String orderNo = generateOrderNo(request);

// 2. 构建订单对象
Order order = new Order();
order.setOrderNo(orderNo); // 唯一索引字段
order.setUserId(request.getUserId());
order.setAmount(request.getAmount());
order.setStatus(OrderStatus.PENDING);

try {
// 3. 插入订单(唯一索引保证幂等性)
orderMapper.insert(order);

// 4. 返回成功
return OrderResult.success(order);
} catch (DuplicateKeyException e) {
// 5. 唯一索引冲突,说明订单已存在(幂等性保证)
log.info("Order already exists: {}", orderNo);

// 6. 查询已存在的订单
Order existingOrder = orderMapper.selectByOrderNo(orderNo);

// 7. 返回已存在的订单(幂等性:多次调用返回相同结果)
return OrderResult.success(existingOrder);
} catch (Exception e) {
log.error("Create order failed", e);
throw new BusinessException("创建订单失败", e);
}
}

/**
* 生成唯一订单号
* 格式:ORDER + 时间戳 + 用户ID + 随机数
*/
private String generateOrderNo(OrderRequest request) {
return String.format("ORDER%d%d%04d",
System.currentTimeMillis(),
request.getUserId(),
ThreadLocalRandom.current().nextInt(10000));
}
}

2.1.3 性能优化

问题:唯一索引在高并发下可能成为瓶颈。

优化方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Service
public class OrderService {

@Autowired
private OrderMapper orderMapper;

@Autowired
private RedisTemplate<String, String> redisTemplate;

/**
* 创建订单(性能优化版本)
* 使用Redis预检查 + 唯一索引双重保障
*/
public OrderResult createOrderOptimized(OrderRequest request) {
// 1. 生成唯一订单号
String orderNo = generateOrderNo(request);

// 2. Redis预检查(减少数据库压力)
String cacheKey = "order:exists:" + orderNo;
Boolean exists = redisTemplate.hasKey(cacheKey);

if (Boolean.TRUE.equals(exists)) {
// Redis中已存在,直接返回(快速路径)
Order existingOrder = orderMapper.selectByOrderNo(orderNo);
return OrderResult.success(existingOrder);
}

// 3. 构建订单对象
Order order = new Order();
order.setOrderNo(orderNo);
order.setUserId(request.getUserId());
order.setAmount(request.getAmount());
order.setStatus(OrderStatus.PENDING);

try {
// 4. 插入订单(唯一索引保证幂等性)
orderMapper.insert(order);

// 5. 设置Redis缓存(标记订单已存在)
redisTemplate.opsForValue().set(cacheKey, "1", 1, TimeUnit.HOURS);

return OrderResult.success(order);
} catch (DuplicateKeyException e) {
// 6. 唯一索引冲突(并发场景)
log.info("Order already exists (concurrent): {}", orderNo);

// 7. 设置Redis缓存
redisTemplate.opsForValue().set(cacheKey, "1", 1, TimeUnit.HOURS);

// 8. 查询已存在的订单
Order existingOrder = orderMapper.selectByOrderNo(orderNo);
return OrderResult.success(existingOrder);
} catch (Exception e) {
log.error("Create order failed", e);
throw new BusinessException("创建订单失败", e);
}
}
}

2.2 分布式锁方案(深入)

2.2.1 原理深入

分布式锁方案:使用分布式锁保证同一时间只有一个请求能执行操作。

适用场景

  • 更新操作(UPDATE)
  • 需要保证原子性的场景
  • 高并发场景

实现要点

  • 锁的粒度要合适(不能太粗,也不能太细)
  • 需要设置合理的锁超时时间
  • 需要处理锁释放失败的情况

2.2.2 高并发场景实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
@Service
public class AccountService {

@Autowired
private RedissonClient redissonClient;

@Autowired
private AccountMapper accountMapper;

/**
* 转账(高并发场景)
* 使用分布式锁 + 幂等性检查
*/
public TransferResult transfer(TransferRequest request) {
// 1. 生成唯一转账ID(幂等性标识)
String transferId = generateTransferId(request);

// 2. 检查是否已处理(幂等性检查)
TransferRecord existingRecord = transferRecordMapper.selectByTransferId(transferId);
if (existingRecord != null) {
// 已处理,直接返回(幂等性)
return TransferResult.success(existingRecord);
}

// 3. 获取分布式锁(防止并发)
String lockKey = "transfer:lock:" + request.getFromAccountId();
RLock lock = redissonClient.getLock(lockKey);

try {
// 4. 尝试加锁(最多等待100ms,锁定30秒)
boolean locked = lock.tryLock(100, 30, TimeUnit.MILLISECONDS);
if (!locked) {
throw new BusinessException("系统繁忙,请稍后再试");
}

// 5. 双重检查(防止并发场景下的重复处理)
existingRecord = transferRecordMapper.selectByTransferId(transferId);
if (existingRecord != null) {
return TransferResult.success(existingRecord);
}

// 6. 执行转账操作
Account fromAccount = accountMapper.selectById(request.getFromAccountId());
Account toAccount = accountMapper.selectById(request.getToAccountId());

if (fromAccount.getBalance().compareTo(request.getAmount()) < 0) {
throw new BusinessException("余额不足");
}

fromAccount.setBalance(fromAccount.getBalance().subtract(request.getAmount()));
toAccount.setBalance(toAccount.getBalance().add(request.getAmount()));

accountMapper.updateById(fromAccount);
accountMapper.updateById(toAccount);

// 7. 记录转账记录(幂等性标记)
TransferRecord record = new TransferRecord();
record.setTransferId(transferId);
record.setFromAccountId(request.getFromAccountId());
record.setToAccountId(request.getToAccountId());
record.setAmount(request.getAmount());
record.setStatus(TransferStatus.SUCCESS);
transferRecordMapper.insert(record);

return TransferResult.success(record);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException("获取锁失败", e);
} finally {
// 8. 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}

/**
* 生成唯一转账ID
*/
private String generateTransferId(TransferRequest request) {
return String.format("TRANSFER%d%d%d",
System.currentTimeMillis(),
request.getFromAccountId(),
request.getToAccountId(),
request.getAmount().hashCode());
}
}

2.2.3 性能优化

问题:分布式锁可能成为性能瓶颈。

优化方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Service
public class AccountService {

/**
* 转账(性能优化版本)
* 使用分段锁 + 本地缓存
*/
public TransferResult transferOptimized(TransferRequest request) {
String transferId = generateTransferId(request);

// 1. 检查是否已处理(快速路径)
TransferRecord existingRecord = transferRecordMapper.selectByTransferId(transferId);
if (existingRecord != null) {
return TransferResult.success(existingRecord);
}

// 2. 使用分段锁(减少锁竞争)
// 根据账户ID取模,将锁分散到多个分段
int segment = (int) (request.getFromAccountId() % 100);
String lockKey = "transfer:lock:segment:" + segment;
RLock lock = redissonClient.getLock(lockKey);

try {
boolean locked = lock.tryLock(50, 20, TimeUnit.MILLISECONDS);
if (!locked) {
throw new BusinessException("系统繁忙,请稍后再试");
}

// 3. 双重检查
existingRecord = transferRecordMapper.selectByTransferId(transferId);
if (existingRecord != null) {
return TransferResult.success(existingRecord);
}

// 4. 执行转账操作
return doTransfer(request, transferId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException("获取锁失败", e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}

2.3 Token机制(深入)

2.3.1 原理深入

Token机制:客户端请求前先获取Token,请求时携带Token,服务端验证Token后删除。

适用场景

  • 前端防重复提交
  • 表单提交
  • 支付场景

实现要点

  • Token需要设置合理的过期时间
  • Token需要保证唯一性
  • Token验证后必须删除

2.3.2 完整实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@Service
public class TokenService {

@Autowired
private RedisTemplate<String, String> redisTemplate;

/**
* 生成Token
*/
public String generateToken(String userId) {
// 1. 生成唯一Token
String token = UUID.randomUUID().toString();

// 2. 存储到Redis(设置过期时间5分钟)
String tokenKey = "token:" + userId + ":" + token;
redisTemplate.opsForValue().set(tokenKey, "1", 5, TimeUnit.MINUTES);

return token;
}

/**
* 验证并消费Token
*/
public boolean verifyAndConsumeToken(String userId, String token) {
String tokenKey = "token:" + userId + ":" + token;

// 1. 检查Token是否存在
Boolean exists = redisTemplate.hasKey(tokenKey);
if (!Boolean.TRUE.equals(exists)) {
return false; // Token不存在或已过期
}

// 2. 删除Token(保证只能使用一次)
Boolean deleted = redisTemplate.delete(tokenKey);
return Boolean.TRUE.equals(deleted);
}
}

@RestController
@RequestMapping("/api/order")
public class OrderController {

@Autowired
private TokenService tokenService;

@Autowired
private OrderService orderService;

/**
* 获取Token(防重复提交)
*/
@GetMapping("/token")
public Result<String> getToken(@RequestParam String userId) {
String token = tokenService.generateToken(userId);
return Result.success(token);
}

/**
* 创建订单(使用Token防重复提交)
*/
@PostMapping("/create")
public Result<Order> createOrder(@RequestBody OrderRequest request) {
// 1. 验证Token
if (!tokenService.verifyAndConsumeToken(request.getUserId(), request.getToken())) {
return Result.error("Token无效或已使用,请重新获取");
}

// 2. 创建订单
Order order = orderService.createOrder(request);
return Result.success(order);
}
}

2.3.3 高并发优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Service
public class TokenService {

@Autowired
private RedisTemplate<String, String> redisTemplate;

/**
* 验证并消费Token(原子操作优化)
* 使用Lua脚本保证原子性
*/
public boolean verifyAndConsumeTokenAtomic(String userId, String token) {
String tokenKey = "token:" + userId + ":" + token;

// Lua脚本:检查并删除Token(原子操作)
String luaScript =
"if redis.call('exists', KEYS[1]) == 1 then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";

DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(luaScript);
script.setResultType(Long.class);

Long result = redisTemplate.execute(script, Collections.singletonList(tokenKey));
return result != null && result > 0;
}
}

2.4 状态机方案(深入)

2.4.1 原理深入

状态机方案:通过状态机控制操作流程,只有特定状态才能执行特定操作。

适用场景

  • 订单状态流转
  • 支付状态流转
  • 工作流场景

实现要点

  • 状态转换必须合法
  • 需要处理状态冲突
  • 需要记录状态变更历史

2.4.2 完整实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
public enum OrderStatus {
PENDING(0, "待支付"),
PAID(1, "已支付"),
SHIPPED(2, "已发货"),
COMPLETED(3, "已完成"),
CANCELLED(-1, "已取消");

private final int code;
private final String desc;

OrderStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}

/**
* 检查状态转换是否合法
*/
public boolean canTransitionTo(OrderStatus target) {
switch (this) {
case PENDING:
return target == PAID || target == CANCELLED;
case PAID:
return target == SHIPPED || target == CANCELLED;
case SHIPPED:
return target == COMPLETED;
case COMPLETED:
case CANCELLED:
return false; // 终态,不能转换
default:
return false;
}
}
}

@Service
public class OrderService {

@Autowired
private OrderMapper orderMapper;

/**
* 支付订单(状态机保证幂等性)
*/
@Transactional
public PaymentResult payOrder(Long orderId, BigDecimal amount) {
// 1. 查询订单
Order order = orderMapper.selectById(orderId);
if (order == null) {
throw new BusinessException("订单不存在");
}

// 2. 检查当前状态是否可以支付(状态机检查)
if (!order.getStatus().canTransitionTo(OrderStatus.PAID)) {
// 状态不合法,可能是重复支付(幂等性检查)
if (order.getStatus() == OrderStatus.PAID) {
// 已支付,返回成功(幂等性)
return PaymentResult.success(order, "订单已支付");
}
throw new BusinessException("订单状态不允许支付");
}

// 3. 检查金额
if (order.getAmount().compareTo(amount) != 0) {
throw new BusinessException("支付金额不匹配");
}

// 4. 更新订单状态(乐观锁保证并发安全)
int updated = orderMapper.updateStatusWithVersion(
orderId,
OrderStatus.PENDING,
OrderStatus.PAID,
order.getVersion()
);

if (updated == 0) {
// 更新失败,可能是并发修改或状态已变更
// 重新查询订单
order = orderMapper.selectById(orderId);
if (order.getStatus() == OrderStatus.PAID) {
// 已支付,返回成功(幂等性)
return PaymentResult.success(order, "订单已支付");
}
throw new BusinessException("订单状态已变更,请刷新后重试");
}

// 5. 记录支付记录
PaymentRecord record = new PaymentRecord();
record.setOrderId(orderId);
record.setAmount(amount);
record.setStatus(PaymentStatus.SUCCESS);
paymentRecordMapper.insert(record);

return PaymentResult.success(order);
}
}

2.5 去重表方案(深入)

2.5.1 原理深入

去重表方案:创建专门的去重表,记录已处理的请求。

适用场景

  • 消息队列消费
  • 定时任务
  • 批量处理

实现要点

  • 去重表需要唯一索引
  • 需要定期清理历史数据
  • 需要考虑表性能

2.5.2 消息队列场景实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
@Entity
@Table(name = "message_dedup", indexes = {
@Index(name = "uk_message_id", columnList = "messageId", unique = true)
})
public class MessageDedup {
@Id
private Long id;

@Column(nullable = false, unique = true)
private String messageId; // 消息唯一ID

private String businessType; // 业务类型
private String businessId; // 业务ID
private Integer status; // 处理状态:0-处理中,1-成功,2-失败
private LocalDateTime createTime;
private LocalDateTime updateTime;
}

@Service
public class OrderMessageConsumer {

@Autowired
private MessageDedupMapper messageDedupMapper;

@Autowired
private OrderService orderService;

/**
* 消费订单消息(去重表保证幂等性)
*/
@KafkaListener(topics = "order-created", groupId = "order-processor")
public void handleOrderCreated(ConsumerRecord<String, String> record) {
String messageId = record.key(); // 消息唯一ID
String message = record.value();

// 1. 检查消息是否已处理(去重表检查)
MessageDedup dedup = messageDedupMapper.selectByMessageId(messageId);
if (dedup != null) {
// 消息已处理,直接返回(幂等性)
log.info("Message already processed: {}", messageId);
return;
}

// 2. 插入去重记录(处理中状态)
try {
dedup = new MessageDedup();
dedup.setMessageId(messageId);
dedup.setBusinessType("ORDER_CREATED");
dedup.setStatus(0); // 处理中
dedup.setCreateTime(LocalDateTime.now());
messageDedupMapper.insert(dedup);
} catch (DuplicateKeyException e) {
// 并发场景:其他线程已处理
log.info("Message already processed (concurrent): {}", messageId);
return;
}

// 3. 处理消息
try {
Order order = JSON.parseObject(message, Order.class);
orderService.processOrder(order);

// 4. 更新处理状态为成功
dedup.setStatus(1);
dedup.setUpdateTime(LocalDateTime.now());
messageDedupMapper.updateById(dedup);
} catch (Exception e) {
log.error("Process message failed: {}", messageId, e);

// 5. 更新处理状态为失败
dedup.setStatus(2);
dedup.setUpdateTime(LocalDateTime.now());
messageDedupMapper.updateById(dedup);

// 6. 可以选择重试或发送到死信队列
throw e;
}
}
}

2.5.3 性能优化

问题:去重表在高并发下可能成为瓶颈。

优化方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@Service
public class OrderMessageConsumer {

@Autowired
private RedisTemplate<String, String> redisTemplate;

@Autowired
private MessageDedupMapper messageDedupMapper;

/**
* 消费订单消息(性能优化版本)
* Redis预检查 + 去重表双重保障
*/
@KafkaListener(topics = "order-created", groupId = "order-processor")
public void handleOrderCreatedOptimized(ConsumerRecord<String, String> record) {
String messageId = record.key();
String message = record.value();

// 1. Redis预检查(快速路径)
String cacheKey = "message:processed:" + messageId;
Boolean exists = redisTemplate.hasKey(cacheKey);
if (Boolean.TRUE.equals(exists)) {
log.info("Message already processed (Redis): {}", messageId);
return;
}

// 2. 去重表检查(数据库检查)
MessageDedup dedup = messageDedupMapper.selectByMessageId(messageId);
if (dedup != null) {
// 设置Redis缓存
redisTemplate.opsForValue().set(cacheKey, "1", 1, TimeUnit.HOURS);
return;
}

// 3. 插入去重记录
try {
dedup = new MessageDedup();
dedup.setMessageId(messageId);
dedup.setBusinessType("ORDER_CREATED");
dedup.setStatus(0);
dedup.setCreateTime(LocalDateTime.now());
messageDedupMapper.insert(dedup);
} catch (DuplicateKeyException e) {
// 并发场景
redisTemplate.opsForValue().set(cacheKey, "1", 1, TimeUnit.HOURS);
return;
}

// 4. 处理消息
try {
Order order = JSON.parseObject(message, Order.class);
orderService.processOrder(order);

dedup.setStatus(1);
dedup.setUpdateTime(LocalDateTime.now());
messageDedupMapper.updateById(dedup);

// 5. 设置Redis缓存
redisTemplate.opsForValue().set(cacheKey, "1", 1, TimeUnit.HOURS);
} catch (Exception e) {
log.error("Process message failed: {}", messageId, e);
dedup.setStatus(2);
dedup.setUpdateTime(LocalDateTime.now());
messageDedupMapper.updateById(dedup);
throw e;
}
}
}

3. 常见坑深度分析

3.1 坑1:唯一索引冲突处理不当

3.1.1 问题描述

问题:唯一索引冲突时,直接抛出异常,没有检查是否是重复请求。

错误示例

1
2
3
4
// 错误示例:没有处理唯一索引冲突
public void createOrder(Order order) {
orderMapper.insert(order); // 如果订单已存在,直接抛异常
}

3.1.2 正确做法

1
2
3
4
5
6
7
8
9
10
11
// 正确示例:处理唯一索引冲突
public OrderResult createOrder(Order order) {
try {
orderMapper.insert(order);
return OrderResult.success(order);
} catch (DuplicateKeyException e) {
// 唯一索引冲突,查询已存在的订单
Order existingOrder = orderMapper.selectByOrderNo(order.getOrderNo());
return OrderResult.success(existingOrder); // 返回已存在的订单(幂等性)
}
}

3.2 坑2:分布式锁释放失败

3.2.1 问题描述

问题:分布式锁释放失败,导致后续请求无法获取锁。

错误示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 错误示例:没有在finally中释放锁
public void transfer(TransferRequest request) {
RLock lock = redissonClient.getLock("transfer:lock");
lock.lock();

try {
// 执行转账
doTransfer(request);
} catch (Exception e) {
// 异常时没有释放锁
throw e;
}
// 正常流程也没有释放锁
}

3.2.2 正确做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 正确示例:在finally中释放锁
public void transfer(TransferRequest request) {
RLock lock = redissonClient.getLock("transfer:lock");

try {
lock.lock();
doTransfer(request);
} finally {
// 确保锁被释放
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}

3.3 坑3:Token验证后未删除

3.3.1 问题描述

问题:Token验证后没有删除,导致Token可以重复使用。

错误示例

1
2
3
4
5
6
// 错误示例:Token验证后没有删除
public boolean verifyToken(String token) {
String tokenKey = "token:" + token;
Boolean exists = redisTemplate.hasKey(tokenKey);
return Boolean.TRUE.equals(exists); // 只检查,不删除
}

3.3.2 正确做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 正确示例:Token验证后删除
public boolean verifyAndConsumeToken(String token) {
String tokenKey = "token:" + token;

// 使用Lua脚本保证原子性
String luaScript =
"if redis.call('exists', KEYS[1]) == 1 then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";

DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(luaScript);
script.setResultType(Long.class);

Long result = redisTemplate.execute(script, Collections.singletonList(tokenKey));
return result != null && result > 0;
}

3.4 坑4:状态机检查不完整

3.4.1 问题描述

问题:只检查当前状态,没有检查状态转换是否合法。

错误示例

1
2
3
4
5
6
7
8
9
10
// 错误示例:只检查状态,没有检查状态转换
public void payOrder(Long orderId) {
Order order = orderMapper.selectById(orderId);
if (order.getStatus() != OrderStatus.PENDING) {
throw new BusinessException("订单状态不正确");
}
// 没有检查状态转换是否合法
order.setStatus(OrderStatus.PAID);
orderMapper.updateById(order);
}

3.4.2 正确做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 正确示例:检查状态转换是否合法
public void payOrder(Long orderId) {
Order order = orderMapper.selectById(orderId);

// 检查状态转换是否合法
if (!order.getStatus().canTransitionTo(OrderStatus.PAID)) {
if (order.getStatus() == OrderStatus.PAID) {
// 已支付,返回成功(幂等性)
return;
}
throw new BusinessException("订单状态不允许支付");
}

// 使用乐观锁更新状态
int updated = orderMapper.updateStatusWithVersion(
orderId,
order.getStatus(),
OrderStatus.PAID,
order.getVersion()
);

if (updated == 0) {
// 更新失败,重新查询
order = orderMapper.selectById(orderId);
if (order.getStatus() == OrderStatus.PAID) {
return; // 幂等性
}
throw new BusinessException("订单状态已变更");
}
}

3.5 坑5:去重表数据未清理

3.5.1 问题描述

问题:去重表数据不断积累,导致表越来越大,影响性能。

错误示例

1
2
// 错误示例:没有清理历史数据
// 去重表数据不断积累,没有清理机制

3.5.2 正确做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 正确示例:定期清理历史数据
@Component
public class MessageDedupCleaner {

@Autowired
private MessageDedupMapper messageDedupMapper;

/**
* 定期清理历史数据(保留最近7天的数据)
*/
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void cleanHistoryData() {
LocalDateTime cutoffTime = LocalDateTime.now().minusDays(7);
int deleted = messageDedupMapper.deleteByCreateTimeBefore(cutoffTime);
log.info("Cleaned {} message dedup records", deleted);
}
}

3.6 坑6:并发场景下的双重检查失效

3.6.1 问题描述

问题:在高并发场景下,双重检查可能失效。

错误示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 错误示例:双重检查没有加锁
public void processOrder(Order order) {
// 第一次检查
if (orderProcessed(order.getId())) {
return;
}

// 没有加锁,可能并发执行
processOrderInternal(order);

// 第二次检查(可能已经晚了)
if (orderProcessed(order.getId())) {
return;
}
}

3.6.2 正确做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 正确示例:双重检查 + 分布式锁
public void processOrder(Order order) {
// 第一次检查(快速路径)
if (orderProcessed(order.getId())) {
return;
}

// 获取分布式锁
String lockKey = "order:process:lock:" + order.getId();
RLock lock = redissonClient.getLock(lockKey);

try {
if (lock.tryLock(100, 30, TimeUnit.MILLISECONDS)) {
// 第二次检查(加锁后检查)
if (orderProcessed(order.getId())) {
return;
}

// 执行处理
processOrderInternal(order);
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}

4. 真实项目案例

4.1 案例1:高并发支付系统

4.1.1 场景

需求:支付系统,需要保证支付幂等性,支持高并发。

挑战

  • 高并发场景(QPS 10万+)
  • 网络重试
  • 消息重复

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@Service
public class PaymentService {

@Autowired
private PaymentRecordMapper paymentRecordMapper;

@Autowired
private RedisTemplate<String, String> redisTemplate;

@Autowired
private RedissonClient redissonClient;

/**
* 支付(高并发场景)
* 使用:唯一索引 + Redis预检查 + 分布式锁 + 状态机
*/
@Transactional
public PaymentResult pay(PaymentRequest request) {
// 1. 生成唯一支付ID
String paymentId = generatePaymentId(request);

// 2. Redis预检查(快速路径)
String cacheKey = "payment:processed:" + paymentId;
Boolean exists = redisTemplate.hasKey(cacheKey);
if (Boolean.TRUE.equals(exists)) {
PaymentRecord record = paymentRecordMapper.selectByPaymentId(paymentId);
return PaymentResult.success(record);
}

// 3. 数据库检查(去重表)
PaymentRecord existingRecord = paymentRecordMapper.selectByPaymentId(paymentId);
if (existingRecord != null) {
redisTemplate.opsForValue().set(cacheKey, "1", 1, TimeUnit.HOURS);
return PaymentResult.success(existingRecord);
}

// 4. 获取分布式锁(防止并发)
String lockKey = "payment:lock:" + request.getOrderId();
RLock lock = redissonClient.getLock(lockKey);

try {
if (!lock.tryLock(100, 30, TimeUnit.MILLISECONDS)) {
throw new BusinessException("系统繁忙,请稍后再试");
}

// 5. 双重检查
existingRecord = paymentRecordMapper.selectByPaymentId(paymentId);
if (existingRecord != null) {
redisTemplate.opsForValue().set(cacheKey, "1", 1, TimeUnit.HOURS);
return PaymentResult.success(existingRecord);
}

// 6. 检查订单状态(状态机)
Order order = orderMapper.selectById(request.getOrderId());
if (order.getStatus() != OrderStatus.PENDING) {
if (order.getStatus() == OrderStatus.PAID) {
return PaymentResult.success("订单已支付");
}
throw new BusinessException("订单状态不允许支付");
}

// 7. 执行支付
PaymentRecord record = doPayment(request, paymentId);

// 8. 更新订单状态(乐观锁)
int updated = orderMapper.updateStatusWithVersion(
request.getOrderId(),
OrderStatus.PENDING,
OrderStatus.PAID,
order.getVersion()
);

if (updated == 0) {
// 状态已变更,可能是并发支付
order = orderMapper.selectById(request.getOrderId());
if (order.getStatus() == OrderStatus.PAID) {
return PaymentResult.success("订单已支付");
}
throw new BusinessException("订单状态已变更");
}

// 9. 设置Redis缓存
redisTemplate.opsForValue().set(cacheKey, "1", 1, TimeUnit.HOURS);

return PaymentResult.success(record);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException("获取锁失败", e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}

4.2 案例2:消息队列幂等消费

4.2.1 场景

需求:Kafka消息消费,需要保证幂等性。

挑战

  • 消息可能重复投递
  • 高并发消费
  • 性能要求高

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@Component
public class OrderMessageConsumer {

@Autowired
private RedisTemplate<String, String> redisTemplate;

@Autowired
private MessageDedupMapper messageDedupMapper;

/**
* 消费订单消息(高性能幂等消费)
*/
@KafkaListener(topics = "order-created", groupId = "order-processor")
public void handleOrderCreated(ConsumerRecord<String, String> record) {
String messageId = record.key();
String message = record.value();

// 1. Redis预检查(快速路径,99%的重复消息在这里被拦截)
String cacheKey = "message:processed:" + messageId;
Boolean exists = redisTemplate.hasKey(cacheKey);
if (Boolean.TRUE.equals(exists)) {
return; // 已处理,直接返回
}

// 2. 去重表检查(数据库检查)
MessageDedup dedup = messageDedupMapper.selectByMessageId(messageId);
if (dedup != null) {
// 设置Redis缓存(防止后续重复检查数据库)
redisTemplate.opsForValue().set(cacheKey, "1", 1, TimeUnit.HOURS);
return;
}

// 3. 插入去重记录(唯一索引保证并发安全)
try {
dedup = new MessageDedup();
dedup.setMessageId(messageId);
dedup.setBusinessType("ORDER_CREATED");
dedup.setStatus(0); // 处理中
dedup.setCreateTime(LocalDateTime.now());
messageDedupMapper.insert(dedup);
} catch (DuplicateKeyException e) {
// 并发场景:其他线程已处理
redisTemplate.opsForValue().set(cacheKey, "1", 1, TimeUnit.HOURS);
return;
}

// 4. 处理消息
try {
Order order = JSON.parseObject(message, Order.class);
orderService.processOrder(order);

// 5. 更新处理状态
dedup.setStatus(1);
dedup.setUpdateTime(LocalDateTime.now());
messageDedupMapper.updateById(dedup);

// 6. 设置Redis缓存
redisTemplate.opsForValue().set(cacheKey, "1", 1, TimeUnit.HOURS);
} catch (Exception e) {
log.error("Process message failed: {}", messageId, e);

// 7. 更新处理状态为失败
dedup.setStatus(2);
dedup.setUpdateTime(LocalDateTime.now());
messageDedupMapper.updateById(dedup);

// 8. 可以选择重试或发送到死信队列
throw e;
}
}
}

5. 性能优化最佳实践

5.1 多级缓存策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Service
public class IdempotentService {

@Autowired
private RedisTemplate<String, String> redisTemplate;

@Autowired
private CaffeineCache localCache; // 本地缓存

/**
* 多级缓存检查(本地缓存 + Redis + 数据库)
*/
public boolean isProcessed(String businessId) {
// 1. 本地缓存检查(最快)
Boolean localResult = localCache.getIfPresent(businessId);
if (Boolean.TRUE.equals(localResult)) {
return true;
}

// 2. Redis检查(较快)
String cacheKey = "processed:" + businessId;
Boolean redisResult = redisTemplate.hasKey(cacheKey);
if (Boolean.TRUE.equals(redisResult)) {
// 回填本地缓存
localCache.put(businessId, true);
return true;
}

// 3. 数据库检查(最慢,但最准确)
boolean dbResult = checkInDatabase(businessId);
if (dbResult) {
// 回填缓存
redisTemplate.opsForValue().set(cacheKey, "1", 1, TimeUnit.HOURS);
localCache.put(businessId, true);
}

return dbResult;
}
}

5.2 批量处理优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Service
public class BatchIdempotentService {

/**
* 批量检查幂等性(减少数据库查询)
*/
public Map<String, Boolean> batchCheckProcessed(List<String> businessIds) {
// 1. 批量查询Redis
List<String> cacheKeys = businessIds.stream()
.map(id -> "processed:" + id)
.collect(Collectors.toList());

List<Boolean> redisResults = redisTemplate.hasKeys(cacheKeys);

// 2. 找出需要查询数据库的ID
List<String> needDbCheck = new ArrayList<>();
Map<String, Boolean> result = new HashMap<>();

for (int i = 0; i < businessIds.size(); i++) {
if (Boolean.TRUE.equals(redisResults.get(i))) {
result.put(businessIds.get(i), true);
} else {
needDbCheck.add(businessIds.get(i));
}
}

// 3. 批量查询数据库
if (!needDbCheck.isEmpty()) {
Map<String, Boolean> dbResults = batchCheckInDatabase(needDbCheck);
result.putAll(dbResults);

// 4. 回填Redis缓存
for (String id : needDbCheck) {
if (Boolean.TRUE.equals(dbResults.get(id))) {
redisTemplate.opsForValue().set("processed:" + id, "1", 1, TimeUnit.HOURS);
}
}
}

return result;
}
}

6. 总结

6.1 核心要点

  1. 幂等性实现方案:唯一索引、分布式锁、Token机制、状态机、去重表
  2. 高并发优化:Redis预检查、分段锁、批量处理、多级缓存
  3. 常见坑:唯一索引冲突处理、锁释放失败、Token未删除、状态机检查不完整、去重表未清理、双重检查失效
  4. 性能优化:多级缓存、批量处理、快速路径优化
  5. 最佳实践:根据场景选择合适的方案,组合使用多种方案

6.2 关键理解

  1. 幂等性不是性能的敌人:通过合理的优化,可以实现高性能的幂等性
  2. 组合使用:多种方案组合使用,取长补短
  3. 快速路径优化:大部分重复请求在快速路径被拦截
  4. 双重检查:加锁后的双重检查是必须的

6.3 最佳实践

  1. 高并发场景:Redis预检查 + 唯一索引 + 分布式锁
  2. 消息队列场景:去重表 + Redis缓存
  3. 支付场景:Token机制 + 状态机 + 分布式锁
  4. 性能优化:多级缓存、批量处理、快速路径
  5. 容错处理:所有方案都要考虑异常情况和并发场景

相关文章