分布式缓存设计的一些思考

最近工作中遇到一个需要使用分布式缓存的场景,缓存这个说烂了的话题,有人一听便没了兴趣,但如果要结合工作中遇到的场景具体仔细的分析,还是有一些收获的。

本文就是在这样的情况下,结合工作中遇到的场景,一些思考后的产物。


场景:

A 系统 需要把调用B系统的结果缓存起来,这样在缓存失效前,A都不用在请求B,直接返回缓存系统的结果就好了.

前提条件:

1).   A和B都是分布式系统,A无状态,B有状态

2).   A对B的一次请求有一定的耗时,比如500ms.

3).   B系统有一定的承受能力极限,比如QPS 1000.


看似简单的一个场景,缓存的设计可并不那么简单,我们一一来解说:


方案一,A系统各个节点把调用B系统的结果缓存在各个节点内存里,设置过期时间 , 

      定时任务(在缓存失效前)拿到各节点全部缓存的key,请求B,把A中所有的缓存结果都更新一次.


这种方案存在的问题:

(1). 最糟糕的情况下,B在某一时间点需要承受A所有节点所有缓存的key的全部请求,这可能造成B系统瞬间QPS急促上升,B的压力剧增,B如果扛不住挂了,整个A,B系统瘫痪.

(2). A各个节点都需要缓存一份缓存数据,A中的各个节点间有很大的数据冗余.


方案二,把A系统各个节点的缓存结果放在分布式存储组件里(如tair),定时任务去刷新缓存.

     这样的设计方案相比 方案一,有如下优点:

(1),把A中的各个节点的定时任务请求合并为一个定时任务请求了。

(2),把A中各个节点的缓存结果合并为一个结果了,减少了冗余。

      但也还存在一些问题:

(1),如果分布式存储组件里存储的key特别多,定时任务执行完一次的需要一定的周期, 但缓存的某些结果在失效后,定时任务更新前,有新的请求来拿这些结果,怎么办?

(2),每次把分布式缓存组件里的结果全部都更新一次,但这些缓存真的全部都用到了吗?有浪费吗?


方案三,同步哨兵请求+分布式缓存+双key(长生命周期)

方案三是同事已经设计了一种基于tair(阿里组件)的缓存方案,解决了上述方案二的问题。

原文链接: http://blog.lichengwu.cn/architecture/2015/06/14/distributed-cache/

该方案的原理如下:




一个缓存结果需要2个key来保证它的有效性和实时性:(1)key对应的分布式锁(dk)  和 (2)key 对应的缓存结果

并且 (1) 比 (2)的存活周期要短,短至少一个A到B的请求周期.  

这样,判断缓存结果 "失效“ 的依据就成了拿不到分布式缓存的锁或者拿到的是初次生成的锁。


上述方案一次完整的请求处理流程是:

1,去拿分布式缓存锁,如果没有锁则插入一把锁,如果有则看看是不是初次生成的锁,如果是,则执行2

2,更新锁(置为非初次),并且本次请求需要去B拿结果,并更新缓存,如果不是则执行3

3,正常拿key取分布式缓存对应的缓存结果并返回,没有则自旋等待比它早来的请求更新缓存。

注意:此处分布式的锁的生命周期比缓存的生命周期要短. 这是为了让分布式锁提前苏醒(在value缓存失效之前)去B拿结果,然后更新缓存.


这个方案, 我的理解的一个核心竞争力的优势是 “按需分配资源”,这个资源包括2部分:

1,分布式缓存里的存储空间.

2,A请求B,B响应A带来的系统和网络消耗.


极端情况下:

例如: 在某一时刻,请求A系统缓存key key1的有10个请求,而此时key1在分布式缓存系统里已经失效,上面的方案是如何处理这种情况的呢?

这10个请求都去争一个分布式锁,只有一个请求会拿到锁, 那么这个线程立马去B请求结果并更新缓存,其它9个线程自旋等待,直到拿到缓存结果立即返回(自旋有个超时设置).


上述方案已经很不错了,基本上能满足日常的需求了。

但它依然存在下列问题:

假设分布式锁的生命周期是30秒,缓存结果是35秒

第31秒,来了5个请求,其中一个请求拿到了分布式锁,其它四个请求发现缓存没过期,直接返回,拿到锁的那个线程去B拿最新的结果,并更新缓存. 

问题一:

5个线程中,有一个请求是走了全链路请求,其它四个命中缓存返回.

问题二:

每个请求都需要访问2次tair,能不能降低tair的访问次数.


方案四:异步哨兵请求+分布式缓存+双key(短生命周期)

这个方案是在同事哨兵请求的基础上做的改良版本,原理如下图所示:



上图中: 虚线=异步 , 实线=同步

同时,缓存失效的时间写入key对应的value中. 


预加载: 缓存未完全失效,但快要失效了的中间状态。本例中指:  30~35秒之间这一段时间的缓存状态


一次完整的请求流程变成如下:

(1),  请求缓存结果, 判断缓存的过期时间是否达到 "预加载" 的条件, 未达到直接返回 , 达到执行(2)

(2),  抢分布式锁dk,抢到发送”预加载“指令,没抢到直接返回


该方案的核心优势是:

1, 大多数请求只需要访问一次分布式存储组件(缓存未失效,且未达到 ”预加载“ 条件时).

2, 解决预加载期间,执行预加载请求的线程命中缓存( 全链路访问结果,相当于是没命中缓存).


看上去上述方案已经够好了,还有更好的方案吗?


方案五:方案四+本地内存缓存

分布式组件缓存固然好,但如果分布式组件的抗压能力并不好,能不能有效解决高并发带来的问题,每个节点把缓存的结果缓存在本地如何?

存本地有下面几个先决条件:

1,存储在内存的数据量要有限。不能像分布式缓存那样无所节制。

2,   缓存有效期内至少2次以上的请求该数据.


这次的实际实现方案里,我统计了线上的日志及请求分布状态,发现35%热门的请求集中在少量(1/300)的数据量上面.

如果我们把这1/300的数据缓存在内存中,看上去会有很不错的收获.


基于上面的这些思考和调研,我在同事设计的分布式缓存上做了新的设计和改良.

代码放置在我的github地址上:  https://github.com/kuntang/github_java_base/tree/master/src/main/java/com/java/base/distributedCache

期待下周五的全链路压测,看看它的效果吧.


  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值