jedis高級特性初探

首先我們要談一下redis,他是一款常見且常用的緩存數(shù)據(jù)庫。他有幾個顯著的特性讓開發(fā)與DBA所喜愛。

1. Redis支持數(shù)據(jù)的持久化,可以將內(nèi)存中的數(shù)據(jù)保持在磁盤中,重啟的時候可以再次加載進行使用。即使宕掉,數(shù)據(jù)也不會丟失。

2. Redis不僅僅支持簡單的key-value類型的數(shù)據(jù),同時還提供list,set,zset,hash等數(shù)據(jù)結(jié)構(gòu)的存儲。這樣就給開發(fā)同學(xué)提供了更多的選擇。

3. Redis支持數(shù)據(jù)的備份,即master-slave模式的數(shù)據(jù)備份,數(shù)據(jù)穩(wěn)定存在

jedis是redis的一個性能良好的客戶端,里面包含了所有的redis的特性,用起來相當(dāng)爽,本文著重講述jedis中的高級特性。本文實例是在2.9.0版本上測試通過。自己測試時可以引入下面的內(nèi)容,下面開始。

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

一. Publish/Subscribe(發(fā)布訂閱)
觀察者模式的具體體現(xiàn)。常用來解耦合,有很多版本都實現(xiàn)了此類的功能,比如說Rxjava,EventBus,Zk等。jedis也有自己的實現(xiàn),首先說一下原理:首先客戶端與服務(wù)端建立連接,客戶端注冊感興趣的事件,同時定義一個當(dāng)感興趣的事件過來時,需要如何處理的回調(diào)(繼承JedisPubSub類,覆寫里面的方法,例如onMessage,是消息接受的方法)。客戶端或設(shè)置與服務(wù)端的socket鏈接超時時間為0(不超時),然后阻塞在RedisInputStream中的read方法,等待消息的到來(也就是說RedisInputStream中有數(shù)據(jù))。當(dāng)有的客戶端發(fā)布了感興趣的信息,那么redis 的 server端就會查詢哪些client,關(guān)注了這個消息,然后,將結(jié)果放入該客戶端的RedisInputStream中,客戶端就接收了感興趣的消息,然后處理自己的邏輯。下面是測試代碼實例:

static class MyListener extends JedisPubSub {

    @Override
    public void onMessage(String channel, String message) {
        System.out.println(channel + "============" + message);
        super.onMessage(channel, message);
    }
}
 
@Test
public void testSubscribe() {
    jedis.subscribe(new MyListener(), "channel1");
}

@Test
public void testPublish() {
    jedis.publish("channel1", "message1");
}

現(xiàn)在跟著代碼看一下,關(guān)鍵性的步驟都增加了注釋

public void proceed(Client client, String... channels) {
  this.client = client;
  client.subscribe(channels);   //訂閱
  client.flush();               //數(shù)據(jù)flush,發(fā)送給server端
  process(client);              //do while 獲取數(shù)據(jù)
}
private void process(Client client) {

  do {
    List<Object> reply = client.getRawObjectMultiBulkReply();    //阻塞獲取server端傳遞的數(shù)據(jù),根據(jù)數(shù)據(jù),觸發(fā)相應(yīng)的回調(diào)
    final Object firstObj = reply.get(0);  //得到的列表中的第一個元素是服務(wù)端推送過來信息的事件類型,用來觸發(fā)客戶端定義的回調(diào)方法
    if (!(firstObj instanceof byte[])) {
      throw new JedisException("Unknown message type: " + firstObj);
    }
    final byte[] resp = (byte[]) firstObj;
    if (Arrays.equals(SUBSCRIBE.raw, resp)) {
      subscribedChannels = ((Long) reply.get(2)).intValue();
      final byte[] bchannel = (byte[]) reply.get(1);
      final String strchannel = (bchannel == null) ? null : SafeEncoder.encode(bchannel);
      onSubscribe(strchannel, subscribedChannels);  //訂閱上的回調(diào)
    } else if (Arrays.equals(UNSUBSCRIBE.raw, resp)) {
      subscribedChannels = ((Long) reply.get(2)).intValue();
      final byte[] bchannel = (byte[]) reply.get(1);
      final String strchannel = (bchannel == null) ? null : SafeEncoder.encode(bchannel);
      onUnsubscribe(strchannel, subscribedChannels); //取消后的回調(diào)
    } else if (Arrays.equals(MESSAGE.raw, resp)) {
      final byte[] bchannel = (byte[]) reply.get(1);
      final byte[] bmesg = (byte[]) reply.get(2);
      final String strchannel = (bchannel == null) ? null : SafeEncoder.encode(bchannel);
      final String strmesg = (bmesg == null) ? null : SafeEncoder.encode(bmesg);
      onMessage(strchannel, strmesg);  //接收到信息的回調(diào)
    } else if (Arrays.equals(PMESSAGE.raw, resp)) {
      final byte[] bpattern = (byte[]) reply.get(1);
      final byte[] bchannel = (byte[]) reply.get(2);
      final byte[] bmesg = (byte[]) reply.get(3);
      final String strpattern = (bpattern == null) ? null : SafeEncoder.encode(bpattern);
      final String strchannel = (bchannel == null) ? null : SafeEncoder.encode(bchannel);
      final String strmesg = (bmesg == null) ? null : SafeEncoder.encode(bmesg);
      onPMessage(strpattern, strchannel, strmesg);
    } else if (Arrays.equals(PSUBSCRIBE.raw, resp)) {
      subscribedChannels = ((Long) reply.get(2)).intValue();
      final byte[] bpattern = (byte[]) reply.get(1);
      final String strpattern = (bpattern == null) ? null : SafeEncoder.encode(bpattern);
      onPSubscribe(strpattern, subscribedChannels);
    } else if (Arrays.equals(PUNSUBSCRIBE.raw, resp)) {
      subscribedChannels = ((Long) reply.get(2)).intValue();
      final byte[] bpattern = (byte[]) reply.get(1);
      final String strpattern = (bpattern == null) ? null : SafeEncoder.encode(bpattern);
      onPUnsubscribe(strpattern, subscribedChannels);
    } else if (Arrays.equals(PONG.raw, resp)) {
      final byte[] bpattern = (byte[]) reply.get(1);
      final String strpattern = (bpattern == null) ? null : SafeEncoder.encode(bpattern);
      onPong(strpattern);
    } else {
      throw new JedisException("Unknown message type: " + firstObj);
    }
  } while (isSubscribed());

  /* Invalidate instance since this thread is no longer listening */
  this.client = null;

  /*
   * Reset pipeline count because subscribe() calls would have increased it but nothing
   * decremented it.
   */
  client.resetPipelinedCount();
}
private static Object process(final RedisInputStream is) {

  final byte b = is.readByte();   //阻塞獲取server端傳遞的數(shù)據(jù),這里有必要點一下,因為這是核心,傳統(tǒng)的IO流再獲取數(shù)據(jù)的時候會阻塞,直到有數(shù)據(jù)過來。
  if (b == PLUS_BYTE) {
    return processStatusCodeReply(is);
  } else if (b == DOLLAR_BYTE) {
    return processBulkReply(is);
  } else if (b == ASTERISK_BYTE) {
    return processMultiBulkReply(is);
  } else if (b == COLON_BYTE) {
    return processInteger(is);
  } else if (b == MINUS_BYTE) {
    processError(is);
    return null;
  } else {
    throw new JedisConnectionException("Unknown reply: " + (char) b);
  }
}

二. pipeline(管道)
傳統(tǒng)的交互方式是request->response request->response request->response,而pipeline是 request->request->request-> response->response->response,將多次請求的報文放到一個請求體中,服務(wù)端也是一樣,將多次的響應(yīng)信息合并成一次返回,所以 ,能不快嗎?!這也就是pipeline的基本原理。下面是測試代碼實例:

@Test
public void test36() {
    Pipeline pipelined = jedis.pipelined();
    pipelined.set("test1", "22");
    pipelined.set("test2", "33");

    pipelined.sync();   //無返回值

    pipelined.get("test1");
    pipelined.get("test2");

    List<Object> objects = pipelined.syncAndReturnAll();  //有返回值
    for (Object o : objects) {
        System.out.print(o + " ");
    }
}
 
result: 22 33

跟著代碼看一下:

public Response<String> set(byte[] key, byte[] value) {
  getClient(key).set(key, value);     //執(zhí)行set命令
  return getResponse(BuilderFactory.STRING);   //注意設(shè)置響應(yīng)信息的順序
}
protected Connection sendCommand(final Command cmd, final byte[]... args) {
  try {
    connect(); //與server端形成鏈接,實例化輸入輸出流
    Protocol.sendCommand(outputStream, cmd, args);   //往輸出流中按照協(xié)議寫入key與value,但不發(fā)送,調(diào)用sync()或syncAndReturnAll()時發(fā)送
    pipelinedCommands++;
    return this;
  } catch (JedisConnectionException ex) {
    /*
     * When client send request which formed by invalid protocol, Redis send back error message
     * before close connection. We try to read it to provide reason of failure.
     */
    try {
      String errorMessage = Protocol.readErrorLineIfPossible(inputStream);
      if (errorMessage != null && errorMessage.length() > 0) {
        ex = new JedisConnectionException(errorMessage, ex.getCause());
      }
    } catch (Exception e) {
      /*
       * Catch any IOException or JedisConnectionException occurred from InputStream#read and just
       * ignore. This approach is safe because reading error message is optional and connection
       * will eventually be closed.
       */
    }
    // Any other exceptions related to connection?
    broken = true;
    throw ex;
  }
}
protected <T> Response<T> getResponse(Builder<T> builder) {
  Response<T> lr = new Response<T>(builder);  //初始化一個Response
  pipelinedResponses.add(lr);    //放入列表中,保證順序
  return lr;
}

當(dāng)調(diào)用syncAndReturnAll時

public List<Object> syncAndReturnAll() {
  if (getPipelinedResponseLength() > 0) {    //判斷Response列表中的數(shù)量
    List<Object> unformatted = client.getAll();    //獲取所有的結(jié)果,阻塞獲取
    List<Object> formatted = new ArrayList<Object>();

    for (Object o : unformatted) {
      try {
        formatted.add(generateResponse(o).get());    //解析每一個得到的信息,因為原來都是byte[],需要解析成String
      } catch (JedisDataException e) {
        formatted.add(e);
      }
    }
    return formatted;
  } else {
    return java.util.Collections.<Object> emptyList();
  }
}
public List<Object> getAll(int except) {
  List<Object> all = new ArrayList<Object>();
  flush();
  while (pipelinedCommands > except) {
    try {
      all.add(readProtocolWithCheckingBroken());  //根據(jù)pipelinedCommands值判斷all里面需要多少跳數(shù)據(jù)
    } catch (JedisDataException e) {
      all.add(e);
    }
    pipelinedCommands--;
  }
  return all;
}

readProtocolWithCheckingBroken還是阻塞獲取


private static Object process(final RedisInputStream is) {

  final byte b = is.readByte();  //阻塞獲取值,判斷第一個字節(jié)的類型,從而進入不同的處理
  if (b == PLUS_BYTE) {
    return processStatusCodeReply(is);
  } else if (b == DOLLAR_BYTE) {
    return processBulkReply(is);
  } else if (b == ASTERISK_BYTE) {
    return processMultiBulkReply(is);
  } else if (b == COLON_BYTE) {
    return processInteger(is);
  } else if (b == MINUS_BYTE) {
    processError(is);
    return null;
  } else {
    throw new JedisConnectionException("Unknown reply: " + (char) b);
  }
}

三. Transaction(事務(wù))
1. 事務(wù)為提交之前出現(xiàn)異常,那么事務(wù)內(nèi)的操作都不執(zhí)行。這個很好理解,操作同pipeline,事務(wù)提交的時候才會flush數(shù)據(jù)同服務(wù)器交互。
2. 結(jié)合watch。事務(wù)開始前觀察某個值,在事務(wù)期間如果其他的線程改了所觀察的值,事務(wù)不執(zhí)行!

@Test
public void testTransaction() {
    //要在事務(wù)執(zhí)行之前進行監(jiān)視
    jedis.watch("key100", "key101");
    Transaction multi = jedis.multi();

    multi.set("key100", "key100");
    multi.set("key101", "key101");

    try {
        Thread.sleep(10000);
    } catch (InterruptedException e) {
    }

     multi.exec();

    String key6 = jedis.get("key100");
    System.out.println(key6);
    String key7 = jedis.get("key101");
    System.out.println(key7);

}

@Test
public void test() {
    jedis.set("key100", "value3");
    String key10 = jedis.get("key100");
    System.out.println(key10);
}

先執(zhí)行testTransaction,再執(zhí)行test,test的結(jié)果

value3

10s后testTransaction的結(jié)果

value3
null
  1. redis的事務(wù)不支持回滾。因為這種復(fù)雜的功能和redis追求的簡單高效的設(shè)計主旨不符合,并且他認為,redis事務(wù)的執(zhí)行時錯誤通常都是編程錯誤造成的,這種錯誤通常只會出現(xiàn)在開發(fā)環(huán)境中,而很少會在實際的生產(chǎn)環(huán)境中出現(xiàn),所以他認為沒有必要為redis開發(fā)事務(wù)回滾功能。

總結(jié): jedis這個類庫比較輕便,可以幫助我們已更加原汁原味的方式操作redis。首先客戶端與服務(wù)端的交互,第一個核心點就是協(xié)議的約定。服務(wù)端通過協(xié)議解析客戶端傳遞過來的數(shù)據(jù)包,客戶端根據(jù)協(xié)議解析服務(wù)端的數(shù)據(jù)。第二個就是網(wǎng)絡(luò)編程,比較輕便也通俗易懂,方便我們?nèi)胧志W(wǎng)絡(luò),這也是我想寫的第二個原因。有太多的時候我們接觸更多的上層業(yè)務(wù),忽略了基礎(chǔ)構(gòu)建,仿佛學(xué)習(xí)武功,只學(xué)習(xí)招式,不學(xué)習(xí)內(nèi)功,最終頂多算是個雜耍者,永遠成不了高手。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,993評論 19 139
  • 本文將從Redis的基本特性入手,通過講述Redis的數(shù)據(jù)結(jié)構(gòu)和主要命令對Redis的基本能力進行直觀介紹。之后概...
    kelgon閱讀 61,286評論 23 625
  • 轉(zhuǎn)載:Redis 寶典 | 基礎(chǔ)、高級特性與性能調(diào)優(yōu) 本文由 DevOpsDays 本文由簡書作者kelgon供稿...
    meng_philip123閱讀 3,167評論 1 34
  • 1.給羅輯思維投稿 拖延癥那篇 從知識到行動那篇 修訂之后投稿 2.有系列文章《我讀《好好學(xué)習(xí)》》 關(guān)于能力圈的結(jié)...
    王立剛_Leon閱讀 159評論 0 0
  • 臺燈 你以為 你開了一盞燈 其實是 開啟了寂寞 你以為 開了燈 就能感受到 光明 事實是 心里的黑暗 一直在
    月色襲人閱讀 134評論 0 2