springboot+Redis缓存讲解+案例+原理+整合(简单详细!)

1 篇文章 0 订阅
1 篇文章 0 订阅

一、缓存(Cache)介绍: 

保存一些临时性的数据。常用的方法有两种JSR107规范和Spring自己定义的规范

 

  • JSP107的java规范:  麻烦,这个规范用的比较少,一般使用的都是Spring自己的缓存抽象。
    java的cacheing定义了5个接口,分别是CacheProvider, CacheManager, Cache, Entry 和 Expiry。
 
        1、 CacheingProvider可以管理(创建、配置、获取、管理和控制)多个CacheManager,并且一个程序在运行期间可以访问多个CacheProvider。
        2、 CacheManager可以管理(创建、配置、获取、管理和控制)多个唯一的key的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。CacheManager可以管理不同的缓存(Redis的缓存、Ehcache的缓存等,下面Spring缓存中有写)。
        3、 Cache是一个类似Map的数据结构并将key作为临时索引加以保存,一个Cache仅被一个CacheManager所拥有。每个Cache分别管理不同业务的缓存,如下图的3,左边管理员工的缓存、中间管理部门的缓存、右边管理商品的等。Cache和CacheManager相当于数据库连接池(池中获取连接)。
        4、 Entry是Cache的k-v键值对。
        5、 Expiry定义Cache的有效期,超时就过期,过期就不能访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。
 
             
    
  • Spring自己的缓存抽象: 简化了缓存开发技术
    Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用JCache(JSR-107)注解简化我们开发;
 
    1、 Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;
 
    2、 Cache接口下Spring提供了各种xxxCache的实现,如RedisCache,EhCacheCache , ConcurrentMapCache等;每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果返回给用户。下次直接从缓存中获取。
 
    3、 使用Spring缓存抽象时我们需要关注以下两点;
            第一点:确定方法需要被缓存以及他们的缓存策略 
            第二点:从缓存中读取之前缓存存储的数据
 
  • 注解开发:  
             
    
 
    

二、@Cacheable

使用的是Spring抽象缓存技术
    
  • pom
             
 
 
  • 搭建三层架构的环境: 

              表-实体类:这里使用的是easyCode,或者使用mp的自动生成

            注意:  1、 要给三层加载到容器里面,@Service之类的注解。  注解版:Dao层写@Mapper就行,或者配置类上加@MapperScan
                   2、 mysql环境配置
                   3、 如果不写mapper.xml文件,就使用注解版编写sql语句,写在Dao/mapper的接口层上 @Select("xxx")。    
                   4、 如果起初还是启动不起来,就在配置类上使用@ComponentScan()重新扫描一遍所有的三层注解的类,加载到IOC中
                   5、 开启驼峰命名,不然id不能获取到。(这个id是外键,不是主键) mybatis.configuration.map-underscore-to-camel-case: true
    
  • 开启注解类的缓存:  在配置类上添加 @EnableCaching 
 
  • 开启方法缓存:  @Cacheable 标注在方法上(Service层的方法上)
        默认是没有缓存的,所以每次进行CRUD操作都会直接去查询数据库。 
            * 开启日志帮助理解(可省):
                logging.level.包名: debug。  
                # 没有缓存只要执行查询就会执行sql语句,然后显示到控制台,如果有了缓存就不会查询数据库,所以就不再显示到控制台
                     
 
            * 添加@Cacheable注解到要缓存方法上:下面的是该注解的属性:
 
  • ====> @Cacheable属性 <====

            cacheNames/value:缓存组件的名字,该组件内可以包含多个缓存(就是将一个缓存分到哪组里面,这个组是缓存的父亲),
                 该缓存可以通过该属性放在多个父亲里面,这个属性是一个数组形式。
 
            key: 指定缓存的名字,默认使用的是方法参数的值,key可以使用SpEl表达式。
                 如:
                     将该方法的第一个参数当做key: key="#id" 或 key="#root.args[0]"
                     将key设置为缓存父亲的名字: key="#root.caches[0].name" 就能获取cacheNames或value中的第一个名字
                     将key设置为方法名拼接参数id: 如:getDept[1]:  key = "#root.methodName+'['+#id+']'"
                     将key设置为参数,第一个例子使用的#id就是这个。   #id = #a0 = #p0 = root.args[0]
                 
 
 
            keyGenerator: key的生成器,可以自己指定key的生成器(自己写一个keyGenerator放在IOC)。该属性的值为自定义的Bean的id
                    该属性和key属性二选一使用。
                      
 
 
            cacheManager: 设置缓存管理器。
                    如: 在EhCache的缓存服务器(类似于Redis)中的project(父亲)中有loginCache缓存,
                         在Redis缓存服务器里面也有该缓存,那么就可以使用cacheManager指定使用哪个缓存服务器里面的缓存。
 
            cacheResolver: 缓存解析器。
                    该属性和cacheManager二选一使用。
 
            condition: 满足一定条件才进行缓存,当条件为true才缓存
                    如: condition = "#id>0"   //当取到的参数的id>0的时候才进行缓存。或者: #a0>0,可以写复合条件
 
            unless: 满足条件就不缓存,当条件为true不缓存!和condition相反。  注意: 既满足condition又满足unless的数据不缓存
                        unless可以获取方法结果进行判断,在key属性的参数里面有个#result就是获取方法返回值的和unless组合使用。
                    如: unless = "#result == null"  //返回结果为空就不缓存。
                         unless = "#a0 == 2"  //如果第一个参数为0就不缓存
 
            sync: 缓存是否使用异步模式进行缓存。默认为false使用的是同步模式,如果改为异步(sync=true)就不支持unless属性
 
  • 代码: 
     //主类:  @EnableCaching
            @SpringBootApplication
            @EnableCaching      //开启缓存
            public class Springboot11CacheApplication {
     //Service层: @Cacheable
            @Cacheable(value = "dept",key = "#id")  //开启方法缓存
            public Department queryById(Integer id) {
                return this.departmentMapper.queryById(id);
            }

 

    

三、缓存原理

缓存如何工作的?
 
  • 找到缓存的自动配置类:    CacheAutoConfiguration
@EnableConfigurationProperties({CacheProperties.class})
@AutoConfigureAfter({CouchbaseDataAutoConfiguration.class, HazelcastAutoConfiguration.class, HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class})
@Import({CacheAutoConfiguration.CacheConfigurationImportSelector.class,CacheAutoConfiguration.CacheManagerEntityManagerFactoryDependsOnPostProcessor.class})
public class CacheAutoConfiguration {

* // 主要就是看Import的类是一个xxxSelector类,该类会自动给springboot自动导入cache相关的组件。点进去CacheConfigurationImportSelector



static class CacheConfigurationImportSelector implements ImportSelector {
    CacheConfigurationImportSelector() {    //无参构造器
    }

    public String[] selectImports(AnnotationMetadata importingClassMetadata) {   //主要就是这个方法,会自动导入组件
        CacheType[] types = CacheType.values();
        String[] imports = new String[types.length];

        for(int i = 0; i < types.length; ++i) {
            imports[i] = CacheConfigurations.getConfigurationClass(types[i]);  //这个imports就是接收所有的组件的一个参数
        }

        return imports;            //所以直接在这里打断点会直接获取到所有的组件
    }
}

* // 如果有缓存,一定会进入这个方法,然后断点会获取下面的数据: 

                     
                        0 = "org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration"
                        1 = "org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration"
                        2 = "org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration"
                        3 = "org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration"
                        4 = "org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration"
                        5 = "org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration"
                        6 = "org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration"
                        7 = "org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration"
                        8 = "org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration"【默认】
                        9 = "org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration"
 
 
* // 但是这些组件并不是全部都会生效的,只会有一部分组件会生效。springboot只会将cache所有会用到的一些类加载到IOC,而这么多组件你并不是都会用到,尽管导入了这么多的组件。但是其中的一些组件并没有全的包,所以只是导入了并不会生效。
* // 所以在配置文件里面加上:  debug: true 可以查看生效的组件(可以分析类图,可以一个一个进入上面的包中查看jar是否全的,可以加上debug = true)
 
    SimpleCacheConfiguration matched:    //只有这一个生效!
    CaffeineCacheConfiguration: 
       Did not match:
    CouchbaseCacheConfiguration: 
       Did not match:
    EhCacheCacheConfiguration:
       Did not match:
    GenericCacheConfiguration:
       Did not match:
    HazelcastCacheConfiguration:
       Did not match:
    InfinispanCacheConfiguration:
       Did not match:
    JCacheCacheConfiguration:
       Did not match:
    NoOpCacheConfiguration:
       Did not match:
    RedisCacheConfiguration:
       Did not match:
 
 
* // 那么 SimpleCacheConfiguration 给容器做了什么?  点进该类里面去只注册了一个cacheManager组件   Concurrent:并发)
 
 
                
 
 
* // 点进ConcurrentMapCacheManager 类里面,看看这个类是做什么的: 如果要缓存数据就会先获取getCache,参数传递@Cacheable的cacheNames/value值
 
                
 
 
* // 点进getCache()方法: 如果没有缓存就新建一个缓存,所以第一次查询数据库之后就会新建一个缓存,之后查询不需要再查询数据库了。
 
public Cache getCache(String name) {   //这个参数name就会传进来使用@Cacheable的value或cacheNames属性指定的值。
    Cache cache = (Cache)this.cacheMap.get(name);    
    if (cache == null && this.dynamic) {
        synchronized(this.cacheMap) {    //双重检查
            cache = (Cache)this.cacheMap.get(name);
            if (cache == null) {    // 如果在CacheMap中(就是一个map)获取不到name的值,就新建一个缓存放到map里面(就是新建了一个Entry)
                cache = this.createConcurrentMapCache(name);   //但是,这是如何新建的缓存呢?
                this.cacheMap.put(name, cache);
            }
        }
    }

*// 为了了解如何建立缓存,点进 createConcurrentMapCache

protected Cache createConcurrentMapCache(String name) {
   SerializationDelegate actualSerialization = (isStoreByValue() ? this.serialization : null);
   return new ConcurrentMapCache(name, new ConcurrentHashMap<>(256), isAllowNullValues(), actualSerialization);  //直接返回ConcurrentMapCache
}

* // 点进ConcurrentMapCache: 有一些操作缓存的方法,如查询一些缓存的数据,执行lookup()方法。 

        XxManager管理XxCache(RedisCacheManager管理RedisCache,类比这个类。因此如果想使用RedisCache就直接覆盖Manager即可!)
 
                
 
 
* // 点进lookup()方法: 会去store里面获取key。 因此了解到 ConcurrentMapCache 的作用是将数据保存到 ConcurrentMap 里面
 
protected Object lookup(Object key) {  //先去查询ConcurrentMapCache的map中是否有这个key的缓存,没有就会执行put方法
   return this.store.get(key);   //this是ConcurrentMapCache; store是ConcurrentMap类型的:缓存数据都是放在Map里面的。ConcurrentMap<Object, Object> store;
}

就理解一句话: 照着最上面的图理解!

    CacheManager管理着Cache,Cache中的数据是存在Map里面的。一个Cache中要先指定了父亲之后,就可以存储多个子缓存。
 
 
总结:
    @Cachable标注的方法在执行之前先检查(lookup)缓存中是否有这个数据,默认按照参数的值为key去查找缓存,如果没有就将结果放入缓存(put)。以后再调用就会直接从缓存中获取。
    key的生成策略:默认使用的是keyGenerator生成的,默认使用的是SimpleKeyGenerator生成key。
    
    

四、@CahcePut

更新缓存(Service层的方法上)
    既调用方法,又更新缓存。  修改了数据库的某个已经换粗你的数据后,缓存的数据也会更新。
     
  • ====> @CachePut属性 <====
        和 @Cacheable 的属性一样
 
  •  @CachePUt的运行时机:
        1 先调用目标方法
        2 将目标方法的结果缓存起来(就是在目标方法执行之后执行该注解,所以可以使用#result;而@Cacheable是在目标方法执行之前执行注解)
    
  • 代码:
 
//Service层:
    @CachePut("dept")
    public Department update(Department department) {
        this.departmentDao.update(department);

        return this.queryById(department.getId());
    }
//Controller层:

    @GetMapping("/dept")
    public Department update(Department department){
        departmentService.update(department);

        return department;
    }
  • 为什么更新缓存的还是显示更新前的旧值?
因为cache中保存数据是通过k-v保存的,而key默认的是参数,而
        查询缓存的时候的key是 queryById(Integer id) 参数id,
        更新缓存的时候的key是 update(Department department) 参数department
    两个key不一样,所以更新也会将一个新的key为department的数据存入缓存,而不会替换查询时的key为id的缓存。
 
  • 更新如何获取旧的id?
    因为@Cacheable和@CachePut注解中的属性都是一样的,所以他们的用法也都是一样的。
    所以,使用 key = #表.id 或者 key = #result.id 就可以获取到id。
        #result只能在@CachePut中使用。上面解释过了
 
@CachePut(value = "dept",key = "#result.id")  // 操作的表示department,所以可以写 #department.id
public Department update(Department department) {
    this.departmentDao.update(department);
    return this.queryById(department.getId());
}

    

五、@CacheEvict

删除缓存
    
  • ====> @CacheEvict 属性 <====:
        其他属性和@Cacheable完全一样,只是多了下面两个属性:
 
        allEntries: 删除缓存中的所有数据。默认false。
            @CacheEvict(allEntries = true) : 删除所有缓存
 
        beforeInvocation: 是否在方法执行之前清除缓存。默认false为执行方法之后。(可以使用事务!)
                因为方法可能会出错,当方法出错后默认就不会清楚缓存,如果在方法执行之前清除缓存,方法出错就无关了。
            @CacheEvict(beforeInvocation= true)
 
  • 如果要指定删除那个key,通过key指定
        @CacheEvict(value="dept",key="#id")   //删除dept的cache中的key为id的缓存
    
  • 删除之后再查询就会去查数据库了,重新执行@Cacheable注解
    
  • 代码 1: 删除指定的缓存数据
//Controller层:
    @GetMapping("/delDeptpt/{id}")
    public String delete(@PathVariable("id") Integer id){
        departmentService.deleteById(id);
        return "删除成功";
    }

//Service层:
    @CacheEvict(value = "dept",key = "#id")   // 删除dept中key为id的缓存。
    public boolean deleteById(Integer id) {
        return this.departmentDao.deleteById(id) > 0;
    }

 

  • 代码 2: 删除全部缓存数据
//Service层:
    @CacheEvict(allEntries = true)   // 删除dept中key为id的缓存。
    public boolean deleteById(Integer id) {
        return this.departmentDao.deleteById(id) > 0;
    }

   

六、@Caching和@CacheConfig

 
  • @Caching: 指定复杂的缓存规则
 
  • ====> @Caching属性 <====
    @Caching是集合了三个主要缓存注解。 并且每个注解都是以数组方式作为属性的!!
            
 
  • 代码使用:
        
 
    
  • @CacheConfig 指定公共的缓存规则。放在类上面
             
    
 
  • ====> @CacheConfig属性 <====: 下面的四个属性都可以用于公共缓存的配置上。
            cacheNames: 指定缓存的父亲cache
        
            keyGenerator:key的生成策略
 
            cacheManager:缓存管理器
 
            cacheResolver: 缓存解析器(一般和cacheManager二选一)
 
         

七、整合Redis缓存: 

通过Java操作Redis:
  • 1、导入jar包:(Web和Spring Data Redis)
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

 

    说明: 由于更新到springboot2.x之后,就已经不再使用Jedis操作Redis,而是改版为Lettuce
 
为什么改版?
    Jedis:采用的是直连,就是直接去连接数据库,如果有多个线程操作的话就是不安全的,想要避免不安全,就需要使用Jedis pool连接池。
    Lettuce:底层采用的Netty,异步请求,实例可以在多个线程中共享,所以不存在线程不安全的情况,可以减少线程数量,就不需要再开连接池
 
  • 在springboot-autoconfigure/META-INF/spring.factories 中查找redis自动配置了AutoConfiguration源码:
@Bean
@ConditionalOnMissingBean(name = {"redisTemplate"}) //我们可以自己定义一个替换默认的,这就能够让springboot操作redis不再乱码!!
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
    //默认使用的 RedisTemplate 没有过多的配置,Redis中的对象保存都是需要序列化的,而这里没有序列化等操作
    //而且 RedisTemplate 的量个泛型都是Object Object类型,所以我们使用的时候要强制转换,因为,Redis使用的都是 String Object
    RedisTemplate<Object, Object> template = new RedisTemplate();  //RedisTemplate 是写项目时整合需要使用到的bean。
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}

@Bean
@ConditionalOnMissingBean
//因为String类型是Redis中最常用的类型,所以单独提取出来了
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
    StringRedisTemplate template = new StringRedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}

 

  • 2、配置连接
    #如果只配置url和port,那么可以不配置,因为这两个属性都是默认的localhost和6379
 
#配置Redis连接,他的属性去RedisAutoConfiguration中查看
spring:
    redis:   
        url: 127.0.0.1 # 默认就是localhost,后面使用的都是 Lettuce ,因为默认springboot2.x的Jedis的源码不生效(源码的jar包导入不全)
        port: 6379  #默认是6379。配置的是 spring.redis.lettuce.xxx,不是spring.redis.jedis.xxx
                         
 
  • 3、测试连接: RedisTemplate!
package com.springboot.redis02springboot;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;

@SpringBootTest
class Redis02SpringbootApplicationTests {

    //注入RedisAutoConfiguration中的Bean --- RedisTemplate
    @Autowired
    private RedisTemplate redisTemplate;
            // RedisTemplate 的方法和 Redis 的常用的指令操作是一样的,事务和基本的CRUD等都是使用RedisTemplate

            // opsForValue     => String
            // opsForList      => list 如: redisTemplate.opsForList().leftPush("name","zs"); 集合左边添加数据
            // opsForSet       => Set
            // opsForHash      => Hash
            // opsForzset      => zset
            // opsForGeo       => Geospatial
            // keys            => 正则匹配,查询key
            // multi           => 开启事务
            // discard         => 事务的删除操作
            // exec            => 执行事务
            // 常用的操作。事务、CRUD等都是RedisTemplate


            // 获取 Redis 连接对象,一般很少用。有flushdb、flushall等操作,也可以操作Redis。
            // RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
            // connection.flushAll();
            // connection.flushDb();

    @Test
    void contextLoads() {

        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        //因为是创建的共享连接(我理解为池redis pool),所以不能使用select选择数据库。
        connection.flushDb();
        connection.close();

        redisTemplate.opsForValue().set("name","zs"); //java操作redis存数据会乱码,但是idea控制台正常显示,后面讲如何修改。
        System.out.println(redisTemplate.opsForValue().get("name"));
    }

}

 

  • 为啥redis会乱码?
    原因就在RedisTemplate类里面,很明显的4个序列化属性配置,而且返回类型都是RedisSerializer,继续往下看发现RedisTemplate默认的是JDK序列化,他会让字符串转义。
      
  • 如何让SpringBoot操作Redis不会乱码? 也就是替换默认的JDK序列化方式。 如果不想替换可以直接使用StringRedisTemplate!!!
    在RedisAutoConfiguration类里面有两个方法,一个就是RedisTemplate()方法,这个类使用的是@ConditionalOnMissingBean修饰的,所以就能自定义自己的Bean来覆盖默认的Bean,而且上面默认使用的是JDK序列化的也是RedisTemplate类。因此自定义 redisTemplate()!!
 
    先看下面的两个例子,Redis后台使用keys * 输出的都是乱码
    
    1.  开发就是传递的都是json字符串到Redis中(set的是一个JSON字符串),不是实体对象!!  例外:当传递一个session会传递对象
@Test
public void test1() throws JsonProcessingException {

    User u = new User("zs",23);

    ObjectMapper objectMapper = new ObjectMapper();  //使用的是jackson转换json对象
    String user = objectMapper.writeValueAsString(u);

    //实际开发中用json传递对象,而不是 redisTemplate.opsForValue().set("user",new User("",""));
    redisTemplate.opsForValue().set("user",user);

    System.out.println(redisTemplate.opsForValue().get("user"));
}   

这样执行成功,

    然而在控制台打印的数据是:    {"name":"zs","age":23}
    查询redis使用key *查看到的是:  "\xac\xed\x00\x05t\x00\x04user"   //因为使用的是JDK序列化会自动转义字符串
 
    2.  如果要想set一个实体对象到Redis中,必须要先给实体类序列化(实现 Serixx接口)
    因为默认使用的是JDK序列化,所以会报 SerializationException:Failed to serialize object using DefaultSerializer异常
//实体类:
 
 
@Data 
@AllArgsConstructor 
@NoArgsConstructor 
public class User implements Serializable {
    ...
}

 

向Redis中插入对象:
 
 
//序列化pojo后,重新set一个实体类对象
@Test
public void test2() throws JsonProcessingException {

    User user = new User("zs",23);

    //实际开发中传递的都是json,而不是 redisTemplate.opsForValue().set("user",new User("","")); 黄色部分
    redisTemplate.opsForValue().set("user1",user);   

    System.out.println(redisTemplate.opsForValue().get("user1")); 
}

    控制台打印的是:  User{name='zs', age=23}

    使用keys *查看到的是:"\xac\xed\x00\x05t\x00\x05user1"

    
    总结:上面的两个例子,一般开发中需要将对象存到Redis中,就需要将对象转换为Json字符串(Jackson、fastjson),然后set进去。如果特殊情况需要传递对象到Redis中,如需要传递session到对象里面,就必须将pojo类序列化(实例化Serixxx接口)。
 
   分析:两种方式在Redis中存入的key都是乱码形式的对象("\xac\xed\x00\x05t\x00\x05user1"),原因就是默认使用的JDK序列化方式,所以现在就需要配置自己的序列化方式(RedisAutoConfiguration中的RedisTemplate没有做一些额外的配置,只是创建了一个自己的对象;然后点进去RedisTemplate类里面,发现默认使用的是JDK序列化方式,因为RedisAutoConfiguration里面的RedisTemplate()方法使用了@ConditionalOnMissingBea修饰,所以用户可以自己定义Bean覆盖默认的RedisTemplate,然后就会替换默认JDK序列化方式。)
    
 
  • 使用StringRedisTemplate: 如果只存储String类型数据,好处是不用重写JDK,复杂类型还是推荐RedisTemplate
@Autowired
StringRedisTemplate stringRedisTemplate;

//StringRedisTemplate
@Test
public void test3() throws JsonProcessingException {

    User u = new User("zs",23);

    stringRedisTemplate.opsForValue().set("user4",String.valueOf(u));

    System.out.println(stringRedisTemplate.opsForValue().get("user4"));
}

    控制台: User{name='zs', age=23}

    Redis使用 keys *: user4   ,get user4 : "User{name='zs', age=23}"

 
  • StringRedisTemplate和RedisTemplate的区别
    1. 两者的关系是StringRedisTemplate继承RedisTemplate。
 
    2. 两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。
 
    3. SDR默认采用的序列化策略有两种,一种是String的序列化策略,一种是JDK的序列化策略。
       StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。
       RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。
 
 
    总结:当你的redis数据库里面本来存的是字符串数据或者你要存取的数据就是字符串类型数据的时候,那么你就使用StringRedisTemplate即可,但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从Redis里面取出一个对象,那么使用RedisTemplate是更好的选择。
 
 
  • 到底如何才能替换默认的JDK序列化? 替换之后就没有序列化的问题了!
别着急,先分析一波:
 
    因为@ConditionalOnMissingBean的缘故,要替换默认的RedisTemplate方法就必须和RedisAutoConfiguration里面的RedisTemplate方法名相同,这里建议将方法直接复制到自定义的类里面(我们现在写的是一个Bean,忘了什么? 配置类@Configuration注解)
    然后点进去RedisTemplate类里面,看到的就是上面 "为什么Redis会乱码?" 图片里面的信息,映入眼帘的就是四个属性,返回值都是RedisSerializer类型,继续点进去。看下图:
            
 
    看到了啥? 这些罗列出来的都是RedisSerializer接口的实现类,默认使用的就是JdkSerxxx序列(就是上面蓝色条选中的那个)。有Stringxxx的用来序列字符串的,Jacksonxxx用来序列Jackson,使用Jackson就使用这个。
    因此,如果需要使用什么序列化方式,就使用哪种序列,一般就是传递的key是String,value就是json类型。下面的模板是一个通用模板!!
 
    通用模板!:
package com.springboot.config;    //对照 StringRedisTemplate 看,里面的步骤基本相同,而且可以变出自己的Util类

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.net.UnknownHostException;

@Configuration // 所有要想覆盖的默认的代码部分都要加载到容器里面! 不要忘记!这里每次都忘
public class MyRedisTemplate {

@Bean
//1. 为了开发方便,所以将第一个泛型修改为String
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) //这个Factory就是:LettuceConnectionFactory
            throws UnknownHostException {

        RedisTemplate<String, Object> template = new RedisTemplate<>();
        //2. 连接Lettuce工厂
        template.setConnectionFactory(factory);
        //3.序列化配置:
                //3.1:Json序列化,让Json解析任意的对象,让User对象变成Json序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper(); 
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

                //3.2:String序列化:
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //所有的key采用String类型的序列化
        template.setKeySerializer(stringRedisSerializer);
        //所有的Hash的key也采用String类型的序列化
        template.setHashKeySerializer(stringRedisSerializer);
        //所以的value采用json类型的序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //所有的hash的value采用json类型的序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        //将所有的属性set进去
        template.afterPropertiesSet();

        return template;

    }
}   

 

测试:
 

//自己定义了RedisTemplate后的测试:覆盖了原有的JDK序列化
@Test
public void test4() throws JsonProcessingException {

    User u = new User("zs",23);

    ObjectMapper objectMapper = new ObjectMapper();
    String user = objectMapper.writeValueAsString(u);

    redisTemplate.opsForValue().set("user11",user);

    System.out.println(redisTemplate.opsForValue().get("user11"));
}

 

    控制台输出: User{name='zs', age=23}
    Redis:keys * 查看: user11   ,get user11: "\"{\\\"name\\\":\\\"zs\\\",\\\"age\\\":23}\""
 
 
  • 问题描述: 如果向Redis中插入中文出现下面情况,不用担心:
    控制台: {"name":"李四","age":24}
    使用get user1 查看:  "\"{\\\"name\\\":\\\"\xe6\x9d\x8e\xe5\x9b\x9b\\\",\\\"age\\\":24}\""
 
    里面的数据依然是乱码: 这里只是显示的问题,数据库中存放的依然是中文,编码格式不同意,使用redis-cli --raw 进行原样显示: 如果用的是cmd控制台操作的Redis,这样还是会乱码,因为cmd的默认编码也不是utf-8。 
    步骤: chcp 查看当前页码为 936 ,chcp 65001 换为UTF8, 然后 属性-字体-选择Lucida 确定 ,ok
 
    cmd查询出来的数据:
        127.0.0.1:6379> get user1
         "{\"name\":\"李四\",\"age\":24}"
  • 提供除了整合里面的util之外的另一种解决插入对象乱码的思路:
      
        @Bean
        public RedisTemplate redisTemplate(xx factory){
        
            RedisTemplate<String, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(factory);

            // 下面两句话就行
            Jackson2Rxxx jackson = new Jackson2RedisCacheSerxxx<>(Object.class);
            template.setDefaultxxx(jackson);

            return template;
        }
 
 

八、自定义CacheManager

 
  • 判断CacheManager生效的规则:
 
   默认使用的是SimpleCacheConfiguration,因为上一步整合Redis时候引入了Redis的Starter,所以也会自动引入了RedisCacheConfiguration。
    又因为判断SimpleCacheConfiguration生效的条件是@ConditionalOnMissingBean(CacheManager.class是否引入了CacheManager组件;
    RedisCacheConfiguration生效的判断条件也是这个条件,是否引入了CacheManager组件。而RedisCacheConfiguration中已经在类中引入了
                
 
    因此,默认的SimpleCacheConfiguraion就不生效了,然后生效的就是RedisCacheConfiguration。
    所以,引入Stater后就是RedisCacheManager生效!!
    
  • RedisCacheManager原理:
        和 三、缓存原理 相同,只是这里缓存到的是RedisCache,数据保存的不是Map,而是 RedisCacheWriter(和Map相似)
    
  • Redis如何缓存?
        注解和之前的一样(@Cacheable、@CachPut、等),用法完全一样,唯一的一步就是引入了redis-stater。
    之后缓存的数据就会直接保存到redis服务器中,如使用RedisDesktopManager查看就是: 
    
 
默认Redis配置是: 所以可以不用配置,对数据库的CRUD都会直接缓存到Redis中。
    spring:
      redis:
        port: 6379
        url: 127.0.0.1 
 
但是问题就是,缓存数据又乱码了。 下面开始解决乱码!(缓存乱码和保存对象的乱码不是一个意思)
    因为保存的数据都是默认Object类型的,利用的是JDK序列化保存(对,Redis默认的是JDK序列化),所以乱码。
 
    
  • 自定义CacheManager解决缓存乱码!: 如果报错就清空缓存

  
 @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
    RedisCacheConfiguration cacheConfiguration =
        RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofDays(1))
            .disableCachingNullValues()
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new
                GenericJackson2JsonRedisSerializer()));

        return RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build();
    }
 
  • 在编码的时候使用缓存: 注入 RedisCacheManager 组件
            
    
    
    
 
 
    
    
 
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值