背景
最近,项目中遇到一个redis缓存使用的问题,当redis连接不上时,直接导致业务异常。redis不是做为缓存使用吗?当缓存中查询不到,不是应该主动从数据库加载吗?
最后发现是利用RedisTemplate操作缓存,没有进行异常捕捉处理,导致异常抛出影响到业务的正常执行。
那么,你的项目中,缓存操作真的做到了解耦吗?
缓存原理
缓存的使用
目前redis缓存主要有2种使用方式:
方式一:结合Spring Cache使用,通过@Cacheable、@CachePut 、@CacheEvict这3个缓存注解实现缓存控制
方式二:通过RedisTemplate模板方法通过编码控制缓存
代码实战
依赖包:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
缓存连接属性配置:
# Redis_config spring.redis.host=localhost spring.redis.port=6379 spring.redis.password=123456 # 根据需要 # 连接超时时间(毫秒) spring.redis.timeout=10s # Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0 spring.redis.database=0 # 连接池最大连接数(使用负值表示没有限制) 默认 8 spring.redis.lettuce.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1 spring.redis.lettuce.pool.max-wait=-1s # 连接池中的最大空闲连接 默认 8 spring.redis.lettuce.pool.max-idle=8 # 连接池中的最小空闲连接 默认 0 spring.redis.lettuce.pool.min-idle=0
config配置类
/** * * 1、@EnableCaching是为了开启spring cache的缓存注解功能 * 2、继承CachingConfigurerSupport是为了配置spring cache的主键生成策略keyGenerator和cacheManager * 3、配置RedisTemplate的序列化机制Jackson * 4、配置spring cache的异常处理类CacheErrorHandler * @program: wxswj * @description: redis配置类 * @author: wanli * @create: 2018-10-09 18:39 **/ @Configuration @EnableCaching @AutoConfigureAfter(RedisAutoConfiguration.class) public class RedisConfig extends CachingConfigurerSupport { /** * @return 自定义策略生成的key * @description 自定义的缓存key的生成策略 * 若想使用这个key 只需要讲注解上keyGenerator的值设置为keyGenerator即可</br> */ @Bean @Override 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 public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) { //设置序列化 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); //配置redisTemplate RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>(); redisTemplate.setConnectionFactory(redisConnectionFactory); RedisSerializer stringSerializer = new StringRedisSerializer(); //key序列化 redisTemplate.setKeySerializer(stringSerializer); //value序列化 redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); //Hash key序列化 redisTemplate.setHashKeySerializer(stringSerializer); //Hash value序列化 redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } //缓存管理器 @Bean public RedisCacheManager cacheManager(LettuceConnectionFactory redisConnectionFactory) { Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig(); redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) .entryTtl(Duration.ofHours(1)); return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory)).cacheDefaults(redisCacheConfiguration).build(); } @Override @Bean public CacheErrorHandler errorHandler(){ //CacheErrorHandler cacheErrorHandler = new SimpleCacheErrorHandler(); CacheErrorHandler cacheErrorHandler = new CacheErrorHandler() { private Logger logger = LoggerFactory.getLogger(CacheErrorHandler.class); @Override public void handleCacheGetError(RuntimeException e, Cache cache, Object o) { logger.error("redis 异常:key=[{}]",o,e); } @Override public void handleCachePutError(RuntimeException e, Cache cache, Object o, Object o1) { logger.error("redis 异常:key=[{}]",o,e); } @Override public void handleCacheEvictError(RuntimeException e, Cache cache, Object o) { logger.error("redis 异常:key=[{}]",o,e); } @Override public void handleCacheClearError(RuntimeException e, Cache cache) { logger.error("redis 异常:",e); } }; return cacheErrorHandler; } }
这里补充说明一下,CacheErrorHandler是Spring Cache里面注解控制缓存的异常处理类,其默认实现是SimpleCacheErrorHandler,里面对异常的处理都是直接抛出。
所以,当redis服务器出现连接异常或操作失败时,会影响后续的业务代码执行。
public class SimpleCacheErrorHandler implements CacheErrorHandler { public SimpleCacheErrorHandler() { } public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) { throw exception; } public void handleCachePutError(RuntimeException exception, Cache cache, Object key, @Nullable Object value) { throw exception; } public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) { throw exception; } public void handleCacheClearError(RuntimeException exception, Cache cache) { throw exception; } }
需要缓存的实体:
@Data public class Person { private Integer id; private String name; private Integer age; }
通过@Cacheable控制读操作的缓存
/** * 通过注解@Cacheable中的value相当于声明一个存放缓存的文件夹,可以理解为 "get:"+keyGenerator * keyGenerator = "#id" * @param id * @return */ @Cacheable(value = "person",keyGenerator = "keyGenerator") @Override public Person get(Integer id){ log.info("未命中缓存,从数据库查询"); Person person = new Person(); person.setId(id); person.setName("laowan"); person.setAge(25); return person; }
通过RedisTemplate封装缓存操作服务类:
/** * @program: redis * @description: 缓存工具类 * @author: wanli * @create: 2020-05-12 09:42 **/ public interface CacheService { /** * 直接设置缓存 * @param key * @param value * @return */ boolean setCache(String key,Object value); /** * 设置缓存并设置过期时间 * @param key * @param value * @param timeout * @param timeUnit * @return */ boolean setCacheExpire(String key, Object value, long timeout, TimeUnit timeUnit); /** * 不设置回调返回的获取方法 * @param key * @param clazz * @param <T> * @return */ <T> T getCache(String key,Class<T> clazz); /** * 传递回调方法,重设缓存时设置过期时间 * @param key 键 * @return 值 */ <T> T getCache(String key,Class<T> clazz,long timeout, TimeUnit timeUnit,CacheCallBack<T,String> callBack); <T> T getCache(String key,Class<T> clazz,CacheCallBack<T,String> callBack); /** * 删除缓存 * @param key * @return */ boolean deleteCache(String key); }
从缓存获取为空的回调方法:
/** * @program: redis * @description: 缓存回调接口 * @author: wanli * @create: 2020-05-12 09:43 **/ public interface CacheCallBack <O,I> { O execute(I input); }
/** * @program: redis * @description: 缓存接口实现类 * @author: wanli * @create: 2020-05-12 09:50 **/ @Slf4j @Service public class CacheServiceImpl implements CacheService { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public boolean setCache(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } @Override public boolean setCacheExpire(String key, Object value, long timeout, TimeUnit timeUnit) { try { if(timeout>0){ redisTemplate.opsForValue().set(key, value, timeout, timeUnit); }else{ this.setCache(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } @Override public <T> T getCache(String key, Class<T> clazz) { T o = null; try { if(key!=null){ Object result = redisTemplate.opsForValue().get(key); o = result!=null?(T)result:null; } }catch (Exception e) { e.printStackTrace(); } return o; } @Override public <T> T getCache(String key, Class<T> clazz, CacheCallBack<T, String> callBack) { T o = null; try { o = this.getCache(key,clazz); if(o==null){ log.info("未命中缓存,执行CacheCallBack回调函数"); o = callBack.execute(key); if(o!=null){ this.setCache(key,o); } } }catch (Exception e) { e.printStackTrace(); } return o; } @Override public <T> T getCache(String key, Class<T> clazz, long timeout, TimeUnit timeUnit, CacheCallBack<T, String> callBack) { T o = null; try { o = this.getCache(key,clazz); if(o==null){ log.info("未命中缓存,执行CacheCallBack回调函数"); o = callBack.execute(key); if(o!=null){ this.setCacheExpire(key,o,timeout,timeUnit); } } }catch (Exception e) { e.printStackTrace(); } return o; } @Override public boolean deleteCache(String key) { try { redisTemplate.delete(key); return true; } catch (Exception e) { e.printStackTrace(); return false; } } }
通过封装的服务类CacheServiceImpl控制缓存:
private String person_cache_key="person:get:"; @Autowired CacheService cacheService; /** * 硬编码实现查询缓存——》判空——》然后查询数据库——》判空——》更新缓存 * @param id * @return */ @Override public Person getPerson(Integer id){ String key = person_cache_key + id; Person person = cacheService.getCache(key,Person.class); if(person!=null){ log.info("命中缓存,结果为:{}" ,person.toString()); }else{ //模拟数据库查询 person = new Person(); person.setId(id); person.setName("laowan"); person.setAge(25); if(person!=null){ log.info("未命中缓存,从数据库查询结果为:{}",person.toString()); cacheService.setCache(key,person); } } return person; } /** * 通过传递回调函数,减少重复的查询缓存——》判空——》然后查询数据库——》判空——》更新缓存 编码操作 * @param id * @return */ @Override public Person getPersonWithCallBack(Integer id){ String key = person_cache_key + id; Person person = cacheService.getCache(key, Person.class, new CacheCallBack<Person, String>() { @Override public Person execute(String input) { //模拟数据库查询 Person personDB = new Person(); personDB.setId(id); personDB.setName("laowan"); personDB.setAge(25); return personDB; } }); return person; }
单元测试:
@SpringBootTest @Slf4j class RedisApplicationTests { @Autowired PersonService personService; @Test void getTest() { Person person = personService.get(102); log.info("查询结果为:" + person.toString()); } @Test void getPersonTest() { Person person = personService.getPerson(102); log.info("查询结果为:" + person.toString()); } @Test void getPersonWithClosureTest() { Person person = personService.getPersonWithCallBack(104); log.info("查询结果为:" + person.toString()); } }
总结
1、操作redis缓存的常见2种方式:Spring Cache注解方式和redisTemplate编码方式。
2、两种缓存操作方式的异常处理,实现业务操作和缓存解耦:缓存查询失败,会继续查询数据库执行业务。
3、redis缓存的序列化控制:默认使用java自带的序列化机制,存储的对象需要实现Serializable接口;这里我们配置的是采用Jackson序列化,所以不需要实现Serializable接口。
4、通过封装回调方法CacheCallBack,减少了重复的“查询缓存——》判空——》查询数据库——》判空——》更新缓存 ”的硬编码操作