系列文章目录
Redis总结(1)——基本介绍
Redis总结(2)——Java客户端,Jedis与Lettuce
Redis总结(3)——基本原理
前言
作为开发人员,我们很少使用上一篇文章中介绍的客户端来访问Redis,更多的是使用代码来交互,下面介绍两种常用的Java客户端,同时给出简单代码示例。一、Jedis
1.Jedis介绍
Jedis是一个经典易用的Java客户端,提供了非常全面的Redis命令支持,其API和Redis命令几乎完全一致,它的底层实现和使用也都非常简单,对初学者非常友好。具体API可参考https://www.mklab.cn/onlineapi/jedis/,Github仓库https://github.com/redis/jedis。
Jedis简单易用的代价就是线程不安全。Jedis是直连服务端的,属于阻塞IO,当多个线程同时使用一个客户端实例连接时就可能报错,可以参考文章https://www.cnblogs.com/gxyandwmm/p/13485226.html。那么在多线程的情况下我们应该如何使用Jedis呢?答案就是连接池,连接池简单来说就是多个客户端连接的集合,这样多个线程同时连接Redis时,就回到连接池里获取不同的客户端。于此同时,连接池内的连接在close时,不会销毁,而是回到连接池等待下一次调用,避免了频繁建立和销毁连接的开销。因此,在并发环境中务必使用连接池。
2.Jedis结构
Jedis的实现是比较简单的,因此我们可以适当了解一下源码,有助于更好的使用它。下面是Jedis的核心类Jedis类的结构关系图(转载自https://www.cnblogs.com/c-xiaohai/p/8376364.html),可以看到Jedis继承了BinaryJedis,同时实现了JedisCommands, MultiKeyCommands, AdvancedJedisCommands等多个接口。
首先来说一下父类BinaryJedis,顾名思义BinaryJedis是一个二进制的客户端,具体来说BinaryJedis与Jedis中有许多重名的方法,但是BinaryJedis中方法的参数是byte[],而Jedis中方法的参数是String,也就是说BinaryJedis是直接操作字节的,而Jedis是操作字符串的;继续看源码,可以发现Jedis方法的核心操作是调用其持有的Client对象的相关方法,Client则会把String转换为byte[]然后调用BinaryClient中的方法,而BinaryJedis是直接调用BinaryClient中的方法,也就是二者最终还是一致的。其次是Jedis实现的一系列接口,每一个接口都代表了一个命令簇,例如MultiKeyCommands包含了处理多个Key的方法,ClusterCommands包含了集群相关的方法。
3.Jedis使用
通过Maven导入依赖,我使用的是比较老的版本
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
下面是使用的简单示例,在实际工程中建议包装成工具类
public class JedisDemo {
/**
* 创建一个Jedis连接池
*/
public static JedisPool configPool() {
String redisIp = "127.0.0.1"; // ip
int redisPort = 6379; // 端口
String redisAuth = null; // 密码
int connectionTimeout = 200; // 连接超时时间ms
// 连接池常用参数,更多参数请自行查阅
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxWaitMillis(1000);// 连接用完后,调用者最大等待时间ms
config.setMaxIdle(10);// 最大空闲的连接数
config.setMaxTotal(20);// 最大连接数
config.setTestOnBorrow(true);// 连接时是否检测有效性,(无效连接会被移除,多一次ping的开销)
return new JedisPool(config, redisIp, redisPort, connectionTimeout, redisAuth);
}
/**
* 创建一个Jedis连接,由于生产上较少使用,仅配置ip和port,其他参数详见API文档
*/
public static Jedis congifJedis() {
return new Jedis("172.0.0.1", 6379);
}
public static void main(String[] args) {
JedisPool jedisPool = configPool();
try (Jedis jedis = jedisPool.getResource()) { // JDK8 Try-with-resources
// string
jedis.set("key1", "value1"); // 通过String新增键值对
jedis.set("key2".getBytes(), "value2".getBytes()); // 通过byte[]新增键值对
byte[] bytes1 = jedis.get("key1".getBytes()); // 以byte[]形式获取value
System.out.println("key1的值(Bytes):" + Arrays.toString(bytes1));
String string2 = jedis.get("key2"); // 以String形式获取value
System.out.println("key2的值(String):" + string2);
Boolean bool1 = jedis.exists("key3"); // 判断key是否存在
System.out.println("key3是否存在:" + bool1);
// 所有set类的命令都有返回值,可用来判断是否成功
System.out.println("set返回值:" + jedis.set("test", "test"));
// list
jedis.lpush("list","a","b"); // list左侧添加元素
jedis.rpush("list","c"); // list右侧添加元素
String string3 = jedis.lpop("list"); // 弹出元素
System.out.println("list弹出:" + string3);
String string4 = jedis.lindex("list",1); // 根据索引取出元素
System.out.println("list中index为1的值:" + string4);
// hash
Map<String, String> hash1 = new HashMap<>();
hash1.put("name", "Pablo");
hash1.put("age", "18");
jedis.hmset("hash", hash1); // 向hash中添加多个键值对
jedis.hset("hash","job","programmer"); // 向hash中添加单个键值对
Map<String, String> hash2 = jedis.hgetAll("hash"); // 取出全部键值对
System.out.println("hash中的全部键值对:" + hash2);
// set
jedis.sadd("set", "a"); // 向set中添加元素
jedis.sadd("set", "b");
jedis.sadd("set", "c");
Set<String> set1 = jedis.smembers("set"); // 取出set中全部元素
System.out.println("set中的全部元素:" + set1);
jedis.srem("set", "c"); // 删除元素
Boolean bool2 = jedis.sismember("set","c"); // 判断c是否是set中的元素
System.out.println("set中是否存在元素c:" + bool2);
// zset
jedis.zadd("zset", 1993, "Bob"); // 向zset中添加元素
jedis.zadd("zset", 1991, "Tom");
jedis.zadd("zset", 1990, "Jack");
Long long1 = jedis.zcard("zset"); // 获取zset中元素数量
System.out.println("zset中的元素数量:" + long1);
Set<String> set2 = jedis.zrange("zset", 0, 1); // 获取指定范围内的元素
System.out.println("zset中的排序0-1之间的元素:" + set2);
}
}
}
运行结果
key1的值(Bytes):[118, 97, 108, 117, 101, 49]
key2的值(String):value2
key3是否存在:false
set返回值:OK
list弹出:b
list中index为1的值:c
hash中的全部键值对:{name=Pablo, job=programmer, age=18}
set中的全部元素:[a, c, b]
set中是否存在元素c:false
zset中的元素数量:3
zset中的排序0-1之间的元素:[Jack, Tom]
二、Lettuce
1.Lettuce简介
Lettuce也是目前比较流行的Java客户端,尤其是在Spring Boot框架中。Lettuce采用Netty框架进行开发,一个连接实例可以在多个线程间并发访问,而且线程安全,同时它还是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。这点上Lettuce与Jedis非常不同,因此在大部分情况下它不需要连接池。此外Lettuce还封装了同步、异步和响应式API,便于并发系统调用。
可以看到Lettuce在功能上更进了一步,更适合分布式的系统,但是这也导致了其实现非常复杂,再加上我本身使用也不多,就不对其进行深入解析了,下面直接看用例。
2.Lettuce使用
Maven依赖
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.0.2.RELEASE</version>
</dependency>
Lettuce连接示例,仅展示最简单的同步API。异步API可以参考https://blog.csdn.net/u012549626/article/details/111470220
public class LettuceDemo {
public static void main(String[] args) {
// 连接配置
RedisURI uri = RedisURI.builder().withHost("127.0.0.1").withPort(6379).build();
// 创建客户端
RedisClient client = RedisClient.create(uri);
// 连接
StatefulRedisConnection<String, String> connection = client.connect();
// 同步API
RedisCommands<String, String> commands = connection.sync();
// get操作,其他操作类似不再展示
String value = commands.get("key");
System.out.println(value);
// 若要使用基于byte[]的相关操作,则要在连接时声明
StatefulRedisConnection<byte[], byte[]> byteConnection = client.connect(ByteArrayCodec.INSTANCE);
RedisCommands<byte[], byte[]> byteCommands = byteConnection.sync();
// 关闭连接
connection.close();
client.shutdown();
}
}
下面用Lettuce来实现https://www.cnblogs.com/gxyandwmm/p/13485226.html中的并发场景,可以发现没有报错,说明Lettuce是线程安全的
public class ConcurrentTest {
private static StatefulRedisConnection<String, String> connection;
public static void main(String[] args) throws InterruptedException {
// 配置客户端
RedisURI uri = RedisURI.builder().withHost("127.0.0.1").withPort(6379).build();
RedisClient client = RedisClient.create(uri);
connection = client.connect();
// 多线程调用
ExecutorService pool = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
pool.execute(new RedisSet());
}
pool.shutdown();
// 判断任务是否结束,结束则关闭连接
while (true) {
if (pool.isTerminated()) {
connection.close();
client.shutdown();
System.out.println("end");
break;
}
Thread.sleep(200);
}
}
/**
* 使用同步API,set命令作为多线程任务
*/
static class RedisSet implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
connection.sync().set("hello", "world");
}
}
}
}