乐趣区

关于java:初探-Redis-客户端-Lettuce真香

一、Lettuce 是啥?

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

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

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

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

“当初很多都是用 Lettuce 了,你们不会不晓得吧?”老王推了推眼镜淡淡地说道,随即缓缓关上镜片后那双心灵的窗户,用关心的眼神仰视着咱们几只菜鸡。

Lettuce?生菜?满头雾水的我连忙关上了 Redis 官网的客户端列表。发现 Java 语言有三个官网举荐的实现:JedisLettuce 和 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]#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> {
    @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 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

退出移动版