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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 02:42:52