Jedis是Redis官方推荐的Java连接开发工具。要在Java开发中使用好Redis中间件,必须对Jedis熟悉才能写成漂亮的代码。这篇文章不描述怎么安装Redis和Reids的命令,只对Jedis的使用进行对介绍。
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对象的创建和销毁过程。
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; }
我们知道,连接池可以大大提高应用访问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服务。
对于大应用来说,单台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();
当然,采用这种方式也存在两个问题
对于扩容问题,Redis的作者提出了一种名为Pre-Sharding的方式。即事先部署足够多的Redis服务。
对于单点故障问题,我们可以使用Redis的HA高可用来实现。利用Redis-Sentinal来通知主从服务的切换。当然,Jedis没有实现这块。我将会在下一篇文章进行介绍。
对于Jedis的基本使用还是很简单的。要根据不用的应用场景选择对于的使用方式。
另外,Spring也提供了Spring-data-redis包来整合Jedis的操作,另外Spring也单独分装了Jedis(我将会在另外一篇文章介绍)。
链接:https://www.jianshu.com/p/a1038eed6d44