Java并发实战|Java 高并发缓存与Guava Cache

1,211 阅读7分钟

这是我参与更文挑战的第11天,活动详情查看: 更文挑战

前言:

随着互联网的发展,用户量及信息量也越来越复杂访问量同时也越来越庞大,我们的应用服务器资源是有限的,数据库每秒能接受文件的读写的请求次数也是有限的,为了能够有效利用有限的资源来提供尽可能大的吞吐量,一个有效的办法就是引入缓存,缓存是现在系统中必不可少的模块,并且已经成为了高并发高性能架构的一个关键组件,打破标准流程,有的请求我们可以从缓存中直接获取目标数据并返回,从而减少计算量,降低数据库读写次数,有效提升响应速度,让有限的资源服务更多的用户。

缓存:

我们通常将缓存分为以下俩种

local cache(本地缓存)

指的是在应用中的缓存组件也就是一级缓存

优点:

  • 应用和cache是在同一个进程内部,存储在服务器的内存中,缓存读写非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;

缺点:

  • 缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。
  • 本读缓存数据直接保存在JVM中,需要考虑缓存数据的大小、JVM的垃圾回收性能消耗
  • 单服务是集群部署的时候,应该考虑是否需要做集群中本地缓存的数据同步
remote cache(分布式缓存)

指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。如我们工作中比较常见的mongodb、memcached、Redis也都是我们必须要掌握的技能;

目前市场上有着各种类型的缓存组件,每一种缓存方案都有着他适合的业务场景,我们需要根据自身的业务场景,数据类型、来准确判断使用何种类型的缓存,如何使用这种缓存,以最小的成本最快的效率达到最优的目的。

Guava Cache

当我们在项目中需要使用local cache的时候,一般都会通过基于 ConcurrentHashMap或者LinkedHashMap来实现自己的本地Cache。然而使用本地缓存的时候一般都会面临以下这些问题:

  1. Java内存是有限的,所以需要限定缓存的最大容量.
  2. 如何清除“太旧”的缓存.
  3. 如何应对并发时候的读写安全.

Guava Cache与ConcurrentMap很相似,他也是线程安全的,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素,可设置过期时间。在某些场景下,尽管LoadingCache 不回收元素,它也是很有用的,因为它会自动加载缓存。

代码示例:

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * 本地缓存工具类
 *
 * @author taoze
 * @version 1.0
 * @date 6/9/21 3:27 PM
 */
@Component
public class LocalCacheUtil {

    private static LoadingCache<String, String> cache = CacheBuilder.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .refreshAfterWrite(5, TimeUnit.MINUTES)
            .build(new CacheLoader<String, String>() {
                @Override
                public String load(String key) {
                    return safeGetTokenFromLocal(key);
                }
            });


    private static String safeGetTokenFromLocal(String key) {
        return key+":"+UUID.randomUUID().toString().replaceAll("-", "");
    }

    public static String getToken(String key){
        String value = cache.getIfPresent(key);
        if(StringUtils.isBlank(value)){
            value = safeGetTokenFromLocal(key);
        }
        return value;
    }

    public static void main(String[] args) throws ExecutionException {
        String test = getToken("test");
        System.out.println(test);
    }
}

上面列举了一个简单的例子,我们来看一下CacheBuilder的三种基于时间的清理或刷新缓存数据的方式:

expireAfterAccess: 当缓存项在指定的时间段内没有被读或写就会被回收。 expireAfterWrite:当缓存项在指定的时间段内没有更新就会被回收。 refreshAfterWrite:当缓存项上一次更新操作之后的多久会被刷新。

考虑到时效性,我们可以使用expireAfterWrite,使每次更新之后的指定时间让缓存失效,然后重新加载缓存。guava cache会严格限制只有1个加载操作,这样会很好地防止缓存失效的瞬间大量请求穿透到后端引起雪崩效应。

refreshAfterWrite的特点是,在refresh的过程中,严格限制只有1个重新加载操作,而其他查询先返回旧值,这样有效地可以减少等待和锁争用,所以refreshAfterWrite会比expireAfterWrite性能好。但是它也有一个缺点,因为到达指定时间后,它不能严格保证所有的查询都获取到新值

可以看出refreshAfterWrite和expireAfterWrite两种方式各有优缺点,各有使用场景。所以我们俩种方式都用,控制缓存每5min进行refresh,如果超过10min没有访问,那么则让缓存失效,下次访问时不会得到旧值,而是必须得待新值加载。

常用方法api:

V getIfPresent(Object key) 获取缓存中key对应的value,如果缓存没命中,返回null。
V get(K key) throws ExecutionException 获取key对应的value,若缓存中没有,则调用LocalCache的load方法,从数据源中加载,并缓存。
void put(K key, V value) 如果缓存有值,覆盖,否则,新增
void putAll(Map m);循环调用单个的方法
void invalidate(Object key); 删除缓存
void invalidateAll(); 清楚所有的缓存,相当远map的clear操作。
long size(); 获取缓存中元素的大概个数。为什么是大概呢?元素失效之时,并不会实时的更新size,所以这里的size可能会包含失效元素。
CacheStats stats(); 缓存的状态数据,包括(未)命中个数,加载成功/失败个数,总共加载时间,删除个数等。
asMap()方法获得缓存数据的ConcurrentMap快照
cleanUp()清空缓存
refresh(Key) 刷新缓存,即重新取缓存数据,更新缓存
ImmutableMap getAllPresent(Iterable keys) 一次获得多个键的缓存值

核心类:

CacheBuilder:类,缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。 
CacheBuilder在build方法中,会把前面设置的参数,全部传递给LocalCache,它自己实际不参与任何计算。这种初始化参数的方法值得借鉴,代码简洁易读。
CacheLoader:抽象类。用于从数据源加载数据,定义load、reload、loadAll等操作。
Cache:接口,定义get、put、invalidate等操作,这里只有缓存增删改的操作,没有数据加载的操作。
AbstractCache:抽象类,实现Cache接口。其中批量操作都是循环执行单次行为,而单次行为都没有具体定义。
LoadingCache:接口,继承自Cache。定义get、getUnchecked、getAll等操作,这些操作都会从数据源load数据。
AbstractLoadingCache:抽象类,继承自AbstractCache,实现LoadingCache接口。
LocalCache:类。整个guava cache的核心类,包含了guava cache的数据结构以及基本的缓存的操作方法。
LocalManualCache:LocalCache内部静态类,实现Cache接口。 
其内部的增删改缓存操作全部调用成员变量 localCache(LocalCache类型)的相应方法。
LocalLoadingCache:LocalCache内部静态类,继承自LocalManualCache类,实现LoadingCache接口。 
其所有操作也是调用成员变量localCache(LocalCache类型)的相应方法。
CacheStats:缓存加载/命中统计信息。

ok!今日分享到此结束,大家可根据自身业务情况选择缓存类型,希望可以对大家有帮助,有不对的地方希望大家可以提出来的,共同成长;

整洁成就卓越代码,细节之中只有天地