一祖乳、Lettuce 是啥?
一次技術(shù)討論會上秉氧,大家說起 Redis 的 Java 客戶端哪家強(qiáng)眷昆,我第一時(shí)間毫不猶豫地喊出 "Jedis, YES!"
“Jedis 可是官方客戶端汁咏,用起來直接省事亚斋,公司中間件都用它。除了 Jedis 外難道還有第二個(gè)能打的梆暖?”我直接扔出王炸伞访。
剛學(xué) Spring 的小張聽了不服:“SpringDataRedis 都用 RedisTemplate!Jedis轰驳?不存在的厚掷。”
“坐下吧秀兒级解,SpringDataRedis 就是基于 Jedis 封裝的冒黑。”旁邊李哥呷了一口剛開的快樂水勤哗,嘴角微微上揚(yáng)抡爹,露出一絲不屑兔魂。
“現(xiàn)在很多都是用 Lettuce 了癌瘾,你們不會不知道吧绷杜?”老王推了推眼鏡淡淡地說道所森,隨即緩緩打開鏡片后那雙心靈的窗戶盖彭,用關(guān)懷的眼神俯視著我們幾只菜雞兰粉。
Lettuce棚赔?生菜单寂?滿頭霧水的我趕緊打開了 Redis 官網(wǎng)的客戶端列表拼苍。發(fā)現(xiàn) Java 語言有三個(gè)官方推薦的實(shí)現(xiàn):Jedis笑诅、Lettuce和 Redission。
(截圖來源:https://redis.io/clients#java)
Lettuce 是什么客戶端?沒聽過吆你。但發(fā)現(xiàn)它的官方介紹最長:
Advanced Redis client for thread-safe sync, async, and reactive usage. Supports Cluster, Sentinel, Pipelining, and codecs.
趕緊查著字典翻譯了下:
高級客戶端
線程安全
支持同步弦叶、異步和反應(yīng)式 API
支持集群、哨兵妇多、管道和編解碼
老王擺擺手示意我收好字典伤哺,不緊不慢介紹起來。
1.1 高級客戶端
“師爺砌梆,你給翻譯翻譯默责,什么(嗶——)叫做(嗶——)高級客戶端?”
“高級客戶端嘛咸包,高級嘛桃序,就是 Advanced 啊烂瘫!new 一下就能用媒熊,什么實(shí)現(xiàn)細(xì)節(jié)都不用管,拿起業(yè)務(wù)邏輯直接突突坟比÷ⅲ”
1.2 線程安全
這是和 Jedis 主要不同之一。
Jedis 的連接實(shí)例是線程不安全的葛账,于是需要維護(hù)一個(gè)連接池柠衅,每個(gè)線程需要時(shí)從連接池取出連接實(shí)例,完成操作后或者遇到異常歸還實(shí)例籍琳。當(dāng)連接數(shù)隨著業(yè)務(wù)不斷上升時(shí)菲宴,對物理連接的消耗也會成為性能和穩(wěn)定性的潛在風(fēng)險(xiǎn)點(diǎn)。
Lettuce 使用 Netty 作為通信層組件趋急,其連接實(shí)例是線程安全的喝峦,并且在條件具備時(shí)可訪問操作系統(tǒng)原生調(diào)用 epoll, kqueue 等獲得性能提升。
我們知道 Redis 服務(wù)端實(shí)例雖然可以同時(shí)連接多個(gè)客戶端收發(fā)命令呜达,但每個(gè)實(shí)例執(zhí)行命令時(shí)都是單線程的谣蠢。
這意味著如果應(yīng)用可以通過多線程+單連接方式操作 Redis,將能夠精簡 Redis 服務(wù)端的總連接數(shù)查近,而多應(yīng)用共享同一個(gè) Redis 服務(wù)端時(shí)也能夠獲得更好的穩(wěn)定性和性能眉踱。對于應(yīng)用來說也減少了維護(hù)多個(gè)連接實(shí)例的資源消耗。
1.3 支持同步霜威、異步和反應(yīng)式 API
Lettuce 從一開始就按照非阻塞式 IO 進(jìn)行設(shè)計(jì)谈喳,是一個(gè)純異步客戶端,對異步和反應(yīng)式 API 的支持都很全面侥祭。
即使是同步命令叁执,底層的通信過程仍然是異步模型,只是通過阻塞調(diào)用線程來模擬出同步效果而已矮冬。
1.4 支持集群谈宛、哨兵、管道和編解碼
“這些特性都是標(biāo)配胎署,Lettuce 可是高級客戶端吆录!高級,懂嗎琼牧?”老王說到這里興奮地用手指點(diǎn)著桌面恢筝,但似乎不想多做介紹,我默默地記下打算好好學(xué)習(xí)一番巨坊。
(在項(xiàng)目使用過程中撬槽,pipeling 機(jī)制用起來和 Jedis 相比稍微抽象已點(diǎn),下文會給出在使用過程中遇到的小坑和解決辦法趾撵。)
1.5 在 Spring 中的使用情況
除了 Redis 官方介紹侄柔,我們也可以發(fā)現(xiàn) Spring Data Redis 在升級到 2.0 時(shí),將 Lettuce 升級到了 5.0占调。其實(shí) Lettuce 早就在 SpringDataRedis 1.6 時(shí)就被官方集成了暂题;而 SpringSessionDataRedis 則直接將 Lettuce 作為默認(rèn) Redis 客戶端,足見其成熟和穩(wěn)定究珊。
Jedis 廣為人知甚至是事實(shí)上的標(biāo)準(zhǔn) Java 客戶端(de-facto standard driver)薪者,和它推出時(shí)間早(1.0.0 版本 2010 年 9 月,Lettuce 1.0.0 是 2011 年 3 月)剿涮、API 直接易用言津、對 Redis 新特性支持最快等特點(diǎn)都密不可分。
但 Lettuce 作為后進(jìn)幔虏,其優(yōu)勢和易用性也獲得了 Spring 等社區(qū)的青睞纺念。下面會分享我們在項(xiàng)目中集成 Lettuce 時(shí)的經(jīng)驗(yàn)總結(jié),供大家參考想括。
二陷谱、Jedis 和 Lettuce 有啥主要區(qū)別?
說了這么多瑟蜈,Lettuce 和老牌客戶端 Jedis 主要都有哪些區(qū)別呢烟逊?我們可以看下 Spring Data Redis 幫助文檔給出的對比表格:
(截圖來源:https://docs.spring.io)
注:其中 X 標(biāo)記的是支持.
經(jīng)過比較我們可以發(fā)現(xiàn):
Jedis 支持的 Lettuce 都支持;
Jedis 不支持的 Lettuce 也支持铺根!
這么看來 Spring 中越來越多地使用 Lettuce 也就不奇怪了宪躯。
三、Lettuce 初體驗(yàn)
光說不練假把式位迂,給大家分享我們嘗試 Lettuce 時(shí)的收獲访雪,尤其是批量命令部分花了比較多的時(shí)間踩坑详瑞,下文詳解。
3.1 快速開始
如果最簡單的例子都令人費(fèi)解臣缀,那這個(gè)庫肯定流行不起來坝橡。Lettuce 的快速開始真的夠快:
a. 引入 maven 依賴(其他依賴類似,具體可見文末參考資料)
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>5.3.6.RELEASE</version>
</dependency>
b. 填上 Redis 地址精置,連接计寇、執(zhí)行、關(guān)閉脂倦。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 方案蝶押,大家應(yīng)該非常熟悉不再多介紹,官方文檔可參考 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 可以在實(shí)例故障時(shí)自動切換到從節(jié)點(diǎn)繼續(xù)提供服務(wù)离陶,官方文檔可參考 Redis Sentinel Documentation。
仍然是替換客戶端的創(chuàng)建方式就可以了:
// 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哀军。一般都需要自行歸并各個(gè) key 所在的 slot 和實(shí)例后再批量執(zhí)行 pipeline沉眶。
官網(wǎng)對集群下的 pipeline 支持 PR 截至本文寫作時(shí)(2021年2月)四年過去了仍然未合入,可見 Cluster pipelining杉适。
Lettuce 雖然號稱支持 pipeling谎倔,但并沒有直接看到 pipeline 這種 API,這是怎么回事猿推?
3.4.1 實(shí)現(xiàn) pipeline
使用 AsyncCommands 和 flushCommands 實(shí)現(xiàn) pipeline片习,經(jīng)過閱讀官方文檔可以知道,Lettuce 的同步蹬叭、異步命令其實(shí)都共享同一個(gè)連接實(shí)例藕咏,底層使用 pipeline 的形式在發(fā)送/接收命令。
區(qū)別在于:
connection.sync() 方法獲取的同步命令對象秽五,每一個(gè)操作都會立刻將命令通過 TCP 連接發(fā)送出去孽查;
connection.async() 獲取的異步命令對象,執(zhí)行操作后得到的是 RedisFuture<?>坦喘,在滿足一定條件的情況下才批量發(fā)送盲再。
由此我們可以通過異步命令+手動批量推送的方式來實(shí)現(xiàn) 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 這么做有沒有問題?
乍一看很完美答朋,但其實(shí)有暗坑:setAutoFlushCommands(false) 設(shè)置后济榨,會發(fā)現(xiàn) sync() 方法調(diào)用的同步命令都不返回了!這是為什么呢绿映?我們再看看官方文檔:
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 在底層實(shí)現(xiàn)上都是一樣的,只是 sync 通過阻塞調(diào)用線程的方式模擬了同步操作腐晾。并且 setAutoFlushCommands 通過源碼可以發(fā)現(xiàn)就是作用在 connection 對象上叉弦,于是該操作對 sync 和 async 命令對象都生效。
所以藻糖,只要某個(gè)線程中設(shè)置了 auto flush commands 為 false淹冰,就會影響到所有使用該連接實(shí)例的其他線程。
/**
* 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);
}
}
對應(yīng)的巨柒,如果多個(gè)線程調(diào)用 async() 獲取異步命令集樱拴,并在自身業(yè)務(wù)邏輯完成后調(diào)用 flushCommands(),那將會強(qiáng)行 flush 其他線程還在追加的異步命令洋满,原本邏輯上屬于整批的命令將被打散成多份發(fā)送晶乔。
雖然對于結(jié)果的正確性不影響,但如果因?yàn)榫€程相互影響打散彼此的命令進(jìn)行發(fā)送牺勾,則對性能的提升就會很不穩(wěn)定正罢。
自然我們會想到:每個(gè)批命令創(chuàng)建一個(gè) connection,然后……這不和 Jedis 一樣也是靠連接池么驻民?
回想起老王鏡片后那穿透靈魂的目光翻具,我打算硬著頭皮再挖掘一下。果然回还,再次認(rèn)真閱讀文檔后我發(fā)現(xiàn)了另外一個(gè)好東西:Batch Execution裆泳。
3.4.3 Batch Execution
既然 flushCommands 會對 connection 產(chǎn)生全局影響,那把 flush 限制在線程級別不就行了柠硕?我從文檔中找到了示例官方示例工禾。
回想起前文 Lettuce 是高級客戶端,看了文檔后發(fā)現(xiàn)確實(shí)高級蝗柔,只需要定義接口就行了(讓人想起 MyBatis 的 Mapper 接口)帜篇,下面是項(xiàng)目中使用的例子:
/
/**
* 定義會用到的批量命令
*/
@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);
}
調(diào)用時(shí)這樣操作:
// 創(chuàng)建客戶端
RedisClusterClient client = RedisClusterClient.create(DefaultClientResources.create(), "redis://" + address);
// service 中持有 factory 實(shí)例,只創(chuàng)建一次诫咱。第二個(gè)參數(shù)表示 key 和 value 使用 byte[] 編解碼
RedisCommandFactory factory = new RedisCommandFactory(connect, Arrays.asList(ByteArrayCodec.INSTANCE, ByteArrayCodec.INSTANCE));
// 使用的地方笙隙,創(chuàng)建一個(gè)查詢實(shí)例代理類調(diào)用命令,最后刷入命令
List<RedisFuture<?>> futures = new ArrayList<>();
RedisBatchQuery batchQuery = factory.getCommands(RedisBatchQuery.class);
for (RedisMetaGroup redisMetaGroup : redisMetaGroups) {
// 業(yè)務(wù)邏輯坎缭,循環(huán)調(diào)用多個(gè) key 并將結(jié)果保存到 futures 結(jié)果中
appendCommand(redisMetaGroup, futures, batchQuery);
}
// 異步命令調(diào)用完成后執(zhí)行 flush 批量執(zhí)行竟痰,此時(shí)命令才會發(fā)送給 Redis 服務(wù)端
batchQuery.flush();
就是這么簡單签钩。
此時(shí)批量的控制將在線程粒度上進(jìn)行,并在調(diào)用 flush 或達(dá)到 @BatchSize 配置的緩存命令數(shù)量時(shí)執(zhí)行批量操作坏快。而對于 connection 實(shí)例铅檩,不用再設(shè)置 auto flush commands,保持默認(rèn)的 true 即可莽鸿,對其他線程不造成影響昧旨。
ps:優(yōu)秀、嚴(yán)謹(jǐn)?shù)哪憧隙〞氲剑喝绻麊蚊顖?zhí)行耗時(shí)長或者誰放了個(gè)諸如 BLPOP 的命令的話祥得,肯定會造成影響的兔沃,這個(gè)話題官方文檔也有涉及,可以考慮使用連接池來處理级及。
3.5 還能再給力一點(diǎn)嗎乒疏?
Lettuce 支持的當(dāng)然不僅僅是上面所說的簡單功能,還有這些也值得一試:
3.5.1 讀寫分離
我們知道 Redis 實(shí)例是支持主從部署的饮焦,從實(shí)例異步地從主實(shí)例同步數(shù)據(jù)怕吴,并借助 Redis Sentinel 在主實(shí)例故障時(shí)進(jìn)行主從切換。
當(dāng)應(yīng)用對數(shù)據(jù)一致性不敏感县踢、又需要較大吞吐量時(shí)转绷,可以考慮主從讀寫分離方式。Lettuce 可以設(shè)置 StatefulRedisClusterConnection 的 readFrom 配置來進(jìn)行調(diào)整:
3.5.2 配置自動更新集群拓?fù)?/h3>
當(dāng)使用 Redis Cluster 時(shí)硼啤,服務(wù)端發(fā)生了擴(kuò)容怎么辦暇咆?
Lettuce 早就考慮好了——通過 RedisClusterClient#setOptions 方法傳入 ClusterClientOptions 對象即可配置相關(guān)參數(shù)(全部配置見文末參考鏈接)。
ClusterClientOptions 中的 topologyRefreshOptions 常見配置如下:
3.5.3 連接池
雖然 Lettuce 基于線程安全的單連接實(shí)例已經(jīng)具有非常好的性能丙曙,但也不排除有些大型業(yè)務(wù)需要通過線程池來提升吞吐量爸业。另外對于事務(wù)性操作是有必要獨(dú)占連接的亏镰。
Lettuce 基于 Apache Common-pool2 組件提供了連接池的能力(以下是官方提供的 RedisCluster 對應(yīng)的客戶端線程池使用示例):
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 創(chuàng)建連接池默認(rèn)設(shè)置 wrapConnections 參數(shù)為 true。此時(shí)借出的對象 close 方法將通過動態(tài)代理的方式重載為歸還連接索抓;若設(shè)置為 false 則 close 方法會關(guān)閉連接。
Lettuce 也支持異步的連接池(從連接池獲取連接為異步操作)逼肯,詳情可參考文末鏈接。還有很多特性不能一一列舉篮幢,都可以在官方文檔上找到說明和示例,十分值得一讀三椿。
四葫辐、使用總結(jié)
Lettuce 相較于Jedis,使用上更加方便快捷伴郁,抽象度高耿战。并且通過線程安全的連接降低了系統(tǒng)中的連接數(shù)量,提升了系統(tǒng)的穩(wěn)定性焊傅。
對于高級玩家剂陡,Lettuce 也提供了很多配置、接口狐胎,方便對性能進(jìn)行優(yōu)化和實(shí)現(xiàn)深度業(yè)務(wù)定制的場景鸭栖。
另外不得不說的一點(diǎn),Lettuce 的官方文檔寫的非常全面細(xì)致顽爹,十分難得。社區(qū)比較活躍骆姐,Commiter 會積極回答各類 issue镜粤,這使得很多疑問都可以自助解決。
相比之下玻褪,Jedis 的文檔肉渴、維護(hù)更新速度就比較慢了。JedisCluster pipeline 的 PR 至今(2021年2月)四年過去還未合入带射。
參考資料
其中兩個(gè) GitHub 的 issue 含金量很高同规,強(qiáng)烈推薦一讀!
1.Lettuce 快速開始:https://lettuce.io
3.Lettuce 官網(wǎng):https://lettuce.io
4.SpringDataRedis 參考文檔
6.Why is Lettuce the default Redis client used in Spring Session Redis
7.Cluster-specific options:https://lettuce.io
9.客戶端配置:https://lettuce.io/core/release
10.SSL配置:https://lettuce.io
作者:vivo互聯(lián)網(wǎng)數(shù)據(jù)智能團(tuán)隊(duì)-Li Haoxuan