高并发系统设计 --基于MySQL构建评论系统
如何用MySQL来实现评论系统
为什么我不用mongodb?
- 社区成熟度不如MySQL,redis
- 需要学习的东西很多,迁移,扩容,片建,集群
- redis > mongodb
架构设计
使用MySQL进行存储的话,就必须要用到Redis来做缓存,后台admin需要接通ES来进行查询,comment-service通过异步来进行写Redis和MySQL评论数据,MySQL和ES通过Canal进行binlog同步。
缓存模式
首先我们需要预读,我们读第一页的时候,也需要把第二页的内容加载出来。读第二页的时候,我们预先读第三页,这样可以避免大量的cache miss。
但是这里有一个致命的问题就是:当缓存抖动的时候,会触发大量的cache rebuild,因为我们使用了预加载,容易造成OOM(内存溢出)。因此我们需要使用消息队列来进行逻辑异步化,对于当前请求,只返回MySQL中的部分数据即可。
写的逻辑
至于写的操作,我们要穿透到存储层,因此最好使用消息队列异步削峰。例如我的评论发布出去了,用户过100ms才看到评论,这是无所谓的。
存储设计
comment_subject表
id | int | 主键 |
---|---|---|
obj_id | int | 对象id |
obj_type | int | 对象类型 |
member_id | int | 作者id |
count | int | 评论总数 |
root_count | int | 跟评总数 |
all_count | int | 评论+回复总数 |
state | int | 状态 0:正常 1:隐藏 |
attrs | int | 属性 0:置顶 1:不置顶 |
create_time+update_time | datatime | 创建时间,修改时间 |
obj_id+obj_type把评论系统设计成中台。 一般是指搭建一个灵活快速应对变化的架构,快速实现前端提的需求,避免重复建设,达到提高工作效率目的。
obj_id+obj_type形成了一个业务键,比如微博,发帖,发视频,你可以发视频,你也可以发文章,评论系统设计成中台。
comment_index
id | int | 主键id |
---|---|---|
obj_id | int | 对象id |
obj_type | int | 对象类型 |
member_id | int | 发表者id |
root | int | 根评论id,不为0是回复评论 |
parent | int | 父评论id,为0是root评论 |
floor | int | 评论楼层 |
count | int | 评论总数 |
root_count | int | 根评论总数 |
like | int | 点赞数 |
hate | int | 点踩数 |
state | int | 状态,0:正常;1:隐藏 |
attrs | int | 属性 |
create_time | datetime | 创建时间 |
update_time | datetime | 修改时间 |
parent:父评论id,其实就是记录是否是回复评论。
comment_content表:
comment_id | int | 主键 |
---|---|---|
at_member_ids | varchar | 对象id |
ip | int | 对象类型 |
platform | int | 发表者id |
device | varchar | 跟评论id,不为0是回复评论 |
message | varchar | 评论内容 |
meta | varchar | 评论元数据:背景,字体 |
create_time | datetime | 创建时间 |
update_time | datetime | 修改时间 |
index是索引表,content是内容表。
数据写入:事务更新comment_subject
,comment_index
,comment_content
三张表,其中content
是非强制性需要一致性考虑的。因此可以先写入content
,之后事务更新其他表。即便content
更新成功,后续失败仅仅存在一条ghost
数据。
数据读取:基于obj_id
+ obj_type
在comment_index
表找到评论列表,where root=0 order by floor
。之后根据comment_index
的id字段捞出comment_content
的评论内容。对于二级的子楼层,where parent/root in(id...)
。
为什么要把index和content分成两个表
comment_index:评论楼层的索引表,实际并不包含内容。comment_content:评论内容的表,包含评论的具体内容。其中comment_index的id字段和comment_content是1对1的关系,这里面包含了几种设计思想。
- 表都有主键,comment_content没有id,是为了减少一次二级索引查找,直接基于主键检索,同时comment_id在写入要尽可能的顺序自增。
- 索引,内容分离,方便mysql_datapage缓存更多的row,如果和content耦合,会导致更大的IO。长远来看content信息可以直接使用KV storage存储。
缓存设计
comment_subject_cache【string】
key | string | oid_type |
---|---|---|
value | int | subject marshal string |
expire | duration | 24h |
comment_index_cache【sorted set】
key | string | cache key:oid_type_sort其中sort为排序方式,0:楼层,1:回复数量 |
---|---|---|
member | int | comment_id:评论id |
score | double | 楼层号,回复数量,排序得分 |
expire | duration | 8h |
comment_content_cache
key | string | comment_id |
---|---|---|
value | int | content |
expire | duration | 24h |
comment_subject_cache:对应主题的缓存,value使用protobuf序列化的方式存入,这样调用rpc的时候速度可以更块一点。
comment_index_cache:使用redis sorted set进行索引的缓存,索引即数据的组织顺序,而非数据内容。通过预加载少量数据,通过增量加载的方式逐渐预热填充缓存,而redis sorted set skiplist的实现可以做到O(logN) + O(M)的时间复杂度,效率很高
sorted set是要增量追加的,因此必须判定key存在,才能zadd
comment_content_cache:对应评论内容数据,使用protobuf序列化的方式存入。
增量加载(目标表仅更新源数据表中变化的内容)+lazy加载(延迟加载,种将资源标识为非阻塞(非关键)资源并仅在需要时加载它们的策略)
可用性设计
其实就是各种缓存问题,消息队列消息等问题。
如果这个key是热点的key的话可以使用本地缓存+分布式缓存。
那么如何统计是否是热点呢?
这里,我们就移步到我们的下一篇文章了,谢谢大家。