Redis 学习笔记 2:Java 客户端
常见的 Redis Java 客户端有三种:
- Jedis,优点是API 风格与 Redis 命令命名保持一致,容易上手,缺点是连接实例是线程不安全的,多线程场景需要用线程池来管理连接。
- Redisson,在Redis基础上实现了分布式的可伸缩的java数据结构,例如Map、Queue等,而且支持跨进程的同步机制:Lock、Semaphore等待,比较适合用来实现特殊的功能需求。
- lettuce,基于 Netty 实现,支持同步/异步和响应式编程,并且是线程安全的。支持 Redis 的哨兵模式、集群模式和管道模式。
Spring 对 Jedis 和 lettuce 进行了封装,spring-data-redis 提供统一的 API 进行操作。
Jedis
单个连接
下面是一个简单的 Jedis 连接示例。
创建一个 mvn 工程,并添加 Jedis 和 Junit 依赖:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.0.0</version>
</dependency>
编写一个单元测试:
public class AppTests {
private Jedis jedis;
@BeforeEach
public void beforeEach() {
jedis = new Jedis("192.168.0.88", 6379);
jedis.auth("123321");
jedis.select(0);
}
@Test
public void testString() {
String res = jedis.set("name", "Jack");
System.out.println(res);
res = jedis.get("name");
System.out.println(res);
}
@Test
public void testHash() {
jedis.hset("user:1", "name", "Jack");
jedis.hset("user:1", "age", "18");
Map<String, String> map = jedis.hgetAll("user:1");
System.out.println(map);
}
@AfterEach
public void afterEach() {
if (jedis != null) {
jedis.close();
}
}
}
在这个单元测试中,展示了如何使用 Jedis 客户端连接 Redis,并用 API 操作 String 类型和 Hash 类型的数据。基本上,这些 API 的命名和使用方式与前文介绍的 Redis 命令是相似的。
连接池
创建一个 Jedis 连接池的工具类:
public class JedisConnectionFactory {
// Jedis 连接池
private static JedisPool jedisPool;
// 初始化 Jedis 连接池
static {
// 设置连接池配置
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 最大连接数
jedisPoolConfig.setMaxTotal(8);
// 最大空闲连接数
jedisPoolConfig.setMaxIdle(8);
// 最小空闲连接数
jedisPoolConfig.setMinIdle(0);
// 尝试从连接池中获取空闲连接时的等待时间(如果没有空闲连接),超时会产生错误
jedisPoolConfig.setMaxWait(Duration.ofSeconds(5));
// 创建连接池
jedisPool = new JedisPool(jedisPoolConfig,
"192.168.0.88", 6379, 1000, "123321");
}
/**
* 返回一个空闲的 Redis 连接实例
* @return Redis 连接实例
*/
public static Jedis getJedisConnection() {
return jedisPool.getResource();
}
}
之前的 Jedis 测试用例修改为使用连接池的版本:
@BeforeEach
public void beforeEach() {
jedis = JedisConnectionFactory.getJedisConnection();
jedis.auth("123321");
jedis.select(0);
}
spring-data-redis
SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis,官网地址:https://spring.io/projects/spring-data-redis
spring-data-redis 包含以下特性:
- 提供了对不同Redis客户端的整合(Lettuce和Jedis)
- 提供了RedisTemplate统一API来操作Redis
- 支持Redis的发布订阅模型
- 支持Redis哨兵和Redis集群
- 支持基于Lettuce的响应式编程
- 支持基于JDK、JSON、字符串、Spring对象的数据序列化及反序列化
- 支持基于Redis的JDKCollection实现
SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同的类型中:
示例
创建一个 Spring 项目,并添加以下依赖:
<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>
这里的commons-pool2
是一个连接池依赖。
在appication.yml
中添加以下配置:
spring:
data:
redis:
host: 192.168.0.88
port: 6379
password: 123321
database: 0 # 默认连接的数据库
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: 100ms
注意,因为 Spring-data-redis 默认使用 lettuce 作为底层的 Redis 客户端,所以这里配置的是 lettuce 的连接池。
单元测试:
@SpringBootTest
class SpringDataRedisDemoApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void testString() {
ValueOperations ops = redisTemplate.opsForValue();
ops.set("name", "王二");
String val = (String) ops.get("name");
System.out.println(val);
}
}
这里注入RedisTemplate
实例,并用它实现对 Redis 的操作。
序列化和反序列化
Redis 本身只能处理字符串形式的 Key 和 Value,而 RedisTemplate 默认设置的 Key 和 Value 可以是 Object 类型,因此 RedisTemplate
底层实现了 Object 的序列化和反序列化,这些序列化和反序列化的实现是由RedisTemplate
中的四个属性决定的:
@Nullable
private RedisSerializer keySerializer = null;
@Nullable
private RedisSerializer valueSerializer = null;
@Nullable
private RedisSerializer hashKeySerializer = null;
@Nullable
private RedisSerializer hashValueSerializer = null;
默认情况下,这些序列化和反序列化的实现都是基于 JDK 的对象流实现的:
public class DefaultSerializer implements Serializer<Object> {
public DefaultSerializer() {
}
public void serialize(Object object, OutputStream outputStream) throws IOException {
if (!(object instanceof Serializable)) {
String var10002 = this.getClass().getSimpleName();
throw new IllegalArgumentException(var10002 + " requires a Serializable payload but received an object of type [" + object.getClass().getName() + "]");
} else {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(object);
objectOutputStream.flush();
}
}
}
因此,之前的示例中虽然在代码中是设置了一个 Key 为name
的键值对,但实际上在 Redis 服务器上创建的是一个\xac\xed\x00\x05t\x00\x04name
这样的键,一般来说我们是不能接受的。
因此我们需要自己定义一个使用特定序列化实现的RedisTemplate
,而不是使用默认实现:
@Configuration
public class WebConfig {
@Bean
RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
return redisTemplate;
}
}
这里对RedisTemplate
使用了类型参数,因为一般而言,key 和 HashKey 都是 String 类型的。在这种情况下他们都只需要使用RedisSerializer.string()
进行序列化和反序列化,这个序列化器实际上就是将字符串按照 UTF-8 编码转换为字节(或者相反)。对于 Value 和 HashValue,这里使用 Jackson 将其转换为 JSON 字符串(或者相反)。
因为这里需要使用 Jackson,所以需要添加相应的依赖:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.0</version>
</dependency>
一般的 Spring 项目不需要额外引入,因为 spring-mvc 默认包含 Jackson 依赖。
重新编写测试用例,使用类型参数:
@SpringBootTest
class SpringDataRedisDemoApplicationTests {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Test
void testString() {
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
ops.set("name", "王二");
String val = (String) ops.get("name");
System.out.println(val);
ops.set("user:2", new User("Jack", 18));
User user = (User) ops.get("user:2");
System.out.println(user);
}
}
这里使用了一个自定义的 POJO 类:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String name;
private Integer age;
}
需要添加 Lombok 依赖。
现在在 Redis 服务器上就能看到 Key 为user:2
,值为 JSON 串的键值对:
{
"@class": "cn.icexmoon.springdataredisdemo.pojo.User",
"name": "Jack",
"age": 18
}
可以看到,Value 中包含类的完整包名,这也是为什么可以反序列化出具体类型的对象。
StringRedisTemplate
上面的方案虽然可以很好的解决序列化和反序列化的问题,但有一个缺点:Value 中包含完整类名,占用 Redis 的存储空间。如果不希望 Redis 的 Value 中包含完整类名占用额外空间,就需要手动序列化和反序列化:这样我们只需要向 RedisTemplate 中传入 String 类型的 Key 和 Value,此时我们可以使用一个更简单的类型——StringRedisTemplate:
public class Tests2 {
@Autowired
private StringRedisTemplate redisTemplate;
private static final ObjectMapper mapper = new ObjectMapper();
@Test
public void test() throws JsonProcessingException {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
User user = new User("Jack", 18);
String jsonUser = mapper.writeValueAsString(user);
ops.set("user:3", jsonUser);
jsonUser = ops.get("user:3");
user = mapper.readValue(jsonUser, User.class);
System.out.println(user);
}
}
此时user:3
中的 Value:
{
"name": "Jack",
"age": 18
}
本文的完整示例代码可以从这里获取。
参考资料
- 黑马程序员Redis入门到实战教程