Spring Cache整合Redis
文章介绍继myabtis二级缓存整合redis之后,利用课余时间又研究了一下Spring Cache整合redis。原本是计划将这两篇文章和redis的介绍整合成一篇文章,但是。。。手贱的我在整合过程中不小心点击了刷新,导致新写的内容被清空了。花了我两天的宝贵时间啊。。。。。。额额额,这一篇是继删除之后,根据回忆,又重写了一篇。虽然原文档丢了,但是还好,参考的blog,笔记浏览器里面还保存着,不至
文章介绍
继myabtis二级缓存整合redis之后,利用课余时间又研究了一下Spring Cache整合redis。原本是计划将这两篇文章和redis的介绍整合成一篇文章,但是。。。手贱的我在整合过程中不小心点击了刷新,导致新写的内容被清空了。花了我两天的宝贵时间啊。。。。。。
额额额,这一篇是继删除之后,根据回忆,又重写了一篇。虽然原文档丢了,但是还好,参考的blog,笔记浏览器里面还保存着,不至于让我太过难受,好了。拿重点。
本篇主要从原生spring cache实现缓存操作,背后源码浅析,再到redis整合。最后又通过fastjson进行序列化,使保存在redis中的内容不至于乱码。
Spring Cache基本使用
简介
- Spring Cache将缓存作用于方法上,在方法的执行后缓存方法的返回内容
- 缓存数据时,默认以类名+方法名+参数以键,以方法的返回值为value进行缓存(当然,key可以自定义)
常用注解解释
下面的注解解释中,为了演示方便,我集成了redis分布式缓存,大家可以看到缓存的信息。即使不使用reids缓存,下方代码照样可以正常执行。文章在之后会介绍如何整合redis分布式缓存。
1.@CacheConfig
定义在类上,通常使用cacheNames,定义之后,该类下的所有含缓存注解的key之前都会拼接其属性值(附带两个::)。适用于某一个service的实现类或者mapper。
注意:如果service和mapper有联系时,比如都操纵的App这个entity,则service上的cacheNames会覆盖掉mapper
可定义的属性
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
String[] cacheNames() default {};
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
}
举例:
serviceImpl
@Service
@Transactional //控制事务
@AllArgsConstructor //代替@Autowired
@CacheConfig(cacheNames = "app")
public class AppServiceImpl implements IAppService {
private final AppDAO appDAO;
@Override
@Cacheable(key = "'id:'+#id") //当结果的name属性为zhq时,不进行缓存
public App findOne(Long id) {
App app = appDAO.findOne(id);
return app;
}
}
@Repository
@CacheConfig(cacheNames = "appDao")
public interface AppDAO extends BaseMapper<App> {
List<App> findAll();
App findOne(Long id);
void deleteOne(Long id);
}
效果:
从上面的运行结果我们可以看出,虽然我们在serviceImpl都定义了CacheCongfig,并且是不同的cacheNames,但是最后还是显示的serviceImpl上的cacheNames。
2.@Cacheable
-
定义在方法上,待方法运行结束时,缓存该方法的返回值。
-
每次执行该方法前,会先去缓存中查有没有相同条件下,缓存的数据,有的话直接拿缓存的数据,没有的话执行方法,并将执行结果返回。
-
默认以类名+方法名+参数为key,返回值为value
5个常用属性
key:
可以为null
存储在缓存中的键,可以根据它获取响应的值。默认是类名+方法名+参数。
可以通过SpEL进行自定义
默认:
自定义:
多参数时,某些参数可能不适合做key,此时我们就可以指定参数进行缓存。
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="#map['bookid'].toString()")
public Book findBook(Map<String, Object> map)
value/cacheNames:
不能为null
缓存名称,二者选任意一个即可。
源码中使用了@AliasFor注解,链接了cacheNames,这是一个别名注解。意味着value和cacheNames可以互相替换。当类上使用@ConfigConfig{cacheNames}定义时,cacheable中的cacheNames或者value会将其替代。
当我们定义多个cacheNames时,会给我们生成多个缓存名称,我们进行查询时,他也会查两个缓存文件下的数据。
condition:
根据条件判断结果是否缓存
默认为true 缓存
执行语句
public void testRedis(){
appService.findOne(8L);
appService.findOne(9L);
}
unless:
不被缓存的条件,默认为false。即能执行的都被缓存
id大于8的将不被缓存
public void testRedis(){
appService.findOne(8L);
appService.findOne(9L);
}
condition和unless的区别。
- condition默认为true,unless默认为false。
- condition为false时,unless为true。不被缓存
- condition为false,unless为false。不被缓存
- condition为true,unless为true。 不被缓存
- conditon为true,unless为false。缓存
@Cacheable(key = "'deptId:'+#deptId",unless="#result == null") 设置当结果为null,则不进行缓存
sync
是否同步,true/false。在一个多线程的环境中,某些操作可能被相同的参数并发地调用,这样同一个 value 值可能被多次计算(或多次访问 db),这样就达不到缓存的目的。针对这些可能高并发的操作,我们可以使用 sync 参数来告诉底层的缓存提供者将缓存的入口锁住,这样就只能有一个线程计算操作的结果值,而其它线程需要等待,这样就避免了 n-1 次数据库访问。
sync = true 可以有效的避免缓存击穿的问题。
3.@CachePut
和Cacheablle有相同的属性(没有 sync 属性),通常用于更新操作。
@Cacheable 的逻辑是:查找缓存 - 有就返回 -没有就执行方法体 - 将结果缓存起来;
@CachePut 的逻辑是:执行方法体 - 将结果缓存起来;
注意:@Cacheable 和 @CachePut 注解到同一个方法。
@Override
@CachePut(key = "'id:'+#app.appId")
public App updateApp(App app) {
appDAO.updateByPrimaryKey(app);
return app;
}
@Test
public void testInsert() {
//増
App app = new App();
app.setAppId(null);
app.setName("spring缓存测试");
app.setDescription("第一次测试");
appService.saveApp(app);
//查
System.out.println("第一次查询"+ appService.findOne(app.getAppId()));
//再次查
System.out.println("第二次查询"+appService.findOne(app.getAppId()));
//改
app.setName("更新项目");
appService.updateApp(app);
System.out.println("更新后"+appService.findOne(app.getAppId()));
}
4.@CacheEvit
删除缓存,每次调用它注解的方法,就会执行删除指定的缓存
跟 @Cacheable 和 @CachePut 一样,@CacheEvict 也要求指定一个或多个缓存,也指定自定义一的缓存解析器和 key 生成器,也支持指定条件(condition 参数)
CacheEvit,有两个特有的属性:allEntries和beforeInvocation
allEntries:
默认为false,为true时,表示清空该cachename下的所有缓存
beforeInvocation:
默认为false,为true时,先删除缓存,再删除数据库。
//先走缓存,并且删除所有改cachename下的内容
@Override
@CacheEvict(key = "'id:'+#id",beforeInvocation =true ,allEntries = true)
public void deleteOne(Long id) {
appDAO.deleteOne(id);
}
测试
@Test
public void testRedis(){
appService.findOne(8L);
appService.findOne(9L);
}
app下有两条记录,我们再执行删除。
@Test
public void testDelete(){
//删除
appService.deleteOne(8L);
}
虽然我们删除的是8L,但是因为我们经过了@CacheEvit这个注解标注的方法,并且属性时allEntries,所以我们会清空缓存。
**注意:**因为缓存和数据库的执行顺序不同,很可能我们当删完数据库/或缓存其中一个时,服务器出现异常,导致其中一个没有删除。
(我的解决思路:重要的数据不使用CacheEvit维护,使用逻辑维护缓存,之后会在用后演示如何用逻辑维护缓存)
5.@Caching
组合 注解,有时候我们可以一个方法上定义多个缓存注解。
比如:一个添加的方法
我添加一个内容,并缓存这个添加的内容。(Cacheable)
当我添加新的内容时,我要先清空缓存或清除某一个缓存。(CacheEvit)
@Caching(cacheable = {
@Cacheable(value = "emp",key = "#p0"),
...
},
put = {
@CachePut(value = "emp",key = "#p0"),
...
},evict = {
@CacheEvict(value = "emp",key = "#p0"),
....
})
public User save(User user) {
....
}
在这里插入代码片
6.@EnableCaching
会自动扫描所有public中包含缓存的相关注解,使用来开启缓存的。
可以定义在缓存的配置类中(之后在解释),也可以配置在入口类中。
两个接口
1.CacheManager
缓存管理器,用于管理缓存组件。
cacheManager接口的作用是用来获取Cache,类似一种对象工厂,所有的Cache,必须依赖与CacheManager来获取
这里以redis缓存进行测试
@Autowired
CacheManager cacheManager;
@Test
public void getCacheBean(){
//获取我们定义的cacheNames
Collection<String> cacheNames = cacheManager.getCacheNames();
cacheNames.forEach(item->{
System.out.println(item);
// app:
// user:
});
//获取RedisCacheManager 名称默认以第一个字母小写。
Cache redisCacheManager = cacheManager.getCache("redisCacheManager");
System.out.println(redisCacheManager.getName());//redisCacheManager
}
缓存的形式有很多,每一种缓存的实现,都依靠一个缓存管理器,来配置该缓存的基本信息(序列化方法,缓存数据的过期时间等。)但是我们使用时,通常只使用一种。
默认情况下,spring为我们提供了这些缓存处理接口,当我们引入redis依赖时,他会再生成一个RedisCacheManager,上面的案例就是引入了redis的依赖。
2.cacheResolver
CacheResolver,缓存解析器是用来管理缓存管理器的,CacheResolver 保持一个 cacheManager 的引用,并通过它来检索缓存。CacheResolver 与 CacheManager 的关系有点类似于 KeyGenerator 跟 key。spring 默认提供了一个 SimpleCacheResolver,开发者可以自定义并通过 @Bean 来注入自定义的解析器,以实现更灵活的检索。
大多数情况下,我们的系统只会配置一种缓存,所以我们并不需要显式指定 cacheManager 或者 cacheResolver。但是 spring 允许我们的系统同时配置多种缓存组件,这种情况下,我们需要指定。指定的方式是使用 @Cacheable 的 cacheManager 或者 cacheResolver 参数。
注意:按照官方文档,cacheManager 和 cacheResolver 是互斥参数,同时指定两个可能会导致异常。
Spring默认本地缓存实现
默认情况下,spring默认使用的是SimpleCacheConfiguration,即使用ConcurrentMapCacheManager来实现缓存。
我们发现,在SimpleCacheConfiguration中,缓存的配置只对cacheNames进行了相关设置。对于缓存的过期时间,最大缓存数量等都没有进行设置。
也就是说在默认缓存的情况下,缓存的功能是比较局限的,我们需要手动配置。
同样,如果使用redis进行缓存时,我们需要对RedisCacheManager进行配置(之后会讲解)。
快速上手
使用步骤:
- 引入依赖
<!-- springcache-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
- 添加cache启动缓存注解EnableCaching
@SpringBootApplication
@MapperScan("com.cache.mycache.dao")
@EnableCaching
public class MycacheApplication {
public static void main(String[] args) {
SpringApplication.run(MycacheApplication.class, args);
}
}
- 相应位置添加缓存注解
@Service
@Transactional //控制事务
@AllArgsConstructor //代替@Autowired
@CacheConfig(cacheNames = "app")
public class AppServiceImpl implements IAppService {
private final AppDAO appDAO;
@Override
@Cacheable(key = "'id:'+#id",condition = "#id>8")
public App findOne(Long id) {
App app = appDAO.findOne(id);
return app;
}
@Override
public List<App> findAll() {
List<App> allApp = appDAO.findAll();
System.out.println(allApp);
return allApp;
}
//先走缓存,并且删除所有改cachename下的内容
@Override
@CacheEvict(key = "'id:'+#id",beforeInvocation =true ,allEntries = true)
public void deleteOne(Long id) {
appDAO.deleteOne(id);
}
@Override
// @CachePut(key = "'id:'+#app.getAppId()")
public void saveApp(App app) {
appDAO.insert(app);
}
@Override
@CachePut(key = "'id:'+#app.appId")
public App updateApp(App app) {
appDAO.updateByPrimaryKey(app);
return app;
}
//自定义的清除所有缓存的方法
@Override
@CacheEvict(allEntries = true)
public void clearCache(){
}
}
- 测试使用
先添加,再进行两次同一条数据查询,再更新。我们再查一个其他id的数据,此时缓存中应该有两条数据。最后执行删除,因为我们删除时,设置的CacheEvit的allEntries为true,应该是删除所有数据。此时我们应该再进行数据库查,而非缓存查。
@Test
public void testInsert() {
//増
App app = new App();
app.setAppId(null);
app.setName("spring缓存测试");
app.setDescription("第一次测试");
appService.saveApp(app);
//查
System.out.println("第一次查询"+ appService.findOne(app.getAppId()));
//再次查
System.out.println("第二次查询"+appService.findOne(app.getAppId()));
//改
app.setName("更新项目");
appService.updateApp(app);
System.out.println("更新后"+appService.findOne(app.getAppId()));
//我们再查一个id为35的。此时会缓存两条数据,一条是我们刚刚新建立的那个app的id,一个是35
//删除掉35的id
appService.deleteOne(35L);
//查新建立的appid
System.out.println("清空缓存之后,再次查"+appService.findOne(app.getAppId()));
}
总结
通过原生cache的使用,我们发现其弊端很明显,因为他同mybatis默认的二级缓存一样,数据是存储在应用服务器上,当我们项目重启时,他就会删除掉之前的所有缓存。对于中大型项目会非常的不适用。
Spring Cache整合Redis
之前在CacheManager介绍那里,我们知道,当我们使用一种缓存方式时,必须定义一个缓存的管理器,来操作我们的缓存,并设置一些基本的信息,满足的需求。所以,我们整合redis,实现自定义缓存管理,也得从这两个方面入手。一个是缓存管理器,一个是缓存的配置信息。
整合步骤
1.引入redis和cache依赖
<!-- redis缓存-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- springcache-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2.配置redis的相关信息
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=20
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=10
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=1000
3.配置缓存管理器
创建RedisConfig类,继承 CachingConfigurerSupport 类。
CachingConfigurerSupport :继承了CachingConfiguer接口。这个接口为我们提供了四个方法,我们可以进行相关的配置。
配置方式一:完全自定义配置(本文的方式)
重新创建一个RedisCacheManager,定义其主键生成策略,基本配置,以及错误处理接口
基本配置在RedisCacheManager中设置,我们不采用bean注入的方式设置redis缓存管理的基本配置,通过RedisCacheManager的cacheDefaults方法进行设置。。
使用自定义的RedisCacheManager,我们可以更自由的设置其属性,比如我们可以根据不同的cacheNames从而设置不同的过期时间。
期间我们使用了fastjson进行序列化,这样我们通过注解往缓存中存储数据就不会乱码了
//缓存管理器。可以管理多个缓存
//只有CacheManger才能扫描到cacheable注解
//spring提供了缓存支持Cache接口,实现了很多个缓存类,其中包括RedisCache。但是我们需要对其进行配置,这里就是配置RedisCache
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheManager cacheManager = RedisCacheManager.RedisCacheManagerBuilder
//Redis链接工厂
.fromConnectionFactory(connectionFactory)
//缓存配置 通用配置 默认存储一小时
.cacheDefaults(getCacheConfigurationWithTtl(Duration.ofHours(1)))
//配置同步修改或删除 put/evict
.transactionAware()
//对于不同的cacheName我们可以设置不同的过期时间
.withCacheConfiguration("app:",getCacheConfigurationWithTtl(Duration.ofHours(5)))
.withCacheConfiguration("user:",getCacheConfigurationWithTtl(Duration.ofHours(2)))
.build();
return cacheManager;
}
//缓存的基本配置对象
private RedisCacheConfiguration getCacheConfigurationWithTtl(Duration duration) {
return RedisCacheConfiguration
.defaultCacheConfig()
//设置key value的序列化方式
// 设置key为String
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
// 设置value 为自动转Json的Object
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer))
// 不缓存null
.disableCachingNullValues()
// 设置缓存的过期时间
.entryTtl(duration);
}
配置方式二:
使用原有的RedisCacheManager。
这种方式特别简单,不创建新的RedisCacheManager的Bean对象,通过RedisCacheConfiguration来设置缓存的基本设置。
默认过期时间是2个小时
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
configuration =
configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer)).entryTtl(Duration.ofHours(2));
return configuration;
}
默认RedisCacheManager的源代码
4.自定义主键key的生成策略
当我们不设置主键时,主键的生成策略
//主键生成策略 不设置主键时的生成策略 类名+方法名+参数
@Override
@Bean
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuffer sb = new StringBuffer();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params
) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
5.缓存的异常处理
当进行缓存出现异常时,不进行缓存操作
//缓存的异常处理
@Bean
@Override
public CacheErrorHandler errorHandler() {
// 异常处理,当Redis发生异常时,打印日志,但是程序正常走
log.info("初始化 -> [{}]", "Redis CacheErrorHandler");
return new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
log.error("Redis occur handleCacheGetError:key -> [{}]", key, e);
}
@Override
public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);
}
@Override
public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);
}
@Override
public void handleCacheClearError(RuntimeException e, Cache cache) {
log.error("Redis occur handleCacheClearError:", e);
}
};
}
6.重新定义redis操作模板
spring为我们提供了RedisTemplate和StringRedisTemplate,但是因为他们存储数据的值默认是使用jdk进行序列化的,存储的时候是以2进制的形式进行存储,不便于我们观看。
我们采用FastJson进行序列化,来存储便于我们观看的对象信息。
//操纵缓存的模板
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
System.out.println("redisTemplate");
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(fastJsonRedisSerializer);
redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
//操纵缓存的模板
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
System.out.println("stringTemplate");
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setKeySerializer(new StringRedisSerializer());
stringRedisTemplate.setValueSerializer(fastJsonRedisSerializer);
stringRedisTemplate.setConnectionFactory(factory);
stringRedisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
return stringRedisTemplate;
}
7.使用FastJson解决缓存的乱码问题
我们知道,缓存默认使用的是jdk序列化,它是以二进制的形式往缓存中存储数据的,导致我们存储的数据成乱码的形式。
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
//重写FastJsonRedisSerialize的实现方式。
class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
private ObjectMapper objectMapper = new ObjectMapper();
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
static {
// 全局开启AutoType,这里方便开发,使用全局的方式
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// 建议使用这种方式,小范围指定白名单
// ParserConfig.getGlobalInstance().addAccept("me.zhengjie.domain");
// key的序列化采用StringRedisSerializer
}
public FastJsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
//序列化 我们存储时,存储的是json对象,而默认存储的是byte类型的,所以在可视化窗口上显示时,看到的是乱码
@Override
public byte[] serialize(T t) throws SerializationException {
System.out.println("进行序列化");
if (t == null) {
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
//反序列化
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length <= 0) {
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
}
8.测试使用
@Test
public void testInsert() {
//増
App app = new App();
app.setAppId(null);
app.setName("spring缓存测试");
app.setDescription("第一次测试");
appService.saveApp(app);
//查
System.out.println("第一次查询"+ appService.findOne(app.getAppId()));
//再次查
System.out.println("第二次查询"+appService.findOne(app.getAppId()));
//改
app.setName("更新项目");
appService.updateApp(app);
System.out.println("更新后"+appService.findOne(app.getAppId()));
//我们再查一个id为35的。此时会缓存两条数据,一条是我们刚刚新建立的那个app的id,一个是35
//删除掉35的id
appService.deleteOne(35L);
//查新建立的appid
System.out.println("清空缓存之后,再次查"+appService.findOne(app.getAppId()));
}
我们发现,除了配置,其他的使用都和原生的cache一模一样。只是我们是分布式缓存,我们重启应用服务时,缓存中的数据并不会清空。
最后一次我们查的是41,我们这次重启服务,直接查一下41
System.out.println(appService.findOne(41L));
没有走数据库,直接查询的缓存。
9…自定义缓存维护
之前我们的缓存实现,都是通过spring提供的缓存注释完成的。虽然能完成某条缓存的增删改,但是我们也发现,以上的情况我们都是用于单表的情况下。
假如我们在下面这个场景中:
我们要获取一个人员及其所在的部门。人员和部门各在一张表。
(user表的dept_id指向dept表的id)
SELECT u.`id`,u.`nick_name`,d.`id` AS deptId ,d.`name`
FROM sys_user u,sys_dept d
WHERE u.dept_id = d.id
这是我们查出来的信息。
我们往缓存中存储时,以user的id为key进行存储。
key:
id:3
value: { “dept”: {“id”: 18,“name”: “家族一期” }, “id”: 3, “name”: “光亮”}
此时问题来了,如果我们修改了用户的信息,我们使用CacheEvit删除掉当前的用户即可。再次查询时,再把他存入缓存。但是如果我们修改了部门的信息,那么我们该如何进行缓存维护呢?
serviceimpl
userServiceImpl
@Override
@Cacheable(key = "'id:'+#id")
public UserDTO findUserAndDeptById(Long id) {
return userDAO.findUserAndDeptById(id);
}
deptServiceImpl
@Override
public void deleteOne(Long id) {
deptDAO.deleteOne(id);
}
mapper.xml
<select id="findUserAndDeptById" parameterType="com.cache.mycache.entity.dto.UserDTO" resultMap="userMessage">
SELECT u.`id`,u.`nick_name`,d.`id` AS deptId ,d.`name` FROM sys_user u,sys_dept d WHERE u.dept_id = d.id and u.id=#{id}
</select>
<resultMap id="userMessage" type="com.cache.mycache.entity.dto.UserDTO">
<id property="id" column="id" />
<result property="name" column="nick_name"/>
<association property="dept" javaType="com.cache.mycache.entity.Dept">
<result property="id" column="deptId"/>
<result property="name" column="name"/>
</association>
</resultMap>
测试:
@Test
public void testUserAndDept(){
//查询进缓存
UserDTO userAndDeptById = userService.findUserAndDeptById(3L);
System.out.println("在缓存中查询"+userService.findUserAndDeptById(3L));
//删除部门
deptService.deleteOne(userAndDeptById.getDept().getId());
}
查看缓存:
我们发现,我们的部门已经删除,但是缓存中的数据还是原始的数据。
此时凭借缓存注解,貌似已经很难完成我们的业务了。所以我们只能靠逻辑来处理了。
分析:
用户基本信息+部门信息 ( 键为用户id)
删除/更新该用户:使用注解CacheEvid,指向该id,删除缓存记录。
@Override
@CacheEvict(key = "'id:'+#id")
public void deleteOne(Long id) {
userDAO.deleteOne(id);
}
删除/更新部门:通过逻辑获取该部门中的所有人员id,清空该部门下所有以该人员id为键的缓存信息(因为一个部门有很多人员,可能不止一个人缓存了基本信息和部门信息)
删除部门
@Override
public void deleteDept(Long id) {
//获取该部门下的所有员工
List<Long> userByDeptId = userService.findUserByDeptId(id);
userByDeptId.forEach(item->redisTemplate.delete("user::id:"+item));
deptDAO.deleteOne(id);
}
我们查询id为1的数据,之后再查一遍看是否进缓存,之后再删除部门7,看看缓存中的数据是否清除
@Test
public void testUserAndDept(){
//查询进缓存
UserDTO userAndDeptById = userService.findUserAndDeptById(1L);
System.out.println("在缓存中查询"+userService.findUserAndDeptById(1L));
//删除部门
deptService.deleteOne(userAndDeptById.getDept().getId());
}
查看缓存
我们发现,缓存中的内容已经成功删除。
我们上面演示了删除操作,更新操作同理。
附:
SpringCache整合redis配置类
@Slf4j
@Configuration
@EnableCaching
@ConditionalOnClass(RedisOperations.class) // 该配置类执行的条件是,RedisOperations的bean对象已经存在
@EnableConfigurationProperties(RedisProperties.class) //使RedisProperties的@ConfigurationProperties生效
public class RedisConfig extends CachingConfigurerSupport {
private static final FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
//主键生成策略 不设置主键时的生成策略 类名+方法名+参数
@Override
@Bean
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuffer sb = new StringBuffer();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params
) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
//缓存管理器。可以管理多个缓存
//只有CacheManger才能扫描到cacheable注解
//spring提供了缓存支持Cache接口,实现了很多个缓存类,其中包括RedisCache。但是我们需要对其进行配置,这里就是配置RedisCache
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheManager cacheManager = RedisCacheManager.RedisCacheManagerBuilder
//Redis链接工厂
.fromConnectionFactory(connectionFactory)
//缓存配置 通用配置 默认存储一小时
.cacheDefaults(getCacheConfigurationWithTtl(Duration.ofHours(1)))
//配置同步修改或删除 put/evict
.transactionAware()
//对于不同的cacheName我们可以设置不同的过期时间
.withCacheConfiguration("app:",getCacheConfigurationWithTtl(Duration.ofHours(5)))
.withCacheConfiguration("user:",getCacheConfigurationWithTtl(Duration.ofHours(2)))
.build();
return cacheManager;
}
//缓存的基本配置对象
private RedisCacheConfiguration getCacheConfigurationWithTtl(Duration duration) {
return RedisCacheConfiguration
.defaultCacheConfig()
//设置key value的序列化方式
// 设置key为String
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
// 设置value 为自动转Json的Object
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer))
// 不缓存null
.disableCachingNullValues()
// 设置缓存的过期时间
.entryTtl(duration);
}
//操纵缓存的模板
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
System.out.println("redisTemplate");
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(fastJsonRedisSerializer);
redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
//操纵缓存的模板
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
System.out.println("stringTemplate");
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setKeySerializer(new StringRedisSerializer());
stringRedisTemplate.setValueSerializer(fastJsonRedisSerializer);
stringRedisTemplate.setConnectionFactory(factory);
stringRedisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
return stringRedisTemplate;
}
//缓存的异常处理
@Bean
@Override
public CacheErrorHandler errorHandler() {
// 异常处理,当Redis发生异常时,打印日志,但是程序正常走
log.info("初始化 -> [{}]", "Redis CacheErrorHandler");
return new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
log.error("Redis occur handleCacheGetError:key -> [{}]", key, e);
}
@Override
public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);
}
@Override
public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);
}
@Override
public void handleCacheClearError(RuntimeException e, Cache cache) {
log.error("Redis occur handleCacheClearError:", e);
}
};
}
}
//重写FastJsonRedisSerialize的实现方式。
class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
private ObjectMapper objectMapper = new ObjectMapper();
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
static {
// 全局开启AutoType,这里方便开发,使用全局的方式
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// 建议使用这种方式,小范围指定白名单
// ParserConfig.getGlobalInstance().addAccept("me.zhengjie.domain");
// key的序列化采用StringRedisSerializer
}
public FastJsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
//序列化 我们存储时,存储的是json对象,而默认存储的是byte类型的,所以在可视化窗口上显示时,看到的是乱码
@Override
public byte[] serialize(T t) throws SerializationException {
System.out.println("进行序列化");
if (t == null) {
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
//反序列化
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length <= 0) {
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
}
Redis工具类
spring虽然提供了template对redis的操作进行了封装,但是实际使用还是有些麻烦,实际项目中,我们通常使用redis的工具类来进行操作。
附:redis工具类
/**
* 功能描述:SpringData Redis 的工具类
*
* @author
* Date: 2020/4/11 21:07
**/
@RequiredArgsConstructor
@Component
@SuppressWarnings({"unchecked", "all"})
@Slf4j
public class RedisUtils {
private final RedisTemplate<Object, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 根据 key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(Object key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 查找匹配key
*
* @param pattern key
* @return /
*/
public List<String> scan(String pattern) {
ScanOptions options = ScanOptions.scanOptions().match(pattern).build();
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection rc = Objects.requireNonNull(factory).getConnection();
Cursor<byte[]> cursor = rc.scan(options);
List<String> result = new ArrayList<>();
while (cursor.hasNext()) {
result.add(new String(cursor.next()));
}
try {
RedisConnectionUtils.releaseConnection(rc, factory);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 分页查询 key
*
* @param patternKey key
* @param page 页码
* @param size 每页数目
* @return /
*/
public List<String> findKeysForPage(String patternKey, int page, int size) {
ScanOptions options = ScanOptions.scanOptions().match(patternKey).build();
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection rc = Objects.requireNonNull(factory).getConnection();
Cursor<byte[]> cursor = rc.scan(options);
List<String> result = new ArrayList<>(size);
int tmpIndex = 0;
int fromIndex = page * size;
int toIndex = page * size + size;
while (cursor.hasNext()) {
if (tmpIndex >= fromIndex && tmpIndex < toIndex) {
result.add(new String(cursor.next()));
tmpIndex++;
continue;
}
// 获取到满足条件的数据后,就可以退出了
if (tmpIndex >= toIndex) {
break;
}
tmpIndex++;
cursor.next();
}
try {
RedisConnectionUtils.releaseConnection(rc, factory);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 批量获取
*
* @param keys
* @return
*/
public List<Object> multiGet(List<String> keys) {
Object obj = redisTemplate.opsForValue().multiGet(Collections.singleton(keys));
return null;
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间
* @param timeUnit 类型
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time, TimeUnit timeUnit) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, timeUnit);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// ================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, - by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================增 list @ 2020/2/6=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return /
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
return redisTemplate.opsForList().remove(key, count, value);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//-----------------------自定义工具扩展 @ 2020/2/6----------------------
/**
* 功能描述:在list的右边添加元素
* 如果键不存在,则在执行推送操作之前将其创建为空列表
*
* @param key 键
* @return value 值
* @author
* Date: 2020/2/6 23:22
*/
public Long rightPushValue(String key, Object value) {
return redisTemplate.opsForList().rightPush(key, value);
}
/**
* 功能描述:在list的右边添加集合元素
* 如果键不存在,则在执行推送操作之前将其创建为空列表
*
* @param key 键
* @return value 值
* @author
* Date: 2020/2/6 23:22
*/
public Long rightPushList(String key, List<Object> values) {
return redisTemplate.opsForList().rightPushAll(key, values);
}
/**
* 指定缓存失效时间,携带失效时间的类型
*
* @param key 键
* @param time 时间(秒)
* @param unit 时间的类型 TimeUnit枚举
*/
public boolean expire(String key, long time, TimeUnit unit) {
try {
if (time > 0) {
redisTemplate.expire(key, time, unit);
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* @param prefix 前缀
* @param ids id
*/
public void delByKeys(String prefix, Set<Long> ids) {
Set<Object> keys = new HashSet<>();
for (Long id : ids) {
keys.addAll(redisTemplate.keys(new StringBuffer(prefix).append(id).toString()));
}
long count = redisTemplate.delete(keys);
// 此处提示可自行删除
log.debug("--------------------------------------------");
log.debug("成功删除缓存:" + keys.toString());
log.debug("缓存删除数量:" + count + "个");
log.debug("--------------------------------------------");
}
}
参考Blog
spring cache
spring cache 缓存注解的使用
SpringBoot + Redis:基本配置及使用
SpringBoot配置多CacheManager
Spring缓存源码剖析:(二)CacheManager
Redis使用FastJson序列化/FastJson2JsonRedisSerializer
SpringBoot下Redis相关配置是如何被初始化的(细看)
更多推荐
所有评论(0)