vlambda博客
学习文章列表

Redis关系类数据的缓存该如何设计?

在日常的业务中,有很多关系类的数据,比如微博博主发表过的动态列表,资讯类产品一个频道下的新闻列表。相比于单条数据(比如每条动态、新闻)的缓存方案,在设计关系类数据的缓存方案时,考虑的点会多一些,主要有如下三点:

  1. 1. 需要有定长。缓存只保留最近的热数据,因此不太会将数据库的列表数据都加载到缓存中。例如某个博主发表过的动态列表数据,只会缓存最近500条。

  2. 2. 在增删列表中的数据项时,需要保证缓存和db数据一致性。否则比如博主新发了一条动态,db写入成功,但缓存写入失败,其他人在浏览该博主的动态列表,优先从缓存加载数据,就会看不到这条动态。

  3. 3. 需要尽可能提升资源的利用率。内存较硬盘来讲还是昂贵一些,因此对于每个列表数据,需要按照数据热度区分对待,设置不同的定长,让同等的内存空间(比如只给8G的总量)使用的更有效率一些。例如拿微博来说,对于一些大V,由于其动态列表被访问的频率较高,因此可以多缓存一些列表数据;对于普通用户来讲,列表被访问的频率较低,可以少缓存一些列表数据。

下面我们就来看看,针对这些问题,怎么来设计列表数据的缓存。

首先是常规的缓存结构设计,在Redis中一般使用zset来存储列表类的数据,key含有列表主体的唯一Id,member是列表项的唯一Id,score是13位毫秒级时间戳。例如拿微博博主发表过的动态列表来讲,缓存结构如下:

其次,需要结合业务数据的增长情况设置定长及过期时间,设置定长的目的是为了避免列表数据无限增长,会占用过多的内存及引起查询性能下降的问题;设置过期时间的目的是为了让那些在一定时间内被没有访问的数据失效,提高内存的利用率。

对于设置定长,一个需要特别注意的点是当某个列表数据量小于定长长度时,需要在zset(按score降序)末尾追加一个tail标记(score=-1),标明缓存中是全部数据,防止查询(比如查尾页)时的缓存穿透。

接下来我们来看看缓存方案的详细设计,总体来说,以读时回源的思路(即用户加载列表数据时回源构建缓存)来设计整体方案。

新增一条关系数据

Redis关系类数据的缓存该如何设计?

对上图做一些说明:

  1. 1. 如果缓存key不存在,用户下次读时才会构建缓存,因此只需要写入db就可以了;

  2. 2. 每次zadd都需要设置(延长)过期时间;

  3. 3. 写缓存要有重试机制,尽可能保证db写成功后缓存也要写成功,否则数据会不一致。如果超过重试次数缓存仍没写成功,放入消息队列进行稍后的异步重试,必要时可采取人工介入的方式处理;

  4. 4. 对于热点的列表主体,需要有缓存预加载机制,不能只依赖上图的流程。即比如对于一些资讯类产品,上线一个热门主题,在创建主题的同时就需要把该主题的列表数据缓存创建上,否则在高并发的情况下读列表数据的操作全部打到db,很容易把db打崩;

  5. 5. 截定长的逻辑可以优化。举个例子:比如对于群聊天来说,一个发言很活跃的500人大群,有可能大部分用户都不怎么看信息,那么如果每一条新信息都要对500个人的消息列表截一次定长,会造成瞬时的写压力。优化的方式是不必每次都截,对于缓存定长为a,可以设置一个强制截断上限b,然后按照下图的方式来分散写压力:

    • • 当缓存长度在a~b之间时,每次截定长都算一个概率来决定是否要截,如果命中这个概率,就截,否则就不截;

    • • 当每次都没命中这个概率,且缓存长度达到b时,强制截断。

Redis关系类数据的缓存该如何设计?

删除一条关系数据

Redis关系类数据的缓存该如何设计?

对上图做一些说明:

  1. 1. 如果zrem success为否,说明有可能删的是旧数据,因此也需要删db;

  2. 2. 如果zset last member是tail,说明缓存中的是全部数据,因此不需要删db。

查询列表数据

查询需要考虑的场景比新增、删除来讲要多一些,主要有以下6种:

Redis关系类数据的缓存该如何设计?

对于上面的这些场景,假设要查的列表每页数据条数为n,整个流程如下:

Redis关系类数据的缓存该如何设计?

说明一下上图中的一些逻辑点:

  1. 1. 查n+1条缓存数据,这里不查n条数据的原因是可以少一步逻辑判断:当cache.size=n时根据cache.lastElement是否为tail返回n条还是n-1条数据;

  2. 2. cache.size>0为否时,有两种情况:key不存在,或者查询数据的范围超出了缓存数据的范围;

  3. 3. 异步重建缓存可以根据业务线上访问量考虑是否需要使用分布式锁保证同一时刻只允许存在一个重建任务,如果缓存数据失效且用户并发读列表数据的情况不是很多,可以先不用分布式锁。

来看一下上图中异步重建缓存的流程(假设缓存长度还是a):

上图中,还要再查一次db增量数据的原因是考虑到有可能在重建缓存的过程中会有新增的关系数据,这样就可以避免重建时出现缓存和db数据不一致的情况。

最后简单说一下文章最开头提到的第3点问题,如何提升资源的利用率?可以从统计线上列表数据的业务访问量来入手,明确有哪些热点的列表主体,根据不同热度的列表主体给予不同档位的缓存定长。比如目前有三档缓存定长,分别是100、200、500,假设对于微博博主这个场景来讲,大V给500的缓存定长,普通用户给100的缓存定长。这样,在提高内存利用率的同时,对于大V来讲,由于其列表数据更频繁地被用户访问,较长的缓存也能降低从db回源旧数据的概率。

以上就是如何来设计Redis关系类数据缓存的全部内容了。相较于单条数据(比如每条动态、新闻)的缓存方案,关系类数据在设计缓存时要考虑的细节点确实要多一些。最后,留一道思考题给你:对于方案的落地,代码如何来写能更通用一些(即不要A业务数据写一套代码,B业务数据也写一套代码,只有数据不一样,大体的流程都一样)?欢迎把你的思考发表在评论区,另外,如果对文中的内容有疑问,也欢迎在评论区与我交流沟通。