在后台业务系统里,很多核心链路必须留出人工审核、复核或确认的环节。一旦把人工介入直接写进同步调用里,请求线程就会一直卡住,直到人点完按钮。人多的时候,线程池被占满,正常自动任务也进不来。异步通知与等待的思路是把“等人”这件事从主干移除,让流程先挂起,等人处理完再被通知唤醒,或者超时后走默认路径。

为什么同步等待人工必定拖垮系统
最直观的写法是在接口里调用一个审核方法,里面循环查询数据库里的审核状态,直到变成通过或拒绝。这种做法在测试环境只有一个用户时没问题,但上线后并发一来就暴露出致命缺陷。每个挂起的请求都占用一个容器线程,而人工审核可能要几分钟甚至几小时,线程长时间不能释放,连接池和线程池迅速耗尽。
除了资源占用,同步等待还让超时和兜底变得极难处理。如果用户在两天后才处理,当初的HTTP连接早断开,前端也不可能真等这么久。业务上又要求“超时自动拒绝”或“超时升级给主管”,这些逻辑塞进一个阻塞循环里既丑陋又容易出bug。本质问题是:人工响应时间和机器响应时间差了几个数量级,却用了同一套阻塞模型。
我们用一段典型的问题代码说明。下面这段Java伪代码把线程睡在循环里,生产环境绝对不能用。
// 错误示例:同步阻塞等待人工审核
public ReviewResult waitManualReview(long orderId) {
while (true) {
Status s = dao.queryStatus(orderId);
if (s == Status.PASS) {
return ReviewResult.PASS;
}
if (s == Status.REJECT) {
return ReviewResult.REJECT;
}
try {
Thread.sleep(5000); // 每5秒查一次,线程一直被占着
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
异步通知与等待的核心实现模式
可行的方案是把“任务”和“线程”解耦。流程执行到人工节点时,把当前上下文落地到数据库或Redis,标记状态为WAITING,然后直接返回给调用方一个受理号。人工在后台操作页面点击通过后,后端接口修改状态,并往消息队列发一条通知,或者触发一个回调接口。另一边,等待方可以是定时任务轮询,也可以是被消息唤醒的消费者。
如果用数据库状态加定时轮询,结构非常清晰。一张process_instance表有status、update_time字段。人工页面提交后更新status=PASS。一个每十秒跑一次的调度器扫出status=WAITING且超过超时时间的实例,按规则流转。没超时的继续等。这种办法缺点是有最多十秒延迟,但对多数业务够用,且不需要引入MQ。
更及时的作法是配合消息队列。人工操作接口里发一个事件到topic,消费者收到后根据业务ID把等待中的流程唤醒继续执行。下面用Python演示一个基于Redis键值与过期回调思路的简化版等待逻辑,真实项目里可把redis换成RabbitMQ。
import redis
import time
r = redis.Redis(host='127.0.0.1', port=6379, db=0)
def suspend_task(task_id, timeout=3600):
# 把任务设为等待,并设置过期时间作为超时兜底
r.set(f'task:{task_id}', 'WAITING', ex=timeout)
print(f'任务 {task_id} 已挂起,等待人工或超时')
def manual_finish(task_id, result):
# 人工处理完,写结果并删掉等待标记
r.set(f'result:{task_id}', result)
r.delete(f'task:{task_id}')
# 实际可在此发布消息通知消费者
print(f'人工已处理 {task_id},结果 {result}')
def wait_with_notify(task_id):
while True:
if not r.exists(f'task:{task_id}'):
val = r.get(f'result:{task_id}')
if val:
return val.decode()
else:
return 'TIMEOUT_DEFAULT' # 超时后被清理,走默认
time.sleep(2)
超时兜底与状态一致性保障
人工介入最怕的是“没人管”。所以等待机制必须带超时。超时后系统不能 silently 卡死,而要自动流转到拒绝、升级或告警。上面Redis的ex参数就是最简单兜底:键消失即代表超时。数据库轮询方案则在SQL里用update_time < now()-3600来捞超时记录。
另一个关键是状态一致性。人工页面和等待方可能同时操作,要用乐观锁或数据库事务防止重复流转。比如在把WAITING改成PASS时,用update ... where status='WAITING'并判断影响行数,行数为0说明已被别人处理。消息通知可能因为网络重发,消费者要幂等,根据task_id先查是否已结束。
下表列出两种常见实现的取舍,方便按团队基础设施选择。
| 方案 | 实时性 | 复杂度 | 适用场景 |
|---|---|---|---|
| 数据库轮询 | 秒级延迟 | 低 | 中小流量,无MQ |
| 消息队列通知 | 毫秒级 | 中 | 高并发,要求及时唤醒 |
最后提醒,人工介入节点要在前端明确展示“已提交,等待审核”,不要让用户重复点。后端用异步通知解耦后,整体吞吐量能提升几十倍,因为机器线程不再被人类节奏拖死。