SpringBoot 精通系列-SpringBoot整合Redis的常用操作

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/nihui123/article/details/101151134

导语
  在之前的博客中介绍过关于Memcache的使用,这篇文章中主要介绍关于Redis的有关概念及如何与SpringBoot整合使用。
  首先Redis是目前使用最为广泛的缓存中间件,相比较Memcache来说,Redis支持更多的数据结构而且对于这些数据结构的操作也很多,当然Redis也支持非常丰富的高可用集群解决方案。下面就来看一下关于Redis相关操作。

Redis介绍

  Redis是一个高速非关系型数据库也就是常说的No-SQL数据库,它可以存储键值对,支持五种不同Value的存储,可以将存储在内存的键值对持久化到硬盘中,可以使用复制的特点扩展其可读性,还可以使用客户端进行分片来进行扩展。

  为了高性能,Redis也采用的是内存数据集,根据使用场景,可以通过每隔一段时间转存数据到磁盘,或者追加每条命令到日志来维持持久化操作。当然持久化也可以被禁用,如果只是需要一个功能则可以不用持久化。

数据模型

  Redis数据模型不仅仅与关系型数据库不同,也不同于其他的NoSQL键值对存储。Redis数据类型类似于编程中的基本数据类型,可以通过这个基本的数据类型做其他的高级操作。主要支持的数据类型包括如下一些

  • String
  • Hash
  • List
  • Set
  • ZSet

优势

  Redis是使用C语言来编程的,直接操作内存在速度上有了优势,其次它支持的基本数据类型丰富、支持原子性的操作、对于所有语言的通用性等等。

  • 高性能:每秒可以执行10w个set以及10w个get操作
  • 数据类型丰富:Redis对于多数开发人员已知的数据类型提供了原生的支持
  • 原子性:Redis操作都是原子性的,也就是多个客户端并发访问Redis服务器,获取到的值一定是最新的。
  • 其他特性:Redis所适用的场景有很多,包括缓存、消息队列(Redis原生支持)、短期的应用数据存储(例如Session、Web命中数据等)等。

spring-boot-starter-data-redis

  作为一个优秀的企业级解决方案。SpringBoot提供了与Redis集成的组件包,spring-boot-starter-date-redis, 它依赖与spring-data-redis 和 lettuce。在SpringBoot1.0默认使用的是Jedis客户端,2.0的时候被替换成了Lettuce,如果使用的是1.5.x的SpringBoot是感觉不到差异的,因为SpringBoot隔离了其中的一些差异性。

  • Lettuce:一个可伸缩并且线程安全的Redis客户端,多个线程同时共享了一个RedisConnection,利用了Netty NIO作为IO连接来高效的管理多个连接。
  • SpringData: Spring中的提供数据存储解决方案的框架,包括支持关系型数据库、非关系型数据库、Map-Reduce框架、云数据服务等。
  • SpringData Redis: 是SpringData提供的一个关于Redis的解决方案,实现了对于Redis客户端API的高度整合。

关系如下

Lettuce
SpringDataRedis
SpringData
Spring-Boot-Data-Redis

如上图所示,其实SpringDataRedis和Lettuce具备的功能,在SpringBoot Redis的启动器中都会存在。

快速开发

第一步、引入相关依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.nh.redis</groupId>
    <artifactId>redis</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>redis</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

注意
  这里说明一下为什么要使用commons-pool2是因为Lettuce需要使用commons-pool2来创建连接池。

第二步、application的配置

# Redis 数据库索引
spring.redis.database=0
# Redis 服务器地址
spring.redis.host=localhost
# Redis 服务器连接端口
spring.redis.port=6379
# Redis 服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数据 默认为 8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞时间 默认为 -1
spring.redis.lettuce.pool.max-wait= -1
# 连接池中最大空闲连接
spring.redis.lettuce.pool.max-idle=8
# 连接池中最小空闲连接
spring.redis.lettuce.pool.min-idle=0

第三步、缓存配置

  这里是为了更好的整合Redis而设置一些全局的配置,例如key的生成策略,设置默认参数等等操作。

@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();   
            }       
        };
    }   
}

第四步、单元测试

  在单元测试中注入一个RedisTemplate,对于这个RedisTemplate与JDBCTemplate的作用是相同的。而String是以最长用的数据类型,普通的KV存储都可以使用String类型,当然这个是不一定的,也是使用其他的。

@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    public void contextLoads() {
        redisTemplate.opsForValue().set("nihui","hello");
        Assert.assertEquals("hello",redisTemplate.opsForValue().get("nihui"));
    }

}

  从上面的流程来看只需要使用spring-boot-starter-data-redis只需要三步就可以与Redis进行集成。下面就来介绍一下Redis对于其他数据类型的操作。

Redis对于其他数据类型的操作

  Redis在SpringBoot中支持的数据类型有,实体、哈希、列表、集合、有序集合。这些类型在SpringBoot中怎么去使用,下面就来看看各个数据类型怎么使用。

1、实体类型

  首先来看看Redis对于Pojo的支持,首先来创建一个User对象,将其放入到缓存中,然后再取出来,测试操作如下

@Test
public void testObj(){
     User user = new User("nihui",23,"[email protected]");
     ValueOperations<String,User> operations = redisTemplate.opsForValue();
     operations.set("user",user);
     User u = operations.get("user");
     System.out.println("user : "+ u.toString());
}

2、超时失效

  Redis对于每个存储到其中的数据都可以设置一个超时时间,过了这个时间就会自动删除数据,这种特点就适合进行阶段性的数据缓存操作。

@Test
public void testExpire() throws InterruptedException {
    User user = new User("nihui",23,"[email protected]");
    ValueOperations<String,User> operations = redisTemplate.opsForValue();
    operations.set("nihui",user,100,TimeUnit.MILLISECONDS);
    Thread.sleep(1000);
    boolean exists = redisTemplate.hasKey("nihui");
    if (exists){
        System.out.println("exists is true");
    }else {
        System.out.println("exists is false");
    }
}

  从测试结果来看Redis已经不能存在User对象了,也就是说这个数据已经过期了,同时在则是过程中使用了hasKey来判断key是否存在。

3、删除数据

  在有些时候需要对过期的数据进行清理,下面就来看看如何清理这些数据。

@Test
public void testDelete(){
    redisTemplate.opsForValue().set("deleteKey","hello,nihui");
    redisTemplate.delete("deleteKey");
    boolean exists = redisTemplate.hasKey("deleteKey");
    if (exists){
        System.out.println("exists is true");
    }else {
        System.out.println("exists is false");
    }
}

4、Hash

  在正常情况下存储一个键自然会对应有一个值,实际上在很多场景中这种操作并不是最好的,Redis存储一个键只会占用很小的内存,但是不管你这个键值字节数有多少都会是一样的,这个时候就是用Hash来解决这个资源浪费的问题。

  Hash 就表示使用Hash这种数据结构进行key和value的存储。哈希表本身就是一个键值对的集合。这个集合可以根据kv的大小来动态开辟空间。对于一些有规律性的数据可以使用这种方式进行存储。

在这里插入图片描述
从上图可以看到IDEA给出的操作提示,第一个参数表示hash表的Key,第二个参数表示Hash表中的key,第三个参数表示Hash表中的Value。

@Test
public void testHash(){
   HashOperations<String,Object,Object> hashOperations = redisTemplate.opsForHash();
   hashOperations.put("nihui","hello","hello");
   String value = (String) hashOperations.get("nihui","hello");
   System.out.println("hash value "+value);
}

5、List

  在Redis中的List使用的场景也是比较多的,也作为Redis中最为重要的一个数据结构存在,使用List可以实现一个数据队列的存储,也就是说支持一类数据的存储集合,使用List最常用的一个场景就是消息队列。可以利用List进行Push操作,将任务存储到List中,然后工作线程从中执行先进先出的操作,从List中操作对应的任务。

@Test
public void testList(){
    ListOperations<String,String> listOperations = redisTemplate.opsForList();
    listOperations.leftPush("list","nihui");
    listOperations.leftPush("list","hello");
    listOperations.leftPush("list","know");
        
    String value = listOperations.leftPop("list");
    System.out.println("list value : "+value.toString());
}

  从上面的操作中可以看到从左面进从左面出,表示这个是一个栈,后进先出,所有最后取出的值是know,当然也可以从左边进从右边出构成一个队列。这可以后续自己设置符合自己程序的数据结构进行操作。

注意
  Redis List 是实现了一个双向链表,也就是支持反向的查找和遍历操作,但是使用双向链表所带来的结果就是一部分的内存开销,在Redis内部有很多的实现方式。具体的双向链表可以关注后续的数据结构系列。

6、Set

  Redis Set对外提供的是一个类似于List的功能,特殊的地方是在于Set是可以自动重排的,需要存储一个列表数据,又不希望出现重复数据的时候可以使用这种数据结构,当然这个也是集合这种数据结构的特点,集合中的元素是不重复的,当然也可以使用Set来判断是否存在某个元素。这个是List不能实现的功能。

@Test
public void testSet(){
    String key = "set";
    SetOperations<String,String> setOperations = redisTemplate.opsForSet();
    setOperations.add(key,"nihui");
    setOperations.add(key,"nihui");
    setOperations.add(key,"hello");
    setOperations.add(key,"world");

    Set<String> values = setOperations.members(key);

    for (String v:values) {
        System.out.println("set value : "+v);
    }
}

  当然这里往这个Set中存储了两个nihui,但是读取的时候只有一条数据也就是说Set对数据进行了重排。

7、对于Set的其他操作

&emps; 从数学的角度上讲,对于一个集合来说有交集、并集、差集等操作,Redis中对这些操作也提供了一些支持

7.1、difference

@Test
public void testSetDifference(){
    SetOperations<String,String> setOperations = redisTemplate.opsForSet();
    String key1 = "key1";
    String key2 = "key2";
        
    setOperations.add(key1,"nihui");
    setOperations.add(key1,"hello");
    setOperations.add(key1,"hello");
    setOperations.add(key1,"world");
    setOperations.add(key2,"xxx");
    setOperations.add(key2,"nihui");
        
    Set<String> diffs = setOperations.difference(key1,key2);
    for (String v :diffs){
        System.out.println("diffs set value :"+v);
    }     
}

  上面这个例子是吧key1中不同于key2 的数据对比出来。类似于在集合A中但是不在集合B中的操作。在数学上被称为是补集。

7.2、unions

@Test
public void testSetUnions(){
    SetOperations<String,String> setOperations = redisTemplate.opsForSet();
    String key1 = "key1";
    String key2 = "key2";

    setOperations.add(key1,"nihui");
    setOperations.add(key1,"hello");
    setOperations.add(key1,"xxx");
    setOperations.add(key2,"aa");
    setOperations.add(key2,"bb");
    setOperations.add(key2,"world");

    Set<String> unions = setOperations.union(key1,key2);
    for (String v :unions){
        System.out.println("unions set value :"+v);
    }

}

  根据测试结果可以发现,unions会联合两个集合,类似于并集。

注意
  其实Set的内部实现是一个Value永远为null的HashMap,也就是说实际是通过计算Hash的方式来快速重排,这个也是为什么Set可以快速判断是否有数据在集合中的原因,如果有重复元素就会产生Hash冲突,如果没有Hash冲突则不会有重复集合。

8、Zet

  Redis Sorted Set 的使用场景与Set是类似的,区别是Set不会自动有序,而Sorted Set可以通过用户额外提供的一个Score来实现排序操作并且插入有序,也就是自动排序,
注意这里的排序和重排是两个不一样的概念
  在使用Zset的时候需要额外输入一个Score的参数,Zset会根据Score的值来对集合进行排序,可以利用这个特点来实现根据权重进行排序,例如最重要消息为Score为1,重要消息Score为2 然后通过权重来获取消息。

 @Test
public void testZset(){
    String key = "zset";
    redisTemplate.delete(key);
    ZSetOperations<String,String> zSetOperations = redisTemplate.opsForZSet();
    zSetOperations.add(key,"nihui",1);
    zSetOperations.add(key,"hello",6);
    zSetOperations.add(key,"world",4);
    zSetOperations.add(key,"test",3);
        
        
    Set<String> zsets = zSetOperations.range(key,0,3);
    for (String value:zsets) {
        System.out.println("zset value : "+value);
    }
        
    Set<String> zsett = zSetOperations.rangeByScore(key,0,3);
    for (String value:zsett) {
        System.out.println("zsett value : "+value);
    }
}

  通过上面的例子会发现插入的Zset中的数据会更具Score进行重新排序,根据这特性可以做优先级队列等操作,另外Zset还提供了rangeByScore的方法,获取Score范围内的数据排序数据。
注意
  Redis Sorted Set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的成员到Score的映射,而跳跃表中存放的是所有的数据成员,排序依据的是HashMap里的Score,使用跳跃表结构可以获得比较高的查找效率,并且在实现上也相对比较简单。

  到这里关于Redis所支持的数据类型的基本操作都已经说完了,其他的高级操作可以继续关注博客提供更优秀的解决方案。

封装缓存

  在实际工作中并不是使用RedisTemplate来进行操作的,不是为每个类都注入一个RedisTemplate,而是对不同的功能模块进行封装操作,然后暴露其中的一个服务接口来进行缓存操作。这也符合面向对象的三大特性。

@Service
public class RedisCacheService {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    //增加缓存
    public boolean set(final String key,Object value){
        boolean result = false;
        try{
            ValueOperations<Serializable,Object> operations = redisTemplate.opsForValue();
            operations.set(key,value);
            result = true;
        }catch (Exception e){
            e.printStackTrace();
        }
        return result;
    }
    
    //删除某一类键
    public void removePattern(final String pattern){
        Set<Serializable> keys = redisTemplate.keys(pattern);
        if (keys.size()>0){
            redisTemplate.delete(keys);
        }
    }
}

  在其他地方使用的时候只需要注入这个服务即可

总结

  相比较于Memcache来说,Redis是比较优秀的一款高可用性的缓存中间件,在有些企业中更是使用Redis来作为数据库来使用,Spring官方也为Redis提供良好的支持,Redis支持丰富的数据类型以及对数据类型的操作,方便了各种场景下的使用。提供了很多内置的支持。当然这个只是Redis在SpringBoot中的使用方式,后续的博客中将对Redis做一个专题的学习。敬请期待

猜你喜欢

转载自blog.csdn.net/nihui123/article/details/101151134