查看原文
其他

芋道 Spring Boot Redis 入门(上)

老艿艿 芋道源码 2020-02-12

点击上方“芋道源码”,选择“设为星标

做积极的人,而不是积极废人!

源码精品专栏

 


摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/Redis/ 「芋道源码」欢迎转载,保留摘要,谢谢!


  • 1. 概述

  • 2. 快速入门

    • 2.1 引入依赖

    • 2.2 配置文件

    • 2.3 简单测试

    • 2.4 RedisTemplate

  • 3. 序列化

    • 3.1 RedisSerializer

    • 3.2 配置序列化方式

    • 3.3 自定义 RedisSerializer 实现类


1. 概述

在快速入门 Spring Boot 整合 Redis 之前,我们先来做个简单的了解。在 Spring 的生态中,我们使用 Spring Data Redis 来实现对 Redis 的数据访问。

可能这个时候,会有胖友会有疑惑,市面上已经有 Redis、Redisson、Lettuce 等优秀的 Java Redis 工具库,为什么还要有 Spring Data Redis 呢?学不动了,头都要秃了!不要慌,我们先来看一张图:

Spring Data Redis 调用
  • 对于下层,Spring Data Redis 提供了统一的操作模板(后文中,我们会看到是 RedisTemplate 类),封装了 Jedis、Lettuce 的 API 操作,访问 Redis 数据。所以,实际上,Spring Data Redis 内置真正访问的实际是 Jedis、Lettuce 等 API 操作

  • 对于上层,开发者学习如何使用 Spring Data Redis 即可,而无需关心 Jedis、Lettuce 的 API 操作。甚至,未来如果我们想将 Redis 访问从 Jedis 迁移成 Lettuce 来,无需做任何的变动。😈 相信很多胖友,在选择 Java Redis 工具库,也是有过烦恼的。

  • 目前,Spring Data Redis 暂时只支持 Jedis、Lettuce 的内部封装,而 Redisson 是由 redisson-spring-data 来提供。

OK ,哔哔结束,我们先来快速上手下 Spring Data Redis 的使用。

2. 快速入门

示例代码对应仓库:spring-data-redis-with-jedis 。

在 spring-boot-starter-data-redis 项目 2.X 中,默认使用 Lettuce 作为 Java Redis 工具库,猜测是因为 Jedis 中间有一段时间诈尸,基本不太更新。

感兴趣的胖友可以看看 https://mvnrepository.com/artifact/redis.clients/jedis 地址,会发现 2016 年到 2018 年的 Jedis 更新频率。所幸,2018 年底又突然复活了。

考虑到自己项目中,使用 Jedis 为主,并且问了几个朋友,都是使用 Jedis ,并且有吐槽 Lettuce 坑多多,所以个人推荐的话,生产中还是使用 Jedis ,稳定第一。也因此,本节我们是 Spring Data Redis + Jedis 的组合。

同时,艿艿目前使用的 SkyWalking 中间件,暂时只支持 Jedis 的自动化的追踪,那么更加考虑使用 Jedis 啦。

这里在分享一个 Jedis 和 Lettuce 的对比讨论。

2.1 引入依赖

在 pom.xml 文件中,引入相关依赖。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.3.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>

    <!-- 实现对 Spring Data Redis 的自动化配置 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <exclusions>
            <!-- 去掉对 Lettuce 的依赖,因为 Spring Boot 优先使用 Lettuce 作为 Redis 客户端 -->
            <exclusion>
                <groupId>io.lettuce</groupId>
                <artifactId>lettuce-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <!-- 引入 Jedis 的依赖,这样 Spring Boot 实现对 Jedis 的自动化配置 -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>

    <!-- 方便等会写单元测试 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- 等会示例会使用 fastjson 作为 JSON 序列化的工具 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.61</version>
    </dependency>

    <!-- Spring Data Redis 默认使用 Jackson 作为 JSON 序列化的工具 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>

</dependencies>

具体每个依赖的作用,胖友自己认真看下艿艿添加的所有注释噢。

2.2 配置文件

在 application.yml 中,添加 Redis 配置,如下:

spring:
  # 对应 RedisProperties 类
  redis:
    host: 127.0.0.1
    port: 6379
    password: # Redis 服务器密码,默认为空。生产中,一定要设置 Redis 密码!
    database: 0 # Redis 数据库号,默认为 0 。
    timeout: 0 # Redis 连接超时时间,单位:毫秒。
    # 对应 RedisProperties.Jedis 内部类
    jedis:
      pool:
        max-active: 8 # 连接池最大连接数,默认为 8 。使用负数表示没有限制。
        max-idle: 8 # 默认连接数最小空闲的连接数,默认为 8 。使用负数表示没有限制。
        min-idle: 0 # 默认连接池最小空闲的连接数,默认为 0 。允许设置 0 和 正数。
        max-wait: -1 # 连接池最大阻塞等待时间,单位:毫秒。默认为 -1 ,表示不限制。

具体每个参数的作用,胖友自己认真看下艿艿添加的所有注释噢。

2.3 简单测试

创建 Test01 测试类,我们来测试一下简单的 SET 指令。代码如下:

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

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void testStringSetKey() {
        stringRedisTemplate.opsForValue().set("yunai""shuai");
    }
}

通过 StringRedisTemplate 类,我们进行了一次 Redis SET 指令的执行。关于 StringRedisTemplate 是什么,我们先卖个关子,在 「2.4 RedisTemplate」 中来介绍。

我们先来执行下 #testStringSetKey() 方法这个测试方法。执行完成后,我们在控制台查询,看看是否真的执行成功了。

$ redis-cli get yunai
"shuai"
  • 请大声的告诉我,Redis 是怎么夸奖 "yunai" 的,哈哈哈哈。

2.4 RedisTemplate

org.springframework.data.redis.core.RedisTemplate<K, V> 类,从类名上,我们就明明白白知道,提供 Redis 操作模板 API 。核心属性如下:

// RedisTemplate.java
// 艿艿省略了一些不重要的属性。

// <1> 序列化相关属性
@SuppressWarnings("rawtypes"private @Nullable RedisSerializer keySerializer = null;
@SuppressWarnings("rawtypes"private @Nullable RedisSerializer valueSerializer = null;
@SuppressWarnings("rawtypes"private @Nullable RedisSerializer hashKeySerializer = null;
@SuppressWarnings("rawtypes"private @Nullable RedisSerializer hashValueSerializer = null;
private RedisSerializer<String> stringSerializer = RedisSerializer.string();

// <2> Lua 脚本执行器
private @Nullable ScriptExecutor<K> scriptExecutor;

// <3> 常见数据结构操作类
// cache singleton objects (where possible)
private @Nullable ValueOperations<K, V> valueOps;
private @Nullable ListOperations<K, V> listOps;
private @Nullable SetOperations<K, V> setOps;
private @Nullable ZSetOperations<K, V> zSetOps;
private @Nullable GeoOperations<K, V> geoOps;
private @Nullable HyperLogLogOperations<K, V> hllOps;
  • <1> 处,看到了四个序列化相关的属性,用于 KEY 和 VALUE 的序列化。

    • 例如说,我们在使用 POJO 对象存储到 Redis 中,一般情况下,会使用 JSON 方式序列化成字符串,存储到 Redis 中。详细的,我们在 「3. 序列化」 小节中来说明。

    • 在上文中,我们看到了 `org.springframework.data.redis.core.StringRedisTemplate` 类,它继承 RedisTemplate 类,使用 `org.springframework.data.redis.serializer.StringRedisSerializer` 字符串序列化方式。直接点开 StringRedisSerializer 源码,看下它的构造方法,瞬间明明白白。

  • <2> 处,Lua 脚本执行器,提供 Redis scripting API 操作。

  • <3> 处,Redis 常见数据结构操作类。

    • ValueOperations 类,提供 Redis String API 操作。

    • ListOperations 类,提供 Redis List API 操作。

    • SetOperations 类,提供 Redis Set API 操作。

    • ZSetOperations 类,提供 Redis ZSet(Sorted Set) API 操作。

    • GeoOperations 类,提供 Redis Geo API 操作。

    • HyperLogLogOperations 类,提供 Redis HyperLogLog API 操作。

那么 Pub/Sub、Transaction、Pipeline、Keys、Cluster、Connection 等相关的 API 操作呢?它在 RedisTemplate 自身提供,因为它们不属于具体每一种数据结构,所以没有封装在对应的 Operations 类中。哈哈哈,胖友打开 RedisTemplate 类,去瞅瞅,妥妥的明白。

3. 序列化

艿艿:为了尽量把序列化说的清楚一些,所以本小节内容会略长。

因为有些地方,直接撸源码,比吓哔哔一段话更易懂,所以会有一些源码,保持淡定。

3.1 RedisSerializer

org.springframework.data.redis.serializer.RedisSerializer 接口,Redis 序列化接口,用于 Redis KEY 和 VALUE 的序列化。简化代码如下:

// RedisSerializer.java
public interface RedisSerializer<T> {

    @Nullable
    byte[] serialize(@Nullable T t) throws SerializationException;

    @Nullable
    T deserialize(@Nullable byte[] bytes) throws SerializationException;

}
  • 定义了对象 和二进制数组的转换。

  • 啊,可能有胖友会有疑惑了:我们在 redis-cli 终端,看到的不都是字符串么,怎么这里是序列化成二进制数组呢?实际上,Redis Client 传递给 Redis Server 是传递的 KEY 和 VALUE 都是二进制值数组。好奇的胖友,可以打开 Jedis `Connection#sendCommand(final Command cmd, final byte[]… args)` 方法,传入的参数就是二进制数组,而 cmd 命令也会被序列化成二进制数组。

RedisSerializer 的实现类,如下图:

Spring Data Redis 调用

主要分成四类:

  • JDK 序列化方式

  • String 序列化方式

  • JSON 序列化方式

  • XML 序列化方式

3.1.1 JDK 序列化方式

org.springframework.data.redis.serializer.JdkSerializationRedisSerializer ,默认情况下,RedisTemplate 使用该数据列化方式。具体的,可以看看 RedisTemplate#afterPropertiesSet() 方法,在 RedisTemplate 未设置序列化的情况下,使用 JdkSerializationRedisSerializer 作为序列化实现。在 Spring Boot 自动化配置 RedisTemplate Bean 对象时,就未设置。

绝大多数情况下,可能 99.9999% ,我们不会使用 JdkSerializationRedisSerializer 进行序列化。为什么呢?我们来看一个示例,代码如下:

// Test01.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class Test01 {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testStringSetKey02() {
        redisTemplate.opsForValue().set("yunai""shuai");
    }

}

我们先来执行下 #testStringSetKey02() 方法这个测试方法。注意,此处我们使用的是 RedisTemplate 而不是 StringRedisTemplate 。执行完成后,我们在控制台查询,看看是否真的执行成功了。

# 在 `redis-cli` 终端中

127.0.0.1:6379> scan 0
1) "0"
2) 1) "\xac\xed\x00\x05t\x00\x05yunai"

127.0.0.1:6379> get "\xac\xed\x00\x05t\x00\x05yunai"
"\xac\xed\x00\x05t\x00\x05shuai"
  • 通过 Redis SCAN 命令,我们扫描出了一个奇怪的 "yunai" KEY ,前面带着奇怪的 16 进制字符。而后,我们使用这个奇怪的 KEY 去获取对应的 VALUE ,结果前面也是一串奇怪的 16 进制字符。

    具体为什么是这样一串奇怪的 16 进制,胖友可以看看 ObjectOutputStream#writeString(String str, boolean unshared) 的代码,实际就是标志位 + 字符串长度 + 字符串内容。

对于 KEY 被序列化成这样,我们线上通过 KEY 去查询对应的 VALUE 势必会非常不方便,所以 KEY 肯定是不能被这样序列化的。

对于 VALUE 被序列化成这样,除了阅读可能困难一点,不支持跨语言外,实际上也没啥问题。不过,实际线上场景,还是使用 JSON 序列化居多。

3.1.2 String 序列化方式

① org.springframework.data.redis.serializer.StringRedisSerializer ,字符串和二进制数组的直接转换。代码如下:

// StringRedisSerializer.java

private final Charset charset;

@Override
public String deserialize(@Nullable byte[] bytes) {
    return (bytes == null ? null : new String(bytes, charset));
}

@Override
public byte[] serialize(@Nullable String string) {
    return (string == null ? null : string.getBytes(charset));
}
  • 是不是很直接简单。

绝大多数情况下,我们 KEY 和 VALUE 都会使用这种序列化方案。而 VALUE 的序列化和反序列化,自己在逻辑调用 JSON 方法去序列化。为什么呢?继续往下看。

② org.springframework.data.redis.serializer.GenericToStringSerializer<T> ,使用 Spring ConversionService 实现 <T> 对象和 String 的转换,从而 String 和二进制数组的转换。

例如说,序列化的过程,首先 <T> 对象通过 ConversionService 转换成 String ,然后 String 再序列化成二进制数组。反序列化的过程,胖友自己结合源码思考下 🤔 。

当然,GenericToStringSerializer 貌似基本不会去使用,所以不用去了解也问题不大,哈哈哈。

3.1.3 JSON 序列化方式

① org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer ,使用 Jackson 实现 JSON 的序列化方式,并且从 Generic 单词可以看出,是支持所有类。怎么体现呢?参见构造方法的代码:

// GenericJackson2JsonRedisSerializer.java

public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) {

    this(new ObjectMapper());

    // simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here since we need
    // the type hint embedded for deserialization using the default typing feature.
    mapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName)));

    // <1>
    if (StringUtils.hasText(classPropertyTypeName)) {
        mapper.enableDefaultTypingAsProperty(DefaultTyping.NON_FINAL, classPropertyTypeName);
    // <2>
    } else {
        mapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY);
    }
}
  • <1> 处,如果传入了 classPropertyTypeName 属性,就是使用使用传入对象的 classPropertyTypeName 属性对应的值,作为默认类型(Default Typing)。

  • <2> 处,如果未传入 classPropertyTypeName 属性,则使用传入对象的类全名,作为默认类型(Default Typing)。

那么,胖友可能会问题,什么是默认类型(Default Typing)呢?我们来思考下,在将一个对象序列化成一个字符串,怎么保证字符串反序列化成对象的类型呢?Jackson 通过 Default Typing ,会在字符串多冗余一个类型,这样反序列化就知道具体的类型了。来举个例子,使用我们等会示例会用到的 UserCacheObject 类。

  • 标准序列化的结果,如下:

    {
        "id"1,
        "name""芋道源码",
        "gender"1
    }
  • 使用 Jackson Default Typing 机制序列化的结果,如下:

     {
            "@class""cn.iocoder.springboot.labs.lab10.springdatarediswithjedis.cacheobject.UserCacheObject",
            "id"1,
            "name""芋道源码",
            "gender"1
        }
    • 看 @class 属性,反序列化的对象的类型不就有了么?

下面我们来看一个 GenericJackson2JsonRedisSerializer 的示例。在看之前,胖友先跳到 「3.2 配置序列化方式」 小节,来看看如何配置 GenericJackson2JsonRedisSerializer 作为 VALUE 的序列化方式。然后,马上调回到此处。

示例代码如下:

// Test01.java

@Autowired
private RedisTemplate redisTemplate;

@Test
public void testStringSetKeyUserCache() {
    UserCacheObject object = new UserCacheObject()
            .setId(1)
            .setName("芋道源码")
            .setGender(1); // 男
    String key = String.format("user:%d", object.getId());
    redisTemplate.opsForValue().set(key, object);
}

@Test
public void testStringGetKeyUserCache() {
    String key = String.format("user:%d"1);
    Object value = redisTemplate.opsForValue().get(key);
    System.out.println(value);
}

胖友分别执行 #testStringSetKeyUserCache() 和 #testStringGetKeyUserCache() 方法,然后对着 Redis 的结果看看,比较简单,就不多哔哔了。

我们在回过头来看看 @class 属性,它看似完美解决了反序列化后的对象类型,但是带来 JSON 字符串占用变大,所以实际项目中,我们也并不会采用 Jackson2JsonRedisSerializer 类。

② org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer<T> ,使用 Jackson 实现 JSON 的序列化方式,并且显示指定 <T> 类型。代码如下:

// Jackson2JsonRedisSerializer.java
public class Jackson2JsonRedisSerializer<T> implements RedisSerializer<T> {
    // ... 省略不重要的代码

    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    /**
     * 指定类型,和 <T> 要一致。
     */

    private final JavaType javaType;

    private ObjectMapper objectMapper = new ObjectMapper();

}

因为 Jackson2JsonRedisSerializer 序列化类里已经声明了类型,所以序列化的 JSON 字符串,无需在存储一个 @class 属性,用于存储类型。

但是,我们抠脚一想,如果使用 Jackson2JsonRedisSerializer 作为序列化实现类,那么如果我们类型比较多,岂不是每个类型都要定义一个 RedisTemplate Bean 了?!所以实际场景下,我们也并不会使用 Jackson2JsonRedisSerializer 类。😈

③ com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer ,使用 FastJSON 实现 JSON 的序列化方式,和 GenericJackson2JsonRedisSerializer 一致,就不重复赘述。

注意,GenericFastJsonRedisSerializer 不是 Spring Data Redis 内置实现,而是由于 FastJSON 自己实现。

④ com.alibaba.fastjson.support.spring.FastJsonRedisSerializer<T> ,使用 FastJSON 实现 JSON 的序列化方式,和 Jackson2JsonRedisSerializer 一致,就不重复赘述。

注意,GenericFastJsonRedisSerializer 不是 Spring Data Redis 内置实现,而是由于 FastJSON 自己实现。

3.1.4 XML 序列化方式

org.springframework.data.redis.serializer.OxmSerializer ,使用 Spring OXM 实现将对象和 String 的转换,从而 String 和二进制数组的转换。

因为 XML 序列化方式,暂时没有这么干过,我自己也没有,所以就直接忽略它吧。😝

3.2 配置序列化方式

创建 RedisConfiguration 配置类,代码如下:

@Configuration
public class RedisConfiguration {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 创建 RedisTemplate 对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。
        template.setConnectionFactory(factory);

        // 使用 String 序列化方式,序列化 KEY 。
        template.setKeySerializer(RedisSerializer.string());

        // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
        template.setValueSerializer(RedisSerializer.json());
        return template;
    }

}
  • RedisSerializer#string() 静态方法,返回的就是使用 UTF-8 编码的 StringRedisSerializer 对象。代码如下:

    // RedisSerializer.java
    static RedisSerializer<String> string() {
        return StringRedisSerializer.UTF_8;
    }

    // StringRedisSerializer.java
    public static final StringRedisSerializer ISO_8859_1 = new StringRedisSerializer(StandardCharsets.ISO_8859_1);
  • RedisSerializer#json() 静态方法,返回 GenericJackson2JsonRedisSerializer 对象。代码如下:

    // RedisSerializer.java

    static RedisSerializer<Object> json() {
        return new GenericJackson2JsonRedisSerializer();
    }

3.3 自定义 RedisSerializer 实现类

我们直接以 GenericFastJsonRedisSerializer 举例子,直接莽源码。代码如下:

// GenericFastJsonRedisSerializer.java

public class GenericFastJsonRedisSerializer implements RedisSerializer<Object> {
    private final static ParserConfig defaultRedisConfig = new ParserConfig();
    static { defaultRedisConfig.setAutoTypeSupport(true);}

    public byte[] serialize(Object object) throws SerializationException {
        // 空,直接返回空数组
        if (object == null) {
            return new byte[0];
        }
        try {
            // 使用 JSON 进行序列化成二进制数组,同时通过 SerializerFeature.WriteClassName 参数,声明写入类全名。
            return JSON.toJSONBytes(object, SerializerFeature.WriteClassName);
        } catch (Exception ex) {
            throw new SerializationException("Could not serialize: " + ex.getMessage(), ex);
        }
    }

    public Object deserialize(byte[] bytes) throws SerializationException {
        // 如果为空,则返回空对象
        if (bytes == null || bytes.length == 0) {
            return null;
        }
        try {
            // 使用 JSON 解析成对象。
            return JSON.parseObject(new String(bytes, IOUtils.UTF8), Object.class, defaultRedisConfig);
        } catch (Exception ex) {
            throw new SerializationException("Could not deserialize: " + ex.getMessage(), ex);
        }
    }
}

完成自定义 RedisSerializer 配置类后,我们就可以参照 「3.2 配置序列化方式」 小节,将 VALUE 序列化的修改成我们的,哈哈哈。



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

已在知识星球更新源码解析如下:


如果你喜欢这篇文章,喜欢,转发。

生活很美好,明天见(。・ω・。)ノ♡

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存