Caffeine 当下最优秀的内存缓存框架的使用与最佳实践+配合Redis做二级缓存[亲测有效]

Caffeine 当下最优秀的内存缓存框架的使用与最佳实践+配合Redis做二级缓存[亲测有效]如图,Caffeine是当前最优秀的内存缓存框架,不论读还是写的效率都远高于其他缓存,而且在Spring5开始的默认缓存实现就将Caffeine代替原来的Google Guava 基础使用 手动创建缓

大家好,欢迎来到IT知识分享网。

如图,Caffeine是当前最优秀的内存缓存框架,不论读还是写的效率都远高于其他缓存,而且在Spring5开始的默认缓存实现就将Caffeine代替原来的Google Guava

image.png

基础使用

<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.0.3</version>
</dependency>

IT知识分享网

手动创建缓存

IT知识分享网        Cache<Object, Object> cache = Caffeine.newBuilder()
                //初始数量
                .initialCapacity(10)
                //最大条数
                .maximumSize(10)
                //PS:expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准。
                //最后一次写操作后经过指定时间过期
                .expireAfterWrite(1, TimeUnit.SECONDS)
                //最后一次读或写操作后经过指定时间过期
                .expireAfterAccess(1, TimeUnit.SECONDS)
                //监听缓存被移除
                .removalListener((key, val, removalCause) -> { })
                //记录命中
                .recordStats()
                .build();

        cache.put("1","张三");
        System.out.println(cache.getIfPresent("1"));
        System.out.println(cache.get("2",o -> "默认值"));

自动创建缓存

LoadingCache<String, String> loadingCache = Caffeine.newBuilder()
        //创建缓存或者最近一次更新缓存后经过指定时间间隔,刷新缓存;refreshAfterWrite仅支持LoadingCache
        .refreshAfterWrite(10, TimeUnit.SECONDS)
        .expireAfterWrite(10, TimeUnit.SECONDS)
        .expireAfterAccess(10, TimeUnit.SECONDS)
        .maximumSize(10)
        //根据key查询数据库里面的值
        .build(key -> new Date().toString());

异步获取缓存

关于JDK8 CompletableFuture 说明

IT知识分享网AsyncLoadingCache<String, String> asyncLoadingCache = Caffeine.newBuilder()
        //创建缓存或者最近一次更新缓存后经过指定时间间隔刷新缓存;仅支持LoadingCache
        .refreshAfterWrite(1, TimeUnit.SECONDS)
        .expireAfterWrite(1, TimeUnit.SECONDS)
        .expireAfterAccess(1, TimeUnit.SECONDS)
        .maximumSize(10)
        //根据key查询数据库里面的值
        .buildAsync(key -> {
            Thread.sleep(1000);
            return new Date().toString();
        });

//异步缓存返回的是CompletableFuture
CompletableFuture<String> future = asyncLoadingCache.get("1");
future.thenAccept(System.out::println);

PS:可以使用.executor()自定义线程池

记录命中数据

LoadingCache<String, String> cache = Caffeine.newBuilder()
        //创建缓存或者最近一次更新缓存后经过指定时间间隔,刷新缓存;refreshAfterWrite仅支持LoadingCache
        .refreshAfterWrite(1, TimeUnit.SECONDS)
        .expireAfterWrite(1, TimeUnit.SECONDS)
        .expireAfterAccess(1, TimeUnit.SECONDS)
        .maximumSize(10)
        //开启记录缓存命中率等信息
        .recordStats()
        //根据key查询数据库里面的值
        .build(key -> {
            Thread.sleep(1000);
            return new Date().toString();
        });


cache.put("1", "小明");
cache.get("1");

/* * hitCount :命中的次数 * missCount:未命中次数 * requestCount:请求次数 * hitRate:命中率 * missRate:丢失率 * loadSuccessCount:成功加载新值的次数 * loadExceptionCount:失败加载新值的次数 * totalLoadCount:总条数 * loadExceptionRate:失败加载新值的比率 * totalLoadTime:全部加载时间 * evictionCount:丢失的条数 */
System.out.println(cache.stats());

PS:会影响性能,生产环境下建议不开启

淘汰策略

先了解一下常见的淘汰策略

  • LRU 最近最少使用,淘汰最长时间没有被使用的页面。
  • LRU 最不经常使用,淘汰一段时间内,使用次数最少的页面
  • FIFO 先进先出

LRU的优点:LRU相比于 LFU 而言性能更好一些,因为它算法相对比较简单,不需要记录访问频次,可以更好的应对突发流量。

LRU的缺点:虽然性能好一些,但是它通过历史数据来预测未来是局限的,它会认为最后到来的数据是最可能被再次访问的,从而给与它最高的优先级。有些非热点数据被访问过后,占据了高优先级,它会在缓存中占据相当长的时间,从而造成空间浪费。

LFU的优点:LFU根据访问频次访问,在大部分情况下,热点数据的频次肯定高于非热点数据,所以它的命中率非常高。

LFU的缺点:LFU 算法相对比较复杂,性能比 LRU 差。有问题的是下面这种情况,比如前一段时间微博有个热点话题热度非常高,就比如那种可以让微博短时间停止服务的,于是赶紧缓存起来,LFU 算法记录了其中热点词的访问频率,可能高达十几亿,而过后很长一段时间,这个话题已经不是热点了,新的热点也来了,但是,新热点话题的热度没办法到达十几亿,也就是说访问频次没有之前的话题高,那之前的热点就会一直占据着缓存空间,长时间无法被剔除。

而Caffeine 采用W-TinyLFU淘汰算法,结合LRU与LFU达到更佳的命中率与性能,具体参考: www.cnblogs.com/zhaoxinshan…

4种淘汰方式与例子

Caffeine有4种缓存淘汰设置

  1. 大小 (会使用上面说到的W-TinyLFU算法进行淘汰)
  2. 权重 (大小与权重 只能二选一)
  3. 时间
  4. 引用 (不常用,本文不介绍)

例子:

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Scheduler;
import com.github.benmanes.caffeine.cache.Weigher;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

import java.util.concurrent.TimeUnit;

/** * @author yejunxi * @date 2021/7/23 */
@Slf4j
public class CacheTest {


    /** * 缓存大小淘汰 */
    @Test
    public void maximumSizeTest() throws InterruptedException {
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                //超过10个后会使用W-TinyLFU算法进行淘汰
                .maximumSize(10)
                .evictionListener((key, val, removalCause) -> {
                    log.info("淘汰缓存:key:{} val:{}", key, val);
                })
                .build();

        for (int i = 1; i < 20; i++) {
            cache.put(i, i);
        }
        Thread.sleep(500);//缓存淘汰是异步的

        // 打印还没被淘汰的缓存
        System.out.println(cache.asMap());
    }

    /** * 权重淘汰 */
    @Test
    public void maximumWeightTest() throws InterruptedException {
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                //限制总权重,若所有缓存的权重加起来>总权重就会淘汰权重小的缓存
                .maximumWeight(100)
                .weigher((Weigher<Integer, Integer>) (key, value) -> key)
                .evictionListener((key, val, removalCause) -> {
                    log.info("淘汰缓存:key:{} val:{}", key, val);
                })
                .build();

        //总权重其实是=所有缓存的权重加起来
        int maximumWeight = 0;
        for (int i = 1; i < 20; i++) {
            cache.put(i, i);
            maximumWeight += i;
        }
        System.out.println("总权重=" + maximumWeight);
        Thread.sleep(500);//缓存淘汰是异步的

        // 打印还没被淘汰的缓存
        System.out.println(cache.asMap());
    }


    /** * 访问后到期(每次访问都会重置时间,也就是说如果一直被访问就不会被淘汰) */
    @Test
    public void expireAfterAccessTest() throws InterruptedException {
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                .expireAfterAccess(1, TimeUnit.SECONDS)
                //可以指定调度程序来及时删除过期缓存项,而不是等待Caffeine触发定期维护
                //若不设置scheduler,则缓存会在下一次调用get的时候才会被动删除
                .scheduler(Scheduler.systemScheduler())
                .evictionListener((key, val, removalCause) -> {
                    log.info("淘汰缓存:key:{} val:{}", key, val);

                })
                .build();
        cache.put(1, 2);
        System.out.println(cache.getIfPresent(1));
        Thread.sleep(3000);
        System.out.println(cache.getIfPresent(1));//null
    }

    /** * 写入后到期 */
    @Test
    public void expireAfterWriteTest() throws InterruptedException {
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.SECONDS)
                //可以指定调度程序来及时删除过期缓存项,而不是等待Caffeine触发定期维护
                //若不设置scheduler,则缓存会在下一次调用get的时候才会被动删除
                .scheduler(Scheduler.systemScheduler())
                .evictionListener((key, val, removalCause) -> {
                    log.info("淘汰缓存:key:{} val:{}", key, val);
                })
                .build();
        cache.put(1, 2);
        Thread.sleep(3000);
        System.out.println(cache.getIfPresent(1));//null
    }
}

另外还有一个refreshAfterWrite()表示x秒后自动刷新缓存可以配合以上的策略使用

private static int NUM = 0;

@Test
public void refreshAfterWriteTest() throws InterruptedException {
    LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
            .refreshAfterWrite(1, TimeUnit.SECONDS)
            //模拟获取数据,每次获取就自增1
            .build(integer -> ++NUM);

    //获取ID=1的值,由于缓存里还没有,所以会自动放入缓存
    System.out.println(cache.get(1));// 1

    // 延迟2秒后,理论上自动刷新缓存后取到的值是2
    // 但其实不是,值还是1,因为refreshAfterWrite并不是设置了n秒后重新获取就会自动刷新
    // 而是x秒后&&第二次调用getIfPresent的时候才会被动刷新
    Thread.sleep(2000);
    System.out.println(cache.getIfPresent(1));// 1

    //此时才会刷新缓存,而第一次拿到的还是旧值
    System.out.println(cache.getIfPresent(1));// 2
}

最佳实践

在实际开发中如何配置淘汰策略最优呢,根据我的经验常用的还是以大小淘汰为主

实践1

配置:设置 maxSize、refreshAfterWrite,不设置 expireAfterWrite/expireAfterAccess

优缺点:因为设置expireAfterWrite当缓存过期时会同步加锁获取缓存,所以设置expireAfterWrite时性能较好,但是某些时候会取旧数据,适合允许取到旧数据的场景

实践2 配置:设置 maxSize、expireAfterWrite/expireAfterAccess,不设置 refreshAfterWrite

优缺点:与上面相反,数据一致性好,不会获取到旧数据,但是性能没那么好(对比起来),适合获取数据时不耗时的场景

与Srping Boot集成

<!--缓存-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.0.3</version>
</dependency>

配置方式有2种

  1. yml 不推荐,因为淘汰策略是公用的,不可以给每一个缓存配置不一样的淘汰策略,此处不演示
  2. 使用@Configuration

此处演示第二种配置方式

  1. 开启缓存 @EnableCaching
@Configuration
public class CaffeineConfig {

    @Bean
    public CacheManager caffeineCacheManager() {
        List<CaffeineCache> caffeineCaches = new ArrayList<>();

        //可自行在yml或使用枚举设置多个缓存,不同名字的缓存的不同配置
        caffeineCaches.add(new CaffeineCache("cache1",
                Caffeine.newBuilder()
                        .expireAfterWrite(10, TimeUnit.SECONDS)
                        .build())
        );

        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(caffeineCaches);
        return cacheManager;
    }
}

直接可以使用Spring 缓存注解,@Cacheable、@CacheEvict、@CachePut等,此处也不作详解

@Service
@Slf4j
public class StudentService {
    @Cacheable(value = "cache1")
    public String getNameById(int id) {
        log.info("从DB获取数据:id=" + id);
        return new Date().toString();
    }
}

配合Redis做二级缓存

缓存的解决方案一般有三种

  1. 本地内存缓存,如Caffeine、Ehcache; 适合单机系统,速度最快,但是容量有限,而且重启系统后缓存丢失
  2. 集中式缓存,如Redis、Memcached; 适合分布式系统,解决了容量、重启丢失缓存等问题,但是当访问量极大时,往往性能不是首要考虑的问题,而是带宽。现象就是 Redis 服务负载不高,但是由于机器网卡带宽跑满,导致数据读取非常慢
  3. 第三种方案就是结合以上2种方案的二级缓存应运而生,以内存缓存作为一级缓存、集中式缓存作为二级缓存

市面上已有成熟的框架,开源中国官方开源的工具:J2Cache

大致原理就是这样

未命名文件 (1).png

配合spring boot使用参考:

<!-- 可以不引入caffine,j2cache默认使用2.x版本 -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.0.3</version>
</dependency>

<dependency>
    <groupId>net.oschina.j2cache</groupId>
    <artifactId>j2cache-spring-boot2-starter</artifactId>
    <version>2.8.0-release</version>
</dependency>

<dependency>
    <groupId>net.oschina.j2cache</groupId>
    <artifactId>j2cache-core</artifactId>
    <version>2.8.0-release</version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
        </exclusion>
    </exclusions>
</dependency>

bootstrap.yml

j2cache:
  config-location: classpath:/j2cache-${spring.profiles.active}.properties
  # 开启对spring cahce的支持
  open-spring-cache: true
  # jedis 或 lettuce 对应在j2cache.properties 配置
  redis-client: lettuce
  # 是否允许null值
  allow-null-values: true
  # 是否开始二级缓存
  l2-cache-open: true
  # 如下配置在application.properties,可以选择缓存清除的模式
  # * 缓存清除模式
  # * active:主动清除,二级缓存过期主动通知各节点清除,优点在于所有节点可以同时收到缓存清除
  # * passive:被动清除,一级缓存过期进行通知各节点清除一二级缓存
  # * blend:两种模式一起运作,对于各个节点缓存准确性以及及时性要求高的可以使用(推荐使用前面两种模式中一种)
  cache-clean-mode: passive

新增2个properties配置文件(不支持yml):

j2cache-test.properties

#J2Cache configuration
# caffeine 本地缓存定义文件
caffeine.properties=/caffeine.properties
j2cache.broadcast=net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy
j2cache.L1.provider_class=caffeine
j2cache.L2.provider_class=net.oschina.j2cache.cache.support.redis.SpringRedisProvider
j2cache.L2.config_section=redis
# 序列化方式
j2cache.serialization=json
#########################################
# Redis connection configuration
#########################################
#########################################
# Redis Cluster Mode
#
# single -> single redis server
# sentinel -> master-slaves servers
# cluster -> cluster servers (数据库配置无效,使用 database = 0)
# sharded -> sharded servers (密码、数据库必须在 hosts 中指定,且连接池配置无效 ;
# 例子:redis://user:password@127.0.0.1:6379/0)
# sharded需要指定cluster name :redis.cluster_name = mymaster
#########################################
redis.mode=single
# redis通知节点删除本地缓存通道名
redis.channel=j2cache
# redis缓存key前缀
redis.namespace=j2cache
## connection
#redis.hosts = 127.0.0.1:26378,127.0.0.1:26379,127.0.0.1:26380
redis.hosts=127.0.0.1:6379
redis.timeout=2000
redis.password=xfish311
redis.database=1

caffeine.properties:

#########################################
# Caffeine configuration 定义本地缓存
# [name] = size, xxxx[s|m|h|d] (过期时间)
#########################################

default = 1000, 30m
cache1 = 1000, 30m

完毕。以上配置是本人整理后的配置,具体使用还需要查阅官方文档。

另外对二级缓存原理有兴趣建议去看另一个项目,源码相比j2cache写得比较容易理解,属于阉割版的二级缓存:github.com/pig-mesh/mu…

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/13262.html

(0)
上一篇 2023-03-17 17:00
下一篇 2023-03-17 19:00

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信