之前,在用ENode开发forum案例时,遇到了关于如何实现论坛帖子的回复的统计信息如何更新的问题。后来找到了自己认为比较合理的解决方案,分享给大家。也希望能和大家交流,擦出更多的火花。

论坛核心领域问题分析

论坛领域的核心概念是:帖子、回复。大家都知道,一个帖子可以有零个或多个回复。对同一个帖子,不同的人可以并行发表回复。回复发表后,查看帖子详情时,可以根据回复的发表时间排序显示;此外,我们还关心某个帖子的最新发表的回复、最新回复的作者、最新回复时间,以及总回复数。

我们设计的系统,应该在实现上述的领域问题的前提下,尽量做到发表回复时要快,且能保证帖子能对它的所有回复的统计信息能正确的统计出来。

方案1

把帖子设计为聚合根,回复设计为帖子的子实体。然后发表回复就是在帖子聚合根里添加一个回复实体。

优点:

  1. 模型清晰并符合人们对领域的一般认识,帖子和回复1对多,很自然想到这个模型设计;
  2. 帖子内部就已经聚合了所有的回复信息,数据强一致性,所以统计信息自然不用担心;
  3. 在DB层面,当发表一个回复时,先插入回复,再更新帖子回复统计信息,两个步骤在一个数据库事务里,确保了数据强一致性。

缺点:

  1. 当很多人同时对同一个帖子进行回复时,也就是回复的并发很高时,会被阻塞;因为每个回复都要同步更新帖子的回复统计信息;
  2. 帖子由于聚合了所有的回复,所以会导致帖子本身比较重;必须要求ORM框架支持延迟加载,否则获取帖子会成为一个问题;比如,我仅仅为了修改帖子的内容,而要把整个帖子的回复都加载出来,会付出很多不必要的代价。

这种方案,大部分情况下都没问题。因为大部分论坛,对一个帖子的回复的并发都不会太高。所以,我觉得设计总体来说是可行的。

方案2

把帖子和回复都设计为聚合根,回复不再是帖子内的子实体。发表回复就是新增了一个回复聚合根。

优点:

  1. 帖子聚合根比较轻量级了,因为它内部不在维护回复了;
  2. 高并发创建回复时,不会再有性能问题。但前提是,创建回复后,更新帖子的统计信息必须采用异步的方案,否则如果也是像方案1那样采用同步+事务的方式,那DB层面还是成为瓶颈,并发上不去;

缺点:

  1. 模型一般人理解有点困难,大部分人会问的一个问题是,为何回复不是帖子的子实体,回复不是离开帖子后就没意义了吗?这个问题,这里先不做讨论,我之前的文章中有讨论过这个问题。
  2. 本质问题和方案1类似;如果采用同步更新统计信息,那并发也是上不去,只是模型的设计改变了一下;如果采用异步更新统计信息,那就是消息驱动的架构,就需要考虑消息的丢失、幂等、乱序等问题;比如消息丢失,那统计信息最终就不正确;假如消息重复被处理(分布式消息队列一般不会保证消息绝对不会被重复投递),那统计信息也不正确;假如消息的处理顺序乱序了,那最后的统计信息也会不对;开发人员需要考虑到这些问题。当然,如果你不care,也可以,呵呵。

这种方案,模型层面做了一些变化,DB层面,引入了异步更新统计信息的思想,但要求技术上需要处理EDA架构所带来的典型问题。如果论坛的并发问题确实影响了用户的体验,则可以尝试考虑次方案;

方案3

模型层面,设计为两个聚合根还是一个聚合根无所谓。然后统计信息决定不保存,也就是不冗余存储统计信息了。大家知道,统计信息,一般只是用来展示数据使用,并不会参与到业务逻辑中去。所以,理论上我们不保存统计信息也可以,因为我们总是可以在需要的时候动态查询统计出所需要的信息,SQL的统计功能是很强大的,呵呵。

优点:

  1. 无需冗余存储统计信息,设计简单;
  2. 发表回复时也无并发问题;

缺点:

  1. 需要付出更多的查询代价,尤其是在论坛数据量大,查询并发高的时候;而且还要根据统计信息的结果进行分页的话,数据量一大,性能一定比较糟糕。当然,我们还有办法,比如分库分表,减少单表的数据量,确保查询性能;或者,总是只支持查询近期2周的数据,历史数据不显示,必须通过其他方式查看。这样的话,也可以控制活跃数据在一定的数量级之内;
  2. 上面提到的分库分表,方案显得有点重;只保显示近期活跃的数据相当于牺牲了一部分业务功能,换来更高的查询性能;只要业务上能接受,就可行;

这种方案,我觉得需要业务人员和设计人员仔细评估考量。大家觉得如何呢?

方案4

这种方案,一般老外的开源论坛中出现的较多。领域模型的设计有较大不同,因为对论坛核心领域的认识有所不同。当用户发帖时,我们把帖子的标题和内容看成两个部分,标题叫thread,内容是属于这个thread下的第一个message;然后对这个帖子的回复,看成是第二个message。所以,通过这样的理解,thread还是可以理解为帖子,但其含义和我们通常所理解的帖子稍微有点不同,因为这个帖子仅仅只有标题没有内容,它的作用就是“穿针引线”,thread的英文意思就是线索、穿成串的意思。可见,一个thread就是对很多message的串联,我们也可以把thread理解为一个主题,这个主题下有若干个讨论内容。然后一个message,即消息,就表达了一个内容。所以,当用户发帖时,就是会生成一个thread,以及一个message,两个聚合根;当用户发表回复时,就是只是创建一个message聚合根。另外,也很明显thread应该维护所有的回复的统计信息,因为我们设计它的目的也在于此(串联message,以及维护message统计信息)。然后,message就是简单的表达某个内容即可,同时message上记录当前自己所属的threadId即可。好像说的有点啰嗦,呵呵!

上面这个描述的是我们对论坛核心领域有不同的认识,最后设计出来的领域模型也完全不同。所以,我们发现,当我们在做DDD领域驱动设计时,往往每个人最后设计出来的领域模型是不同的,因为每个人对同一个领域的问题的本质理解不同。甚至可以说,这个是每个人的世界观的不同导致。虽然这样,但DDD的最大价值在于会要求我们主动去思考领域,思考如何用模型,从OO的角度去思考问题,思考状态的一致性维护,状态变化的规则,等。相比传统数据库驱动的方式、面向过程的方式,脑子里只有数据结构、关系、以及过程。没有对象、交互、职责这方面的思考意识。

我承认,这个领域模型的设计确实不错,甚至更好!但我们不能说,前面的领域模型的设计(帖子包含标题和内容、回复只有内容)是错误的,因为只是对领域问题的不同理解而已。前面的领域模型的设计,也是自然合理的,我认为。

当然,讨论回来,在DB层面,不管领域模型如何设计,我们在更新帖子统计信息时,还是会碰到并发的情况。这个其实是多用户并发回帖导致的技术问题,不是业务上的问题。技术问题就是需要通过技术手段去解决。当然,有时也可以把某些看似是技术问题的问题,提炼出合适的业务规则,通过聚合根封装业务规则,确保数据一致性的思路来解决。下面我们来看一下方案5。

方案5

基于ENode框架实现,采用CQRS架构。领域模型的设计,也是设计为帖子和回复两个聚合根。不同的是帖子中聚合了回复的统计信息,设计一个值对象,表示帖子的当前回复的统计信息。包括:最近回复的ID、作者、回复时间,以及总回复数。为什么要这样设计?因为经过对领域进一步的分析和思考,发现了领域内的一个潜在的业务规则。就是我们关心帖子的“最后”的回复信息。关键问题就是在这个“最后”两个字上。既然是最后,那就必须要知道哪个是最后,根据什么判断?就是根据回复的创建时间。然后,在高并发创建回复的时候,我们需要有一个地方,可以准确的统计出这个最后的回复是哪个。前面几个方案,要么通过DB的强一致性事务保证,要么依赖消息队列的顺序消息处理(必须依靠开发人员要处理好各种EDA的问题才行)。这些方案虽然最终却是能统计出这个最后的回复信息;但我认为,这个是属于领域内的一个业务规则;这个规则是由于我们所关心的统计信息而必须引入的。所以,更好的方案应该是用聚合根来维护这个业务规则。

所以,帖子本身可以不用聚合所有的回复信息,因为帖子本身确实不关心回复信息本身,它只是关心回复的统计信息(最后一个回复的信息以及总回复数);因此,我们设计帖子聚合时,只需要再让其内聚一个包含回复统计信息的值对象即可。

当有一个新的回复产生后,发送一个命令,通知回复的帖子更新其统计信息;然后帖子处理这个命令时,内部判断当前回复的时间是否是最新时间,如果是,则更新最后回复信息和总回复数;如果不是,则只更新回复数。然后帖子的统计信息更新后,产生领域事件,表示帖子的统计信息有更新,然后CQRS的event handler,根据领域事件,更新读库帖子表的那几个统计字段即可;

这种方案,通过ENode提供的技术保证,可以确保消息至少投递一次,确保消息的幂等处理,以及消息(领域事件)的顺序处理,以及最重要的一点,最同一个聚合根,确保尽量做到了无并发操作;就算出现并发,也能框架层面自动重试。所以,开发人员就不用再关心这些技术相关的问题了。

个人认为,这种方案在基于ENode框架引入CQRS架构的异步思路,解决高并发的问题的同时,进一步挖掘了业务需求,分析出了潜在的业务规则,通过用聚合根来维护这个业务规则,最终确保了数据的最终一致性;缺点是,依赖了ENode框架。对我个人来说,肯定是最喜欢这种方式,呵呵。目前enode forum案例,就是采用这种方式来实现帖子的回复统计信息的维护和存储。

结束语

我觉得很多问题的解决思路都是类似的。我总喜欢使用大家都通俗易懂的案例来分析、讨论。因为只有大家对这个业务都比较理解,才具有讨论的基础。另外,我相信每个人都有举一反三的能力。一旦我们把某个业务问题分析清楚,那也许遇到其他类似的业务问题时,我们曾经的业务问题分析经验会帮助到我们,从而可以更加快速的理解和解决相似问题领域。这也是我为什么要花精力写出这么多方案的原因。多思考一点,多深入一点。自己就会多成长一点,对未来的领域问题的分析就会更快速一点。

不知不觉又下半夜了,眼睛酸,不写了,基本把能想到的都写了,呵呵。

DDD实践问题之 - 关于论坛的帖子回复统计信息的更新的思考的更多相关文章

  1. DDD实践反思

    某大型互联网公司于2019年开始在XX中台财务域进行DDD实践.事后回顾,整体并没有达到预期的效果,个人也做了很多的反思和总结,形成此文. 1. 背景 为什么当时要实践DDD?其中的缘由比较复杂,可以 ...

  2. DDD实践切入点(二)

    最近发现下面关于上下文的理解有些问题,不太好改,暂时先不改了 承前:大型系统的支撑,应用系统开发思想的变迁,DDD实践切入点(一) 从大比例结构入手已经开始了系统的建设,大家都知道需求是会不断变化不断 ...

  3. DDD实践案例:引入事件驱动与中间件机制来实现后台管理功能

    DDD实践案例:引入事件驱动与中间件机制来实现后台管理功能 一.引言 在当前的电子商务平台中,用户下完订单之后,然后店家会在后台看到客户下的订单,然后店家可以对客户的订单进行发货操作.此时客户会在自己 ...

  4. DDD实践2

    DDD实践切入点(二) 承前:大型系统的支撑,应用系统开发思想的变迁,DDD实践切入点(一) 从大比例结构入手已经开始了系统的建设,大家都知道需求是会不断变化不断深入的,刚开始自然是模糊的大比例结构对 ...

  5. DDD实践(一)

    DDD实践切入点(一) 前两篇:大型系统的支撑,应用系统开发思想的变迁 之前大致说了使用DDD的前期准备,现在可以真正开始实践了,以我刚刚结束的一个简单的经典DDD方式的项目为例子,当然由于比较简单, ...

  6. 【pyhon】理想论坛单帖爬虫取得信息存入MySql数据库

    代码: # 单帖爬虫,用于爬取理想论坛单个帖子得到发帖人,发帖时间和回帖时间并存入数据库,url例子见main函数 from bs4 import BeautifulSoup import reque ...

  7. Beautifulsoup提取特定丁香园帖子回复

    DataWhale-Task3(Beautifulsoup爬取丁香园) 简要分析 完整代码 结果图 参考资料 简要分析 任务3:爬取丁香园论坛特定帖子,包括帖子主题,帖子介绍,回贴内容(用户名,用户头 ...

  8. 【DDD】领域驱动设计实践 —— 业务建模实例(‘发布帖子’)

    本文是基于上一篇‘业务建模小招数’的实践,后面的多篇博文类似.本文主要讲解‘发表帖子’场景的业务建模,包括:业务建模.业务模型.示例代码:示例代码会使用java编写,文末附有github地址.相比于& ...

  9. DDD实践切入点(一)

    前两篇:大型系统的支撑,应用系统开发思想的变迁 之前大致说了使用DDD的前期准备,现在可以真正开始实践了,以我刚刚结束的一个简单的经典DDD方式的项目为例子,当然由于比较简单,所以很多时候会脱离它来介 ...

随机推荐

  1. Android-Spinner [使用C# And Java实现]

    效果如下: C#实现代码 using Android.App; using Android.OS; using Android.Widget; namespace SpinnerDemo { [Act ...

  2. 使用base.调用父类里面的属性

    使用base.调用父类里面的属性public class parent { public string a; }public class child :parent { public string g ...

  3. Protocol Buffer搭建及示例

    本文来源:http://www.tanhao.me/code/150911.html/ Protocol Buffer(简称Protobuf或PB)是由Google推出的一种数据交换格式,与传统的XM ...

  4. Domino----The Address Book does not contain a cross certificate capable of validating the public key.

    The Address Book does not contain a cross certificate capable of validating the public key. 地址本不包含交叉 ...

  5. WebRTC音频预处理单元APM的整体编译及使用

    正文 行的gnu静态库链接路径是针对NDK版本 r8d 的,如读者版本不匹配,请自行找到 libgnustl_static.a 静态库的路径进行替换. 3)本示例并不打算编译 WebRTC 的测试工程 ...

  6. ODAC (odp.net) 从开发到部署

    2013-09-30 16:08 4097人阅读 评论(0) 收藏 举报  分类: Oracle(10)  版权声明:本文为博主原创文章,未经博主允许不得转载. 1. 确定你开发机和服务器的操作系统是 ...

  7. Angular内置指令(一)

    要注意的是不要把自己开发的指令以ng开头,以免与内置指令冲突  目录:ng-disabled,ng-readonly,ng-checked,ng-selected,ng-href,ng-src,ng- ...

  8. 北京电子科技学院(BESTI)实验报告5

    北京电子科技学院(BESTI)实验报告5 课程: 信息安全系统设计基础 班级:1452.1453 姓名:(按贡献大小排名) 郑凯杰.周恩德 学号:(按贡献大小排名) 20145314.20145217 ...

  9. 一、PID控制原理

    在模拟控制系统中,控制器最常用的控制规律是PID控制.模拟PID控制系统原理框图如下图.系统由模拟PID控制器和被控对象组成. PID控制器是一种线性控制器,它根据给定值Yd(t)与实际输出值Y(t) ...

  10. mac osx Forbidden You don't have permission to access / on this server解决方法

    (1)首先查看*.conf 是否有读写权限,如果没有要将文件赋予读写权限,比如 localhost.conf (2)再查看/Users/username/Sites/localhost/文件夹是否有i ...