
在多用户并发场景下,为避免数据被意外覆盖(如重复抢占同一座位),应优先采用带条件的 sql `update` 语句,并通过影响行数判断操作是否成功,而非先查后改——这能从根本上消除竞态条件,是数据库层面最可靠、最常用的安全更新方案。
在典型的资源独占类业务(如影院选座、会议室预约、工单认领)中,“检查并更新”(check-then-act)逻辑极易引发并发问题。你提出的两种方式看似等价,实则存在本质差异:
✅ 推荐方式:原子化 SQL 条件更新(首选)
直接在数据库层完成“仅当座位空闲时才分配”的判断与写入,利用数据库事务的原子性与行级锁保障一致性:
UPDATE ticket SET user_user_id = ? WHERE place = ? AND user_user_id IS NULL;
关键在于:执行后必须校验 UPDATE 影响的行数。JDBC 中可通过 PreparedStatement.executeUpdate() 返回值获取:
public void assignTicket(String place, Long userId) throws DAOException {
String sql = "UPDATE ticket SET user_user_id = ? WHERE place = ? AND user_user_id IS NULL";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setLong(1, userId);
ps.setString(2, place);
int affectedRows = ps.executeUpdate();
if (affectedRows == 0) {
throw new DAOException("Place '" + place + "' has already been taken");
}
} catch (SQLException e) {
throw new DAOException("Failed to assign ticket", e);
}
}⚠️ 不推荐方式:应用层先查后改(存在竞态风险)
你示例中的 Service 层逻辑看似清晰,但在高并发下存在经典的时间窗口漏洞:
线程 A 查询 place='A1' → 返回 user_user_id = NULL 线程 B 同时查询 place='A1' → 同样返回 NULL A 执行 UPDATE 占用座位 B 也执行 UPDATE —— 成功覆盖 A 的结果!
即使加了 synchronized 或分布式锁,也会严重损害吞吐量和可扩展性;而数据库原生的 WHERE 条件更新天然具备行锁(InnoDB 下会为匹配的记录加 Next-Key Lock),既安全又高效。
? 进阶建议
立即学习“Java免费学习笔记(深入)”;
- 若需返回更新后的完整记录(如生成含用户信息的票据),可配合 SELECT ... FOR UPDATE 在事务中显式加锁,但务必缩短事务范围;
- 对于更高并发场景,可结合乐观锁(如版本号字段 version)或分布式锁(如 Redis + Lua)进一步增强健壮性,但基础安全仍应由数据库条件更新兜底;
- 始终将并发控制逻辑下沉至 DAO 层,Service 层专注业务编排,避免将数据一致性责任推给上层。
总结:安全更新的核心不是“先确认再行动”,而是“行动即确认”——让数据库用一条原子 SQL 完成判断与变更,再由应用校验结果。这是工业级 Java 应用处理共享状态更新的黄金实践。










