Jedis使用教程完整版

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

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

概述

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

1. 基本使用

Jedis的基本使用非常簡(jiǎn)單,只需要?jiǎng)?chuàng)建Jedis對(duì)象的時(shí)候指定host,port, password即可。當(dāng)然,Jedis對(duì)象又很多構(gòu)造方法,都大同小異,只是對(duì)應(yīng)和Redis連接的socket的參數(shù)不一樣而已。

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

Jedis基本使用十分簡(jiǎn)單,在每次使用時(shí),構(gòu)建Jedis對(duì)象即可。在Jedis對(duì)象構(gòu)建好之后,Jedis底層會(huì)打開(kāi)一條Socket通道和Redis服務(wù)進(jìn)行連接。所以在使用完Jedis對(duì)象之后,需要調(diào)用Jedis.close()方法把連接關(guān)閉,不如會(huì)占用系統(tǒng)資源。當(dāng)然,如果應(yīng)用非常平凡的創(chuàng)建和銷(xiāo)毀Jedis對(duì)象,對(duì)應(yīng)用的性能是很大影響的,因?yàn)闃?gòu)建Socket的通道是很耗時(shí)的(類(lèi)似數(shù)據(jù)庫(kù)連接)。我們應(yīng)該使用連接池來(lái)減少Socket對(duì)象的創(chuàng)建和銷(xiāo)毀過(guò)程。

2. 連接池使用

Jedis連接池是基于apache-commons pool2實(shí)現(xiàn)的。在構(gòu)建連接池對(duì)象的時(shí)候,需要提供池對(duì)象的配置對(duì)象,及JedisPoolConfig(繼承自GenericObjectPoolConfig)。我們可以通過(guò)這個(gè)配置對(duì)象對(duì)連接池進(jìn)行相關(guān)參數(shù)的配置(如最大連接數(shù),最大空數(shù)等)。

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連接池之后,在每次用完連接對(duì)象后一定要記得把連接歸還給連接池。Jedis對(duì)close方法進(jìn)行了改造,如果是連接池中的連接對(duì)象,調(diào)用Close方法將會(huì)是把連接對(duì)象返回到對(duì)象池,若不是則關(guān)閉連接??梢圆榭慈缦麓a

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

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

3. 高可用連接

我們知道,連接池可以大大提高應(yīng)用訪(fǎng)問(wèn)Reids服務(wù)的性能,減去大量的Socket的創(chuàng)建和銷(xiāo)毀過(guò)程。但是Redis為了保障高可用,服務(wù)一般都是Sentinel部署方式(可以查看我的文章詳細(xì)了解)。當(dāng)Redis服務(wù)中的主服務(wù)掛掉之后,會(huì)仲裁出另外一臺(tái)Slaves服務(wù)充當(dāng)Master。這個(gè)時(shí)候,我們的應(yīng)用即使使用了Jedis連接池,Master服務(wù)掛了,我們的應(yīng)用獎(jiǎng)還是無(wú)法連接新的Master服務(wù)。為了解決這個(gè)問(wèn)題,Jedis也提供了相應(yīng)的Sentinel實(shí)現(xiàn),能夠在Redis Sentinel主從切換時(shí)候,通知我們的應(yīng)用,把我們的應(yīng)用連接到新的 Master服務(wù)。先看下怎么使用。

注意: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的使用也是十分簡(jiǎn)單的,只是在JedisPool中添加了Sentinel和MasterName參數(shù)。Jedis Sentinel底層基于Redis訂閱實(shí)現(xiàn)Redis主從服務(wù)的切換通知。當(dāng)Reids發(fā)生主從切換時(shí),Sentinel會(huì)發(fā)送通知主動(dòng)通知Jedis進(jìn)行連接的切換。JedisSentinelPool在每次從連接池中獲取鏈接對(duì)象的時(shí)候,都要對(duì)連接對(duì)象進(jìn)行檢測(cè),如果此鏈接和Sentinel的Master服務(wù)連接參數(shù)不一致,則會(huì)關(guān)閉此連接,重新獲取新的Jedis連接對(duì)象。

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);
        }
    }
}

當(dāng)然,JedisSentinelPool對(duì)象要時(shí)時(shí)監(jiān)控RedisSentinel的主從切換。在其內(nèi)部通過(guò)Reids的訂閱實(shí)現(xiàn)。具體的實(shí)現(xiàn)看JedisSentinelPool的兩個(gè)方法就很清晰

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...");
    //啟動(dòng)后臺(tái)線(xiàn)程監(jiān)控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的監(jiān)控時(shí)使用MasterListener這個(gè)對(duì)象來(lái)實(shí)現(xiàn)的。看對(duì)應(yīng)源碼可以發(fā)現(xiàn)是基于Redis的訂閱實(shí)現(xiàn)的,其訂閱頻道為"+switch-master"。當(dāng)MasterListener接收到switch-master消息時(shí)候,會(huì)使用新的Host和port進(jìn)行initPool。這樣對(duì)連接池中的連接對(duì)象清除,重新創(chuàng)建新的連接指向新的Master服務(wù)。

4. 客戶(hù)端分片

對(duì)于大應(yīng)用來(lái)說(shuō),單臺(tái)Redis服務(wù)器肯定滿(mǎn)足不了應(yīng)用的需求。在Redis3.0之前,是不支持集群的。如果要使用多臺(tái)Reids服務(wù)器,必須采用其他方式。很多公司使用了代理方式來(lái)解決Redis集群。對(duì)于Jedis,也提供了客戶(hù)端分片的模式來(lái)連接“Redis集群”。其內(nèi)部是采用Key的一致性hash算法來(lái)區(qū)分key存儲(chǔ)在哪個(gè)Redis實(shí)例上的。

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();

當(dāng)然,采用這種方式也存在兩個(gè)問(wèn)題

  1. 擴(kuò)容問(wèn)題:
    因?yàn)槭褂昧艘恢滦怨∵M(jìn)行分片,那么不同的key分布到不同的Redis-Server上,當(dāng)我們需要擴(kuò)容時(shí),需要增加機(jī)器到分片列表中,這時(shí)候會(huì)使得同樣的key算出來(lái)落到跟原來(lái)不同的機(jī)器上,這樣如果要取某一個(gè)值,會(huì)出現(xiàn)取不到的情況。
  2. 單點(diǎn)故障問(wèn)題:
    當(dāng)集群中的某一臺(tái)服務(wù)掛掉之后,客戶(hù)端在根據(jù)一致性hash無(wú)法從這臺(tái)服務(wù)器取數(shù)據(jù)。

對(duì)于擴(kuò)容問(wèn)題,Redis的作者提出了一種名為Pre-Sharding的方式。即事先部署足夠多的Redis服務(wù)。
對(duì)于單點(diǎn)故障問(wèn)題,我們可以使用Redis的HA高可用來(lái)實(shí)現(xiàn)。利用Redis-Sentinal來(lái)通知主從服務(wù)的切換。當(dāng)然,Jedis沒(méi)有實(shí)現(xiàn)這塊。我將會(huì)在下一篇文章進(jìn)行介紹。

5. 小結(jié)

對(duì)于Jedis的基本使用還是很簡(jiǎn)單的。要根據(jù)不用的應(yīng)用場(chǎng)景選擇對(duì)于的使用方式。
另外,Spring也提供了Spring-data-redis包來(lái)整合Jedis的操作,另外Spring也單獨(dú)分裝了Jedis(我將會(huì)在另外一篇文章介紹)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容