You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Servlet与JDBC的配送服务项目中,如何实现订单创建时轨道号生成及入库操作的原子性?

嘿,这个并发场景下的轨道号生成问题确实是订单系统里的典型痛点,我来给你梳理几个靠谱的解决方案,从最推荐的数据库层面方案到应用层锁的优化,帮你搞定原子性问题:

推荐方案:数据库层面的原子性保障(最可靠+高性能)

其实你不需要在应用层先查询所有活跃轨道号再生成,把这个原子性的问题交给数据库来处理会更靠谱——毕竟数据库天生就是为解决并发数据一致性设计的。核心思路是利用部分唯一索引,让数据库自动保证「同一时间不会有两个NEW/PENDING状态的订单使用相同轨道号」,然后应用层只需要处理冲突重试即可。

第一步:创建部分唯一索引

针对你的订单表,创建一个仅作用于NEW/PENDING状态的唯一索引(以PostgreSQL为例,MySQL 8.0+等主流数据库也支持类似的部分索引):

CREATE UNIQUE INDEX idx_active_track_number ON orders (track_number) 
WHERE status IN ('NEW', 'PENDING');

这个索引会强制要求:只要订单状态是NEW或PENDING,它的track_number必须唯一;而COMPLETED状态的订单不受限制,自然就能复用轨道号。

第二步:JDBC代码实现

在JDBC中利用事务+异常重试来完成订单插入,逻辑如下:

  1. 先填充订单的基础信息(不需要锁或事务)
  2. 生成一个4位随机轨道号
  3. 在事务中尝试插入订单,若因为唯一索引冲突失败,就重新生成轨道号再试
  4. 插入成功后发送确认邮件

代码示例:

public void createOrder(Order order) throws SQLException {
    // 1. 先填充订单的非轨道号信息(比如客户ID、配送地址等)
    fillOrderBasicInfo(order);

    try (Connection conn = getDbConnection()) {
        conn.setAutoCommit(false); // 开启事务
        String insertSql = "INSERT INTO orders (track_number, status, date) VALUES (?, ?, ?)";

        while (true) {
            // 2. 生成随机4位字母数字混合的轨道号
            String trackNumber = generateTrackNumber();
            order.setTrackNumber(trackNumber);

            try (PreparedStatement pstmt = conn.prepareStatement(insertSql)) {
                pstmt.setString(1, trackNumber);
                pstmt.setString(2, order.getStatus());
                pstmt.setDate(3, order.getDate());

                int rowsAffected = pstmt.executeUpdate();
                if (rowsAffected == 1) {
                    conn.commit(); // 提交事务
                    sendConfirmationEmail(order); // 发送确认邮件
                    return; // 成功完成,退出循环
                }
            } catch (SQLIntegrityConstraintViolationException e) {
                // 捕获唯一索引冲突异常,回滚当前事务(未提交的事务回滚无影响)
                conn.rollback();
                // 继续循环,生成新的轨道号重试
            }
        }
    }
}

// 生成4位字母数字混合轨道号的示例方法
private String generateTrackNumber() {
    String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    StringBuilder sb = new StringBuilder(4);
    Random random = new Random();
    for (int i = 0; i < 4; i++) {
        sb.append(chars.charAt(random.nextInt(chars.length())));
    }
    return sb.toString();
}

为什么这个方案好?

  • 原子性绝对保障:数据库的唯一约束是原子级别的,不管多少并发请求,都不会出现重复轨道号的活跃订单
  • 并发性能优异:不需要锁住整个操作流程,只有当轨道号真的冲突时才会重试,大部分请求一次就能成功
  • 分布式友好:即使以后扩展到多个应用节点,数据库的索引约束依然生效,不会出现跨节点的并发问题
  • 代码更简洁:省去了查询所有活跃轨道号的步骤,减少数据库查询开销
备选方案:应用层锁的优化(适合单节点低并发)

如果你坚持要用ReentrantLock,那一定要注意锁的粒度,尽量缩短锁的持有时间,避免因为数据库操作耗时导致的并发阻塞:

private final ReentrantLock trackLock = new ReentrantLock(true); // 公平锁避免线程饥饿

public void createOrder(Order order) throws SQLException {
    // 先填充订单基础信息,这部分不需要锁
    fillOrderBasicInfo(order);

    String trackNumber = null;
    // 只锁住轨道号生成+校验的核心部分
    trackLock.lock();
    try {
        // 查询当前活跃的轨道号
        Set<String> activeTracks = getActiveTracks();
        // 生成不重复的轨道号
        do {
            trackNumber = generateTrackNumber();
        } while (activeTracks.contains(trackNumber));
    } finally {
        trackLock.unlock(); // 尽快释放锁,减少阻塞时间
    }

    // 释放锁后再执行数据库插入和邮件发送(这两步不影响原子性)
    order.setTrackNumber(trackNumber);
    storeOrder(order);
    sendConfirmationEmail(order);
}

// 查询活跃轨道号的DAO方法
private Set<String> getActiveTracks() throws SQLException {
    Set<String> tracks = new HashSet<>();
    String sql = "SELECT track_number FROM orders WHERE status IN ('NEW', 'PENDING')";
    try (Connection conn = getDbConnection();
         PreparedStatement pstmt = conn.prepareStatement(sql);
         ResultSet rs = pstmt.executeQuery()) {
        while (rs.next()) {
            tracks.add(rs.getString("track_number"));
        }
    }
    return tracks;
}

这个方案的局限性:

  • 单节点限制:ReentrantLock是进程内的锁,如果以后部署多个应用节点,锁无法跨节点生效,依然会出现并发问题
  • 并发性能一般:如果查询活跃轨道号的耗时较长,或者生成轨道号的循环次数多,锁会被长时间持有,阻塞其他请求
  • 极小风险:从释放锁到插入订单的间隙,理论上可能有其他节点(如果有的话)生成相同轨道号,不过单节点下不会有这个问题
额外思路:改进轨道号生成逻辑

如果想进一步减少冲突概率,可以调整轨道号的生成规则:

  • 结合时间戳:比如取当前时间的后几位数字+随机字母,减少重复概率
  • 利用数据库序列:把BIGSERIAL的ID转换成base36格式(字母+数字),取最后4位,这样轨道号和订单ID绑定,只要活跃订单数不超过36^4(约168万),就不会重复

不过这个思路需要结合你的业务订单量来评估,毕竟你允许COMPLETED的轨道号复用,所以纯随机的方式其实已经足够了。


内容的提问来源于stack exchange,提问作者jimmayhem

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 22:19:05