Jedis使用教程完整版

記錄是一種精神,是加深理解最好的方式之一。

最近深入研究了Jedis的源碼,對Jedis的使用進行深入理解,提筆記錄。
曹金桂 cao_jingui@163.com(如有遺漏之處還請指教)
時間:2016年11月26日15:00

概述

Jedis是Redis官方推薦的Java連接開發工具。要在Java開發中使用好Redis中間件,必須對Jedis熟悉才能寫成漂亮的代碼。這篇文章不描述怎么安裝Redis和Reids的命令,只對Jedis的使用進行對介紹。

1. 基本使用

Jedis的基本使用非常簡單,只需要創建Jedis對象的時候指定host,port, password即可。當然,Jedis對象又很多構造方法,都大同小異,只是對應和Redis連接的socket的參數不一樣而已。

Jedis jedis = new Jedis("localhost", 6379);  //指定Redis服務Host和port
jedis.auth("xxxx"); //如果Redis服務連接需要密碼,制定密碼
String value = jedis.get("key"); //訪問Redis服務
jedis.close(); //使用完關閉連接

Jedis基本使用十分簡單,在每次使用時,構建Jedis對象即可。在Jedis對象構建好之后,Jedis底層會打開一條Socket通道和Redis服務進行連接。所以在使用完Jedis對象之后,需要調用Jedis.close()方法把連接關閉,不如會占用系統資源。當然,如果應用非常平凡的創建和銷毀Jedis對象,對應用的性能是很大影響的,因為構建Socket的通道是很耗時的(類似數據庫連接)。我們應該使用連接池來減少Socket對象的創建和銷毀過程。

2. 連接池使用

Jedis連接池是基于apache-commons pool2實現的。在構建連接池對象的時候,需要提供池對象的配置對象,及JedisPoolConfig(繼承自GenericObjectPoolConfig)。我們可以通過這個配置對象對連接池進行相關參數的配置(如最大連接數,最大空數等)。

JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(8);
config.setMaxTotal(18);
JedisPool pool = new JedisPool(config, "127.0.0.1", 6379, 2000, "password");
Jedis jedis = pool.getResource();
String value = jedis.get("key");
......
jedis.close();
pool.close();

使用Jedis連接池之后,在每次用完連接對象后一定要記得把連接歸還給連接池。Jedis對close方法進行了改造,如果是連接池中的連接對象,調用Close方法將會是把連接對象返回到對象池,若不是則關閉連接。可以查看如下代碼

@Override
public void close() { //Jedis的close方法
    if (dataSource != null) {
        if (client.isBroken()) {
            this.dataSource.returnBrokenResource(this);
        } else {
            this.dataSource.returnResource(this);
        }
    } else {
        client.close();
    }
}

//另外從對象池中獲取Jedis鏈接時,將會對dataSource進行設置
// JedisPool.getResource()方法
public Jedis getResource() {
    Jedis jedis = super.getResource();   
    jedis.setDataSource(this);
    return jedis;
}

3. 高可用連接

我們知道,連接池可以大大提高應用訪問Reids服務的性能,減去大量的Socket的創建和銷毀過程。但是Redis為了保障高可用,服務一般都是Sentinel部署方式(可以查看我的文章詳細了解)。當Redis服務中的主服務掛掉之后,會仲裁出另外一臺Slaves服務充當Master。這個時候,我們的應用即使使用了Jedis連接池,Master服務掛了,我們的應用獎還是無法連接新的Master服務。為了解決這個問題,Jedis也提供了相應的Sentinel實現,能夠在Redis Sentinel主從切換時候,通知我們的應用,把我們的應用連接到新的 Master服務。先看下怎么使用。

注意:Jedis版本必須2.4.2或更新版本

Set<String> sentinels = new HashSet<>();
sentinels.add("172.18.18.207:26379");
sentinels.add("172.18.18.208:26379");
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(5);
config.setMaxTotal(20);
JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels, config);
Jedis jedis = pool.getResource();
jedis.set("jedis", "jedis");
......
jedis.close();
pool.close();

Jedis Sentinel的使用也是十分簡單的,只是在JedisPool中添加了Sentinel和MasterName參數。Jedis Sentinel底層基于Redis訂閱實現Redis主從服務的切換通知。當Reids發生主從切換時,Sentinel會發送通知主動通知Jedis進行連接的切換。JedisSentinelPool在每次從連接池中獲取鏈接對象的時候,都要對連接對象進行檢測,如果此鏈接和Sentinel的Master服務連接參數不一致,則會關閉此連接,重新獲取新的Jedis連接對象。

public Jedis getResource() {
    while (true) {
        Jedis jedis = super.getResource();
        jedis.setDataSource(this);

        // get a reference because it can change concurrently
        final HostAndPort master = currentHostMaster;
        final HostAndPort connection = new HostAndPort(jedis.getClient().getHost(), jedis.getClient().getPort());
        if (master.equals(connection)) {
            // connected to the correct master
            return jedis;
        } else {
            returnBrokenResource(jedis);
        }
    }
}

當然,JedisSentinelPool對象要時時監控RedisSentinel的主從切換。在其內部通過Reids的訂閱實現。具體的實現看JedisSentinelPool的兩個方法就很清晰

private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
    HostAndPort master = null;
    boolean sentinelAvailable = false;
    log.info("Trying to find master from available Sentinels...");
    for (String sentinel : sentinels) {
        final HostAndPort hap = HostAndPort.parseString(sentinel);
        log.fine("Connecting to Sentinel " + hap);
        Jedis jedis = null;
        try {
            jedis = new Jedis(hap.getHost(), hap.getPort());
            //從RedisSentinel中獲取Master信息
            List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
            sentinelAvailable = true; // connected to sentinel...
            if (masterAddr == null || masterAddr.size() != 2) {
                log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap + ".");
                continue;
            }
            master = toHostAndPort(masterAddr);
            log.fine("Found Redis master at " + master);
            break;
        } catch (JedisException e) {
            // it should handle JedisException there's another chance of raising JedisDataException
            log.warning("Cannot get master address from sentinel running @ " + hap + ". Reason: " + e + ". Trying next one.");
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }
    if (master == null) {
        if (sentinelAvailable) {
            // can connect to sentinel, but master name seems to not monitored
            throw new JedisException("Can connect to sentinel, but " + masterName + " seems to be not monitored...");
        } else {
            throw new JedisConnectionException("All sentinels down, cannot determine where is " + masterName + " master is running...");
        }
    }
    log.info("Redis master running at " + master + ", starting Sentinel listeners...");
    //啟動后臺線程監控RedisSentinal的主從切換通知
    for (String sentinel : sentinels) {
        final HostAndPort hap = HostAndPort.parseString(sentinel);
        MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
        // whether MasterListener threads are alive or not, process can be stopped
        masterListener.setDaemon(true);
        masterListeners.add(masterListener);
        masterListener.start();
    }
    return master;
}


private void initPool(HostAndPort master) {
    if (!master.equals(currentHostMaster)) {
        currentHostMaster = master;
        if (factory == null) {
            factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout, soTimeout, password, database, clientName, false, null, null, null);
            initPool(poolConfig, factory);
        } else {
            factory.setHostAndPort(currentHostMaster);
            // although we clear the pool, we still have to check the returned object
            // in getResource, this call only clears idle instances, not
            // borrowed instances
            internalPool.clear();
        }
        log.info("Created JedisPool to master at " + master);
    }
}

可以看到,JedisSentinel的監控時使用MasterListener這個對象來實現的。看對應源碼可以發現是基于Redis的訂閱實現的,其訂閱頻道為"+switch-master"。當MasterListener接收到switch-master消息時候,會使用新的Host和port進行initPool。這樣對連接池中的連接對象清除,重新創建新的連接指向新的Master服務。

4. 客戶端分片

對于大應用來說,單臺Redis服務器肯定滿足不了應用的需求。在Redis3.0之前,是不支持集群的。如果要使用多臺Reids服務器,必須采用其他方式。很多公司使用了代理方式來解決Redis集群。對于Jedis,也提供了客戶端分片的模式來連接“Redis集群”。其內部是采用Key的一致性hash算法來區分key存儲在哪個Redis實例上的。

JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(500);
config.setTestOnBorrow(true);
List<JedisShardInfo> jdsInfoList = new ArrayList<>(2);
jdsInfoList.add(new JedisShardInfo("192.168.2.128", 6379));
jdsInfoList.add(new JedisShardInfo("192.168.2.108", 6379));
pool = new ShardedJedisPool(config, jdsInfoList, Hashing.MURMUR_HASH, Sharded.DEFAULT_KEY_TAG_PATTERN);
jds.set(key, value);
......
jds.close();
pool.close();

當然,采用這種方式也存在兩個問題

  1. 擴容問題:
    因為使用了一致性哈稀進行分片,那么不同的key分布到不同的Redis-Server上,當我們需要擴容時,需要增加機器到分片列表中,這時候會使得同樣的key算出來落到跟原來不同的機器上,這樣如果要取某一個值,會出現取不到的情況。
  2. 單點故障問題:
    當集群中的某一臺服務掛掉之后,客戶端在根據一致性hash無法從這臺服務器取數據。

對于擴容問題,Redis的作者提出了一種名為Pre-Sharding的方式。即事先部署足夠多的Redis服務。
對于單點故障問題,我們可以使用Redis的HA高可用來實現。利用Redis-Sentinal來通知主從服務的切換。當然,Jedis沒有實現這塊。我將會在下一篇文章進行介紹。

5. 小結

對于Jedis的基本使用還是很簡單的。要根據不用的應用場景選擇對于的使用方式。
另外,Spring也提供了Spring-data-redis包來整合Jedis的操作,另外Spring也單獨分裝了Jedis(我將會在另外一篇文章介紹)。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容