記錄是一種精神,是加深理解最好的方式之一。
最近深入研究了Jedis的源碼,對Jedis的使用進(jìn)行深入理解,提筆記錄。
曹金桂 cao_jingui@163.com(如有遺漏之處還請指教)
時(shí)間:2016年11月26日15:00
概述
Jedis是Redis官方推薦的Java連接開發(fā)工具。要在Java開發(fā)中使用好Redis中間件,必須對Jedis熟悉才能寫成漂亮的代碼。這篇文章不描述怎么安裝Redis和Reids的命令,只對Jedis的使用進(jìn)行對介紹。
1. 基本使用
Jedis的基本使用非常簡單,只需要創(chuàng)建Jedis對象的時(shí)候指定host,port, password即可。當(dāng)然,Jedis對象又很多構(gòu)造方法,都大同小異,只是對應(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"); //訪問Redis服務(wù)
jedis.close(); //使用完關(guān)閉連接
Jedis基本使用十分簡單,在每次使用時(shí),構(gòu)建Jedis對象即可。在Jedis對象構(gòu)建好之后,Jedis底層會打開一條Socket通道和Redis服務(wù)進(jìn)行連接。所以在使用完Jedis對象之后,需要調(diào)用Jedis.close()方法把連接關(guān)閉,不如會占用系統(tǒng)資源。當(dāng)然,如果應(yīng)用非常平凡的創(chuàng)建和銷毀Jedis對象,對應(yīng)用的性能是很大影響的,因?yàn)闃?gòu)建Socket的通道是很耗時(shí)的(類似數(shù)據(jù)庫連接)。我們應(yīng)該使用連接池來減少Socket對象的創(chuàng)建和銷毀過程。
2. 連接池使用
Jedis連接池是基于apache-commons pool2實(shí)現(xiàn)的。在構(gòu)建連接池對象的時(shí)候,需要提供池對象的配置對象,及JedisPoolConfig(繼承自GenericObjectPoolConfig)。我們可以通過這個(gè)配置對象對連接池進(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連接池之后,在每次用完連接對象后一定要記得把連接歸還給連接池。Jedis對close方法進(jìn)行了改造,如果是連接池中的連接對象,調(diào)用Close方法將會是把連接對象返回到對象池,若不是則關(guān)閉連接。可以查看如下代碼
@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鏈接時(shí),將會對dataSource進(jìn)行設(shè)置
// JedisPool.getResource()方法
public Jedis getResource() {
Jedis jedis = super.getResource();
jedis.setDataSource(this);
return jedis;
}
3. 高可用連接
我們知道,連接池可以大大提高應(yīng)用訪問Reids服務(wù)的性能,減去大量的Socket的創(chuàng)建和銷毀過程。但是Redis為了保障高可用,服務(wù)一般都是Sentinel部署方式(可以查看我的文章詳細(xì)了解)。當(dāng)Redis服務(wù)中的主服務(wù)掛掉之后,會仲裁出另外一臺Slaves服務(wù)充當(dāng)Master。這個(gè)時(shí)候,我們的應(yīng)用即使使用了Jedis連接池,Master服務(wù)掛了,我們的應(yīng)用獎還是無法連接新的Master服務(wù)。為了解決這個(gè)問題,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的使用也是十分簡單的,只是在JedisPool中添加了Sentinel和MasterName參數(shù)。Jedis Sentinel底層基于Redis訂閱實(shí)現(xiàn)Redis主從服務(wù)的切換通知。當(dāng)Reids發(fā)生主從切換時(shí),Sentinel會發(fā)送通知主動通知Jedis進(jìn)行連接的切換。JedisSentinelPool在每次從連接池中獲取鏈接對象的時(shí)候,都要對連接對象進(jìn)行檢測,如果此鏈接和Sentinel的Master服務(wù)連接參數(shù)不一致,則會關(guān)閉此連接,重新獲取新的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);
}
}
}
當(dāng)然,JedisSentinelPool對象要時(shí)時(shí)監(jiān)控RedisSentinel的主從切換。在其內(nèi)部通過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...");
//啟動后臺線程監(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è)對象來實(shí)現(xiàn)的。看對應(yīng)源碼可以發(fā)現(xiàn)是基于Redis的訂閱實(shí)現(xiàn)的,其訂閱頻道為"+switch-master"。當(dāng)MasterListener接收到switch-master消息時(shí)候,會使用新的Host和port進(jìn)行initPool。這樣對連接池中的連接對象清除,重新創(chuàng)建新的連接指向新的Master服務(wù)。
4. 客戶端分片
對于大應(yīng)用來說,單臺Redis服務(wù)器肯定滿足不了應(yīng)用的需求。在Redis3.0之前,是不支持集群的。如果要使用多臺Reids服務(wù)器,必須采用其他方式。很多公司使用了代理方式來解決Redis集群。對于Jedis,也提供了客戶端分片的模式來連接“Redis集群”。其內(nèi)部是采用Key的一致性hash算法來區(qū)分key存儲在哪個(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è)問題
- 擴(kuò)容問題:
因?yàn)槭褂昧艘恢滦怨∵M(jìn)行分片,那么不同的key分布到不同的Redis-Server上,當(dāng)我們需要擴(kuò)容時(shí),需要增加機(jī)器到分片列表中,這時(shí)候會使得同樣的key算出來落到跟原來不同的機(jī)器上,這樣如果要取某一個(gè)值,會出現(xiàn)取不到的情況。 - 單點(diǎn)故障問題:
當(dāng)集群中的某一臺服務(wù)掛掉之后,客戶端在根據(jù)一致性hash無法從這臺服務(wù)器取數(shù)據(jù)。
對于擴(kuò)容問題,Redis的作者提出了一種名為Pre-Sharding的方式。即事先部署足夠多的Redis服務(wù)。
對于單點(diǎn)故障問題,我們可以使用Redis的HA高可用來實(shí)現(xiàn)。利用Redis-Sentinal來通知主從服務(wù)的切換。當(dāng)然,Jedis沒有實(shí)現(xiàn)這塊。我將會在下一篇文章進(jìn)行介紹。
5. 小結(jié)
對于Jedis的基本使用還是很簡單的。要根據(jù)不用的應(yīng)用場景選擇對于的使用方式。
另外,Spring也提供了Spring-data-redis包來整合Jedis的操作,另外Spring也單獨(dú)分裝了Jedis(我將會在另外一篇文章介紹)。