首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >企业OA系统三个核心性能瓶颈的实战解法(附代码与压测数据)

企业OA系统三个核心性能瓶颈的实战解法(附代码与压测数据)

原创
作者头像
IT爱学堂AA
修改2026-08-12 18:26:42
修改2026-08-12 18:26:42
790
举报

企业OA系统三个核心性能瓶颈的实战解法(附代码与压测数据)

腾讯云开发者社区 首发 作者:某集团信息中心 技术负责人

背景

我们集团自研的OA系统,上线半年后日活突破1.2万,但随之而来三个痛点:

  1. 审批流引擎频繁出现乐观锁异常,业务数据和流程状态不一致
  2. 多租户数据隔离导致SQL杂乱,权限错漏风险高
  3. 待办列表查询在早高峰(9:00-9:30)平均响应时间超过5秒,数据库CPU飙到90%

本文将逐个击破,给出可复用的解决方案,所有代码均已脱敏。


一、动态审批人与事务最终一致性

1.1 动态审批人解析器(支持Groovy脚本)

我们要求审批人可配置为“部门负责人”“岗位角色”或任意Groovy表达式。设计SPI解析器:

代码语言:javascript
复制
// 解析器接口
public interface ApproverResolver {
    List<String> resolve(Map<String, Object> variables, String expression);
}

// 部门负责人解析器
@Component
public class DeptLeaderResolver implements ApproverResolver {
    @Override
    public List<String> resolve(Map<String, Object> vars, String expr) {
        // 表达式: deptLeader(表单.deptId)
        String deptId = (String) SpelExpressionParser.evaluate(expr, vars);
        return userDao.listDeptManagers(deptId).stream()
                      .map(User::getUserId).collect(Collectors.toList());
    }
}

// 脚本解析器(支持热加载)
@Component
public class ScriptResolver implements ApproverResolver {
    @Override
    public List<String> resolve(Map<String, Object> vars, String expr) {
        // expr: script:approvers.groovy
        String scriptPath = expr.substring("script:".length());
        GroovyScriptEngine engine = new GroovyScriptEngine("/scripts");
        Binding binding = new Binding(vars);
        return (List<String>) engine.run(scriptPath, binding);
    }
}

启动流程时,通过监听器动态设置 assigneeList 变量:

代码语言:javascript
复制
@EventListener
public void onStart(ProcessStartedEvent event) {
    Map<String, Object> vars = event.getVariables();
    String expr = (String) vars.get("approverExpr");
    List<String> approvers = resolverRegistry.resolve(expr, vars);
    vars.put("assigneeList", approvers); // 供UserTask使用
}

1.2 事务分离 + 补偿消息

原代码将业务保存和流程启动放在同一 @Transactional 中,导致长事务和乐观锁冲突。改为事件驱动 + 独立事务:

代码语言:javascript
复制
@Service
public class ApprovalSubmitService {
    @Transactional(transactionManager = "bizTxManager")
    public void submit(ApprovalForm form) {
        approvalDao.insert(form);
        // 发布事件,异步处理流程启动
        applicationEventPublisher.publishEvent(new ApprovalCreatedEvent(form));
    }
}

@EventListener
public void handleApprovalCreated(ApprovalCreatedEvent event) {
    // 使用独立事务管理器
    TransactionTemplate flowTmpl = new TransactionTemplate(flowTxManager);
    flowTmpl.execute(status -> {
        try {
            runtimeService.startProcessInstanceByKey(...);
        } catch (StaleObjectException e) {
            // 记录重试消息(发送到CMQ,最多重试3次)
            retryProducer.send(new RetryMessage(event.getFormId(), 3));
        }
        return null;
    });
}

效果:业务事务平均耗时从 1.2s 降至 180ms,流程启动异常率从 5% 降至 0.1%。


二、多租户 + 数据权限:SQL拦截器防漏查

2.1 字段隔离 + 自动填充

使用 MyBatis-Plus 元数据填充 tenant_id

代码语言:javascript
复制
@Component
public class TenantMetaHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "tenantId", Long.class, 
                              TenantContext.getCurrentTenantId());
    }
}

2.2 自定义拦截器强制添加 tenant_id 条件

代码语言:javascript
复制
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", ...)})
@Component
public class TenantInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        MetaObject meta = SystemMetaObject.forObject(handler);
        MappedStatement ms = (MappedStatement) meta.getValue("delegate.mappedStatement");
        
        // 忽略系统表或标注 @IgnoreTenant 的方法
        if (shouldSkip(ms)) return invocation.proceed();
        
        String sql = (String) meta.getValue("delegate.boundSql.sql");
        String tenantId = TenantContext.getCurrentTenantId().toString();
        
        // 智能插入:只在主表WHERE后追加 AND tenant_id = ?
        String newSql = SqlInjector.injectTenant(sql, tenantId);
        meta.setValue("delegate.boundSql.sql", newSql);
        return invocation.proceed();
    }
}

坑点:对于带别名的表,如 SELECT * FROM org_user u WHERE u.status=1,注入时要识别别名。我们规定所有表必带别名,注入逻辑改为:

代码语言:javascript
复制
-- 原始
SELECT * FROM org_user u WHERE u.status = 1
-- 注入后
SELECT * FROM org_user u WHERE u.status = 1 AND u.tenant_id = '1001'

实现方法:正则匹配 FROM\s+(\w+)\s+(\w+) 获取主表别名,然后追加 AND 别名.tenant_id = ?

2.3 数据权限(部门范围)叠加

再增加一个拦截器,根据 @DataScope 注解追加 dept_id IN (...) 条件,优先级在租户之后。两者组合实现“租户→部门→个人”三级过滤,零业务侵入。


三、待办列表查询:从5秒到50毫秒的优化实录

3.1 原始SQL性能分析

原查询关联 ACT_RU_TASKACT_RU_EXECUTIONACT_RU_IDENTITYLINK、业务表单表,数据量达80万行,执行计划显示全表扫描 + Using temporary。

3.2 优化步骤

第一步:冗余字段 + 复合索引

ACT_RU_TASK 增加 ASSIGNEE_USER_ID_(bigint)和 TENANT_ID_,建立复合索引:

代码语言:javascript
复制
CREATE INDEX idx_tenant_assignee_time ON ACT_RU_TASK(TENANT_ID_, ASSIGNEE_USER_ID_, CREATE_TIME_);

单用户待办查询直接命中索引,但仍有大量回表。

第二步:宽表 + 异步更新

构建 todo_task_summary 宽表,包含所有展示字段。流程完成、转办、驳回时,通过MQ(腾讯云CKafka)异步更新宽表。

查询变成单表分页:

代码语言:javascript
复制
@Cacheable(value = "todo", key = "#userId + '_' + #page")
public PageResult<TodoVO> query(Long userId, int page) {
    return todoSummaryMapper.selectPage(
        new Page<>(page, 20),
        new LambdaQueryWrapper<TodoVO>()
            .eq(TodoVO::getAssigneeUserId, userId)
            .orderByDesc(TodoVO::getCreateTime)
    );
}

第三步:Redis缓存 + 主动失效

采用“永久缓存 + 定时刷新”策略,避免雪崩。当任务状态变更时,发送广播消息清除相关用户缓存。

代码语言:javascript
复制
@Component
public class TodoCacheManager {
    @CacheEvict(value = "todo", key = "#userId")
    public void evictUser(Long userId) { }
}

3.3 压测数据(JMeter 200并发)

场景

优化前 RT (ms)

优化后 RT (ms)

DB CPU

单用户待办

4850

48

85%→12%

100并发混合

超时 (12s)

102

-


四、云原生部署与监控(腾讯云实践)

我们将服务容器化,使用 TKE(腾讯云容器服务)部署,结合 CLS 日志服务、Prometheus监控。

4.1 Dockerfile 优化

代码语言:javascript
复制
FROM openjdk:11-jre-slim
COPY target/oa-flow.jar /app/app.jar
ENTRYPOINT ["java", "-Xmx1g", "-XX:+UseG1GC", "-jar", "/app/app.jar"]

4.2 配置外部化(使用 Apollo)

所有环境配置(数据库、MQ、Redis)通过 Apollo 管理,不同租户可动态调整。

4.3 监控告警

  • 业务指标:流程积压量、审批超时率(通过 CLS 日志结构化分析)
  • JVM指标:通过 Prometheus JMX Exporter 采集,Grafana 展示
  • 自定义告警:当待办查询 P99 > 200ms 时触发钉钉通知

五、经验总结

  1. 乐观锁重试:Flowable 的 StaleObjectException 不可避免,用 @Retryable 配合指数退避,成功率提升至 99.9%。
  2. 多租户拦截器必须加白名单,避免误伤系统表;同时注意别名解析。
  3. 性能优化要结合业务特性:我们之所以用宽表,是因为待办列表展示字段固定,且变更频率低,完全值得牺牲写性能换读性能。
  4. 云原生不是万能的,但结合腾讯云 CKafka、CLS 确实降低了运维成本。

后续计划

  • 引入 Canal 监听 binlog 同步宽表,进一步降低业务耦合
  • 基于历史数据训练审批人推荐模型,减少手动选择

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 企业OA系统三个核心性能瓶颈的实战解法(附代码与压测数据)
    • 背景
    • 一、动态审批人与事务最终一致性
      • 1.1 动态审批人解析器(支持Groovy脚本)
      • 1.2 事务分离 + 补偿消息
    • 二、多租户 + 数据权限:SQL拦截器防漏查
      • 2.1 字段隔离 + 自动填充
      • 2.2 自定义拦截器强制添加 tenant_id 条件
      • 2.3 数据权限(部门范围)叠加
    • 三、待办列表查询:从5秒到50毫秒的优化实录
      • 3.1 原始SQL性能分析
      • 3.2 优化步骤
      • 3.3 压测数据(JMeter 200并发)
    • 四、云原生部署与监控(腾讯云实践)
      • 4.1 Dockerfile 优化
      • 4.2 配置外部化(使用 Apollo)
      • 4.3 监控告警
    • 五、经验总结
    • 后续计划
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档