美文网首页程序员
高性能系统设计:互联网点赞系统设计及实践

高性能系统设计:互联网点赞系统设计及实践

作者: you的日常 | 来源:发表于2022-04-15 10:01 被阅读0次

    1 什么是点赞系统

    点赞是互联网中常见的交互方式,系统根据用户的点赞操作来跟踪用户的行为,并对用户的喜好进行分析。

    点赞,在互联网中是一个比较简单的操作。用户看到自己喜欢的信息,点击“赞”按钮,点亮“赞”操作,再次点击,取消之前的“赞”操作。是不是很 easy?

    点赞

    但,一个明星微博的点赞数可能高达几十万,甚至上百万。一个热点新闻,可能有数千、数万、甚至数十万用户同时点赞,如何处理这些极端情况呢?

    2 系统设计要点

    如果要构建一套通用点赞系统,首先需要对系统所涉及角色、主功能进行梳理。

    2.1 系统角色

    系统角色主要涉及 点赞发起者点赞目标对象

    这两个角色,本质上是对其他对象的一种引用。从领域设计角度,应该是对其他限界上下文中聚合的引用。两个角色,本身没有唯一标识,并根据属性确定其相等性。因此,符合值对象建模规范,应该作为值对象处理。

    通常情况下,点赞发起者对应系统的用户(有的系统会有多个用户系统)。但,点赞的目标对象可能会有很多,如新闻、评论、帖子等等。

    点赞发起者&目标

    系统角色如下:

    角色 含义 建模方式
    Owner 点赞发起者 值对象
    Target 点赞目标对象 值对象

    2.2 系统功能用例

    系统功能,主要围绕系统角色展开。

    点赞发起者 Owner,可以选择一个点赞对象 Target 进行点击操作。当显示目标对象 Target 时,需要判断该 Target 是否已经点过赞。

    对于点赞对象 Target,只有一个应用场景,就是在显示时,获取总的点赞数量。

    用户用例

    详细用例如下:

    用例 含义
    点击 Owner 对特定 Target 进行点击。如果没有点赞,则点赞;如果已经点赞,则取消点赞
    是否点赞 判断特定 Owner 对 特定 Target 是否已经点赞
    获取点赞数量 获取特定 Target 总的点赞数量

    2.3 赞功能设计

    点赞发起者点击“赞”按钮,点亮“赞”操作,再次点击,取消之前的“赞”操作。

    2.3.1 识别建模类型

    Like 可以接收点击操作,并更新内部状态。同一个点赞发起者对同一个点赞目标进行“赞”和“取消”操作时,针对的应该是同一个 “Like” 实例,需要唯一标识对操作进行跟踪。综上分析 “Like” 是一个实体。

    2.3.2 实体建模

    首先,需要明确实体的名称,在这里,我们简单命名为 Like。

    行为建模

    分析下 Like 的业务行为,Like 对外操作只提供一个 click 方法,当触发 click 操作时,Like 在 Submitted 和 Cancelled 之间进行切换。当 Like 状态发生变化时,需要发布对应的内部事件。

    Like 所涉及的业务方法如下:

    业务方法 含义 事件 业务规则
    click 用户点击行为
    submit 点赞 LikeSubmittedEvent 当用户未点赞时触发
    cancel 取消点赞 LikeCancelledEvent 当用户已经点赞时触发

    为了避免 Like 的臃肿,我们将 Like 的状态进行单独建模。构建一个单独的值对象,并将状态相关的操作下推到该值对象中。我们称为 LikeStatus

    属性建模

    Like 所关联的对象,包括 Target、Owner 和 LikeStatus,并且三者都是值对象。

    属性 类型 含义
    owner Owner 点赞发起者
    target Target 点赞目标对象
    status LikeStatus 点赞状态

    创建方式建模

    Like 的创建方式比较简单,没有太复杂的业务逻辑,因此,采用静态方法对其进行创建。

    2.3.3 小结

    Like 是一个比较简单的聚合,具体结构如下:

    Like 聚合

    赞功能所涉及的对象见下表。

    对象 含义 建模方式
    Like 实体&聚合根
    LikeStatus 赞状态 值对象
    LikeSubmittedEvent 点赞事件 内部领域事件
    LikeCancelledEvent 取消赞事件 内部领域事件

    2.4 日志功能设计

    Like 代表的是当前点赞状态,对于多次点击,只会记录最后的结果,而中间的过程数据丢失了。

    日志,本身不属于业务功能,但对用户行为分析非常重要,我们应该将用户的所有操作保存下来。我们称这些过程数据为 LikeLogger。

    2.4.1 识别建模类型

    日志主要用于记录谁(Owner)对什么(Target)进行哪个操作(Action),在创建后就不在改变。基本符合值对象建模条件,但,我们如何对其进行持久化呢?

    一般情况下,值对象的持久化依赖于包含它的实体,值对象会随着实体的持久化而持久化。但,Logger 是个整体概念,本身不属于任何实体。在这种情况下,我们可以将其建模成一个不变实体,一来借助实体进行持久化,二来避免对实体的修改。

    2.4.2 不变实体建模

    LikeLogger 为不变实体,内部所包含的属性,不允许修改。

    属性建模

    LikeLogger 所包含属性如下:

    属性 类型 含义
    owner Owner 点赞发起者
    target Target 点赞目标对象
    actionType ActionType 操作类型

    创建方式建模

    LikeLogger 支持 Like 和 Cancel 两种类型的日志,可以根据 ActionType 构建静态方法,以完成各自的创建。

    方法 含义
    createLikeAction 创建点赞日志
    createCancelAction 创建取消点赞日志
    2.4.3 小结

    LikeLogger 所涉及对象包括:

    对象 含义 建模方式
    LikeLogger 赞日志 不变实体
    ActionType 操作类型 值对象

    2.5 计数功能设计

    最简单的计数功能,便是通过 SQL 对 Like 进行 “count group by” 来完成,但在高并发系统中,group by 是一大忌讳。

    从单一职责原则角度,Like 承载了过多的责任,将统计功能强加到服务于业务的 Like 也非常不合适。因此,我们对计数功能进行独立的业务建模。 我们称为 TargetCount。

    2.5.1 识别建模类型

    TargetCount 需要根据点赞和取消点赞对计数进行增减操作。对于同一个 Target,需要持续跟踪其数量变化。可见,TargetCount 是一个实体。

    2.5.2 实体建模

    行为建模

    TargetCount 的操作,主要有 incrdecr 两个业务操作。在进行 count 更新时,存在一个业务规则,及 count 不能小于零。

    业务方法 含义 事件 业务规则
    incr 增加点赞数
    decr 减少点赞数 count 不能小于零

    属性建模

    TargetCount 的属性包括:

    属性 类型 含义
    target Target 点赞目标对象
    count Long 总的点赞数

    创建方式建模

    TargetCount 的创建方式比较简单,因此,采用静态方法对其进行创建。

    2.5.3 小结

    计数功能所涉及对象包括:

    对象 含义 建模方式
    TargetCount 点赞目标计数 实体

    2.6 用例走查

    用例走查,主要从用例角度,验证当前设计是否满足业务需要。

    用例 支持方式
    点击 由 Like 聚合的 click 方法进行支持
    是否点赞 由 Like 聚合的 LikeStatus 进行支持
    获取点赞数量 由 TargetCount 的计数进行支持

    LikeLogger 不直接服务于业务,仍旧有很大意义。

    2.7 架构设计

    到现在,整个系统的核心组件就设计完成了,接下来,我们需要将其组装起来,以形成一个可用系统。

    这设计架构前,有几个非功能性需求需要考虑。

    • 点击行为的高并发
    • 获取计数的高并发
    • Like 与 Logger、 Count 的数据一致性
    2.7.1 点击行为的高并发

    点击行为是典型的写操作,需要对写操作进行优化。

    对于写操作优化,常见的策略包括:

    • 数据散列。也就是我们常说的分库分表,将写操作分散到多个数据库实例中,从而提升系统的整体吞吐。
    • 先入队列,后台消费。将用户请求添加到队列,启动后台线程,从队列中获取请求,并挨个消费。这种策略的最大特点就是可以起到消峰的作用,将瞬间巨大的请求缓存起来,不会对后台服务造成很大冲击。

    对于一致性要求高的业务场景(比如支付),数据散列是唯一解决方案;对于一致性要求不高的业务场景(比如咱们的点赞系统),队列方案是最佳解决方案。

    在此,我们使用队列方案来应对点击行为的高并发。即用户提交点击请求并不会直接调用业务方法,而是将请求放入消息队列;后台消费线程从消息队列中获取请求,并调用业务方法执行业务逻辑。

    2.7.2 获取计数的高并发

    获取计数是典型的读操作,需要对读操作进行优化。

    对读操作的优化,主要是使用缓存进行访问加速。我们使用 Redis 来加速访问。

    具体的操作如下:

    • 首先从 Redis 中获取计数信息,如果命中,直接返回
    • 如果 Redis 未命中,从数据库中获取计数,将结果添加到 Redis 中,然后返回
    • 当计数发生变化时,清理 Redis 的过期数据
    2.7.3 Like 与 Logger、 Count 的数据一致性

    在系统中 Like、Logger、Count 是三个聚合根,我们需要保证三者的数据一致性。

    系统的操作入口只有 Like 聚合,当 Like 发生变化时,Logger 和 Count 都需要跟着联动起来。Like 与 Count、Logger 具有很强的因果关系,这也是领域事件建模的重要信号。

    在常规操作中,我们会在操作 Like 后,直接调用 Logger、Count 相关接口进行业务操作。但在 DDD 中,是绝对不允许的,一个操作只能对一个聚合根进行处理,聚合根之间的同步只能基于事件通过最终一致性解决。

    我们可以基于内存总线和内部事件,通过订阅 Like 相关事件在内存中完成与 Logger、Count 的业务同步;也可以使用专用消息队列和外部事件,完成多个系统间的数据同步。

    考虑到系统读写的扩展性,在此,我们使用消息队列和外部事件完成数据一致性保障。

    Like 在执行完业务操作后,将内部领域事件直接发布到内存总线(EventBus),Exporter 组件从内存总线中获取领域事件,将其转换为外部事件,并发送到消息队列中。Consumer 组件负责从消息队列中获取事件,调用 Logger、Count 相关业务接口以完成业务操作。

    2.7.4 小结

    综上分析,我们的最终架构如下:

    相关文章

      网友评论

        本文标题:高性能系统设计:互联网点赞系统设计及实践

        本文链接:https://www.haomeiwen.com/subject/beqpbktx.html