redis缓存解耦详解

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: redis是现在最主流的缓存利器,但是你的项目中,缓存真正做到了解耦了吗?

背景


最近,项目中遇到一个redis缓存使用的问题,当redis连接不上时,直接导致业务异常。redis不是做为缓存使用吗?当缓存中查询不到,不是应该主动从数据库加载吗?


最后发现是利用RedisTemplate操作缓存,没有进行异常捕捉处理,导致异常抛出影响到业务的正常执行。


那么,你的项目中,缓存操作真的做到了解耦吗?


缓存原理


38.png


缓存的使用


目前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

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
16天前
|
存储 缓存 NoSQL
解决Redis缓存数据类型丢失问题
解决Redis缓存数据类型丢失问题
158 85
|
13天前
|
缓存 监控 NoSQL
Redis经典问题:缓存穿透
本文详细探讨了分布式系统和缓存应用中的经典问题——缓存穿透。缓存穿透是指用户请求的数据在缓存和数据库中都不存在,导致大量请求直接落到数据库上,可能引发数据库崩溃或性能下降。文章介绍了几种有效的解决方案,包括接口层增加校验、缓存空值、使用布隆过滤器、优化数据库查询以及加强监控报警机制。通过这些方法,可以有效缓解缓存穿透对系统的影响,提升系统的稳定性和性能。
|
2月前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
2月前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构
|
2月前
|
缓存 NoSQL Redis
Redis 缓存使用的实践
《Redis缓存最佳实践指南》涵盖缓存更新策略、缓存击穿防护、大key处理和性能优化。包括Cache Aside Pattern、Write Through、分布式锁、大key拆分和批量操作等技术,帮助你在项目中高效使用Redis缓存。
340 22
|
2月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
46 5
|
2月前
|
缓存 NoSQL 中间件
redis高并发缓存中间件总结!
本文档详细介绍了高并发缓存中间件Redis的原理、高级操作及其在电商架构中的应用。通过阿里云的角度,分析了Redis与架构的关系,并展示了无Redis和使用Redis缓存的架构图。文档还涵盖了Redis的基本特性、应用场景、安装部署步骤、配置文件详解、启动和关闭方法、systemctl管理脚本的生成以及日志警告处理等内容。适合初学者和有一定经验的技术人员参考学习。
247 7
|
存储 缓存 NoSQL
Spring Boot2.5 实战 MongoDB 与高并发 Redis 缓存|学习笔记
快速学习 Spring Boot2.5 实战 MongoDB 与高并发 Redis 缓存
Spring Boot2.5 实战 MongoDB 与高并发 Redis 缓存|学习笔记
|
缓存 NoSQL 安全
6.0Spring Boot 2.0实战 Redis 分布式缓存6.0|学习笔记
快速学习6.0Spring Boot 2.0实战 Redis 分布式缓存6.0。
342 0
6.0Spring Boot 2.0实战 Redis 分布式缓存6.0|学习笔记
|
缓存 NoSQL Redis
首页数据显示-添加 redis 缓存(3)| 学习笔记
快速学习 首页数据显示-添加 redis 缓存(3)
157 0
首页数据显示-添加 redis 缓存(3)| 学习笔记