调研环境说明

etcd –version
etcd Version: 3.5.1
Git SHA: d42e8589e
Go Version: go1.17.2
Go OS/Arch: darwin/amd64

参数说明

启动参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
--name etcd-1 // 节点名称
--data-dir /Users/noogel/Debug/data/etcd1 // 数据目录
--initial-advertise-peer-urls http://127.0.0.1:238
--listen-peer-urls http://127.0.0.1:2381
--listen-client-urls http://127.0.0.1:2379
--advertise-client-urls http://127.0.0.1:2379
--initial-cluster-token etcd-lock-cluster-1 // 集群 token
--initial-cluster etcd-1=http://127.0.0.1:2381,etcd-2=http://127.0.0.1:2382,etcd-3=http://127.0.0.1:2383 // 集群节点信息
--initial-cluster-state new // 初始化的集群状态
--heartbeat-interval 1000 // 心跳间隔
--auto-compaction-retention 1 // 开启自动压缩,间隔 1h 执行
--auto-compaction-mode periodic
--quota-backend-bytes 8589934592 // 后端存储大小
--election-timeout 5000 // 选举超时时间

关于自动压缩
https://etcd.io/docs/v3.4/op-guide/maintenance/#defragmentation

  • --auto-compaction-mode=revision --auto-compaction-retention=1000 每5分钟自动压缩”latest revision” - 1000
  • --auto-compaction-mode=periodic --auto-compaction-retention=12h 每1小时自动压缩并保留12小时窗口。

自动压缩碎片后还需要单独再清理占用的系统存储空间,etcdctl defrag

最佳实践

  1. heartbeat timeout 默认为 100ms,推荐配置为 1s;
  2. election timeout 默认为 1000ms,推荐为 5s(election timeout >= 5 * heartbeat timeout);
  3. quota-backend-bytes 默认为 2G(最大值8G),推荐根据集群容量预估调整;
  4. 配置 auto-compaction-retention=1 和 auto-compaction-mode=periodic 参数,定期压缩历史数据;
  5. 推荐通过 cronjob 定期执行 etcdctl defrag(如果 defrag 执行时间 > election timeout,则集群会进入重新选主模式)

环境模拟

端口映射

单机环境写集群搭建,以下是端口映射

etcd1 2379 -> 2391 2380 -> 2381

etcd2 2379 -> 2392 2380 -> 2382

etcd3 2379 -> 2393 2380 -> 2383

启动命令

1
2
3
4
5
ETCDCTL_API=3 etcd --name etcd-1 --data-dir /Users/noogel/Debug/data/etcd1 --initial-advertise-peer-urls http://127.0.0.1:2381 --listen-peer-urls http://127.0.0.1:2381 --listen-client-urls http://127.0.0.1:2379 --advertise-client-urls http://127.0.0.1:2379 --initial-cluster-token etcd-lock-cluster-1 --initial-cluster etcd-1=http://127.0.0.1:2381,etcd-2=http://127.0.0.1:2382,etcd-3=http://127.0.0.1:2383 --initial-cluster-state new --heartbeat-interval 1000 --auto-compaction-mode=revision --auto-compaction-retention=1000 --quota-backend-bytes 8589934592 --election-timeout 5000 > /Users/noogel/Debug/data/etcd1/run.log 2>&1 &

ETCDCTL_API=3 etcd --name etcd-2 --data-dir /Users/noogel/Debug/data/etcd2 --initial-advertise-peer-urls http://127.0.0.1:2382 --listen-peer-urls http://127.0.0.1:2382 --listen-client-urls http://127.0.0.1:2378 --advertise-client-urls http://127.0.0.1:2378 --initial-cluster-token etcd-lock-cluster-1 --initial-cluster etcd-1=http://127.0.0.1:2381,etcd-2=http://127.0.0.1:2382,etcd-3=http://127.0.0.1:2383 --initial-cluster-state new --heartbeat-interval 1000 --auto-compaction-mode=revision --auto-compaction-retention=1000 --quota-backend-bytes 8589934592 --election-timeout 5000 > /Users/noogel/Debug/data/etcd2/run.log 2>&1 &

ETCDCTL_API=3 etcd --name etcd-3 --data-dir /Users/noogel/Debug/data/etcd3 --initial-advertise-peer-urls http://127.0.0.1:2383 --listen-peer-urls http://127.0.0.1:2383 --listen-client-urls http://127.0.0.1:2377 --advertise-client-urls http://127.0.0.1:2377 --initial-cluster-token etcd-lock-cluster-1 --initial-cluster etcd-1=http://127.0.0.1:2381,etcd-2=http://127.0.0.1:2382,etcd-3=http://127.0.0.1:2383 --initial-cluster-state new --heartbeat-interval 1000 --auto-compaction-mode=revision --auto-compaction-retention=1000 --quota-backend-bytes 8589934592 --election-timeout 5000 > /Users/noogel/Debug/data/etcd3/run.log 2>&1 &

历史数据清理命令

1
2
3
4
rm -rf /Users/noogel/Debug/data/etcd1
rm -rf /Users/noogel/Debug/data/etcd2
rm -rf /Users/noogel/Debug/data/etcd3
mkdir etcd1 etcd2 etcd3

日常运维

常规命令

1
2
3
4
5
6
7
export ETCDCTL_API=3
// 节点列表查询
etcdctl member list
// 节点状态
etcdctl --endpoints=127.0.0.1:2381,127.0.0.1:2382,127.0.0.1:2383 endpoint status --write-out=table
// 整理磁盘碎片
etcdctl --endpoints=127.0.0.1:2381,127.0.0.1:2382,127.0.0.1:2383 --user root:123456 defrag

开启鉴权

1
2
3
4
5
6
7
8
// 添加 root 用户
etcdctl --endpoints=127.0.0.1:2381,127.0.0.1:2382,127.0.0.1:2383 user add root
// 授权 root 角色
etcdctl --endpoints=127.0.0.1:2381,127.0.0.1:2382,127.0.0.1:2383 user grant-role root root
// 查看用户列表
etcdctl --endpoints=127.0.0.1:2381,127.0.0.1:2382,127.0.0.1:2383 --user=root:123456 user list
// 开启鉴权
etcdctl --endpoints=127.0.0.1:2381,127.0.0.1:2382,127.0.0.1:2383 auth enable

生产集群节点启动方式

在生产机通过 systemd 启动。第一次启动命令--initial-cluster-state new,后续节点的增加需要修改为 --initial-cluster-state existing,不明白看节点增加部分。

修改配置

1
2
3
4
5
6
7
8
// 编辑配置
vim /lib/systemd/system/etcd.service
// 重新加载配置
systemctl daemon-reload
// 启动服务
systemctl start etcd.service
// 查看服务状态
systemctl status etcd.service

问题处理

Etcd 的 compact 机制

Etcd 默认不会自动 compact,需要设置启动参数,或者通过命令进行compact,如果变更频繁建议设置,否则会导致空间和内存的浪费以及错误。Etcd v3 的默认的 backend quota 2GB,如果不 compact,boltdb 文件大小超过这个限制后,就会报错:”Error: etcdserver: mvcc: database space exceeded”,导致数据无法写入。

要从空间不足配额警报中恢复:

  1. Compact etcd的历史。
  2. 对每个etcd端点进行碎片整理。
  3. 解除警报。
1
2
3
4
5
6
7
8
9
10
11
# 1、获取当前的版本
$ rev=$(ETCDCTL_API=3 etcdctl --endpoints=:2379 endpoint status --write-out="json" | egrep -o '"revision":[0-9]*' | egrep -o '[0-9].*')
# 2、压缩当前版本之前的所有记录
$ ETCDCTL_API=3 etcdctl compact $rev
compacted revision 1516
# 3、清理多余的碎片空间
$ ETCDCTL_API=3 etcdctl defrag
Finished defragmenting etcd member[127.0.0.1:2381]
# 4、解除警告
$ ETCDCTL_API=3 etcdctl alarm disarm
memberID:13803658152347727308 alarm:NOSPACE

需要注意的是整理碎片释放空间,要一个一个节点执行,因为在执行期间节点是无响应的,直到处理完。防止因为全部节点无响应导致的服务不可用

碎片整理

压缩key空间后,会出现内部碎片,这些压缩出来的碎片空间可以被etcd使用,但是不会真正的释放物理空间,需要进行碎片整理,如:

1
2
$ etcdctl defrag
Finished defragmenting etcd member[127.0.0.1:2379]以上指令只作用于当前所在的主机,不会在集群

环境中复刻。可以使用–cluster标记指定所有成员以自动查找所有集群成员。如:

1
2
3
4
$ etcdctl defrag --cluster
Finished defragmenting etcd member[http://127.0.0.1:2381]
Finished defragmenting etcd member[http://127.0.0.1:2382]
Finished defragmenting etcd member[http://127.0.0.1:2383]

节点增减

1
2
3
4
5
6
7
8
# 查看成员信息
ETCDCTL_API=3 etcdctl member list
# 移除节点
ETCDCTL_API=3 etcdctl member remove wallet0x
# 添加节点
ETCDCTL_API=3 etcdctl member add wallet0x --peer-urls="http://10.137.158.119:2380"
# 最后再启动服务
# 其中启动命令 --initial-cluster-state 需要设置为 existing。

需要先移除故障节点成员,再添加进去成员列表。然后清理掉故障节点的工作目录内容,之后再启动服务,启动后服务会自动同步数据。

其中启动命令需要设置为 --initial-cluster-state existing

参考链接

http://www.zhaowenyu.com/etcd-doc/ops/data-space-manage.html
https://xieys.club/etcd-backup-restore
https://www.cnblogs.com/lowezheng/p/10307592.html
https://bbotte.github.io/service_config/etcd-cluster-troubleshooting.html
https://www.mytecdb.com/blogDetail.php?id=211
https://www.cnblogs.com/tencent-cloud-native/p/14893209.html
http://www.dockone.io/article/2955
https://mytecdb.com/blogDetail.php?id=199

概况

作为一个北方汉子对于面食真的是十分热爱的,尤其是发面后的。从小在家兜包子都是只能看不让参与的,长大后在外面都是买现成的,如今也想自己做一做。也许是从小在家耳濡目染,第一次做整体的效果还不错。
宋丹丹老师曾经说过把大象装进冰箱总共要分三步,那么蒸包子总共需要分为如下五步:

  1. 准备原材料
  2. 发面
  3. 调馅
  4. 擀面片
  5. 蒸包子

可是,真的就是只蒸了包子吗?

关于蒸包子的一些思考

分治法

就像我之前聊到过的,复杂的东西是由许多简单的东西组成的。就像蒸包子这件事不一定多么复杂,但也不那么简单,要想做好也是要拆分成上面五个相对简单的步骤执行。作为一个后端工程师,现在做事情总是会带入一些思维模型去看,这个例子是分治法来将蒸包子这件事简单化,只要我们把其中简单的每一步做好就可以最终把包子蒸好。

批处理

当我把包子蒸好以后,发现耗时很长,那么怎么提高效率呢。刨除在网上买菜等菜的时间,大致的时间线如下:

发面准备:15:00 -> 15:30
发面期间:15:30 -> 17:00
做、调馅:16:30 -> 17:30
擀皮 :17:30 -> 18:00
包、蒸 :18:00 -> 19:00

总计人力耗时 3 小时,出锅 18 个包子。如果我要是蒸 36 个包子就要消耗 6 小时的时间吗?答案是要小于 6 小时的,主要原因有以下几点:

  1. 蒸笼仍有一半的空间未用,可以节省掉一次烧水蒸包子的时间。
  2. 每个环节都可以节省准备工具,收拾工具的时间。

但是我要再多蒸一倍的包子是不是平均耗时会更短,这个就不一定了,如果蒸锅的承载上限就是 36 个包子,那平均耗时就不会再减少。
批处理的方案在一定程度上会提高我们的效率,但不会无限提高,而是有一个最优解,这个最优解取决于外在条件。

并发思维

以上情况分析的是单人力情况下,如果再有一个人一起做。可以有以下方面提升:

  1. 我们发现面团在 17:00 发好的,擀皮在 17:30 才开始的,中间阻塞的时间在弄馅。把弄馅的时间交给第二个人做,可以减少 30 分钟阻塞。
  2. 发面准备分两个人做可以大致减少 15 分钟耗时。
  3. 两个人一起包包子可以大致减少15分钟耗时。
  4. 额外损耗,例如工具准备上因为多加了一套工具会产生额外耗时。

最后大致的总人力耗时会大于 3 小时,平均人力耗时在 1.5 到 2小时之间。并发思维又是我们另外一个手段。

流水线模型

上面需要人操作的蒸包子需要四步,其中每一步都会有一些内耗是在每步切换时都需要思考下一步该怎么做,以及准备对应步骤的工具撤掉上一步骤的工具。如果我们厨房能供四人同时使用,并且每人只做一步的事,那么每一步的耗时就会因为熟能生巧而使时间大大缩短。如果这是一家包子店的厨房,那么这四个人就可以源源不断的高效生产包子。通过流水线模型来提高效率,这也是并发的一种。

工具化思维

再假如,北京所有的人早餐都要来这家店吃包子,那么任凭这四个人怎么日夜生产,也不能满足整个北京的需求。如果还是这四个人怎么做?如果恰好其中有一个人学过机械相关的知识并且动手能力又很强,这时候他可以和其他三个人一起交流蒸包子的心得,然后结合整个知识,设计出蒸包子机器,然后找工厂生产出几十台日夜生产,我想北京的包子供应应该就没问题了。他们四个人只需要盯盯机器,坏了修一修就好了。这里面就用到了工具化思维,可以极大的提高我们的生产效率。工业革命的意义之一就是创造了巨大的生产力。

说了这么多,下面附上我的蒸包子攻略~

蒸包子攻略

准备原材料

  • 准备面皮
    • 面粉 1 kg
    • 酵母粉 5g
    • 温水
  • 准备馅(猪肉大葱)
    • 猪肉馅 500g
    • 大葱 500g
    • 姜、料酒、胡椒粉
    • 耗油、鸡精、生抽
    • 香油
    • 老抽

发面

  1. 用温水冲开酵母粉,混合均匀后加入面粉中和面。

  1. 慢慢慢慢~加水,搅拌成絮状。

  1. 最后柔成光滑的圆面团。

  1. 放到温暖的地方发酵,一小时左右吧,面团最后会放大一倍左右。

加水的时候需要慢慢加慢慢抓,慢慢会形成一个光滑的圆团,面团的软硬跟水的多少有关,如果面硬的话可以适当加一些水调整软硬程度。

调馅

  1. 趁着面团发酵期间,可以开始准备调馅了。买的是绞好的猪肉馅,然后就是把两颗大葱切碎,切点姜碎进去。
  2. 加入上述的各种调料调整肉馅的口味。由于放了老抽和生抽,盐可以少放或者不放,根据个人口味来看。
  3. 加好以后搅拌均匀就行了。

其中老抽用来调色,生抽、鸡精、耗油用来调味,胡椒粉、姜碎、料酒用来去腥,加一些香油可以让肉馅变得超级香。

擀面片

  1. 调好馅可以休息一会,待发好面取出来继续揉捏,直到里面没有气泡了。
  2. 然后揉成一个细条状,用刀切成一段一段的。用手掌按成扁圆的。
  3. 用擀面杖一点点擀成扁片,厚度比饺子皮厚一些,标准的面片是边缘较薄中心较厚的圆形。

蒸包子

  1. 准备一个篦子,上面放好屉布,待包子包好放上去。至于兜包子的手法直接网上查吧。
  2. 准备好蒸锅,放上包子开火后蒸20分钟左右即可,关火后放 5分钟。
  3. 最后的出锅,因为包子会变大导致互相黏连,可以准备一些清水滴到屉布和包子连接处湿润,这样包子就可以完好的取下了。

最后来一张出锅照,年轻人的第一锅包子就这样做好了。

最后

工作和生活中我们会遇到很多事和物,事事物物之间有很多共通之处,包括问题的产生和解决办法。不同表象的背后相同的本质的东西是思维方式还是抽象模型?看清它们,能带给我的是做出好吃的包子,不仅仅是这些,还有更多。

什么是复杂性

复杂或复杂性与简单相对立,那么复杂是什么?它是我们大脑中的一个概念,但是我在网上找不到一个给复杂恰当的定义描述,它会有不同的解释。
其中洛克在《人类理解论》中说道:『一些思想是由简单的思想组合而成,我称此为复杂;比如美、感激、人、军队、宇宙等。』
作为研究复杂系统的专家 Melanie Mitchell,也没有给出一个明确的公认的定义。她在《复杂》一书中给出了复杂系统加以定义:『复杂系统是由大量组分组成的网络,不存在中央控制,通过简单运作规则产生出复杂的集体行为和复杂的信息处理,并通过学习和进化产生适应性。』
上述复杂系统中的组分对应软件系统中的组成部分,基于不同粒度可以是对象、函数、类、包、模块、组件和服务等。每一部分都应该是相对单一的职责,细粒度部分之间耦合提供更粗粒度功能,不同组分之间相互协作来提供系统功能,继而组合成我们复杂的软件系统。

软件系统复杂性由何而来

计算机的产生对我们生产生活产生的影响不言而喻,其中软件系统的功能是随着我们实际生活需求的变化而变化的。人有七情六欲带来的各种需求,接收信息的方式主要是视觉、听觉。而机器擅长的只是简单的逻辑处理和数值计算,两者之间有着巨大的鸿沟。如何让机器提供视觉和听觉的手段来满足人们的需求,这里抛开硬件不谈,软件层面有操作系统提供基本的软件运行环境。
软件系统则只需要专注于如何组织和管理数据来满足人们的工作生活娱乐需求,一方面要关注人的需求和需求变化,另一方面要关注机器层面能提供的计算能力。
软件系统的复杂性来自于两个方面,一方面是需求侧复杂,导致大多数系统的功能都难以理解;另一方面是难以把控需求的变化,虽然我们遵循一些设计原则可以对未来进行一些预判,但还是存在不可预测的风险。

如何度量复杂度

在《复杂》一书中作者列举了不同角度可能度量复杂性的方法。

  • 生物学上尝试通过基因组的规模来度量。
  • 信息学上尝试通过熵、信息量、交互信息来度量。
  • 用算法信息量度量复杂性(能够产生对事物完整描述的最短计算机程序的长度。)
  • 此外还有逻辑深度、热力学深度、分形维度等方面。

复杂度并没有一个统一明确的度量方式,我们可以站在一个角度上对具体的某类或粒度提供一个可供参考的度量方法。不论我们如何度量,我们在开发软件系统中的一个重要目标就是控制和降低系统复杂度。在巨著《人月神话》中提出了两个重要概念:

  • 本质复杂度:指由于一问题的本质不适合简单的求解方式,所有可行的求解方式都很复杂的情形。
  • 偶然复杂度:指电脑软件开发过程中所引入不必要的复杂度。

偶然复杂度不是待求解问题的本质,相对而言, 本质复杂度和待求解问题的本质有关,是无法避免的。偶然复杂度一般是因为选用求解问题的方法时所引入的。

在源代码层面为了描述工程质量有以下两个方面衡量:

  • 圈复杂度:根据代码中的路径数量计算的循环复杂性。每当一个函数的控制流发生分裂时,复杂度计数器就会增加1。每个函数的最小复杂度为1。由于关键字和功能的不同,这种计算方法在语言上略有不同。以 Java 为例增加复杂度的关键字有:if, for, while, case, catch, throw, &&。
  • 认知复杂度:是由sonarQube设计的一个算法,算法将一段程序代码被理解的复杂程度,估算成一个整数——可以等同于代码的理解成本。作为指导程序员编写“既可测试又可维护”的方法。

在认知复杂度的计算方法中主要基于以下三条规则:

  1. 忽略那些允许将多个语句可读性地速记为一个的结构。
  2. 在代码的线性流程中,每中断一次就累加 1。
  3. 当断流结构被嵌套时难度累加 1。

下面实例对比两种复杂度度量方法的差异,在不同写法上圈复杂度的统计和认知复杂度的统计有何差异。

上图是两种写法在圈复杂度的统计方法,得出的值都是 4,也就是从逻辑上来说是相同的。但是在可读性上来说,明显右侧的 switch 代码更高。认知复杂度就是为了度量人的易于理解性上存在的。

以上是认知复杂度算法给这两种方法打出了明显不同的分数,这些分数更能反映出它们的相对可理解性。更具体的内容可以查看 CognitiveComplexity

如何管理系统复杂度

架构的本质目标就是管理复杂度,而管理复杂度有以下三种有效的手段:

  • 抽象:从众多的具体事物当中抽取共同的、本质的属性,摒弃差异的非本质属性,简化描述形成概念。
  • 分治:把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
  • 领域知识:是指一组有内在联系的知识的集合,它往往与特定的职业、研究方向、兴趣、社群或文化圈层等相关联。

初步认知

以 Drools 为例子

这个组件是什么,有什么功能?

Drools 是一个基于Charles Forgy’s的RETE算法的,易于访问企业策略、易于调整以及易于管理的开源业务规则引擎,符合业内标准,速度快、效率高。业务分析师人员或审核人员可以利用它轻松查看业务规则,从而检验是否已编码的规则执行了所需的业务规则。
Drools相关概念

  • 事实(Fact):对象之间及对象属性之间的关系
  • 规则(rule):是由条件和结论构成的推理语句,一般表示为if…Then。一个规则的if部分称为LHS,then部分称为RHS。
  • 模式(module):就是指IF语句的条件。这里IF条件可能是有几个更小的条件组成的大条件。模式就是指的不能在继续分割下去的最小的原子条件。

Drools通过 事实、规则和模式相互组合来完成工作,drools在开源规则引擎中使用率最广,但是在国内企业使用偏少,保险、支付行业使用稍多。

能解决什么问题?

「规则引擎主要完成的就是将业务规则从代码中分离出来。」 在规则引擎中,利用规则语言将规则定义为if-then的形式,if中定义了规则的条件,then中定义了规则的结果。规则引擎会基于数据对这些规则进行计算,找出匹配的规则。这样,当规则需要修改时,无需进行代码级的修改,只需要修改对应的规则,可以有效减少代码的开发量和维护量。

这个组件对比竞品有什么优势和劣势?

易用性、广泛性、高性能、高可用、高一致性等方面。
Java开源的规则引擎有:Drools、Easy Rules、Mandarax、IBM ILOG。使用最为广泛并且开源的是Drools。
规则引擎优点

  • 声明式编程
  • 逻辑和数据分离
  • 速度和可扩展性
  • 知识集中化

规则引擎缺点

  • 复杂性提高
  • 需要学习新的规则语法
  • 引入新组件的风险

原理了解

这个组件实现机制是什么样的?

Drools规则引擎的结构示意图:


在 Drools 中,规则被存 放在 Production Memory(规则库)中,推理机要匹配的 facts(事实)被存在 Working Memory(工作内存)中。当时事实被插入到工作内存中后,规则引擎会把事实和规则库里的模式进行匹配,对于匹配成功的规则再由 Agenda 负责具体执行推理算法中被激发规则的结论部分,同时 Agenda 通过冲突决策策略管理这些冲突规则的执行顺序。
Drools 中规则冲突决策策略有

  • 优先级策略
  • 复杂度优先策略
  • 简单性优先策略
  • 广度策略
  • 深度策略
  • 装载序号策略
  • 随机策略

使用了什么算法\模型\框架?

Rete 算法
最初是由卡内基梅隆大学的 Charles L.Forgy 博士在 1974 年发表的论文中所阐述的算法 , 该算法提供了专家系统的一个高效实现。自 Rete 算法提出以后 , 它就被用到一些大型的规则系统中 , 像 ILog、Jess、JBoss Rules 等都是基于 RETE 算法的规则引擎。

Rete 在拉丁语中译为”net”,即网络。Rete 匹配算法是一种进行大量模式集合和大量对象集合间比较的高效方法,通过网络筛选的方法找出所有匹配各个模式的对象和规则。

其核心思想是将分离的匹配项根据内容动态构造匹配树,以达到显著降低计算量的效果。Rete 算法可以被分为两个部分:规则编译和规则执行。当Rete算法进行事实的断言时,包含三个阶段:匹配、选择和执行,称做 match-select-act cycle。

Drools 中的 Rete 算法被称为 ReteOO,表示 Drools 为面向对象系统(Object Oriented systems)增强并优化了 Rete 算法。

上手使用

使用场景有哪些?

从Drools规则引擎的使用模版来看,输入、输出和判断三个中,判断是变化的,而输入和输出是基本固定的,所以适用的场合可以分为下面几种:

  • 输入和输出的参数不变,即:规则文件接收固定的参数,产生固定的输出。比如:根据货物重量计算运输价格,输入参数是货物重量,规则根据级差价格表,输出运输价格。
  • 输入和输出的JavaBean Object不变,即:规则文件接收固定类型的JavaBean,产生固定类型的JavaBean。比如:根据顾客信息和当前购物信息计算优惠价格,输入参数是顾客当前的类别(VIP客户等)和当前购物的种类、数量,规则根据顾客类别、商品种类和购买数量输出优惠价格。

所以,规则引擎适用于「问题确定」的场景,并且存在比较复杂的业务规则并且业务规则会「频繁变动」的系统。比如:

  • 风险控制系统(风险贷款、风险评估)
  • 反欺诈项目(银行贷款、征信验证)
  • 决策平台系统(财务计算)
  • 促销平台系统(满减、打折、加价购)

在项目中使用上需要怎么做?

TODO

在使用过程中容易踩到哪些坑?

TODO

参考资料

网络文章、杂志专栏、论文等
https://blog.csdn.net/Taobaojishu/article/details/108231696

模板的好处就在于可以让你快速且全面的规划方案,一些你能想到和不能想到的地方。既能避免你在做旅游规划时漏掉什么,又能节省你思考的时间。有了模板你只需要按照大纲去调研即可。

如果你旅游前需要详细规划,那么这个模板很适合你。
如果你是想走开车就走的那种,可以跳过这篇。

模板按需填充即可,比如去西藏就需要特别关注海拔信息。去三亚就需要关注一下日出日落和潮汐时间。下面是我在做西藏旅游攻略做的一个简单攻略,仅供参考。

阅读全文 »

你有没有遇到过一个函数几百行长度,如果没有可以跳过本文章了,如果没有那么应该看看下面的内容。一个超长函数的复杂性不在于那些胶水代码,而是其中的逻辑分支,大量的逻辑分支导致你的代码难以理解。而重构的过程分为两步:

  1. 编写单元测试,如果覆盖到了函数的每个分支,那么重构的风险性就会降到最低。
  2. 重构代码,对代码进行拆分重写,为了使代码变得易于维护。

重构分为三个层次:

  1. 小重构
    • 消除重复代码
    • 拆分小函数(单一职责)
  2. 设计模式
    • GoF 23种
  3. 抽象建模
    • 四色建模法
    • 风暴建模法

复杂度问题的应对办法,防止偶然复杂性。

TODO

背景介绍

首先介绍一下实体电商通用的售后流程。

  1. 用户申请:在用户申请操作时需要填写退货、换货,以及原因。
  2. 商家审核:商家会根据沟通情况审核售后申请。
  3. 用户回寄:审核通过后需要用户回寄商品。
  4. 确认退换:商家收货确认后会给用户退款或者邮寄新货。

虚拟商品的售后通用流程如下:

  1. 管理员发起退换操作
  2. 处理退换
    • 退:先退货后退款
    • 换:先退货后发货

在以上两个流程的处理流程有个共通的地方,就是一次操作需要涉及多个子流程的处理,这就是接下来需要讲的通用售后流程抽象。多个子流程的处理意味着要和多个子系统分别进行沟通处理退货、换货和退款。

这里就涉及到分布式系统的一致性问题了,售后模块作为资源的协调方,我们是否可以采用 TCC 的强一致性方案?答案是 No,成本有点高。普遍的做法是采用弱一致性方案保证最终一致性,我们可以考虑采用 Pipeline 机制。

概念比较

Pipeline 管道模式

在 Pipeline 机制中有三个基本概念:

  • Pipeline 管道
  • Valve 阀门
  • Context 上下文数据

一个 Pipeline 管理多个 Valve,多个 Valve 共享一个 Context 数据。用类图来表达如下:

为了管理处理的进度,我们每个 Valve 都是有状态的,那么所有的状态和就是整个 Pipeline 的状态。

TCC 方案

TCC 是一种补偿事务方案,他要求对每个服务提供如下三个接口:

  1. Try:尝试对业务进行一致性检查(一致性),然后对资源的锁定(隔离性)。
  2. Confirm:如果所有的服务都锁定成功并且可执行,就进行确认资源的处理。
  3. Cancel:如果有问题就释放资源。

它的适用场景有以下两个要求:

  1. 一致性和隔离性要求比较强
  2. 执行时间比较短的业务

举例说明是账户间转账、跨系统转账等资金类操作上。

但是它有一个比较明显的缺点就是实现成本比较高,需要协调的服务方越多,系统压力也就越大。

在售后场景中,TCC 是明显不适合的。对于整个售后流程说,各个环节也会出现资源占用导致处理失败的情况,受到 Try 的启发,我们不锁资源,只是在整个处理前挨个进行 qualification 资格检查,全部通过后再进入执行阶段。

责任链模式

责任链模式,用来处理相关业务逻辑的一条执行链,执行链上有多个节点,每个节点都可以处理请求,如果某个节点处理完毕就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕的结果。

对于责任链模式,为什么有的实现和 Pipeline 很像。有人对责任链模式进行了如下两种分类:

  • 纯责任链:链上只有一个节点会处理请求。比如双亲委派。
  • 不纯责任链:链上的每个节点都可以处理请求,并转发到下一个节点。比如 Spring Filter 机制。

从上面来看不纯责任链就是 Pipeline 模式,要说区别目前有如下结论。不纯责任链更偏重于数据的过滤和加工,Pipeline 模式是数据的加工,并且更突出节点的状态。

总结

对于售后事件的各个环节处理,如果其中一个环节执行失败,之前的环节怎么办,有以下几种方式来解决:

  1. 增加审核环节,对整个事件审核并预判后续环节的可行性。
  2. 状态回滚,某个环节执行失败,之前的环节都回滚。
  3. 执行前对各个环节进行资格检查。
  4. 最严格的 TCC 资源锁定,一起提交一起失败。

以上四种方式各有利弊,需要根据实际场景来判定使用。

本文主要介绍知乎订单系统后端语言栈的转型升级过程,包括其间踩过的一些坑和遇到的一些问题。一来是想通过本篇文章为其它应用服务转型提供借鉴经验,二来是总结对于订单系统的理解。鉴于文字功底不足,对于业务理解不充分的地方,欢迎留言交流。文章大纲如下:

  • 迁移背景
  • 前期准备
    • 迁移方案
    • 接口验证
    • 指标梳理
  • 可用性保障
    • MTTR 快速响应
    • MTBF 降低故障率
  • 一致性保障
  • 事后总结
    • 目标回顾
    • 执行计划
    • 执行结果
    • 问题整理
  • 业务沉淀
  • 方法论实践
  • 参考文章
  • 招聘信息

迁移背景

随着知乎整体技术栈的变化,原有的 Python 技术栈逐渐被抛弃,新的 Go 和 Java 技术栈逐渐兴起。知乎交易系统的稳定性相比其它业务系统的稳定性重要很多,因为交易系统核心链路发生故障不仅会造成数据问题,还会造成严重的资损问题。
随着公司业务的不断壮大发展,交易场景变得复杂,重构和优化难以避免,因为语言特性,Python 虽然开始撸代码很爽,但是后期的维护成本慢慢变高,不过 Python 在数据分析和人工智能方向上还是有很大优势的,只是在交易领域目前看起来不太合适。从技术生态上来说,用 Java 做交易系统会更有优势,所以接下来要说的知乎订单系统语言栈转型。
另外一个因素是 Python 的 GIL 锁导致它无法发挥多核的优势,性能上受到很大限制,在实际情况中遇到过多次主线程被 hang 住导致的可用性故障,所以坚定决心来迁移掉旧系统。

前期准备

工欲善其事,必先利其器。

语言栈转型首先要明确转型的三个开发流程,即 MRO (Migration, Reconstruction, Optimization)

  • 迁移 就是把原语言代码照着抄一遍到新语言项目上,按照新语言的工程实现风格来做就可以。其间最忌掺杂代码优化和 bug 修复,会容易引起新的问题,增加验证代码的难度。
  • 重构 目的是提高项目代码的可维护性和可迭代性,让代码更优雅和易读懂,可以放到迁移完成来做。
  • 优化 通过在模块依赖、调用关系、接口字段等方面的调整来降低项目的复杂性,提高合理性。

对于语言栈转型来说,迁移流程是肯定要做的,重构和优化如何选择,可以按模块划分功能拆成子任务来分别评估方案,参考依据为现有模块如果同时优化或重构带来的直接收益和间接收益有多少。

  • 收益:完成新旧语言栈的转换,系统维护性更好,模块边界更清晰。
  • 成本:需要投入的人力成本,迁移过程中的并行开发成本,使有更高价值的工作被阻塞的损失。
  • 风险:引入新的 bug,增加测试的复杂性。

在风险可控的前提下,成本与收益要互相权衡,一般会有两种方案可供参考:第一种是锁定需求,堆人力开发上线,一步到位;第二种则是小步快走,迭代上线,分批交付。

基于以上分析,在本次转型过程中,人力成本是一个更重要的因素,所以采用只迁移的方案,来压缩人力成本,降低 bug 引入风险的同时也具有很好的可测试性。并且为了不阻塞业务需求,采用小步快走的方式分批交付,以最长两周作为一个迭代周期进行交付。

迁移方案

确定了交付方式,下面我们需要梳理当前系统中的功能模块,做好任务拆分和排期计划。知乎交易系统在迁移前的业务是针对虚拟商品的交易场景,交易路径比较短,用户从购买到消费内容的流程如下:

  1. 在商品详情页浏览
  2. 生成订单进入收银台和用户支付
  3. 确认支付后订单交付
  4. 用户回到详情页消费内容
  5. 特定商品的七天无理由退款

当时订单系统支持的功能还不多,业务模型和订单模型没有足够地抽象,梳理订单系统业务如下:

完成了订单模块的拆分后,新老系统如何无缝切换?如何做到业务无感?如何保障交易系统稳定性?出现故障如何及时止损?基于上面讲述的原则,将整个系统的迁移划分成两个阶段,迁移前后的数据存储和模型都不变。

接口验证

不论是在迁移的哪个阶段,总需要调整订单接口,可以从订单操作角度分为读操作和写操作,需要针对读接口和写接口做不同的验证方案。

写操作可以通过白名单测试以及灰度放量的方式进行验证上线,将接口未预期异常输出到 IM 工具以得到及时响应。主要的写操作相关接口有:

  • 订单的创建接口。
  • 订单绑定支付单的提交接口。
  • 用户支付后回调确认接口。
  • 用户发起退款接口。

下图展示的是 AB 平台的流量配置界面:

下图展示了部分交易预警通知消息:

读操作往往伴随在写操作中。我们利用平台的录制回放功能进行接口的一致性检查,通过对比得出差异排查问题。主要的读操作接口有:

  • 获取支付方式列表接口
  • 获取订单支付履约状态接口
  • 获取充值列表接口
  • 批量查询用户新客状态接口

下图展示的是流量录制回放系统的数据大盘:

指标梳理

监控是我们系统的『第三只眼』,可以及时反应系统的健康状况,及时发出告警信息,并帮助我们在出现故障时分析问题和快速缩小排查范围。硬件、数据库、中间件的监控已经在平台层得到支持,这里只需要梳理出应用的监控指标。

  • 日志监控:请求日志、服务端的错误日志。
  • 订单业务指标
    • 下单量、成单量、掉单量
      • 单量环比数据
    • 首次履约异常量
    • 补偿机制履约量
    • 各通知事件 P95 耗时
    • 成功履约 P95 耗时
    • 履约准时率/成功率
  • 支付业务指标
    • 支付渠道履约延迟 P95
    • 支付履约延迟 P95。
  • 用户购买完整耗时 P95。

可用性保障

在整个交付的过程中,转型前后对 SLA 要提供一致的可用性保障,可以看看下面的几个衡量标准:

一般 3 个 9 的可用性全年宕机时间约为 8.76 小时,不同系统不同用户规模对于系统可用性的要求不一样,边缘业务的要求可能会低一些,但是对于核心链路场景 TPS 可能不高,但是必须要求保证高可用级别。如何保证或者提升服务的 SLA 是我们接下来要探讨的内容,一般有下面两个影响因素:

  • MTBF (Mean Time Between Failures) 系统服务平均故障时间间隔

  • MTTR (Mean Time To Recover) 系统服务平均故障恢复时长

也就是说我们要尽可能地降低故障频率,并确保出现故障后可以快速恢复。基于这两点我们在做系统平稳过渡时,要充分测试所有 case ,并且进行灰度方案和流量录制回放,发现异常立即回滚,定位问题解决后再重新灰度。

MTTR 快速响应

持续监控

感知系统稳定性的第一步就是监控,通过监控来反映系统的健康状况以及辅助定位问题,监控有两个方向:

第一个方向是指标型监控,这里监控是在系统代码中安排各种实时打点,上报数据后通过配置报表呈现出来的。

  1. 基础设施提供的机器监控以及接口粒度的响应稳定性监控。
    1. 物理资源监控,如 CPU、硬盘、内存、网络 IO 等。
    2. 中间件监控,消息队列、缓存、Nginx 等。
    3. 服务接口,HTTP、RPC 接口等。
    4. 数据库监控,连接数、QPS、TPS、缓存命中率、主从延迟等。
  2. 业务数据层面的多维度监控,从客户端和服务端两个角度来划分。
    1. 从客户端角度来监控服务端的接口成功率,支付成功率等维度。
    2. 从服务端角度从单量突变、环比变化、交易各阶段耗时等维度持续监控。

以上两点基于公司的 statsd 组件进行业务打点,通过配置 Grafana 监控大盘实时展示系统的健康状况。

第二个方向是日志型监控,这主要依赖公司的 ELK 日志分析平台和 Sentry 异常捕获平台。通过 Sentry 平台可以及时发现系统告警日志和新发生的异常,便于快速定位异常代码的发生位置。ELK 平台则可以将关键的日志详细记录下来以便于分析产生的场景和复现问题,用来辅助修复问题。

异常告警

基于以上实时监控数据配置异常告警指标,能够提前预知故障风险,并及时发出告警信息。然而达到什么阈值需要告警?对应的故障等级是多少呢?

首先我们要在交易的黄金链路上制定比较严格的告警指标,从下单、提单、确认支付到履约发货的每个环节做好配置,配置的严重程度依次递增分为 Info、Warning、Critical。按照人员类别和通知手段来举例说明告警渠道:

IM 中的预警消息截图如下:

订单主要预警点如下:

  • 核心接口异常
  • 掉单率、成单率突变
  • 交易各阶段耗时增加
  • 用户支付后履约耗时增加
  • 下单成功率过低

MTBF 降低故障率

系统监控告警以及日志系统可以帮我们快速的发现和定位问题,以及时止损。接下来说的质量提升则可以帮助我们降低故障发生率以避免损失,主要从两个方向来说明:

规范化的验收方案

① 开发完成包括逻辑功能和单元测试,优先保证单测行数覆盖率再去保证分支覆盖率。然后在联调测试环境中自测,通过后向 QA 同学提测。
② QA 同学可以在测试环境下同时进行功能验收和接口测试,测试通过后便部署到 Staging 环境。
③ 在 Staging 环境下进行功能验收并通过。
④ 灰度交付以及双读验证可以根据实际情况选择性使用。
⑤ 上线后需要最后进行回归测试。

统一的编码规约以及多轮 CR 保障

代码上线前一般至少要经过两次代码评审,太小的 MR 直接拉一位同事在工位 CR 即可,超过百行的变更需要拉会研讨,两次评审的关注点也不同。

第一次评审应关注编码风格,这样可以避免一些因在写法上自由发挥而带来的坑,以此来沉淀出组内相对统一的编码规约,在编码的稳定性上建立基本的共识,提升代码质量。

第二次评审应关注代码逻辑,这里有个需要注意的点是,如果明确只做迁移,那么其间发现旧逻辑难理解的地方不要随便优化,因为在不了解背景的情况下很有可能会写一个 bug 带上线(这种事见过好几次)。另外这样也好去对比验证,验证通过上线后再去优化。

只有通过明确目的和流程并且遵循这个流程做,才能更快更好地交付有质量的代码。

一致性保障

每一个微服务都有自己的数据库,微服务内部的数据一致性由数据库事务来保障,Java 中采用 Spring 的 @Transtaction 注解可以很方便地实现。

而跨微服务的分布式事务,像 支付、订单、会员三个微服务之间采用最终一致性,类似 TCC 模式的两阶段提交,订单通过全局发号器生成订单 ID,然后基于订单 ID 创建支付单,如果用户支付后订单会变更自身状态后通知会员微服务,履约成功则事务结束,履约失败则触发退款,如果用户未支付,那么订单系统将该订单以及支付单做关单处理。

对应一致性保障,我们对订单接口做了两个方面的处理:

分布式锁

对于上游的支付消息监听、支付 HTTP 回调、订单主动查询支付结果三个同步机制分别基于订单 ID 加锁后再处理,保证同步机制不会被并发处理。

接口幂等

加锁后对订单状态做了检查,处理过则响应成功,否则处理后响应成功,保证上游消息不会被重复处理。

订单对于下游的履约,是通过订单 ID 作为幂等 key 来实现的,以保证同一个订单不会被重复履约,并且通过 ACK 机制保证履约后不会再重复调到下游。

其中分布式锁采用 etcd 锁,通过锁租约续期机制以及数据库唯一索引来进一步保障数据的一致性。

补偿模式,虽然我们通过多种手段来保证了系统最终一致,但是分布式环境下会有诸多的因素,如网络抖动、磁盘 IO、数据库异常等都可能导致我们的处理中断。这时我们有两种补偿机制来恢复我们的处理:

带惩罚机制的延时重试

如果通知中断,或者未收到下游的 ACK 响应,则可以将任务放到延迟队列进行有限次的重试,重试间隔逐次递增。最后一次处理失败报警人工处理。

定时任务兜底

为了防止以上机制都失效,我们的兜底方案是定时扫描异常中断的订单再进行处理。如果处理依然失败则报警人工处理。

事后总结

目标回顾

目标一:统一技术栈,降低项目维护成本。目标结果是下线旧订单系统。
目标二:简化下单流程,降低端接入成本。目标结果是后端统一接口,端上整合 SDK。

执行计划

迁移的执行总共分成了三个大阶段:

  • 第一阶段是迁移逻辑,即将客户端发起的 HTTP 请求转发到 RPC 接口,再由新系统执行。第一阶段做到所有的新功能需求都在新系统上开发,旧系统只需要日常维护。

  • 第二阶段是通过和客户端同学合作,迁移并整合当前知乎所有下单场景,提供统一的下单购买接口,同时客户端也统一提供交易 SDK,新组件相对更加稳定和可监控,在经过灰度放量后于去年底完全上线。第二阶段做到了接口层的统一,更利于系统的维护和稳定,随着新版的发布,旧接口流量已经变得很低,大大降低了下阶段迁移的风险。

  • 第三阶段是旧 HTTP 接口迁移,由新系统承载所有端的请求,提供相同规格的 HTTP 接口,最后通过修改 NGINX 配置完成接口迁移。第三阶段迁移完成后旧系统最终实现了下线。

执行结果

截至此文撰写时间,语言栈已经 100% 迁移到新的系统上,旧系统已经完全下线,总计下线 12 个系统服务, 32 个对外 HTTP 接口,21 个 RPC 接口,15 个后台 HTTP 接口。

根据 halo 指标,迁移前后接口 P95 耗时平均减少约 40%,硬件资源消耗减少约 20%。根据压测结果比较,迁移后支撑的业务容量增长约 10 倍。

系统迁移完成只是取得了阶段性的胜利,接下来系统还需要经过一些小手术来消除病灶,主要是以下几点:

  1. 不断细化监控粒度,优化告警配置,继续提高服务的稳定性。
  2. 对于 Python 的硬翻译还需要不断重构和优化,这里借鉴 DDD 设计思想。
  3. 完善监控大盘,通过数据驱动来运营优化我们的流程。
  4. 项目复盘总结以及业务普及宣讲,提升人员对于业务细节的认知。

问题整理

迁移总是不能一帆风顺的,其间遇到了很多奇奇怪怪的问题,为此头发是真没少掉。

问题 1:迁移了一半新需求来了,又没有人力补上来怎么办?

迁移后再做重构和优化过程,其实很大一部分考量是因为人力不足啊,而且现状也不允许锁定需求。那么只能写两遍了,优先支持需求,后面再迁移。如果人力充足可以选择一个小组维护新的系统一个小组维护旧的系统。

问题 2:我明明请求了,可日志怎么就是不出来呢?

不要怀疑平台的问题,要先从自身找问题。总结两个原因吧,一个是新旧系统的迁移点太分散导致灰度不好控制,另一个是灰度开关忘记操作了,导致流量没有成功导到新系统上。这里要注意一个点就是在迁移过程中要尽可能的快速交付上线。

问题 3:公司 Java 基础服务不够完善,很多基础平台没有支持怎么办?

于是自研了分布式延迟队列、分布式定时任务等组件,这里就不展开聊了。

问题 4:如何保证迁移过程中两个系统数据的一致性?

首先我们前面讲到的是系统代码迁移,而数据存储不变,也就是说两个系统处理的数据会存在竞争,解决的办法是在处理时加上分布式锁,同时接口的处理也是要幂等的。这样即使在上下游系统做数据同步的时候也能避免竞争,保证数据的一致性。
就用户支付后支付结果同步到订单系统这一机制来说,采用推拉的机制。
① 用户支付后订单主动轮询支付结果,则是在主动拉取数据。
② 支付系统发出 MQ 消息被订单系统监听到,这是被动推送。
③ 支付成功后触发的订单系统 HTTP 回调机制,这也是被动推送。
以上三种机制结合使用使得我们系统数据一致性有一个比较高的保障。我们要知道,一个系统绝非 100% 可靠,作为交易支付的核心链路,需要有多条机制保证数据的一致性。

问题 5:用户支付后没有收到会员权益是怎么回事?

在交易过程中,订单、支付、会员是三个独立的服务,如果订单丢失了支付的消息或者会员丢失了订单的消息都会导致用户收不到会员权益。上一个问题中已经讲到最终一致性同步机制,可能因为中间件或者网络故障导致消息无法同步,这时可以再增加一个补偿机制,通过定时任务扫描未完成的订单,主动检查支付状态后去会员业务履约,这是兜底策略,可保障数据的最终一致。

业务沉淀

从接收项目到现在也是对订单系统从懵懂到逐渐加深理解的一个过程,对于当前交易的业务和业务架构也有了一个理解。

交易系统本身作为支付系统的上层系统,提供商品管理能力、交易收单能力、履约核销能力。外围业务子系统主要关注业务内容资源的管理。业务的收单履约管理接入交易系统即可,可减轻业务的开发复杂度。收单流程展示如下:

  1. 业务定制商品详情页,然后通过详情页底栏调用端能力进入订单收银台。在这里客户端需要调用业务后端接口来获取商品详情,然后调用交易底栏的展示接口获取底部按钮的情况。
  2. 用户通过底部按钮进入收银台后,在收银台可以选择支付方式和优惠券,点击确认支付调起微信或者支付宝付款。收银台展示以及获取支付参数的接口由交易系统提供。
  3. 订单后台确认收款后会通知业务履约,用户端会回到详情页,用户在详情页进入内容播放页享受权益。履约核销流程是业务后端与交易系统后端的接口调用来完成的。

现在知乎站内主要是虚拟商品的交易,一个通用的交易流程如下图:

用户经历了从商品的浏览到进入收银台下单支付,再回到内容页消费内容。随着业务的发展,不同的交易场景和交易流程叠加,系统开始变得复杂,一个交易的业务架构慢慢呈现。

订单系统主要承载知乎站内站外的各种交易服务,提供稳定可靠的交易场景支撑。主要分为以下几个部分:

  1. 首先产品服务层是面向用户能感受到的交互界面,提供对于这些页面的统一下单支付 API 网关。
  2. 然后是订单服务层,由上层网关调用,提供着不同场景下的交易服务支撑。
  3. 再往下是订单领域层,承载订单最核心逻辑代码,首先是用户购买需要的算价聚合,然后是管理订单模型的交易聚合,最后是买完商品后的履约处理的交付聚合。
  4. 最底层是基础支撑服务层,主要是提供基本的服务支持以及交易依赖的一些服务。
  5. 最后是运营服务,提供交易相关的后台功能支持。

方法论实践

凡此以上,不论系统迁移方案还是架构理解都归结于参与人员的理解与认知,一个优秀的方案或合适的架构不是设计出来的,是迭代出来的。人的认知也是这样,需要不断的迭代升级,和很多的方法论一样,PDCA 循环为我们提炼了一个提升路径。

  • Plan 计划,明确我们迁移的目标,调研现状指定计划。
  • Do 执行,实现计划中的内容。
  • Check 检查,归纳总结,分析哪些做好了,还有什么问题。
  • Action 调整,总结经验教训,在下一个循环中解决。

很多时候,也许你只做了前两步,但其实后两步对你的提升会有很大帮助。所以一个项目的复盘,一次 Code Review 很重要,有语言的交流和碰撞才更容易打破你的固有思维,做到业务认知的提升。

参考文章

https://mp.weixin.qq.com/s/eKc8qoqNCgqrnont2nYNgA
https://zhuanlan.zhihu.com/p/138222300
https://blog.csdn.net/g6U8W7p06dCO99fQ3/article/details/103415254

招聘信息

知乎技术团队大量岗位持续招聘中,欢迎感兴趣的同学加入我们,可投简历至:luohuijuan@zhihu.com

本文是对于订单建模的一点思考整理,里面应用一些对领域驱动设计的思考,默认读者对于领域概念有一些基本了解。

基本元素

交易最早是通过以物易物的方式来交换,后面产生等价通用物品即货币。交易上下文领域对象包括:

1
2
3
4
顾客 Consumer
货 Goods
货币 Currency
商家 Merchant

对于交易一句话描述就是,顾客在商家那里通过等价货币购换取了货物。

上面描述的四种领域对象作为领域实体,每一种领域实体通过 key 组合来确定实体的唯一性,ext 是值对象,用来描述实体。对应的实体描述如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Consumer:
{
"key": "USER_ID",
"ext": { }
}
Goods
{
"key": "SKU_ID",
"amount": 100,
"ext": { }
}
Currency
{
"type": "CNY|VIRTUAL_COIN",
"amount": 100
}
Merchant
{
"key": "yO7LmcWjqsJz1N5j",
"ext": { }
}

状态事件抽象

订单状态变更命令通用流程抽象如下图,一个领域命令被触发后,首先进行状态变更前的资格校验事件回调,所有校验方均校验通过后方可进行后续处理,将交易修改为对应状态,状态修改后发布对应的状态变更事件,经由事件总线发布事件,所有监听方进行后续处理。一个领域命令执行结束。此流程可以套用到任何状态变更命令。

状态变化通过领域命令发起 command,首先进行 BeforeOperating 操作前的处理,每一个命令都定义了事件的资格校验 qualification 接口,实现接口的可能是子域内处理,比如下单的库存校验,优惠校验等,也可能是具体业务的校验,需要实现资格校验接口。通过后进行事务性的状态变更操作 updateState,操作后进行状态变更后 AfterOperating 的事件发布,由各个业务监听进行后续处理。这样处理方便业务逻辑解耦,订单子域专注于订单状态的管理。

对应到订单系统,对于订单状态和物流状态节点以及领域事件如下表示:

订单状态

  • 已创建 CREATE(COMMIT)
  • 已支付 PAID
  • 已退款 REFUND(REFUNDING)
  • 已关闭 CLOSED

物流状态

  • 待发货 WAITING
  • 已发货 DELIVERED
  • 已收货 RECEIVED
  • 已退货 RETURN

领域命令

  • 创建订单 createOrder
  • 超时关单 closeOrder
  • 支付订单 payOrder
  • 申请退款 refundOrder
  • 订单履约 deliverOrder
  • 商家发货 deliverGoods
  • 用户收货 receiveGoods
  • 用户退货 returnGoods

子域拆分

订单子域的拆分也是业务职责的拆分,订单管理和物流管理是相对独立的两个模块,订单主要关注收款履约退款,物流主要关注用户的收退货状态,所以整个交易域的订单和物流会被拆分成两个子域。

订单事件举例

订单创建命令,首先对注册的条件回调方进行回调,确认当前的购买资格,通过后进行事务性的订单创建,创建后进行订单已创建的事件发布,所有监听方接收到消息后进行分析处理。

BPMN 有什么优势呢,用了一段时间主要使用在业务流程表达上,表达符号比较多,相比流程图可以更清楚的表达业务流程,同步、异步,异常中断、事件消息等等,如果看图的人都对这些符号有概念,可以比较轻松的看懂业务流程。缺点就是符号太多,学习成本相对高一些。学会了就会对业务表达上有很好的助力。

范围

BPMN仅限于支持对业务流程有用的建模概念。这意味着组织所做的非业务目的其他类型建模将排除在BPMN之外。例如,以下方面的建模不属于BPMN的一部分:

  • 组织结构
  • 职能分解
  • 数据模型

此外,虽然BPMN会显示数据的流(消息)以及活动与数据器物的关联,但它并非数据流图(data flow diagram)。

要素

BPMN用很小一套图形要素做简单的图来建模,这将令业务用户与开发者一样容易理解其中的过程和流。它的四种基本要素如下:

  1. 流对象(Flow Object):事件(Events),活动(Activities),关口(Gateways)
  2. 连接对象(Connecting Objects):顺序流(Sequence Flow),消息流(Message Flow),关联(Association)
  3. 泳道(Swimlanes):池(Pool),道(Lane)
  4. 器物(Artifacts/Artefacts):数据对象(Data Object),组(Group),注释(Annotation)

这四大类对象令我们有机会做出简单的业务流程图(BPD, business process diagram)。同时,BPMN也允许在BPD中创建你自己的流对象、器物类型,使图更好理解。

流对象与连接对象

泳道与器物

业务流程图的类型

常用符号

免费画 BPMN 流程图的工具

https://app.diagrams.net/

参考

https://github.com/Pingren/bpmn-dataflow-diagram-editor
https://github.com/zhangqiangboss/WorkflowAndCamunda/blob/master/docs/camunda/CamundaProEngine.md
https://www.edrawsoft.cn/bpmn-symbols/