一、Lettuce 是啥?

一次技术讨论会上,大家说起 Redis 的 Java 客户端哪家强,我第一时间毫不犹豫地喊出 “Jedis, YES!”

“Jedis 可是官方客户端,用起来直接省事,公司中间件都用它。除了 Jedis 外难道还有第二个能打的?”我直接扔出王炸。

刚学 Spring 的小张听了不服:“SpringDataRedis 都用 RedisTemplate!Jedis?不存在的。”

“坐下吧秀儿,SpringDataRedis 就是基于 Jedis 封装的。”旁边李哥呷了一口刚开的快乐水,嘴角微微上扬,露出一丝不屑。

“现在很多都是用 Lettuce 了,你们不会不知道吧?”老王推了推眼镜淡淡地说道,随即缓缓打开镜片后那双心灵的窗户,用关怀的眼神俯视着我们几只菜鸡。

Lettuce?生菜?满头雾水的我赶紧打开了 Redis 官网的客户端列表。发现 Java 语言有三个官方推荐的实现:JedisLettuceRedission

(截图来源:https://redis.io/clients#java)

Lettuce 是什么客户端?没听过。但发现它的官方介绍最长:

Advanced Redis client for thread-safe sync, async, and reactive usage. Supports Cluster, Sentinel, Pipelining, and codecs.

赶紧查着字典翻译了下:

  • 高级客户端

  • 线程安全

  • 支持同步、异步和反应式 API

  • 支持集群、哨兵、管道和编解码

老王摆摆手示意我收好字典,不紧不慢介绍起来。

1.1 高级客户端

“师爷,你给翻译翻译,什么(哔——)叫做(哔——)高级客户端?”

“高级客户端嘛,高级嘛,就是 Advanced 啊!new 一下就能用,什么实现细节都不用管,拿起业务逻辑直接突突。”

1.2 线程安全

这是和 Jedis 主要不同之一。

Jedis 的连接实例是线程不安全的,于是需要维护一个连接池,每个线程需要时从连接池取出连接实例,完成操作后或者遇到异常归还实例。当连接数随着业务不断上升时,对物理连接的消耗也会成为性能和稳定性的潜在风险点。

Lettuce 使用 Netty 作为通信层组件,其连接实例是线程安全的,并且在条件具备时可访问操作系统原生调用 epoll, kqueue 等获得性能提升。

我们知道 Redis 服务端实例虽然可以同时连接多个客户端收发命令,但每个实例执行命令时都是单线程的。

这意味着如果应用可以通过多线程+单连接方式操作 Redis,将能够精简 Redis 服务端的总连接数,而多应用共享同一个 Redis 服务端时也能够获得更好的稳定性和性能。对于应用来说也减少了维护多个连接实例的资源消耗。

1.3 支持同步、异步和反应式 API

Lettuce 从一开始就按照非阻塞式 IO 进行设计,是一个纯异步客户端,对异步和反应式 API 的支持都很全面。

即使是同步命令,底层的通信过程仍然是异步模型,只是通过阻塞调用线程来模拟出同步效果而已。

1.4 支持集群、哨兵、管道和编解码

“这些特性都是标配,Lettuce 可是高级客户端!高级,懂吗?”老王说到这里兴奋地用手指点着桌面,但似乎不想多做介绍,我默默地记下打算好好学习一番。

(在项目使用过程中,pipeling 机制用起来和 Jedis 相比稍微抽象已点,下文会给出在使用过程中遇到的小坑和解决办法。)

1.5 在 Spring 中的使用情况

除了 Redis 官方介绍,我们也可以发现 Spring Data Redis 在升级到 2.0 时,将 Lettuce 升级到了 5.0。其实 Lettuce 早就在 SpringDataRedis 1.6 时就被官方集成了;而 SpringSessionDataRedis 则直接将 Lettuce 作为默认 Redis 客户端,足见其成熟和稳定。

Jedis 广为人知甚至是事实上的标准 Java 客户端(de-facto standard driver),和它推出时间早(1.0.0 版本 2010 年 9 月,Lettuce 1.0.0 是 2011 年 3 月)、API 直接易用、对 Redis 新特性支持最快等特点都密不可分。

但 Lettuce 作为后进,其优势和易用性也获得了 Spring 等社区的青睐。下面会分享我们在项目中集成 Lettuce 时的经验总结,供大家参考。

二、Jedis 和 Lettuce 有啥主要区别?

说了这么多,Lettuce 和老牌客户端 Jedis 主要都有哪些区别呢?我们可以看下 Spring Data Redis 帮助文档给出的对比表格:

(截图来源:https://docs.spring.io)

注:其中 X 标记的是支持.

经过比较我们可以发现:

  • Jedis 支持的 Lettuce 都支持;

  • Jedis 不支持的 Lettuce 也支持!

这么看来 Spring 中越来越多地使用 Lettuce 也就不奇怪了。

三、Lettuce 初体验

光说不练假把式,给大家分享我们尝试 Lettuce 时的收获,尤其是批量命令部分花了比较多的时间踩坑,下文详解。

3.1 快速开始

如果最简单的例子都令人费解,那这个库肯定流行不起来。Lettuce 的快速开始真的够快:

a. 引入 maven 依赖(其他依赖类似,具体可见文末参考资料)

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

b. 填上 Redis 地址,连接、执行、关闭。Perfect!

import io.lettuce.core.*;// Syntax: redis://[password@]host[:port][/databaseNumber]
// Syntax: redis://[username:password@]host[:port][/databaseNumber]
RedisClient redisClient = RedisClient.create("redis://password@localhost:6379/0");
StatefulRedisConnection<String, String> connection = redisClient.connect();
RedisCommands<String, String> syncCommands = connection.sync();syncCommands.set("key", "Hello, Redis!");connection.close();
redisClient.shutdown();

3.2 支持集群模式吗?支持!

Redis Cluster 是官方提供的 Redis Sharding 方案,大家应该非常熟悉不再多介绍,官方文档可参考 Redis Cluster 101。

Lettuce 连接 Redis 集群对上述客户端代码一行换一下即可:

// Syntax: redis://[password@]host[:port]
// Syntax: redis://[username:password@]host[:port]
RedisClusterClient redisClient = RedisClusterClient.create("redis://password@localhost:7379");

3.3 支持高可靠吗?支持!

Redis Sentinel 是官方提供的高可靠方案,通过 Sentinel 可以在实例故障时自动切换到从节点继续提供服务,官方文档可参考 Redis Sentinel Documentation。

仍然是替换客户端的创建方式就可以了:

// Syntax: redis-sentinel://[password@]host[:port][,host2[:port2]][/databaseNumber]#sentinelMasterId
RedisClient redisClient = RedisClient.create("redis-sentinel://localhost:26379,localhost:26380/0#mymaster");

3.4 支持集群下的 pipeline 吗?支持!

Jedis 虽然有 pipeline 命令,但不能支持 Redis Cluster。一般都需要自行归并各个 key 所在的 slot 和实例后再批量执行 pipeline。

官网对集群下的 pipeline 支持 PR 截至本文写作时(2021年2月)四年过去了仍然未合入,可见 Cluster pipelining。

Lettuce 虽然号称支持 pipeling,但并没有直接看到 pipeline 这种 API,这是怎么回事?

3.4.1 实现 pipeline

使用 AsyncCommands 和 flushCommands 实现 pipeline,经过阅读官方文档可以知道,Lettuce 的同步、异步命令其实都共享同一个连接实例,底层使用 pipeline 的形式在发送/接收命令。

区别在于:

  • connection.sync() 方法获取的同步命令对象,每一个操作都会立刻将命令通过 TCP 连接发送出去;

  • connection.async() 获取的异步命令对象,执行操作后得到的是 RedisFuture<?>,在满足一定条件的情况下才批量发送。

由此我们可以通过异步命令+手动批量推送的方式来实现 pipeline,来看官方示例:

StatefulRedisConnection<String, String> connection = client.connect();
RedisAsyncCommands<String, String> commands = connection.async();// disable auto-flushing
commands.setAutoFlushCommands(false);// perform a series of independent calls
List<RedisFuture<?>> futures = Lists.newArrayList();
for (int i = 0; i < iterations; i++) {futures.add(commands.set("key-" + i, "value-" + i));
futures.add(commands.expire("key-" + i, 3600));
}// write all commands to the transport layer
commands.flushCommands();// synchronization example: Wait until all futures complete
boolean result = LettuceFutures.awaitAll(5, TimeUnit.SECONDS,
futures.toArray(new RedisFuture[futures.size()]));// later
connection.close();

3.4.2 这么做有没有问题?

乍一看很完美,但其实有暗坑:setAutoFlushCommands(false) **设置后,会发现 sync() 方法调用的同步命令都不返回了!**这是为什么呢?我们再看看官方文档:

Lettuce is a non-blocking and asynchronous client. It provides a synchronous API to achieve a blocking behavior on a per-Thread basis to create await (synchronize) a command response… As soon as the first request returns, the first Thread’s program flow continues, while the second request is processed by Redis and comes back at a certain point in time

sync 和 async 在底层实现上都是一样的,只是 sync 通过阻塞调用线程的方式模拟了同步操作。并且 setAutoFlushCommands 通过源码可以发现就是作用在 connection 对象上,于是该操作对 sync 和 async 命令对象都生效。

所以,只要某个线程中设置了 auto flush commands 为 false,就会影响到所有使用该连接实例的其他线程。

/**
* An asynchronous and thread-safe API for a Redis connection.
*
* @param <K> Key type.
* @param <V> Value type.
* @author Will Glozer
* @author Mark Paluch
*/
public abstract class AbstractRedisAsyncCommands<K, V> implements RedisHashAsyncCommands<K, V>, RedisKeyAsyncCommands<K, V>,
RedisStringAsyncCommands<K, V>, RedisListAsyncCommands<K, V>, RedisSetAsyncCommands<K, V>,
RedisSortedSetAsyncCommands<K, V>, RedisScriptingAsyncCommands<K, V>, RedisServerAsyncCommands<K, V>,
RedisHLLAsyncCommands<K, V>, BaseRedisAsyncCommands<K, V>, RedisTransactionalAsyncCommands<K, V>,
RedisGeoAsyncCommands<K, V>, RedisClusterAsyncCommands<K, V> {@Overridepublic void setAutoFlushCommands(boolean autoFlush) {connection.setAutoFlushCommands(autoFlush);}
}

对应的,如果多个线程调用 async() 获取异步命令集,并在自身业务逻辑完成后调用 flushCommands(),那将会强行 flush 其他线程还在追加的异步命令,原本逻辑上属于整批的命令将被打散成多份发送。

虽然对于结果的正确性不影响,但如果因为线程相互影响打散彼此的命令进行发送,则对性能的提升就会很不稳定。

自然我们会想到:每个批命令创建一个 connection,然后……这不和 Jedis 一样也是靠连接池么?

回想起老王镜片后那穿透灵魂的目光,我打算硬着头皮再挖掘一下。果然,再次认真阅读文档后我发现了另外一个好东西:Batch Execution

3.4.3 Batch Execution

既然 flushCommands 会对 connection 产生全局影响,那把 flush 限制在线程级别不就行了?我从文档中找到了示例官方示例。

回想起前文 Lettuce 是高级客户端,看了文档后发现确实高级,只需要定义接口就行了(让人想起 MyBatis 的 Mapper 接口),下面是项目中使用的例子:

/
/*** 定义会用到的批量命令*/
@BatchSize(100)
public interface RedisBatchQuery extends Commands, BatchExecutor {RedisFuture<byte[]> get(byte[] key);RedisFuture<Set<byte[]>> smembers(byte[] key);RedisFuture<List<byte[]>> lrange(byte[] key, long start, long end);RedisFuture<Map<byte[], byte[]>> hgetall(byte[] key);
}

调用时这样操作:

// 创建客户端
RedisClusterClient client = RedisClusterClient.create(DefaultClientResources.create(), "redis://" + address);// service 中持有 factory 实例,只创建一次。第二个参数表示 key 和 value 使用 byte[] 编解码
RedisCommandFactory factory = new RedisCommandFactory(connect, Arrays.asList(ByteArrayCodec.INSTANCE, ByteArrayCodec.INSTANCE));// 使用的地方,创建一个查询实例代理类调用命令,最后刷入命令
List<RedisFuture<?>> futures = new ArrayList<>();
RedisBatchQuery batchQuery = factory.getCommands(RedisBatchQuery.class);
for (RedisMetaGroup redisMetaGroup : redisMetaGroups) {// 业务逻辑,循环调用多个 key 并将结果保存到 futures 结果中appendCommand(redisMetaGroup, futures, batchQuery);
}// 异步命令调用完成后执行 flush 批量执行,此时命令才会发送给 Redis 服务端
batchQuery.flush();

就是这么简单。

此时批量的控制将在线程粒度上进行,并在调用 flush 或达到 @BatchSize 配置的缓存命令数量时执行批量操作。而对于 connection 实例,不用再设置 auto flush commands,保持默认的 true 即可,对其他线程不造成影响。

ps:优秀、严谨的你肯定会想到:如果单命令执行耗时长或者谁放了个诸如 BLPOP 的命令的话,肯定会造成影响的,这个话题官方文档也有涉及,可以考虑使用连接池来处理。

3.5 还能再给力一点吗?

Lettuce 支持的当然不仅仅是上面所说的简单功能,还有这些也值得一试:

3.5.1 读写分离

我们知道 Redis 实例是支持主从部署的,从实例异步地从主实例同步数据,并借助 Redis Sentinel 在主实例故障时进行主从切换。

当应用对数据一致性不敏感、又需要较大吞吐量时,可以考虑主从读写分离方式。Lettuce 可以设置 StatefulRedisClusterConnection 的 readFrom 配置来进行调整:

3.5.2 配置自动更新集群拓扑

当使用 Redis Cluster 时,服务端发生了扩容怎么办?

Lettuce 早就考虑好了——通过 RedisClusterClient#setOptions 方法传入 ClusterClientOptions 对象即可配置相关参数(全部配置见文末参考链接)。

ClusterClientOptions 中的 topologyRefreshOptions 常见配置如下:

3.5.3 连接池

虽然 Lettuce 基于线程安全的单连接实例已经具有非常好的性能,但也不排除有些大型业务需要通过线程池来提升吞吐量。另外对于事务性操作是有必要独占连接的。

Lettuce 基于 Apache Common-pool2 组件提供了连接池的能力(以下是官方提供的 RedisCluster 对应的客户端线程池使用示例):

RedisClusterClient clusterClient = RedisClusterClient.create(RedisURI.create(host, port));GenericObjectPool<StatefulRedisClusterConnection<String, String>> pool = ConnectionPoolSupport.createGenericObjectPool(() -> clusterClient.connect(), new GenericObjectPoolConfig());// execute work
try (StatefulRedisClusterConnection<String, String> connection = pool.borrowObject()) {connection.sync().set("key", "value");connection.sync().blpop(10, "list");
}// terminating
pool.close();
clusterClient.shutdown();

这里需要说明的是:createGenericObjectPool 创建连接池默认设置 wrapConnections 参数为 true。此时借出的对象 close 方法将通过动态代理的方式重载为归还连接;若设置为 false 则 close 方法会关闭连接。

Lettuce 也支持异步的连接池(从连接池获取连接为异步操作),详情可参考文末链接。还有很多特性不能一一列举,都可以在官方文档上找到说明和示例,十分值得一读。

四、使用总结

Lettuce 相较于Jedis,使用上更加方便快捷,抽象度高。并且通过线程安全的连接降低了系统中的连接数量,提升了系统的稳定性。

对于高级玩家,Lettuce 也提供了很多配置、接口,方便对性能进行优化和实现深度业务定制的场景。

另外不得不说的一点,Lettuce 的官方文档写的非常全面细致,十分难得。社区比较活跃,Commiter 会积极回答各类 issue,这使得很多疑问都可以自助解决。

相比之下,Jedis 的文档、维护更新速度就比较慢了。JedisCluster pipeline 的 PR 至今(2021年2月)四年过去还未合入。

参考资料

其中两个 GitHub 的 issue 含金量很高,强烈推荐一读!

1.Lettuce 快速开始:https://lettuce.io

2.Redis Java Clients

3.Lettuce 官网:https://lettuce.io

4.SpringDataRedis 参考文档

5.Question about pipelining

6.Why is Lettuce the default Redis client used in Spring Session Redis

7.Cluster-specific options:https://lettuce.io

8.Lettuce 连接池

9.客户端配置:https://lettuce.io/core/release

10.SSL配置:https://lettuce.io

作者:vivo互联网数据智能团队-Li Haoxuan

初探 Redis 客户端 Lettuce:真香!相关推荐

  1. 闲谈Redis客户端Lettuce

    简介 Lettuce是一个高性能基于Java编写的Redis驱动框架,底层集成了Project Reactor提供天然的反应式编程,通信框架集成了Netty使用了非阻塞IO,5.x版本之后融合了JDK ...

  2. Redis客户端Lettuce源码【一】Jedis vs Lettuce

    文章目录 基本实现对比 性能对比 Jedis的基本用法 Jedis配合Springboot RedisTemplate使用 Lettuce的基本用法 Lettuce配合Springboot Redis ...

  3. 慢连接 java_记一次redis的java客户端lettuce操作慢的解决方案

    因为项目业务需要,我们要把数据库中的大量数据缓存到redis中,并且会随时更新缓存,刚开始更新频率是1Hz,没有什么问题,后来更新频率达到了5Hz,lettuce开始疯狂报错:redis comman ...

  4. Redis高级客户端Lettuce详解

    前提 Lettuce是一个Redis的Java驱动包,初识她的时候是使用RedisTemplate的时候遇到点问题Debug到底层的一些源码,发现spring-data-redis的驱动包在某个版本之 ...

  5. Redis 高级 Java 客户端 Lettuce 的用法及踩坑经验

    如果你在网上搜索 Redis 的 Java 客户端,你会发现,大多数文献介绍的都是 Jedis,不可否认,Jedis 是一个优秀的基于 Java 语言的 Redis 客户端,但是,其不足也很明显:Je ...

  6. springboot项目中redis客户端(Jedis、Lettuce、Redisson)

    一.redis客户端的对比 1).Jedis Jedis作为Redis官方推荐的一款客户端,也算是简单好用,基础功能齐全,在中小型项目中还是很好用的,但是Jedis是直连模式,在多个线程间共享一个Je ...

  7. Redis的Java客户端Lettuce简介

    环境 操作系统:Ubuntu 20.04 Redis:6.2.6 开发工具:IntelliJ IDEA 2022.1 (Community Edition) Lettuce的官网( https://l ...

  8. Redis客户端 Jedis 与 Lettuce

    Lettuce 和 Jedis 的定位都是 Redis 的 client,所以它们可以直接连接redis server. Jedis 在实现上是直接连接的 redis server,如果在多线程环境下 ...

  9. Centos6下Redis学习(一)——Java客户端Lettuce的使用、Springboot整合

    几年前我用的还是Jedis,貌似现在Lettuce更加流行了,而且Springboot现在集成的也是Lettuce 官方文档https://lettuce.io/docs/getting-starte ...

  10. jpa query oracle 参数int为空_撸一个预言机(Oracle)服务,真香!—中篇

    本文作者:六天 一.文章结构 本文将通过上.中.下三篇文章带领大家一步步开发实现一个中心化的 Oracle 服务,并通过一个抽奖合约演示如何使用我们的 Oracle 服务.文章内容安排如下: 上篇:O ...

最新文章

  1. 信息与计算机科学考研学校排名,计算机科学与技术考研学校排名
  2. 日志管理之 Docker logs - 每天5分钟玩转 Docker 容器技术(87)
  3. 企业网络推广下的B站二次上市:致力于造就国内最具活力和创造力的内容社区...
  4. 开发日记-20190914 关键词 汇编语言王爽版 第三章
  5. 学习android开发中遇到的一些小问题
  6. [ZJOI2008]瞭望塔
  7. Linux下fdisk命令操作磁盘详解--添加、删除、转换分区
  8. JAVA校内报纸实验_实验(实训)中心2011—2012学年第二学期工作计划
  9. 安装composer以及laravel框架
  10. 初窥R(基本说明、获取帮助、工作空间、输入输出、包)
  11. Python中的strip(),lstrip(),rstrip()的用法
  12. 哈夫曼编码与哈夫曼树
  13. 湖南大学计算机专业推免生,湖南大学2018年招收推荐免试攻读研究生简章
  14. 华为--三层交换实验(原理与实验详情)
  15. 和数研究院4周年庆,初心不改,笃行致远!
  16. Android Gradle是什么?
  17. earlier的意思_earlier和before都有之前的意思?有什么区别吗
  18. “知了”来了,西电的小朋友们看过来!
  19. 从银行业客服模式进化看联络中心智能化趋势
  20. Java实现七牛云文件或图片上传下载

热门文章

  1. C/C++ 反编译工具开源软件
  2. 奥迪A6(C5)停车加热(驻车暖风)01406故障
  3. java excelhandle oschina,基于alibab的easyexcel进行excel表的导出(可自定义handler去设计excel格式)...
  4. 类和对象1:基础学习
  5. LSD_SLAM 编译、安装到运行demo
  6. Beamer编译排错
  7. Java实现合成图片
  8. Android Verified Boot 2.0简要
  9. Hyperledger fabric-couchdb镜像版本的坑
  10. oFono学习笔记(一):oFono中增加消息与接口