大家好,欢迎来到IT知识分享网。
如图,Caffeine是当前最优秀的内存缓存框架,不论读还是写的效率都远高于其他缓存,而且在Spring5开始的默认缓存实现就将Caffeine代替原来的Google Guava
基础使用
<!-- 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种缓存淘汰设置
- 大小 (会使用上面说到的W-TinyLFU算法进行淘汰)
- 权重 (大小与权重 只能二选一)
- 时间
- 引用 (不常用,本文不介绍)
例子:
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种
- yml 不推荐,因为淘汰策略是公用的,不可以给每一个缓存配置不一样的淘汰策略,此处不演示
- 使用
@Configuration
类
此处演示第二种配置方式
- 开启缓存
@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做二级缓存
缓存的解决方案一般有三种
- 本地内存缓存,如Caffeine、Ehcache; 适合单机系统,速度最快,但是容量有限,而且重启系统后缓存丢失
- 集中式缓存,如Redis、Memcached; 适合分布式系统,解决了容量、重启丢失缓存等问题,但是当访问量极大时,往往性能不是首要考虑的问题,而是带宽。现象就是 Redis 服务负载不高,但是由于机器网卡带宽跑满,导致数据读取非常慢
- 第三种方案就是结合以上2种方案的二级缓存应运而生,以内存缓存作为一级缓存、集中式缓存作为二级缓存
市面上已有成熟的框架,开源中国官方开源的工具:J2Cache
大致原理就是这样
配合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