SpringBoot2 | 第十四篇:整合Lettuce Redis

Spring Boot 除了支持常见的 ORM 框架外,更是对常用的中间件提供了非常好封装,随着Spring Boot2.x的到来,支持的组件越来越丰富,也越来越成熟,其中对Redis的支持不仅仅是丰富了它的API,更是 替换掉底层 Jedis的依赖,取而代之 换成了Lettuce(生菜)

[TOC]

Redis介绍

Redis 是一个开源的使用 ANSIC 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。相比 Memcached它支持存储的类型相对更多包括string(字符串)、list(链表、set(集合)、zset(sorted set –有序集合)和 hash(哈希类型),GEO,同时Redis是线程安全的。2010年3月15日起,Redis 的开发工作由 VMware 主持,2013年5月开始,Redis的开发由 Pivotal赞助。

Lettuce

LettuceJedis 的都是连接Redis Server的客户端程序。Jedis实现上是直连 redis server,多线程环境下非线程安全,除非使用连接池,为每个Jedis实例增加物理连接Lettuce基于Netty的连接实例(StatefulRedisConnection),可以在多个线程间并发访问,且线程安全,满足多线程环境下的并发访问,同时它是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例

环境/版本一览:

  • 开发工具:Intellij IDEA 2018.2.2
  • springboot: 2.0.5.RELEASE
  • jdk:1.8.0_171
  • maven:3.3.9
  • lettuce:5.0.5.RELEASE
  • commons-pool2:2.5.0

1、pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<dependencies>
<!-- web starter关联依赖 spring-boot-starter-json -->
<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>
<!-- 整合Lettuce Redis需要commons-pool2 -->
<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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
</dependency>
</dependencies>

2、application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
redis:
host: localhost
password: 123456
# Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0
database: 1
lettuce:
pool:
# 当池耗尽时,在引发异常之前连接分配可以阻塞的最长时间(使用负值表示没有限制) 默认 -1
max-wait: -1ms
# 连接池最大连接数(使用负值表示没有限制) 默认 8
max-active: 8
# 连接池中的最大空闲连接 默认 8
max-idle: 8
# 连接池中的最小空闲连接 默认 0
min-idle: 0
# 连接超时时间
timeout: 10000ms

3、config

默认情况下的模板只能支持RedisTemplate<String, String>,也就是只能存入字符串,这在开发中是不友好的,所以自定义模板是很有必要的,当自定义了模板又想使用String存储这时候就可以使用StringRedisTemplateRedisTemplate<String, String>这两种方式,它们并不冲突…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.fatal.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.io.Serializable;

/**
* Redis 配置类
* @author: Fatal
* @date: 2018/10/13 0013 14:52
*/
@Configuration
public class RedisConfig {

/**
* 自定义RedisTemplate模板
*/
@Bean
public RedisTemplate<String, Serializable> serializableRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer()); // key序列化器
template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // value序列化器
template.setConnectionFactory(redisConnectionFactory);
return template;
}

}

4、entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.fatal.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import java.io.Serializable;

/**
* User 实体
* @author: Fatal
* @date: 2018/10/13 0013 14:47
*/
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor // 反序列化需要无参构造器
public class User implements Serializable {

private Long id;
private String username;
private String password;

}

5、Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package com.fatal;

import com.fatal.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.Serializable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class Chapter14ApplicationTests {

@Autowired
private StringRedisTemplate stringRedisTemplate;

/**
* RedisTemplate 的泛型默认只能支持 <String, String>
* 如果你写别的泛型如 <String, Object>,它会提示你 Could not autowire.
* 所以,我们才需要自定义一个能存储其他类型的模板
*/
@Autowired
private RedisTemplate<String, String> redisTemplate;

@Autowired
private RedisTemplate<String, Serializable> serializableRedisTemplate;


/**
* 测试线程安全(模拟10000并发)
*/
@Test
public void testThreadSecurity() throws InterruptedException {
// 该闩锁主要用于使main线程处于等待状态,防止提前关闭redis连接
CountDownLatch latch = new CountDownLatch(10000);
// 拦住所有线程,等计数为 0 时,同时释放
CountDownLatch concurrentLatch = new CountDownLatch(10000);
// 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
ExecutorService executorService = Executors.newFixedThreadPool(10000);
IntStream.range(0, 10000).forEach(i ->
executorService.execute(() -> {
concurrentLatch.countDown();
latch.countDown();
try {
concurrentLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread = Thread.currentThread();
log.info("【thread】 = {}", thread.getName());
stringRedisTemplate.opsForValue().increment("fatal", 1);
})
);

// 使当前线程等待直到闩锁计数为零,除非线程被中断。
latch.await();
String value = stringRedisTemplate.opsForValue().get("fatal");
log.info("【fatal】 = {}", value);
}

/**
* 测试模板 -> StringRedisTemplate
*/
@Test
public void testStringRedisTemplate() {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String key = "name";
ops.set(key, "米彩");
String name = ops.get(key);
log.info("测试模板 -> StringRedisTemplate 【字符串缓存结果】 = {}", name);
}

/**
* 测试默认模板 -> RedisTemplate<String, String>
*/
@Test
public void testDefaultRedisTemplate() {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
String key = "name";
ops.set(key, "米琪");
String name = ops.get(key);
log.info("测试默认模板 -> RedisTemplate<String, String> 【字符串缓存结果】 = {}", name);
}

/**
* 测试自定义模板 -> RedisTemplate<String, Serializable>
*/
@Test
public void testUser() {
ValueOperations<String, Serializable> ops = serializableRedisTemplate.opsForValue();
User user = new User(1l, "米彩", "18");
String key = "fatal:user";
ops.set(key, user);
User result = (User)ops.get(key);
log.info("测试自定义模板 -> RedisTemplate<String, Serializable>【对象缓存结果】 = {}", result);
}

}

6、控制台

  1. 测试模板 -> StringRedisTemplate

    1560678030661

  2. 测试默认模板 -> RedisTemplate<String, String>

    1560678101531

  3. 测试自定义模板 -> RedisTemplate<String, Serializable>

    控制台

    1560678205019

    redis 图形化界面

    1560678205019

Redis其它操作方式

下列的就是Redis其它类型所对应的操作方式

  • opsForValue: 对应 String(字符串)
  • opsForZSet: 对应 ZSet(有序集合)
  • opsForHash: 对应 Hash(哈希)
  • opsForList: 对应 List(列表)
  • opsForSet: 对应 Set(集合)
  • opsForGeo: 对应 GEO(地理位置)

参考资料

spring-data-redis文档
Redis 文档
Redis 中文文档
一起来学SpringBoot | 第九篇:整合Lettuce Redis

总结

  1. 如果少 spring-boot-starter-web 依赖,会报错

    1
    Caused by: java.lang.ClassNotFoundException: com.fasterxml.jackson.databind.JsonSerializer

    1539417560095

  2. 如果少 commons-pool2 依赖,会报错

    1
    Caused by: java.lang.ClassNotFoundException: org.apache.commons.pool2.impl.GenericObjectPoolConfig

    1539417633847

  3. 反序列化的时候,实体类必须含有 无参构造方法

    1
    Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.fatal.entity.User`

    1539417822756

  4. 序列化

    1、序列化是干什么的?
    ​ 简单说就是为了保存在内存中的各种对象的 状态 而非行为(也就是实例变量,不是方法),并且可以把保存的对象状态再读出来。虽然你可以用你自己的各种各样的方法来保存object states,但是Java给你提供一种应该比你自己好的保存对象状态的机制,那就是 序列化

    2、什么情况下需要序列化
    ​ a)当你想把的内存中的对象状态保存到一个文件的时候;
    ​ b)当你想用套接字在 网络上传送对象 的时候;
    ​ c)当你想通过 RMI 传输对象 的时候;

    3、相关注意事项
    ​ a)序列化时,只对对象的状态进行保存,而不管对象的方法;
    ​ b)当一个父类实现序列化,子类自动实现序列化,不需要显式实现 Serializable接口
    ​ c)当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化。

SpringBoot的知识已经有前辈在我们之前探索了。比较喜欢的博主有:唐亚峰 | Battcn方志朋的专栏程序猿DD纯洁的微笑。对这门技术感兴趣的可以去他们的博客逛逛。谢谢他们的分享~~

以上文章是我用来学习的Demo,都是基于 SpringBoot2.x 版本。

源码地址: https://github.com/ynfatal/springboot2-learning/tree/master/chapter14

学习 唐亚峰 前辈的经验