Spring Boot 2.x使用篇(四)—— Spring Boot与Redis

文章目录

  • 1、spring-data-redis项目简介
    • 1.1 spring-data-redis项目的设计
    • 1.2 RedisTemplate
    • 1.3 序列化器
    • 1.4 Spring对Redis数据类型操作的封装
  • 1.5 同一个连接执行多个Redis命令
  • 2、在Spring Boot中配置和使用Redis
    • 2.1 在Spring Boot中引入Redis依赖
    • 2.2 在application.properties中配置Redis
    • 2.3 修改RedisTemplate的序列化器
    • 2.4 在Spring Boot中操作Redis数据类型
      • 2.4.1 操作字符串和散列数据类型
      • 2.4.2 操作列表(链表)数据类型
      • 2.4.3 操作集合数据类型
      • 2.4.4 操作有序集合类型
  • 3、Redis的一些特殊用法
    • 3.1 使用Redis事务
    • 3.2 使用Redis流水线
    • 3.3 使用Redis发布订阅
    • 3.4 使用Lua脚本

1、spring-data-redis项目简介

1.1 spring-data-redis项目的设计

  Spring是通过spring-data-redis项目对Redis开发进行支持的。这里仅讨论Spring推荐使用的类库Jedis的使用。

  Spring提供了一个RedisConnectionFactory接口,通过它可以生成一个RedisConnection接口对象,而RedisConnection接口对象是对Redis底层接口的封装。例如我们讨论的Jedis驱动,那么Spring就会提供RedisConnection接口的实现类JedisConnection去封装原有的Jedis对象。

  在Spring中是通过RedisConnection接口操作Redis的,而RedisConnection则对原生的Jedis进行封装。要获取RedisConnection接口对象,是通过RedisConnectionFactory接口去生成的,所以第一步要配置的便是这个工厂了,而配置这个工厂主要是配置Redis的连接池,对于连接池可以限定其最大连接数、超时时间等属性。

  我们在config包下创建名为RedisConfig的配置类,手动装配一个RedisConnectionFactory,具体代码如下:

package com.ccff.springboot.demo.chapter7.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import redis.clients.jedis.JedisPoolConfig;

/**
 * Created by wangzhefeng01 on 2019/8/9.
 */
@Configuration
public class RedisConfig {
    private RedisConnectionFactory connectionFactory = null;

    @Bean(name = "redisConnectionFactory")
    public RedisConnectionFactory initRedisConnectionFactory(){
        if (this.connectionFactory != null){
            return this.connectionFactory;
        }
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        //最大空闲数
        poolConfig.setMaxIdle(50);
        //最大连接数
        poolConfig.setMaxTotal(100);
        //最大等待时间毫秒数
        poolConfig.setMaxWaitMillis(2000);
        //创建Jedis连接工厂
        JedisConnectionFactory connectionFactory = new JedisConnectionFactory(poolConfig);
        //获取单机的Redis配置
        RedisStandaloneConfiguration configuration = connectionFactory.getStandaloneConfiguration();
        configuration.setHostName("127.0.0.1");
        configuration.setPort(6379);
        configuration.setPassword("123456");
        this.connectionFactory = connectionFactory;
        return connectionFactory;
    }
}

  上面的代码通过一个连接池的配置创建了RedisConnectionFactory,通过它就能够创建RedisConnection接口对象。但是我们在使用一条连接时,要先从RedisConnectionFactory工厂获取,然后在使用完成后还要自己关闭它。Spring为了进一步简化开发,提供了RedisTemplate。

1.2 RedisTemplate

  应该说RedisTemplate是使用最多的类,所以它是Spring操作Redis的重点内容。

  首先它会自动从RedisConnectionFactory工厂中获取连接,然后执行对应的Redis命令,在最后还会关闭Redis连接。这些在RedisTemplate中都被封装了,所以并不需要开发者关注Redis的连接的闭合问题。

  修改RedisConfig配置类,手动配置RedisTemplate,具体代码如下:

package com.ccff.springboot.demo.chapter7.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import redis.clients.jedis.JedisPoolConfig;

/**
 * Created by wangzhefeng01 on 2019/8/9.
 */
@Configuration
public class RedisConfig {
    private RedisConnectionFactory connectionFactory = null;

    @Bean(name = "redisConnectionFactory")
    public RedisConnectionFactory initRedisConnectionFactory(){
        if (this.connectionFactory != null){
            return this.connectionFactory;
        }
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        //最大空闲数
        poolConfig.setMaxIdle(50);
        //最大连接数
        poolConfig.setMaxTotal(100);
        //最大等待时间毫秒数
        poolConfig.setMaxWaitMillis(2000);
        //创建Jedis连接工厂
        JedisConnectionFactory connectionFactory = new JedisConnectionFactory(poolConfig);
        //获取单机的Redis配置
        RedisStandaloneConfiguration configuration = connectionFactory.getStandaloneConfiguration();
        configuration.setHostName("127.0.0.1");
        configuration.setPort(6379);
        configuration.setPassword("123456");
        this.connectionFactory = connectionFactory;
        return connectionFactory;
    }

    @Bean(name="redisTemplate")
    public RedisTemplate<Object, Object> initRedisTemplate() {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(initRedisConnectionFactory());
        return redisTemplate;
    }
}

  在手动装配RedisTemplate后,我们就可以使用其操作Redis了。在test文件夹下创建名为Test的测试类,具体代码如下所示:

import com.ccff.springboot.demo.chapter7.config.RedisConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * Created by wangzhefeng01 on 2019/8/9.
 */
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(RedisConfig.class);
        RedisTemplate redisTemplate = context.getBean(RedisTemplate.class);
        redisTemplate.opsForValue().set("key1","value1");
        redisTemplate.opsForHash().put("hash","field","hvalue");
    }
}

  执行测试方法后,在Redis客户端通过命令“key *key1”进行查看,结果如下:
Spring Boot 2.x使用篇(四)—— Spring Boot与Redis_第1张图片
  这里我们需要注意的是:Redis是一种基于字符串存储的NoSQL,而Java是基于对象的语言,对象是无法存储到Redis中的。不过,Java提供了序列化机制,只要类实现了java.io.Serializable接口,就代表类的对象能够进行序列化,通过将类对象进行序列化就能够得到二进制字符串,这样Redis就可以将这些类对象以字符串的形式进行存储。这也就是我们在上面的截图中看到的结果。同时,Java也可以将那些二进制字符串通过反序列化转为对象,通过这个原理,Spring提供了序列化器的机制,并且实现了几个序列化器。

1.3 序列化器

  对于序列化器,Spring提供了RedisSerializer接口,它有两个方法。这两个方法,一个是serialize,它能够把那些可以序列化的对象转换为二进制字符串;另一个是deserialize,它能够通过反序列化把二进制字符串转换为Java对象。

  在Spring中,最常用的两个序列化器是StringRedisSerializer和JdkSerializationRedisSerializer,其中JdkSerializationRedisSerializer是RedisTemplate默认的序列化器。

  RedisTemplate提供了下表所示的几个可以配置的属性

属性 描述 备注
defaultSerializer 默认序列化器 如果没有设置,则使用JdkSerializationRedisSerializer
keySerializer Redis键序列化器 如果没有设置,则使用默认序列化器
valueSerializer Redis值序列化器 如果没有设置,则使用默认序列化器
hashKeySerializer Redis散列结构field序列化器 如果没有设置,则使用默认序列化器
hashValueSerializer Redis散列结构value序列化器 如果没有设置,则使用默认序列化器
stringSerializer 字符串序列化器 RedisTemplate自动赋值为StringSerializer对象

  为了使我们在Redis客户端显示的结果变为可读的字符串,因此需要修改在RedisConfig配置类中对RedisTemplate的装配。具体修改如下:

@Bean(name="redisTemplate")
public RedisTemplate<Object, Object> initRedisTemplate() {
    RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(initRedisConnectionFactory());
    RedisSerializer<String> stringRedisSerializer = redisTemplate.getStringSerializer();
    redisTemplate.setKeySerializer(stringRedisSerializer);
    redisTemplate.setHashKeySerializer(stringRedisSerializer);
    redisTemplate.setHashValueSerializer(stringRedisSerializer);
    return redisTemplate;
}

  通过上面的代码,主动将Redis的键和散列结构的field和value均采用了字符串序列化器,这样把它们转换出来时就采用字符串了。再次执行测试类后,在Redis的客户端中进行查看,结果如下:
Spring Boot 2.x使用篇(四)—— Spring Boot与Redis_第2张图片

1.4 Spring对Redis数据类型操作的封装

  Redis能够支持7种类型的数据结构,这7种类型是:

  • 字符串
  • 散列
  • 列表(链表)
  • 集合
  • 有序集合
  • 基数
  • 地理位置

  为此,Spring针对每一种数据结构的操作都提供了对应的操作接口

操作接口 功能 备注
GeoOperations 地理位置操作接口 使用不多
HashOperations 散列操作接口 -
HyperLogLogOperations 基数操作接口 使用不多
ListOperations 列表(链表)操作接口 -
SetOperations 集合操作接口 -
ValueOperations 字符串操作接口 -
ZSetOperations 有序集合操作接口 -

  而这些操作接口都可以通过RedisTemplate得到,得到的方法也很简单:

//获取地理位置操作接口
redisTemplate.opsForGeo();
//获取散列操作操作接口
redisTemplate.opsForHash();
//获取基数操作操作接口
redisTemplate.opsForHyperLogLog();
//获取列表(链表)操作接口
redisTemplate.opsForList();
//获取集合操作接口
redisTemplate.opsForSet();
//获取字符串操作接口
redisTemplate.opsForValue();
//获取有序集合操作接口
redisTemplate.opsForZSet();

1.5 同一个连接执行多个Redis命令

  在当前的测试类中,两个操作并不是在同一个Redis的连接下完成的。在执行第一个操作时,redisTemplate会先从工厂中获取一个连接,然后执行对应的Redis命令,再关闭这个连接。在执行第二个操作时,从工厂获取另外一个连接,然后执行对应的Redis命令,再关闭这个连接。通过在控制台输出的日志即可验证
在这里插入图片描述

  上面所描述的这种情况显然是很浪费资源的,而且更多时候我们希望在同一个连接中执行多个命令。

  Spring提供了对应的BoundXXXOperations接口,用于连续操作某个数据类型多次,具体如下:

接口 说明
BoundGeoOperations 绑定一个地理位置数据类型的键操作,不常用
BoundHashOperations 绑定一个散列数据类型的键操作
BoundListOperations 绑定一个列表(链表)数据类型的键操作
BoundSetOperations 绑定一个集合数据类型的键操作
BoundValueOperations 绑定一个字符串数据类型的键操作
BoundZSetperations 绑定一个有序集合数据类型的键操作

  同样的,RedisTemplate也对获取它们提供了相应的方法,具体如下:

//获取地理位置绑定键操作接口
redisTemplate.boundGeoOps("geo");
//获取散列绑定键操作接口
redisTemplate.boundHashOps("hash");
//获取列表(链表)绑定键操作接口
redisTemplate.boundListOps("list");
//获取集合绑定键操作接口
redisTemplate.boundSetOps("set");
//获取字符串绑定键操作接口
redisTemplate.boundValueOps("value");
//获取有序列表绑定键操作接口
redisTemplate.boundZSetOps("zset");

  在获取其中的操作接口后,我们就恶意对某个键的数据进行多次操作。这样我们再通过SessionCallback接口和RedisCallback接口实现多条命令在同一个连接中完成。

  SessionCallback接口和RedisCallback接口的作用是让RedisTemplate进行回调,通过它们可以在同一条连接下执行过个Redis命令。其中SessionCallback提供了良好的封装,对于开发者比较友好,因此在实际开发中应该优先选择使用它;相对而言,RedisCallback接口比较底层,需要处理的内容也比较多,可读性较差,所以在非必要的时候尽量不要使用它。

  修改Test测试类的测试方法如下:

import com.ccff.springboot.demo.chapter7.config.RedisConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;

/**
 * Created by wangzhefeng01 on 2019/8/9.
 */
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(RedisConfig.class);
        RedisTemplate redisTemplate = context.getBean(RedisTemplate.class);

        redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                redisOperations.opsForValue().set("key1","value1");
                redisOperations.opsForHash().put("hash","field","hvalue");
                return null;
            }
        });
    }
}

  再次执行测试方法,查看输出在控制台的日志信息,发现此事两个Redis命令均在一个连接中执行。
Spring Boot 2.x使用篇(四)—— Spring Boot与Redis_第3张图片

2、在Spring Boot中配置和使用Redis

2.1 在Spring Boot中引入Redis依赖

  要想在Spring Boot中使用Redis,需要先加入关于Redis的依赖。同样Spring Boot为期提供了starter,然后允许我们在配置文件中进行配置。所以首先我们在pom文件中引入相关依赖,具体如下:

<dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
            <exclusions>
                
                <exclusion>
                    <groupId>io.lettucegroupId>
                    <artifactId>lettuce-coreartifactId>
                exclusion>
            exclusions>
        dependency>
        
        
        <dependency>
            <groupId>redis.clientsgroupId>
            <artifactId>jedisartifactId>
        dependency>
    dependencies>

  这里需要注意的是:在默认情况下,spring-boot-starter-data-redis(版本2.x)会依赖Lettuce的Redis客户端驱动,而在一般的项目中,我们会使用Jedis,所以在代码中使用了exclusions元素将其依赖排除掉,与此同时引入了Jedis的依赖,这样就可以使用Jedis进行编程了。

2.2 在application.properties中配置Redis

  在引入Redis的相关依赖后,想要使用Redis,则需要在application.properties配置文件中对其进行简单的配置,下面配置了连接池和服务器的属性,用以连接Redis服务器,这样Spring Boot的自动装配机制就会读取这些配置来生成有关Redis的操作对象。这里它会自动生成RedisConnectionFactory、RedisTemplate、StringRedisTemplate等常用的对象。

# =========================== 配置Jedis连接池属性 ===========================
spring.redis.jedis.pool.min-idle=5
spring.redis.jedis.pool.max-active=10
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.max-wait=2000
# =========================== 配置Jedis连接池属性 ===========================

# =========================== 配置Redis服务器属性 ===========================
spring.redis.port=6579
spring.redis.host=127.0.0.1
spring.redis.timeout=1000
# =========================== 配置Redis服务器属性 ===========================

2.3 修改RedisTemplate的序列化器

  RedisTemplate会默认使用JdkSerializationSerializer进行序列化键值,这样便能够存储到Redis服务器中。如果这样,Redis服务器存入的便是一个经过序列化的特殊字符串,有时候对于我们跟踪并不是很友好。

  如果我们在Redis只是使用字符串,则使用Spring Boot自动为我们生成的StringRedisTemplate即可,但是这样就只能支持字符串了,并不能支持Java对象的存储。

  为了克服上述的两种情况产生的问题,我们可以通过设置RedisTemplate的序列化器来处理。

  修改Spring Boot的启动类如下:

package com.ccff.springboot.demo.chapter7;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;

import javax.annotation.PostConstruct;

/**
 * Created by wangzhefeng01 on 2019/8/9.
 */
@SpringBootApplication
public class Chapter7Application {
    //注入RedisTemplate
    @Autowired
    private RedisTemplate redisTemplate = null;

    //定义Spring自定义后初始化方法
    @PostConstruct
    public void init(){
        initRedisTemplate();
    }

    //设置RedisTemplate的序列化器
    private void initRedisTemplate() {
        RedisSerializer stringSerializer = redisTemplate.getStringSerializer();
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
    }

    public static void main(String[] args) {
        SpringApplication.run(Chapter7Application.class, args);
    }
}

  由上面的代码可知,首先自动注入了Spring Boot根据配置文件自动生成的RedisTemplate对象,然后利用Spring Bean生命周期的特性使用注解@PostConstruct自定义后初始化方法。在这个方法里,把RedisTemplate中的键序列化器和散列数据的field都修改为StringRedisSerializer。

  由于在RedisTemplate中它会默认地定义一个StringRedisSerializer对象,所以这里并没有自己创建一个新的StringRedisSerializer,而是从RedisTemplate中获取。

2.4 在Spring Boot中操作Redis数据类型

  在controller包下创建名为RedisController的控制器类,这里用于从RedisTemplate的角度演示常用Redis类型(字符串,散列,列表,集合和有序集合)的操作。初始RedisController的控制器类代码如下:

package com.ccff.springboot.demo.chapter7.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Created by wangzhefeng01 on 2019/8/9.
 */
@RestController
@RequestMapping("/redis")
public class RedisController {
    @Autowired
    private RedisTemplate redisTemplate = null;

    @Autowired
    private StringRedisTemplate stringRedisTemplate = null;
}

2.4.1 操作字符串和散列数据类型

  在RedisController的控制器类中创建名为stringAndHash的方法,具体代码如下:

@GetMapping("/stringAndHash")
public Map<String, Object> stringAndHash(){
    redisTemplate.opsForValue().set("key1", "value1");
    // 注意这里使用了JDK的序列化器,所以Redis保存的时候不是整数,不能运算
    redisTemplate.opsForValue().set("int_key", "1");
    stringRedisTemplate.opsForValue().set("int", "1");
    // 使用运算
    stringRedisTemplate.opsForValue().increment("int", 1);
    // 获取底层Jedis连接
    Jedis jedis = (Jedis) stringRedisTemplate.getConnectionFactory().getConnection().getNativeConnection();
    // 减一操作,这个命令RedisTemplate不支持,所以笔者先获取底层的连接再操作
    jedis.decr("int");
    Map<String, String> hash = new HashMap<String, String>();
    hash.put("field1", "value1");
    hash.put("field2", "value2");
    // 存入一个散列数据类型
    stringRedisTemplate.opsForHash().putAll("hash", hash);
    // 新增一个字段
    stringRedisTemplate.opsForHash().put("hash", "field3", "value3");
    // 绑定散列操作的key,这样可以连续对同一个散列数据类型进行操作
    BoundHashOperations hashOps = stringRedisTemplate.boundHashOps("hash");
    // 删除两个个字段
    hashOps.delete("field1", "field2");
    // 新增一个字段
    hashOps.put("filed4", "value5");
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("success", true);
    return map;
}

2.4.2 操作列表(链表)数据类型

  列表也是常用的数据类型。在Redis中列表是一种链表结构,因此查询性能不高,但增删节点性能高。在使用链表的过程中需要注意如下两个问题:

  • 列表元素的顺序问题:是从左到右还是从右到左
  • 列表元素的下标问题:在Redis中下标是从0开始的

  在RedisController的控制器类中创建名为testList的方法,具体代码如下:

@GetMapping("/list")
public Map<String, Object> testList() {
    // 插入两个列表,注意它们在链表的顺序
    // 链表从左到右顺序为v10,v8,v6,v4,v2 —— 对应链表的头插法
    stringRedisTemplate.opsForList().leftPushAll("list1", "v2", "v4", "v6", "v8", "v10");
    // 链表从左到右顺序为v1,v2,v3,v4,v5,v6 —— 对应链表的尾插法
    stringRedisTemplate.opsForList().rightPushAll("list2", "v1", "v2", "v3", "v4", "v5", "v6");
    // 绑定list2链表操作
    BoundListOperations listOps = stringRedisTemplate.boundListOps("list2");
    // 从右边弹出一个成员
    Object result1 = listOps.rightPop();
    // 获取定位元素,Redis从0开始计算,这里值为v2
    Object result2 = listOps.index(1);
    // 从左边插入链表
    listOps.leftPush("v0");
    // 求链表长度
    Long size = listOps.size();
    // 求链表下标区间成员,整个链表下标范围为0到size-1,这里不取最后一个元素
    List elements = listOps.range(0, size - 2);
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("success", true);
    return map;
}

2.4.3 操作集合数据类型

  对于集合,在Redis中是不允许集合中的元素有重复的,它在数据结构上是一个散列表的结构,所以对于它而言是无序的,对于两个或者两个以上的集合,Redis还提供了交集、并集和差集的运算。

  在RedisController的控制器类中创建名为testSet的方法,具体代码如下:

@GetMapping("/set")
public Map<String, Object> testSet() {
    // 请注意:这里v1重复2次,由于集合不允许重复,所以只是插入5个成员到集合中
    stringRedisTemplate.opsForSet().add("set1", "v1", "v1", "v2", "v3", "v4", "v5");
    stringRedisTemplate.opsForSet().add("set2", "v2", "v4", "v6", "v8");
    // 绑定set1集合操作
    BoundSetOperations setOps = stringRedisTemplate.boundSetOps("set1");
    // 增加两个元素
    setOps.add("v6", "v7");
    // 删除两个元素
    setOps.remove("v1", "v7");
    // 返回所有元素
    Set set1 = setOps.members();
    // 求成员数
    Long size = setOps.size();
    // 求交集
    Set inter = setOps.intersect("set2");
    // 求交集,并且用新集合inter保存
    setOps.intersectAndStore("set2", "inter");
    // 求差集
    Set diff = setOps.diff("set2");
    // 求差集,并且用新集合diff保存
    setOps.diffAndStore("set2", "diff");
    // 求并集
    Set union = setOps.union("set2");
    // 求并集,并且用新集合union保存
    setOps.unionAndStore("set2", "union");
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("success", true);
    return map;
}

2.4.4 操作有序集合类型

  有序集合与集合的差异并不大,它也是一种散列表存储的方式,同时它的有序性只是靠它在数据结构中增加了一个属性 —— score(分数)得以支持。为了支持这个变化,Spring提供了TypedTuple接口方法,并且Spring还提供了其默认的实现类DefaultTypedTuple。

  在TypedTuple接口的设计中,value是保存有序集合的值,score则是保存分数,Redis是使用分数来完成集合的排序的。

  在默认情况下,有序集合是从小到大地排序的,按下标、分数和值进行排序获取有序集合的元素,或者连同分数一起返回,有时还可以进行从大到小的排序,只是在使用排序时,我们可以使用SPring为我们创建的Range类,它可以定义值的范围,还有大于、等于、大于等于、小于等于等范围定义,方便筛选对应元素。

  在RedisController的控制器类中创建名为testSet的方法,具体代码如下:

@GetMapping("/zset")
public Map<String, Object> testZset() {
    Set<TypedTuple<String>> typedTupleSet = new HashSet<>();
    for (int i = 1; i <= 9; i++) {
        // 分数
        double score = i * 0.1;
        // 创建一个TypedTuple对象,存入值和分数
        TypedTuple<String> typedTuple = new DefaultTypedTuple<String>("value" + i, score);
        typedTupleSet.add(typedTuple);
    }
    // 往有序集合插入元素
    stringRedisTemplate.opsForZSet().add("zset1", typedTupleSet);
    // 绑定zset1有序集合操作
    BoundZSetOperations<String, String> zsetOps = stringRedisTemplate.boundZSetOps("zset1");
    // 增加一个元素
    zsetOps.add("value10", 0.26);
    Set<String> setRange = zsetOps.range(1, 6);
    // 按分数排序获取有序集合
    Set<String> setScore = zsetOps.rangeByScore(0.2, 0.6);
    // 定义值范围
    Range range = new Range();
    range.gt("value3");// 大于value3
    // range.gte("value3");// 大于等于value3
    // range.lt("value8");// 小于value8
    range.lte("value8");// 小于等于value8
    // 按值排序,请注意这个排序是按字符串排序
    Set<String> setLex = zsetOps.rangeByLex(range);
    // 删除元素
    zsetOps.remove("value9", "value2");
    // 求分数
    Double score = zsetOps.score("value8");
    // 在下标区间下,按分数排序,同时返回value和score
    Set<TypedTuple<String>> rangeSet = zsetOps.rangeWithScores(1, 6);
    // 在分数区间下,按分数排序,同时返回value和score
    Set<TypedTuple<String>> scoreSet = zsetOps.rangeByScoreWithScores(1, 6);
    // 按从大到小排序
    Set<String> reverseSet = zsetOps.reverseRange(2, 8);
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("success", true);
    return map;
}

3、Redis的一些特殊用法

3.1 使用Redis事务

  Redis是支持一定事务能力的NoSQL。在Redis中使用事务,通常的命令组合是watch……multi……exec。也就是说要在一个Redis连接中执行多个命令,这时我们可以考虑使用SessionCallback接口来达到目的。

  watch命令是可以监控Redis的一些键。

  multi命令是开始事务。开始事务后,该客户端的命令不会马上被执行,而是存放在一个队列中,这点是需要注意的地方。也就是说在这时我们执行一些返回数据的命令,Redis也是不会马上执行的,而是把命令放入一个队列中,所以此时调用Redis的命令,结果都是返回null的,这时初学者容易犯的错误。

  exec命令的意义在于执行事务。只是它在队列命令执行前会判断被watch监控的Redis键的数据是否发生过变化(即使赋予之前相同的值也会被认为是变化过的),如果它认为发生了变化,那么Redis就会取消事务,否则就会执行事务。

  Redis在执行事务时,要么全部执行,要么全部不执行,而且不会被其他客户端打断,这样就保证了Redis事务下的数据一致性。

  为了测试Redis事务,在RedisController类中添加testMulti方法用于测试事务,具体代码如下:

@GetMapping("/testMulti")
public Map<String, Object> testMulti() {
    redisTemplate.opsForValue().set("key1", "value1");
    List list = (List) redisTemplate.execute(new SessionCallback() {
        @Override
        public Object execute(RedisOperations operations) throws DataAccessException {
            // 设置要监控key1
            operations.watch("key1");
            // 开启事务,在exec命令执行前,全部都只是进入队列
            operations.multi();
            operations.opsForValue().set("key2", "value2");
            operations.opsForValue().set("key1", "update_value1");// ①
            // 获取值将为null,因为redis只是把命令放入队列,
            Object value2 = operations.opsForValue().get("key2");
            System.out.println("命令在队列,所以value为null【" + value2 + "】");
            operations.opsForValue().increment("key1",1);    // ②
            operations.opsForValue().set("key3", "value3");
            Object value3 = operations.opsForValue().get("key3");
            System.out.println("命令在队列,所以value为null【" + value3 + "】");
            // 执行exec命令,将先判别key1是否在监控后被修改过,如果是不执行事务,否则执行事务
            return operations.exec();// ③
        }
    });
    System.out.println(list);
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("success", true);
    return map;
}

  接下来根据代码中的①、②、③处进行测试。

  测试一:将代码行①,代码行②注释掉,执行启动类,在浏览器内输入对应的URL后,查看结果发现key2和key3对应的值已经存入了Redis中。且由于multi后的命令不是直接执行而是进入队列的,因此在事务中获取key2和key3的值为null。在控制台和Redis客户端查看结果分别如下:
在这里插入图片描述
Spring Boot 2.x使用篇(四)—— Spring Boot与Redis_第4张图片
  测试二:将代码行①,代码行②注释掉,然后在代码③行添加断点,进入debug模式后在浏览器输入对应URL后,请求将抵达断点处。此时在Redis客户端通过命令修改key1的值为“update_value1”后,继续执行直至完成。此时发现key2和key3的值均为空(nil)。这就说明了当监控的键key1发生变化后,事务将不再执行。在控制台和Redis客户端查看结果分别如下:
在这里插入图片描述
Spring Boot 2.x使用篇(四)—— Spring Boot与Redis_第5张图片
  测试三:将代码行①注释去掉,代码行②仍然注释,去掉代码③行的断点。执行启动类,在浏览器内输入对应的URL后,发现事务正常执行。在控制台和Redis客户端查看结果分别如下:
在这里插入图片描述
Spring Boot 2.x使用篇(四)—— Spring Boot与Redis_第6张图片
  这里大家是否会有这样的疑问:为什么在代码行①处明明对监控的键key1的值进行了修改,为什么事务还能成功呢? 这里的确有一个坑,但是仔细想想其实事情并不是这样的,由于通过multi开启事务后,所有的命令都是进入队列的,而不是直接执行的。也就是说在代码行①处对key1的值的修改并没有执行,那么在事务执行时对key1的值进行判断是否发生变化时自然为否,则执行事务。而代码行①处的代码仅仅是作为事务中一条普通的命令执行了,只不过该命令是修改的key1的值而已。

  测试四:将代码行①注释保留,代码行②注释取消,去掉代码③行的断点。执行启动类,在浏览器内输入对应的URL后,由于代码行②处的命令是对字符串加一,显然这是不能运算的,因此在控制台会报异常。但当我们在Redis客户端查看key2和key3的值时,发现它们此时已经具有了值。在控制台和Redis客户端查看结果分别如下:
Spring Boot 2.x使用篇(四)—— Spring Boot与Redis_第7张图片
Spring Boot 2.x使用篇(四)—— Spring Boot与Redis_第8张图片
  这里需要注意的是: 这就是Redis事务和数据库事务的不一样的地方。对于Redis事务,是先让命令进入队列,所以一开始它并没有检测这个加一命令是否能够执行成功,只有在exec命令执行的时候,才能发现错误,对于出错的命令Redis只是报出错误,而错误后面的命令依旧被执行, 所以key2和key3的值都存在数据,这就是Redis事务的特点,也是使用Redis事务需要特别注意的地方。为了克服这个问题,一般我们要在执行Redis事务前,严格地检查数据,以免发生这样的情况。

3.2 使用Redis流水线

  在默认情况下,Redis客户端是一条一条命令发送给Redis服务器的,这样显然性能不高。因此Redis也可以使用流水线(pipeline)技术大幅度地在需要执行很多命令时提升Redis的性能。

  为了测试流水线技术,在RedisController中添加方法testPipeline,具体代码如下:

@GetMapping("/testPipeline")
public Map<String, Object> testPipeline() {
    Long start = System.currentTimeMillis();
    List list = (List) redisTemplate.executePipelined((RedisOperations operations) -> {
        for (int i = 1; i <= 100000; i++) {
            operations.opsForValue().set("pipeline_" + i, "value_" + i);
            String value = (String) operations.opsForValue().get("pipeline_" + i);
            if (i == 100000) {
                System.out.println("命令只是进入队列,所以值为空【" + value + "】");
            }
        }
        return null;
    });
    Long end = System.currentTimeMillis();
    System.out.println("耗时:" + (end - start) + "毫秒。");
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("success", true);
    return map;
}

  上述代码沿用了SessionCallback接口执行写入和读出各10万次Redis命令,并打印出耗时。

  这里需要注意以下两点:

  • 上述代码只用于测试,在运行如此多的命令时,需要考虑的另一个问题是内存空间的消耗。因为对于程序而言,它最终会返回一个List对象,如果过度的命令执行返回的结果都保存在这个List中,显然会造成内存消耗过大,尤其在那些高并发的网站中就很容易造成JVM内存溢出异常,这个时候应该考虑使用迭代的方法执行Redis命令。
  • 与事务一样,使用流水线的过程中,所有的命令也只是进入队列而没有执行,所以执行的命令返回值为空,这也是需要注意的地方。

3.3 使用Redis发布订阅

  首先是Redis提供一个渠道,让消息能够发送到这个渠道上,而多个系统可以监听这个渠道,如短信、微信和邮件系统都可以监听这个渠道,当一条消息发送到渠道,渠道就会通知它的监听者,这样短信,微信和邮件系统就能够得到这个渠道给它们的消息了,这些监听者会根据自己的需要去处理这个消息。

  为了接收Redis渠道发送过来的消息,需要在listener包中定义一个消息监听器,具体代码如下:

package com.ccff.springboot.demo.chapter7.listener;

import com.ccff.springboot.demo.chapter7.controller.RedisController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;

/**
 * Created by wangzhefeng01 on 2019/8/12.
 */
@Component
public class RedisMessageListener implements MessageListener {
    @Override
    public void onMessage(Message message, @Nullable byte[] pattern) {
        //消息体
        String messageBody = new String(message.getBody());
        //渠道名称
        String topic  = new String(pattern);
        System.out.println("消息体: "+messageBody);
        System.out.println("渠道名称: "+topic);
    }
}

  上述代码中的onMessage方法是得到消息后的处理方法,其中message参数代表Redis发送过来的消息,pattern是渠道名称。这里因为标注了注解@Component,所以在Spring Boot扫描后,会把它自动装配到IoC容器中。

  接着我们需要在Spring Boot启动类中配置其他信息,让系统能够监控Redis的消息,具体代码如下:

package com.ccff.springboot.demo.chapter7;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.Topic;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

import javax.annotation.PostConstruct;

/**
 * Created by wangzhefeng01 on 2019/8/9.
 */
@SpringBootApplication
public class Chapter7Application {
    //注入RedisTemplate
    @Autowired
    private RedisTemplate redisTemplate = null;

    //注入Redis连接工厂
    @Autowired
    private RedisConnectionFactory connectionFactory = null;

    //注入Redis消息监听器
    @Autowired
    private MessageListener redisMessageListener = null;

    //任务池
    private ThreadPoolTaskScheduler taskScheduler = null;

    //定义Spring自定义后初始化方法
    @PostConstruct
    public void init(){
        initRedisTemplate();
    }

    /**
     * 设置RedisTemplate的序列化器
     */
    private void initRedisTemplate() {
        RedisSerializer stringSerializer = redisTemplate.getStringSerializer();
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
    }

    /**
     * 创建任务池,运行线程等待处理Redis的消息
     * @return
     */
    @Bean
    public ThreadPoolTaskScheduler initTaskScheduler(){
        if (taskScheduler != null){
            return taskScheduler;
        }
        taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(20);
        return taskScheduler;
    }

    /**
     * 定义Redis的监听容器
     * @return
     */
    @Bean
    public RedisMessageListenerContainer initRedisContainer(){
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        //Redis连接工厂
        container.setConnectionFactory(connectionFactory);
        //设置运行任务池
        container.setTaskExecutor(initTaskScheduler());
        //定义监听渠道,名称为topic1
        Topic topic = new ChannelTopic("topic1");
        //使用监听器监听Redis的消息
        container.addMessageListener(redisMessageListener,topic);
        return container;
    }

    public static void main(String[] args) {
        SpringApplication.run(Chapter7Application.class, args);
    }
}

  在上面的代码中,RedisTemplate和RedisConnectionFactory对象都是Spring Boot自动创建的,所以这里只是把它们注入进来,只需要注解@Autowired即可。然后定义了一个任务池,并设置了任务池的大小为20,这样它将可以运行线程,等待Redis消息的传入。接着再定义了一个Redis消息监听容器RedisMessageListenerContainer,并往容器设置了Redis连接工厂和指定运行消息的线程池,定义了接收“topic1”渠道的消息,这样系统就可以监听Redis关于“topic1”渠道的消息了。

  启动Spring Boot项目后,在Redis的客户端输入命令:

publish topic1 message1

  在控制台可看到如下信息:
在这里插入图片描述
  在Spring在,我们也可以使用RedisTemplate来发送消息:

//channel:代表渠道
//message:代表消息
redisTemplate.convertAndSend(channel,message);

3.4 使用Lua脚本

  为了增强Redis的计算能力,Redis在2.6版本后提供了Lua脚本的支持,而且执行Lua脚本在Redis中还具备原子性,所以在需要保证数据一致性的高并发环境中,我们也可以使用Redis的Lua语言来保证数据的一致性,且Lua脚本具备更加强大的运算功能,在高并发需要保证数据一致性时,Lua脚本方案比使用Redis自身提供的事务要更好一些。

  在Redis中运行Lua的方法有如下两种:

  • 第一种是直接发送Lua到Redis服务器去执行
  • 第二种是先把Lua发送给Redis,Redis会对Lua脚本进行缓存,然后返回一个SHA1的32位编码回来,之后只需要发送SHA1和相关参数给Redis便可以执行了。

  这里需要解释的是为什么会存在通过32位编码执行的方法。如果Lua脚本很长,那么就需要通过网络传递脚本给Redis去执行了,而现实的情况是网络的传递速度往往跟不上Redis的执行速度,所以网络就会变成Redis的执行瓶颈。如果只传递32位编码和参数,那么需要传递的消息就少了许多,这样就可以极大地减少网络传输的内容,从而提高系统的性能。

  为了支持Redis的Lua脚本,Spring提供了RedisScript接口,与此同时也有一个DefaultRedisScript实现类。RedisScript接口具有如下三个主要方法:

  • String getSha1():获取脚本的Sha1
  • Class< T > getResultType():获取脚本返回值
  • String getScriptAsString():获取脚本的字符串

  在RedisController中添加一个执行十分简单的Lua脚本的测试方法testLua,具体代码如下:

@GetMapping("/lua")
public Map<String, Object> testLua() {
    DefaultRedisScript<String> rs = new DefaultRedisScript<String>();
    // 设置脚本
    rs.setScriptText("return 'Hello Redis'");
    // 定义返回类型,注意如果没有这个定义Spring不会返回结果
    rs.setResultType(String.class);
    RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();
    // 执行Lua脚本
    String str = (String) redisTemplate.execute(rs, stringSerializer, stringSerializer, null);
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("str", str);
    return map;
}

  在上面的代码中,首先Lua只是定义了一个简单的字符串,然后就返回了,而返回的类型则定义为字符串。这里必须定义返回类型,否则Spring不会把脚本执行的结果返回。 接着获取了由RedisTemplate自动创建的字符串序列化器,而后使用RedisTemplate的execute方法执行了脚本。在RedisTemplate中,execute方法执行脚本的方法有如下两种:

public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) { /* compiled code */ }

public <T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer, List<K> keys, Object... args) { /* compiled code */ }

  在这两个方法中,从参数的名称可以知道:

  • script:就是我们定义的RedisScript接口对象
  • keys:代表Redis的键
  • args:是这段Lua脚本的参数
  • argsSerializer:键的序列化器
  • resultSerializer:参数序列化器

  上述两个方法最大区别就是一个存在序列化器的参数,另一个不存在。对于不存在序列化参数的方法,Spring将采用RedisTemplate提供的valueSerializer序列化器对传递的键和参数进行序列化。

  上面的Lua脚本十分简单,下面考虑存在参数的情况,例如下面的Lua脚本,用来判断两个字符串是否相同,具体如下:

redis.call('set', KEYS[1], ARGV[1]) 
redis.call('set', KEYS[2], ARGV[2]) 
local str1 = redis.call('get', KEYS[1]) 
local str2 = redis.call('get', KEYS[2]) 
if str1 == str2 then  
eturn 1 
end 
return 0 

  这里的脚本中使用了两个键来保存两个参数,然后对这两个参数进行比较,如果相等则返回1,否则返回0。注意脚本中的KEYS[1]和KEYS[2]的写法,它们代表客户端传递的第一个键和第二个键,而ARGV[1]和ARGV[2]则表示客户端传递的第一个和第二个参数。

  在RedisController中添加执行带有参数的Lua脚本的测试方法testLua2,具体代码如下:

@GetMapping("/lua2")
public Map<String, Object> testLua2(String key1, String key2, String value1, String value2) {
    // 定义Lua脚本
    String lua = " redis.call('set', KEYS[1], ARGV[1]) \n"
            + " redis.call('set', KEYS[2], ARGV[2]) \n"
            + " local str1 = redis.call('get', KEYS[1]) \n"
            + " local str2 = redis.call('get', KEYS[2]) \n"
            + " if str1 == str2 then  \n" + "return 1 \n"
            + " end \n"
            + " return 0 \n";
    System.out.println(lua);
    // 结果返回为Long
    DefaultRedisScript<Long> rs = new DefaultRedisScript<Long>();
    rs.setScriptText(lua);
    rs.setResultType(Long.class);
    // 采用字符串序列化器
    RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();
    // 定义key参数
    List<String> keyList = new ArrayList<>();
    keyList.add(key1);
    keyList.add(key2);
    // 传递两个参数值,其中第一个序列化器是key的序列化器,第二个序列化器是参数的序列化器
    Long result = (Long) redisTemplate.execute(rs, stringSerializer, stringSerializer, keyList, value1, value2);
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("result", result);
    return map;
}

  在上述代码中使用了keyList保存了各个键,然后通过Redis的execute方法传递,参数则可以使用可变化的方式传递,且设置了给键和参数的序列化器都是字符串序列化器,这样便能够运行这段脚本了。脚本返回一个数字,这里值得注意的是:因为Java会把整数当做长整型(Long),所以这里的返回值设置为Long。
Spring Boot 2.x使用篇(四)—— Spring Boot与Redis_第9张图片

你可能感兴趣的:(Java)