SpringBoot+Caffeine+Redis声明式缓存
最近接到一个项目,里面同时整合了Caffeine和Redis。
对于像验证码,或者对用户操作做一些限制的缓存,还有分布式锁等等操作就利用redis来缓存,
对于一些热点数据,为了降低数据库查询频率,就使用Caffeine本地缓存来实现。
至于为什么这么做?这个问题问得好!下次别问了!
记录一个项目中同时整合了Caffeine和Redis时,怎么使用@Cacheable这样的注解,优雅地实现缓存。
相关知识
对相关原理和注解@Cacheable/@CachePut/@CacheEvit不熟练的同学请移步相关文章,能很好地理解。
代码实践】
废话不多讲,直接开撸。
引入组件
先创建一个springboot项目,再pom文件中导入以下依赖:
<!-- Redis缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.1</version>
</dependency>
<!-- 咖啡因缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.1</version>
</dependency>
配置文件
接下来就是针对两个缓存组件的配置
在application.yml中,需要对redis的连接信息做一些基础配置,Caffeine不用。
spring:
redis:
# Redis服务器地址
host: 127.0.0.1
# Redis数据库索引(默认为0)
database: 1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password: pas
jedis:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 0
# 连接超时时间(毫秒)
timeout: 3000ms
配置类
CacheConfig 配置类:
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
@Configuration
public class CacheConfig {
@Bean("caffeineCacheManager")
@Primary
public CacheManager cacheManager(){
SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
ArrayList<Cache> caches = new ArrayList<>();
caches.add(new CaffeineCache("test1",
Caffeine.newBuilder()
.expireAfterWrite(100, TimeUnit.SECONDS)
.recordStats()
.maximumSize(Integer.MAX_VALUE)
.removalListener((key, value, cause) -> {
System.out.println("");
}).build()));
caches.add(new CaffeineCache("test2",
Caffeine.newBuilder()
.expireAfterWrite(100, TimeUnit.SECONDS)
.recordStats()
.maximumSize(Integer.MAX_VALUE)
.removalListener((key, value, cause) -> {
System.out.println("");
}).build()));
simpleCacheManager.setCaches(caches);
return simpleCacheManager;
}
@Bean("redisCacheManager")
public CacheManager redisCacheManager(RedisConnectionFactory factory){
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// 设置缓存的默认过期时间
.entryTtl(Duration.ofSeconds(180))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()))
// 不缓存空值
.disableCachingNullValues();
return RedisCacheManager
.builder(factory)
.cacheDefaults(config)
.transactionAware()
.build();
}
}
之所以要配置两个Manager的原因简单说一下。
我们在使用@Cacheable注解时,在Caffeine中,Spring底层是通过@Cacheable的cacheManager属性的值去找对应的CacheManger中名为value属性值的缓存容器实例;
而在Redist中又不一样,整个Redis就是一个缓存容器,所以是通过CacheManager的属性值去调用对应的Redis缓存容器实例,而此时的value属性值和key属性的值,一起组成了redis的key。
启动类
启动类上添加注解@EnableCaching开启自动缓存支持。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableCaching //开启自动缓存
@EnableAsync //开启异步支持
@EnableTransactionManagement // 开启事务支持
public class ProjectApplication {
public static void main(String[] args) {
SpringApplication.run(ProjectApplication.class, args);
}
}
基本上就算是配置结束了,下面可以直接使用了。
实体类
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* @version 1.0.0
* @Author DG
* @Date 2022/1/6 15:13
*/
@Data
@Accessors(chain = true) // 链式编程
public class Person implements Serializable {
private Long id;
private String name;
private int age;
private String[] hobby;
private String address;
private Date createTime;
}
控制层
import com.xxx.xxx.entity.Person;
import com.xxx.xxx.service.base.CacheService;
import com.xxx.xxx.service.base.CacheTestService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.support.atomic.RedisAtomicLong;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* @version 1.0.0
* @Author DG
* @Date 2022/1/6 15:23
*/
@RestController
@Api(value = "测试功能", tags = "功能测试")
public class CacheController {
@Resource
private CacheTestService cacheTestService;
@GetMapping("/get")
@ApiOperation(value = "这是测试一级缓存和二级缓存同时使用的控制器", tags = "如果入参为奇数走redis,如果入参为偶数走caffeine")
@ApiImplicitParam(name = "id", value = "对象ID,就是一个标记而已", dataType = "Long", dataTypeClass = Long.class, defaultValue = "0", example = "0")
public List<Person> selectPerson(Long id){
Person cache1;
Person cache2;
if (id % 2 == 0) {
cache1 = cacheTestService.testCaffeineCache1(id);
cache2 = cacheTestService.testRedisCache1(id);
} else {
cache1 = cacheTestService.testCaffeineCache2(id);
cache2 = cacheTestService.testRedisCache2(id);
}
return Arrays.asList(cache1, cache2);
}
}
业务层
CacheTestService 接口:
import com.xxx.xxx.entity.Person;
public interface CacheTestService {
/**
* 测试caffeine缓存1
* @return
*/
Person testCaffeineCache1(Long id);
/**
* 测试caffeine缓存2
* @return
*/
Person testCaffeineCache2(Long id);
/**
* 测试redis缓存1
* @return
*/
Person testRedisCache1(Long id);
/**
* 测试redis缓存2
* @return
*/
Person testRedisCache2(Long id);
}
CacheTestServiceImpl 实现类
import com.xxx.xxx.entity.Person;
import com.xxx.xxx.service.base.CacheTestService;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
public class CacheTestServiceImpl implements CacheTestService {
@Override
@Cacheable(value = "test1", key = "#id", cacheManager = "caffeineCacheManager")
public Person testCaffeineCache1(Long id) {
// 模拟数据库查询并返回
return new Person()
.setId(id)
.setAge(18)
.setHobby(new String[]{"java"})
.setAddress("松下问童子")
.setName("caffeineCache1")
.setCreateTime(new Date());
}
@Override
@Cacheable(value = "test2", key = "#id", cacheManager = "caffeineCacheManager")
public Person testCaffeineCache2(Long id) {
// 模拟数据库查询并返回
return new Person()
.setId(id)
.setAge(19)
.setHobby(new String[]{"C#"})
.setAddress("言师采药去")
.setName("caffeineCache2")
.setCreateTime(new Date());
}
@Override
@Cacheable(value = "test1", key = "#id", cacheManager = "redisCacheManager")
public Person testRedisCache1(Long id) {
// 模拟数据库查询并返回
return new Person()
.setId(id)
.setAge(20)
.setHobby(new String[]{"Python"})
.setAddress("只在此山中")
.setName("redisCache1")
.setCreateTime(new Date());
}
@Override
@Cacheable(value = "test2", key = "#id", cacheManager = "redisCacheManager")
public Person testRedisCache2(Long id) {
// 模拟数据库查询并返回
return new Person()
.setId(id)
.setAge(21)
.setHobby(new String[]{"Go"})
.setAddress("云深不知处")
.setName("redisCache2")
.setCreateTime(new Date());
}
}