3、高性能审批引擎:异步处理架构、消息队列集成、缓存策略、数据库读写分离优化
说实话,很多OA系统到了后期,最大的痛点就是审批慢。
用户点一下「提交」,转圈转半天。领导批一个流程,页面卡住不动。我见过最夸张的一个项目,高峰期审批单积压了上万条,数据库直接被打挂了。
为什么会这样?
因为传统的审批引擎是同步的。每一步都要写库、查库、通知、再写库。所有操作串在一起,一个环节慢了,整个链条就堵死了。
今天我们就来聊聊,怎么把审批引擎改造成高性能的异步架构。
3.1 异步处理架构:别让用户等你
我个人的习惯是,把审批流程拆成两段:
- 前端响应:用户提交后,立刻返回「提交成功」
- 后端处理:真正的审批逻辑、状态变更、通知推送,全部异步执行
说白了,用户只关心「我提交了没」,不关心「数据库写完了没」。你想想看,淘宝下单也是先告诉你「下单成功」,然后后台慢慢扣库存、发货。审批引擎也是一样的道理。
核心思路:将审批流程中的「写操作」与「读操作」分离。写操作异步化,读操作缓存化。
我在项目中遇到过这样一个场景:一个集团公司的请假审批,每次提交都要同时更新考勤表、工资表、排班表。如果同步执行,一次提交要等3秒。改成异步后,用户0.1秒就看到了「提交成功」,剩下的2.9秒后台慢慢跑。用户体验瞬间提升。
3.2 消息队列集成:解耦才是王道
异步处理的核心,就是消息队列。我个人偏爱RabbitMQ和RocketMQ,小项目用Redis的List也能凑合。
消息队列在审批引擎里主要干三件事:
- 任务分发:审批单提交后,扔到队列里,消费者慢慢处理
- 状态同步:审批节点变更时,发消息通知下游系统(比如ERP、HR系统)
- 失败重试:处理失败的消息,自动重试,不用人工干预
来看一个简单的消息生产者代码示例:
// 审批单提交后,发送消息到队列
public void submitApproval(ApprovalForm form) {
// 1. 先保存到本地表,状态设为「处理中」
approvalDao.save(form, Status.PENDING);
// 2. 发送消息到队列
Message message = new Message();
message.setTopic("APPROVAL_SUBMIT");
message.setBody(JSON.toJSONString(form));
message.setDelayLevel(0); // 不延迟,立即消费
messageQueueProducer.send(message);
// 3. 直接返回用户「提交成功」
return Result.success("提交成功,正在处理");
}
消费者那边就更简单了:
// 消息消费者
@MessageListener(topic = "APPROVAL_SUBMIT")
public void handleApprovalSubmit(Message message) {
ApprovalForm form = JSON.parseObject(message.getBody(), ApprovalForm.class);
try {
// 执行真正的审批逻辑
approvalService.process(form);
// 处理成功,更新状态
approvalDao.updateStatus(form.getId(), Status.SUCCESS);
} catch (Exception e) {
// 处理失败,记录日志,消息自动重试
log.error("审批处理失败,formId: {}", form.getId(), e);
// 如果重试次数超过3次,进入死信队列
if (message.getRetryCount() > 3) {
deadLetterQueue.send(message);
}
}
}
避坑指南:我曾经犯过一个错误,把消息队列的消费逻辑写得太重。一个消息里包含了审批、通知、日志、统计四个操作。结果某个环节挂了,整个消息都要重试。后来我拆成了四个独立队列,每个队列只干一件事。这样即使通知队列挂了,审批队列还能正常跑。
3.3 缓存策略:别让数据库扛所有压力
审批引擎里,读操作远多于写操作。尤其是「待办列表」、「已办列表」、「流程详情」这些接口,每秒可能被刷几百次。
我建议的缓存策略是分层缓存:
| 缓存层级 | 存储内容 | 过期时间 | 命中率 |
|---|---|---|---|
| L1 本地缓存(Caffeine) | 用户待办列表、常用审批模板 | 30秒 | 60% |
| L2 分布式缓存(Redis) | 审批单详情、流程定义、组织架构 | 5分钟 | 30% |
| L3 数据库 | 全量数据 | 永久 | 10% |
你想想看,如果90%的请求都在缓存里解决了,数据库的压力能降多少?
这里有个细节要注意:缓存更新策略。我习惯用「先更新数据库,再删除缓存」的模式。为什么?
因为如果先删缓存再更新数据库,在高并发下会出现「缓存穿透」——A线程删了缓存,B线程读不到缓存去查数据库,查到旧数据又写回缓存,然后A线程才更新数据库。结果缓存里永远都是旧数据。
注意:缓存不是万能的。审批流程中的「待办数量」这种实时性要求极高的数据,我建议直接查数据库,或者用Redis的原子操作来维护。否则用户看到「您有3条待办」,点进去发现只有2条,体验反而更差。
3.4 数据库读写分离优化:让数据库喘口气
大多数OA系统的数据库瓶颈,都是读压力太大。审批列表、流程查询、历史记录……这些读操作占了总请求的80%以上。
读写分离的思路很简单:
- 主库:只处理写操作(提交审批、审批通过、驳回等)
- 从库:只处理读操作(查询待办、查看详情、搜索历史)
但这里有个坑:主从延迟。你刚提交了一个审批单,主库写进去了,但从库还没同步。用户刷新页面,发现待办列表里没有这条记录。这就尴尬了。
我个人的解决方案是:
- 强制读主库:对于刚提交的审批单,在30秒内强制从主库读取
- 业务容忍:对于历史数据、统计报表,允许秒级延迟
- 缓存兜底:提交成功后,直接把数据写入缓存,读请求优先走缓存
来看一个简单的读写分离配置:
// 动态数据源路由
public class DynamicDataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 判断当前操作是读还是写
if (DataSourceContextHolder.isReadOnly()) {
return DataSourceType.SLAVE;
}
return DataSourceType.MASTER;
}
}
// 使用示例
@Transactional
public void approve(Long approvalId, String userId) {
// 写操作,自动路由到主库
approvalDao.updateStatus(approvalId, "APPROVED");
// 写完后,把最新数据写入缓存
cacheService.set("approval:" + approvalId, approvalDao.findById(approvalId));
}
public ApprovalVO getApprovalDetail(Long approvalId) {
// 读操作,优先查缓存
ApprovalVO vo = cacheService.get("approval:" + approvalId);
if (vo != null) {
return vo;
}
// 缓存没有,查从库
DataSourceContextHolder.setReadOnly(true);
try {
return approvalDao.findById(approvalId);
} finally {
DataSourceContextHolder.clear();
}
}
避坑指南:我曾经在一个项目里,把所有读请求都路由到了从库。结果有个报表查询跑了10秒,直接把从库CPU打满,导致主从同步延迟飙升到30秒。后来我加了「读库熔断」机制——如果从库延迟超过5秒,自动切回主库读。虽然主库压力大了点,但至少业务不中断。
3.5 总结:高性能审批引擎的三大原则
嗯,说了这么多,其实核心就三条:
- 异步化:用户只关心「提交成功」,不关心「处理完成」
- 缓存化:90%的读请求不要落到数据库
- 分离化:读写分离、队列分离、业务分离
我见过太多OA系统,一开始设计时没考虑性能,等到用户量上来,每天加班改代码。其实这些优化,在架构设计阶段花两天时间就能搞定。别等到线上出问题了再后悔。
下一章,我们会聊聊审批流程的「动态表单设计」。说白了,就是怎么让用户自己画表单,而不是每次改流程都要改代码。敬请期待。