做好设计:流程设计

没有什么是一成不变的。


在“做好设计:存储设计基础”一文中,探讨了软件设计中的关键环节:存储设计。存储设计完成之后,要进行流程设计。

要做好流程设计,首先要熟悉各种基本流程模式。可阅:““驯服”业务流程:盘点业务开发中的常见流程模式 ”。

熟悉常用流程模式之后,就需要灵活加以组合,来构建复杂的业务流程。

业务流程需要满足的若干特性

  • 完整性:构建完整的业务流程,从数据流入到数据存储或数据流出,畅通无阻。
  • 清晰性:流程表达要足够清晰,容易理解。
  • 健壮性:对于流程中的各种错误,要确保合理地处理,避免流程中断,或者僵死在某个状态。
  • 性能:流程在可接受的时间内完成。
  • 稳定性: 流程在大数据量或大流量的压力下,不会阻塞崩溃。
  • 可扩展性:如果要在流程中添加新的子流程处理,能否很好地添加而不影响已有。

构建复杂流程的方法

如何构建复杂的业务流程呢? 运用“分解-抽象-封装-复用-组合-关联-重构-函数式编程”的思想和方法来搞定。

分解

分解,即是将具有独立语义的子流程和流程逻辑单元分离出来。

  • 将整体流程分解为若干个子流程;
  • 将每个子流程进一步分解为更小粒度的流程逻辑单元;
  • 一直分解,直至每个流程逻辑单元简单而清晰(只做一件事)。

构建流程逻辑单元的模式可以对应到““驯服”业务流程:盘点业务开发中的常见流程模式 ”一文中的基本流程模式。

分解这一步非常重要:

  • 将整体流程中的子流程分离出来再组合,可使其依赖和组合关系分割得更清晰,使凌乱的整体流程逻辑整通顺,变得整洁有序;
  • 分解整体流程的过程中可以增进对整体流程的理解;
  • 为抽象、封装和复用打下基础。

分解得足够细,就更容易抽象和封装,更容易复用。

抽象

在分解的基础上,将关键信息提炼出来,将流程共性提炼出来,封装成可复用的标准对象和方法。

  • 关键信息提炼;
  • 共性流程提炼。

标准对象提炼非常重要,体现了对流程中关键信息的认知。流程通常是围绕关键信息而构建起来的。

流程共性抽象亦非常重要,将子流程的共性抽离出来,更容易复用,减少重复代码及伴随的BUG。

比如恶意进程检测流程的关键信息是什么? MD5, SHA256, filePath。准确地构造这 3 个信息,基本就把握了恶意进程检测流程的关键。

比如各种检测流程有什么共性流程?获取规则信息、构建告警详情、存储告警详情、白名单检测。

抽象的结果通常是若干个可复用的独立语义的概念。而封装则需要将概念转换为具体的实现形式。

封装

抽象好后,就需要设计良好的可复用的封装形式。

  • 关键信息封装成标准对象(可对接不同业务)。
  • 子流程共性封装成流程组件;
  • 流程组件封装成高层次的流程组块。

良好的封装可以减少重复代码的出现,让流程更清晰而容易理解,减少认知负担。

封装单元可大可小。封装单元可以是一个对象、方法、组件类,也可以是多个类组成的库或框架,或一个相对完整的流程。

比如知道恶意进程检测的关键信息 MD5, SHA256, filePath,就可以把这三个信息封装成一个标准对象 VirusCheckTaskInfo。

比如通用入侵检测流程里的获取规则信息、检测结果填充、告警详情构建、告警存储、发送大数据,都可以封装成流程组件,可以在不同的检测流程里复用。

复用

  • 提炼流程组件的通用部分进行复用;
  • 提炼流程组块的通用部分进行复用。

有了封装的基础,复用就比较容易了。只需考虑在具体的业务场景如何去引用,或者对现有封装做一点小的改造。

流程组块是理解和构建复杂流程的基础。越是能够在高层次流程组块思考和构建流程,就能驾驭越复杂的流程。

有了可复用的封装的基础,接下来就只需要考虑封装单元的组合和关联。

组合

  • 在可复用的流程组件和流程组块的级别上进行组合。

比如上述多个流程组件可组合成通用入侵检测流程的核心部分,而这个通用入侵检测流程的核心部分又在更复杂的检测流程里复用。比如过各种已有引擎检测库,就几个字,却代表着一个包含多个引擎库的复杂的恶意进程检测流程。

组合通常是串行、并发的各种流程组合模式。这就运用到““驯服”业务流程:盘点业务开发中的常见流程模式 ”一文总结的各种业务流程模式及组合模式。

关联

  • 将有关联的信息通过唯一标识关联起来;
  • 通过实体表的唯一标识字段的外键关联来建立。

比如脱壳进程的文件和原进程的文件信息,就可以通过在脱壳进程文件记录里关联原进程文件记录的 MD5 或 SHA256 或 fileId 来实现。

重构

  • 反复运用上述方法,对已有流程进行改小步重构,是一种很好的理解和梳理复杂流程的方式;
  • 小步重构技巧:每次抽离一部分流程,然后写一个短函数进行调用。

函数式编程

函数式编程是一种强大的编程思想和编程技艺,可以很好地将共性和差异抽离,让整个流程更加顺畅。

可阅:“函数式+泛型编程:编写简洁可复用的代码

质量属性

性能

常用的提升流程性能的手段如下:

  • 快速过滤或去重。
  • 正确的索引(必要索引,联合索引,减少不必要的索引及索引字段)。
  • 内存数据结构和算法降重优化。
  • 加缓存。
  • 并发(串行变并发,线程池)。
  • 异步(同步变异步,将次要部分从主体流程中分离)。
  • 批量处理(单个变批量)。
  • 确保表设计合理(大小维度的表分开)。
  • 合并重复流程(避免重复文件上传或下载、重复创建同一个对象)。
  • 合并重复调用(重复查询同一条记录)。
  • 加检测结果库(黑白名单),避免重复检测。
  • 精简流程
  • 使用 arthas trace 命令测量方法 RT,找到性能热点区域,专项优化。

稳定性

  • 对接口添加限流。
  • 使用消息中间件削峰填谷。

可扩展性

  • 使用 Dispatcher-Handler 模式。多线程并发模式中,可以在 Dispatcher 中分发任务给不同的 Handler ,Dispatcher 在主线程中执行,不同的 Handler 在不同的线程中执行。 Channel 并发模式中,可以通过 channel 将任务分给不同的 channel ,不同的 Handler 消费不同的 channel 里的任务独立执行。

业务流程构建的四个注意事项

  • 闭环:如果流程有任务状态,则必须让任务状态最终落在完成或失败,避免滞留在中间状态。
  • 并发:多个线程访问共享资源,要保证数据正确性,必要的地方要加同步。
  • 异步:如果流程比较耗时,则需要拆分成异步,保证响应的体验。
  • 多节点: 考虑多节点情形的处理,必要时候需要分布式锁。

闭环问题

当流程中的某个操作失败后,就会导致整体流程无法完成,停滞在某个点上,无法闭环。

闭环的解决思路:

  • 直接失败。对于不能重试(可能导致资损)的情形,直接失败是最简单有效的办法。
  • 重试、设置最大失败次数。对于可重试场景,可间隔一段时间重试一次,达到最大失败次数则设置为终态失败。
  • 保存点。设置保存点,失败之后可以从保存点重续执行,最终达到终态成功。
  • 回滚。作为事务,失败时回滚到最初一致状态。
  • 超时失败定时任务机制。如果超过指定时间,则更新任务状态为失败,原因是超时。超时需要与更新时间做比较。

并发

  • 如果有大量数据要获取或者处理,通常需要采用并发来提升性能。
  • 并发线程获取要设置超时时间,避免网络不佳时长时间阻塞。
  • 如果有多个线程或者多台机器同时访问同一条记录,则需要加锁(行锁或分布式锁)。

异步

  • 不同端的通信,通常采用异步的方式。比如前端是通过 ajax 来异步拉取服务端数据。服务端要发送指令给客户端,也是先下发指令,等客户端完成后,返回给服务端回调消息进行后置处理。
  • 耗时操作。耗时操作如果采用同步,则很容易超时导致失败。一般采用异步来分离。
  • 高并发操作。高并发情况下,为保证核心流程的TPS,可以将非核心流程读写用异步分离出去。
  • 次要操作。一个长流程中,有一些次要操作,不影响最终结果,可以通过异步分离出去单独处理。
  • 异步要注意多操作间的时间差问题。两个操作是异步的,在一个操作完成后在页面查看另一个操作的结果时可能会不一致。比如上报一条告警同时异步上传一个文件。当告警入库展示在页面时,文件上传可能未完成,这时候文件是无法下载的。还有一种情形是两个线程同时更新同一条记录的状态,一个线程可能会覆写另一个线程写入的状态。这个写入顺序的不确定会导致最终记录状态的不确定。

多节点

  • 避免读写本地缓存。因为缓存只在单个 JVM 里,其它机器上读不到该缓存,就会导致问题。
  • 只读本地缓存。本地缓存通常从公共数据库中获取和建立。
  • 采用外部存储来同步。比如使用 redis 来存储会话、做分布式锁。
  • 采用中间件来通信和解耦。

流程不是一成不变的

随着需求变更,流程会反复更改。因此不要局限于流程的现有实现。当遇到不合理之处时,大胆质疑和改造优化,是为上策。

小结

流程千变万化。掌握基本的流程模式和复杂流程的构建方法,熟悉一些注意事项,辅以并发、异步、同步、消息中间件通信,没有什么流程是搞不定的。

posted @ 2024-01-14 10:29  琴水玉  阅读(60)  评论(0编辑  收藏  举报