开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情
一、本地缓存Caffeine介绍
除了分布式缓存,其实还有一种缓存 - 本地缓存:直接从本地内存中读取,没有网络开销,在某些场景比远程缓存更合适Caffeine号称是本地缓存绝对的王者。
Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库。缓存和ConcurrentMap有点相似,但还是有所区别,最根本的区别是ConcurrentMap将会持有所有加入到缓存当中的元素,直到它们被从缓存当中手动移除。
Caffeine的底层使用了ConcurrentHashMap,支持按照一定的规则或者自定义的规则使缓存的数据过期,然后销毁。
二、Caffeine功能与性能
Caffeine提供了多种灵活的构造方法,从而可以创建多种特性的本地缓存。
- 自动把数据加载到本地缓存中,并且可以配置异步;
- 基于数量剔除策略;
- 基于失效时间剔除策略,这个时间是从最后一次操作算起【访问或者写入】;
- 异步刷新;
- Key会被包装成Weak引用;
- Value会被包装成Weak或者Soft引用,从而能被GC掉,而不至于内存泄漏;
- 数据剔除提醒;
- 写入广播机制;
- 缓存访问可以统计;
三、Caffeine 配置说明
Caffeine主要提供了以下一些配置:
- initialCapacity=[integer]: 设置初始缓存的空间大小;
- maximumSize=[long]: 设置缓存的最大条数;
- maximumWeight=[long]: 设置缓存的最大权重;
- expireAfterAccess=[持续时间]: 最后一次写入或者访问后经过多久时间过期;
- expireAfterWrite=[持续时间]: 最后一次写入后经过多久时间过期;
- refreshAfterWrite=[持续时间]: 创建缓存或者最后一次更新缓存后经过多久时间间隔,刷新缓存;
- weakKeys: 打开key的弱引用;
- weakValues: 打开value的弱引用;
- softValues: 打开value的软引用;
- recordStats: 打开统计功能;
注意:
- weakValues 和 softValues 不可以同时使用
- maximumSize 和 maximumWeight 不可以同时使用
- expireAfterWrite 和 expireAfterAccess 同时存在时,以expireAfterWrite为准
四、SpringBoot 集成 Caffeine、Redis实现多级缓存
首先我们要明白为什么要使用多级缓存?
- 如果只使用redis来做缓存我们会有大量的请求到redis,但是每次请求的数据都是一样的,假如这一部分数据就放在应用服务器本地,那么就省去了请求redis的网络开销,请求速度就会快很多;
- 如果只使用Caffeine来做本地缓存,我们的应用服务器的内存是有限,并且单独为了缓存去扩展应用服务器是非常不划算。所以,只使用本地缓存也是有很大局限性的;
因此在项目中,我们可以将热点数据放本地缓存,作为一级缓存,将非热点数据放redis缓存,作为二级缓存,减少Redis的查询压力。
使用流程大致如下:
- 首先从一级缓存(caffeine-本地应用内)中查找数据;
- 如果没有的话,则从二级缓存(redis-内存)中查找数据;
- 如果还是没有的话,再从数据库(数据库-磁盘)中查找数据;
SpringBoot 有两种使用 Caffeine 作为缓存的方式:
方式一:直接引入 Caffeine 依赖,然后使用 Caffeine 方法实现缓存; 方式二:引入 Caffeine 和 Spring Cache 依赖,使用 SpringCache 注解方法实现缓存; 我们先以第一种方式介绍下如何集成Redis、Caffeine实现多级缓存的。
五、使用Caffeine方法实现缓存
(一)、Maven 引入相关依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.dog</groupId>
<artifactId>springboot_caffeine</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot_caffeine</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--caffeine-->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(二)、Redis相关配置文件
spring:
redis:
host: 127.0.0.1
port: 6379
password:
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 500
min-idle: 0
lettuce:
shutdown-timeout: 0
(三)、本地缓存类
package org.dog.server.config;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* @Author: Odin
* @Date: 2023/1/27 18:27
* @Description:
*/
@Configuration
public class CacheConfig {
@Bean("localCacheManager")
public Cache<String, Object> localCacheManager() {
return Caffeine.newBuilder()
//写入或者更新5s后,缓存过期并失效, 实际项目中肯定不会那么短时间就过期,根据具体情况设置即可
.expireAfterWrite(5, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(50)
// 缓存的最大条数,通过 Window TinyLfu算法控制整个缓存大小
.maximumSize(500)
//打开数据收集功能
.recordStats()
.build();
}
}
(四)、Redis缓存配置类
package org.dog.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
/**
* @Author: Odin
* @Date: 2023/1/27 18:35
* @Description:
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
return template;
}
}
(五)、定义实体类对象
package org.dog.server.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @Author: Odin
* @Date: 2023/1/27 18:22
* @Description:
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private String id;
private String name;
}
(六)、定义Service接口类
package org.dog.server.service;
import org.dog.server.entity.User;
/**
* @Author: Odin
* @Date: 2023/1/27 18:43
* @Description:
*/
public interface UserService {
void add(User user);
User getById(String id);
User update(User user);
void deleteById(String id);
}
(七)、定义Service接口实现类
package org.dog.server.service.impl;
import com.alibaba.fastjson.JSON;
import com.github.benmanes.caffeine.cache.*;
import lombok.extern.slf4j.Slf4j;
import org.dog.server.entity.User;
import org.dog.server.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class UserServiceImpl implements UserService {
/**
* 模拟数据库存储数据
*/
private static HashMap<String, User> userMap = new HashMap<>();
private final RedisTemplate<String, Object> redisTemplate;
private final Cache<String, Object> caffeineCache;
@Autowired
public UserServiceImpl(RedisTemplate<String, Object> redisTemplate,
@Qualifier("localCacheManager") Cache<String, Object> caffeineCache) {
this.redisTemplate = redisTemplate;
this.caffeineCache = caffeineCache;
}
static {
userMap.put("1", new User("1", "zhangsan"));
userMap.put("2", new User("2", "lisi"));
userMap.put("3", new User("3", "wangwu"));
userMap.put("4", new User("4", "zhaoliu"));
}
@Override
public void add(User user) {
// 1.保存Caffeine缓存
caffeineCache.put(user.getId(), user);
// 2.保存redis缓存
redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);
// 3.保存数据库(模拟)
userMap.put(user.getId(), user);
}
@Override
public User getById(String id) {
// 1.先从Caffeine缓存中读取
Object o = caffeineCache.getIfPresent(id);
if (Objects.nonNull(o)) {
log.info("从Caffeine中查询到数据...");
return (User) o;
}
// 2.如果缓存中不存在,则从Redis缓存中查找
String jsonString = (String) redisTemplate.opsForValue().get(id);
User user = JSON.parseObject(jsonString, User.class);
if (Objects.nonNull(user)) {
log.info("从Redis中查询到数据...");
// 保存Caffeine缓存
caffeineCache.put(user.getId(), user);
return user;
}
// 3.如果Redis缓存中不存在,则从数据库中查询
user = userMap.get(id);
if (Objects.nonNull(user)) {
// 保存Caffeine缓存
caffeineCache.put(user.getId(), user);
// 保存Redis缓存,20s后过期
redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);
}
log.info("从数据库中查询到数据...");
return user;
}
@Override
public User update(User user) {
User oldUser = userMap.get(user.getId());
oldUser.setName(user.getName());
// 1.更新数据库
userMap.put(oldUser.getId(), oldUser);
// 2.更新Caffeine缓存
caffeineCache.put(oldUser.getId(), oldUser);
// 3.更新Redis数据库
redisTemplate.opsForValue().set(oldUser.getId(), JSON.toJSONString(oldUser), 20, TimeUnit.SECONDS);
return oldUser;
}
@Override
public void deleteById(String id) {
// 1.删除数据库
userMap.remove(id);
// 2.删除Caffeine缓存
caffeineCache.invalidate(id);
// 3.删除Redis缓存
redisTemplate.delete(id);
}
}
- caffeineCache.put(user.getId(), user):保存本地缓存;
- caffeineCache.invalidate(id):移除指定的本地缓存;
- caffeineCache.getIfPresent(id): 从本地缓存中获取值,如果缓存中不存指定的值,则方法将返回 null;
(八)、视图控制器测试
package org.dog.server.service.impl;
import com.alibaba.fastjson.JSON;
import com.github.benmanes.caffeine.cache.*;
import lombok.extern.slf4j.Slf4j;
import org.dog.server.entity.User;
import org.dog.server.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class UserServiceImpl implements UserService {
/**
* 模拟数据库存储数据
*/
private static HashMap<String, User> userMap = new HashMap<>();
private final RedisTemplate<String, Object> redisTemplate;
private final Cache<String, Object> caffeineCache;
@Autowired
public UserServiceImpl(RedisTemplate<String, Object> redisTemplate,
@Qualifier("localCacheManager") Cache<String, Object> caffeineCache) {
this.redisTemplate = redisTemplate;
this.caffeineCache = caffeineCache;
}
static {
userMap.put("1", new User("1", "zhangsan"));
userMap.put("2", new User("2", "lisi"));
userMap.put("3", new User("3", "wangwu"));
userMap.put("4", new User("4", "zhaoliu"));
}
@Override
public void add(User user) {
// 1.保存Caffeine缓存
caffeineCache.put(user.getId(), user);
// 2.保存redis缓存
redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);
// 3.保存数据库(模拟)
userMap.put(user.getId(), user);
}
@Override
public User getById(String id) {
// 1.先从Caffeine缓存中读取
Object o = caffeineCache.getIfPresent(id);
if (Objects.nonNull(o)) {
log.info("从Caffeine中查询到数据...");
return (User) o;
}
// 2.如果缓存中不存在,则从Redis缓存中查找
String jsonString = (String) redisTemplate.opsForValue().get(id);
User user = JSON.parseObject(jsonString, User.class);
if (Objects.nonNull(user)) {
log.info("从Redis中查询到数据...");
// 保存Caffeine缓存
caffeineCache.put(user.getId(), user);
return user;
}
// 3.如果Redis缓存中不存在,则从数据库中查询
user = userMap.get(id);
if (Objects.nonNull(user)) {
// 保存Caffeine缓存
caffeineCache.put(user.getId(), user);
// 保存Redis缓存,20s后过期
redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);
}
log.info("从数据库中查询到数据...");
return user;
}
@Override
public User update(User user) {
User oldUser = userMap.get(user.getId());
oldUser.setName(user.getName());
// 1.更新数据库
userMap.put(oldUser.getId(), oldUser);
// 2.更新Caffeine缓存
caffeineCache.put(oldUser.getId(), oldUser);
// 3.更新Redis数据库
redisTemplate.opsForValue().set(oldUser.getId(), JSON.toJSONString(oldUser), 20, TimeUnit.SECONDS);
return oldUser;
}
@Override
public void deleteById(String id) {
// 1.删除数据库
userMap.remove(id);
// 2.删除Caffeine缓存
caffeineCache.invalidate(id);
// 3.删除Redis缓存
redisTemplate.delete(id);
}
}
测试接口,执行结果如下:
- 查询

-
添加

-
修改

-
删除

六、使用 SpringCache 注解方法实现缓存
(一)、Maven引入依赖
<dependencies>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--caffeine-->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!--spring cache-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
(二)、Redis相关配置文件
spring:
redis:
host: 127.0.0.1
port: 6379
password:
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 500
min-idle: 0
lettuce:
shutdown-timeout: 0
(三)、Caffeine自动配置类
/**
* @Description: Caffeine自动配置类
*/
//自动配置功能
@Configuration
//开启缓存功能
@EnableCaching
public class CaffeineCacheConfig {
@Bean
@Primary
public CacheManager caffeineCacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<CaffeineCache> caches = CaffeineCacheInitializer.initCaffeineCache();
if (CollectionUtils.isEmpty(caches)) {
return cacheManager;
}
cacheManager.setCaches(caches);
return cacheManager;
}
}
/**
* @Description: CaffeineCache初始化器
*/
public class CaffeineCacheInitializer {
public static List<CaffeineCache> initCaffeineCache() {
List<CaffeineCache> caffeineCacheList = new ArrayList<>();
CaffeineCache userCache = new CaffeineCache(CacheKey.USER_CACHE_KEY, Caffeine.newBuilder().recordStats()
.expireAfterWrite(5, TimeUnit.SECONDS)
.maximumSize(100)
.build());
caffeineCacheList.add(userCache);
//将所有需要定义的CaffeineCache添加到容器中
//....
return caffeineCacheList;
}
}
/**
* @Description: 缓存Key常量,统一维护
*/
public class CacheKey {
public static final String USER_CACHE_KEY = "userCache";
}
如果使用了多个cahce,比如redis、caffeine等,必须指定某一个CacheManage为@Primary,在@Cacheable注解中没指定 cacheManager 的话,则使用标记为primary的那个。
(四)、Redis缓存配置类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
return template;
}
}
(五)、定义实体对象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private String id;
private String name;
}
(六)、定义Service接口类
package org.dog.server.service;
import org.dog.server.entity.User;
/**
* @Author: Odin
* @Date: 2023/1/27 18:43
* @Description:
*/
public interface UserService {
void add(User user);
User getById(String id);
User update(User user);
void deleteById(String id);
}
(七)、定义Service实现类
package org.dog.server.service.impl;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.dog.server.config.CacheKey;
import org.dog.server.entity.User;
import org.dog.server.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class UserServiceImpl implements UserService {
/**
* 模拟数据库存储数据
*/
private static HashMap<String, User> userMap = new HashMap<>();
private final RedisTemplate<String, Object> redisTemplate;
@Autowired
public UserServiceImpl(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
static {
userMap.put("1", new User("1", "zhangsan"));
userMap.put("2", new User("2", "lisi"));
userMap.put("3", new User("3", "wangwu"));
userMap.put("4", new User("4", "zhaoliu"));
}
@Override
// 1.保存Caffeine缓存 注意必须返回User对象出去,如果是void的话,Caffeine并不能帮我们存入缓存中
@CachePut(value = CacheKey.USER_CACHE_KEY, key = "#user.id")
public User add(User user) {
// 2.保存redis缓存
redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);
// 3.保存数据库(模拟)
userMap.put(user.getId(), user);
return user;
}
@Override
// 1.先从Caffeine缓存中读取
@Cacheable(value = CacheKey.USER_CACHE_KEY, key = "#id", sync = true)
public User getById(String id) {
// 2.如果缓存中不存在,则从Redis缓存中查找
String jsonString = (String) redisTemplate.opsForValue().get(id);
User user = JSON.parseObject(jsonString, User.class);
if (Objects.nonNull(user)) {
log.info("从Redis中查询到数据...");
return user;
}
// 3.如果Redis缓存中不存在,则从数据库中查询
user = userMap.get(id);
if (Objects.nonNull(user)) {
// 保存Redis缓存,20s后过期
redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);
}
log.info("从数据库中查询到数据...");
return user;
}
@Override
//1.更新Caffeine缓存
@CachePut(value = CacheKey.USER_CACHE_KEY, key = "#user.id")
public User update(User user) {
User oldUser = userMap.get(user.getId());
oldUser.setName(user.getName());
// 2.更新数据库
userMap.put(oldUser.getId(), oldUser);
// 3.更新Redis数据库
redisTemplate.opsForValue().set(oldUser.getId(), JSON.toJSONString(oldUser), 20, TimeUnit.SECONDS);
return oldUser;
}
@Override
//1.删除Caffeine缓存
@CacheEvict(value = CacheKey.USER_CACHE_KEY, key = "#id")
public void deleteById(String id) {
// 2.删除数据库
userMap.remove(id);
// 3.删除Redis缓存
redisTemplate.delete(id);
}
}
Spring Cache方面的注解主要有以下5个:
-
@Cacheable :触发缓存入口(这里一般放在创建和获取的方法上,@Cacheable注解会先查询是否已经有缓存,有会使用缓存,没有则会执行方法并缓存);
-
@CacheEvict :触发缓存的eviction(用于删除的方法上);
-
@CachePut :更新缓存且不影响方法执行(用于修改的方法上,该注解下的方法始终会被执行);
-
@Caching :将多个缓存组合在一个方法上(该注解可以允许一个方法同时设置多个注解);
-
@CacheConfig :在类级别设置一些缓存相关的共同配置(与其它缓存配合使用);
总结一下@Cacheable 和 @CachePut的区别:
-
@Cacheable:它的注解的方法是否被执行取决于Cacheable中的条件,方法很多时候都可能不被执行;
-
@CachePut:这个注解不会影响方法的执行,也就是说无论它配置的条件是什么,方法都会被执行,更多的时候是被用到修改上;
注意:标注了@Cacheable、@CachePut的方法,如果方法的返回类型是void或者方法返回值是null,那么将会把null值存入缓存中,当插入了数据后,再执行查询,会查出null的,必须注意这一点。因此推荐方法的返回值类型是要存进缓存的值类型。