论获取缓存值的正确姿势

论获取缓存值的正确姿势

cache

时至今日,大家对缓存想必不在陌生。我们身边各种系统中或多或少的都存在缓存,自从有个缓存,我们可以减少很多计算压力,提高应用程序的QPS。

你将某些需要大量计算或查询的结果,设置过期时间后放入缓存。下次需要使用的时候,先去缓存处查询是否存在缓存,没有就直接计算/查询,并将结果塞入缓存中。

Object result = cache.get(CACHE_KEY);if(result == null){    //重新获取缓存
    result = xxxx(xxx);
    cache.put(CACHE_KEY,CACHE_TTL,result); 
}return result;

Bingo~~,一切都在掌握之中,程序如此完美,可以支撑更大的访问压力了。

不过,这样的获取缓存的逻辑,真的没有问题吗?


高并发下暴露问题

你的程序一直正常运行,直到某一日,运营的同事急匆匆的跑来找到你,你的程序挂了,可能是XXX在大量抓你的数据。我们重启了应用也没用,没几秒程序又挂了。

机智的你通过简单的排查,得出数据库顶不住访问压力,顺利的将锅甩走。 不过仔细一想,我们不是有缓存吗,怎么缓存没起作用? 查看下缓存,一切正常,也没发现什么问题啊?

进过各种debug、查日志、测试环境模拟,花了整整一下午,你终于找到罪魁祸首,原因很简单,正是我们没有使用正确的姿势使用缓存~~~


问题分析

这里我们排除熔断、限流等外部措施,单纯讨论缓存问题。

假设你的应用需要访问某个资源(数据库/服务),其能支撑的最大QPS为100。为了提高应用QPS,我们加入缓存,并将缓存过期时间设置为X秒。此时,有个200并发的请求访问我们系统中某一路径,这些请求对应的都是同一个缓存KEY,但是这个键已经过期了。此时,则会瞬间产生200个线程访问下游资源,下游资源便有可能瞬间就奔溃了~~~

我们有什么更好的方法获取缓存吗?当然有,这里通过guava cache来看下google是怎么处理获取缓存的。


guava 和 guava cache

guava是一个google发布的一个开源java工具库,其中guava cacha提供了一个轻量级的本地缓存实现机制,通过guava cache,我们可以轻松实现本地缓存。其中,guava cacha对缓存不存在或者过期情况下,获取缓存值得过程称之为Loading。

直接上代码,看看guava cache是如何get一个缓存的。

        V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
            ...            try {                if(this.count != 0) {
                    LocalCache.ReferenceEntry ee = this.getEntry(key, hash);                    if(ee != null) {                        long cause1 = this.map.ticker.read();
                        Object value = this.getLiveValue(ee, cause1);                        if(value != null) {                            this.recordRead(ee, cause1);                            this.statsCounter.recordHits(1);
                            Object valueReference1 = this.scheduleRefresh(ee, key, hash, value, cause1, loader);                            return valueReference1;
                        }

                        LocalCache.ValueReference valueReference = ee.getValueReference();                        if(valueReference.isLoading()) {
                            Object var9 = this.waitForLoadingValue(ee, key, valueReference);                            return var9;
                        }
                    }
                }

                Object ee1 = this.lockedGetOrLoad(key, hash, loader);                return ee1;
            } catch (ExecutionException var13) {
                ...
            } finally {
                ...
            }
        }

可见,核心逻辑主要在scheduleRefresh(...)和lockedGetOrLoad(...)中。

先看和lockedGetOrLoad,

        V lockedGetOrLoad(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
            LocalCache.ValueReference valueReference = null;
            LocalCache.LoadingValueReference loadingValueReference = null;
            boolean createNewEntry = true;            //先加锁
            this.lock();

            LocalCache.ReferenceEntry e;            try {                long now = this.map.ticker.read();                this.preWriteCleanup(now);                int newCount = this.count - 1;
                AtomicReferenceArray table = this.table;                int index = hash & table.length() - 1;
                LocalCache.ReferenceEntry first = (LocalCache.ReferenceEntry)table.get(index);                for(e = first; e != null; e = e.getNext()) {
                    Object entryKey = e.getKey();                    if(e.getHash() == hash && entryKey != null && this.map.keyEquivalence.equivalent(key, entryKey)) {
                        valueReference = e.getValueReference();                        //判断是否有其他线程正在执行loading动作
                        if(valueReference.isLoading()) {
                            createNewEntry = false;
                        } else {
                            Object value = valueReference.get();                            if(value == null) { 
                                this.enqueueNotification(entryKey, hash, valueReference, RemovalCause.COLLECTED);
                            } else {                                //有值且没有过期,直接返回
                                if(!this.map.isExpired(e, now)) {                                    this.recordLockedRead(e, now);                                    this.statsCounter.recordHits(1);
                                    Object var16 = value;                                    return var16;
                                }   
                                this.enqueueNotification(entryKey, hash, valueReference, RemovalCause.EXPIRED);
                            }                            this.writeQueue.remove(e);                            this.accessQueue.remove(e);                            this.count = newCount;
                        }                        break;
                    }
                }                
                //创建一个LoadingValueReference
                if(createNewEntry) {
                    loadingValueReference = new LocalCache.LoadingValueReference();                    if(e == null) {
                        e = this.newEntry(key, hash, first);
                        e.setValueReference(loadingValueReference);
                        table.set(index, e);
                    } else {
                        e.setValueReference(loadingValueReference);
                    }
                }
            } finally {
               ...
            }            if(createNewEntry) {
                Object var9;                try {                    //没有其他线程在loading情况下,同步Loading获取值
                    synchronized(e) {
                        var9 = this.loadSync(key, hash, loadingValueReference, loader);
                    }
                } finally {                    this.statsCounter.recordMisses(1);
                }                return var9;
            } else {                //等待其他线程返回值
                return this.waitForLoadingValue(e, key, valueReference);
            }
        }

可见正常情况下,guava会单线程处理回源动作,其他并发的线程等待处理线程Loading完成后直接返回其结果。这样也就避免了多线程同时对同一资源并发Loading的情况发生。

不过,这样虽然只有一个线程去执行loading动作,但是其他线程会等待loading线程接受后才能一同返回接口。此时,guava cache通过刷新策略,直接返回旧的缓存值,并生成一个线程去处理loading,处理完成后更新缓存值和过期时间。guava 称之为异步模式。

V scheduleRefresh(LocalCache.ReferenceEntry<K, V> entry, K key, int hash, V oldValue, long now, CacheLoader<? super K, V> loader) {        if(this.map.refreshes() && now - entry.getWriteTime() > this.map.refreshNanos && !entry.getValueReference().isLoading()) {
            Object newValue = this.refresh(key, hash, loader, true);            if(newValue != null) {                return newValue;
            }
        }        return oldValue;
    }

Refreshing is not quite the same as eviction. As specified in LoadingCache.refresh(K), refreshing a key loads a new value for the key, possibly asynchronously. The old value (if any) is still returned while the key is being refreshed, in contrast to eviction, which forces retrievals to wait until the value is loaded anew.

此外guava还提供了同步模式,相对于异步模式,唯一的区别是有一个请求线程去执行loading,其他线程返回过期值。(目前spirng cache中,还未支持guava cahce的同步刷新)

@Beta@GwtIncompatible("To be supported (synchronously).")public CacheBuilder<K, V> refreshAfterWrite(long duration, TimeUnit unit) {
    Preconditions.checkNotNull(unit);
    Preconditions.checkState(this.refreshNanos == -1L, "refresh was already set to %s ns", new Object[]{Long.valueOf(this.refreshNanos)});
    Preconditions.checkArgument(duration > 0L, "duration must be positive: %s %s", new Object[]{Long.valueOf(duration), unit});    this.refreshNanos = unit.toNanos(duration);    return this;
}

总结

看似简单的获取缓存值的业务逻辑没想到还暗藏玄机。当然,这里guava cache只是本地缓存,如果依葫芦画瓢用在redis等分布式缓存时,势必还要考虑更多的地方。

最后,如果喜欢本文,请点赞~~~~

原文发布于微信公众号 - Golang语言社区(Golangweb)

原文发表时间:2016-10-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大史住在大前端

一统江湖的大前端(4)shell.js——穿上马甲我照样认识你

码农界存在着无数条鄙视链,linux使用者对windows的鄙视便是其中之一,cli使用者对GUI用户的嘲讽也是如此,在这样一个讲究逼格的时代,如果你的桌面上没...

2095
来自专栏小灰灰

Quick-Task 动态脚本支持框架之使用介绍篇

文章链接:https://liuyueyi.github.io/hexblog/2018/07/19/180719-Quick-Task-动态脚本支持框架之使用...

1162
来自专栏Java 源码分析

Netty 入门

1. 粘包问题 一 .长连接与短连接: 1.长连接:Client方与Server方先建立通讯连接,连接建立后不断开, 然后再进行报文发送和接收。长连接在 net...

3857
来自专栏小曾

.Net Web开发技术栈

有很多朋友有的因为兴趣,有的因为生计而走向了.Net中,有很多朋友想学,但是又不知道怎么学,学什么,怎么系统的学,为此我以我微薄之力总结归纳写了一篇.Net w...

3363
来自专栏涤生的博客

天池中间件大赛——单机百万消息队列存储设计与实现

这次天池中间件性能大赛初赛和复赛的成绩都正好是第五名,本次整理了复赛《单机百万消息队列的存储设计》的思路方案分享给大家,实现方案上也是决赛队伍中相对比较特别的。

1991
来自专栏安恒网络空间安全讲武堂

MOCTF WEB 题解

0x00 MOCTF平台是CodeMonster和Mokirin这两支CTF战队所搭建的一个CTF在线答题系统。网址是http://www.moctf.com/...

7779
来自专栏Java 源码分析

Netty 入门

1. 粘包问题 一 .长连接与短连接: 1.长连接:Client方与Server方先建立通讯连接,连接建立后不断开, 然后再进行报文发送和接收。长连接在 net...

3005
来自专栏高性能服务器开发

(六)关于网络编程的一些实用技巧和细节

这些年,接触了形形色色的项目,写了不少网络编程的代码,从windows到linux,跌进了不少坑,由于网络编程涉及很多细节和技巧,一直想写篇文章来总结下这方面的...

4335
来自专栏大内老A

[WCF-Discovery]让服务自动发送上/下线通知[原理篇]

到目前为止,我们所介绍的都是基于客户端驱动的服务发现模式,也就是说客户端主动发出请求以探测和解析可用的目标服务。在介绍WS-Discovery的时候,我们还谈到...

2186
来自专栏美团技术团队

Node.js Stream - 进阶篇

在构建较复杂的系统时,通常将其拆解为功能独立的若干部分。这些部分的接口遵循一定的规范,通过某种方式相连,以共同完成较复杂的任务。譬如,shell通过管道|连接各...

4174

扫码关注云+社区

领取腾讯云代金券