springboot中redis的使用
作者:mmseoamin日期:2024-03-20

springboot中redis的使用

      • springboot中redis的使用
        • 一、springboot整合redis
          • 1.1 基本使用
          • 1.2 使用缓存注解
          • 二、redis做缓存过程中的难点以及解决方案
            • 1、问题
            • 2、问题的思考
            • 3、思考的过程以及解决方案
            • 4、我还是希望站在巨人的肩膀上
            • 5、在springboot中的使用

              springboot中redis的使用

              一、springboot整合redis
              1.1 基本使用

              1、导入依赖

              
                  org.springframework.boot
                  spring-boot-starter-data-redis
              
              

              2、添加redis配置

              spring.redis.password=123456
              

              此时已经可以在代码中使用redisTemplate对象了。例如:

              @Service
              public class DepartmentServiceImpl implements DepartmentService {
                  @Resource
                  private DepartmentDAO departmentDAO;
                  @Resource
                  private RedisTemplate redisTemplate;
                  @Override
                  public List findAll() {
                      // json处理的对象
                      ObjectMapper mapper = new ObjectMapper();
                      // 1、查询缓存
                      // 定义一个key(唯一、尽量不会被推导)
                      // 使用一个唯一的名字 + 参数值 + md5
                      String key = MD5Utils.md5("findAllDepartments");
                      // 得到redisTemplate中的操作对象
                      BoundValueOperations boundValueOps = redisTemplate.boundValueOps(key);
                      // 得到缓存中的值
                      String value = boundValueOps.get();
                      System.out.println("value===" + value);
                      // 2、判断缓存是否有该数据
                      if (value != null){
                          // 3、如果有则返回该数据
                          System.out.println("查询缓存");
                          List list = null;
                          try {
                              list = mapper.readValue(value, new TypeReference>(){});
                          }catch (Exception e){
                              e.printStackTrace();
                          }
                          return list;
                      }else{
                          // 4、如果没有,则去数据库中查询,并保存到缓存中
                          System.out.println("查询数据库");
                          List departments = departmentDAO.findAll();
                          if (departments != null && departments.size() > 0){
                              // 转换成json格式
                              try {
                                  String string = mapper.writeValueAsString(departments);
                                  // 保存到缓存中
                                  boundValueOps.set(string);
                              }catch (Exception e){
                                  e.printStackTrace();
                              }
                          }
                          return departments;
                      }
                  }
              }
              

              但是,查看redis中存储的数据发现,key和value前面都有一段看似乱码的数据,而且中文也进行编码。为了解决该问题,需要参考前面ssm中整合redis的方案,对redis的key和value进行配置。

              3、添加redis的设置

              @Configuration
              public class MyRedisConfig {
                  @Bean
                  public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
                      StringRedisTemplate template = new StringRedisTemplate(factory);
                      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);
                      template.setValueSerializer(jackson2JsonRedisSerializer);
                      template.afterPropertiesSet();
                      return template;
                  }
              }
              

              再使用前面的service代码进行测试,发现上述问题已解决。

              1.2 使用缓存注解

              直接编写redis的逻辑,相对代码比较复杂,而且逻辑基本固定,可以使用缓存注解来简化该流程。

              spring框架提供一套缓存注解:

              @Cacheable:表示当前查询结果会使用缓存。(如果有缓存,直接使用,否则进行数据库查询并放入缓存)
              @CachePut:表示当前操作的数据的结果会更新到缓存中。(如果修改的数据在缓存存在,会同时修改缓存中的数据)
              @CacheEvict:表示如果删除的数据在缓存中,也会对应的删除。
              

              1、添加缓存注解的配置

              @Configuration // 配置文件
              @EnableCaching // 允许缓存使用,开启缓存的配置,需要继承CachingConfigurerSupport类
              public class MyRedisConfig extends CachingConfigurerSupport {
                  @Bean
                  public CacheManager cacheManager(RedisConnectionFactory factory){
                      RedisSerializer keyRedisSerializer = new StringRedisSerializer(); // redis的key序列化方式
                      Jackson2JsonRedisSerializer valueRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); // redis的value的序列化
                      //解决查询缓存转换异常的问题
                      ObjectMapper om = new ObjectMapper();
                      om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
                      om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
                      valueRedisSerializer.setObjectMapper(om);
                      //配置序列化(解决乱码的问题)
                      RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                              .entryTtl(Duration.ZERO) // 默认生存时间
                              .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keyRedisSerializer))
                              .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueRedisSerializer))
                              .disableCachingNullValues();
                      //缓存配置map
                      Map cacheConfigurationMap=new HashMap<>();
                      //自定义缓存名,后面使用的@Cacheable的CacheName
                      cacheConfigurationMap.put("myRedis",config);
              //        cacheConfigurationMap.put("default",config);
                      RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                              .cacheDefaults(config)
                              .withInitialCacheConfigurations(cacheConfigurationMap)
                              .build();
                      return cacheManager;
                  }
                  @Bean
                  public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
                      StringRedisTemplate template = new StringRedisTemplate(factory);
                      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);
                      template.setValueSerializer(jackson2JsonRedisSerializer);
                      template.afterPropertiesSet();
                      return template;
                  }
              }
              

              4、使用

              @Service
              public class EmployeeServiceImpl implements EmployeeService{
                  @Resource
                  private EmployeeDAO employeeDAO;
                  @Override
                  public PageInfo findAll(Integer page) {
                      PageHelper.startPage(page, 10);
                      List list = employeeDAO.findAll();
                      PageInfo info = new PageInfo(list);
                      return info;
                  }
                  @Override
                  // @Cacheable将查询结果进行缓存
                  // key的定义:
                  // T()里面写一个类型,表示当作一个类处理,后面调用该类方法
                  //  如果是字符串,需要使用单引号
                  // 如果要使用方法中的参数,需要在变量名前面加上#
                  @Cacheable(cacheNames = "myRedis", key = "T(com.qf.sbems.utils.MD5Utils).md5('EmployeeService_findById' + #id)")
                  public Employee findById(Integer id) {
                      System.out.println("进入方法,去数据库查询");
                      return employeeDAO.findById(id);
                  }
                  @Override
                  public void save(Employee employee) {
                        employeeDAO.save(employee);
                  }
                  @Override
                  // @CachePut将返回值作为数据替换掉原来的缓存数据
                  @CachePut(cacheNames = "myRedis", key = "T(com.qf.sbems.utils.MD5Utils).md5('EmployeeService_findById' + #employee.id)")
                  public Employee update(Employee employee) {
                      employee.setUpdatetime(new Date());
                      employeeDAO.update(employee);
                      return employee;
                  }
                  @Override
                  // @CacheEvict删除缓存
                  @CacheEvict(cacheNames = "myRedis", key = "T(com.qf.sbems.utils.MD5Utils).md5('EmployeeService_findById' + #id)")
                  public void delete(Integer id) {
                        employeeDAO.delete(id);
                  }
              }
              
              二、redis做缓存过程中的难点以及解决方案
              1、问题

              为了让缓存使用方式通用,选择了使用注解的方案,即@Cacheable, @CachePut等。

              思考一下缓存中需要处理的问题,在使用注解过程中是否得以解决。

              问题如下:缓存击穿,缓存穿透,大量的key过期导致的雪崩问题。

              2、问题的思考

              1、缓存击穿问题的原因是同一时间大量的请求访问,导致访问到数据库,可以使用缓存预热来解决。或者对请求使用削峰。【已解决】

              2、穿透问题的原因是无法缓存null值,导致每次都访问数据库。解决问题的方案需要缓存空值。而且需要设置一个较短的过期时间,相较于非空数据的时间要短很多。【未解决】

              3、大量key过期导致的缓存雪崩问题。原因主要是由于缓存时间设置一致。解决方案是设置不同的时间。【未解决】

              3、思考的过程以及解决方案

              1、在@Cache注解中使用unless属性。

              @Cacheable(value=“XXX”,key=“#info”,unless = “#result==null”)

              上面的办法能够解决空值出现的异常问题,但是又出现了新的问题------缓存穿透,每一次都会访问数据库。

              2、使用自定义的AOP方案,切入到缓存存储的时机,自定义过程。

              【方式比较麻烦,而且中间还会产生aop失效问题https://my.oschina.net/guangshan/blog/1807721】

              3、自己使用redisTemplet的opsForValue().set()方法。

              可能需要写很多次代码,也比较麻烦,而且没有充分利用到@Cacheable注解。

              4、我还是希望站在巨人的肩膀上

              找到了一个分布式落地方案:layering-cache

              https://my.oschina.net/xiaolyuh/blog/2245782

              5、在springboot中的使用

              1、导入启动器

              
                  org.springframework.boot
                  spring-boot-starter-data-redis
              
              
              
                  com.github.xiaolyuh
                  layering-cache-starter
                  3.1.8
              
              
                  org.springframework
                  spring-aspects
              
              
              
                  com.esotericsoftware.kryo
                  kryo
                  2.21
              
              
              
                  com.alibaba
                  fastjson
                  1.2.47
              
              

              2、在application.properties中添加配置

              #layering-cache 配置
              layering-cache.stats=true
              # 缓存命名空间,如果不配置取 "spring.application.name"
              layering-cache.namespace=layering-cache-web
              # redis单机
              layering-cache.redis.database=0
              layering-cache.redis.host=127.0.0.1
              layering-cache.redis.port=6379
              layering-cache.redis.password=
              # redis集群
              #layering-cache.redis.password=
              #layering-cache.redis.cluster=127.0.0.1:6379,127.0.0.1:6378
              

              3、配置类中添加注解@EnableLayeringCache启用layering-cache

              @SpringBootApplication
              @EnableLayeringCache // 启用多级缓存框架
              public class LayeringCacheStartDemoApplication {
                  public static void main(String[] args) {
                      SpringApplication.run(LayeringCacheStartDemoApplication.class, args);
                  }
              }
              

              4、使用注解配置

              参考:https://my.oschina.net/xiaolyuh/blog/2245782

              不需要使用MyRedisConfig配置类。

              import com.github.xiaolyuh.annotation.*;
              @Service
              public class EmployeeServiceImpl implements EmployeeService {
                  @Resource
                  private EmployeeDAO employeeDAO;
                  @Override
                  public List findAll() {
                      return employeeDAO.findAll();
                  }
                  @Override
                  @Cacheable(value = "employee:info", key = "T(com.qf.sbems.utils.MD5Utils).md5('EmployeeService_findById' + #id)"
                          , depict = "用户信息缓存", enableFirstCache = true,
                          firstCache = @FirstCache(expireTime = 400, timeUnit = TimeUnit.SECONDS),
                          secondaryCache = @SecondaryCache(expireTime = 1000, preloadTime = 200,
                                  forceRefresh = true, timeUnit = TimeUnit.SECONDS, isAllowNullValue = true, magnification = 100))
                  public Employee findById(Integer id) {
                      System.out.println("查询数据库");
                      return employeeDAO.findById(id);
                  }
                  @Override
                  @CachePut(value = "employee:info", key = "T(com.qf.sbems.utils.MD5Utils).md5('EmployeeService_findById' + #id)"
                          , depict = "用户信息缓存", enableFirstCache = true,
                          firstCache = @FirstCache(expireTime = 400, timeUnit = TimeUnit.SECONDS),
                          secondaryCache = @SecondaryCache(expireTime = 1000, preloadTime = 200,
                                  forceRefresh = true, timeUnit = TimeUnit.SECONDS, isAllowNullValue = true, magnification = 100))
                  public Employee update(Employee employee) {
                      employee.setUpdatetime(new Date());
                      employeeDAO.update(employee);
                      return findById(employee.getId());
                  }
                  @CacheEvict(value = "employee:info", key = "T(com.qf.sbems.utils.MD5Utils).md5('EmployeeService_findById' + #id)")
                  public void delete(Integer id) {
              //        employeeDAO.delete(id);
                  }
              }