介绍
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 集成了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);
// 连接空闲多久后释放, 当空闲时间>该值 且 空闲连接>最大空闲连接数 时直接释放
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
-
解决办法
- 修改配置文件redis.conf,将bind注释掉,允许外面的机器连接
- 修改配置文件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>