guava使用

对于Guava Cache自己就很少作介绍了,一个很是好用的本地cache lib,能够彻底取代本身手动维护ConcurrentHashMap。mysql

背景

目前须要开发一个接口I,对性能要求有很是高的要求,TP99.9在20ms之内。初步开发后发现耗时彻底没法知足,mysql稍微波动就超时了。sql

 

 

主要耗时在DB读取,请求一次接口会读取几回配置表Entry表。而Entry表的信息更新又不频繁,对实时性要求不高,因此想到了对DB作一个cache,理论上就能够大幅度提高接口性能了。数据库

 

DB表结构(这里的代码都是为了演示,不过原理、流程和实际生产环境基本是一致的)缓存

复制代码
CREATE TABLE `entry` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` int(11) NOT NULL,
  `value` varchar(50) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
复制代码

 

接口中的查询是根据name进行select操做,此次的目的就是设计一个cache类,将DB查询cache化。服务器

 

基础使用

首先,天然而然的想到了最基本的guava cache的使用,以下:并发

复制代码
@Slf4j
@Component
public class EntryCache {

    @Autowired
    EntryMapper entryMapper;

    /**
     * guava cache 缓存实体
     */
    LoadingCache<String, Entry> cache = CacheBuilder.newBuilder()
            // 缓存刷新时间
            .refreshAfterWrite(10, TimeUnit.MINUTES)
            // 设置缓存个数
            .maximumSize(500)
            .build(new CacheLoader<String, Entry>() {
                @Override
                // 当本地缓存命没有中时,调用load方法获取结果并将结果缓存
                public Entry load(String appKey) {
                    return getEntryFromDB(appKey);
                }
                
                // 数据库进行查询
                private Entry getEntryFromDB(String name) {
                    log.info("load entry info from db!entry:{}", name);
                    return entryMapper.selectByName(name);
                }
            });

    /**
     * 对外暴露的方法
     * 从缓存中取entry,没取到就走数据库
     */
    public Entry getEntry(String name) throws ExecutionException {
        return cache.get(name);
    }
    
}
复制代码

这里用了refreshAfterWrite,和expireAfterWrite区别是expireAfterWrite到期会直接删除缓存,若是同时多个并发请求过来,这些请求都会从新去读取DB来刷新缓存。DB速度较慢,会形成线程短暂的阻塞(相对于读cache)。app

而refreshAfterWrite,则不会删除cache,而是只有一个请求线程会去真实的读取DB,其余请求直接返回老值。这样能够避免同时过时时大量请求被阻塞,提高性能。异步

可是还有一个问题,那就是更新线程仍是会被阻塞,这样在缓存key集体过时时,可能还会使响应时间变得不知足要求。ide

 

后台线程刷新

就像上面所说,只要刷新缓存,就必然有线程被阻塞,这个是没法避免的。性能

虽然没法避免线程阻塞,可是咱们能够避免阻塞用户线程,让用户无感知便可。

因此,咱们能够把刷新线程放到后台执行。当key过时时,有新用户线程读取cache时,开启一个新线程去load DB的数据,用户线程直接返回老的值,这样就解决了这个问题。

代码修改以下:

复制代码
@Slf4j
@Component
public class EntryCache {

    @Autowired
    EntryMapper entryMapper;

    ListeningExecutorService backgroundRefreshPools =
            MoreExecutors.listeningDecorator(new ThreadPoolExecutor(10, 10,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<>()));

    /**
     * guava cache 缓存实体
     */
    LoadingCache<String, Entry> cache = CacheBuilder.newBuilder()
            // 缓存刷新时间
            .refreshAfterWrite(10, TimeUnit.MINUTES)
            // 设置缓存个数
            .maximumSize(500)
            .build(new CacheLoader<String, Entry>() {
                @Override
                // 当本地缓存命没有中时,调用load方法获取结果并将结果缓存
                public Entry load(String appKey) {
                    return getEntryFromDB(appKey);
                }

                @Override
                // 刷新时,开启一个新线程异步刷新,老请求直接返回旧值,防止耗时过长
                public ListenableFuture<Entry> reload(String key, Entry oldValue) throws Exception {
                    return backgroundRefreshPools.submit(() -> getEntryFromDB(key));
                }

                // 数据库进行查询
                private Entry getEntryFromDB(String name) {
                    log.info("load entry info from db!entry:{}", name);
                    return entryMapper.selectByName(name);
                }
            });

    /**
     * 对外暴露的方法
     * 从缓存中取entry,没取到就走数据库
     */
    public Entry getEntry(String name) throws ExecutionException {
        return cache.get(name);
    }

    /**
     * 销毁时关闭线程池
     */
    @PreDestroy
    public void destroy(){
        try {
            backgroundRefreshPools.shutdown();
        } catch (Exception e){
            log.error("thread pool showdown error!e:{}",e.getMessage());
        }

    }
}
复制代码

 

改动就是新添加了一个backgroundRefreshPools线程池,重写了一个reload方法。

ListeningExecutorService是guava的concurrent包里的类,负责一些线程池相关的工做,感兴趣的能够本身去了解一下。

在reload方法里提交一个新的线程,就能够用这个线程来刷新cache了。

若是刷新cache没有完成的时候有其余线程来请求该key,则会直接返回老值。

同时,千万不要忘记销毁线程池。

 

初始化问题

上面两步达到了不阻塞刷新cache的功能,可是这个前提是这些cache已经存在。

项目刚刚启动的时候,全部的cache都是不存在的,这个时候若是大批量请求过来,一样会被阻塞,由于没有老的值供返回,都得等待cache的第一次load完毕。

解决这个问题的方法就是在项目启动的过程当中,将全部的cache预先load过来,这样用户请求刚到服务器时就会直接读cache,不用等待。

复制代码
@Slf4j
@Component
public class EntryCache {

    @Autowired
    EntryMapper entryMapper;

    ListeningExecutorService backgroundRefreshPools =
            MoreExecutors.listeningDecorator(new ThreadPoolExecutor(10, 10,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<>()));

    /**
     * guava cache 缓存实体
     */
    LoadingCache<String, Entry> cache = CacheBuilder.newBuilder()
            // 缓存刷新时间
            .refreshAfterWrite(10, TimeUnit.MINUTES)
            // 设置缓存个数
            .maximumSize(500)
            .build(new CacheLoader<String, Entry>() {
                @Override
                // 当本地缓存命没有中时,调用load方法获取结果并将结果缓存
                public Entry load(String appKey) {
                    return getEntryFromDB(appKey);
                }

                @Override
                // 刷新时,开启一个新线程异步刷新,老请求直接返回旧值,防止耗时过长
                public ListenableFuture<Entry> reload(String key, Entry oldValue) throws Exception {
                    return backgroundRefreshPools.submit(() -> getEntryFromDB(key));
                }

                // 数据库进行查询
                private Entry getEntryFromDB(String name) {
                    log.info("load entry info from db!entry:{}", name);
                    return entryMapper.selectByName(name);
                }
            });

    /**
     * 对外暴露的方法
     * 从缓存中取entry,没取到就走数据库
     */
    public Entry getEntry(String name) throws ExecutionException {
        return cache.get(name);
    }

    /**
     * 销毁时关闭线程池
     */
    @PreDestroy
    public void destroy(){
        try {
            backgroundRefreshPools.shutdown();
        } catch (Exception e){
            log.error("thread pool showdown error!e:{}",e.getMessage());
        }

    }

    @PostConstruct
    public void initCache() {
        log.info("init entry cache start!");
        //读取全部记录
        List<Entry> list = entryMapper.selectAll();

        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        for (Entry entry : list) {
            try {
                this.getEntry(entry.getName());
            } catch (Exception e) {
                log.error("init cache error!,e:{}", e.getMessage());
            }
        }
        log.info("init entry cache end!");
    }
}
复制代码

结果

让咱们用数据看看这个cache类的表现:

200QPS,TP99.9是9ms,完美达标。

能够看出来,合理的使用缓存对接口性能仍是有很大提高的。

相关文章
相关标签/搜索