赞
踩
继myabtis二级缓存整合redis之后,利用课余时间又研究了一下Spring Cache整合redis。原本是计划将这两篇文章和redis的介绍整合成一篇文章,但是。。。手贱的我在整合过程中不小心点击了刷新,导致新写的内容被清空了。花了我两天的宝贵时间啊。。。。。。
额额额,这一篇是继删除之后,根据回忆,又重写了一篇。虽然原文档丢了,但是还好,参考的blog,笔记浏览器里面还保存着,不至于让我太过难受,好了。拿重点。
本篇主要从原生spring cache实现缓存操作,背后源码浅析,再到redis整合。最后又通过fastjson进行序列化,使保存在redis中的内容不至于乱码。
下面的注解解释中,为了演示方便,我集成了redis分布式缓存,大家可以看到缓存的信息。即使不使用reids缓存,下方代码照样可以正常执行。文章在之后会介绍如何整合redis分布式缓存。
定义在类上,通常使用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。
定义在方法上,待方法运行结束时,缓存该方法的返回值。
每次执行该方法前,会先去缓存中查有没有相同条件下,缓存的数据,有的话直接拿缓存的数据,没有的话执行方法,并将执行结果返回。
默认以类名+方法名+参数为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的区别。
@Cacheable(key = "'deptId:'+#deptId",unless="#result == null") 设置当结果为null,则不进行缓存
- 1
sync
是否同步,true/false。在一个多线程的环境中,某些操作可能被相同的参数并发地调用,这样同一个 value 值可能被多次计算(或多次访问 db),这样就达不到缓存的目的。针对这些可能高并发的操作,我们可以使用 sync 参数来告诉底层的缓存提供者将缓存的入口锁住,这样就只能有一个线程计算操作的结果值,而其它线程需要等待,这样就避免了 n-1 次数据库访问。
sync = true 可以有效的避免缓存击穿的问题。
和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())); }
删除缓存,每次调用它注解的方法,就会执行删除指定的缓存
跟 @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维护,使用逻辑维护缓存,之后会在用后演示如何用逻辑维护缓存)
组合 注解,有时候我们可以一个方法上定义多个缓存注解。
比如:一个添加的方法
我添加一个内容,并缓存这个添加的内容。(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) {
....
}
在这里插入代码片
会自动扫描所有public中包含缓存的相关注解,使用来开启缓存的。
可以定义在缓存的配置类中(之后在解释),也可以配置在入口类中。
缓存管理器,用于管理缓存组件。
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的依赖。
CacheResolver,缓存解析器是用来管理缓存管理器的,CacheResolver 保持一个 cacheManager 的引用,并通过它来检索缓存。CacheResolver 与 CacheManager 的关系有点类似于 KeyGenerator 跟 key。spring 默认提供了一个 SimpleCacheResolver,开发者可以自定义并通过 @Bean 来注入自定义的解析器,以实现更灵活的检索。
大多数情况下,我们的系统只会配置一种缓存,所以我们并不需要显式指定 cacheManager 或者 cacheResolver。但是 spring 允许我们的系统同时配置多种缓存组件,这种情况下,我们需要指定。指定的方式是使用 @Cacheable 的 cacheManager 或者 cacheResolver 参数。
注意:按照官方文档,cacheManager 和 cacheResolver 是互斥参数,同时指定两个可能会导致异常。
默认情况下,spring默认使用的是SimpleCacheConfiguration,即使用ConcurrentMapCacheManager来实现缓存。
我们发现,在SimpleCacheConfiguration中,缓存的配置只对cacheNames进行了相关设置。对于缓存的过期时间,最大缓存数量等都没有进行设置。
也就是说在默认缓存的情况下,缓存的功能是比较局限的,我们需要手动配置。
同样,如果使用redis进行缓存时,我们需要对RedisCacheManager进行配置(之后会讲解)。
使用步骤:
<!-- springcache-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
@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默认的二级缓存一样,数据是存储在应用服务器上,当我们项目重启时,他就会删除掉之前的所有缓存。对于中大型项目会非常的不适用。
之前在CacheManager介绍那里,我们知道,当我们使用一种缓存方式时,必须定义一个缓存的管理器,来操作我们的缓存,并设置一些基本的信息,满足的需求。所以,我们整合redis,实现自定义缓存管理,也得从这两个方面入手。一个是缓存管理器,一个是缓存的配置信息。
<!-- 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>
# 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
创建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的源代码
当我们不设置主键时,主键的生成策略
//主键生成策略 不设置主键时的生成策略 类名+方法名+参数 @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(); } }; }
当进行缓存出现异常时,不进行缓存操作
//缓存的异常处理 @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); } }; }
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; }
我们知道,缓存默认使用的是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); } }
@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));
没有走数据库,直接查询的缓存。
之前我们的缓存实现,都是通过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());
}
查看缓存
我们发现,缓存中的内容已经成功删除。
我们上面演示了删除操作,更新操作同理。
@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); } }
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("--------------------------------------------"); } }
spring cache
spring cache 缓存注解的使用
SpringBoot + Redis:基本配置及使用
SpringBoot配置多CacheManager
Spring缓存源码剖析:(二)CacheManager
Redis使用FastJson序列化/FastJson2JsonRedisSerializer
SpringBoot下Redis相关配置是如何被初始化的(细看)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。