Redis客戶端Lettuce源碼【三】Lettuce是如何發(fā)送Command命令到redis的

lettuce-core版本: 5.1.7.RELEASE

在上一篇介紹了Lettuce是如何基于Netty與Redis建立連接的介褥,其中提到了一個(gè)很重要的CommandHandler類,這一期會(huì)介紹CommandHandler是如何在發(fā)送Command到Lettuce中發(fā)揮作用的亿乳,以及Lettuce是如何實(shí)現(xiàn)多線程共享同一個(gè)物理連接的鲫售。
還是先看一下我們的示例代碼共螺,這一篇主要是跟進(jìn)去sync.get方法看看Lettuc是如何發(fā)送get命令到Redis以及是如何讀取Redis的命令的。

/**
 * @author xiaobing
 * @date 2019/12/20
 */
public class LettuceSimpleUse {
    private void testLettuce() throws ExecutionException, InterruptedException {
        //構(gòu)建RedisClient對(duì)象情竹,RedisClient包含了Redis的基本配置信息藐不,可以基于RedisClient創(chuàng)建RedisConnection
        RedisClient client = RedisClient.create("redis://localhost");

        //創(chuàng)建一個(gè)線程安全的StatefulRedisConnection,可以多線程并發(fā)對(duì)該connection操作,底層只有一個(gè)物理連接.
        StatefulRedisConnection<String, String> connection = client.connect();

        //獲取SyncCommand秦效。Lettuce支持SyncCommand雏蛮、AsyncCommands、ActiveCommand三種command
        RedisStringCommands<String, String> sync = connection.sync();
        String value = sync.get("key");
        System.out.println("get redis value with lettuce sync command, value is :" + value);

        //獲取SyncCommand阱州。Lettuce支持SyncCommand挑秉、AsyncCommands、ActiveCommand三種command
        RedisAsyncCommands<String, String> async = connection.async();
        RedisFuture<String> getFuture = async.get("key");
        value = getFuture.get();
        System.out.println("get redis value with lettuce async command, value is :" + value);
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        new LettuceSimpleUse().testLettuce();
    }
}

在看sync.get方法之前先看一下RedisStringCommands是如何生成生成的苔货,從下面的代碼可以看到RedisStringCommands其實(shí)是對(duì)RedisAsyncCommands<String, String>方法調(diào)用的同步阻塞版本犀概。

    //創(chuàng)建一個(gè)sync版本的RedisCommand
    protected RedisCommands<K, V> newRedisSyncCommandsImpl() {
                //async()方法返回的就是該Connection對(duì)應(yīng)的RedisAsyncCommand
        return syncHandler(async(), RedisCommands.class, RedisClusterCommands.class);
    }
    //返回一個(gè)動(dòng)態(tài)代理類立哑,代理類的實(shí)現(xiàn)在FutureSyncInvocationHandler類中
    protected <T> T syncHandler(Object asyncApi, Class<?>... interfaces) {
        FutureSyncInvocationHandler h = new FutureSyncInvocationHandler((StatefulConnection<?, ?>) this, asyncApi, interfaces);
                //基于FutureSyncInvocationHandler生成動(dòng)態(tài)代理類
        return (T) Proxy.newProxyInstance(AbstractRedisClient.class.getClassLoader(), interfaces, h);
    }
        //異步轉(zhuǎn)同步的關(guān)鍵
    class FutureSyncInvocationHandler extends AbstractInvocationHandler {

            ...

            @Override
            @SuppressWarnings("unchecked")
            protected Object handleInvocation(Object proxy, Method method, Object[] args) throws Throwable {

                    try {

                            Method targetMethod = this.translator.get(method);
                            Object result = targetMethod.invoke(asyncApi, args);

                            // RedisAsyncCommand返回的大部分對(duì)象類型都是RedisFuture類型的
                            if (result instanceof RedisFuture<?>) {

                                    RedisFuture<?> command = (RedisFuture<?>) result;

                                    if (isNonTxControlMethod(method.getName()) && isTransactionActive(connection)) {
                                            return null;
                                    }
                                    //獲取配置的超時(shí)時(shí)間
                                    long timeout = getTimeoutNs(command);
                                    //阻塞的等待RedisFuture返回結(jié)果
                                    return LettuceFutures.awaitOrCancel(command, timeout, TimeUnit.NANOSECONDS);
                            }

                            return result;
                    } catch (InvocationTargetException e) {
                            throw e.getTargetException();
                    }
            }
        }
        ...

所以sync.get操作最終調(diào)用的還是async.get操作,接下來(lái)看async.get是怎么做的姻灶。還是先看一張時(shí)序圖铛绰,心里有一個(gè)概念。

AbstractRedisAsyncCommands

    @Override
    public RedisFuture<V> get(K key) {
        return dispatch(commandBuilder.get(key));
    }

commandBuilder.get(key)

這一步驟主要是根據(jù)用戶的輸入?yún)?shù)key产喉、命令類型get捂掰、序列化方式來(lái)生成一個(gè)command對(duì)象。而這個(gè)command對(duì)象會(huì)按照Redis的協(xié)議格式把命令序列化成字符串曾沈。

    Command<K, V, V> get(K key) {
        notNullKey(key);
        //Valueoutput基于序列化
        return createCommand(GET, new ValueOutput<>(codec), key);
    }
        
    protected <T> Command<K, V, T> createCommand(CommandType type, CommandOutput<K, V, T> output, K key) {
        CommandArgs<K, V> args = new CommandArgs<K, V>(codec).addKey(key);
        return createCommand(type, output, args);
    }
        
    protected <T> Command<K, V, T> createCommand(CommandType type, CommandOutput<K, V, T> output, CommandArgs<K, V> args) {
        return new Command<K, V, T>(type, output, args);
    }
        

AbstractRedisAsyncCommands.dispatch

public <T> AsyncCommand<K, V, T> dispatch(RedisCommand<K, V, T> cmd) {
        //用AsyncCommand對(duì)RedisCommand做一個(gè)包裝處理这嚣,這個(gè)AsyncCommand實(shí)現(xiàn)了RedisFuture接口,最后返回給調(diào)用方的就是這個(gè)對(duì)象晦譬。當(dāng)Lettuce收到Redis的返回結(jié)果時(shí)會(huì)調(diào)用AsyncCommand的complete方法疤苹,異步的方式返回?cái)?shù)據(jù)。
        AsyncCommand<K, V, T> asyncCommand = new AsyncCommand<>(cmd);
        //調(diào)用connection的dispatch方法把Command發(fā)送給Redis敛腌,這個(gè)connection就是上一篇中說(shuō)的那個(gè)StatefulRedisConnectionImpl
        RedisCommand<K, V, T> dispatched = connection.dispatch(asyncCommand);
        if (dispatched instanceof AsyncCommand) {
                return (AsyncCommand<K, V, T>) dispatched;
        }
        return asyncCommand;
}

StatefulRedisConnectionImpl.dispatch

    @Override
    public <T> RedisCommand<K, V, T> dispatch(RedisCommand<K, V, T> command) {
                //對(duì)command做預(yù)處理卧土,當(dāng)前主要是根據(jù)不同的命令配置一些異步處理,如:auth命令之后成功之后把password寫入到相應(yīng)變量中像樊,select db操作成功之后把db值寫入到相應(yīng)變量中等等尤莺。
        RedisCommand<K, V, T> toSend = preProcessCommand(command);

        try {
                        //真正的dispatch是在父類實(shí)現(xiàn)的
            return super.dispatch(toSend);
        } finally {
            if (command.getType().name().equals(MULTI.name())) {
                multi = (multi == null ? new MultiOutput<>(codec) : multi);
            }
        }
    }
    //父類RedisChannelHandler的dispatch方法
    protected <T> RedisCommand<K, V, T> dispatch(RedisCommand<K, V, T> cmd) {

            if (debugEnabled) {
                    logger.debug("dispatching command {}", cmd);
            }
            //tracingEnable的代碼先不用看
            if (tracingEnabled) {

                    RedisCommand<K, V, T> commandToSend = cmd;
                    TraceContextProvider provider = CommandWrapper.unwrap(cmd, TraceContextProvider.class);

                    if (provider == null) {
                            commandToSend = new TracedCommand<>(cmd, clientResources.tracing()
                                            .initialTraceContextProvider().getTraceContext());
                    }

                    return channelWriter.write(commandToSend);
            }
            //其實(shí)就是直接調(diào)用channelWriter.write方法,而這個(gè)channelWriter就是上一節(jié)說(shuō)的那個(gè)屏蔽底層channel實(shí)現(xiàn)的DefaultEndpoint類
            return channelWriter.write(cmd);
    }

DefaultEndpoint.write

    @Override
    public <K, V, T> RedisCommand<K, V, T> write(RedisCommand<K, V, T> command) {

        LettuceAssert.notNull(command, "Command must not be null");

            try {
                    //sharedLock是Lettuce自己實(shí)現(xiàn)的一個(gè)共享排他鎖生棍。incrementWriters相當(dāng)于獲取一個(gè)共享鎖颤霎,當(dāng)channel狀態(tài)發(fā)生變化的時(shí)候,如斷開連接時(shí)會(huì)獲取排他鎖執(zhí)行一些清理操作涂滴。
                    sharedLock.incrementWriters();
                    // validateWrite是驗(yàn)證當(dāng)前操作是否可以執(zhí)行友酱,Lettuce內(nèi)部維護(hù)了一個(gè)保存已經(jīng)發(fā)送但是還沒有收到Redis消息的Command的stack,可以配置這個(gè)stack的長(zhǎng)度柔纵,防止Redis不可用時(shí)stack太長(zhǎng)導(dǎo)致內(nèi)存溢出缔杉。如果這個(gè)stack已經(jīng)滿了,validateWrite會(huì)拋出異常
                    validateWrite(1);
                    //autoFlushCommands默認(rèn)為true搁料,即每執(zhí)行一個(gè)Redis命令就執(zhí)行Flush操作發(fā)送給Redis或详,如果設(shè)置為false,則需要手動(dòng)flush郭计。由于flush操作相對(duì)較重霸琴,在某些場(chǎng)景下需要繼續(xù)提升Lettuce的吞吐量可以考慮設(shè)置為false。
                    if (autoFlushCommands) {
                            if (isConnected()) {
                                    //寫入channel并執(zhí)行flush操作昭伸,核心在這個(gè)方法的實(shí)現(xiàn)中
                                    writeToChannelAndFlush(command);
                            } else {
                                    // 如果當(dāng)前channel連接已經(jīng)斷開就先放入Buffer中梧乘,直接返回AsyncCommand,重連之后會(huì)把Buffer中的Command再次嘗試通過(guò)channel發(fā)送到Redis中
                                    writeToDisconnectedBuffer(command);
                            }

                    } else {
                            writeToBuffer(command);
                    }
            } finally {
                    //釋放共享鎖
                    sharedLock.decrementWriters();
                    if (debugEnabled) {
                            logger.debug("{} write() done", logPrefix());
                    }
            }

            return command;
    }

DefaultEndpoint.writeToChannelAndFlush

    private void writeToChannelAndFlush(RedisCommand<?, ?, ?> command) {
                //queueSize字段做cas +1操作
        QUEUE_SIZE.incrementAndGet(this);
                
        ChannelFuture channelFuture = channelWriteAndFlush(command);
                //Lettuce的可靠性:保證最多一次庐杨。由于Lettuce的保證是基于內(nèi)存的宋下,所以并不可靠(系統(tǒng)crash時(shí)內(nèi)存數(shù)據(jù)會(huì)丟失)
        if (reliability == Reliability.AT_MOST_ONCE) {
            // cancel on exceptions and remove from queue, because there is no housekeeping
            channelFuture.addListener(AtMostOnceWriteListener.newInstance(this, command));
        }
                //Lettuce的可靠性:保證最少一次嗡善。由于Lettuce的保證是基于內(nèi)存的,所以并不可靠(系統(tǒng)crash時(shí)內(nèi)存數(shù)據(jù)會(huì)丟失)
        if (reliability == Reliability.AT_LEAST_ONCE) {
            // commands are ok to stay within the queue, reconnect will retrigger them
            channelFuture.addListener(RetryListener.newInstance(this, command));
        }
    }
        
        //可以看到最終還是調(diào)用了channle的writeAndFlush操作学歧,這個(gè)Channel就是netty中的NioSocketChannel
        private ChannelFuture channelWriteAndFlush(RedisCommand<?, ?, ?> command) {

        if (debugEnabled) {
            logger.debug("{} write() writeAndFlush command {}", logPrefix(), command);
        }

        return channel.writeAndFlush(command);
    }

到這里其實(shí)就牽扯到Netty的Channel、EventLoop相關(guān)概念了各吨,簡(jiǎn)單的說(shuō)channel會(huì)把需要write的對(duì)象放入Channel對(duì)應(yīng)的EventLoop的隊(duì)列中就返回了枝笨,EventLoop是一個(gè)SingleThreadEventExector,它會(huì)回調(diào)Bootstrap時(shí)配置的CommandHandler的write方法

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {

    if (debugEnabled) {
            logger.debug("{} write(ctx, {}, promise)", logPrefix(), msg);
    }
                
    if (msg instanceof RedisCommand) {
        //如果是單個(gè)的RedisCommand就直接調(diào)用writeSingleCommand返回
        writeSingleCommand(ctx, (RedisCommand<?, ?, ?>) msg, promise);
        return;
    }

    if (msg instanceof List) {

        List<RedisCommand<?, ?, ?>> batch = (List<RedisCommand<?, ?, ?>>) msg;

        if (batch.size() == 1) {

                writeSingleCommand(ctx, batch.get(0), promise);
                return;
        }
        //批量寫操作揭蜒,暫不關(guān)心
        writeBatch(ctx, batch, promise);
        return;
    }

    if (msg instanceof Collection) {
        writeBatch(ctx, (Collection<RedisCommand<?, ?, ?>>) msg, promise);
    }
}

writeSingleCommand 核心在這里

Lettuce使用單一連接支持多線程并發(fā)向Redis發(fā)送Command横浑,那Lettuce是怎么把請(qǐng)求Command與Redis返回的結(jié)果對(duì)應(yīng)起來(lái)的呢,秘密就在這里屉更。

private void writeSingleCommand(ChannelHandlerContext ctx, RedisCommand<?, ?, ?> command, ChannelPromise promise)
 {

    if (!isWriteable(command)) {
            promise.trySuccess();
            return;
    }
    //把當(dāng)前command放入一個(gè)特定的棧中徙融,這一步是關(guān)鍵
    addToStack(command, promise);
    // Trace操作,暫不關(guān)心
    if (tracingEnabled && command instanceof CompleteableCommand) {
            ...
    }
    //調(diào)用ChannelHandlerContext把命令真正發(fā)送給Redis瑰谜,當(dāng)然在發(fā)送給Redis之前會(huì)由CommandEncoder類對(duì)RedisCommand進(jìn)行編碼后寫入ByteBuf
    ctx.write(command, promise);
    
    private void addToStack(RedisCommand<?, ?, ?> command, ChannelPromise promise) {

        try {
            //再次驗(yàn)證隊(duì)列是否滿了欺冀,如果滿了就拋出異常
            validateWrite(1);
            //command.getOutput() == null意味這個(gè)這個(gè)Command不需要Redis返回影響。一般不會(huì)走這個(gè)分支
            if (command.getOutput() == null) {
                    // fire&forget commands are excluded from metrics
                    complete(command);
            }
            //這個(gè)應(yīng)該是用來(lái)做metrics統(tǒng)計(jì)用的萨脑,暫時(shí)先不考慮
            RedisCommand<?, ?, ?> redisCommand = potentiallyWrapLatencyCommand(command);
            //無(wú)論promise是什么類型的隐轩,最終都會(huì)把command放入到stack中,stack是一個(gè)基于數(shù)組實(shí)現(xiàn)的雙向隊(duì)列
            if (promise.isVoid()) {
                    //如果promise不是Future類型的就直接把當(dāng)前command放入到stack
                    stack.add(redisCommand);
            } else {
                    //如果promise是Future類型的就等f(wàn)uture完成后把當(dāng)前command放入到stack中渤早,當(dāng)前場(chǎng)景下就是走的這個(gè)分支
                    promise.addListener(AddToStack.newInstance(stack, redisCommand));
            }
        } catch (Exception e) {
            command.completeExceptionally(e);
            throw e;
        }
    }
}

那么Lettuce收到Redis的回復(fù)消息之后是怎么通知RedisCommand职车,并且把結(jié)果與RedisCommand對(duì)應(yīng)上的呢。Netty在收到Redis服務(wù)端返回的消息之后就會(huì)回調(diào)CommandHandler的channelRead方法

public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        ByteBuf input = (ByteBuf) msg;

        ...

        try {
            ...
                        //重點(diǎn)在這里
            decode(ctx, buffer);
        } finally {
            input.release();
        }
    }
        
        protected void decode(ChannelHandlerContext ctx, ByteBuf buffer) throws InterruptedException {
                //如果stack為空鹊杖,則直接返回悴灵,這個(gè)時(shí)候一般意味著返回的結(jié)果找到對(duì)應(yīng)的RedisCommand了
        if (pristine && stack.isEmpty() && buffer.isReadable()) {

            ...

            return;
        }

        while (canDecode(buffer)) {
                        //重點(diǎn)來(lái)了。從stack的頭上取第一個(gè)RedisCommand
            RedisCommand<?, ?, ?> command = stack.peek();
            if (debugEnabled) {
                logger.debug("{} Stack contains: {} commands", logPrefix(), stack.size());
            }

            pristine = false;

            try {
                                //直接把返回的結(jié)果buffer給了stack頭上的第一個(gè)RedisCommand骂蓖。
                                //decode操作實(shí)際上拿到RedisCommand的commandoutput對(duì)象對(duì)Redis的返回結(jié)果進(jìn)行反序列化的积瞒。
                if (!decode(ctx, buffer, command)) {
                    return;
                }
            } catch (Exception e) {

                ctx.close();
                throw e;
            }

            if (isProtectedMode(command)) {
                onProtectedMode(command.getOutput().getError());
            } else {

                if (canComplete(command)) {
                    stack.poll();

                    try {
                        complete(command);
                    } catch (Exception e) {
                        logger.warn("{} Unexpected exception during request: {}", logPrefix, e.toString(), e);
                    }
                }
            }

            afterDecode(ctx, command);
        }

        if (buffer.refCnt() != 0) {
            buffer.discardReadBytes();
        }
    }

從上面的代碼可以看出來(lái),當(dāng)Lettuce收到Redis的回復(fù)消息時(shí)就從stack的頭上取第一個(gè)RedisCommand涯竟,這個(gè)RedisCommand就是與該Redis返回結(jié)果對(duì)應(yīng)的RedisCommand赡鲜。為什么這樣就能對(duì)應(yīng)上呢,是因?yàn)長(zhǎng)ettuce與Redis之間只有一條tcp連接庐船,在Lettuce端放入stack時(shí)是有序的银酬,tcp協(xié)議本身是有序的,redis是單線程處理請(qǐng)求的筐钟,所以Redis返回的消息也是有序的揩瞪。這樣就能保證Redis中返回的消息一定對(duì)應(yīng)著stack中的第一個(gè)RedisCommand。當(dāng)然如果連接斷開又重連了篓冲,這個(gè)肯定就對(duì)應(yīng)不上了李破,Lettuc對(duì)斷線重連也做了特殊處理宠哄,防止對(duì)應(yīng)不上。

Command.encode

public void encode(ByteBuf buf) {
                
        buf.writeByte('*');
                //寫入?yún)?shù)的數(shù)量
        CommandArgs.IntegerArgument.writeInteger(buf, 1 + (args != null ? args.count() : 0));
                //換行
        buf.writeBytes(CommandArgs.CRLF);
                //寫入命令的類型嗤攻,即get
        CommandArgs.BytesArgument.writeBytes(buf, type.getBytes());

        if (args != null) {
                        //調(diào)用Args的編碼毛嫉,這里面就會(huì)使用我們之前配置的codec序列化,當(dāng)前使用的是String.UTF8
            args.encode(buf);
        }
    }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末妇菱,一起剝皮案震驚了整個(gè)濱河市承粤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌闯团,老刑警劉巖辛臊,帶你破解...
    沈念sama閱讀 212,185評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異房交,居然都是意外死亡彻舰,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,445評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門候味,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)刃唤,“玉大人,你說(shuō)我怎么就攤上這事负溪⊥复В” “怎么了?”我有些...
    開封第一講書人閱讀 157,684評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵川抡,是天一觀的道長(zhǎng)辐真。 經(jīng)常有香客問我,道長(zhǎng)崖堤,這世上最難降的妖魔是什么侍咱? 我笑而不...
    開封第一講書人閱讀 56,564評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮密幔,結(jié)果婚禮上楔脯,老公的妹妹穿的比我還像新娘。我一直安慰自己胯甩,他們只是感情好昧廷,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,681評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著偎箫,像睡著了一般木柬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上淹办,一...
    開封第一講書人閱讀 49,874評(píng)論 1 290
  • 那天眉枕,我揣著相機(jī)與錄音,去河邊找鬼。 笑死速挑,一個(gè)胖子當(dāng)著我的面吹牛谤牡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播姥宝,決...
    沈念sama閱讀 39,025評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼翅萤,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了腊满?” 一聲冷哼從身側(cè)響起断序,我...
    開封第一講書人閱讀 37,761評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎糜烹,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體漱凝,經(jīng)...
    沈念sama閱讀 44,217評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡疮蹦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,545評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了茸炒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片愕乎。...
    茶點(diǎn)故事閱讀 38,694評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖壁公,靈堂內(nèi)的尸體忽然破棺而出感论,到底是詐尸還是另有隱情,我是刑警寧澤紊册,帶...
    沈念sama閱讀 34,351評(píng)論 4 332
  • 正文 年R本政府宣布比肄,位于F島的核電站,受9級(jí)特大地震影響囊陡,放射性物質(zhì)發(fā)生泄漏芳绩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,988評(píng)論 3 315
  • 文/蒙蒙 一撞反、第九天 我趴在偏房一處隱蔽的房頂上張望妥色。 院中可真熱鬧,春花似錦遏片、人聲如沸嘹害。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,778評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)笔呀。三九已至,卻和暖如春线衫,著一層夾襖步出監(jiān)牢的瞬間凿可,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,007評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留枯跑,地道東北人惨驶。 一個(gè)月前我還...
    沈念sama閱讀 46,427評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像敛助,于是被迫代替她去往敵國(guó)和親粗卜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,580評(píng)論 2 349

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

  • background netty 是一個(gè)異步事件驅(qū)動(dòng)的網(wǎng)絡(luò)通信層框架纳击,其官方文檔的解釋為 Netty is a N...
    高級(jí)java架構(gòu)師閱讀 613評(píng)論 0 0
  • 一 簡(jiǎn)單概念 RPC:(Remote Procedure Call),遠(yuǎn)程調(diào)用過(guò)程,是通過(guò)網(wǎng)絡(luò)調(diào)用遠(yuǎn)程計(jì)算機(jī)的進(jìn)程...
    Java大生閱讀 514評(píng)論 0 1
  • 一 簡(jiǎn)單概念 RPC:(Remote Procedure Call),遠(yuǎn)程調(diào)用過(guò)程,是通過(guò)網(wǎng)絡(luò)調(diào)用遠(yuǎn)程計(jì)算機(jī)的進(jìn)程...
    Java大生閱讀 775評(píng)論 0 0
  • 簡(jiǎn)述這一章是netty源碼分析系列的第一章续扔,在這一章中只展示Netty的客戶端和服務(wù)端的初始化和啟動(dòng)的過(guò)程,給讀者...
    水欣閱讀 1,496評(píng)論 0 0
  • 安全性 設(shè)置客戶端連接后進(jìn)行任何其他指令前需要使用的密碼焕数。 警告:因?yàn)閞edis 速度相當(dāng)快纱昧,所以在一臺(tái)比較好的服...
    OzanShareing閱讀 1,682評(píng)論 1 7