redis缓存解耦详解

redis是现在最主流的缓存利器,但是你的项目中,缓存真正做到了解耦了吗?

背景

最近,项目中遇到一个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即可
*/
@Bean@Overridepublic KeyGenerator keyGenerator() {return new KeyGenerator() {@Overridepublic 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();}};}@Beanpublic 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);//配置redisTemplateRedisTemplate<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;}//缓存管理器@Beanpublic 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@Beanpublic CacheErrorHandler errorHandler(){//CacheErrorHandler cacheErrorHandler = new SimpleCacheErrorHandler();CacheErrorHandler cacheErrorHandler = new CacheErrorHandler() {private Logger logger = LoggerFactory.getLogger(CacheErrorHandler.class);@Overridepublic void handleCacheGetError(RuntimeException e, Cache cache, Object o) {logger.error("redis 异常:key=[{}]",o,e);}@Overridepublic void handleCachePutError(RuntimeException e, Cache cache, Object o, Object o1) {logger.error("redis 异常:key=[{}]",o,e);}@Overridepublic void handleCacheEvictError(RuntimeException e, Cache cache, Object o) {logger.error("redis 异常:key=[{}]",o,e);}@Overridepublic 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 * @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 {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Overridepublic boolean setCache(String key, Object value) {try {redisTemplate.opsForValue().set(key, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}@Overridepublic 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;}}@Overridepublic <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;}@Overridepublic <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;}@Overridepublic <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;}@Overridepublic 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:";@AutowiredCacheService cacheService;/*** 硬编码实现查询缓存——》判空——》然后查询数据库——》判空——》更新缓存* @param id* @return*/@Overridepublic 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*/@Overridepublic Person getPersonWithCallBack(Integer id){String key = person_cache_key + id;Person person = cacheService.getCache(key, Person.class, new CacheCallBack<Person, String>() {@Overridepublic 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 {@AutowiredPersonService personService;@Testvoid getTest() {Person person = personService.get(102);log.info("查询结果为:" + person.toString());}@Testvoid getPersonTest() {Person person = personService.getPerson(102);log.info("查询结果为:" + person.toString());}@Testvoid 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,减少了重复的“查询缓存——》判空——》查询数据库——》判空——》更新缓存 ”的硬编码操作

实战代码Git地址:https://github.com/StarlightWANLI/redis.git

更多精彩,关注我吧。
图注:跟着老万学java


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部