1 Redis客户端
官网推荐的Java客户端有3个:Jedis,Lettuce和Redisson
配置 | 作用 |
---|---|
Jedis | A blazingly small and sane redis java client(体积非常小,但功能很完善) |
lettuce | Advanced Redis client for thread-safe sync, async, and reactive usage. Supports Cluster, Sentinel, Pipelining, and codecs(高级客户端,支持线程安全、异步、反应式编程、集群、哨兵、pipeline、编解码). |
Redisson | distributed and scalable Java data structures on top of Redis server (基于Redis服务实现的Java高级可拓展的数据结构) |
在SpringBoot 2.x之前,RedisTemplate默认使用Jedis,2.x之后默认使用lettuce。
1.1 Jedis
Jedis是我们最熟悉和最常用的客户端。如果不使用RedisTemplate,就可以直接创建Jedis的连接。
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.44.181", 6379);
jedis.set("qingshan", "2673jedis");
System.out.println(jedis.get("qingshan"));
jedis.close();
}
Jedis有一个问题:多个线程使用一个连接的时候线程不安全。
下面也提供了解决思路:使用连接池,为每个连接创建不同的连接,基于Apache common pool实现。
Jedis的连接池有三个实现:JedisPool、ShardingJedisPool、JedisSentinelPool,都是用getResource从连接池获取一个连接。
/**
* 普通连接池
*/
public static void ordinaryPool(){
JedisPool pool = new JedisPool("192.168.44.181",6379);
Jedis jedis = pool.getResource();
jedis.set("qingshan","平平无奇盆鱼宴");
System.out.println(jedis.get("qingshan"));
}
/**
* 分片连接池
*/
public static void shardedPool() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
// Redis服务器
JedisShardInfo shardInfo1 = new JedisShardInfo("192.168.44.181", 6379);
// 连接池
List<JedisShardInfo> infoList = Arrays.asList(shardInfo1);
ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, infoList);
ShardedJedis jedis = jedisPool.getResource();
jedis.set("qingshan","分片测试");
System.out.println(jedis.get("qingshan"));
}
/**
* 哨兵连接池
*/
public static void sentinelPool() {
String masterName = "redis-master";
Set<String> sentinels = new HashSet<String>();
sentinels.add("192.168.44.186:26379");
sentinels.add("192.168.44.187:26379");
sentinels.add("192.168.44.188:26379");
JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels);
pool.getResource().set("qingshan", "哨兵" + System.currentTimeMillis() + "盆鱼宴");
System.out.println(pool.getResource().get("qingshan"));
}
Jedis的功能比较完善,Redis官方的特性全部支持,比如发布订阅、事务、lua脚本、客户端分片、哨兵、集群、pipeline等等。
Sentinel和Cluster的功能上一章节已分析过。Jedis连接Sentinel需要配置所有的哨兵地址。Jedis连接Cluster只需要配置任何一个master或者slave的地址就可以了。
1.1.1 Sentinel获取连接原理
在构造方法中
//必须要指定masterName
JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels);
HostAndPort master = this.initSentinels(sentinels, masterName);
private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
HostAndPort master = null;
boolean sentinelAvailable = false;
log.info("Trying to find master from available Sentinels...");
// 有多个sentinel,遍历这些个sentinel
for (String sentinel : sentinels) {
//host:port表示的sentinel地址转化为一个HostAndPort对象
final HostAndPort hap = HostAndPort.parseString(sentinel);
log.fine("Connecting to Sentinel " + hap);
Jedis jedis = null;
try {
// 连接到sentinel
jedis = new Jedis(hap.getHost(), hap.getPort());
// 根据masterName 得到master的地址,返回一个list,host=list[0],port=list[1]
List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
// connected to sentinel...
sentinelAvailable = true;
if (masterAddr == null || masterAddr.size() != 2) {
log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap
+ ".");
continue;
}
master = toHostAndPort(masterAddr);
log.fine("Found Redis master at " + master);
break;
} catch (JedisException e) {
// resolves #1036, it should handle JedisException there's another chance
// of raising JedisDataException
log.warning("Cannot get master address from sentinel running @ " + hap + ". Reason: " + e
+ ". Trying next one.");
} finally {
if (jedis != null) {
jedis.close();
}
}
}
if (master == null) {
if (sentinelAvailable) {
// can connect to sentinel, but master name seems to not
// monitored
throw new JedisException("Can connect to sentinel, but " + masterName
+ " seems to be not monitored...");
} else {
throw new JedisConnectionException("All sentinels down, cannot determine where is "
+ masterName + " master is running...");
}
}
log.info("Redis master running at " + master + ", starting Sentinel listeners...");
// 启动每一个sentinel的监听,MasterListener 本质为一个线程,它会订阅sentinel上关于master节点地址改变的消息
for (String sentinel : sentinels) {
final HostAndPort hap = HostAndPort.parseString(sentinel);
MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
// whether MasterListener threads are alive or not, process can be stopped
masterListener.setDaemon(true);
masterListeners.add(masterListener);
masterListener.start();
}
return master;
}
1.1.2 Cluster原理
使用jedis连接Cluster的时候,我们只需要连接任意一个或者多个redis group中的实例地址,那我们是怎么获取到需要操作的Redis Master实例呢?
为了表面set、get的时候发生重定向错误,我们需要把slot和redis节点的关系保存起来,在本地计算slot,就可以获得Redis节点信息。
问题:如何存储slot和redis连接池的关系
public static void main(String[] args) throws IOException {
// 不管是连主备,还是连几台机器都是一样的效果
/*
HostAndPort hp1 = new HostAndPort("192.168.44.181",7291);
HostAndPort hp2 = new HostAndPort("192.168.44.181",7292);
HostAndPort hp3 = new HostAndPort("192.168.44.181",7293);
*/
HostAndPort hp4 = new HostAndPort("192.168.44.181",7294);
HostAndPort hp5 = new HostAndPort("192.168.44.181",7295);
HostAndPort hp6 = new HostAndPort("192.168.44.181",7296);
Set nodes = new HashSet<HostAndPort>();
/*
nodes.add(hp1);
nodes.add(hp2);
nodes.add(hp3);
*/
nodes.add(hp4);
nodes.add(hp5);
nodes.add(hp6);
JedisCluster cluster = new JedisCluster(nodes);
cluster.set("gupao:cluster", "qingshan2673--------------");
System.out.println(cluster.get("gupao:cluster"));;
cluster.close();
}
private void initializeSlotsCache(Set<HostAndPort> startNodes, GenericObjectPoolConfig poolConfig, String password) {
for (HostAndPort hostAndPort : startNodes) {
//获取一个jedis实例
Jedis jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort());
if (password != null) {
jedis.auth(password);
}
try {
//获取redis节点和slot虚拟槽
cache.discoverClusterNodesAndSlots(jedis);
//直接跳出循环
break;
} catch (JedisConnectionException e) {
// try next nodes
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
public void discoverClusterNodesAndSlots(Jedis jedis) {
w.lock();
try {
reset();
//获取节点集合
List<Object> slots = jedis.clusterSlots();
//遍历三个主节点
for (Object slotInfoObj : slots) {
//slotinfo 槽开始,槽结束,主,从
//{[0,5460,7291,7294],[5461,10922,7292,7295],[10923,16383,7293,7296]}
List<Object> slotInfo = (List<Object>) slotInfoObj;
// 如果size<2,代表没有分配slot
if (slotInfo.size() <= MASTER_NODE_INDEX) {
continue;
}
//获取分配到当前master节点的数据槽,例如7291节点的{0,1,2,3.......5460}
List<Integer> slotNums = getAssignedSlotArray(slotInfo);
// hostInfos
int size = slotInfo.size(); //size=4,槽开始,槽结束,主,从
for (int i = MASTER_NODE_INDEX; i < size; i++) {
//第3位和第4位是主从端口的信息
List<Object> hostInfos = (List<Object>) slotInfo.get(i);
if (hostInfos.size() <= 0) {
continue;
}
//根据ip 和端口生成HostAndPort 实例
HostAndPort targetNode = generateHostAndPort(hostInfos);
setupNodeIfNotExist(targetNode);
if (i == MASTER_NODE_INDEX) {
//把slot和jedisPool缓存起来(16384个),key是slot下标,value是连接池
assignSlotsToNode(slotNums, targetNode);
}
}
}
} finally {
w.unlock();
}
}
获取slot和redis实例对应关系之后,接下来就是从集群环境中存取值。
Jedis集群模式下所有的命令都要调用这个方法:JedisClusterCommand#runWithRetries
connection = this.connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
步骤如下:
- 把key作为参数,执行CRC16算法,获取key所在的slot值
- 通过该slot值,去slot对应的map集合中获取jedisPool实例
- 通过jedisPool实例获取jedis实例,最终完成redis数据存取操作。
1.1.3 Jedis实现分布式锁
分布式锁的基本需求: