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即可</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,減少了重複的“查詢緩存——》判空——》查詢數據庫——》判空——》更新緩存 ”的硬編碼操作

實戰代碼Git地址:https://github.com/StarlightWANLI/redis.git

更多精彩,關注我吧。
圖注:跟着老萬學java

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章