Spring Boot多线程场景下如何实现员工任务分配的互斥控制?
问题描述
我正在开发一个Spring Boot Java应用,包含如下Employee实体:
Employee - Entity with following fields id status // valid values FREE,BUSY age
我开发了一个POST /doTask接口,用于将Employee的状态从FREE修改为BUSY。需求是员工完成任务需要一定时间,在此期间不能接受新任务,即同一员工同一时间只能处理一个任务。
请问当多线程同时调用POST /doTask接口时,如何确保只有一个请求能成功分配任务,其余请求均返回「员工正忙」的提示?
我了解可以使用@Transactional注解,但不确定是否能达到预期效果:仅一个请求能将状态更新为BUSY,其余请求能立即读取到该最新状态。
解决方案
1. 数据库层面锁(单实例场景最可靠)
悲观锁方案
直接在查询员工时加行锁,确保同一时间只有一个线程能获取该员工的修改权限,必须在事务内执行:
@Transactional public String assignTask(Long employeeId) { // 通过FOR UPDATE给目标员工行加锁 Employee employee = employeeRepository.findByIdForUpdate(employeeId); if (employee == null) { return "员工不存在"; } if (EmployeeStatus.BUSY.equals(employee.getStatus())) { return "员工正忙"; } employee.setStatus(EmployeeStatus.BUSY); employeeRepository.save(employee); // 异步执行任务逻辑,避免占用事务资源 taskExecutor.execute(() -> { try { // 模拟任务执行耗时 Thread.sleep(5000); // 任务完成后单独开事务更新状态为FREE updateEmployeeStatusToFree(employeeId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); return "任务分配成功"; }
对应JPA Repository方法:
@Query("SELECT e FROM Employee e WHERE e.id = :id FOR UPDATE") Employee findByIdForUpdate(@Param("id") Long id);
这种方式在数据库层面锁定行,能保证绝对的线程安全,适合并发量较高的场景。
乐观锁方案
给Employee实体添加版本号字段,利用JPA的乐观锁机制实现冲突检测:
@Entity public class Employee { @Id private Long id; private String status; private Integer age; @Version // 新增版本号字段,JPA自动维护 private Integer version; // getter/setter }
业务逻辑中通过版本冲突判断并发请求:
@Transactional public String assignTask(Long employeeId) { Employee employee = employeeRepository.findById(employeeId).orElse(null); if (employee == null) { return "员工不存在"; } if (EmployeeStatus.BUSY.equals(employee.getStatus())) { return "员工正忙"; } employee.setStatus(EmployeeStatus.BUSY); try { employeeRepository.save(employee); } catch (OptimisticLockingFailureException e) { // 版本冲突,说明已有其他线程修改了状态 return "员工正忙"; } // 异步执行任务逻辑 taskExecutor.execute(() -> { try { Thread.sleep(5000); updateEmployeeStatusToFree(employeeId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); return "任务分配成功"; }
乐观锁适合并发冲突较少的场景,不会占用数据库锁资源,但冲突发生时需要捕获异常并返回提示。
2. 分布式锁(多实例部署场景)
如果应用是多实例部署,数据库锁无法跨实例生效,需要使用分布式锁(以Redis为例):
@Autowired private StringRedisTemplate redisTemplate; public String assignTask(Long employeeId) { String lockKey = "employee:task:" + employeeId; // 尝试获取锁,超时时间设为任务最长执行时间+缓冲时间 Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", Duration.ofMinutes(10)); if (!Boolean.TRUE.equals(locked)) { return "员工正忙"; } try { // 必须结合数据库状态检查,避免锁过期后重复分配 Employee employee = employeeRepository.findById(employeeId).orElse(null); if (employee == null) { return "员工不存在"; } if (EmployeeStatus.BUSY.equals(employee.getStatus())) { return "员工正忙"; } employee.setStatus(EmployeeStatus.BUSY); employeeRepository.save(employee); // 异步执行任务 taskExecutor.execute(() -> { try { Thread.sleep(5000); updateEmployeeStatusToFree(employeeId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { // 任务完成后释放锁 redisTemplate.delete(lockKey); } }); return "任务分配成功"; } finally { // 同步执行任务时需在此释放锁,异步则在任务结束后释放 // 避免异常导致锁无法释放 if (!taskIsAsync) { redisTemplate.delete(lockKey); } } }
分布式锁需注意设置合理的过期时间,防止死锁,同时必须结合数据库状态检查,避免锁过期后出现重复分配。
关于@Transactional的说明
单独使用@Transactional无法解决并发问题:
- 默认事务隔离级别(如MySQL的REPEATABLE READ)会导致其他线程在当前事务提交前无法读取最新状态,可能出现多个线程都认为员工是
FREE的情况。 - 即使提高隔离级别到READ COMMITTED,也无法阻止多个线程同时查询到
FREE状态并尝试更新,最终可能出现多个请求修改成功的情况。
因此必须结合锁机制(数据库锁或分布式锁)才能保证并发安全。
内容的提问来源于stack exchange,提问作者sachin p
相关产品推荐
相关产品推荐

