一、背景

缓存功能业务在所有公司里面都是很重要的,做为提升性能的重要法宝之一,具有举足轻重的地位,所在公司业务有一种场景需要使用缓存进行性能的改善,举例场景:A Service(提供对外服务,性能550ms)调用 B Service(性能受到多种制约无法优化,性能500ms),这个时候性能优化就需要寄希望于缓存这个法宝。

二、方案选择

由于我们选择使用缓存,就必须接受缓存所带来的问题,数据的实时性就会降低,但是我们又希望对数据的实时性影响要尽量的小,如果将TTL设置太小,那么缓存就会大量失效,可能会导致比没有缓存的性能还差,如果缓存的时间太长,那么数据实时性影响就会非常大,因此调研对比了Varnish、Redis、Guava Cache、Caffeine Cache,决定结合缓存的优点开发一款兼顾数据实时性和性能,还必须支持分布式的Cache(因为Service为分布式部署,本地内存缓存容易造成数据一致性问题)

名称 优点 不足 备注 |
Redis 可以实现分布式缓存;无法主动刷新 有网络损耗
Guava Cache 提供了主动刷新机制,可以实现长效缓存; 仅本地内存缓存,无法分布式; API实现较为复杂; Guava
Varnish 可以实现分布式; 天然Service缓存;自带Grace功能; 无法主动刷新数据;Grace提供的数据影响数据实时性; Varnish
Caffeine Cache 提供了主动刷新机制,可以实现长效缓存; 仅本地内存缓存,无法分布式;基于Guava Cache API开发使用更加简单 Caffeine

三、设计

3.1 设计目标

为了设计一款能分布式使用,且具有长效存储和兼顾数据实时性的缓存,我们可以结合Guava Cache和Redis来进行设计,因此我们进行如下设计:

  1. refreshAfterWrite, 在写入一定时间后,监听会主动刷新缓存
  2. expireAfterAccess,距离最后的缓存访问时间间隔超过了允许的时间,数据即失效,并且不再进行监听刷新。
  3. 统一的分布式存储,提供网络访问方式,统一存储。

3.2 设计方案

  1. 以Redis Cluster为存储媒介
  2. expireAfterAccess特性,使用Redis Key设置TTL实现,每次读取成功重新设置TTL
  3. refreshAfterWrite特性,设计一个监听器,监听Key写入的时间,根据设置的时间获取有效的缓存数据进行异步刷新,但是不更新数据的TTL。
  4. 缓存数据设计使用Redis的String类型存储,KV设计为,

 K:${Bussiness}-${Hash(Key)}   V: {"key":${Key},"value":${Value}}  Redis的Key设计使用业务名称加上缓存数据的Key对象的Hash值,Redis Value采用Json字符串存储(当然也设计可以使用二进制序列化数据,但是为了排查问题方便,设计为可读的Json字符较为方便)

  1. 监听器设计为异步线程获取Redis一个Set数据,一个监听器的KV设计为:

K:${Bussiness}-Listener-${DataTime}  V:[${Bussiness}-${Hash(Key)} ] Redis Key设计为按照时间产生的监听器,并不是按照每个缓存对象一个监听器,不然监听器的规模过于庞大,Redis Value是一个Set集,里面数据为缓存对象的Redis Key

  1. 需设计一个分布式锁,由于微服务部署一般为分布式多节点部署,因此我们需要通过分布式锁控制监听器的执行权限,获取锁的监听线程,才能执行监听器,执行缓存数据的刷新动作,缓存刷新后,删除监听器的Redis Key,并释放锁并删除锁,重新设置下一轮监听器。

分布式锁采用Redis设计,Redis Key为:${Bussiness}-Lock-${DataTime}

3.3 架构设计图

缓存分为三个模块角色设计,缓存操作模块、监听模块、缓存刷新模块 架构图

四、项目源代码

项目地址