本文目录导读:

这是一个关于Java唯一索引实现幂等性的经典案例,在分布式系统或高并发场景中,防止重复请求对数据造成污染(如重复下单、重复支付)极为关键,利用数据库的唯一索引,结合先查后插或直接插入捕获异常的策略,是实现幂等性有效且简洁的方式。
下面我会为你提供一个完整的、包含代码示例的案例。
核心原理
利用数据库表的某个或某几个字段的唯一约束(唯一索引),当业务上认为请求已经处理过(比如相同的订单号、相同的业务流水号)时,数据库会因为唯一键冲突而插入失败。
捕获重复插入异常(推荐,性能好)
思路:不查询,直接尝试插入“幂等记录表”,如果插入成功,则执行业务逻辑;如果插入失败并抛出DuplicateKeyException(或类似的数据库唯一约束冲突异常),则说明该请求已经处理过,返回“重复请求”或已存在的结果。
适用场景:业务逻辑允许直接插入,且对性能要求较高(减少了一次数据库查询)。
数据库表设计(幂等表)
CREATE TABLE `idempotent_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `idempotent_key` varchar(255) NOT NULL COMMENT '幂等键,订单号_操作类型', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0-处理中,1-成功', `result` text COMMENT '业务结果', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_idempotent_key` (`idempotent_key`) -- 关键:唯一索引 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Java 代码实现(Spring Boot + MyBatis)
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
@Autowired
private IdempotentRecordMapper idempotentRecordMapper;
@Autowired
private OrderMapper orderMapper;
/**
* 创建订单,保证幂等
* @param orderNo 订单号(幂等键的一部分)
* @param userId 用户ID
* @return 是否是新创建的订单
*/
@Transactional(rollbackFor = Exception.class)
public boolean createOrder(String orderNo, Long userId) {
String idempotentKey = "CREATE_ORDER_" + orderNo; // 组合幂等键
// 1. 尝试插入幂等记录
IdempotentRecord record = new IdempotentRecord();
record.setIdempotentKey(idempotentKey);
record.setStatus(0); // 处理中
try {
idempotentRecordMapper.insertSelective(record);
} catch (DuplicateKeyException e) {
// 2. 唯一索引冲突,说明请求已处理过,直接返回false(或已存在的结果)
log.warn("幂等记录已存在,请求重复, key: {}", idempotentKey);
return false;
}
// 3. 执行业务逻辑 (扣库存、创建订单等)
// 注意:这里假设业务逻辑本身也是幂等的,但通常我们依赖幂等表来保证
Order order = new Order();
order.setOrderNo(orderNo);
order.setUserId(userId);
order.setStatus(1);
orderMapper.insertSelective(order);
// 4. 更新幂等记录状态为成功
idempotentRecordMapper.updateStatusByKey(idempotentKey, 1);
return true;
}
}
关键点说明
DuplicateKeyException:Spring 对数据库唯一约束冲突的封装,如果你使用 MyBatis,Mapper 插入时遇到唯一索引冲突会抛出org.springframework.dao.DuplicateKeyException(或DataIntegrityViolationException)。- 事务性:
@Transactional确保幂等记录插入和业务逻辑在同一个事务中,如果业务逻辑失败,幂等记录会自动回滚,下次同一请求可以重新执行(除非你希望记录失败状态)。 - 性能:相比“先查询再插入”,这种方案在高并发下性能更好,因为它减少了数据库的一次查询在成功路径上的开销,并且唯一索引的检查非常快。
先查询后插入(简单易懂,能容忍重复查询)
思路:在执行关键业务前,先查询幂等记录是否存在,如果存在,则直接返回;如果不存在,则执行业务并插入幂等记录。
适用场景:对并发要求不是极高,或者业务本身对状态检查有明确需求时。
代码示例
@Transactional(rollbackFor = Exception.class)
public boolean createOrderV2(String orderNo, Long userId) {
String idempotentKey = "CREATE_ORDER_" + orderNo;
// 1. 先查询幂等记录
IdempotentRecord existingRecord = idempotentRecordMapper.selectByKey(idempotentKey);
if (existingRecord != null) {
log.warn("请求重复, key: {}", idempotentKey);
return false; // 或返回已有的业务结果
}
// 2. 插入幂等记录(利用唯一索引做双重保险)
IdempotentRecord record = new IdempotentRecord();
record.setIdempotentKey(idempotentKey);
record.setStatus(0);
try {
idempotentRecordMapper.insertSelective(record);
} catch (DuplicateKeyException e) {
// 高并发下,两个线程同时查询都不存在,但插入时依然可能冲突
log.warn("高并发下幂等插入冲突, key: {}", idempotentKey);
return false;
}
// 3. 业务逻辑...
// ...
return true;
}
方案二的优缺点
- 优点:逻辑清晰,适合业务上需要先检查状态的场景。
- 缺点:多了一次数据库查询,在高并发下存在“查询-检查-插入”的时间窗口,所以还需要配合
catch来兜底,代码变得冗余。
使用状态机 + 幂等键
如果业务表本身就有唯一索引(例如订单表的order_no字段),且业务状态有流转,可以直接利用业务表自己的唯一索引,无需再建单独的幂等表。
案例:订单表create_order(order_no, user_id, status),order_no有唯一约束,创建订单时,直接插入订单数据,捕获DuplicateKeyException。
public boolean createOrder(String orderNo, Long userId) {
Order order = new Order();
order.setOrderNo(orderNo);
order.setUserId(userId);
order.setStatus(OrderStatus.INIT); // 初始状态
try {
orderMapper.insertSelective(order);
} catch (DuplicateKeyException e) {
// 重复订单号,幂等处理
log.warn("订单号重复,幂等处理: {}", orderNo);
return false;
}
// 后续业务...
return true;
}
适用场景:业务表本身的主键或某个字段就是具有业务含义的幂等键(如订单号、支付流水号)。
最佳实践建议
- 明确幂等键:选择一个在业务上能唯一标识一次请求的字段(如订单号、全局ID、用户ID+时间戳等)。
- 优先使用方案一:对于大多数高并发场景,直接插入并捕获
DuplicateKeyException是最高效且可靠的方式。 - 事务保障:幂等操作务必要在事务中执行,确保幂等记录与业务数据的一致性。
- 异常处理:务必捕获
DuplicateKeyException或DataIntegrityViolationException,不要捕获过于宽泛的Exception,以免掩盖真实错误。 - 性能考虑:不要在大批量、非幂等的查询中使用此模式,幂等设计通常只用于写操作(Insert/Update)。
- 配合锁:如果业务场景对状态要求极高(比如不能重复扣款),可以结合分布式锁(如 Redis)做第一道防线,数据库唯一索引做最终防线(兜底)。
简单示例代码片段(纯JDBC风格)
public boolean idempotentInsert(Connection connection, String key) throws SQLException {
String sql = "INSERT INTO idempotent_record(idempotent_key, status) VALUES (?, 0)";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, key);
stmt.executeUpdate();
return true; // 插入成功,幂等
} catch (SQLException e) {
// 根据数据库驱动判断是唯一约束冲突
if (e.getErrorCode() == 1062) { // MySQL 唯一约束冲突错误码
return false; // 重复请求
}
throw e; // 其他错误再向上抛
}
}
通过这个案例,你应该能够理解并应用 Java 中利用唯一索引实现幂等性的方法,核心就是让数据库本身来保证同一唯一键只能被插入一次。