一、背景
缓存功能业务在所有公司里面都是很重要的,做为提升性能的重要法宝之一,具有举足轻重的地位,所在公司业务有一种场景需要使用缓存进行性能的改善,举例场景: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来进行设计,因此我们进行如下设计:
- refreshAfterWrite, 在写入一定时间后,监听会主动刷新缓存
- expireAfterAccess,距离最后的缓存访问时间间隔超过了允许的时间,数据即失效,并且不再进行监听刷新。
- 统一的分布式存储,提供网络访问方式,统一存储。
3.2 设计方案
- 以Redis Cluster为存储媒介
- expireAfterAccess特性,使用Redis Key设置TTL实现,每次读取成功重新设置TTL
- refreshAfterWrite特性,设计一个监听器,监听Key写入的时间,根据设置的时间获取有效的缓存数据进行异步刷新,但是不更新数据的TTL。
- 缓存数据设计使用Redis的String类型存储,KV设计为,
K:${Bussiness}-${Hash(Key)} V: {"key":${Key},"value":${Value}} Redis的Key设计使用业务名称加上缓存数据的Key对象的Hash值,Redis Value采用Json字符串存储(当然也设计可以使用二进制序列化数据,但是为了排查问题方便,设计为可读的Json字符较为方便)
- 监听器设计为异步线程获取Redis一个Set数据,一个监听器的KV设计为:
K:${Bussiness}-Listener-${DataTime} V:[${Bussiness}-${Hash(Key)} ] Redis Key设计为按照时间产生的监听器,并不是按照每个缓存对象一个监听器,不然监听器的规模过于庞大,Redis Value是一个Set集,里面数据为缓存对象的Redis Key
- 需设计一个分布式锁,由于微服务部署一般为分布式多节点部署,因此我们需要通过分布式锁控制监听器的执行权限,获取锁的监听线程,才能执行监听器,执行缓存数据的刷新动作,缓存刷新后,删除监听器的Redis Key,并释放锁并删除锁,重新设置下一轮监听器。
分布式锁采用Redis设计,Redis Key为:${Bussiness}-Lock-${DataTime}
3.3 架构设计图
缓存分为三个模块角色设计,缓存操作模块、监听模块、缓存刷新模块
四、项目源代码