Activiti TaskService并发执行任务完成操作失败问题求助
这种并发任务遇到的数据库锁问题我太熟悉了——不少开发者都踩过这个坑!你推测的方向完全正确:单独执行时没有资源竞争,数据库锁不会触发冲突,但并发场景下,第二个任务大概率被第一个任务持有的锁阻塞,只有刚好赶上锁释放的极小窗口才会成功。下面给你梳理几种靠谱的解决方案,附上示例代码:
核心原因复盘
本质是竞态条件:多个线程/进程同时操作同一份数据库资源(比如同一条任务记录),第一个任务先获取锁并更新数据,第二个任务尝试操作时要么被锁阻塞超时,要么拿到数据后发现已经被修改,导致更新失败。只有当第一个任务刚好在第二个任务发起操作前释放锁,才会出现“偶尔成功”的情况。
常见解决方案及示例代码
1. 乐观锁:适合高并发、冲突率低的场景
乐观锁基于“假设冲突很少发生”的思路,通过版本号或时间戳来控制更新,只有当版本匹配时才允许修改,冲突时重试即可。
示例(Java + MyBatis)
首先给任务表加一个version字段(初始值为0,每次更新自增):
ALTER TABLE task ADD COLUMN version INT DEFAULT 0;
Mapper层的更新语句,带版本校验:
@Update("UPDATE task SET status = #{status}, version = version + 1 WHERE id = #{id} AND version = #{version}") int updateTaskWithOptimisticLock(Task task);
业务层处理重试逻辑:
public boolean executeTask(Long taskId) { int retryTimes = 3; // 根据业务调整重试次数 while (retryTimes > 0) { // 先查询最新的任务数据和版本号 Task task = taskMapper.selectById(taskId); if (task == null) { return false; } // 执行任务的业务逻辑(比如调用接口、处理数据) task.setStatus(TaskStatus.COMPLETED); // 尝试更新,只有版本匹配才会生效 int affectedRows = taskMapper.updateTaskWithOptimisticLock(task); if (affectedRows > 0) { return true; // 更新成功,任务执行完成 } retryTimes--; // 重试前短暂休眠,避免高频重试压垮数据库 try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } } return false; // 多次重试仍失败,可记录告警 }
2. 悲观锁:适合冲突率高的场景
悲观锁会在查询数据时就直接加锁,确保后续的更新操作不会被其他线程干扰,适合任务冲突频繁的场景。
示例(Java + MyBatis)
Mapper层查询时加行锁(必须在事务中执行):
@Select("SELECT * FROM task WHERE id = #{id} FOR UPDATE") Task selectTaskForUpdate(Long id);
业务层开启事务并执行任务:
@Transactional(rollbackFor = Exception.class) public boolean executeTask(Long taskId) { // 查询时获取行锁,其他线程查询同一任务会被阻塞 Task task = taskMapper.selectTaskForUpdate(taskId); if (task == null) { return false; } // 执行任务逻辑 task.setStatus(TaskStatus.COMPLETED); taskMapper.updateById(task); return true; }
注意:使用悲观锁时要控制事务时长,避免长时间持有锁导致其他任务阻塞;同时确保数据库引擎支持行锁(比如MySQL的InnoDB,MyISAM不支持行锁)。
3. 分布式锁:多实例部署的必选方案
如果你的服务是多实例集群部署,数据库锁只能在单个实例内生效,这时候需要用分布式锁(比如Redis、ZooKeeper)来跨实例控制并发。
示例(Java + Redisson)
用Redisson实现可重入的分布式锁:
@Autowired private RedissonClient redissonClient; public boolean executeTask(Long taskId) { String lockKey = "task:execution:lock:" + taskId; RLock lock = redissonClient.getLock(lockKey); try { // 尝试获取锁:最多等待5秒,持有锁10秒(根据任务实际耗时调整) if (lock.tryLock(5, 10, TimeUnit.SECONDS)) { // 执行任务逻辑(包括数据库操作) Task task = taskMapper.selectById(taskId); if (task == null) { return false; } task.setStatus(TaskStatus.COMPLETED); taskMapper.updateById(task); return true; } else { // 获取锁失败,说明其他实例正在执行该任务 return false; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } finally { // 确保锁被正确释放 if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }
额外注意事项
- 任务幂等性:无论用哪种锁机制,都要保证任务执行多次和一次的结果一致(比如用唯一业务ID做幂等校验),避免锁失效时出现重复执行的问题。
- 数据库隔离级别:建议使用
READ COMMITTED级别,避免过高隔离级别(比如REPEATABLE READ)导致的锁升级或幻读问题。 - 监控告警:对并发任务的失败情况做监控,当重试失败次数过多时及时告警,排查锁冲突的根源。
内容的提问来源于stack exchange,提问作者ubreddy
相关产品推荐
相关产品推荐

