spring-cache 雪崩

原创 2016年08月29日 18:38:01

spring-cache 基本原理是利用拦截器,先尝试读取缓存,未命中缓存,先读库在写入缓存,经过查看源码如果在并发量大的时候容易造成“雪崩”。原因是在更新缓存逻辑中没有做并发更新的处理。

原始代码

private Object execute(CacheOperationInvoker invoker, CacheOperationContexts contexts) {
        // Process any early evictions
        processCacheEvicts(contexts.get(CacheEvictOperation.class), true, ExpressionEvaluator.NO_RESULT);

        // Check if we have a cached item matching the conditions
        Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));

        // Collect puts from any @Cacheable miss, if no cached item is found
        List<CachePutRequest> cachePutRequests = new LinkedList<CachePutRequest>();
        if (cacheHit == null) {
            collectPutRequests(contexts.get(CacheableOperation.class), ExpressionEvaluator.NO_RESULT, cachePutRequests);
        }

        Cache.ValueWrapper result = null;

        // If there are no put requests, just use the cache hit
        if (cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
            result = cacheHit;
        }

        // Invoke the method if don't have a cache hit
        if (result == null) {
            result = new SimpleValueWrapper(invokeOperation(invoker));
        }

        // Collect any explicit @CachePuts
        collectPutRequests(contexts.get(CachePutOperation.class), result.get(), cachePutRequests);

        // Process any collected put requests, either from @CachePut or a @Cacheable miss
        for (CachePutRequest cachePutRequest : cachePutRequests) {
            cachePutRequest.apply(result.get());
        }

        // Process any late evictions
        processCacheEvicts(contexts.get(CacheEvictOperation.class), false, result.get());

        return result.get();
    }

经过分析代码会发现在这里并没有任何锁来处理并发更新

// Invoke the method if don't have a cache hit
        if (result == null) {
            result = new SimpleValueWrapper(invokeOperation(invoker));
        }

测试代码

@Cacheable
public List<Regions> queryAllCity() {
    try {
        Thread.sleep(1000 * 30);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    log.info("getAllCity");
    return regionsMapper.getAllCity();
}

这里用jmeter简单的测试了一下(200个并发),效果如下

这里写图片描述

这里写图片描述

从日志中可以看到在并发更新缓存时,200次一次都没有命中,而是执行了200次读取数据方法(这里只是简单的用sleep模拟了读取数据的长时间操作)

改造

期望结果:并发更新缓存时,只会读取一次数据库,其他的请求都会命中缓存

改造后的代码样例:

Lock lock = new ReentrantLock();
    private Object execute(CacheOperationInvoker invoker, CacheOperationContexts contexts) {
        // Process any early evictions
        processCacheEvicts(contexts.get(CacheEvictOperation.class), true, ExpressionEvaluator.NO_RESULT);


        // Check if we have a cached item matching the conditions
        Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));

        // Collect puts from any @Cacheable miss, if no cached item is found
        List<CachePutRequest> cachePutRequests = new LinkedList<CachePutRequest>();
        if (cacheHit == null) {
            collectPutRequests(contexts.get(CacheableOperation.class), ExpressionEvaluator.NO_RESULT, cachePutRequests);
        }

        Cache.ValueWrapper result = null;

        // If there are no put requests, just use the cache hit
        if (cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
            result = cacheHit;
        }

        // Invoke the method if don't have a cache hit
        if (result != null) {
            return result.get();
        }
        lock.lock();
        try{
            logger.info(" get lock");
            result = findCachedItem(contexts.get(CacheableOperation.class));
            if (result == null) {
                result = new SimpleValueWrapper(invokeOperation(invoker));
            } else {
                logger.info("hit cache");
            }

            // Collect any explicit @CachePuts
            collectPutRequests(contexts.get(CachePutOperation.class), result.get(), cachePutRequests);

            // Process any collected put requests, either from @CachePut or a @Cacheable miss
            for (CachePutRequest cachePutRequest : cachePutRequests) {
                cachePutRequest.apply(result.get());
            }

            // Process any late evictions
            processCacheEvicts(contexts.get(CacheEvictOperation.class), false, result.get());
            return result.get();
        } finally {
            lock.unlock();
        }

    }

相同的测试200个并发

这里写图片描述

这里写图片描述

这里写图片描述

从日志可以看到获取锁(get lock)一共200次,但是实际读取数据(getAllCity)只有1次,命中(hit cache)缓存199次,这正是我们所希望的结果

这里只是使用了Lock锁,如果要处理多实例的命中缓存,就需要一个分布式锁,可以用redis实现setIfAbsent。当然也可以引入锁的超时机制。这里有个简单基于redis的分布式锁: spring-distributelock

如果你的应用的实例只有十几个,其实只要保证每个实例只有1个线程在更新缓存就可以了(ReentrantLock),还免去了分布式的复杂和网络通信时间。

版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

利用spring的拦截器自定义缓存的实现

Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载。它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高动态、数据库驱动网站的速度。本文利用Memca...

简单注解实现集群同步锁(spring+redis+注解)

互联网面试的时候,是不是面试官常问一个问题如何保证集群环境下数据操作并发问题,常用的synchronized肯定是无法满足了,或许你可以借助for update对数据加锁。本文的最终解决方式你只要在方...

利用redis缓存mysql查询结果,关于缓存命名

redis 缓存 MySQL 查询结果的一些思考

故障案例--mongo 3.0鉴权导致cpu居高不下

故障现象 CPU奇高,达到接近物理机核数上限; 错误日志中的业务SQL执行较快,SQL不存在问题; 错误日志大量的刷屏以下信息 原因分析 从错误日志看,绝大部分情况都处于saslSt...

Spring cache

  • 2016年04月05日 10:52
  • 140KB
  • 下载

SSM与memcached整合项目Spring Cache

  • 2016年10月31日 17:23
  • 29.06MB
  • 下载

Spring Cache原理与使用及ICOP平台中的缓存应用

一.Spring Cache概述 Spring3.1中提供了基于注解的缓存技术,他本身并不是缓存的具体实现,而是一个缓存的抽象。通过在既有程序的方法上添加spring cache的注解,可以达到缓存...

spring-modules-cache

  • 2009年11月13日 15:47
  • 170KB
  • 下载

spring cache demo

  • 2013年03月02日 19:14
  • 128KB
  • 下载

Spring Cache使用详解

Spring Cache Spring Cache使用方法与Spring对事务管理的配置相似。Spring Cache的核心就是对某个方法进行缓存,其实质就是缓存该方法的返回结果,并把方法参...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:spring-cache 雪崩
举报原因:
原因补充:

(最多只允许输入30个字)