如何实现队列系统中按部门线程安全的取号功能?
解决方案
一、数据库层面(推荐,更可靠)
应用层锁在分布式部署场景下会失效,数据库层面的行级锁能从根源避免重复取号问题,同时保证不同部门的并行性。
1. 优化查询逻辑,使用行级锁
修改getNextTurnForUser的底层查询,在获取下一个号时直接锁定对应的行,且仅锁定当前用户所属部门的未调用号。PostgreSQL支持SELECT ... FOR UPDATE SKIP LOCKED语法,能跳过已被锁定的行,让同部门的其他请求直接取下一个可用号,无需等待:
SELECT * FROM daily_turns WHERE turn_status_id = ? -- 未被调用的状态值 AND department_id = (SELECT department_id FROM users WHERE user_id = ?) -- 关联用户所属部门 ORDER BY turn_id ASC LIMIT 1 FOR UPDATE SKIP LOCKED;
FOR UPDATE:锁定选中的行,防止其他事务修改SKIP LOCKED:让后续请求跳过已被锁定的行,直接获取下一个可用号,提升并发效率
后续的更新操作(save)只会处理拿到行锁的记录,同部门请求自动串行处理,不同部门因为查询的是不同部门的行,完全不会互相阻塞。
2. 原子化更新操作
把“取号+更新”合并成一个原子SQL,彻底消除中间状态的并发问题。PostgreSQL的UPDATE ... RETURNING语法可以直接完成这个操作:
UPDATE daily_turns SET turn_status_id = ?, event_date = ?, user_id = ? WHERE turn_id = ( SELECT turn_id FROM daily_turns WHERE turn_status_id = ? AND department_id = (SELECT department_id FROM users WHERE user_id = ?) ORDER BY turn_id ASC LIMIT 1 FOR UPDATE SKIP LOCKED ) RETURNING *;
整个操作由数据库保证原子性,不需要应用层处理并发逻辑,同一部门的请求自动串行,不同部门完全并行。
二、Java应用层方案(仅适用于单节点部署)
如果是单节点应用,可以通过部门ID映射锁对象,实现按部门的同步控制。
1. 定义部门锁容器
用ConcurrentHashMap存储每个部门对应的锁对象,确保每个部门只有一个锁:
private static final ConcurrentHashMap<Integer, Object> DEPARTMENT_LOCKS = new ConcurrentHashMap<>(); // 获取部门锁的工具方法 private Object getDepartmentLock(int departmentId) { return DEPARTMENT_LOCKS.computeIfAbsent(departmentId, k -> new Object()); }
2. 修改取号方法,按部门加锁
先通过userId获取对应部门ID,再用该部门的锁对象做同步:
private DailyTurns callTurnLocal(int userId) { // 先获取用户所属部门ID,假设你有对应的查询方法 int departmentId = getDepartmentIdByUserId(userId); Object lock = getDepartmentLock(departmentId); synchronized (lock) { // 同部门共用一个锁,不同部门用不同锁,实现并行 try { DailyTurns turn = null; DailyTurns updateTurn = null; turn = getNextTurnForUser(userId); if (turn != null) { updateTurn = turn; updateTurn.setTurnStatusId(TURN_STATUS_CALLED); updateTurn.setEventDate(new Date()); updateTurn.setUserId(userId); updateTurn = save(updateTurn); } return updateTurn; } catch (Exception e) { logger.error( "Exception: " + e.getMessage(), e ); return null; } } }
注意:该方案仅适合单节点应用,多节点部署时不同节点的锁对象独立,会出现跨节点的同部门并发问题。另外要定期清理废弃部门的锁对象,避免内存泄漏。
总结
- 分布式部署:优先用数据库层面的
SELECT ... FOR UPDATE SKIP LOCKED或UPDATE ... RETURNING方案,可靠且能保证不同部门的并行性。 - 单节点部署:可以用应用层部门锁方案,实现简单,但要注意锁对象的内存管理。
内容的提问来源于stack exchange,提问作者camaleongdl
相关产品推荐
相关产品推荐

