activiti需加锁是因为多线程并发操作流程实例或任务时引发数据不一致,根源在于共享数据库资源竞争而非线程池本身;应以流程实例id为粒度使用redis分布式锁等方案,在业务逻辑块内精准加锁。

为什么需要加锁?根源不在线程池,而在共享资源竞争
Activiti 的流程状态(如 `ACT_RU_EXECUTION`、`ACT_RU_TASK` 表)由数据库持久化,多线程并发调用 `runtimeService.signal()`、`taskService.complete()` 或 `managementService.executeJob()` 时,若未协调好执行顺序,会出现:
- 同一任务被多次完成(乐观锁失败抛 `OptimisticLockingException`)
- 流程变量覆盖(后写入的值覆盖先写入的,无原子性)
- 异步任务重复触发(如两个线程同时查询到待执行的 async job 并争抢执行)
Java 线程池(如 `ThreadPoolExecutor`)只是执行载体,它放大了并发冲突,但锁的对象不是线程池本身,而是流程实例 ID(`processInstanceId`)或任务 ID(`taskId`)所代表的业务逻辑单元。
推荐加锁策略:以流程实例为粒度的分布式可重入锁
单机场景可用 `ConcurrentHashMap
- 数据库行锁:对 `ACT_RU_EXECUTION` 表中对应 `ID_ = ?` 的记录执行 `SELECT ... FOR UPDATE`(需确保事务隔离级别为 REPEATABLE READ 或更高,且该 SQL 在同一事务中早于状态变更操作)
-
Redis 锁(推荐):使用 `SET key value NX PX 30000` + Lua 脚本保证原子性释放,key 建议为
"activiti:lock:proc:" + processInstanceId,value 为唯一请求标识(如 UUID),超时时间略大于最长预期处理耗时 - 避免锁整个线程池:不要对 `ExecutorService` 加锁,那会串行化所有异步任务,彻底失去并发意义
在 Activiti 监听器或委托类中安全嵌入锁逻辑
典型位置是 `ExecutionListener`(onStart/onEnd)或 `JavaDelegate` 中。示例(Redis + Lettuce):
// 在 JavaDelegate#execute() 开头加锁
String lockKey = "activiti:lock:proc:" + execution.getProcessInstanceId();
String lockValue = UUID.randomUUID().toString();
if (!redisLock.tryLock(lockKey, lockValue, 30, TimeUnit.SECONDS)) {
throw new RuntimeException("获取流程锁失败,可能正在被其他线程处理");
}
try {
// ✅ 此处执行变量设置、外部系统调用、条件判断等敏感操作
runtimeService.setVariable(execution.getProcessInstanceId(), "result", calculate());
} finally {
redisLock.unlock(lockKey, lockValue); // 必须在 finally 中释放
}
注意:锁范围应严格限定在真正需要互斥的业务代码块内,避免包裹 `taskService.complete()` 等 Activiti 内部已带乐观锁的操作,否则反而增加死锁风险。
比加锁更优的替代方案:利用 Activiti 自身机制降并发
很多场景其实无需手动加锁,可通过配置规避竞争:
- 将关键节点设为
async="false",强制流程引擎同步执行,由事务保证一致性 - 启用
jobExecutorActivate=false,改用定时任务+主动拉取方式执行异步作业,配合数据库 `SELECT FOR UPDATE` 控制消费节奏 - 对高频触发事件(如多次回调)设计幂等性:用流程变量记录已处理状态,`if (!"done".equals(runtimeService.getVariable(procId, "callbackStatus"))) { ... }`
手动加锁是兜底手段,优先通过流程建模和引擎配置收敛并发点,更轻量、更可靠。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











