全面解析!由浅入深的教你如何学习springboot中使用redis!

正文

很多时候,我们会在springboot中配置redis,但是就那么几个配置就配好了,没办法知道为什么,这里就详细的讲解一下

这里假设已经成功创建了一个springboot项目。

redis连接工厂类

第一步,需要加上springboot的redis jar包

<dependency>
     <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

然后我们写一个配置类,创建了一个redis连接的工厂的spring bean。(Redis连接工厂会生成到Redis数据库服务器的连接)

@Configuration
public class RedisConfig {
    @Bean
    public RedisConnectionFactory redisCF(){
        //如果什么参数都不设置,默认连接本地6379端口
        JedisConnectionFactory factory = new JedisConnectionFactory();
        factory.setPort(6379);
        factory.setHostName("localhost");
        return factory;
    }
}

单元测试,看看这个工厂方法的使用

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class RedisTest {

    @Autowired
    RedisConnectionFactory factory;

    @Test
    public void testRedis(){
        //得到一个连接
        RedisConnection conn = factory.getConnection();
        conn.set("hello".getBytes(), "world".getBytes());
        System.out.println(new String(conn.get("hello".getBytes())));
    }

}

输出结果 :world,说明已经成功获取到连接,并且往redis获取添加数据

template(模版)

但是我们发现每次添加的key和value都是byte数组类型(使用很麻烦),于是spring为我们带来了redis template(模版)

Spring Data Redis提供了两个模板:

  • RedisTemplate
  • StringRedisTemplate

首先我们先创建一个RedisTemplate模板类,类型的key是String类型,value是Object类型(如果key和value都是String类型,建议使用StringRedisTemplate)

@Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory factory){
        //创建一个模板类
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        //将刚才的redis连接工厂设置到模板类中
        template.setConnectionFactory(factory);
        return template;
    }

单元测试

@Autowired    
   RedisTemplate<String, Object> template;

   @Test
   public void testRedisTemplate(){
       template.opsForValue().set("key1", "value1");
       System.out.println(template.opsForValue().get("key1"));
   }

得到结果输出value1,是不是很方便了呢。

如果是操作集合呢,也很方便的哈。

@Test
   public void testRedisTemplateList(){

       Pruduct prud  = new Pruduct(1, "洗发水", "100ml");
       Pruduct prud2  = new Pruduct(2, "洗面奶", "200ml");
       //依次从尾部添加元素
       template.opsForList().rightPush("pruduct", prud);
       template.opsForList().rightPush("pruduct", prud);
       //查询索引0到商品总数-1索引(也就是查出所有的商品)
       List<Object> prodList = template.opsForList().range("pruduct", 0,template.opsForList().size("pruduct")-1);
       for(Object obj:prodList){
           System.out.println((Pruduct)obj);
       }
       System.out.println("产品数量:"+template.opsForList().size("pruduct"));

   }

key和value序列化

当保存一条数据的时候,key和value都要被序列化成json数据,取出来的时候被序列化成对象,key和value都会使用序列化器进行序列化,spring data redis提供多个序列化器

  • GenericToStringSerializer:使用Spring转换服务进行序列化;
  • JacksonJsonRedisSerializer:使用Jackson 1,将对象序列化为JSON;
  • Jackson2JsonRedisSerializer:使用Jackson 2,将对象序列化为JSON;
  • JdkSerializationRedisSerializer:使用Java序列化;
  • OxmSerializer:使用Spring O/X映射的编排器和解排器(marshaler和unmarshaler)实
    现序列化,用于XML序列化;
  • StringRedisSerializer:序列化String类型的key和value。

RedisTemplate会默认使用JdkSerializationRedisSerializer,这意味着key和value都会通过Java进行序列化。StringRedisTemplate默认会使用StringRedisSerializer

例如,假设当使用RedisTemplate的时候,我们希望将Product类型的value序列化为JSON,而key是String类型。RedisTemplate的setKeySerializer()和setValueSerializer()方法就需要如下所示:

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        // 创建一个模板类
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        // 将刚才的redis连接工厂设置到模板类中
        template.setConnectionFactory(factory);
        // 设置key的序列化器
        template.setKeySerializer(new StringRedisSerializer());
        // 设置value的序列化器
        //使用Jackson 2,将对象序列化为JSON
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //json转对象类,不设置默认的会将json转成hashmap
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setValueSerializer(jackson2JsonRedisSerializer);

        return template;
    }

到这里,大家肯定会对springboot使用redis有了简单的了解。

springboot缓存某个方法

申明缓存管理器

在某些时候,我们可能有这样的需求,用户登录的时候,我们会从数据库中读取用户所有的权限,部门等信息。而且每次刷新页面都需要判断该用户有没有这个权限,如果不停的从数据库中读并且计算,是非常耗性能的,所以我们这个时候就要用到了springboot为我们带来的缓存管理器

首先在我们的RedisConfig这个类上加上@EnableCaching这个注解。

这个注解会被spring发现,并且会创建一个切面(aspect) 并触发Spring缓存注解的切点(pointcut) 。 根据所使用的注解以及缓存的状态, 这个切面会从缓存中获取数据, 将数据添加到缓存之中或者从缓存中移除某个值。

接下来我们需要申明一个缓存管理器的bean,这个作用就是@EnableCaching这个切面在新增缓存或者删除缓存的时候会调用这个缓存管理器的方法

  • 申明缓存管理器,会创建一个切面(aspect)并触发Spring缓存注解的切点(pointcut)

  • 根据类或者方法所使用的注解以及缓存的状态,这个切面会从缓存中获取数据,将数据添加到缓存之中或者从缓存中移除某个值

    @Bean
    public RedisCacheManager cacheManager(RedisTemplate redisTemplate) {
        return new RedisCacheManager(redisTemplate);
    }

当然,缓存管理器除了RedisCacheManager还有一些其他的。例如:

  • SimpleCacheManager
  • NoOpCacheManager
  • ConcurrentMapCacheManager
  • CompositeCacheManager
  • EhCacheCacheManager

ConcurrentMapCacheManager,这个简单的缓存管理器使用java.util.concurrent.ConcurrentHashMap作为其缓存存储。它非常简单,因此对于开发、测试或基础的应用来讲,这是一个很不错的选择.

添加缓存

接下来我们在controller层的方法内加上注解,然后启动我们的项目。

    @RequestMapping("/getPrud")
    @Cacheable("prudCache")
    public Pruduct getPrud(@RequestParam(required=true)String id){
        System.out.println("如果第二次没有走到这里说明缓存被添加了");
        return pruductDao.getPrud(Integer.parseInt(id));
    }

发现打印的这段话只被打印一次,说明在走到这个方法的时候触发了一个切面,并且查找返回缓存中的数据。

当然@Cacheable注解也可以放到这个dao层的方法里面,但是这里会报一个错,Integer无法转成String,因为我们dao层方法的参数类型是int,而RedisTemplate的key类型是String,这里是要注意的。

打开redis的客户端发现redis对应的key就是我们的参数1,这个时候就会出问题,比如说我在其他要缓存的方法的参数也是1,就会重复。后面我们会将自定义这个key的值。

除了@Cacheable添加缓存外,springboot还为我们带了了其他几个注解


image

删除缓存
在delete的时候用@CacheEvict清楚这条缓存。

@RequestMapping("/deletePrud")
    @CacheEvict("pruddeleteCache")
    public String deletePrud(@RequestParam(required=true)String id){
        return "SUCCESS";
    }

@CachePut将这个方法的返回值放到缓存,如果我们放一个Pruduct对象,他会将这个对象作为key,这显然不是我们想要的。这个时候就需要自定义我们的key。

自定义key
@Cacheable和@CachePut都有一个名为key属性,这个属性能够替换默认的key,它是通过一个表达式(Spel表达式,spring提供的,很简单)计算得到的。

例如下面的就是将返回对象的id当作key来存储(但是Pruduct的id是int类型,所以需要将数字转化成String类型)

   @RequestMapping("/savePrud")
    @CachePut(value="prudsaveCache",key="#result.id +''")
    public Pruduct savePrud(Pruduct prud){
        return prud;
    }

另外除了#result是代表函数的返回值,spring还为我们带来了其他的一些元数据


image

条件化缓存
通过为方法添加Spring的缓存注解,Spring就会围绕着这个方法创建一个缓存切面。但是,在有些场景下我们可能希望将缓存功能关闭。

@Cacheable和@CachePut提供了两个属性用以实现条件化缓存:unless和condition,这两个属性都接受一个SpEL表达式。如果unless属性的SpEL表达式计算结
果为true,那么缓存方法返回的数据就不会放到缓存中。与之类似,如果condition属性的SpEL表达式计算结果为false,那么对于这个方法缓存就会被禁用掉

表面上来看,unless和condition属性做的是相同的事情。但是,这里有一点细微的差别。

unless属性只能阻止将对象放进缓存,但是在这个方法调用的时候,依然会去缓存中进行查找,如果找到了匹配的值,就会返回找到的值。

与之不同,如果condition的表达式计算结果为false,那么在这个方法调用的过程中,缓存是被禁用的。就是说,不会去缓存进行查找,同时返回值也不会放进缓存中。

   @RequestMapping("/getPrud2")
    @CachePut(value ="prudCache",unless="#result.desc.contains('nocache')")
    public Pruduct getPrud2(@RequestParam(required=true)String id){
        System.out.println("如果走到这里说明,说明缓存没有生效!");
        Pruduct p = new Pruduct(Integer.parseInt(id), "name_nocache"+id, "nocache");
        return p;
    }

上面的代码中,如果返回的对象desc中包含nocache字符串,则不进行缓存。

总结demo:

将类名方法名和pruduct的id作为key

@RequestMapping("/getPrud3")
    @Cacheable(value ="prudCache",key="#root.targetClass.getName() + #root.methodName + #id")
    public Pruduct getPrud3(@RequestParam(required=true)String id){
        System.out.println("如果第二次没有走到这里说明缓存被添加了");
        return pruductDao.getPrud(Integer.parseInt(id));
    }

最后注意

result 方法返回值不能用在@Cacheable上,只能用在@CachePut

springboot配置升级简单化

当然上面的配置只是为了了解原理的哈,实际上我们使用会更简单点。我们重写了RedisConfig

@Configuration
@EnableCaching//开启缓存
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }    
    /**
     * 申明缓存管理器,会创建一个切面(aspect)并触发Spring缓存注解的切点(pointcut)
     * 根据类或者方法所使用的注解以及缓存的状态,这个切面会从缓存中获取数据,将数据添加到缓存之中或者从缓存中移除某个值

     * @return
     */
    @Bean
    public RedisCacheManager cacheManager(RedisTemplate redisTemplate) {
        return new RedisCacheManager(redisTemplate);
    }

    @Bean
    @Primary
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        // 创建一个模板类
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        // 将刚才的redis连接工厂设置到模板类中
        template.setConnectionFactory(factory);
        // 设置key的序列化器
        template.setKeySerializer(new StringRedisSerializer());
        // 设置value的序列化器
        //使用Jackson 2,将对象序列化为JSON
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //json转对象类,不设置默认的会将json转成hashmap
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setValueSerializer(jackson2JsonRedisSerializer);

        return template;
    }

}

然后在resources下的application.properties下配置

# REDIS (RedisProperties)
# Redis数据库索引(默认为0)
spring.redis.database=0  
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379  
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8  
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1  
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8  
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0  
# 连接超时时间(毫秒)
spring.redis.timeout=0

大家发现我们并没有注册RedisConnectionFactory,那是因为spring默认帮我们读取application.properties文件并且注册了一个factorybean

keyGenerator方法帮我们注册了一个key的生成规则,就不用我们写spel表达式了,根据反射的原理读取类名+方法名+参数。但是我们有时候还是需要结合spel的。

然后在controller上加上@Cacheable(“cachename”),之后就可以在redis看到保存了并且key的值是keyGenerator生成的名字

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容