环境与目录
IDE: IntelliJ IDEA 2018.2
Redis:2.0.8 and 4.0.12 (给自己挖了个坑)
Server:VmWare中的两个CentOS7系统(也可以一个系统安装两个Redis节点)
前提:保证每个redis节点能用,并将防火墙对每个节点的端口开放
测试项目的目录:
一、引入Jar包并添加配置文件
本例为Maven项目,采用Jedis操作Redis,同时使用了一个String的校验工具,需要引入的jar,因为最新的jedis版本已经5.0了,工具类中的某些方法如returnBrokenResource已经不再推荐改为Close了如下:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
两个Redis节点和连接池参数的配置如下:
#redis config start
redis1.ip=你的IP1
redis2.ip=你的IP2
redis1.port=6379
redis2.port=6379
redis1.pwd=lc
redis2.pwd=lc
#最大连接数
redis.max.total=20
#最大空闲数
redis.max.idle=10
#最小空闲数
redis.min.idle=2
#从jedis连接池获取连接时,校验并返回可用的连接
redis.test.borrow=false
#把连接放回redis连接池时,校验并返回可用的连接
redis.test.return=false
#redis config end
二、添加读取配置文件的工具类
实际使用中,可以通过maven编译不同环境的配置文件从而实现环境隔离,同时工具类中的值也可以使用Spring读取属性并注入到工具类中。在这里只是简单的读取一下文件内容,同时也没有用到日志框架,简单打印到控制台即可,如有需要请自行修改,具体如下:
/**
* properties文件读取文件读取工具
* @author wlc
*/
public class PropertyUtil {
private static Properties props;
/**
* 加载配置文件内容
*/
static {
String fileName = "redis.properties";
props = new Properties();
try {
props.load(new InputStreamReader(PropertyUtil.class.getClassLoader().getResourceAsStream(fileName),"UTF-8"));
} catch (IOException e) {
System.out.println("配置文件读取异常"+e.toString());
}
}
/**
* 根据key获取value
* @param key
* @return
*/
public static String getProperty(String key){
String value = props.getProperty(key.trim());
if(StringUtils.isBlank(value)){
return null;
}
return value.trim();
}
/**
* 根据key获取value,如获取的value为空,则返回默认值
* @param key
* @param defaultValue
* @return
*/
public static String getProperty(String key,String defaultValue){
String value = props.getProperty(key.trim());
if(StringUtils.isBlank(value)){
value = defaultValue;
}
return value.trim();
}
}
三、连接池封装
此次连接池的封装是写死了两个配置,并没有做到灵活添加和减少redis节点的功能,主要依靠ShardedJedisPool进行Hash分配。具体封装如下:
/**
* 分布式Redis连接池
* @author lc
*/
public class RedisShardedPool {
/**分布式连接池*/
private static ShardedJedisPool pool;
/**最大连接数*/
private static Integer maxTotal=Integer.parseInt(PropertyUtil.getProperty("redis.max.total"));
/**最大空闲连接数*/
private static Integer maxIdle=Integer.parseInt(PropertyUtil.getProperty("redis.max.idle"));
/**最小空闲连接数*/
private static Integer minIdle=Integer.parseInt(PropertyUtil.getProperty("redis.min.idle"));
/**在borrow一个jedis实例时,是否提前进行alidate操作;如果为true,则得到的jedis实例均是可用的*/
private static Boolean testOnBorrow=Boolean.parseBoolean(PropertyUtil.getProperty("redis.test.borrow"));
/**在return给pool时,是否提前进行validate操作*/
private static Boolean testOnReturn=Boolean.parseBoolean(PropertyUtil.getProperty("redis.test.return"));
/**redis1节点的ip*/
private static String redis1Ip = PropertyUtil.getProperty("redis1.ip");
/**redis2节点的ip*/
private static String redis2Ip = PropertyUtil.getProperty("redis2.ip");
/**redis1节点的端口号*/
private static Integer redis1Port = Integer.parseInt(PropertyUtil.getProperty("redis1.port"));
/**redis1节点的端口号*/
private static Integer redis2Port = Integer.parseInt(PropertyUtil.getProperty("redis2.port"));
private static String redis1pwd = PropertyUtil.getProperty("redis1.pwd");
private static String redis2pwd = PropertyUtil.getProperty("redis2.pwd");
static {
initPool();
}
/**
* 初始化连接池
*/
private static void initPool(){
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(maxTotal);
config.setMaxIdle(maxIdle);
config.setMinIdle(minIdle);
config.setTestOnBorrow(testOnBorrow);
config.setTestOnReturn(testOnReturn);
//当连接耗尽的时候是否阻塞等待获取,false会抛出异常,true则阻塞直到超时
config.setBlockWhenExhausted(true);
JedisShardInfo info1 = new JedisShardInfo(redis1Ip,redis1Port,1000*2);
info1.setPassword(redis1pwd);
JedisShardInfo info2 = new JedisShardInfo(redis2Ip,redis2Port,1000*2);
info2.setPassword(redis2pwd);
List<JedisShardInfo> jedisList = new ArrayList<>(2);
jedisList.add(info1);
jedisList.add(info2);
//Hashing.MURMUR_HASH就是使用的Hash一致性算法,最后一个参数为分片方式
pool = new ShardedJedisPool(config,jedisList, Hashing.MURMUR_HASH, Sharded.DEFAULT_KEY_TAG_PATTERN);
}
/**
* 获取分布式jedis连接池中的连接
* @return
*/
public static ShardedJedis getJedis(){
return pool.getResource();
}
/**
* 返还连接
* @param jedis
*/
public static void returnResource(ShardedJedis jedis){
//jedis2.9以后,close方法已经代替了returnBrokenResource和returnResource方法
jedis.close();
}
由于机器的性能可能会有所区别,所以在实例化JedisShardInfo这个类的时候,我们也可以添加权重,分片策略会根据每个Redis节点的权重创建不同数量的虚拟节点(关于虚拟节点的由来,可参考[Redis分布式算法原理—Hash一致性理解],(https://blog.csdn.net/wlccomeon/article/details/86553831) 这篇文章),默认会创建160个。具体的创建过程如下:
private void initialize(List<S> shards) {
this.nodes = new TreeMap();
for(int i = 0; i != shards.size(); ++i) {
S shardInfo = (ShardInfo)shards.get(i);
int n;
if (shardInfo.getName() == null) {
for(n = 0; n < 160 * shardInfo.getWeight(); ++n) {
this.nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n), shardInfo);
}
} else {
for(n = 0; n < 160 * shardInfo.getWeight(); ++n) {
this.nodes.put(this.algo.hash(shardInfo.getName() + "*" + shardInfo.getWeight() + n), shardInfo);
}
}
this.resources.put(shardInfo, shardInfo.createResource());
}
}
四、Redis操作工具类
这部分就是程序员在业务中需要直接使用的方法了,只是几个常用的方法,并不全面,如有需要自己补充一下:
/**
* 分布式redis工具类
* @author wlc
*/
public class RedisShardedUtil {
/**
* 设置key的有效期,单位是秒
* @param key
* @param exTime
* @return
*/
public static Long expire(String key,int exTime){
ShardedJedis jedis = null;
Long result = null;
try {
jedis = RedisShardedPool.getJedis();
result = jedis.expire(key,exTime);
} catch (Exception e) {
e.printStackTrace();
RedisShardedPool.returnResource(jedis);
return result;
}
RedisShardedPool.returnResource(jedis);
return result;
}
/**
* 添加并设置key的有效期,exTime的单位是秒
* @param key
* @param value
* @param exTime
* @return
*/
public static String setEx(String key,String value,int exTime){
ShardedJedis jedis = null;
String result = null;
try {
jedis = RedisShardedPool.getJedis();
result = jedis.setex(key,exTime,value);
} catch (Exception e) {
e.printStackTrace();
RedisShardedPool.returnResource(jedis);
return result;
}
RedisShardedPool.returnResource(jedis);
return result;
}
/**
* 添加字符串键值对
* @param key
* @param value
* @return
*/
public static String set(String key,String value){
ShardedJedis jedis = null;
String result = null;
try {
jedis = RedisShardedPool.getJedis();
result = jedis.set(key,value);
} catch (Exception e) {
e.printStackTrace();
RedisShardedPool.returnResource(jedis);
return result;
}
RedisShardedPool.returnResource(jedis);
return result;
}
/**
* 使用value覆盖key中原来的值,并返回原来的值
* @param key
* @param value
* @return
*/
public static String getSet(String key,String value){
ShardedJedis jedis = null;
String result = null;
try {
jedis = RedisShardedPool.getJedis();
result = jedis.getSet(key,value);
} catch (Exception e) {
e.printStackTrace();
RedisShardedPool.returnResource(jedis);
return result;
}
RedisShardedPool.returnResource(jedis);
return result;
}
/**
* 根据字符串key获取value
* @param key
* @return
*/
public static String get(String key){
ShardedJedis jedis = null;
String result = null;
try {
jedis = RedisShardedPool.getJedis();
result = jedis.get(key);
} catch (Exception e) {
e.printStackTrace();
RedisShardedPool.returnResource(jedis);
return result;
}
RedisShardedPool.returnResource(jedis);
return result;
}
/**
* 根据key删除value
* @param key
* @return
*/
public static Long del(String key){
ShardedJedis jedis = null;
Long result = null;
try {
jedis = RedisShardedPool.getJedis();
result = jedis.del(key);
} catch (Exception e) {
e.printStackTrace();
RedisShardedPool.returnResource(jedis);
return result;
}
RedisShardedPool.returnResource(jedis);
return result;
}
/**
* 添加键值对之前检查,若已存在则不添加
* @param key
* @param value
* @return
*/
public static Long setnx(String key,String value){
ShardedJedis jedis = null;
Long result = null;
try {
jedis = RedisShardedPool.getJedis();
result = jedis.setnx(key,value);
} catch (Exception e) {
e.printStackTrace();
RedisShardedPool.returnResource(jedis);
return result;
}
RedisShardedPool.returnResource(jedis);
return result;
}
}
五、踩过的坑
5.1 连接redis失败之一坑
Exception in thread "main" redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
...
Caused by: java.util.NoSuchElementException: Unable to validate object
...
一开始查了一下原因,说是设置testOnBorrow为false可以解决该问题,设置完毕之后,上面的问题确实没有了,但出现了下面的第二个坑,所以说,根本原因不在这个,解决方法看第2个坑。
5.2 连接redis失败之二坑
redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out
...
Caused by: java.net.SocketTimeoutException: connect timed out
...
这个就很明显了,获取jedis连接超时。这种情况下就得必须确认:
①检查redis的配置是否有问题,确定centos中redis的服务是否已经启动
②确保windows主机和虚拟机之间通讯正常,如通过ping IP和telnet IP 端口 确认
③redis节点的端口是否已经开放,如未开放,则可通过如下形式查看和添加:
firewall-cmd --list-port
firewall-cmd --zone=public(作用域) --add-port=6379/tcp(端口和访问类型) --permanent(永久生效)
firewall-cmd --reload
上面几点能确认之后就不会报坑一和坑二的错了。至于上面说的testOnBorrow设置为false只是不检查坑一的错误而已,但错误还在。
5.3 保护模式下的坑
redis.clients.jedis.exceptions.JedisDataException: DENIED Redis is running in protected mode because protected mode is enabled, no bind address was specified, no authentication password is requested to clients.
....
上面的这种提示归根结底还是我自己挖的坑,centOS系统中的两个redis节点一个2.8版本,一个是4.0版本,这两个版本的redis.conf配置文件中的内容有些配置是不一样的。4.0版本默认处于保护模式,解决这个问题大概有以下4个方式:
①使用redis客户端执行CONFIG SET protected-mode no 临时关闭保护模式
②编辑redis.conf文件中的protected-mode yes配置为protected-mode no ,需要注意的是启动的时候要指定该修改后的配置文件,否则不会生效
③redis服务端开启的时候添加参数 --protected-mode no,如 ./redis-server --port 6380 --protected-mode no
④绑定ip或者连接Redis服务的时候使用密码
我使用的第4种解决方法,为Redis设置了密码,这个也算是再正常不过的事情了,也是我认为比较能彻底解决这个问题的方式。
5.4 拒绝连接之坑
redis.clients.jedis.exceptions.JedisConnectionException: java.net.ConnectException: Connection refused: connect
...
Caused by: java.net.ConnectException: Connection refused: connect
...
这个坑还是4.0版本的redis引起的。这个版本的redis.conf中默认会有个绑定本地ip的配置:bind 127.0.0.1,这样远程肯定操作不了哇。而2.8版本中没有,将这句注掉(#bind 127.0.0.1)之后,以./redis-server …/redis.conf方式重启之后就可以成功运行了。
当以上操作和坑都解决之后,可以写个main方法从分布式redis连接池中获取连接,并使用一个for循环存入10条数据,查看结果就可以了。
六、结语
其实上面有个小坑:redis分片只使用了两个节点,如果需要想要动态扩容还是比较麻烦的。有个改造思路,将redisIp那里可以在配置文件中设置为一个以逗号分隔的多个ip字符串,redisPort那里也如此设置。然后在下面进行遍历,获取多个JedisShardedInfo的时候就可以分别获取IP和端口号,最后组成多个分片,实现动态扩容,如此做的话,修改的地方就只有一个配置文件了。
做分布式Redis的时候,正确的做法应该是用ShardedJedisPool单个单个的测试每个Redis的服务使用情况,确保不会受到其他节点的影响。否则一看报错之后还得挨个确认各个服务是否正常,这样比较浪费时间。再有就是虽然练习的时候对单个服务进行测试能够知道不同版本之间的差别,但服务的版本在生产中必须统一,避免由于版本升级修改配置和代码导致原来的功能失效或者其它莫名其妙的问题。这里仅仅是分布式redis入门级别的封装,如有不足之处欢迎大家指正。