应用场景
如果你的某一个Redis操作必须要使用事务或者脚本保证其原子性或者需要进行多个key的操作的时候,但是公司部署的又是Redis集群。这种情况下就必须使用单实例的Redis来操作了。
方法一、简单粗暴
把集群的每个Master都连接一遍,然后在客户端进行slot映射缓存每个Master对应的JedisPool
方法二、从JedisCluster中获取
介绍这种方法前先介绍一下怎么把多个不同的key写到同一个Redis实例中。下面上官网原文:
键哈希标签(Keys hash tags)
计算哈希槽可以实现哈希标签(hash tags),但这有一个例外。哈希标签是确保两个键都在同一个哈希槽里的一种方式。将来也许会使用到哈希标签,例如为了在集群稳定的情况下(没有在做碎片重组操作)允许某些多键操作。
为了实现哈希标签,哈希槽是用另一种不同的方式计算的。基本来说,如果一个键包含一个 “{…}” 这样的模式,只有 { 和 } 之间的字符串会被用来做哈希以获取哈希槽。但是由于可能出现多个 { 或 },计算的算法如下:
- 如果键包含一个 { 字符。
- 那么在 { 的右边就会有一个 }。
- 在 { 和 } 之间会有一个或多个字符,第一个 } 一定是出现在第一个 { 之后。
然后不是直接计算键的哈希,只有在第一个 { 和它右边第一个 } 之间的内容会被用来计算哈希值。
例子:
- 比如这两个键 {user1000}.following 和 {user1000}.followers 会被哈希到同一个哈希槽里,因为只有 user1000 这个子串会被用来计算哈希值。
- 对于 foo{}{bar} 这个键,整个键都会被用来计算哈希值,因为第一个出现的 { 和它右边第一个出现的 } 之间没有任何字符。
- 对于 foozap 这个键,用来计算哈希值的是 {bar 这个子串,因为它是第一个 { 及其右边第一个 } 之间的内容。
- 对于 foo{bar}{zap} 这个键,用来计算哈希值的是 bar 这个子串,因为算法会在第一次有效或无效(比如中间没有任何字节)地匹配到 { 和 } 的时候停止。
- 按照这个算法,如果一个键是以 {} 开头的话,那么就当作整个键会被用来计算哈希值。当使用二进制数据做为键名称的时候,这是非常有用的。
代码验证一下:
public static void main(String[] args) {
String keyPre = "{dhfhdksaf}";
System.out.println(JedisClusterCRC16.getSlot(keyPre+"2") == JedisClusterCRC16.getSlot(keyPre+"1"));
}通过使用这个方法就可以将多个Key写到同一个实例了。
获取单个Redis实例
先说一下核心原理:
JedisCluster中有两个缓存Map,其中一个是node的Map<String,JedisPool>,具体可以看上一篇文章JedisCluster源码阅读,这个Map可以通过cluster.getClusterNodes()方法获取到,这样我们就可以获取到JedisPool了,然后通过这个JedisPool的jedis实例去执行集群命令cluster node就可以获取集权中的节点信息,这些节点信息中包含了每个Redis实例的slot信息:
最后就可以根据key的slot判断应该使用哪个JedisPool了。
以上这个方法适用于2.9.x版本之前的jedis
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>这种方式还是需要些过多的代码,其实Jedis的源码中已经有这种代码了的,只不过没有开放出来,在2.10.X版本之后就已经提供了一个方法来获取。我们先介绍低版本的方法。
节点代码
//节点信息
public class RedisNode {
private String ip;
private String port;
//slot信息
private List<Slot> slots;
private JedisPool jedisPool;
public RedisNode(){
slots = new ArrayList<>();
}
//Slot对象,保存最大值和最小值
static class Slot{
private int min;
private int max;
public Slot(int min,int max){
this.min = min;
this.max = max;
}
}
/**
* 判断slot是否在这个节点
* @param slot
* @return
*/
public boolean containsSlot(int slot){
for(Slot s : slots){
if( s.min <= slot && slot <= s.max){
return true;
}
}
return false;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getPort() {
return port;
}
public void setPort(String port) {
this.port = port;
}
public JedisPool getJedisPool() {
return jedisPool;
}
public void setJedisPool(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
public void setSlots(int min,int max){
slots.add(new Slot(min,max));
}
}单节点连接池
public class RedisNodePool {
public static final String MASTER = "master";
/**
* 用于存放当前Redis集群中的Master节点
*/
private List<RedisNode> list;
public RedisNodePool(){
list = new CopyOnWriteArrayList<>();
}
public void add(RedisNode redisNode){
list.add(redisNode);
}
/**
* 初始化连接池
* 如果连接发生异常了,可以重新调用这个方法初始化,这个时候会重新从JedisCluster中获取连接
* @param cluster
*/
public void init(JedisCluster cluster){
if(!list.isEmpty()){
list.clear();
}
//获取JedisCluster中的节点缓存信息
Map<String, JedisPool> nodesMap = cluster.getClusterNodes();
for(Map.Entry entry : nodesMap.entrySet()){
JedisPool jedisPool = nodesMap.get(entry.getKey());
Jedis jedis = jedisPool.getResource();
String nodeInfo = jedis.clusterNodes();
//多个节点换行
String [] nodes = nodeInfo.split("\n");
for(String node : nodes){
String [] array = node.split(" ");
RedisNode redisNode = new RedisNode();
//<9的节点没有slot
if(!array[2].contains(MASTER) || array.length < 9){
continue;
}
String [] ipAndPort = array[1].split("\\@")[0].split("\\:");
redisNode.setIp(ipAndPort[0]);
redisNode.setPort(ipAndPort[1]);
for (int i = 8; i < array.length; i++) {
String[] slot = array[i].split("-");
if(slot.length == 1){
//有可能有的节点存在单个不连续的slot
redisNode.setSlots(Integer.valueOf(slot[0]),Integer.valueOf(slot[0]));
}else {
redisNode.setSlots(Integer.valueOf(slot[0]),Integer.valueOf(slot[1]));
}
}
//设置连接池
redisNode.setJedisPool(jedisPool);
list.add(redisNode);
}
}
}
/**
* 根据Key获取一个Jedis
* @param key
* @return
*/
public Jedis getJedis(String key){
//计算key的slot
int slot = JedisClusterCRC16.getSlot(key);
for(RedisNode redisNode : list){
//判断key的slot在哪一个master节点
if(redisNode.containsSlot(slot)){
return redisNode.getJedisPool().getResource();
}
}
throw new IllegalArgumentException("redis node not found,key="+key);
}
/**
* 关闭Jedis的连接
* @param jedis
*/
public void close(Jedis jedis){
if(jedis!=null){
jedis.close();
}else{
throw new NullPointerException("jedis is null");
}
}
}调用方法
@Test
public void test2(){
redisNodePool.getJedis("key").set("key","hello");
System.out.println(redisNodePool.getJedis("key").get("key"));
}下面我们来看一个2.10.x之后的版本的获取方式
我们先看看源码,这个是JedisCluster的父类的代码,通过持有连接的Handler去获取某一个slot对应的Jedis,原理也是我们之前说的。
public class BinaryJedisCluster implements BasicCommands, BinaryJedisClusterCommands, MultiKeyBinaryJedisClusterCommands, JedisClusterBinaryScriptingCommands, Closeable {
public static final int HASHSLOTS = 16384;
protected static final int DEFAULT_TIMEOUT = 2000;
protected static final int DEFAULT_MAX_ATTEMPTS = 5;
protected int maxAttempts;
protected JedisClusterConnectionHandler connectionHandler;
public Map<String, JedisPool> getClusterNodes() {
return this.connectionHandler.getNodes();
}
//获取某一个slot在缓存池中对应的JedisPool中的连接
public Jedis getConnectionFromSlot(int slot) {
return this.connectionHandler.getConnectionFromSlot(slot);
}
//其余代码忽略
}使用方法
public static void main(String[] args) {
JedisCluster jedisCluster;
Jedis jedis = jedisCluster.getConnectionFromSlot(JedisClusterCRC16.getSlot("key"));
}