这是我参与「第五届青训营」笔记创作活动的第9天
需求分析
针对的user、video、comment、message模块中的高频写入操作场景,会先写入redis,然后生产消息到消息队列Pulsar,通过服务消费Pulsar topic消息,再写入mysql,来进行削峰以平缓流量,异步操作,降低如点赞等操作响应耗时,改善用户体验,提升系统稳定性,并且实现了redis和mysql数据的最终一致性。
user服务
- 关注/取消关注
video服务
- 点赞/取消点赞
comment服务
- 评论/删评
message服务
-
消息存储
实现
在每个服务下都建立了Pulsar客户端,并且定义了每个场景的Schema和Topic
const (
LikeVideoTopic = "persistent://public/douyin_prod/like_video"
FollowUserTopic = "persistent://public/douyin_prod/follow_user"
CreateMessageTopic = "persistent://public/douyin_prod/create_message"
CommentTopic = "persistent://public/douyin_prod/comment"
)
具体解释如下:
Pulsar消息被存储为非结构化的字节数组,数据结构(即所谓的模式)只有在读取时才会应用于这些数据。因此,生产者和消费者都需要就消息的数据结构达成一致,包括字段及其相关类型。
Pulsar Schema(模式)是定义如何将原始消息字节转化为更正式的结构类型的元数据,作为生成消息的应用程序和消费消息的应用程序之间的协议。它在将数据发布到主题之前将其序列化为原始字节,并在将原始字节交付给消费者之前将其反序列化。
Pulsar使用模式注册表作为中央存储库来存储已注册的模式信息,这使得生产者/消费者能够通过经纪人协调主题的消息模式。
在任何围绕信息传递和流媒体系统建立的应用中,类型安全都是极其重要的。原始字节对于数据传输来说很灵活,但这种灵活性和中立性是有代价的:你必须覆盖数据类型检查和序列化/反序列化,以确保输入系统的字节可以被读取并成功消费。换句话说,你需要确保数据对应用程序来说是可理解和可用的。
并且在服务初始化的时候,启动了producer生产者,通过一个协程启动了consumer消费者,以Shared的方式来订阅Topic,以支持弹性扩容,然后消费消息来将相关操作记录到mysql中。
高并发下的性能最大瓶颈是mysql的读写,通过消费队列削峰能减少mysql的写入压力。
关注/取消关注设计
不引入pulsar的话,在处理关注/取消关注的请求中,需要等待写入mysql操作完成后才能响应请求,这样在大量请求时,mysql的io压力会剧增,响应时间会拉长甚至是打挂mysql数据库,威胁较大。
从产品设计的角度出发,用户对自己关注列表的实时性要求不高,所以可以引入消息队列组件。
-
关注操作中pulsar消息体的定义如下
-
type FollowUserMessageJSON struct { SrcUserID int64 `json:"srcUserId"` FollowID int64 `json:"followId"` ActionType int `json:"actionType"` }
-
service将相关字段数据打包成上述消息体,push到消息队列
- 如果推送成功,直接返回操作成功
- 如果推送失败,如发出push操作超时后仍未收到响应则也会返回失败,然后返回操作失败
另外一个协程准备一个messageChannel,不断消费消息,消费成功则调用ACK向pulsar表示成功,然后根据定义的schema将消息反序列化为指定消息体变量,然后写入关注信息到mysql中。
Tradeoff: 上述方案虽然对于不合法操作也会返回成功响应,但考虑到实际使用中用户更关心响应耗时,且不合法操作最终也不会影响真实的数据,所以进行权衡之后使用了此方案。
其实操作的设计方案类似,就不一一列举了。