微分享-高并发下的缓存实战

场景

统计一个批量接口会有多少数据,这个接口的QPS在100万级别。有几种方案:

  1. 每次调用都串行计算一次;
  2. 每次调用使用线程池并行计算。

由于并发量特别的大,第1种场景肯定不适合,这会把相应时间拉长。第二种方法每次请求过来都放到一个线程池里面请求,比第一种强很多,用这种方式基本上可以解决80%左右的需求了。那么还有能优化的地方么?答案是有的。

Cache + 线程池

一般在大的公司都有一些监控系统,可以将监控的数据上报到监控系统中。上面两个场景都是每次请求都会调用上报接口,这样特别浪费资源也可能出现性能问题。是否可以想一个办法减少上报次数呢?我们可以使用cache汇总在一起,打包通过线程池异步上报。是不是这种方式会更好一些。

实现

怎么实现呢? 首先我们需要一个cache,这次我们使用Guava Cache。

Guava Cache 是google开发开源项目Guava中带有的功能,只提供堆缓存,也就是说重启机器后就没有了,特点:小巧玲珑,性能最好。

private volatile static Cache<String, MutableInt> metricCache = null;


public static Cache<String, MutableInt> getMetricCache(){
    if (metricCache == null) {
        synchronized (this) {
            if (metricCache == null) {
                metricCache = initMetricCache();
                return metricCache;
            }
        }
    }
    return metricCache;
}

private static Cache<String, MutableInt> initMetricCache(){

    Cache<String, MutableInt> initMetricCache = CacheBuilder.newBuilder()
            // 设置缓存个数
            .maximumSize(1024)
            // 设置cache中的数据在写入之后的存活时间为1秒
            .expireAfterWrite(1, TimeUnit.MINUTES)
            // 设置并发数为8,即同一时间最多只能有5个线程往cache执行写入操作 
            .concurrencyLevel(8)
            // 声明一个监听器,缓存项被移除时做一些额外操作。这里使用异步线程池的形式实现,更加高效。
            .removalListener(RemovalListeners.asynchronous(new RemovalListener<String, MutableInt>(){
                @Override
                public void onRemoval(RemovalNotification<String, MutableInt> notification) {
                    // 删除后的逻辑操作,这里是上报到监控系统中 
                    metricForCount(notification.getKey(), notification.getValue().intValue());
                }
            },
            // 自定义线程池,这里就不在把实现的代码粘进来了 
            taskExecutor.getTaskExecutor()))
            .build();

    return initMetricCache;
}

对上面的代码进行分析:

  • CacheBuilder.newBuilder()创建一个Guava Cache,设置一些配置;
  • 在调用时考虑到高效性,使用了一个小技巧延迟加载,参考getMetricCache()实现;
  • 在Guava Cache中使用removalListener特性,结合我们的需求,当统计记录达到一定的数量后,删除掉并在监听的线程池中实现上报。

应用

看着很牛B,怎么使用呢?

    public static void logMetricForCount(final String key, final int count) {

        try {
            MutableInt logMetric = getMetricCache().get(key, new Callable<MutableInt>() {
                @Override
                public MutableInt call() throws Exception {
                    return new MutableInt(0);
                }
            });

            // 计数
            logMetric.add(count);
            if(logMetric.intValue() > 500){
                // 当计数达到500个时删除此key,从而触发上面配置的removalListener
                getMetricCache().invalidate(key);
            }
        } catch (Exception e) {
            logger.warn("统计{}信息次数{}异常", key, count, e);
        }
    }

在实战的计数操作,apache提供了MutableInt专门用于高效计数的类。还使用到Guava Cache的特性。

MutableInt logMetric = getMetricCache().get(key, new Callable<MutableInt>() {
                @Override
                public MutableInt call() throws Exception {
                    return new MutableInt(0);
                }
            });

当没有get到数据时,自动初始化一个。是不是很棒!
代码是不是就到此结束了? 不是的。我们在开发代码时需要考虑高效。Guava Cache在设计时也考虑到高效性,不过如果不仔细阅读使用文档,也会给自己买坑。

Guava Cache清理什么时候发生?使用CacheBuilder构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做——如果写操作实在太少的话。
如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果你的 缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()。ScheduledExecutorService可以帮助你很好地实现这样的定时调度。

对于高并发量的情况下,我们还需要写一个线程去定时cleanUp。

Runnable metrciCacheCleanUpTask = new Runnable() {
    @Override
    public void run() {
                getMetricCache().cleanUp();
            } catch (Exception e) {
            logger.error("定时cleanUp方法异常",e);
        }
    }
};
// 使用线程池每分钟执行一次
commTaskScheduler.scheduleWithFixedDelay(metrciCacheCleanUpTask, 60000);

线程池相关的实现可以参考我以前的blog,微分享-spring线程池实战

Guava Cache CacheLoader还提供了数据加载机制,有兴趣的话可以研究一下。

参考:
[Google Guava] 3-缓存

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

推荐阅读更多精彩内容