Redis 连接开发工具:Jedis

原创
2020/07/07 20:34
阅读数 2.8K

介绍

  Jedis 是Redis官方推荐的Java连接开发工具,提供了比较全面的Redis命令的支持。Jedis 中的方法调用是比较底层的暴露的Redis的API,也即Jedis中的Java方法基本和Redis的API保持着一致,了解Redis的API,也就能熟练的使用Jedis。
  Jedis 使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。
  Jedis 提供了以下操作方式:

  • 单机单连接方式:此方式仅建议用于开发环境做调试用。
  • 单机连接池方式:此方式适用于仅使用单个Redis实例的场景
  • 多机分布式+连接池方式:此方式适用规模较大的系统,往往会有多个Redis实例做负载均衡。并且还实现主从备份,当主实例发生故障时,切换至从实例提供服务。
  • redis3.0推出JedisCluster。使用JedisCluster连接使用这种方式时,默认Redis已经进行了集群处理,JedisCluster即针对整个集群的连接。

Jedis GitHub

基本使用

  Jedis 集成了Redis的相关命令操作,它是Java语言操作Redis数据库的桥梁。Jedis客户端封装了Redis数据库的大量命令,使用上基本与 redis-cli 无异,因此具有许多Redis操作API。

Jedis的获取

在使用Jedis之前,需要下载Jedis的相关JAR包。如果项目采用的是Maven环境,则需要在pom.xml文件中引入Jedis的配置,配置如下:

<!-- jedis 相关依赖 -->
<dependency>
	<groupid>redis.clients</groupid>
	<artifactid>jedis</artifactid>
	<version>xx.xxxx</version>
</dependency>

> Jedis Maven 地址:https://mvnrepository.com/artifact/redis.clients/jedis

Jedis 基本使用

  Jedis 的基本使用非常简单,只需要创建Jedis对象的时候指定host,port,password即可。当然,Jedis对象又很多构造方法,都大同小异,只是对应和Redis连接的socket的参数不一样而已。Jedis 构造如下所示

public Jedis() {
    super();
}

public Jedis(final String host) {
    super(host);
}

public Jedis(final HostAndPort hp) {
    super(hp);
}

public Jedis(final String host, final int port) {
    super(host, port);
}

public Jedis(final String host, final int port, final boolean ssl) {
    super(host, port, ssl);
}
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import redis.clients.jedis.Jedis;

public final class RedisHelper {
	private String host;
	private int port;
	private String password;

	public RedisHelper(String host, int port) {
		this.host = host;
		this.port = port;
	}

	public RedisHelper(String host, int port, String password) {
		this.host = host;
		this.port = port;
		this.password = password;
	}

	/**
	 * @方法描述: 获取操作redis的客户端
	 * 
	 *        <pre>
	 *        Jedis客户端实例不是线程安全的
	 *        </pre>
	 * 
	 * @return
	 */
	protected synchronized Jedis getRedisClient() {
		Jedis jedis = new Jedis(host, port);
		if (isNotBlank(password)) {
			jedis.auth(password);
		}
		return jedis;
	}

	/**
	 * @方法描述:关闭redis的客户端
	 * @param jedisClient
	 */
	protected void closeRedisClient(Jedis jedisClient) {
		if (jedisClient != null) {
			jedisClient.close();
		}
	}
}

Jedis 连接池使用

  Jedis并不是线程安全的,所以多线程情况下不应共用Jedis实例,但创建大量的Jedis会造成不必要的开销甚至对性能产生较大影响,故使用JedisPool来避免这些问题,它是一个线程安全的网络连接池,可以使用它可靠地创建多个Jedis实例,完成后将Jedis实例回收到连接池中。

连接池配置

import redis.clients.jedis.JedisPoolConfig;

public final class RedisPoolConfig {
	/** 建立连接池配置参数 */
	private static JedisPoolConfig poolConfig = null;

	/**
	 * @方法描述: 连接池基本配置
	 *
	 */
	public synchronized static JedisPoolConfig initPoolConfig() {
		if (poolConfig == null) {
			poolConfig = new JedisPoolConfig();
			// 设置最大连接数,默认值为8.如果赋值为-1,则表示不限制;
			poolConfig.setMaxTotal(1024);
			// 最大空闲连接数
			poolConfig.setMaxIdle(10);
			// 最小空闲连接数
			poolConfig.setMinIdle(10);
			// 获取Jedis连接的最大等待时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException
			poolConfig.setMaxWaitMillis(10000);
			// 每次释放连接的最大数目
			poolConfig.setNumTestsPerEvictionRun(1024);
			// 释放连接的扫描间隔(毫秒),如果为负数,则不运行逐出线程, 默认-1
			poolConfig.setTimeBetweenEvictionRunsMillis(30000);
			// 连接最小空闲时间
			poolConfig.setMinEvictableIdleTimeMillis(1800000);
			// 连接空闲多久后释放, 当空闲时间&gt;该值 且 空闲连接&gt;最大空闲连接数 时直接释放
			poolConfig.setSoftMinEvictableIdleTimeMillis(10000);
			// 在获取Jedis连接时,自动检验连接是否可用
			poolConfig.setTestOnBorrow(true);
			// 在将连接放回池中前,自动检验连接是否有效
			poolConfig.setTestOnReturn(true);
			// 自动测试池中的空闲连接是否都是可用连接
			poolConfig.setTestWhileIdle(true);
			// 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true
			poolConfig.setBlockWhenExhausted(false);
			// 是否启用pool的jmx管理功能, 默认true
			poolConfig.setJmxEnabled(true);
			// 是否启用后进先出, 默认true
			poolConfig.setLifo(true);
			// 每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3
			poolConfig.setNumTestsPerEvictionRun(3);
		}

		return poolConfig;
	}
}

单机使用

连接池配置

import java.util.concurrent.locks.ReentrantLock;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Protocol;

public final class RedisPoolHelper {
	private static ReentrantLock lockPool = new ReentrantLock();

	private String host;
	private int port;
	private String password;

	public RedisPoolHelper(String host, int port) {
		super();
		this.host = host;
		this.port = port;
		getJedisPoolClient();
	}

	public RedisPoolHelper(String host, int port, String password) {
		super();
		this.host = host;
		this.port = port;
		this.password = password;
		getJedisPoolClient();
	}

	/**
	 * 获取单机jedis连接池
	 */
	private synchronized JedisPool getJedisPoolClient() {
		// 断言 ,当前锁是否已经锁住,如果锁住了,就啥也不干,没锁的话就执行下面步骤
		assert !lockPool.isHeldByCurrentThread();

		lockPool.lock();

		try {
			return new JedisPool(RedisPoolConfig.initPoolConfig(), host, port, Protocol.DEFAULT_TIMEOUT, password);
		} catch (Exception exp) {
			// 创建Redis Pool失败
			exp.printStackTrace();
		} finally {
			lockPool.unlock();
		}
		return null;
	}

	/**
	 * @方法描述: 从jedis连接池中获取获取jedis对象
	 *
	 * @return
	 */
	public synchronized Jedis getRedisClient() {
		return getJedisPoolClient().getResource();
	}

	/**
	 * @方法描述: 回收jedis(放到finally中)
	 *
	 * @param jedisClient
	 */
	public void closeRedisClient(Jedis jedisClient) {
		if (jedisClient != null) {
			jedisClient.close();
		}
	}
}

主从模式

连接池配置

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import com.google.common.collect.Lists;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisSentinelPool;
import redis.clients.jedis.Protocol;

public final class RedisSentinelHelper {
	private static ReentrantLock lockPool = new ReentrantLock();
	/** 支持Redis的非切片链接池 */
	private static JedisSentinelPool jedisSentinelPool = null;
	private String masterName;
	private List<string> addressList;
	private String password;

	// 超时时间,默认是2000
	protected int timeout = Protocol.DEFAULT_TIMEOUT;
	// redis数据库的数目
	protected int database = Protocol.DEFAULT_DATABASE;

	public RedisSentinelHelper(String masterName, List<string> addressList, String password) {
		super();
		this.masterName = masterName;
		this.addressList = addressList;
		this.password = password;
		setShutdownWork();
		getJedisSentinelPool();
	}

	private synchronized JedisSentinelPool getJedisSentinelPool() {
		// 断言 ,当前锁是否已经锁住,如果锁住了,就啥也不干,没锁的话就执行下面步骤
		assert !lockPool.isHeldByCurrentThread();
		lockPool.lock();
		try {
			Set<string> sentinels = new HashSet<string>();
			for (String address : addressList) {
				sentinels.add(address);
			}

			if (jedisSentinelPool == null) {
				jedisSentinelPool = new JedisSentinelPool(masterName, sentinels, RedisPoolConfig.initPoolConfig(),
						password);
			}
		} catch (Exception exp) {
			exp.printStackTrace();
		} finally {
			lockPool.unlock();
		}
		if(jedisSentinelPool == null) {
			throw new RuntimeException();
		}
		System.out.println(jedisSentinelPool.isClosed());
		return jedisSentinelPool;
	}

	/**
	 * @方法描述: 设置系统停止时需执行的任务
	 */
	private static void setShutdownWork() {
		try {
			Runtime runtime = Runtime.getRuntime();
			runtime.addShutdownHook(new Thread() {
				@Override
				public void run() {
					try {
						if (jedisSentinelPool != null) {
							jedisSentinelPool.destroy();
							jedisSentinelPool = null;
							System.out.println("关闭Redis Pool成功.");
						}
					} catch (Exception exp) {
						System.err.println("关闭Redis Pool失败.");
						exp.printStackTrace();
					}
				}
			});
			System.out.println("设置系统停止时关闭Redis Pool的任务成功.");
		} catch (Exception e) {
			System.err.println("设置系统停止时关闭Redis Pool的任务失败.");
		}
	}

	/**
	 * @方法描述: 获取Jedis实例
	 *
	 * @return 返回Jedis实例
	 */
	public synchronized Jedis getJedis() {
		if (jedisSentinelPool != null) {
			return jedisSentinelPool.getResource();
		} else {
			return null;
		}
	}

	/**
	 * 演示测试类
	 */
	public static void main(String[] args) {
		List<string> address = Lists.newArrayList();
		address.add("192.168.192.105:27004");
		address.add("192.168.192.106:27004");
		address.add("192.168.192.107:27004");
		RedisSentinelHelper redisSentine = new RedisSentinelHelper("mymaster", address, "");
		System.out.println(redisSentine.getJedis().dbSize());
	}
}

Jedis 连接集群

连接池配置

import java.util.List;
import java.util.Set;
import com.google.common.collect.Lists;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;

public final class RedisClusterHelper {
	private static RedisClusterHelper redisClusterFactory;
	private Set<hostandport> clusterNodes;
	/** 建立连接池配置参数 */
	private JedisPoolConfig poolConfig;
	private RedisConfigFactory factory;

	private RedisClusterHelper(List<string> hostAndPortList) {
		factory = new RedisConfigFactory(hostAndPortList);
		poolConfig = factory.genJedisConfig();
		clusterNodes = factory.genClusterNode();
	}

	/**
	 * @方法描述 : 静态工厂方法
	 * @return
	 */
	public static RedisClusterHelper getInstance(List<string> hostAndPortList) {
		if (redisClusterFactory == null) {
			redisClusterFactory = new RedisClusterHelper(hostAndPortList);
		}
		return redisClusterFactory;
	}

	/**
	 * @方法描述 : 获取操作redis的客户端
	 * @return
	 */
	private synchronized JedisCluster getRedisClient() {
		return new JedisCluster(clusterNodes, poolConfig);
	}

	/**
	 * @方法描述 : 关闭redis的客户端
	 * @return
	 */
	private void closeRedisClient(JedisCluster jedisClient) {
		if (jedisClient != null) {
			try {
				jedisClient.close();
			} catch (Exception quitExp) {
				quitExp.printStackTrace();
			}
		}
	}

	public static void main(String[] args) {
		List<string> hostAndPortList = Lists.newArrayList();
		hostAndPortList.add("192.168.192.105:27001");
		hostAndPortList.add("192.168.192.105:27002");
		hostAndPortList.add("192.168.192.106:27001");
		hostAndPortList.add("192.168.192.106:27002");
		hostAndPortList.add("192.168.192.107:27001");
		hostAndPortList.add("192.168.192.107:27002");
		RedisClusterHelper redisClusterHelper = RedisClusterHelper.getInstance(hostAndPortList);
	}
}

Jedis踩坑

> Jedis 虽然使用起来比较简单,但是不能根据使用场景设置合理的参数(例如连接池参数),或者不合理地使用了一些功能(例如Lua和事务)时,也会产生很多问题,

坑1 连接异常

  • 现象

    redis.clients.jedis.exceptions.JedisConnectionException: 
        java.net.ConnectException: Connection refuse: connect
    
  • 解决办法

    1. 修改配置文件redis.conf,将bind注释掉,允许外面的机器连接
    2. 修改配置文件redis.conf,将protected-mode 的值改为no,关闭保护模式,并重新启动redis服务

坑2 并发异常

  • 现象
    • 最开始仅提供了一个Jedis,所有线程使用同一个Jedis连接。业务中较频繁地报异常,异常信息为java.lang.ClassCastException: java.util.ArrayList cannot be cast to [B等,基本是ClassCastException,异常抛出位置为调用Jedis的位置。
  • 原因
    • Jedis并非线程安全,不应当并发操作。

坑3 需要手动归还连接

  • 现象
    • 业务线程启动后每访问一定次数(调用Jedis达到一定次数)后就完全不响应请求了
  • 原因
    • 结合TCP四次挥手过程,应该是部分资源释放不了导致没有进入LAST_ACK状态,导致很多线程在等待获取Jedis资源。
    • 在Jedis连接使用完毕后,需要调用Jedis的close()方法将资源归还JedisPool。</string></string></string></hostandport></string></string></string></string></string>
展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
2 收藏
0
分享
返回顶部
顶部