redis是如今最主流的緩存利器,但你的項目中,緩存真正作到解耦了嗎?java
背景
最近,項目中遇到一個redis緩存使用的問題,當redis鏈接不上時,直接致使業務異常。redis不是作爲緩存使用嗎?當緩存中查詢不到,不是應該主動從數據庫加載嗎?git
最後發現是利用RedisTemplate操做緩存,沒有進行異常捕捉處理,致使異常拋出影響到業務的正常執行。github
那麼,你的項目中,緩存操做真的作到了解耦嗎?web
緩存原理
緩存的使用
目前redis緩存主要有2種使用方式:
方式一:結合Spring Cache使用,經過@Cacheable、@CachePut 、@CacheEvict這3個緩存註解實現緩存控制
方式二:經過RedisTemplate模板方法經過編碼控制緩存redis
代碼實戰
依賴包:spring
<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配置類緩存
/**
*
* 一、@EnableCaching是爲了開啓spring cache的緩存註解功能
* 二、繼承CachingConfigurerSupport是爲了配置spring cache的主鍵生成策略keyGenerator和cacheManager
* 三、配置RedisTemplate的序列化機制Jackson
* 四、配置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());
}
}
總結
一、操做redis緩存的常見2種方式:Spring Cache註解方式和redisTemplate編碼方式。
二、兩種緩存操做方式的異常處理,實現業務操做和緩存解耦:緩存查詢失敗,會繼續查詢數據庫執行業務。
三、redis緩存的序列化控制:默認使用java自帶的序列化機制,存儲的對象實現須要實現Serializable接口,這裏咱們配置的是採用Jackson序列化。
四、經過封裝回調方法CacheCallBack,減小了重複的「查詢緩存——》判空——》查詢數據庫——》判空——》更新緩存 」的硬編碼操做
實戰代碼Git地址:https://github.com/StarlightWANLI/redis.git
更多精彩,關注我吧。
本文分享自微信公衆號 - 跟着老萬學java(douzhe_2019)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。