基于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中利用事务+异常重试来完成订单插入,逻辑如下:
- 先填充订单的基础信息(不需要锁或事务)
- 生成一个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
相关产品推荐
相关产品推荐

