Java唯一索引幂等案例怎么用

wen java案例 31

本文目录导读:

Java唯一索引幂等案例怎么用

  1. 核心原理
  2. 方案一:捕获重复插入异常(推荐,性能好)
  3. 方案二:先查询后插入(简单易懂,能容忍重复查询)
  4. 方案三:使用状态机 + 幂等键
  5. 最佳实践建议
  6. 简单示例代码片段(纯JDBC风格)

这是一个关于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;
}

适用场景:业务表本身的主键或某个字段就是具有业务含义的幂等键(如订单号、支付流水号)。

最佳实践建议

  1. 明确幂等键:选择一个在业务上能唯一标识一次请求的字段(如订单号、全局ID、用户ID+时间戳等)。
  2. 优先使用方案一:对于大多数高并发场景,直接插入并捕获DuplicateKeyException是最高效且可靠的方式。
  3. 事务保障:幂等操作务必要在事务中执行,确保幂等记录与业务数据的一致性。
  4. 异常处理:务必捕获DuplicateKeyExceptionDataIntegrityViolationException,不要捕获过于宽泛的Exception,以免掩盖真实错误。
  5. 性能考虑:不要在大批量、非幂等的查询中使用此模式,幂等设计通常只用于写操作(Insert/Update)。
  6. 配合锁:如果业务场景对状态要求极高(比如不能重复扣款),可以结合分布式锁(如 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 中利用唯一索引实现幂等性的方法,核心就是让数据库本身来保证同一唯一键只能被插入一次

抱歉,评论功能暂时关闭!