聊聊技术探索与实践:从一个项目出发,谈谈我在架构设计中的“弯路”和成长

宋桂英
2025-06-18 01:48
阅读 485

引子:为什么我要写这篇文章?

引子:为什么我要写这篇文章?

作为一名从业多年的后端架构师,我一直在一线参与大型系统的架构设计和技术攻坚。今天想跟大家分享一次真实项目中遇到的技术挑战,以及我们如何一步步通过技术探索和实践找到最优解的过程。

这个项目不是什么高大上的 AI 或大数据平台,而是一个相对传统的互联网金融系统——用户在上面进行资金交易、风控审批、额度评估等核心业务。但正是这样一个看似“传统”的项目,反而让我对技术选型、架构设计、团队协作等有了更深刻的认识。

希望这篇文章能帮你少踩几个坑,也能感受到我在过程中的一些思考与挣扎。


项目背景:一个“看似简单”的需求

项目背景:一个“看似简单”的需求

我们接到一个新需求:实现一套完整的贷款申请流程引擎,支持复杂的审核规则配置,并具备实时决策能力。

听起来是不是很简单?不就是流程编排 + 决策引擎吗?

可现实远比听起来复杂。我们要面对的挑战包括:

  1. 流程中存在大量分支条件(比如根据用户信用等级、资产状况、历史行为等动态跳转不同节点)
  2. 不同节点可能需要调用不同的微服务接口或外部数据源
  3. 前端要求灵活配置流程结构,非技术人员也能使用
  4. 所有操作要记录日志、支持回溯、并发安全,且响应速度必须快(目标 <500ms)

这已经不是一个简单的 BPM 系统了,而是结合了规则引擎、流程调度、可视化编辑器、事件驱动、分布式状态管理等多个组件的一体化解决方案。


面临的挑战:理想丰满,现实骨感

我们最初的方案是基于已有的工作流框架 Camunda 做扩展开发。Camunda 是一个成熟的开源流程引擎,内置流程建模、任务分配、持久化等功能,社区资源也比较丰富。理想很美好,落地却磕磕绊绊。

挑战一:流程模型不够灵活

Camunda 的 BPMN 支持很强大,但我们的业务规则非常复杂。有些决策逻辑甚至需要嵌套多个 if-else 分支,并依赖调用多个外部接口的数据结果。这导致 BPMN 图变得异常庞大,维护起来非常吃力。

更糟的是,前端产品同学希望“拖拽式”地定义整个流程图,而不是靠 BPMN 工具手动绘制。我们需要一个更轻量级、更容易集成到前端的流程模型描述语言。

挑战二:规则执行性能瓶颈明显

我们原本希望通过 Drools 做规则引擎,用来处理流程中的判断逻辑。但在压力测试时发现:当规则数量超过 200 条时,单次评估耗时就接近 700ms,超出了我们的预期。

分析发现,Drools 在大规模规则下会频繁触发 rete 网络重建,严重影响吞吐量。而且每次新增规则都需要重新加载规则库,影响可用性。

挑战三:流程状态一致性难保障

由于系统采用的是 Event Sourcing + CQRS 架构,流程实例的状态变更也是通过事件来驱动的。但在实际使用中,我们多次遇到状态不一致的问题,尤其在流程节点跳转较多、事件消费并发度较高的场景下。

这个问题一度让我们怀疑是否该放弃 Event Sourcing……


解决思路:换条路走走

面对这些挑战,我们决定重新梳理技术路线,不再拘泥于原有的方案,尝试从以下几个方面重构:

1. 自研轻量流程引擎 + JSON 描述模型

既然 BPMN 太重,又不好让前端直接生成,那我们就自己定义一套流程描述格式——用 JSON 表达流程节点和跳转规则。例如:

{
  "name": "loan-process",
  "nodes": [
    {
      "id": "node_check_credit",
      "type": "service",
      "config": {
        "serviceName": "creditService",
        "method": "checkCreditScore"
      },
      "transitions": [
        {
          "condition": "#creditScore > 600",
          "target": "node_pre_approve"
        },
        {
          "condition": "#creditScore <= 600",
          "target": "node_manual_review"
        }
      ]
    }
  ]
}

这套模型可以由前端表单生成,同时我们还开发了一个小型解释器来解析并执行流程逻辑,这样既减少了对外部工具的依赖,也提高了灵活性。

2. 规则评估模块分离 + 缓存命中优化

为了避免规则频繁评估带来的性能损耗,我们将规则计算模块单独抽离为一个服务,并引入缓存机制。对于相同的输入参数,优先查缓存,避免重复计算。

此外,我们还引入了轻量级表达式引擎 MVEL(类似的还有 Javalang、Aviator 等),它相比 Drools 更加轻便,适用于我们这种规则数量可控、逻辑清晰的场景。

3. 重构流程状态管理模型

为了解决状态一致性问题,我们采用了“状态版本号 + 乐观锁”的方式来控制流程节点的流转。每一个节点变更都携带当前流程实例的版本号,只有版本号匹配的操作才会被接受,否则抛出冲突错误。

这种方式虽然增加了数据库的压力(因为需要更新 version 字段),但在实践中证明是非常有效的,尤其是在高并发场景下。


实践过程:一些关键代码和小坑

流程解释器简化版示例(Java)

实现方案图-2

为了快速验证流程模型的有效性,我们写了如下伪代码作为原型:

public class ProcessEngine {

    private Map<String, NodeHandler> handlers = new HashMap<>();

    public void registerHandler(String type, NodeHandler handler) {
        handlers.put(type, handler);
    }

    public void execute(ProcessDefinition def, ProcessContext context) {
        String currentId = def.getStartNodeId();
        while (currentId != null) {
            Node node = def.findNodeById(currentId);
            NodeHandler handler = handlers.get(node.getType());
            Object result = handler.handle(context);

            Transition transition = findMatchingTransition(node.getTransitions(), context);
            if (transition == null) {
                throw new IllegalStateException("找不到匹配的转移路径");
            }


![技术概念图解-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025061801/124d9b52-b0e4-459b-be17-d89a248b1903.jpg)


            currentId = transition.getTarget();
        }
    }

    // 根据上下文找匹配的转移条件
    private Transition findMatchingTransition(List<Transition> transitions, ProcessContext context) {
        for (Transition trans : transitions) {
            boolean matched = evaluateCondition(trans.getCondition(), context);
            if (matched) return trans;
        }
        return null;
    }

    private boolean evaluateCondition(String condition, ProcessContext ctx) {
        // 这里用 MVEL 或其它表达式引擎做变量替换和评估
        return ExpressionEvaluator.eval(condition, ctx);
    }
}

这段代码只是一个雏形,但帮助我们快速验证了整体流程模型的可行性。

数据库乐观锁更新流程状态(MySQL 示例)

UPDATE process_instance SET 
    current_node = ?, 
    version = version + 1 
WHERE id = ? AND version = ?;

每次更新前都会校验版本号,若匹配不上说明发生了并发冲突,交由上层处理重试逻辑。

小插曲:MVEL 表达式兼容性问题

我们最初选用 Aviator,但后来发现它的类型转换机制在某些情况下会导致 bug(例如数字比较失败)。切换到 MVEL 后稳定了不少,但也需要注意变量命名的大小写问题,比如 context.creditScore 会被 MVEL 转为 getCreditScore() 方法调用,务必确保字段 getter/setter 正确。


效果总结:系统上线后的表现

重构完成后,系统在以下几方面得到了显著提升:

指标 原方案 新方案 提升幅度
单流程执行时间 700ms+ <300ms ✅ 降了近一半
规则评估延迟 ~500ms <80ms ✅ 提升明显
流程修改响应时间 几分钟(BPMN) 秒级(JSON配置) ✅ 提升用户体验
开发迭代效率 痛苦调试 配置即生效 ✅ 显著改善

更重要的是,团队成员对这套新体系的理解成本降低了很多,新人上手流程引擎部分的时间从一周减少到半天以内。


经验分享:给同行的一些建议

如果你也在做类似的工作,或者正在纠结技术选型,这里是我总结的一些经验教训:

✅ 技术选型没有银弹,只有最合适的解

不要盲目追求“流行技术”或“成熟框架”,很多时候它们的抽象层次太高,反而会成为你解决问题的障碍。比如 BPMN 理念很好,但在特定场景下可能并不适合。

✅ 小而精的设计往往比大而全更好

有时候我们太容易陷入“全功能覆盖”的怪圈,试图把所有可能性都涵盖进去。但实际上,在有限范围内做极致优化,比做一堆没用的功能更有价值。

✅ 重视团队协作与文档沉淀

我们初期只顾着赶进度,没有及时记录设计思路,后来出现了好几个“谁也没搞懂是怎么运作”的模块。建议大家在做设计的时候同步输出文档,哪怕是画张草图也好。

✅ 学会“偷懒”:封装 + 工具化

很多重复性的工作可以通过封装和自动化工具解决。比如我们后来开发了一个可视化流程编辑器,配合自动部署脚本,极大提升了效率。

✅ 关注可观测性,别等到出事才想起来埋点

我们在上线初期没做足够监控,后来遇到性能问题只能临时补监控点。建议在设计阶段就考虑好哪些地方需要埋点、日志、追踪链路等,别到最后抓瞎。


结语:技术和成长是一体两面

这次项目的经历让我深刻体会到,所谓“技术探索”,其实就是在不断试错中寻找平衡的过程。有时候你以为找到了一个很棒的框架,结果上线才发现根本不适合自己的业务;有时候你觉得某项技术太过简单,根本看不上眼,结果用了之后才发现原来这么香。

真正的架构师,不是懂得多少牛逼技术的人,而是知道什么时候该用、怎么用、何时换的人。

技术这条路很长,也很有趣。愿我们一起在这条路上越走越稳,越走越宽广。

如有兴趣交流本文提到的内容,欢迎留言或私信我,咱们一起聊聊!

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝