「进击Redis」二十、万字长文解析Redis 高级客户端Lettuce

1,682 阅读8分钟

前言

不知不觉已经是 Redis 系列的第二十篇了,也是开始写博客的第二十二天,现在的文章数量二十五篇,因为 Redis 的前几篇还是篇基础的,所以大概的话也就是每天一篇的样子。一直坚持写是真的挺难的,不过收获还是挺多的,如果有好哥哥现在遇到技术瓶颈了,不妨试试写博客出来分享。我保证能颠覆你对某个技术的认知。昨天有个同学好哥哥问我真的值得吗,我很肯定的回答是值得。因为这里面的收获只有我自己知道,哪怕没人看,那又怎样,我搞定了我自己就 OK(希望不要被打)。
今天的话还是和上一篇Redis 客户端 Jedis 详解 一样,讲的是另外一个 Redis 的高级客户端,是真的高级。为什么要讲两个客户端呢,实际上跟下篇的内容有关系,弄懂了这两个客户端下篇就很简单了。 太难了

概述

Lettuce是一个 Redis 的Java驱动包,Lettuce翻译为生菜,没错,就是吃的那种生菜,所以它的 Logo 是这样的 在这里插入图片描述
Lettuce是一个高性能基于Java编写可伸缩线程安全的 Redis 客户端,底层集成了Project Reactor提供天然的反应式编程,通信框架集成了Netty(不熟悉的好哥哥先忍忍,有时间弄这个的源码解析)使用了非阻塞 IO,提供同步, 异步和反应式 API。如果多个线程避免阻塞和事务操作(例如BLPOPMULTI),则可以共享一个连接EXEC。出色的netty NIO框架可有效管理多个连接。包括对高级 Redis 功能的支持,例如SentinelCluster和 Redis 数据模型。 在5.x版本之后融合了JDK1.8的异步编程特性,在保证高性能的同时提供了十分丰富易用的 API。5.1版本提供了很多新的特性,如下:

  1. 支持 Redis 的新增命令ZPOPMINZPOPMAXBZPOPMINBZPOPMAX
  2. 支持通过Brave模块跟踪 Redis 命令执行。
  3. 支持Redis Streams
  4. 支持异步的主从连接。
  5. 支持异步连接池。
  6. 新增命令最多执行一次模式(禁止自动重连)。
  7. 全局命令超时设置(对异步和反应式命令也有效)。
  8. ......等等

使用

1 maven 依赖

至于版本的会好哥哥们自行选择,我这个目前是最新版本

<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>6.0.1.RELEASE</version>
</dependency>

2 连接 Redis

2.1 使用 URI
public static void main(String[] args) {
   RedisURI redisURI = RedisURI.create("redis://localhost/");
}
2.2 使用构造器
public static void main(String[] args) {
   // 需要注意的是不同版本的包对应的API也可能不一样
   RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1", 6379).withDatabase(0).build();
   RedisClient client = RedisClient.create(redisURI);
}
2.3 使用构造函数
public static void main(String[] args) {
   // 需要注意的是不同版本的包对应的API也可能不一样
   RedisURI redisURI = new RedisURI("localhost", 6379, Duration.ofSeconds(60));
   RedisClient client = RedisClient.create(redisURI);
}

3 基础使用

3.1 获取 key
public static void main(String[] args) {
    RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1", 6379).withDatabase(0).build();
    // 1. 创建连接
    RedisClient redisClient = RedisClient.create(redisURI);
    // 2. 打开Redis连接
    StatefulRedisConnection<String, String> connection = redisClient.connect();
    // 3. 获取用于同步执行的命令API
    RedisCommands<String, String> redisCommands = connection.sync();
    redisCommands.get("hello");
    // 4. 关闭连接
    connection.close();
    // 5. 关闭客户端
    redisClient.shutdown();
}
3.2 三种模式 API

上面有提到Lettuce支持同步(sync)、异步(async)、反应式(reactive),对应的 API 是RedisCommandsRedisAsyncCommandsRedisReactiveCommands。 在使用其操作其他数据结构 API 之前,先把上面代码进行一下优化(将连接抽离成一个方法),不然会有很多重复代码,篇幅会过长。

/**
 * 创建成功的URI
 */
private static RedisClient redisClient = null;
/**
 * 同步执行的命令API
 */
private static StatefulRedisConnection<String, String> connection = null;
/**
 * 初始化操作
 *
 * @return
 */
public static void getConnection() {
    RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1", 6379).withDatabase(0).build();
    // 1. 创建连接
    redisClient = RedisClient.create(redisURI);
    // 2. 打开Redis连接
    connection = redisClient.connect();
}
3.2.1 同步 API
public static void main(String[] args) {
	// 1. 获取一个连接,方法在上面
    getConnection();
    // 2. 获取用于同步执行的命令API
    RedisCommands<String, String> redisCommands = connection.sync();
}
3.2.2 异步 API
public static void main(String[] args) {
    // 1. 获取一个连接,方法在上面
    getConnection();
    // 2. 获取用于同步执行的命令API
    RedisAsyncCommands<String, String> async = connection.async();
}
3.2.3 反应式 API
public static void main(String[] args) {
    // 1. 获取一个连接,方法在上面
    getConnection();
    // 2. 获取用于同步执行的命令API
    RedisReactiveCommands<String, String> reactive = connection.reactive();
}
3.3 操作基础数据类型

由于Lettuce支持三种模式的 API,其实主要是获取对应的模式上会有差异,在操作其他 API 上基本还是一样的,下面使用同步模式来操作。

public static void main(String[] args) {
    // 1. 获取一个连接,方法在上面
    getConnection();
    // 2. 获取用于同步执行的命令API
    RedisCommands<String, String> redisCommands = connection.sync();
    // 3. 操作字符串
    redisCommands.set("hello", "world");
    // 3.1. 获取字符串
    String hello = redisCommands.get("hello");
    // 4. 设置hash
    redisCommands.hset("myHash", "key1", "val1");
    // 4.1. 获取hash
    redisCommands.hget("myHash", "key1");
    // 5. 操作list
    redisCommands.rpush("myList", "val1");
    redisCommands.rpush("myList", "val2");
    // 5.1. 获取list
    redisCommands.lrange("myList", 0, -1);
    // 6. 操作set
    redisCommands.sadd("mySet", "val1");
    redisCommands.sadd("mySet", "val2");
    // 6.1. 获取作set
    redisCommands.smembers("mySet");
    // 7. 操作sorted set
    redisCommands.zadd("mySortedSet", 100, "member1");
    redisCommands.zadd("mySortedSet", 99, "member2");
    // 7.1. 获取sorted set
    redisCommands.zrangeWithScores("mySortedSet", 0, -1);
}
3.4 发布/订阅

Lettuce提供了对单机和集群连接上的发布/订阅的支持。订阅频道或模式后,将在消息/已订阅/未订阅事件中通知连接。提供了同步,异步和反应性API来与 Redis Pub/Sub功能进行交互。非集群模式下的发布订阅依赖于定制的连接StatefulRedisPubSubConnection,集群模式下的发布订阅依赖于定制的连接StatefulRedisClusterPubSubConnection,两者分别来源于RedisClient#connectPubSub()系列方法和RedisClusterClient#connectPubSub()

3.4.1 自定义一个监听器
public static class MyRedisPubSubListener implements RedisPubSubListener {

    @Override
    public void message(Object channel, Object message) {
        System.out.println("channel:" + channel + " say: " + message);
    }

    @Override
    public void message(Object pattern, Object channel, Object message) {
        System.out.println("pattern:" + pattern + "say: " + message);
    }

    @Override
    public void subscribed(Object channel, long count) {
        System.out.println(channel + " subscribed");
    }

    @Override
    public void psubscribed(Object pattern, long count) {
        System.out.println(pattern + " psubscribed");
    }

    @Override
    public void unsubscribed(Object channel, long count) {
        System.out.println(channel + " unsubscribed");
    }

    @Override
    public void punsubscribed(Object pattern, long count) {
        System.out.println(pattern + " punsubscribed");
    }
}
3.4.2 三种方式订阅
public static void main(String[] args) throws ExecutionException, InterruptedException {
     // 1. 获取一个连接,方法在上面(可能是单机、普通主从、哨兵等非集群模式的客户端)
     getConnection();
     StatefulRedisPubSubConnection<String, String> connection = redisClient.connectPubSub();
     connection.addListener(new MyRedisPubSubListener());

     // 同步订阅
     RedisPubSubCommands<String, String> sync = connection.sync();
     sync.subscribe("channel");

     // 异步订阅
     RedisPubSubAsyncCommands<String, String> async = connection.async();
     RedisFuture<Void> future = async.subscribe("channel");

     // 反应式订阅
     RedisPubSubReactiveCommands<String, String> reactive = connection.reactive();
     reactive.subscribe("channel").subscribe();
     reactive.observeChannels().doOnNext(patternMessage -> {
         System.out.println("observeChannels doOnNext " + patternMessage.getMessage());
     }).subscribe();
 }
3.4.3 发布消息

这里测试的时候需要注意的是不能使用同一个连接,否则会报错的。

public static void main(String[] args) {
	// 重新初始化连接
    LettuceTest.getConnection();
    // 发布消息,执行完后再看另外一个控制台能把接受到的消息打印
    LettuceTest.connection.sync().publish("channel", "hello");
}
3.5 执行 Lua

有不熟悉 Redis 执行Lua的好哥哥可以看Redis 万字长文 Lua 详解

public static void main(String[] args) throws ExecutionException, InterruptedException {
    // 1. 获取一个连接,方法在上面(可能是单机、普通主从、哨兵等非集群模式的客户端)
    getConnection();
    RedisCommands<String, String> redisCommands = connection.sync();
    // 2. 定义一个简单的脚本
    String script = "return redis.call('get',KEYS[1])";
    // 3. 第一种方式:直接执行脚本
    Object hello = redisCommands.eval(script, ScriptOutputType.VALUE, "hello");
    // 4. 第二种方式:加载脚本到Redis,然后执行
    String scriptSha = redisCommands.scriptLoad(script);
    // 4.1. 根据sha执行脚本
    Object helloResult = redisCommands.evalsha(scriptSha, ScriptOutputType.VALUE, "hello");
}
3.6 流 API

这个跟 Java8 中的流是一个概念,就是Lettuce支持了这种方式。

public static void main(String[] args) throws ExecutionException, InterruptedException {
    // 1. 获取一个连接,方法在上面(可能是单机、普通主从、哨兵等非集群模式的客户端)
    getConnection();
    RedisCommands<String, String> redisCommands = connection.sync();
    // 添加元素到list
    redisCommands.lpush("key", "one");
    redisCommands.lpush("key", "two");
    redisCommands.lpush("key", "three");

    // 流式返回
    Long count = redisCommands.lrange(new ValueStreamingChannel<String>() {
        @Override
        public void onValue(String value) {
            System.out.println("Value: " + value);
        }
    }, "key", 0, -1);
}

与 Jedis 对比

  1. 线程安全上,Jedis 是线程不安全的,Lettuce 是线程安全的。 这个指的是直连方式来说,实际上Jedis使用连接池也是线程安全的。
  2. 从实现方式上来说,Jedis实现简单、使用简单,而Lettuce 实现是很复杂的,但是还好使用起来简单。
  3. 从网络上来说,Jedis 使用的是阻塞I/O,而Lettuce是基于Netty使用NIO
  4. 从抽象程度来说,Jedis并没有做特别的抽象处理,而Lettuce抽象了三种处理命令的模式(同步、异步、反应式)。
  5. 从获取连接上来说,Jedis一般使用的是多个连接(从连接池获取),而Lettuce共享连接,连接是 long-lived 的和线程安全的,并且会自动重连。
  6. 从社区上来讲,Jedis社区较活跃,版本更新快,而Lettuce的话活跃度就一般了,版本更新较慢(很少关于Lettuce的资料)。

总结

还有一些关于Lettuce API、高可用配置等相关的东西没有写进来。一方面是篇幅的原因,一方面的话也是我们平时不会单独使用Lettuce,都是基于 Spring 的spring-boot-starter-data-redis来对 Redis 做相关操作。这里贴一下Lettuce 的源码地址(Lettuce 源码),感兴趣的好哥哥可以上去了解一下。

本期就到这啦,有不对的地方欢迎好哥哥们评论区留言,另外求关注、求点赞

下一篇: 「进击Redis」二十一、RedisTemplate 可没你想的那么简单
上一篇: Redis 客户端 Jedis 详解