因为工作不可避免的要面对单据需要被修改的问题,而如何存储历史信息与终态信息就成了一大难题。

正好节前有客户提了一个相关的 bug,笔者花了一段时间准备了一些方案准备节后找全组的服务端进行 review 讨论,顺手就写篇博客聊聊吧。

背景 链接到标题

无论是 OA 审批单还是各大问卷系统的问卷,都会提供给用户一个提交后修改的功能,可能有些场景下历史版本信息并不重要,只需要保留一份终态信息就可以了,但在 OA 审批场景下就不一样了,每一次审批单上的内容和审批流的内容都是对于用户来说十分重要的,所有的一切都是应该可以溯源的,因此我们必须存储下来历史版本信息。

一些企业通常会有如下的开发过程:一开始设计的时候并没有开发修改的功能,所以服务端只需要存储用户提交的每一个单据数据即可,后来把修改功能开发出来后就设计了一套附属单规则,用户修改后就存储一份附属单,在附属单数据的关联主单字段里写一个主单唯一标识,并且在主单数据里填好附属单信息,保存好最新附属单的唯一标识,这样在查询的时候只需要都拉主单,遇到有附属单的主单再拉一遍最新附属单后在内存里进行合并即可。

可上面这种解决方式存在一个巨大的问题,就是在附属单里改变了查询条件后会出现漏查和多查的问题。

例如:用户提交了条件 1 里填 A 的主单,而后又提交了一个对应的附属单,把条件 1 里面的内容改成了 B,此时我们拉去条件 1 为 A 的主单时是能够拉出来上述的表单的,但在拉去并合并附属单时这个单据的条件 1 就变成了 B,于是就出现了用条件 1 为 A 筛选时出现了一条为 B 的记录,而用条件 1 为 B 筛选时则找不到这条记录,这可以说是一个十分巨大的问题了。

解决方案 链接到标题

最容易想到的方案就是在第一次拉取数据的时候不管主单还是附属单都拉出来,然后主单去找对应的附属单合并一把,附属单找对应的主单合并一把,最后内存中再筛选一把得到结果。

但在分页这个前提下有可能会出现一个单子在前后两页里重复出现的问题,即主单在第一页里被拉出来了,合并附属单后展示了出来,附属单在第二页里被拉了出来,合并主单后展示了出来,这两条数据就重复了。

因此在只改变查询逻辑这一前提之下解决就变得有些困难了。

方案 1 链接到标题

  1. 先和现有逻辑一样,拉一把满足条件的主单,然后拉对应的附属单进行合并。
  2. 根据条件内存过滤第一步得到的结果。
  3. 进行一次不分页满足条件的附属单拉取(因为评估下来附属单的数据是极少的),找到对应的主单(此步可以和第二步结果进行去重),判断是不是最新的附属单,不是的话就跳过,是的话就合并保存到待选列表中。
  4. 将第四步得到的待选列表中的数据凭借排序的条件插入到第二步得到的结果中,在第二步结果首尾条件外的就抛弃。

这一方案虽然可以完美解决前面提到的漏查、多查和相邻页里重复的问题,但逻辑上其实实现起来十分复杂,不利于后期维护。

方案 2 链接到标题

  1. 拉取一把所有满足条件的单据数量。
  2. 根据第一步得到的数量来决定后续操作是在主库执行还是在备库执行(减少慢 SQL 对系统的影响)。
  3. 拉去所有满足条件的单据。
  4. 最新附属单不在范围内的主单、不是最新附属单的附属单都需要被过滤掉。
  5. 是附属单但主单不在第三步得到结果里的就再拉一遍对应的主单。
  6. 最后合并各种单据,进行内存中的排序和分页。

这一方案虽然较方案 1 简洁了一些,但一次性拉去所有满足条件的数据过于粗暴了,如果没有主备分库、分库分表、高效索引这三层优化架构在的话分分钟就会把系统玩炸,这里就写出来给大家了解一下,几乎没有实用价值。

方案 3 —— 可能的银弹 链接到标题

其实所谓的银弹反而是大家一开始第一直觉就能想到的那个方案。

假如我们的存储不是把主单和附属单都存下来,而是得到附属单的时候把对应内容盖到主单数据上的话,是不是后续的查询就简单的不能再简单了?

那反过来,我们再弄一张拉链表存储历史信息,是不是也能把溯源这一需求解决了?

因此最好的方案就是拿一张表存储单据的终态信息,保留现在在的表用于溯源,这一切就变得十分简单了,唯一要改动的就是落库的逻辑了(其实这里也不是特别好改,但对比这个方案带来的效果来说这就不算什么了)。

这一方案也是很典型的用空间换时间,多存储一份数据可能会带来一笔不小的成本,但对我们开发和维护来说却是十分有帮助,这就需要看架构师对系统当前状态的评估来做选择了。

总结 链接到标题

其实这个问题是很典型的系统发展中遇到的问题,一开始根本没设计修改这一功能,后来做了之后就怎么快怎么做,把查询过程简单兼容了一下就好了,很长一段时间里客户也不怎么用修改这一功能,直到修改这一功能被客户充分使用后,之前存在的问题才浮出水面,这也是不可避免的了。

没有哪个架构师可以站在当前完美预测未来,上各种架构一番折腾可能的结果就是目标用户一个亿,但还没到一万呢这个产品就暴死了,甚至有可能暴死的原因就是一开始架构设计太复杂导致成本过高了。

因此一段时间的架构学习下来,笔者觉得架构师更像是一个做题平衡成本与性能的人,只要架构在一段时间内可以完美解决系统的问题这就是一个好架构,遇到问题了我们再解决就是了。