有了Redis缓存就高枕无忧了?可别忘了本地缓存!

一、本地缓存 GuavaCache 介绍

GuavaGoogle 提供的一套 Java 工具包,而 Guava Cache 是一套非常完善的本地缓存机制(JVM 缓存)。

Guava Cache 的设计来源于 CurrentHashMap,可以按照多种策略来清理存储在其中的缓存值且保持很高的并发读写性能。

二、应用场景与优势

1、本地缓存的应用场景

  • 对性能有非常高的要求

  • 不经常变化

  • 占用内存不大

  • 有访问整个集合的需求

  • 数据允许不实时一致

2、Guava Cache 的优势

  • 缓存过期和淘汰机制

在GuavaCache中可以设置Key的过期时间,包括访问过期和创建过期。

GuavaCache在缓存容量达到指定大小时,采用 LRU + FIFO 的方式,将不常使用的键值从 Cache 中删除。

  • 并发处理能力

GuavaCache 类似 CurrentHashMap,是线程安全的。

提供了设置并发级别的 api,使得缓存支持并发的写入和读取。

采用分离锁机制,分离锁能够减小锁力度,提升并发能力。

分离锁是分拆锁定,把一个集合看分成若干 partition, 每个 partiton 一把锁。ConcurrentHashMap 就是分了 16 个区域,这 16 个区域之间是可以并发的。GuavaCache 采用 Segment 做分区。

  • 更新锁定

一般情况下,在缓存中查询某个 key,如果不存在,则查源数据,并回填缓存。(Cache Aside Pattern

在高并发下会出现,多次查源并重复回填缓存,可能会造成源(DB)的性能下降甚至宕机。而 GuavaCache 可以在 CacheLoader 中加以控制,对同一个 key,只让一个请求去读源并回填缓存,其他请求阻塞等待。

  • 集成数据源

一般我们在业务中操作缓存,都会操作缓存和数据源两部分。而 GuavaCacheget 可以集成数据源,在从缓存中读取不到时可以从数据源中读取数据并回填缓存。

  • 监控缓存加载/命中情况

recordStats 统计

三、代码

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.idol</groupId>
    <artifactId>guava-cache</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>28.2-jre</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

MyDataSource.java

import java.util.HashMap;
import java.util.Map;

/**
 * @author SupremeSir
 * @description 模拟数据库
 * @className com.idol.datasource.MyDataSource
 * @date 2021/4/5 19:08
 **/
public class MyDataSource {
    public static Map<String, String> dataSourc = new HashMap<String, String>();

    static {
        dataSourc.put("1", "idol");
        dataSourc.put("2", "sir");
        dataSourc.put("3", "supreme");
        dataSourc.put("4", "happy");
        dataSourc.put("5", "lucky");
    }
}

MyGuavaCache.java

import com.google.common.cache.*;
import com.idol.datasource.MyDataSource;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

/**
 * @author SupremeSir
 * @description GuavaCache 本地缓存对象
 * @className com.idol.guava.GuavaCacheDemo
 * @date 2021/4/5 18:59
 **/
public class MyGuavaCache {
    private LoadingCache<String, Object> cache;

    public MyGuavaCache() {
        this.cache = CacheBuilder.newBuilder()
                /* 设置缓存大小
                缓存淘汰策略:LRU(Least Recently Used,最近最少使用) + FIFO (First In First Out,先进先出)
                 */
                .maximumSize(3)
                // 添加缓存删除监听
                .removalListener(new RemovalListener<Object, Object>() {
                    public void onRemoval(RemovalNotification<Object, Object> removalNotification) {
                        System.out.println("缓存 " + removalNotification.getKey()
                                + " 被删除,原因是:" + removalNotification.getCause());
                    }
                })
                // 缓存状态统计
                .recordStats()
                // 太久没访问,则删除缓存
                .expireAfterAccess(3, TimeUnit.SECONDS)
                // 写入缓存太久,则删除缓存
                .expireAfterWrite(2, TimeUnit.SECONDS)
                /* 弱值删除
                如果缓存中的 key 所对应的 value 的指针生了变化,则删除缓存
                eg:
                    Object val = new Object();
                    guavaCache.getCache().put("6", val);
                    val = new Object();
                 */
                .weakValues()
                /* 弱键删除
                如果缓存中的 value 所对应的 key 的指针生了变化,则删除缓存
                 */
                .weakKeys()
                // 同时支持 CPU 核数个线程写缓存
                .concurrencyLevel(Runtime.getRuntime().availableProcessors())
                /* 更新锁定,防止缓存穿透/击穿
                 GuavaCache 使用的是惰性删除。所以,如果配置了缓存过期(expireAfterWrite/expireAfterAccess),
                 则缓存过期后被再次获取已过期的缓存时,refreshAfterWrite 配置会允许一个线程回数据源进行数据查询并更新缓存。
                 如果超出指定时间(这里设置的为3秒)缓存仍未被更新,则返回缓存中的旧值。
                 */
                .refreshAfterWrite(3, TimeUnit.SECONDS)
                .build(new CacheLoader<String, Object>() {
                    /* 回调方法
                    如果通过 cache 的 get 方法(不带 Callable 参数)获取缓存为空,则会执行 load 回调。
                    可以在该回调中执行连接数据库并完成缓存回填。
                     */
                    public Object load(String key) {
                        System.out.println("---GuavaCache---  缓存没有命中" + key
                                + ",构造将执行数据库连接,从数据库中查询");
                        // 模拟数据库连接查询数据
                        return MyDataSource.dataSourc.get(key);
                    }
                });
    }

    /**
     * 获取缓存状态
     */
    public CacheStats getCacheStatus() {
         return cache.stats();
    }

    /**
     * 带有回调的缓存取值方法
     * 注意:当前方法由于自带回调,故构造函数中的数据库连接方法 load 将不再执行
     * @param key
     * @throws Exception
     */
    public Object getWithCallBack(final String key) throws Exception {
        return cache.get(key, new Callable<Object>() {
            public Object call() {
                System.out.println("没有命中,将执行 get 回调连接数据源");
                return MyDataSource.dataSourc.get(key);
            }
        });
    }

    /**
     * 查看当前缓存中的内容
     */
    public void display() {
        System.out.print("缓存大小:" + cache.size() + "\r\n");
        Set<Map.Entry<String, Object>> entries = cache.asMap().entrySet();
        for (Map.Entry<String, Object> entry : entries) {
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
        System.out.println();
    }

    /**
     * 初始化本地缓存(JVM缓存)
     * @throws Exception
     */
    public void initData() throws Exception {
        for (int i = 1; i < 4; i++) {
            // 通过 cache 对象调用 get 方法后会自动放入本地缓存
            cache.get(String.valueOf(i));
        }
    }

    public LoadingCache<String, Object> getCache() {
        return cache;
    }
}

CacheTest.java

import com.idol.guava.MyGuavaCache;

import java.util.Arrays;

/**
 * @author SupremeSir
 * @description 测试用例
 * @className CacheTest
 * @date 2021/4/5 19:35
 **/
public class CacheTest {
    public static void main(String[] args) throws Exception {
        // 创建缓存对象
        MyGuavaCache guavaCache = new MyGuavaCache();
        // 初始化缓存
        guavaCache.initData();

        System.out.println("\r\n&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&\r\n");

        System.out.println("命中缓存:" + guavaCache.getCache().get("1") + "\r\n");

        // 删除单个缓存
        guavaCache.getCache().invalidate("1");
        guavaCache.display();

        // 删除多个缓存
        guavaCache.getCache().invalidateAll(Arrays.asList("2", "3"));
        guavaCache.display();

        // 删除所有缓存
        guavaCache.initData();
        guavaCache.getCache().invalidateAll();
        System.out.println();

        // 验证 expireAfterWrite 设置
        guavaCache.initData();
        guavaCache.display();
        Thread.sleep(3000);

        // 查看当前缓存中的内容
        guavaCache.display();

        System.out.println("\r\n脱靶缓存:\r\n" + guavaCache.getCache().get("4"));

        // 验证 expireAfterAccess 设置
        Thread.sleep(4000);

        // 查看当前缓存中的内容
        guavaCache.display();

        // 只从缓存中获取,没有则返回 null
        System.out.println("\r\n没有命中直接返回:" + guavaCache.getCache().getIfPresent("5") + "\r\n");

        // 验证带有回调功能的 get 方法
        System.out.println(guavaCache.getWithCallBack("5") + "\r\n");

        // 查看当前缓存中的内容
        guavaCache.display();

        /* 查看缓存状态
        CacheStats{hitCount=1, missCount=6, loadSuccessCount=5, loadExceptionCount=0, totalLoadTime=2715600, evictionCount=2}
        hitCount:命中数
        missCount:脱靶数
        loadSuccessCount:回调成功数
        loadExceptionCount:回调异常数
        totalLoadTime:总耗时
        evictionCount:驱逐个数(设置缓存大小后,被 LRU+FIFO 算法剔除的缓存个数)
         */
        System.out.println("\r\n" + guavaCache.getCacheStatus());
    }
}

源码

源码下载(免费)

--------------------------------------- 即使行走在黑暗,也应该心中有光。 ---------------------------------------

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值