好 我们接下来看评论的存储设计
因为最终你的存储层意味着
你的整个数据持久化的一个地方
那么这一层的设计其实也非常关键
前面的架构能解决很多这种缓存miss
对吧 最终因为你的数据还是会持久到存储层
那么这一层是最难被扩展的
就是你数据库设计的好
其实最终就决定你的项目的一个成功的情况
也是非常重要的
我们看一下我们数据库的设计是怎么做的
因为当时我们之前也讲了
我们这个地方核心依赖的就是MySQL
我们会设计三张表
我们分别来看一下
第一个是叫comment_subject
我们是按照因为因为这是一张主题表
这长表也不会特别大
就是说一个内容
有一个内容
有一个内容它就有一条主题记录
举个例子
某一个用户的一个视频
那么它对应的就是有一个所谓的这样的主题表
我们当时这里有个下划线comment_subject下划线0~49
意味着我们当时拆了50张表去存的
那我们看一下
每一张表一般都有一个主键
为什么要有一个自增主键
不管这个是自增的主键
还是说你用发号器发的一个主键
我们的MySQL的记录
这个主键要是真的一定要之前的比之后的要小
那么这样的话我们的聚集索引就不会频繁的频繁的被分裂
这样的话其实它也不是一个顺序io
所以一般来说是尽可能是让主键是一个id并且是增的
我们再看一下有个叫obj_id 叫对象id
obj_type
那么这两个对象
这两个对象构成了一个业务
一个业务的键
比方说它可能是漫画的
某一部漫画的一个
可能是某一个视频的某一个id
可能是某一篇文章某一个专栏的id
那么这个id因为可能会冲突
所以说它可能会跟obj_type它们两个构成一个唯一键
也就是说意味着某一个业务类型的某一个id它一定是唯一的
所以这两个是一个可以组成一个唯一键
然后有一个member_id
member就是作者
我这个主题的作者是谁
一般来说这个视频作者 内容或者是文章的作者
然后这里面有三个计数
为什么我们会在主题表里面做计数
我之前见过很多业务
它之所以性能比较差
它把这种计数类的一些统计类的需求
它都做成了一个什么select count(*)
然后加一个where条件
你可以想象一下
一个很高频查询的业务场景里面
它是不是查询效率肯定会非常糟糕的
你每次扫描大量的函做一个count的操作
你的MySQL扛得住吗
肯定是扛不住
怎么解决呢
所以我们一般来说是牺牲它的写性能
优化它的读性能
最常见的做法就是说在写操作的时候
同步的把把数据啊进行一个更新
比方说我们这里有三个count
我们先看第一个是评论总数
也就是说我这一个主题一共有多少个评论
一共有多少个评论
这个这地方注意
一般来说我们评论有可能楼层会进行删除
那么删除因为你的楼层号肯定是一直在增加的
你的楼层号肯定是要一直增加的
那么有可能上抽掉一个楼层的话
你不能说楼层号就重复了
所以我们有两个计数
一个是删除楼层
删除楼层它计数也不会变化
一个删除楼层它计数会变化
说白了一个是用来记录楼层号的
一个是用来记录一共有
一个是记录一共有多少评论
就不管删不删除
一个是当前的可见的评论是多少
所以它两个计数是稍稍有一些区别
那么这边还有个叫root_count
表示一共有多少根评论
根评论就是我的一级楼层的一级楼层的有多少个
所以它是功能上稍稍有些区别
然后有个叫状态字段
这个状态真的是个int8
那么可能是0是正常 1是隐藏
它相当于就是一个不同的值
可能是不同的枚举不同的状态 叫state
我们这里面还有一个attrs 就是所谓的属性
那么我们这个地方是要的int32
那么属性字段
我们其实用法上是把它用成了bit位
比方说第0位表示的是运营置顶
第1位表示的是up主置顶
第2位可能是被大数据过滤掉了
被大数据过滤掉了
它属性实际上是可以表示的是一种多重的共存的一个状态
它其实是每一位有不同的逻辑所以它可以共存
这里int32的话其实就是8×4 32个状态
32个属性可以共存的
然后每一张表一般设计上都会加create_time和update_time
也就是创建时间和修改时间
那么通常我们在建表的时候都会把它设置成
就是current timestamp 对吧
一更新这条记录update_time就会自动更新
对这个主题表
主题表关联的就是你的评论的主体
这个是这是一张主表
那么还有一张表叫comment_index
那么我们这个地方是0~199
也就是说我们拆了200张表
那么我们再看一下
首先一样的它是有主键
它是个主键
另外我们这个地方冗余了obj_id和obj_type
它冗余的这两个字段
你这个地方因为也有
但是我们这个地方是没有用这个主题id来做关联的
而是说直接obj_id obj_type是冗余的一份
为什么
我们其实觉得没必要非要先把主题id查出来
然后再把这里做一个主题id做关联
直接把obj_id obj_type放一起
其实也是问题不大的就可以直接使用
然后有发评论的话
因为你是在某一个主题下发评论
所以它有一个发表者的用户id
对 然后这里面看有一系列的计数
有root root表示根评论的id
如果为0表示是 不为0表示是回复评论
因为我们刚刚说了你要支持它的评论和楼中楼
那么如果root=0
是不是它就是最外层的评论 那么它不等于0
我可能是挂载到某一个主体
挂在了某一个主楼层的一个子评论对吧
所以它有root
那么parent就是我的父评论id是谁
我的父评论id是谁
因为它这个地方相当于是可以是你回复某一个人的
所以它是有一个parent的这样的
有这样的一个id的 然后呢
我们还有所谓的评论的楼层
有个评论的楼层
就是这一条评论它属于第几层的
因为展示的时候要显示它是楼层号是多少
那么floor其实可能是
这个floor可能是先取出来当前的主当前的总楼层
然后把它持久化到这个地方的
也就是我这样的话就知道
我发一条评论的时候一定是先看当前楼层是多少
然后加一
然后同样的把加一后的数字写到这个地方
还有一个因为我们知道主楼层它有个评论的总数
我知道某一个评论下面它有多少个子楼层
所以它有个评论总数
还有就是根评论的总数
反正总之它有一系列的这种计数的
一些用于我们查询时候不需要做count(*)
所以我们一般都是在写操作的时候把这些逻辑计算清楚
一般都是这样做
那么这个根评论总数实际上就是我作为一个root头
我作为一个顶楼层
我下面有多少个
总评论数可能因为也是要删楼层的
那么删除它这个地方也不会变化
那么这个里面还有一些点赞点彩
还有一些状态属性字段都会在在这个主体表里面
我们看到这个其实这张表基本上就是围绕
围绕着一条评论的来建设的
但是我们看到我们当时想法是说
会把评论的内容表再拆出去
因为内容表它一般来说比较大
内容表一般比较大
所以我们先看一下
我们后面再讲一下为什么会这样设计
同样的comment_content表它拆了200张表
然后它也是有这个地方 注意有个主键
主键是和它一一对应的
后面我们再来看一下为什么这样设计啊
然后它里面有什么所谓的你@的谁
然后他的IP 平台（这两个地方写错了）
然后设备
然后评论的内容
评论的原数据
像背景字体这些
然后其实围绕评论内容的相关的一些字段
好 那么我们了解完这三张表以后啊
我们再来看一下数据的写入和读取是怎么操作的
对 我们看一下数据的写入读取是怎么做的
首先我们先看一下数据写入
实际上它就是事务更新
我们在一个transaction事务里面
同时更新这张subject表
因为你要发评论 你要更新计数
然后你要插入一条
你要插入一条comment_index 插入一条评论
那么再事务更新评论的内容表
相当于这张是一张索引表
它关注的是楼层的信息
那么这张内容表
它关注的是评论的内容
因为内容本身有什么背景 色号
哪个IP 哪个设备发的
哪个IP等等一系列的这种属性
所以我们是把评论的索引表和评论内容表又拆成了两张表
那么实际上这段内容表它其实就是
一个纯kv的一个操作
评论ID评论内容
虽然事物是更新这三张表
实际上我们在真正逻辑上
我们在真正的逻辑实现上可以是先插入content表
插入这张content
因为内容你如果没有索引关系
其实这内容表你不同步事物更新也没问题
先插入content表
如果成功了
你在事物更新subject表
主题表和这张索引表是可以的
这样的话你的事务的力度会稍微稍微的小一点点
甚至有一天你把content表切到另外一个kv引擎
比方说别的一些RocksDB做的一些kv引擎也是可以的
那么你的排序的
在这种搜索排序的可能就比较适合用MySQL
你可以建BTree的索引
那么它加上一些为条件的话
它整个检索效率其实也非常高
所以说你把纯kv的
偏这种查的
一个key一个valued就可以放到kv引擎
那么偏这种搜索的你可以放到MySQL
那么事务更新
实际上是这两张表示一定要同步更新的
既更新主题表的里面的楼层
同时把楼层更新后的数据在写入到你的索引表
那么有了主题的计数更新
有了索引的这条记录
以及kv应该是提前写的
那么它就是一次完整的
那么这个地方我们再想象一下
任何一步失败会产生什么样的结果
如果写content表失败
这次操作直接评论写操作直接失败 抛给用户
如果评论表写成功 事务更新失败
无非就是说多了一条垃圾数据
多了一条叫ghost
一条这种灵魂数据
什么灵魂数据
残存在这张comment表
可能是MySQL可能是kv的一个引擎
都没关系
只要后面索引关系
最终可见
事务更新成功
那么你这样content内容信息才有可能会被加载出来
所以这个是可接受的
那么其实我们这个地方你看就一共事务更新了两张表
那么这个是写入操作
那么读取操作
我们先基于业务场景的
所谓的你的业务要接入到评论体系里面来
那么你就肯定要传你的业务号
然后你业务的主键是什么
那么它先会在comment_subject里面
这张主题表先找到它是否存在
你肯定要看一看它是否存在
主题是否存在
那么找到以后
你会去干嘛
你肯定是先做一个这样的判定
当然也可能是先查缓存
它可能也有缓存的
我这个时候一找评论内容的时候
你肯定是去找索引表
先找索引表
那么所以表我就可以wherre条件
root=0 ordered by floor
where root=0表示的是什么意思呢
表示的是先查出来它的主楼层
主楼层第一楼 第二楼第三楼就是外面的跟评论
它的主楼存
查出主楼层以后
再根据主楼层的id再二次查找
做一个迭代查询
二次查comment_index这张表
把主楼层的id where parent
或者是root in刚刚你主楼层的这些id
然后可以干什么
where再and一个floor
就是floor小于等于3
为什么
因为你看我们的产品形态
它是一个主评论
这个是一个主评论
下面的评论我们不会全部显示
每一个主评论它只显示头部的头三条的子评论
所以为什么要做二级查找
实际上就是要为了解决先查出来主评论的id
然后再做一个where parent
或者是root它in刚刚那些它的主楼层的那些id
再把它们做一次二次查找
现在因为你主楼层和二级楼层都在这张表里面
那么你这个时候要加个条件
and floor<=3 order by floor
你要取出来头三条
取出来头三条
就是它的子楼层号
对 因为那个时候就不是倒叙的
可能是一个顺序的
你这个其实是你就可以把二级评论捞出来
其实这个地方其实查了两次
先查出主评论
再查出来二级的子楼层
这个其实是非常有意思的
所以它只要查两次就可以了
那么查出来索引信息以后
我实际上是不是再去把content的内容表批量的一次捞出来
注意这个地方一定要批量操作
你捞出来一坨评论的索引id
那么你再去内容表
再去这张内容表
是不是就可以是不是就可以把它们什么
把它们的评论内容一次捞出来
也是id in操作一次捞出来
其实我们也可以抛
是不是有点像个图数据库
其实是不是有点像个图数据库
未来是不是借助于
这种图存储的方式也是可以捞出来
就避免做一个二级迭代的查询
你可以用因为这种所谓的root + parent的这种设计
它其实要做一个迭代是比较恶心的
我觉得有可能未来使用这种图存储的
也是可以做这个功能
就避免说这种嵌套的子查询非常复杂
这个是读取
我们存储设计里面有个很关键的
就是存储索引表和内容比较分离了
索引表和内容表分离了
为什么我们会这样设计
我们先看一下comment_index它关注的是评论楼层的索引组织
它并不包含评论的实际内容
那么comment_content这张表它包含的是评论内容的表
包含具体的评论内容
也就是说评论的内容大家可能知道比较长
有些话唠可以发出几百个字甚至几千个字
评论一般我们都支持的内容会回复的内容会比较长
那么这里面还有个小设计
是这一个id就是comment_index这个id
和content表的comment_id
它是一一对应的一个关系
我们看上一张图
这个id和这个id它是一一对应的
为什么我们会这样设计
这里面包含两个这样的思想
第一表一定要有主键
主见是物理结构上组织这张表的
它是物理结构上组织表的
就是它排序是基于键排序的
那么comment_content为什么没有自增的id
而是要用comment_index的id
我们这样设计是为了减少一次二级索引查找
注意如果我们content表增加了自增id
然后再加一个唯一键
用comment_id去映射这张表
一对一的关系吗
那么这样的这种方式
基于唯一键
找到索引id用where comment_id等于索引id
那么你先要找到主键
它再会做一个查找
找到评论的内容
因为只有聚集索引
只有它的主键内容才包含这一行的实际的内容
它才包含这一行的实际的内容
注意所以如果你用唯一键去做检索的话
所以MySQL引擎内部实现来说它会多一次查找
但是如果你有主键直接去做检索
意味着什么
它只要查一次
因为主键直接包含了函的内容
所以你看设计上就很巧妙
它就会少一次io操作
这是一个
另外我们主键之前讲过它一定要顺序自增
因为comment_index表插入的时候
它一定是带顺序的
那么同样因为你是事务要去更新comment_content表
所以它大概率也是顺序自增的
所以这个完完全全就去掉了自增id
直接使用comment_index表的自增的id来作为它的主键
作为comment_content的表的主键
这个是非常关键的一个点
这是第一个设计
第二个 索引内容的分离是为什么
我们知道内容他话痨发的评论非常多
我们知道MySQL它是按照所谓的一个data配置
比方8KB 64KB一个这样大小去组织的数据内容
那么这个页它包含的行数取决于什么
你行越小当然缓存的行数越多
我一次要捞100个评论
如果你的评论每一条
你的评论表和内容表藕在一起
是不是意味着我要100条评论会产生什么行为
是不是它的io成本非常高
它的io成本非常高
它要miss好多个页才能把它捞出来
意味着你一个MySQL的data配置这个页它缓存的行变少
所以io成本变高
那么如果你分离以后
你先用二级索引去捞这100条数据的时候
效率极高
因为你的行一个页缓存的行数变多了
所以你还有成本变小了
你捞到这些id以后
你不是还要再查一次
那同学可能会问会不会成本就感觉差不多
其实不会
因为你再去捞评论内容的时候
它捞的是主键
所以它效率是比较高的
或者甚至有一天你把它切到kv引擎里面去也是可以的
所以设计上把他们分离
也是为以后的一个演进做一些考虑的
所以这样拆分之后我觉得效率是比较高
这个效率是比较高的
当然这里面还有一个小细节
我们comment_index
我们这个comment_index还会把一些什么点赞点踩放在里面
原因是因为有可能会基于的index做一些排序
可能会去点赞点踩来做一些排序
所以我们这个地方是放在里面做
可能未来会做一些排序条件的
所以这样的一个设计的方式
我们再来看一下存储设计里面的缓存设计
我们先看第一个叫comment_subject_cache
这一个cache它对应的就是主题的缓存
我们value 一般这个表有好多个字
我们value 一般会把刚刚前面这个主题表的这一坨这些数据
我们一般会用protobuf来定义
或者用json来定义
把它序列化以后变成一个二进制的内容
然后再塞到Redis或者是memcache里面
其实它就是个简单的一个kv的查询
那么第二个comment_index_cache
这种index就是它的索引的cache
大家知道排序结构一般会有什么
一般是用Redis的sortedset
比如说的sortedset那么进行所有的缓存
那么因为它只有索引没有内容
所以索引数据的组织关系它是用来缓存
所以用排序的一个结构不而非数据内容
它是用来排序用的
那么我们之前我也看过像百度贴吧
它的一些设计百度贴吧
我之前看到它还自己做了一个拉链存储来组织这些索引和内容
其实对我来说
我觉得我们当时没有这么多时间去针对这个场景
还独立的去做一个这样的存储
这么一个专门去做一个这样的存储
我们认为在我们这个场景里面MySQL作为主力存储
利用Redis来做加速
其实完完全全就足够了
可能不需要自己再独立开发一个
所谓的这种评论的一个索引系统
为什么我们觉得可这样可可做到
因为可以想象一下
我们cache miss的时候
构建是利用kafka消息队列异步构建的
异步填充缓存的
那么这样的话我的回源的问题不怕被打爆
这是第一个
第二个就是说我们利用Redis还可以做增量的
Redis使用sortedset
底层实现是一个skip list
那么它可以做到一个O(logN)+O(M)的时间复杂度
也就是说我先要找到某一个楼层
然后它加载第100层加载100层或者加载50楼
那么它就是先定位到那一个你要检索楼层
比方O(logN)去定位到
那么你要扫出50条就O(log50)
它的时间复杂度并不高
那么你在缓存设计上
因为Redis sortedset不是说
你评论有10000楼你要全塞进去
你可以增量缓存
什么叫增量缓存
你可以先缓存头两页的
那么你看滑的越多
你就慢慢的增量的去把缓存填充进去
其实非常好做
因为你你知道总楼层号
你知道当前你Redis最后的一条记录是多少层
那是不是意味着
你是不是可以增量去把楼层填充进去
你也不需要一次全塞进去
那么Redis sortedset因为你随着滑动
因为我们评论一般都是往下滑
所以你这样的一个做法的话
你就可以增量缓存
增量缓存来做
那么Redis sortedset反正可以只是放纯索引内容
再大也不会太大
就100万的楼层号
这个sortedset也不可能有一个人真的把100万楼全部翻完
你认为可能其实业务场景里面不太可能的
它实际上不太可能的
它一般翻个几十都了不起了
然后就退出了
甚至我们在业务逻辑上可以限制它
只显示最后的3000页也是可以的
有没必要为了一些特别极端的场景去做一个复杂的存储设计
其实我们看这地方非常平民
就是一个MySQL加一个增量的Redis sortedset就完成
那么sortedset我们看它内部怎么存的
member sortedset的key就是评论主题的id
一般是用member就是评论ID
score就是它的楼层号去排序的
然后我们还有一个过期时间8小时
我们还有一张content
还有张content就是我们的评论内容
其实它就是个简单的Redis string
它就可以完全序列化以后放到Redis里面
非常简单
这个就非常简单
所以在我们这个场景里面用kafka解决的是
累积的或者是异步的一个增量式的一个(说错了)
我们用kafka实际上解决的是异步的缓存的构建
我们又非完全填充的这种方案解决的是增量加载和累计加载
我按需加载
lazy加载就触发到第二页的时候
再往后加载多加载两页
第4页的时候再往后多加载两页
按需构件增量加载
所以其实我认为非常共用
老实讲我们评论系统做出来以后
没出过事故
我们体量其实也不小了
主题也非常之多
其实也没有做没怎么出过事故
非常稳定
其实就是平民版MySQL + Redis就搞定了这么一个复杂系统
我们最后再来看一下这套系统的一些可用性上
我们做了一些点
第一个
对于热门主题
如果存在缓存穿透的情况
很热的主题意外的情况下
突然缓存穿透
它这个时候很容易导致大量的同进程的或者是跨进程
因为你同一个评论主题
大家都会进来
它可能会导致同进程跨进程的一些数据
回源到你那MySQL存储层
可能会引起我们MySQL过载
这种其实cache miss的穿透问题
经常在网上能看到类似的一些答案
大家可能会经常听到搞一个分布式锁
然后只交给一个人去做一个数据的一个查询
然后其他的人在那里等待sleep 5
然后定期再查一下Redis或者是memcache数据有没有准备好
然后类似的这种做法太复杂
尽量不要去依赖一个集中式的状态服务
因为对于一个高并发的系统
第一复杂度很高
第二很容易出事故
有没有平民版的一个架构
有
什么叫归并回源
我们要解决的场景是就像cdn一样
cdn场景里面你比方有个视频miss了
你有个视频那个没命中缓存
你可能要去回原站对吧
你回原站去把这个视频拖过来
你可以想象一下视频
假设这个视频非常热
在你全国各地有几百个时间节点
你这个一瞬间大家都miss了
假设因为意外情况视频都miss了
大家都来原站都来原站拖
并且用户请求又很大
是不是用户请求来一个也会去拖
因为还没拖回来肯定要穿透
你这个时候大家都去拖
一个节点投10次
两个节点投20次
本来一个节点只要投一次
你要拖非要拖那么多
拖到原站的带宽就被打爆
我们一般对于cdn的这种场景
大家知道怎么做吗
两个策略一个叫二级节点 一个叫归并为原
什么叫二级节点
比方说华东地区你布了cdn打开不了
比方说整个华整个华东
你可能布了20个节点
你可能不到20个节点
然后你要去如果他们有一个视频miss了
你大家都去从原站拖
肯定这20个节点拖20次
有没有加二级缓存指的是华东地区的节点
20个节点回到华东的二级节点
那么是不是这20个节点回到华东
华东的节点只有一它一个人去拖一次就够了
是不是就归并20变一
它不就是个归并 20变一
那么这个问题解决了一部分
那么什么叫归并回源呢
二级节点解决的一部分
那归并回源指的是你一个节点
一批人都来去回源
有没有必要你你这个节点可能承担了
100个用户都在看这个视频
你是不是需要都100个人都去拖
不需要都需要几点的拖
只需要交给一个人拖
拖完之后大家共享一下不就好了吗
所以归并回源的思路其实就是把上路非常巨大的流量
指数级的变成一个
指数级下降变成一个
甚至是更少甚至一个
然后一层层漏
像漏斗一样一层层的去漏掉
最终回到原站的请求一定是非常少的
同样的我们就看一下
在Go里面如何去做归并回源
那么Go官方有个子包
有一个subpackage叫singleflight叫单飞
那么单飞我们看它这个库是干嘛用的
这个包它有个叫Do方法 一个Do function
我们看它的签名
一个单飞的group按照某一个key
然后执行一个function的callback
然后返回一个值和一个value
这个方法实际上说白了
就是某一个key等于比方方等于
比方等于10
keyi=10
fn函数指针一瞬间
如果大家有100个并发来调这个函数
它只会交给一个人去执行这个function
那么这个function执行完之后
剩下等待的99个人都会返回value
都会返回value都会返回这个值
其实说白了这个数据被大家集体共享了
那么它的当非实际上保证的是一个进程内单飞成功
我们不需要用分布式锁
我们是不是用单飞先把进程的缩减掉
只有一个请求是不是就大大的减少了
因为你的服务是面向c端的
c端可能上出非常大
但是你透传去做回源miss的时候
其实你进程级别就一个了
那么你节点可能就50个
那么顶多50次回源
我认为勉勉强强还可以接受
勉强现在还可以接受
不需要搞一个分布式锁
那么看它内部源码实现其实也不难
先加锁
然后它这个group我们看一下它内部是什么
有一个map
有一把锁就这么简单
先加锁
然后这里是一个lazy的初始化
如果这个map为空
我就把它make一下
然后你不是要传一个key进来
我先查一下key有没有
那么它map value是什么 是个对象
我们看call对象是什么东西
call对象实际上就是内部有一个value缓存的value的值
然后error还有别的一些字段
我们先看看它怎么玩的
如果查到了对象
把dups++加表示又有人来等待了
然后解锁
然后会调这个
因为map查出来call对象
大家共享call对象对吧
比方你10个并发
第一个人
假设第一个人
我们先看看下面的逻辑
if外面的逻辑
因为如果第一个人没有查到这个对象
它先new这个对象
new一个call对象
然后把waitgroup+1
为什么要加1
交给这个人去干活了
它去MySQL里面查东西去了
然后把它放入到map
然后再解锁
然后再doCall
doCall是干嘛
说白了它就是内部其实就是它要去做这个函数指针
它就是要去调这个函数指针
把数据取出来
它要把这个函数取出来
它要去干活的
是干活的这个人
那么它干完活以后
其他的人为什么走到这个地方要调waitgroup
我们看看错了 应该是这个地方
而其他人不是走的这个逻辑你明白吗
第一个人可能是去干活的
其他人其实在这里 在哪
它在这一步等待的
因为这边如果没结束没有调Done
那么这边就会这边就在等待
如果第一个人结束了
其他人是不是为全部返回
然后在判断一些error的退出异常的情况
然后再把call对象共享直接返回
因为你这个map查到了
我直接读这个就好了
所以它这样的一个效果达到一个人去干活
其他人共享成果
那么这样的话做一个归并回源的策略
我们再回过来看PPT
这样的话我同进程交给一个人去获取MySQL数据
批量等待人全部返回
那么我们叫持有租约的owner
这个时候在我们cache miss场景里面持有租约的这个owner
它还会做的事情
它会投递一个kafka消息
因为我们之前说了
cache miss以后不是要头一个指令给kafka的
下游消费者comment-job
它会去做一个回填缓存的操作吗
我是不是可以用归并的思路只投一个kafka就不需要投50个
比方我有50个并发
我不知道投50个kafka消息
第一个可以大大减少写kafka的量
第二个在我们index就是在我们的缓存
revealed的comment-joob的操作里面
它怎么避免
因为你还有来自不同进程的同一个主题的cache miss
它其实是在我们comment-job
我们怎么做的
因为你这样虽然规定了
但是可能在我kafka里面重复的消息有好几条
所以说我们在comment-job还会做个逻辑
先查一下缓存是否存在
如果已经存在认为前面的人构建成功了
那么把这个指令丢弃掉
如果这个Redis缓存不存在
它才会真正地去MySQL拿数据
然后做一个缓存构建
所以这样的话也大大减少了kafka重复操作MySQL的次数
所以第一步是靠进程间的归并
靠进程内的归并投kafka
并且查MySQL只有一个人去查
第二步是comment-job去做缓存构建的时候
它会去查一下Redis
看这个指定是否是有效的
如果不有效直接丢掉差
效率其实多高
我判断key是否存在就好了
所以这个是有蛮有意思的
那么我们更进一步的如果如果缓存构建需要一两秒钟
那么在这一两秒钟的并发内
还有后续的一些同一个主题的请求打过来
怎么办
是不是又发了好多消息给kafka
然后comment-job要做一些丢弃逻辑
其实你这个地方可以更巧妙一点
在进程内用一个short-lived的flag
它就是一个进程内的LRU
就是用进程内的很短时的缓存
比方说就缓存了5秒钟
那么连续5秒内
只要这个标记存在
那么就把cache rebuild的消息直接drop
那么它又大大减少了写
像kafka的控制命令回源命令的消息数量
所以我理解你的节点数就是你的并发数
那么这50个查询查MySQL相对来说还是可接受的
因为MySQL一般不是说只有一个节点
它是有所谓的主从架构分离的
你可以这个时候写到从的数据库里面去做一些读操作
也是可以的
其实大概率真的是不太会打死的
也没有这么没有这么猛
所以归并回源的思路非常重要
我们这里总结一下提的几个点
第一进程内用归并去读MySQL
然后用归并的思路写kafka
kafka的消费者comment-job
用预判定缓存是否存在来丢弃掉回源构建的这条指令
以及在一个时间段内缓存还没构建完的情况下
在进程内使用一个短时的flag
可能也就3秒钟5秒钟丢弃掉
kafka的一些回源的指令
那么效果打的好
这个还是很有意思的
这个我觉得这个回源思路其实还是蛮有效的
甚至说我们还可以更巧妙
短时的flag
我们甚至是不是可以用一个短时的LRU的一个评论缓存
你可以用短时的LRU的评论缓存也是可以的
这样的话我就不会频繁的再去查MySQL
短时的LRU换成这5秒内有一个人查了MySQL
就缓存住其他的这5秒内再去查
直接走local cache
那么如果local cache最终生效了
这个时候5秒后其实Redis早就构建好了
那么这时再去查Redis问题也不算特别大
也不算特别大
所以这个叫Singleflight
其实是解决穿透
尤其是缓存穿透非常重要的一个点
非常重要的一个点
我是强烈不建议使用分布式锁的
那么还有一个流量热点
是因为突然热门的主题被超高频次的访问
我们这个地方还有一个问题没解决
如果评论非常的热
那么会一致性集到某一个Redis节点
那么这个节点可能会真的被打挂
因为一致性哈希某一个主题一定命中到某一个节点
那么它的负载有可能会非常的高
即便是Redis某一个节点它10万QPS也不一定扛得住
所以说如果我们流量有这么大的话
真的是非常厉害了
这时候remote cache 你的Redis cluster可能也会变成瓶颈
怎么办
在我们这个场景里面
我们利用框架的能力能够自适应的发现热点是非常重要的
怎么做
其实很容易的在你的读接口里面
订阅你的变量
你的什么变量
你的主题的id去订阅变量
你做一个滑动窗口
去统计一个5秒的时间窗口内
一个5秒的时间窗口内有哪一些key被高频访问
那么我们在设计上
我们当时设计了一个环形数组
设计了一个环形数组
每一秒是一个bucket
那么比方第4秒
它内部是hashmap
那么这个key是主题id
那么count表示它读的次数
那么我用hashmap去把一秒内的读的数据缓存起来
那么它用个滑动窗口每次滑动到滑到第一秒的时候
我又会把数据给干掉
先把map清空
那么每滑动一秒
甚至我可以每定时的每5秒钟
我去查对象
把它所有的hashmap的值
每一个bucket的这些值
我导入到一个小堆
我倒知道一个minhip
把它全部导入小堆
然后基于小堆
基于这个5秒的小堆
我可以统计出top k的数据
也就是top k的头部的这些数据
那么我比方说我设定一个阈值
一个节点超过多少一个5秒时间窗口内
它超过多少的频次的访问
我把它变成一个把它变成一个什么
识别为一个热点
那么把识别热点的数据自动的接口
读的数据加上游标的一些信息
因为你有翻页码的信息
自动的把它缓存起来
变成一个热点的一个cache
那么牺牲一点点的一致性
因为你可以做个5秒的
哪怕你做一个就是因为超热数据
你做个5秒的local cache
它效果其实也是极好的
那么你把这个数据也是用lazy的方式
或者是用类似模拟一个sortedset这种方式把这个数据构建缓存起来
在你内存里面
注意一定要控制一下
你不要热点不能太多
因为你可能你local cache的内存也是有限的
你就缓缓存一部分的这些内容信息
你这样的话其实热点的问题也能够被解决
自适应去发现这些热点
然后把它缓存起来
那么对于突发的热门的这种流量
我认为也是扛得住的
所以这个其实就是我们的整体的评论设计的一些思想
从功能设计到它的架构设计
再到它的存储设计
再到它的可用性设计
一定是经过各个一些环节重要环节的一些思考
慢慢沉淀成一个复杂的系统
所以这套平民架构其实也承载了
我们评论系统非常大的
就是我们海量用户的一个非常大的访问量
到现在为止其实很少出事故
我们的确有出现过一些有损的case
比方说存储出现压力的
然后消息出现延迟
这种出现过
但是我们整体的读其实是很少出事故的
所以希望通过这一节的内容
让大家整体做架构设计上
按照我的那种构思方式
去尝试对你的自己系统进行一些剖析和演进
然后做一些这种可能性设计和存储设计
慢慢积累知识
这一块的内容我们今天就到这
到此为止了
好 感谢大家