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: pasjedis: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")@Primarypublic 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 {@Resourceprivate 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());}}