java 缓存架构剖析--本地缓存(LoadingCache)

缓存的使用可以大大提高程序的执行效率,但是如果缓存无法及时更新会导致脏读的情况。

痛点剖析:

记得早期我呆过的一家公司有个核心服务是在启动的时候一下把常用的交易配置信息是从DB查出来放在Map里面来做缓存,先不考虑其他的,如果我想更新一下交易配置信息是不是需要每次都重启服务器呢,又或者说我开几个后门接口用来更新Map信息,这样不还得考虑线程安全的问题么。

好吧,我先上个在中小型项目中,乃至大型项目中也常用的缓存架构,如下:

内存架构图

我大概解释一下流程吧:

1、系统A中使用LoadingCache来维护本地缓存信息

2、当缓存刷新时(同步、异步)调用B系统来更新缓存信息

3、系统B接收A获取配置数据的请求,如果redis缓存中有数据就直接从redis中拿

4、当缓存中不存在请求则穿透到DB里面查询再将结果塞到redis,并返回结果

5、其实还有一步没画出来应该是有个定时job轮询DB配置信息变化时刷新redis信息(或者消息机制来实现缓存更新)

言归正传,下面来详解一下LoadingCache的使用:

public static LoadingCache<String,String> cahceBuilder = CacheBuilder.newBuilder().maximumSize(1).

// expireAfterWrite(1, TimeUnit.SECONDS)

.refreshAfterWrite(2, TimeUnit.MILLISECONDS)

.removalListener(new RemovalListener() {

@Override

public void onRemoval(RemovalNotificationrn) {

System.out.println(rn.getKey() + "被移除");}})

.build(new CacheLoader() {

@Override

public String load(String key) throws Exception {

String strProValue = "hello " + key + "!";

System.out.println("%%%%%" + strProValue);

return strProValue;

}});

public static void main(String[] args) throws ExecutionException, InterruptedException {

cahceBuilder.get("jerry");

cahceBuilder.get("peida");

Thread.sleep(1000);

cahceBuilder.get("jerry1");

}

输出结果为:

%%%%%hello jerry!                            -- 在第一次get的时候没有值会执行load方法,去取值然后塞到本地缓存

%%%%%hello peida!                          -- 在第一次get的时候没有值会执行load方法,去取值然后塞到本地缓存

jerry被移除                                      -- maximumSize(1) 最大值为1,当预存储第二个值的时候第一个值会被移除

%%%%%hello jerry1!                         -- refreshAfterWrite设置2ms自动定时刷新,当有访问时会重新执行load方法更新缓存

peida被移除                                   -- maximumSize(1) 最大值为1,当预存储第二个值的时候第一个值会被移除

方法剖析:

get(K):这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地loading新值(就是上面说的当缓存没有值的时候执行Load方法)

put(key, value):这个方法可以直接显示地向缓存中插入值,这会直接覆盖掉已有键之前映射的值。

缓存回收:

CacheBuilder.maximumSize(long):这个方法规定缓存项的数目不超过固定值(其实你可以理解为一个Map的最大容量),尝试回收最近没有使用或总体上很少使用的缓存项

定时回收(Timed Eviction):

expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。

expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。

显式清除:

任何时候,你都可以显式地清除缓存项,而不是等到它被回收:

个别清除:Cache.invalidate(key)   批量清除:Cache.invalidateAll(keys)   清除所有缓存项:Cache.invalidateAll()

移除监听器

通过CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值。

就如我上面的例子一样,当内存回收或者定时回收都会执行removalListener

不过亲测当有数据refresh刷新额度时候也会触发这个监听功能

警告:默认情况下,监听器方法是在移除缓存时同步调用的。因为缓存的维护和请求响应通常是同时进行的,代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求。在这种情况下,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把监听器装饰为异步操作

刷新,这应该会是我重点讲的:

LoadingCache.refresh(K):刷新和回收不太一样。刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。如果刷新过程抛出异常,缓存将保留旧值,而异常会在记录到日志后被丢弃[swallowed]。重载CacheLoader.reload(K, V)可以扩展刷新时的行为,这个方法允许开发者在计算新值时使用旧的值。

CacheBuilder.refreshAfterWrite(long, TimeUnit):可以为缓存增加自动定时刷新功能。和expireAfterWrite相反,refreshAfterWrite通过定时刷新可以让缓存项保持可用,但请注意:缓存项只有在被检索时才会真正刷新,即只有刷新间隔时间到了你再去get(key)才会重新去执行Loading否则就算刷新间隔时间到了也不会执行loading操作。因此,如果你在缓存上同时声明expireAfterWrite和refreshAfterWrite,缓存并不会因为刷新盲目地定时重置,如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也变得可以回收。还有一点比较重要的是refreshAfterWrite和expireAfterWrite两个方法设置以后,重新get会引起loading操作都是同步串行的。这其实可能会有一个隐患,当某一个时间点刚好有大量检索过来而且都有刷新或者回收的话,是会产生大量的请求同步调用loading方法,这些请求占用线程资源的时间明显变长。如正常请求也就20ms,当刷新以后加上同步请求loading这个功能接口可能响应时间远远大于20ms。为了预防这种井喷现象,可以不设置CacheBuilder.refreshAfterWrite(long, TimeUnit),改用LoadingCache.refresh(K)因为它是异步执行的,不会影响正在读的请求,同时使用ScheduledExecutorService可以帮助你很好地实现这样的定时调度,配上cache.asMap().keySet()返回当前所有已加载键,这样所有的key定时刷新就有了。如果访问量没有这么大则直接用CacheBuilder.refreshAfterWrite(long, TimeUnit)也可以。这个可以评估自己的项目实际情况来决策。

还是上代码:

public class LoadingCacheTest {

private static  ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

private static class EchoServer implements Runnable {

@Override

public void run() {              

try {            

Set<String> keys = cahceBuilder.asMap().keySet();

for(String key : keys){

cahceBuilder.refresh(key);

}

}catch (Exception e) {

}

}}

public static LoadingCachecahceBuilder = CacheBuilder.newBuilder().maximumSize(10).

removalListener(new RemovalListener() {

@Override

public void onRemoval(RemovalNotificationrn) {

System.out.println(rn.getKey() + "被移除");

}).build(new CacheLoader() {

@Override

public String load(String key) throws Exception {

String strProValue = "hello " + key + "!";

System.out.println("%%%%%" + strProValue);

return strProValue;

}});

public static void main(String[] args) throws Execution {

System.out.println(cahceBuilder.get("jerry"));

System.out.println(cahceBuilder.get("peida"));

cahceBuilder.get("jerry1");

executor.scheduleAtFixedRate(new EchoServer(),0,1000,TimeUnit.MILLISECONDS);

}}

其他特性

统计

CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后,Cache.stats()方法会返回CacheStats对象以提供如下统计信息:

hitRate():缓存命中率;

averageLoadPenalty():加载新值的平均时间,单位为纳秒;

evictionCount():缓存项被回收的总数,不包括显式清除

此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。

asMap视图

asMap视图提供了缓存的ConcurrentMap形式,但asMap视图与缓存的交互需要注意:

cache.asMap()包含当前所有加载到缓存的项。因此相应地,cache.asMap().keySet()包含当前所有已加载键;

asMap().get(key)实质上等同于cache.getIfPresent(key),而且不会引起缓存项的加载。这和Map的语义约定一致。

所有读写操作都会重置相关缓存项的访问时间,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合视图上的操作。比如,遍历Cache.asMap().entrySet()不会重置缓存项的读取时间。

最后,欢迎一起讨论,谢谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • com.google.common.cache 1、背景 缓存,在我们日常开发中是必不可少的一种解决性能问题的方法...
    拾壹北阅读 22,210评论 0 25
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • Java程序的运行 配置classpath环境变量() 1. 将java文本文件所在目录放置于classpa...
    29画阅读 243评论 0 0
  • 人在旅途,听着歌,望着窗外的景,几分惬意,几分惆怅。 想写几行字,写写删删,最后不落一字。有时候的心情,挑不出恰当...
    茶润人生阅读 89评论 0 0
  • 路过的小游侠 + 《软件工程(C编码实践篇)》MOOC课程作业http://mooc.study.163.com/...
    流浪的乞丐阅读 314评论 0 0