真的是一段特别的经历。
增加了知识储备:Vert.x异步连接Redis并操作Redis。
事情经过:
团队需要开发离线算法中间件,集成到业务侧后台服务中,
原有的中间件中使用哨兵方式操作Redis,现在需要增加新的逻辑,
使用集群方式操作Redis,中间件中基于Vert.x 3.5.1修改出了一个支持集群连接的版本,
满足集群连接和操作。一切都很美好,测试通过。
于是,自己测试原生Vertx高版本(>3.8.x)的集群连接,这才是噩梦的开始,
由于我自建的Redis集群有密码认证,
所以,使用了Vert.x不同版本测试都无法连接,出于无奈,只能,去掉Redis集群的密码认证,
但是,3.8.x仍旧无法连接,
最后,使用了3.9.4,连接成功,并成功完成Redis操作,
特分享如下,帮助未来有需要的开发者。
Vert.x版本:3.9.4
本文实测:单体Redis和集群Redis,并给出测试结果;
由于没有搭建哨兵环境,所以,哨兵模式没有测试,虽然给出连接样例,但是没有测试结果。
<!-- https://mvnrepository.com/artifact/io.vertx/vertx-core -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>3.9.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.vertx/vertx-redis-client -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-redis-client</artifactId>
<version>3.9.4</version>
</dependency>
为统一管理Redis地址,使用枚举管理,
后续添加Redis连接配置时,从这个枚举库中提取,
代码如下所示:
package com.monkey.java_study.common.enums;
/**
* Redis地址库.
*
* @author xindaqi
* @since 2022-07-01 11:10
*/
public enum RedisAddressEnum {
// Redis集群地址
CLUSTER_NODE_1("redis://192.168.211.129:9001"),
CLUSTER_NODE_2("redis://192.168.211.129:9002"),
CLUSTER_NODE_3("redis://192.168.211.129:9003"),
CLUSTER_NODE_4("redis://192.168.211.129:9004"),
CLUSTER_NODE_5("redis://192.168.211.129:9005"),
CLUSTER_NODE_6("redis://192.168.211.129:9006"),
// 单机Redis地址
STANDALONE_NODE("redis://:123456@127.0.0.1/0"),
;
private String address;
RedisAddressEnum(String address) {
this.address = address;
}
public String getAddress() {
return address;
}
}
Vert.x 3.9.4提供了两种方式配置Redis地址,
分别为:addConnectionString和setEndpoints,
同时,可以指定连接类型,集群为:RedisClientType.CLUSTER
特别注意:集群方式连接时,集群千万不可配置密码认证,否则无法连接到Redis集群
redis://ip:port
/**
* Redis集群连接配置:addConnectionString方式
*
* @return Redis集群配置
*/
private static RedisOptions setClusterRedisAddressOneByOne() {
return new RedisOptions()
.setType(RedisClientType.CLUSTER)
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_1.getAddress())
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_2.getAddress())
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_3.getAddress())
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_4.getAddress())
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_5.getAddress())
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_6.getAddress());
}
List<String>
,因此可以批量添加。 /**
* Redis集群连接配置:setEndpoints方式
*
* @return Redis集群配置
*/
private static RedisOptions setClusterRedisAddressBatch() {
List<String> redisAddressList = Stream.of(
RedisAddressEnum.CLUSTER_NODE_1.getAddress(),
RedisAddressEnum.CLUSTER_NODE_2.getAddress(),
RedisAddressEnum.CLUSTER_NODE_3.getAddress(),
RedisAddressEnum.CLUSTER_NODE_4.getAddress(),
RedisAddressEnum.CLUSTER_NODE_5.getAddress(),
RedisAddressEnum.CLUSTER_NODE_6.getAddress()).collect(Collectors.toList());
return new RedisOptions()
.setType(RedisClientType.CLUSTER)
.setEndpoints(redisAddressList);
}
由Vert.x3.9.4源码可知,Redis客户端连接类型有三种:STANDALONE(单体)、SENTINEL(哨兵)和CLUSTER(集群),
源码如下图所示。
位置:io.vertx.redis.client.RedisClientType
配置Redis集群连接地址后,就可以直接连接Redis集群,
因为,Vert.x3.9.4在初始化时,自动配置了默认值,如:
maxWaitingHandler、maxPoolSize和maxPoolWaiting等,源码如下图所示,
默认情况,连接Redis的类型为单体(RedisClientType.STANDALONE),
哨兵模式下的默认不使用从节点:RedisSlaves.NEVER。
位置:io.vertx.redis.client.RedisOptions#init
为什么会加载这些参数,因为,RedisOptions的构造函数:
位置:io.vertx.redis.client.RedisOptions#RedisOptions()
创建样例如下,通过传入Vertx.vertx()和redisOptions(上文配置的参数对象),
即可获取Redis客户端,获取客户端即与Redis建立起连接,
接下来即可完成CURD操作。
Redis.createClient(Vertx.vertx(), redisOptions);
Vert.x 3.9.4创建Redis客户端源码如下图所示,
由源码可知,创建过程中会根据type类型选择不同的客户端(单体、哨兵和集群)。
位置:io.vertx.redis.client.Redis#createClient(io.vertx.core.Vertx, io.vertx.redis.client.RedisOptions)
Vert.x 3.9.4通过RedisAPI操作Redis数据,
测试样例如下,使用Vert.x可知,异步操作,
因此,读取数据使用Vetx.x线程处理:vert.x-eventloop-thread-0
/**
* 读取数据从Redis
*
* @param redisClient Redis客户端
* @param key Redis中查询的键
*/
public void readDataFromCluster(Redis redisClient, String key) {
RedisAPI redisAPI = RedisAPI.api(redisClient);
redisAPI.get(key, res -> {
if (res.succeeded()) {
String value = String.valueOf(res.result());
logger.info(">>>>>>>>Read data from redis cluster (key, value)->:({},{})", key, value);
} else {
logger.error(">>>>>>>>Redis cluster read data error:", res.cause());
}
});
}
package com.monkey.java_study.thirdparty.vertx_test;
import com.monkey.java_study.common.enums.RedisAddressEnum;
import io.vertx.core.Vertx;
import io.vertx.redis.client.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Vert.x连接Redis集群服务和读数据测试.
*
* @author xindaqi
* @since 2022-06-30 12:16
*/
public class VertxClusterOpRedisTest {
private static final Logger logger = LoggerFactory.getLogger(VertxClusterOpRedisTest.class);
/**
* Redis集群客户端
*/
private static final Redis redisClient;
/**
* Redis连接配置
*/
private static final RedisOptions redisOptions;
static {
redisOptions = setClusterRedisAddressBatch();
redisClient = Redis.createClient(Vertx.vertx(), redisOptions);
}
/**
* Redis集群连接配置:addConnectionString方式
*
* @return Redis集群配置
*/
private static RedisOptions setClusterRedisAddressOneByOne() {
return new RedisOptions()
.setType(RedisClientType.CLUSTER)
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_1.getAddress())
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_2.getAddress())
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_3.getAddress())
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_4.getAddress())
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_5.getAddress())
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_6.getAddress());
}
/**
* Redis集群连接配置:setEndpoints方式
*
* @return Redis集群配置
*/
private static RedisOptions setClusterRedisAddressBatch() {
List<String> redisAddressList = Stream.of(
RedisAddressEnum.CLUSTER_NODE_1.getAddress(),
RedisAddressEnum.CLUSTER_NODE_2.getAddress(),
RedisAddressEnum.CLUSTER_NODE_3.getAddress(),
RedisAddressEnum.CLUSTER_NODE_4.getAddress(),
RedisAddressEnum.CLUSTER_NODE_5.getAddress(),
RedisAddressEnum.CLUSTER_NODE_6.getAddress()).collect(Collectors.toList());
return new RedisOptions()
.setType(RedisClientType.CLUSTER)
.setEndpoints(redisAddressList);
}
public Redis getRedisClient() {
return redisClient;
}
/**
* 读取数据从Redis
*
* @param redisClient Redis客户端
* @param key Redis中查询的键
*/
public void readDataFromCluster(Redis redisClient, String key) {
RedisAPI redisAPI = RedisAPI.api(redisClient);
redisAPI.get(key, res -> {
if (res.succeeded()) {
String value = String.valueOf(res.result());
logger.info(">>>>>>>>Read data from redis cluster (key, value)->:({},{})", key, value);
} else {
logger.error(">>>>>>>>Redis cluster read data error:", res.cause());
}
});
}
public static void main(String[] args) {
VertxClusterOpRedisTest vertxOpRedisTest = new VertxClusterOpRedisTest();
vertxOpRedisTest.readDataFromCluster(vertxOpRedisTest.getRedisClient(), "name");
}
}
由测试结果可知,Vert.x 3.9.4操作Redis使用异步线程:vert.x-enventloop-thread-0,
并完成结果读取。
有了上面集群配置的基础,
配置单体连接和哨兵连接就相对好理解一些,
直接给出完整样例,代码如下:
需要注意的是:配置Redis时需要按照需要配置数据库0-15,
连接类型选择:RedisClientType.STANDALONE,
如果是单体可以不选,因为RedisOptions默认值即为:RedisClientType.STANDALONE。
地址格式:
redis://username:password@ip:port/db
参数说明:
序号 | 参数 | 描述 |
---|---|---|
1 | username | 没有可以不填 |
2 | password | Redis连接密码,没有可以不填 |
3 | ip | Redis运行主机IP |
4 | port | Redis端口 |
5 | db | Redis数据库编号,0-15中的一个 |
特别地:
如果只有密码没有用户名,应该这样配置:redis://:password@ip:port/db
package com.monkey.java_study.thirdparty.vertx_test;
import com.monkey.java_study.common.enums.RedisAddressEnum;
import io.vertx.core.Vertx;
import io.vertx.redis.client.Redis;
import io.vertx.redis.client.RedisAPI;
import io.vertx.redis.client.RedisClientType;
import io.vertx.redis.client.RedisOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Vert.x连接Redis单体服务和读数据测试.
*
* @author xindaqi
* @since 2022-06-30 12:16
*/
public class VertxStandaloneOpRedisTest {
private static final Logger logger = LoggerFactory.getLogger(VertxStandaloneOpRedisTest.class);
/**
* Redis单体客户端
*/
private static final Redis redisClient;
/**
* Redis连接配置
*/
private static final RedisOptions redisOptions;
static {
redisOptions = setStandaloneRedisAddress();
redisClient = Redis.createClient(Vertx.vertx(), redisOptions);
}
/**
* 配置Redis单体连接地址
*
* @return Redis单体配置
*/
private static RedisOptions setStandaloneRedisAddress() {
return new RedisOptions()
.setType(RedisClientType.STANDALONE)
.addConnectionString(RedisAddressEnum.STANDALONE_NODE.getAddress());
}
public Redis getRedisClient() {
return redisClient;
}
/**
* 读取数据从Redis
*
* @param redisClient Redis客户端
* @param key Redis中查询的键
*/
public void readDataFromStandalone(Redis redisClient, String key) {
RedisAPI redisAPI = RedisAPI.api(redisClient);
redisAPI.get(key, res -> {
if (res.succeeded()) {
String value = String.valueOf(res.result());
logger.info(">>>>>>>>Read data from redis standalone (key, value)->:({},{})", key, value);
} else {
logger.error(">>>>>>>>Redis standalone read data error:", res.cause());
}
});
}
public static void main(String[] args) {
VertxStandaloneOpRedisTest vertxOpRedisTest = new VertxStandaloneOpRedisTest();
vertxOpRedisTest.readDataFromStandalone(vertxOpRedisTest.getRedisClient(), "name");
}
}
同理,完整样例如下,
需要注意的是,类型配置为:RedisClientType.SENTINEL,
同时需要配置主服务的名称和角色,即
MasterName(“my_test”)和Role(RedisRole.MASTER),
由于没有测试用的哨兵Redis,因此,这里只给出配置样例,
没有实际测试。
package com.monkey.java_study.thirdparty.vertx_test;
import com.monkey.java_study.common.enums.RedisAddressEnum;
import io.vertx.core.Vertx;
import io.vertx.redis.client.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Vert.x连接Redis哨兵服务和读数据测试.
*
* @author xindaqi
* @since 2022-06-30 12:16
*/
public class VertxSentinelOpRedisTest {
private static final Logger logger = LoggerFactory.getLogger(VertxSentinelOpRedisTest.class);
/**
* Redis哨兵客户端
*/
private static final Redis redisClient;
/**
* Redis连接配置
*/
private static final RedisOptions redisOptions;
static {
redisOptions = setSentinelRedisAddressOneByOne();
redisClient = Redis.createClient(Vertx.vertx(), redisOptions);
}
/**
* Redis哨兵连接配置:addConnectionString方式
*
* @return Redis集群配置
*/
private static RedisOptions setSentinelRedisAddressOneByOne() {
return new RedisOptions()
.setType(RedisClientType.SENTINEL)
.setMasterName("my_test")
.setRole(RedisRole.MASTER)
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_1.getAddress())
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_2.getAddress())
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_3.getAddress())
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_4.getAddress())
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_5.getAddress())
.addConnectionString(RedisAddressEnum.CLUSTER_NODE_6.getAddress());
}
/**
* Redis哨兵连接配置:setEndpoints方式
*
* @return Redis集群配置
*/
private static RedisOptions setSentinelRedisAddressBatch() {
List<String> redisAddressList = Stream.of(
RedisAddressEnum.CLUSTER_NODE_1.getAddress(),
RedisAddressEnum.CLUSTER_NODE_2.getAddress(),
RedisAddressEnum.CLUSTER_NODE_3.getAddress(),
RedisAddressEnum.CLUSTER_NODE_4.getAddress(),
RedisAddressEnum.CLUSTER_NODE_5.getAddress(),
RedisAddressEnum.CLUSTER_NODE_6.getAddress()).collect(Collectors.toList());
return new RedisOptions()
.setType(RedisClientType.SENTINEL)
.setMasterName("my_test")
.setRole(RedisRole.MASTER)
.setEndpoints(redisAddressList);
}
public Redis getRedisClient() {
return redisClient;
}
/**
* 读取数据从Redis
*
* @param redisClient Redis客户端
* @param key Redis中查询的键
*/
public void readDataFromSentinel(Redis redisClient, String key) {
RedisAPI redisAPI = RedisAPI.api(redisClient);
redisAPI.get(key, res -> {
if (res.succeeded()) {
String value = String.valueOf(res.result());
logger.info(">>>>>>>>Read data from redis sentinel (key, value)->:({},{})", key, value);
} else {
logger.error(">>>>>>>>Redis sentinel read data error:", res.cause());
}
});
}
public static void main(String[] args) {
VertxSentinelOpRedisTest vertxOpRedisTest = new VertxSentinelOpRedisTest();
vertxOpRedisTest.readDataFromSentinel(vertxOpRedisTest.getRedisClient(), "name");
}
}
Vert.x 3.9.4连接和操作Redis核心:
(1)配置Redis连接地址,通过RedisOptions,该类实例化时,有默认的参数;
(2)Vert.x3.9.4提供的Redis客户端无法连接需要密码认证的Redis集群,但是,可以连接单体需要密码认证的Redis;
(3)创建连接使用Redis.createClient,该方法根据type选择建立对应的Redis客户端:集群(cluster)、哨兵(sentinel)和单体(standalone);
(4)操作Redis使用RedisAPI完成;
(5)Vert.x操作Redis是通过异步线程完成:Vert.x的线程,如vert.x-eventloop-thread-0。