最近做的项目有用到redis,主要是用于登录保存token,保存微信的accessToken,使用链接池来优化redis,在每个操作后自动释放资源,无需每个 都使用finally{jedis.close();},由于redis 再项目中使用的情况比较平凡,因此使用 单例模式中的 饿汉模式来初始化.
1.1 所需jar
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.30</version>
</dependency>
1.2 配置文件
定义了一些基础的配置 比如:redis 的host:port, 最大链接数等等
# 操作超时时间,默认2秒
timeout=3000
# redis url接口以";"分割多个地址
urls=127.0.0.1:6379
# jedis池最大连接数总数,默认8
maxTotal=8
# jedis池最大空闲连接数,默认8
maxIdle=8
#jedis池最少空闲连接数
minIdle=3
# jedis池没有对象返回时,最大等待时间单位为毫秒
maxWaitTime=60000
# 在borrow一个jedis实例时,是否提前进行validate操作
testOnBorrow=true
1.3 定义一个方法实现的接口,方便统一释放资源
import redis.clients.jedis.Jedis;
/**
* @program: lw_weaver
* @description:
* @author: zhouQ
* @create: 2021-02-08 09:56
**/
// redis具体逻辑接口
public interface HashRedisExecutor<T> {
T execute(Jedis jedis);
}
1.4 创建HashRedisUtil工具类
采用分布式存储,即有多个redis提供服务,配置文件中的urls 可以有多个,定义多个urls 的分隔符,定义一些初始的参数如,JedisPool[]等
// 多个url 间用; 来分割
private static final String DEFAULT_REDIS_SEPARATOR = ";";
// 端口号分隔符
private static final String HOST_PORT_SEPARATOR = ":";
// jedis线程池 默认是0 等到 初始化的时候赋值
private JedisPool[] jedisPools = new JedisPool[0];
// 工具类示例话 final 修饰 唯一 不可变, static 此处处保证在类加载的时候 初始化 HashRedisUtil,执行无参构造, 单例模式 饿汉
private static final HashRedisUtil INSTANCE = new HashRedisUtil();
当类在被加载的时候,static 修饰的 DEFAULT_REDIS_SEPARATOR、HOST_PORT_SEPARATOR创建,执行new HashRedisUtil()方法,实例化HashRedisUtil
1.4.1 HashRedisUtil 的无参构造方法
// 无参构造
private HashRedisUtil() {
initPool();
}
// 初始化 jedis 池
private void initPool() {
// 操作超时时间,默认2秒
int timeout = NumberUtils.toInt(Prop.getPropValue("hashRedis", "timeout"), 2000);
// jedis池最大连接数总数,默认8
int maxTotal = NumberUtils.toInt(Prop.getPropValue("hashRedis", "maxtotal"), 8);
// jedis池最大空闲连接数,默认8
int maxIdle = NumberUtils.toInt(Prop.getPropValue("hashRedis", "maxIdle"), 8);
// jedis池最少空闲连接数
int minIdle = NumberUtils.toInt(Prop.getPropValue("hashRedis", "minIdle"), 0);
// jedis池没有对象返回时,最大等待时间单位为毫秒
long maxWaitMillis = NumberUtils.toLong(Prop.getPropValue("hashRedis", "maxWaitTime"), -1);
// 在borrow一个jedis实例时,是否提前进行validate操作
boolean testOnBorrow = Boolean.parseBoolean(Prop.getPropValue("hashRedis", "testOnBorrow"));
// 设置jedis连接池配置
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(maxTotal);
poolConfig.setMaxIdle(maxIdle);
poolConfig.setMinIdle(minIdle);
poolConfig.setMaxWaitMillis(maxWaitMillis);
poolConfig.setTestOnBorrow(testOnBorrow);
// 取得redis的url
String redisUrls = Prop.getPropValue("hashRedis", "urls");
if (StringUtils.isBlank(redisUrls)) {
throw new IllegalStateException("the urls of redis is not configured");
}
// 生成连接池
List<JedisPool> jedisPoolList = new ArrayList<JedisPool>();
// 根据 ; 切割所有的 redis 服务 并遍历 获取每一个url:port
for (String redisUrl : redisUrls.split(DEFAULT_REDIS_SEPARATOR)) {
// 根据 : 切割 host 和 port
String[] redisUrlInfo = redisUrl.split(HOST_PORT_SEPARATOR);
// 为每一个 redis 服务创建一个 jedis池
jedisPoolList.add(new JedisPool(poolConfig, redisUrlInfo[0], Integer.parseInt(redisUrlInfo[1]), timeout));
}
jedisPools = jedisPoolList.toArray(jedisPools);
}
使用的单例模式 ,因此构造方法为 privite修饰 ,提供一个对外界唯一的访问方法getInstance()
/***
* 提供外界访问的方法
* @return
*/
public static HashRedisUtil getInstance() {
return INSTANCE;
}
1.4.2 定义一个公共的方法execute
/**
* 实现jedis连接的获取和释放,具体的redis业务逻辑由executor实现
*
* @param executor RedisExecutor接口的实现类
* @return
*/
public <T> T execute(String key, HashRedisExecutor<T> executor) {
// 根据存放的key 的hashcode 算出应该存放在那台redis 服务上,获取对应的jedis
Jedis jedis = jedisPools[(0x7FFFFFFF & key.hashCode()) % jedisPools.length].getResource();
T result = null;
try {
// 实现 HashRedisExecutor 接口的 executor 方法,对于具体 redis 的操作方法 只需要执行此 execute方法,并根据业务具体重写 executor.execute(jedis) 就行, 而且执行完成后 最后执行finally 中的 close 方法,无需再每个方法中都去释放资源
result = executor.execute(jedis);
} finally {
if (jedis != null) {
jedis.close();
}
}
return result;
}
关于为什么使用(0x7FFFFFFF & key.hashCode()) % jedisPools.length
使用(0x7FFFFFFF & key.hashCode()) % jedisPools.length 使得存放在每一台redis服务上的数据均衡分布,不会形成数据倾斜
// (0x7FFFFFFF & key.hashCode()
// 0x7FFFFFFF 是用十六进制表示的一个数, 是int 的最大值,表示01111 1111 1111 1111 1111 1111 1111 1111
// key.hashCode() 取到的是key 的hashCode 是一个int 0000 0000 0000 1011 1101 0010 1110 1001 两数&运算 同真为真 一假为假
// 0000 0000 0000 1011 1101 0010 1110 1001
// 可以发现 与 0x7FFFFFFF & 运算后 得到的还是 key.hashCode() 的hashCode 那为什么还要与 0x7FFFFFFF 进行&运算呢,
// 可以发现 0x7FFFFFFF 的最高位 符号位 为 0 ,如果 key.hashCode() 的 是int 的最小值 -2^31次
// 01111 1111 1111 1111 1111 1111 1111 1111
// & 运算
// 11111 1111 1111 1111 1111 1111 1111 1111
// 此时就能保证得到的数 是一个整数 01111 1111 1111 1111 1111 1111 1111 1111
// 至于为什么不用 Math.abs() 取正数呢; int 的取值范围是 -2^31 ~ 2^31 - 1 当取到的负数 -2^31 取到正数 是 2^31 则超出了 int 正数的最大范围
// 01111 1111 1111 1111 1111 1111 1111 1111
// 11111 1111 1111 1111 1111 1111 1111 1111 得到的还是正数 的 01111 1111 1111 1111 1111 1111 1111 1111 不会溢出
使用测试数据如下图所示均衡分布
1.4.3具体方法实现 每个方法都调用 execute()
/***
* set String
* @param key
* @param value
* @return
*/
public String set(final String key, final String value) {
return execute(key, new HashRedisExecutor<String>() {
@Override
public String execute(Jedis jedis) {
return jedis.set(key, value);
}
});
}
/***
*
* @param key
* @param value
* @param nxxx nx key不存在时设置value,成功返回OK,失败返回(nil), xx key存在时设置value,成功返回OK,失败返回(nil)
* @param expx ex 设置失效时长,单位秒 , px 设置失效时长,单位毫秒
* @param time 失效时间
* @return
*/
public String set(final String key, final String value, final String nxxx, final String expx, final long time) {
return execute(key, new HashRedisExecutor<String>() {
@Override
public String execute(Jedis jedis) {
SetParams params = new SetParams();
if (ObjectUtils.equals(nxxx, "xx")) {
params.xx();
} else {
params.nx();
}
if (ObjectUtils.equals(expx, "ex")) {
params.ex((int) time);
} else {
params.px(time);
}
return jedis.set(key, value, params);
}
});
}
/***
* 根据key 取值
* @param key
* @return
*/
public String get(final String key) {
return execute(key, new HashRedisExecutor<String>() {
@Override
public String execute(Jedis jedis) {
return jedis.get(key);
}
});
}
/***
* 根据key 判断是否存在
* @param key
* @return
*/
public Boolean exists(final String key) {
return execute(key, new HashRedisExecutor<Boolean>() {
@Override
public Boolean execute(Jedis jedis) {
return jedis.exists(key);
}
});
}
/***
* key不存在时设置value,成功返回OK,失败返回(nil)
* @param key
* @param value
* @return
*/
public Long setnx(final String key, final String value) {
return execute(key, new HashRedisExecutor<Long>() {
@Override
public Long execute(Jedis jedis) {
return jedis.setnx(key, value);
}
});
}
/***
* set值并且设置失效时间 单位s
* @param key
* @param seconds 设置失效时间 单位 秒
* @param value
* @return
*/
public String setex(final String key, final int seconds, final String value) {
return execute(key, new HashRedisExecutor<String>() {
@Override
public String execute(Jedis jedis) {
return jedis.setex(key, seconds, value);
}
});
}
/***
* key已经存在,设置key生存时间,当key过期时,它会被自动删除。
* @param key
* @param seconds
* @return
*/
public Long expire(final String key, final int seconds) {
return execute(key, new HashRedisExecutor<Long>() {
@Override
public Long execute(Jedis jedis) {
return jedis.expire(key, seconds);
}
});
}
/***
* 将key 对应得value +1, 如果不存在 先 初始化0 再执行 incr 操作
* @param key
* @return
*/
public Long incr(final String key) {
return execute(key, new HashRedisExecutor<Long>() {
@Override
public Long execute(Jedis jedis) {
return jedis.incr(key);
}
});
}
/***
* 将key 对应得value -1, 如果不存在 先 初始化0 再执行 decr 操作
* @param key
* @return
*/
public Long decr(final String key) {
return execute(key, new HashRedisExecutor<Long>() {
@Override
public Long execute(Jedis jedis) {
return jedis.decr(key);
}
});
}
/***
* 为哈希表中的字段赋值
* @param key
* @param field
* @param value
* @return
*/
public Long hset(final String key, final String field, final String value) {
return execute(key, new HashRedisExecutor<Long>() {
@Override
public Long execute(Jedis jedis) {
return jedis.hset(key, field, value);
}
});
}
/***
* 返回哈希表中指定字段的值
* @param key
* @param field
* @return
*/
public String hget(final String key, final String field) {
return execute(key, new HashRedisExecutor<String>() {
@Override
public String execute(Jedis jedis) {
return jedis.hget(key, field);
}
});
}
/***
* 同时将多个 field-value (字段-值)对设置到哈希表中。此命令会覆盖哈希表中已存在的字段。
* @param key
* @param hash
* @return
*/
public String hmset(final String key, final Map<String, String> hash) {
return execute(key, new HashRedisExecutor<String>() {
@Override
public String execute(Jedis jedis) {
return jedis.hmset(key, hash);
}
});
}
/***
* 返回哈希表中指定字段的值。
* @param key
* @param fields
* @return
*/
public List<String> hmget(final String key, final String... fields) {
return execute(key, new HashRedisExecutor<List<String>>() {
@Override
public List<String> execute(Jedis jedis) {
return jedis.hmget(key, fields);
}
});
}
/***
* 根据 key 删除指定的值
* @param key
* @return
*/
public Long del(final String key) {
return execute(key, new HashRedisExecutor<Long>() {
@Override
public Long execute(Jedis jedis) {
return jedis.del(key);
}
});
}
/***
* 返回哈希表中,所有的字段和值。
* @param key
* @return
*/
public Map<String, String> hgetAll(final String key) {
return execute(key, new HashRedisExecutor<Map<String, String>>() {
@Override
public Map<String, String> execute(Jedis jedis) {
return jedis.hgetAll(key);
}
});
}
/***
* 关闭指定 jedis 连接池 释放资源
*/
public void destroy() {
for (int i = 0; i < jedisPools.length; i++) {
jedisPools[i].close();
}
}