初看Redis客户端Lettuce:真好吃
在一次技术讨论会上,大家讨论Redis的Java客户端哪个更好。我立即大喊‘杰迪斯,是的! '
“Jedis是官方客户端,简单易用,所有公司都用它作为中间件,除了Jedis还有谁能打吗?”我直接把王渣扔了。
刚学Spring的小张很不满:“SpringDataRedis都用RedisTemplate!Jedis?不存在。”
“秀儿坐下,SpringDataRedis是基于Jedis进行封装的。”旁边,李大哥喝了一口新开的快乐水,嘴角微微扬起,露出一丝不屑。
“现在很多人都用生菜,你知道吗?”老王推了推眼镜,平静的说道,然后缓缓打开了镜片后面的心灵之窗,用关爱的目光低头看着我们这些新人。
莴苣?莴苣?一头雾水,我赶紧打开Redis官网的客户端列表。发现Java语言官方推荐的实现有三种:Jedis、Lettuce和Redission。
生菜是什么客户?从来没有听说过。但我发现它的官方介绍是最长的:
用于线程安全同步、异步和反应式使用的高级Redis 客户端。支持集群、哨兵、管道和编解码器。
我赶紧查字典翻译:
高级客户端线程安全支持同步、异步和反应式API,支持集群、哨兵、管道和编解码器。老王摆摆手,示意我把字典收起来,慢慢地介绍。
1.1 高级客户端
“主人,请翻译官翻译一下,什么(嘟——)叫(嘟——)高级客户端?”
“高级客户端,高级,就是高级!新的可以立即使用,不需要担心任何实现细节,直接接业务逻辑,直接跳转。”
1.2 线程安全
这是与绝地武士的主要区别之一。
Jedis的连接实例是线程不安全的,所以需要维护一个连接池。每个线程在需要时从连接池中取出连接实例,并在完成操作或遇到异常后返回该实例。当连接数量随着业务不断增长时,物理连接的消耗也将成为性能和稳定性的潜在风险点。
Lettuce使用Netty作为通信层组件。它的连接实例是线程安全的,当满足条件时,它可以访问操作系统的原生调用epoll、kqueue等以获得性能提升。
我们知道,虽然一个Redis服务器实例可以同时连接多个客户端来发送和接收命令,但每个实例在执行命令时都是单线程的。
这意味着,如果应用程序能够通过多线程+单连接的方式操作Redis,就可以减少Redis服务器上的连接总数,并且在多个应用程序共享同一台Redis服务器时可以获得更好的稳定性和性能。对于应用程序来说,也减少了维护多个连接实例的资源消耗。
1.3 支持同步、异步和反应式API
Lettuce从一开始就是按照非阻塞IO来设计的。它是一个纯异步客户端,全面支持异步和反应式API。
即使是同步命令,底层通信过程仍然是异步模型,只是通过阻塞调用线程来模拟同步效果。
1.4 支持集群、哨兵、管道和编解码器
“这些功能都是标准的,Lettuce是高级客户端!高级,你明白吗?”老王说完这句话,兴奋地用手指着桌子,但似乎并不想多做介绍。我默默记下好好学习的笔记。
(项目使用过程中,管道机制比Jedis稍微抽象一些,下面给出使用过程中遇到的坑和解决方案。)
1.5 Spring中的使用
除了官方对Redis的介绍之外,我们还可以发现,当Spring Data Redis升级到2.0时,Lettuce也升级到了5.0。事实上,Lettuce从SpringDataRedis 1.6开始就已经正式集成;而SpringSessionDataRedis直接使用Lettuce作为默认的Redis客户端,可见其成熟性和稳定性。
Jedis 是众所周知的,甚至是事实上的标准驱动程序。它推出较早(2010年9月发布1.0.0版,2011年3月发布Lettuce 1.0.0),其API简单易用。诸如对Redis新功能的最快支持等特性是密不可分的。
2. Jedis和Lettuce的主要区别是什么?
说了这么多,Lettuce 和老客户Jedis 的主要区别是什么?我们可以看一下Spring Data Redis帮助文档中给出的对比表:
注:其中 X 标记的是支持.
经过比较可以发现:
支持Jedis支持的Lettuce; Jedis不支持的生菜也支持!春天生菜的使用越来越多也就不足为奇了。
3. 生菜初体验
3.1 快速入门
如果即使是最简单的示例也令人困惑,那么该库就不会流行。 Lettuce的快速启动速度确实很快:
一个。引入maven依赖(其他依赖类似,具体参见文末参考资料)
依赖项groupIdio.lettuce/groupId artifactIdlettuce-core/artifactId version5.3.6.RELEASE/version/dependencyb。填写Redis地址,连接,执行,关闭。完美的!
导入io.lettuce.core.*; //语法: redis://[密码@]主机[:端口][/databaseNumber]//语法: redis://[用户名:密码@]主机[:端口][/数据库编号]RedisClient redisClient=RedisClient. create('redis://password@localhost:6379/0');StatefulRedisConnectionString, 字符串连接=redisClient.connect();RedisCommandsString, 字符串syncCommands=connection.sync(); syncCommands.set('key', '你好,Redis!' ); connection.close();redisClient.shutdown();3.2 是否支持集群模式?支持!
Redis Cluster是官方提供的Redis Sharding解决方案。大家应该都很熟悉了,我就不多介绍了。官方文档请参考Redis Cluster 101。
要将Lettuce连接到Redis集群,只需将上面的客户端代码逐行更改即可:
//语法: redis://[password@]host[:port]//语法: redis://[username:password@]host[:port]RedisClusterClient redisClient=RedisClusterClient.create('redis://password@localhost:7379'); 3.3 支持是否高可靠?支持!
Redis Sentinel是官方提供的高可靠性解决方案。 Sentinel可以在实例发生故障时自动切换到从节点继续提供服务。官方文档请参考Redis Sentinel文档。
只需替换客户端的创建方式即可:
//语法: redis-sentinel://[密码@]主机[:port][,host2[:port2]][/databaseNumber]#sentinelMasterIdRedisClient redisClient=RedisClient.create('redis-sentinel: //localhost:26379,localhost:26380/0#my master' ) ;3.4 集群下是否支持pipeline?支持!
Jedis虽然有pipeline命令,但是无法支持Redis Cluster。一般情况下,在批量执行管道之前,需要合并每个key所在的slot和实例。
尽管Lettuce声称支持管道,但它并没有直接看到管道API。这是怎么回事?
3.4.1 实现 pipeline
使用AsyncCommands和flushCommands来实现管道。阅读官方文档后我们可以知道,Lettuce的同步和异步命令实际上共享同一个连接实例,底层使用管道来发送/接收命令。
区别在于:
通过connection.sync()方法获得的同步命令对象,每次操作都会立即通过TCP连接发送命令;通过connection.async()获取的异步命令对象会获取RedisFuture吗?执行操作后。当满足某些条件时仅在某些情况下分批发送。由此,我们可以通过异步命令+手动批量推送的方式来实现管道。我们看一下官方的例子:
StatefulRedisConnectionString,字符串连接=client.connect();RedisAsyncCommandsString,字符串命令=connection.async(); //禁用自动刷新命令。setAutoFlushCommands(false); //执行一系列独立的调用ListRedisFuture? futures=Lists.newArrayList() ;for (int i=0; i 次迭代; i++) {futures.add(commands.set('key-' + i, 'value-' + i));futures.add(commands .expire('key-' + i, 3600));} //将所有命令写入传输层commands.flushCommands(); //同步example: 等待所有futures 完成boolean result=LettuceFutures.awaitAll(5, TimeUnit.SECONDS,futures.toArray(new RedisFuture[ futures.size()])); //后来连接.close();
3.4.2 这么做有没有问题?
乍一看很完美,但实际上存在陷阱:
设置setAutoFlushCommands(false)后,你会发现sync()方法调用的同步命令没有返回!这是为什么呢?我们看一下官方文档:
Lettuce 是一个非阻塞异步客户端。它提供了一个同步API 来实现基于每个线程的阻塞行为,以创建等待(同步)命令响应.第一个请求返回后,第一个线程的程序流程将继续,而第二个请求则继续执行。由Redis处理并在某个时间点返回
sync和async的底层实现是一样的,只不过sync是通过阻塞调用线程来模拟同步操作。而setAutoFlushCommands从源码中可以发现它作用于连接对象,所以这个操作对同步和异步命令对象都生效。
因此,只要在一个线程中将自动刷新命令设置为false,就会影响使用该连接实例的所有其他线程。
/*** 用于Redis 连接的异步且线程安全的API。** @param K 键类型。* @param V 值类型。* @author Will Glozer* @author Mark Paluch*/public 抽象类AbstractRedisAsyncCommandsK,V 实现RedisHashAsyncCommandsK、V、RedisKeyAsyncCommandsK、V、RedisStringAsyncCommandsK、V、RedisListAsyncCommandsK、V、RedisSetAsyncCommandsK、V、RedisSortedSetAsyncCommandsK、V、RedisScriptingAsyncCommandsK、V、RedisServerAsyncCommandsK、V、Redis HLLAsyncCommandsK、V、BaseRedisAsyncCommandsK、V、RedisTransactionalAsync命令K、V、RedisGeoAsyncCommandsK、V、RedisClusterAsyncCommandsK , V { @Override public void setAutoFlushCommands(boolean autoFlush) { connection.setAutoFlushCommands(autoFlush); }相应的,如果多个线程调用async()获取异步命令集,并在自己的业务逻辑完成后调用flushCommands(),那么仍然有其他线程追加的异步命令会被强制flush,逻辑上不执行的命令会被强制flush。属于整个批次的将被分成多个副本并发送。
虽然不影响结果的准确性,但是如果线程之间互相干扰,分散了彼此发送的命令,那么性能的提升会很不稳定。
我们自然会想:每一个批处理命令都会创建一个连接,然后……这不就和Jedis一样依赖连接池吗?
想起老王镜头后那双撕心裂肺的眼神,我决定再深入挖掘一下。果然,再次仔细阅读文档后,我发现了另一个好东西:Batch Execution。
3.4.3 Batch Execution
既然flushCommands会对连接产生全局影响,那么将flush限制在线程级别还不够吗?我从文档中找到了示例官方示例。
回顾上一篇文章,Lettuce 是一个高级客户端。看完文档,发现确实很高级。只需要定义一个接口(让人想起MyBatis的Mapper接口)。以下是项目中使用的示例:
/** * 定义将使用的批处理命令*/@BatchSize(100) public interface RedisBatchQuery extends Commands, BatchExecutor { RedisFuturebyte[] get(byte[] key); RedisFutureSetbyte[] smembers(byte[] key); RedisFutureListbyte[ ] lrange(byte[] key, 长开始, 长结束); RedisFutureMapbyte[], byte[] hgetall(byte[] key);} 调用时,执行以下操作:
//创建客户端RedisClusterClient client=RedisClusterClient.create(DefaultClientResources.create(), 'redis://' + 地址); //工厂实例保存在服务中并且仅创建一次。第二个参数表示使用byte[]编码和解码的键和值。 RedisCommandFactory 工厂=new RedisCommandFactory(connect, Arrays.asList(ByteArrayCodec.INSTANCE, ByteArrayCodec.INSTANCE)); //在哪里使用,创建一个查询实例代理类来调用命令,最后刷入命令ListRedisFuture? 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配置的缓存命令数量时进行批量操作。对于连接实例,不需要设置自动刷新命令,保持默认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(主机、端口)); GenericObjectPoolStatefulRedisClusterConnectionString, 字符串池=ConnectionPoolSupport .createGenericObjectPool(() - clusterClient.connect(), new GenericObjectPoolConfig()); //执行worktry (StatefulRedisClusterConnectionString, String connection=pool.borrowObject()) { connection.sync().set('key', 'value'); connection.sync().blpop(10, 'list');} //terminationpool.close();clusterClient .shutdown();这里需要说明的是:createGenericObjectPool创建一个连接池,并将wrapConnections参数默认设置为true。此时,会重载借出对象的close方法,通过动态代理返回连接;如果设置为false,close 方法将关闭连接。
Lettuce还支持异步连接池(从连接池获取连接是一个异步操作)。详细内容请参考文章末尾的链接。功能很多,无法一一列举。你可以在官方文档中找到说明和示例,值得一读。
4. 使用总结
与Jedis相比,Lettuce使用起来更加方便快捷,并且抽象程度较高。并且通过线程安全的连接,减少了系统的连接数量,提高了系统的稳定性。
对于高级玩家,Lettuce还提供了很多配置和接口,方便性能优化和深度业务定制场景。
另外不得不说的是,Lettuce的官方文档非常全面、详细,这是非常难得的。社区比较活跃,Committer 会积极回答各种问题,这使得很多问题可以自己解决。
相比之下,Jedis 的文档、维护和更新都比较慢。 JedisCluster pipeline 的PR 四年前(2021 年2 月)尚未合并。
参考
其中两个 GitHub 的 issue 含金量很高,强烈推荐一读!
1.生菜快速入门:https://lettuce.io
2.Redis Java 客户端
3.生菜官网:https://lettuce.io
4.SpringDataRedis参考文档
5.关于流水线的问题
6.为什么Spring Session Redis默认使用Lettuce作为Redis客户端
7.Cluster-specific options: https://lettuce.io 8. Lettuce 连接池 9.客户端配置: https://lettuce.io/core/release
用户评论
作为游戏开发者,我在项目初期就考虑了Redis客户端Lettuce,从初步接触到现在已经离不开它。
有16位网友表示赞同!
Lettuce让我的数据库交互效率提升了一个档次!真的是物超所值。
有9位网友表示赞同!
从安装到使用都超级方便,完美适配了我们的游戏开发流程,非常推荐给需要高可用性的项目。
有5位网友表示赞同!
相比之前的Redis客户端,Lettuce的性能简直惊人。不论是读写速度还是内存占用都低得多。
有5位网友表示赞同!
尤其是对于并发请求处理能力上,Lettuce几乎能无缝应对我们游戏服务器的大流量挑战。
有9位网友表示赞同!
Lettuce在处理复杂场景时显得游刃有余,优化了我们的游戏服务架构和用户体验。
有16位网友表示赞同!
开始尝试Lettuce有些犹豫,但现在看真是我的眼光毒辣啊。真的很香!
有17位网友表示赞同!
通过使用Lettuce,我们节省了大量的时间在调优数据库方面,精力可以花在更有趣的游戏内容上。
有11位网友表示赞同!
游戏性能提升了不止一个台阶,得益于Lettuce带来的高效和稳定的网络连接管理。
有6位网友表示赞同!
Lettuce稳定性极高,在处理高并发流量时完全没有出现过错误或延迟问题,这使我们充满信心。
有20位网友表示赞同!
我被Redis的高性能吸引了,但Lettuce不仅提供了高性能,还带来了便利性和简洁性。
有7位网友表示赞同!
Lettuce是游戏开发中的强大武器,特别是在需要快速响应和数据高效管理的游戏环境中。
有16位网友表示赞同!
从一开始对新工具抱有抵触到如今成为忠实用户,Lettuce改变了我对 Redis 客户端的全部看法。
有11位网友表示赞同!
对于游戏服务器的优化来说,选择Lettuce是最正确的决定。简单、高效、稳定。
有10位网友表示赞同!
在尝试过很多其他Redis客户端之后,Lettuce让我们团队真正体验到了Redis带来的便利性和强大功能。
有5位网友表示赞同!
不夸张地说,Lettuce是我最满意的游戏开发决策之一。它极大地提高了我们服务器的性能表现。
有6位网友表示赞同!
Lettuce不仅增加了我们的开发效率,而且还稳定地维护了玩家体验,这在竞争激烈的市场中至关重要。
有6位网友表示赞同!
对于一个需要深度集成数据库处理的游戏项目来说,Lettuce简直是无价之宝。
有5位网友表示赞同!
自从用上了Lettuce,我们团队的技能提升和游戏性能优化都得到了显著改善。真香!
有9位网友表示赞同!
Lettuce不仅让我们的游戏数据管理更高效,还减少了错误发生的机会,让游戏体验更加流畅。
有19位网友表示赞同!
在快节奏的游戏开发中找到这样一颗高性能、易用又稳定的Redis客户端,就像找到了宝藏。
有13位网友表示赞同!
对于任何游戏开发者而言,Lettuce是一个不容错过的选择。无论是功能强大还是性能惊人,都能满足你的需要。
有13位网友表示赞同!