Redis Lettuce 客戶端與IO阻塞VS非阻塞 同步VS異步的總結(jié)

概念簡介

簡單介紹闷沥,不做過多描述,網(wǎng)上很多詳細(xì)介紹

  • TCP/IP
    屬于網(wǎng)絡(luò)協(xié)議
  • SOCKET 為內(nèi)核向用戶提供的可以實(shí)現(xiàn)各種網(wǎng)絡(luò)通信協(xié)議的api
  • FD Linux 一切皆文件,進(jìn)程可以打開成百上千個(gè)文件起胰,為了表示和區(qū)分已經(jīng)打開的文件,Linux 會(huì)給每個(gè)文件分配一個(gè)編號(一個(gè) ID)惩琉,這個(gè)編號就是一個(gè)整數(shù)哮独,被稱為文件描述符(File Descriptor)

IO模型(Unix 網(wǎng)絡(luò)編程P124)

首先一個(gè)IO操作其實(shí)分成了兩個(gè)步驟:發(fā)起IO請求和實(shí)際的IO操作,

同步IO和異步IO的區(qū)別就在于第二個(gè)步驟是否阻塞洼滚,如果實(shí)際的IO讀寫阻塞請求進(jìn)程埂息,那么就是同步IO,因此阻塞IO遥巴、非阻塞IO千康、IO復(fù)用、信號驅(qū)動(dòng)IO都是同步IO铲掐,如果不阻塞拾弃,而是操作系統(tǒng)幫你做完IO操作再將結(jié)果返回給你,那么就是異步IO摆霉。

阻塞IO和非阻塞IO的區(qū)別在于第一步豪椿,發(fā)起IO請求是否會(huì)被阻塞,如果阻塞直到完成那么就是傳統(tǒng)的阻塞IO携栋,如果不阻塞搭盾,那么就是非阻塞IO。

同步:所謂同步婉支,就是在發(fā)出一個(gè)功能調(diào)用時(shí)鸯隅,在沒有得到結(jié)果之前,該調(diào)用就不返回磅摹。

異步:異步的概念和同步相對滋迈。當(dāng)一個(gè)異步過程調(diào)用發(fā)出后,調(diào)用者不能立刻得到結(jié)果户誓。實(shí)際處理這個(gè)調(diào)用的部件在完成后饼灿,通過狀態(tài)、通知和回調(diào)來通知調(diào)用者帝美。

  • 阻塞blocking I/O
    可讀事件和實(shí)際數(shù)據(jù)讀全過程全阻塞


    blocking-io.jpg

-非阻塞 nonblocking I/O
可讀事件由用戶態(tài)輪詢,數(shù)據(jù)讀取過程阻塞


non-blocking-io.jpg
  • IO復(fù)用 I/O multiplexing (select and poll)


    multiplexing-io.jpg

1.2 IO多路復(fù)用的歷史
select, poll, epoll 都是I/O多路復(fù)用的具體的實(shí)現(xiàn)碍彭,之所以有這三個(gè)鬼存在,其實(shí)是他們出現(xiàn)是有先后順序的悼潭。

I/O多路復(fù)用這個(gè)概念被提出來以后庇忌, select是第一個(gè)實(shí)現(xiàn) (1983 左右在BSD里面實(shí)現(xiàn)的)。

select
select 被實(shí)現(xiàn)以后舰褪,很快就暴露出了很多問題皆疹。

select 會(huì)修改傳入的參數(shù)數(shù)組,這個(gè)對于一個(gè)需要調(diào)用很多次的函數(shù)占拍,是非常不友好的略就。

select 如果任何一個(gè)sock(I/O stream)出現(xiàn)了數(shù)據(jù)捎迫,select 僅僅會(huì)返回,但是并不會(huì)告訴你是那個(gè)sock上有數(shù)據(jù)表牢,于是你只能自己一個(gè)一個(gè)的找窄绒,10幾個(gè)sock可能還好,要是幾萬的sock每次都找一遍崔兴,這個(gè)無謂的開銷就頗有海天盛筵的豪氣了彰导。

select 只能監(jiān)視1024個(gè)鏈接, 這個(gè)跟草榴沒啥關(guān)系哦敲茄,linux 定義在頭文件中的位谋,參見FD_SETSIZE。

select 不是線程安全的折汞,如果你把一個(gè)sock加入到select, 然后突然另外一個(gè)線程發(fā)現(xiàn)倔幼,尼瑪,這個(gè)sock不用爽待,要收回损同。對不起,這個(gè)select 不支持的鸟款,如果你喪心病狂的竟然關(guān)掉這個(gè)sock, select的標(biāo)準(zhǔn)行為是膏燃。。呃何什。组哩。不可預(yù)測的, 這個(gè)可是寫在文檔中的哦.

“If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”
霸不霸氣

poll
于是14年以后(1997年)一幫人又實(shí)現(xiàn)了poll, poll 修復(fù)了select的很多問題处渣,比如

poll 去掉了1024個(gè)鏈接的限制伶贰,于是要多少鏈接呢, 主人你開心就好罐栈。

poll 從設(shè)計(jì)上來說黍衙,不再修改傳入數(shù)組,不過這個(gè)要看你的平臺了荠诬,所以行走江湖琅翻,還是小心為妙。

其實(shí)拖14年那么久也不是效率問題柑贞, 而是那個(gè)時(shí)代的硬件實(shí)在太弱方椎,一臺服務(wù)器處理1千多個(gè)鏈接簡直就是神一樣的存在了,select很長段時(shí)間已經(jīng)滿足需求钧嘶。

但是poll仍然不是線程安全的棠众, 這就意味著,不管服務(wù)器有多強(qiáng)悍有决,你也只能在一個(gè)線程里面處理一組I/O流摄欲。你當(dāng)然可以那多進(jìn)程來配合了轿亮,不過然后你就有了多進(jìn)程的各種問題。

select/poll的幾大缺點(diǎn)
1胸墙、每次調(diào)用select/poll,都需要把fd集合用戶態(tài)拷貝到內(nèi)核態(tài)按咒,這個(gè)開銷在fd很多時(shí)會(huì)很大
2迟隅、同時(shí)每次調(diào)用select/poll都需要在內(nèi)核遍歷傳遞進(jìn)來的所有fd,這個(gè)開銷在fd很多時(shí)也很大
3励七、針對select支持的文件描述符數(shù)量太小了智袭,默認(rèn)是1024
4.select返回的是含有整個(gè)句柄的數(shù)組,應(yīng)用程序需要遍歷整個(gè)數(shù)組才能發(fā)現(xiàn)哪些句柄發(fā)生了事件掠抬;
5.select的觸發(fā)方式是水平觸發(fā)吼野。(個(gè)人理解:如交易系統(tǒng)每筆交易會(huì)觸發(fā)一次,一次就是把所有fd集合從用戶態(tài)拷貝到內(nèi)核態(tài)两波,所有表示select觸發(fā)頻率也很高)

epoll
于是5年以后, 在2002, 大神 Davide Libenzi 實(shí)現(xiàn)了epoll.

epoll 可以說是I/O 多路復(fù)用最新的一個(gè)實(shí)現(xiàn)瞳步,epoll 修復(fù)了poll 和select絕大部分問題, 比如:
epoll 現(xiàn)在是線程安全的。
epoll 現(xiàn)在不僅告訴你sock組里面數(shù)據(jù)腰奋,還會(huì)告訴你具體哪個(gè)sock有數(shù)據(jù)单起,你不用自己去找了。

  1. select的本質(zhì)是采用32個(gè)整數(shù)的32位劣坊,即32*32= 1024來標(biāo)識嘀倒,fd值為1->1024。當(dāng)fd的值超過1024限制時(shí)局冰,就必須修改FD_SETSIZE的大小测蘑。這個(gè)時(shí)候就可以標(biāo)識32*max值范圍的fd。
    這種設(shè)計(jì)遍歷的過程非晨刀快碳胳,因?yàn)橛梦坏倪壿嫴僮骶涂梢?br> 缺點(diǎn):
    a. fd超過1024時(shí),性能無法滿足赠摇,在linux 早期并發(fā)沒有那么大固逗,還可以大范圍支持。
    b. 網(wǎng)絡(luò)一般分兩步操作藕帜,一步是獲取io事件烫罩,另一步是數(shù)據(jù)讀取,從內(nèi)核態(tài)讀到用戶態(tài).select兩步都是阻塞的

  2. poll
    沒有1024限制洽故,但依然是兩步IO都是阻塞的

  3. epoll
    沒有1024 限制贝攒,但第二步依賴阻塞,這也是java nio 所謂的同步非阻塞IO

  • signal driven I/O (SIGIO)


    signal-driven-io.jpg
  1. 異步 IO
    兩步都不阻塞
  • asynchronous I/O (the POSIX aio_functions)


    asynchronous-io.jpg
  • 比較


    compare-io.gif

Synchronous I/O versus Asynchronous I/O

POSIX defines these two terms as follows:

  • A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes.
  • An asynchronous I/O operation does not cause the requesting process to be blocked.

Using these definitions, the first four I/O models blocking, nonblocking, I/O multiplexing, and signal-driven I/O are all synchronous because the actual I/O operation (recvfrom) blocks the process. Only the asynchronous I/O model matches the asynchronous I/O definition.

requesting process 可以理解為用戶態(tài)當(dāng)前進(jìn)程

長鏈接VS短鏈接

TCP協(xié)議中有長連接和短連接之分时甚。短連接在數(shù)據(jù)包發(fā)送完成后就會(huì)自己斷開隘弊,長連接在發(fā)包完畢后哈踱,會(huì)在一定的時(shí)間內(nèi)保持連接,即我們通常所說的Keepalive(存活定時(shí)器)功能梨熙。

默認(rèn)的Keepalive超時(shí)需要7,200,000 milliseconds开镣,即2小時(shí),探測次數(shù)為5次咽扇。它的功效和用戶自己實(shí)現(xiàn)的心跳機(jī)制是一樣的邪财。開啟Keepalive功能需要消耗額外的寬帶和流量,盡管這微不足道质欲,但在按流量計(jì)費(fèi)的環(huán)境下增加了費(fèi)用树埠,另一方面,Keepalive設(shè)置不合理時(shí)可能會(huì)因?yàn)槎虝旱木W(wǎng)絡(luò)波動(dòng)而斷開健康的TCP連接嘶伟。

image
  • 痹醣铮活

keepalive并不是TCP規(guī)范的一部分。在Host Requirements RFC羅列有不使用它的三個(gè)理由:

1)在短暫的故障期間九昧,它們可能引起一個(gè)良好連接(good connection)被釋放(dropped)绊袋,(2)它們消費(fèi)了不必要的寬帶,

3)在以數(shù)據(jù)包計(jì)費(fèi)的互聯(lián)網(wǎng)上它們(額外)花費(fèi)金錢耽装。

然而愤炸,需要在應(yīng)用層實(shí)現(xiàn)心跳檢測與鏈接重連

  1. 網(wǎng)絡(luò)單通

  2. 連接被防火墻Hand 住

  3. 長時(shí)間GC或者通信線程發(fā)生非預(yù)期異常

  4. 會(huì)導(dǎo)致連接不可用,而又無法及時(shí)發(fā)現(xiàn)掉奄。

應(yīng)用場景

  • 長鏈接
  1. 鏈接固定且頻繁
  • 長連接多用于操作頻繁规个,點(diǎn)對點(diǎn)的通訊,而且連接數(shù)不能太多情況姓建。每個(gè)TCP連接都需要三步握手诞仓,這需要時(shí)間,如果每個(gè)操作都是先連接速兔,再操作的話那么處理速度會(huì)降低很多墅拭,所以每個(gè)操作完后都不斷開,每次處理時(shí)直接發(fā)送數(shù)據(jù)包就OK了涣狗,不用建立TCP連接谍婉。例如:數(shù)據(jù)庫的連接用長連接, 如果用短連接頻繁的通信會(huì)造成socket錯(cuò)誤镀钓,而且頻繁的socket 創(chuàng)建也是對資源的浪費(fèi)穗熬。
  1. 推送業(yè)務(wù)
  • 短鏈接

    a. 隨機(jī)訪問

  • 而像WEB網(wǎng)站的http服務(wù)一般都用短鏈接,因?yàn)殚L連接對于服務(wù)端來說會(huì)耗費(fèi)一定的資源丁溅,而像WEB網(wǎng)站這么頻繁的成千上萬甚至上億客戶端的連接用短連接會(huì)更省一些資源唤蔗,如果用長連接,而且同時(shí)有成千上萬的用戶,如果每個(gè)用戶都占用一個(gè)連接的話妓柜,資源占用太大箱季。所以并發(fā)量大,但每個(gè)用戶無需頻繁操作情況下需用短連好棍掐。

實(shí)際場景分析(lettuce 客戶端)

redis 的lettuce客戶端

  • 驗(yàn)證客戶端的connection與tcp鏈接的關(guān)系藏雏,即與fd的關(guān)系,這里tcp物理連接與fd一一對應(yīng).


    TCP協(xié)議

由于tcp是網(wǎng)絡(luò)層協(xié)議作煌,主要關(guān)系圖中的第三層诉稍,主要幾個(gè)字段source-->ip:port到destination ip:port

  • 驗(yàn)證代碼
    引自 lettuce官方example
/*
 * Copyright 2011-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.lambdaworks.examples;

import com.lambdaworks.redis.RedisClient;
import com.lambdaworks.redis.RedisURI;
import com.lambdaworks.redis.api.StatefulRedisConnection;
import com.lambdaworks.redis.api.sync.RedisCommands;

/**
 * @author Mark Paluch
 */
public class ReadWriteExample {
    public static void main(String[] args) {
        // Syntax: redis://[password@]host[:port][/databaseNumber]
        RedisClient redisClient = RedisClient.create(RedisURI.create("redis://localhost:6379/0"));
        StatefulRedisConnection<String, String> connection = redisClient.connect();
//創(chuàng)建10個(gè)connection
        for (int i=0;i<10;i++) {
            redisClient.connect();
        }
        System.out.println("Connected to Redis");
        RedisCommands<String, String> sync = connection.sync();
        sync.set("foo", "bar");
        String value = sync.get("foo");
        System.out.println(value);
        connection.close();
        redisClient.shutdown();
    }
}

redis-client 監(jiān)控結(jié)果
10個(gè)tcp鏈接對于10個(gè)fd

localhost:0>client list
"id=220 addr=127.0.0.1:62381 fd=13 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=221 addr=127.0.0.1:62382 fd=12 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=222 addr=127.0.0.1:62383 fd=11 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=223 addr=127.0.0.1:62384 fd=18 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=224 addr=127.0.0.1:62385 fd=14 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=225 addr=127.0.0.1:62386 fd=8 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=226 addr=127.0.0.1:62387 fd=15 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=227 addr=127.0.0.1:62388 fd=9 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=36 addr=127.0.0.1:57381 fd=16 name= age=18865 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
id=217 addr=127.0.0.1:62378 fd=20 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=218 addr=127.0.0.1:62379 fd=10 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=219 addr=127.0.0.1:62380 fd=19 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
"
localhost:0>

AbstractRedisClient相關(guān)netty代碼

{

        Bootstrap redisBootstrap = new Bootstrap();
        redisBootstrap.option(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 32 * 1024);
        redisBootstrap.option(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 8 * 1024);
        redisBootstrap.option(ChannelOption.ALLOCATOR, BUF_ALLOCATOR);

        SocketOptions socketOptions = getOptions().getSocketOptions();

        redisBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
                (int) socketOptions.getConnectTimeoutUnit().toMillis(socketOptions.getConnectTimeout()));

        if (LettuceStrings.isEmpty(redisURI.getSocket())) {
            redisBootstrap.option(ChannelOption.SO_KEEPALIVE, socketOptions.isKeepAlive());
            redisBootstrap.option(ChannelOption.TCP_NODELAY, socketOptions.isTcpNoDelay());
        }

        connectionBuilder.timeout(redisURI.getTimeout(), redisURI.getUnit());
        connectionBuilder.password(redisURI.getPassword());

        connectionBuilder.bootstrap(redisBootstrap);
        connectionBuilder.channelGroup(channels).connectionEvents(connectionEvents).timer(timer);
        connectionBuilder.commandHandler(handler).socketAddressSupplier(socketAddressSupplier).connection(connection);
        connectionBuilder.workerPool(genericWorkerPool);
    }

netty 客戶端的chnnel 創(chuàng)建

/**
     * Connect and initialize a channel from {@link ConnectionBuilder}.
     *
     * @param connectionBuilder must not be {@literal null}.
     * @return the {@link ConnectionFuture} to synchronize the connection process.
     * @since 4.4
     */
    @SuppressWarnings("unchecked")
    protected <K, V, T extends RedisChannelHandler<K, V>> ConnectionFuture<T> initializeChannelAsync(
            ConnectionBuilder connectionBuilder) {

        SocketAddress redisAddress = connectionBuilder.socketAddress();

        if (clientResources.eventExecutorGroup().isShuttingDown()) {
            throw new IllegalStateException("Cannot connect, Event executor group is terminated.");
        }

        logger.debug("Connecting to Redis at {}", redisAddress);

        CompletableFuture<Channel> channelReadyFuture = new CompletableFuture<>();
        Bootstrap redisBootstrap = connectionBuilder.bootstrap();

        RedisChannelInitializer initializer = connectionBuilder.build();
        redisBootstrap.handler(initializer);

        clientResources.nettyCustomizer().afterBootstrapInitialized(redisBootstrap);
        CompletableFuture<Boolean> initFuture = initializer.channelInitialized();
//物理TCP鏈接創(chuàng)建
        ChannelFuture connectFuture = redisBootstrap.connect(redisAddress);

        connectFuture.addListener(future -> {

            if (!future.isSuccess()) {

                logger.debug("Connecting to Redis at {}: {}", redisAddress, future.cause());
                connectionBuilder.commandHandler().initialState();
                channelReadyFuture.completeExceptionally(future.cause());
                return;
            }

            initFuture.whenComplete((success, throwable) -> {

                if (throwable == null) {
                    logger.debug("Connecting to Redis at {}: Success", redisAddress);
                    RedisChannelHandler<?, ?> connection = connectionBuilder.connection();
                    connection.registerCloseables(closeableResources, connection);
                    channelReadyFuture.complete(connectFuture.channel());
                    return;
                }

                logger.debug("Connecting to Redis at {}, initialization: {}", redisAddress, throwable);
                connectionBuilder.commandHandler().initialState();
                Throwable failure;

                if (throwable instanceof RedisConnectionException) {
                    failure = throwable;
                } else if (throwable instanceof TimeoutException) {
                    failure = new RedisConnectionException("Could not initialize channel within "
                            + connectionBuilder.getTimeout() + " " + connectionBuilder.getTimeUnit(), throwable);
                } else {
                    failure = throwable;
                }
                channelReadyFuture.completeExceptionally(failure);

                CompletableFuture<Boolean> response = new CompletableFuture<>();
                response.completeExceptionally(failure);

            });
        });

        return new DefaultConnectionFuture<>(redisAddress, channelReadyFuture.thenApply(channel -> (T) connectionBuilder
                .connection()));
    }

由代碼和監(jiān)控可確認(rèn),luttuce redis 的connection對netty的channel一一對應(yīng)最疆,同時(shí)服務(wù)器會(huì)一一創(chuàng)建tcp鏈接!(這里強(qiáng)調(diào)一下是lettuce的redis 客戶端蚤告,而spring的封裝并非如此

Spring Boot RedisTemplate

由于本地redis 這里的demo未使用集群模式努酸,不影響測試效果

  • 配置文件
#spring.redis.cluster.nodes= 192.168.2.10:9000,192.168.2.14:9001,192.168.2.13:9000
#spring.redis.cluster.max-redirects=3
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.timeout=5000ms

# Lettuce
# 連接池最大連接數(shù)(使用負(fù)值表示沒有限制)
spring.redis.lettuce.pool.max-active=8
# 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制)
spring.redis.lettuce.pool.max-wait=1ms
# 連接池中的最大空閑連接
spring.redis.lettuce.pool.max-idle=10
# 連接池中的最小空閑連接
spring.redis.lettuce.pool.min-idle=8
# 關(guān)閉超時(shí)時(shí)間
spring.redis.lettuce.shutdown-timeout=100ms
#驅(qū)逐時(shí)間 初始化延遲時(shí)間 默認(rèn)-1
#if (delay > 0L) 必須>時(shí)才初始化
(這句很重要,這個(gè)參數(shù)默認(rèn)為-1,不>0L則驅(qū)逐任務(wù)不會(huì)生成杜恰,池中的idel max 等參數(shù)等于沒配置获诈,不會(huì)生效。第二心褐,如果是默認(rèn)配置舔涎,即使生效也不會(huì)讀池中鏈接,更浪費(fèi)資源6旱M鱿印!代碼見下邊引用)
#spring.redis.lettuce.pool.time-between-eviction-runs=1s

引用驅(qū)逐器代碼
BaseGenericObjectPool

final void startEvictor(long delay) {
        Object var3 = this.evictionLock;
        synchronized(this.evictionLock) {
            EvictionTimer.cancel(this.evictor, this.evictorShutdownTimeoutMillis, TimeUnit.MILLISECONDS);
            this.evictor = null;
            this.evictionIterator = null;
            if (delay > 0L) {
                this.evictor = new BaseGenericObjectPool.Evictor();
                EvictionTimer.schedule(this.evictor, delay, delay);
            }

        }
    }
  • 測試代碼
package com.sparrow.spring.boot;

import com.sparrow.spring.Application;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.RedisClientInfo;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class})
public class RedisConnectionTest {
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    public void test() throws IOException {
        List<RedisClientInfo> redisClientInfos = redisTemplate.getClientList();
        List<RedisConnection> redisConnections = new ArrayList<>();
//模擬多線程訪問場景掘而,如果按lettuce 的邏輯挟冠,這里應(yīng)該每個(gè)鏈接都會(huì)對應(yīng)一個(gè)socket 鏈接?
        for (int i = 0; i < 200; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        redisTemplate.opsForValue().get("a" + System.currentTimeMillis());
                    }
                }
            }).start();
        }
        System.in.read();
    }
}

  • 執(zhí)行后的redis client list結(jié)果
    只有兩條鏈接(fd),其中一條為當(dāng)前client
localhost:0>client list
"id=36 addr=127.0.0.1:57381 fd=16 name= age=20394 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
id=250 addr=127.0.0.1:63325 fd=14 name= age=18 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=get
"
localhost:0>
  • 可見結(jié)果,spring boot redistemplate 不管開多少個(gè)線程袍睡,都只有一個(gè)鏈接知染,為什么?

摘自LettuceConnectionFactory官方代碼注釋
This factory creates a new @link LettuceConnection on each call to @link #getConnection(). Multiple @link LettuceConnection's share a single thread-safe native connection by default.
當(dāng)我們調(diào)用getConnection()方法獲取鏈接時(shí)斑胜,多個(gè)LettuceConnection會(huì)共享一個(gè)線程安全的 native connection(默認(rèn)情況控淡,意思就是說可以重寫,可以改)

/**
 * Connection factory creating <a >Lettuce</a>-based connections.
 * <p>
 * This factory creates a new {@link LettuceConnection} on each call to {@link #getConnection()}. Multiple
 * {@link LettuceConnection}s share a single thread-safe native connection by default.
當(dāng)我們調(diào)用getConnection()方法獲取鏈接時(shí)止潘,多個(gè)LettuceConnection會(huì)共享一個(gè)線程安全的`native connection`
 * <p>
 * The shared native connection is never closed by {@link LettuceConnection}, therefore it is not validated by default
 * on {@link #getConnection()}. Use {@link #setValidateConnection(boolean)} to change this behavior if necessary. Inject
 * a {@link Pool} to pool dedicated connections. If shareNativeConnection is true, the pool will be used to select a
 * connection for blocking and tx operations only, which should not share a connection. If native connection sharing is
 * disabled, the selected connection will be used for all operations.
 * <p>
 * ....
 */
public class LettuceConnectionFactory
        implements InitializingBean, DisposableBean, RedisConnectionFactory, ReactiveRedisConnectionFactory {

總結(jié)

  • redis 長鏈接情況下掺炭,物理鏈接非常少,甚至可以共享一個(gè)覆山,而并不影響并發(fā)效果竹伸,線上壓測結(jié)果 qps 40000左右,CPU不到2%,當(dāng)然測試效果與實(shí)際key有關(guān)勋篓,但5W左右是沒有問題的
  • redis 的client list 命令和info clients 命令可以查看當(dāng)前服務(wù)器的tcp鏈接數(shù)
  • tcp 物理鏈接與netty的channel一一對應(yīng),可以簡單理解netty的channel 是對tcp鏈接的封裝吧享,里邊實(shí)現(xiàn)了很多事。具體可以參考譬嚣,李林峰老師的《netty權(quán)威指南》
  • channel可以被多線程共享
  • tcp鏈接與fd一一對應(yīng)钢颂,受內(nèi)核的最大fd數(shù)限制,但非1024限制拜银,該值可以修改殊鞭,與內(nèi)存有關(guān),理論上不受限尼桶。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末操灿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子泵督,更是在濱河造成了極大的恐慌趾盐,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件小腊,死亡現(xiàn)場離奇詭異救鲤,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)秩冈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進(jìn)店門本缠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人入问,你說我怎么就攤上這事丹锹。” “怎么了队他?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵卷仑,是天一觀的道長。 經(jīng)常有香客問我麸折,道長锡凝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任垢啼,我火速辦了婚禮窜锯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘芭析。我一直安慰自己锚扎,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布馁启。 她就那樣靜靜地躺著驾孔,像睡著了一般芍秆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上翠勉,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天妖啥,我揣著相機(jī)與錄音,去河邊找鬼对碌。 笑死荆虱,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的朽们。 我是一名探鬼主播怀读,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼骑脱!你這毒婦竟也來了菜枷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤叁丧,失蹤者是張志新(化名)和其女友劉穎犁跪,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體歹袁,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年寝优,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了条舔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,646評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡乏矾,死狀恐怖孟抗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情钻心,我是刑警寧澤凄硼,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站捷沸,受9級特大地震影響摊沉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜痒给,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一说墨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧苍柏,春花似錦尼斧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春烛恤,著一層夾襖步出監(jiān)牢的瞬間母怜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工棒动, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留糙申,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓船惨,卻偏偏與公主長得像柜裸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子粱锐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容