帖子回复列表缓存优化日志

原文链接:https://juejin.im/post/5992b3fa6fb9a03c315008bf

尝试写点工作日志,也算是对平日工作的一些思考和总结。这次主要谈谈刚加入贝聊时接下的一个工作任务,对帖子回复列表接口做优化。

一、业务场景

类似于论坛、社区,有各种各样的帖子,用户可以在帖子下回复互动。

二、现状

目前帖子回复列表接口只对前两页数据做缓存,缓存时间为五分钟,string数据结构。当有新回复或者其它更新操作,则删除缓存。

三、存在问题

只对前两页做缓存,其它页都是直接从DB获取,遇到比较火的帖子会有大量请求到达DB,给DB造成压力。

四、目标

在尽可能少改动的前提下,提高帖子回复列表接口的缓存命中率,让绝大部分请求都能直接从缓存返回,减轻DB压力。

五、设计

使用Redis的Sorted Sets结构缓存帖子的回复信息,回复id当score,回复信息当member。

用增量方式更新缓存,有新回复,直接追加到sortedSet。同样,若删除回复,从Sorted Sets中移走该条回复。

利用Sorted Sets的排序功能,可直接获取某一页的数据返回。

说明:

1、考虑到实际场景,这里并没有缓存某个帖子所有的回复列表。因为用户翻查回复列表,大多数集中在前面几页,后面的极少会翻到,所以我们只缓存了400条数据,按一页20条数据,即20页。如果是超出20页的请求,则直接从DB获取数据返回。

2、帖子回复列表根据回复时间排序,新帖子在前面展示。这里用id当score,是因为我们的回复列表id是自增的,严格与回复创建时间保持一致的顺序。通过ZREVRANGE命令从大到小返回。

若id非自增,可以用回复时间当score。不过时间可能存在重复,若两个回复的时间一致,在查询列表时不存在问题,只是在删除具体某个回复时,不能通过ZREMRANGEBYSCORE命令删除(相同时间不能唯一确定一条回复数据)。

六、优点

1、在更新频繁情况下,依然能保持缓存高命中率。

例如运营在搞盖楼活动时,用户都在抢楼,帖子回复列表更新频繁。如果按之前方案,则前两页缓存会处于“建立-删除-建立”这么一个循环当中,命中率很低。同时还会有大量非前两页请求,DB压力非常大。

新方案是对缓存做增量更新,即使用户拼命抢楼,也能保证前面20页的数据命中缓存,减低DB压力。

2、在访问非前两页数据时,也能命中缓存。

虽然非前两页的请求量相对要低,但在总请求量很大的情况下,穿透缓存访问DB的量还是需要考虑的。这里提供前面20页的缓存,基本覆盖绝大部分请求。毕竟用户在一个帖子下翻了几十页的回复,这种情况基本没有。同时,在产品上,最好不要提供直接跳转第几页的功能。

七、优化前后数据对比

14号上线,对比上线前后4天的统计数据如下八、可能存在的问题

1、数据一致性:

若在增量更新缓存时操作失败,则缓存数据会跟DB数据不一致。

可以通过增加重试机制,降低概率。

或者先更新缓存,发布一个消息到消息队列,异步更新DB。

2、在缓存过期的瞬间,如果并发很高,可能存在多个请求做同样的操作(从DB获取数据,再set到缓存)

可以通过设置一个全局的标记位,若标记位已设置,则只执行从DB获取数据,不需要set到缓存。保证同时只有一个线程在执行set到缓存这一操作。

当然,若不作处理,对数据也无影响。

3、同样在缓存过期的瞬间,若同时存在查询和回复帖子操作,则有可能导致数据不一致

举个例子:

线程A查询列表,发现当前缓存不存在,则从DB获取数据,然后set到缓存。同时,线程B新增一回复,发现当前缓存不存在,直接更新DB。则有可能线程A set到缓存的数据没有包括线程B新增的回复。简单画个图如下:

解决方案:

将添加回复数据记录下来,在线程A将回复列表set到缓存后执行一个回调,将新增加的回复数据更新到缓存。补充:

(1)如果线程A获取的数据已经包含了新回复数据newReply,则线程在执行回调时会重复添加newReply数据到缓存,不过Sorted Set会自动识别为同一条数据,对结果无影响。

(2)如果线程A在线程B记录下回复数据newReply前就完成执行回调操作(此时newReply数据为空),则会导致DB、缓存数据不一致。

针对第(2)种情况,可以考虑在线程A执行完“将数据列表set到缓存”操作后,延时执行回调(例如5秒),基本可以解决此类问题。

4、若业务对此类数据没有强一致性要求,则以上三点均可不考虑。


展开阅读全文

请教论坛帖子回复设计

09-04

想设计DISCUZ这样的论坛帖子效果,用户可以对帖子进行回复,也可以对回复内容进行回复。我看了一下discuz的设计是评论有专门的数据库,应该是需要用到递归查找。rn如果是用下面的办法:rn[code=sql]drop database recur;rncreate database recur;rnuse recur;rn rn rnCREATE TABLE `treenodes` (rn `id` int(11) NOT NULL,rn `nodename` varchar(20) DEFAULT NULL,rn `pid` int(11) DEFAULT NULL,rn PRIMARY KEY (`id`)rn) ENGINE=InnoDB DEFAULT CHARSET=utf8;rn rn rnINSERT INTO `treenodes` VALUES ('1', 'A', '0');rnINSERT INTO `treenodes` VALUES ('2', 'B', '1');rnINSERT INTO `treenodes` VALUES ('3', 'C', '1');rnINSERT INTO `treenodes` VALUES ('4', 'D', '2');rnINSERT INTO `treenodes` VALUES ('5', 'E', '2');rnINSERT INTO `treenodes` VALUES ('6', 'F', '3');rnINSERT INTO `treenodes` VALUES ('7', 'G', '6');rn rnrndelimiter //rnCREATE PROCEDURE showChildList (IN rootId INT,IN)rn rnBEGINrn CREATE TEMPORARY TABLE IF NOT EXISTS tmpLstrn (sno int primary key auto_increment,rn id int,rn depth intrn );rn DELETE FROM tmpLst;rn rn rn CALL createChildLst(rootId,0);rn rn select tmpLst.*,treeNodes.*rn from tmpLst,treeNodesrn where tmpLst.id = treeNodes.id rn order by tmpLst.sno;rn rnEND;rn //rn rnCREATE PROCEDURE createChildLst (IN rootId INT,IN nDepth INT)rnBEGINrn DECLARE done INT DEFAULT 0;rn DECLARE b INT;rn DECLARE cur1 CURSOR FOR SELECT id FROM treeNodes where pid=rootId;rn DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;rn rn insert into tmpLst values (null,rootId,nDepth);rn rn rn OPEN cur1;rn rn FETCH cur1 INTO b;rn WHILE done=0 DOrn CALL createChildLst(b,nDepth+1);rn FETCH cur1 INTO b;rn END WHILE;rn rn CLOSE cur1;rnEND;rn //rn delimiter ;rncall showChildList(1);rncall showChildList(3);rncall showChildList(5);[/code]rn如果多个会话同时调用showChildList 的话,一个临时数据库会不会不安全,我想过一个rootid建一个临时数据库也不现实,请问有没有更好的办法解决? 谢谢。 论坛

没有更多推荐了,返回首页