一、Lettuce 是啥?
一次技术讨论会上,大家说起 Redis 的 Java 客户端哪家强,我第一工夫毫不犹豫地喊出 "Jedis, YES!"
“Jedis 可是官网客户端,用起来间接省事,公司中间件都用它。除了 Jedis 外难道还有第二个能打的?”我间接扔出王炸。
刚学 Spring 的小张听了不服:“SpringDataRedis 都用 RedisTemplate!Jedis?不存在的。”
“坐下吧秀儿,SpringDataRedis 就是基于 Jedis 封装的。”旁边李哥呷了一口刚开的高兴水,嘴角微微上扬,露出一丝不屑。
“当初很多都是用 Lettuce 了,你们不会不晓得吧?”老王推了推眼镜淡淡地说道,随即缓缓关上镜片后那双心灵的窗户,用关心的眼神仰视着咱们几只菜鸡。
Lettuce?生菜?满头雾水的我连忙关上了 Redis 官网的客户端列表。发现 Java 语言有三个官网举荐的实现:Jedis、Lettuce和 Redission。
(截图起源: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]#sentinelMasterIdRedisClient 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-flushingcommands.setAutoFlushCommands(false); // perform a series of independent callsList<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 layercommands.flushCommands(); // synchronization example: Wait until all futures completeboolean result = LettuceFutures.awaitAll(5, TimeUnit.SECONDS,futures.toArray(new RedisFuture[futures.size()])); // laterconnection.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> { @Override public 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 worktry (StatefulRedisClusterConnection<String, String> connection = pool.borrowObject()) { connection.sync().set("key", "value"); connection.sync().blpop(10, "list");} // terminatingpool.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