Jedis源码分析

预备知识:

  1. 设计模式:命令模式、装饰模式
  2. Java Socket编程
  3. Apach Commons Pool
  4. CRC32散列算法(了解)
  5. Redis命令
  6. Redis协议规范(了解)
  7. Redis集群规范

市面上一些参考书籍:

  1. 《Redis In Action》:偏应用讲解,适合redis入门,可以在学习完官网的命令之后,从这本书中提供的一些案例场景进行一些实战。缺点是写的有点啰嗦,中文版的翻译水平一般
  2. 《Redis开发与运维》:作者是搜狐的同事,每一块功能都是redisserver的原理、jedis原理、jedis使用一起讲,融会贯通。
  3. 《Redis设计与实现》:讲redis底层数据结构和原理方面。每个命令怎么执行的,有哪些定义了哪些struct结构。感觉比较适合进一步研究redis源码的同学

Jedis源码分析

一、Jedis使用

1. Jedis的使用

Jedis jedis = null;
try {
	jedis = new Jedis("127.0.0.1", 6379);
	jedis.get("hello");
	} catch (Exception e) {
		logger.error(e.getMessage(),e);
	} finally {
	if (jedis != null) {
		jedis.close();
	}
}

使用流程如下:
在这里插入图片描述

2. Jedis源码分析
Jedis给调用方提供的是简单易读的JAVA API接口,调用方使用API就像使用redis cli一样简单。那么Jedis内部在向redisServer发送请求之前都做了哪些事情呢?

我们通过源码看看Jedis是怎么设计的:
Jedis一共分为四层:

  1. 协议层Protocol类
  2. 连接层Connetion
  3. 原生客户端
  4. Jedis客户端

具体大家可以参考这篇文章:
http://jimgreat.iteye.com/blog/1586671

这三层之间的调用关系如下图所示:
在这里插入图片描述

二、JedisPool

1. JedisPool使用

创建连接池

// common-pool连接池配置,这里使用默认配置,后面小节会介绍具体配置说明
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
// 初始化Jedis连接池
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);

从连接池获取对象使用

Jedis jedis = null;
	try {
		// 1. 从连接池获取jedis对象
		jedis = jedisPool.getResource();
		// 2. 执行操作
		jedis.get("hello");
	} catch (Exception e) {
		logger.error(e.getMessage(),e);
	} finally {
	if (jedis != null) {
		// 如果使用JedisPool,close操作不是关闭连接,代表归还连接池
		jedis.close();
	}
}

使用流程如下:
在这里插入图片描述

2. JedisPool源码分析

在这里插入图片描述
从类关系图中看到JedisPool是通过 Apache Commons Pool 中的GenericObjectPool这个对象池来实现的,掌握JedisPool的关键是掌握GenericObjectPool。
下面我们看一下JedisPool和JedisFactory两个类的源码

直接看项目源码

3. Jedis直连方式和连接池方式对比:
在这里插入图片描述

三、JedisCluster

先看一下如何使用JedisCluster:

Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();
jedisClusterNode.add(new HostAndPort("127.0.0.1", 6379));
JedisCluster jc = new JedisCluster(jedisClusterNode, DEFAULT_TIMEOUT, DEFAULT_TIMEOUT,
    DEFAULT_REDIRECTIONS, "cluster", DEFAULT_CONFIG);
jc.set("foo", "bar");
assertEquals("bar", jc.get("foo"));

从上面的代码中我们可以看到:
构造JediCluster时只传入了1个redis节点信息,而不是多个,为什么?JediCluster在构造的时候做了哪些事情?带着这个问题我们一起看下初始化关键源码

JedisClusterConnectionHandler类

private void initializeSlotsCache(Set<HostAndPort> startNodes, GenericObjectPoolConfig poolConfig,
                                  int connectionTimeout, int soTimeout, String password, String clientName) {
    //循环取节点初始化,成功即退出
    for (HostAndPort hostAndPort : startNodes) {
        Jedis jedis = null;
        try {
            jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort(), connectionTimeout, soTimeout);
            if (password != null) {
                jedis.auth(password);
            }
            if (clientName != null) {
                jedis.clientSetname(clientName);
            }
            //使用jedis发现集群节点和插槽关系信息,初始化时只要没有抛出异常,就认为初始化成功,并退出
            cache.discoverClusterNodesAndSlots(jedis);
            break;
        } catch (JedisConnectionException e) {
            // try next nodes
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }
}

JedisClusterInfoCache 类

/**
 * 负责插槽到JedisPool、节点IP到JedisPool这2个映射关系的初始化、修改、查询
 * 并用读写锁保证线程安全
 */
public class JedisClusterInfoCache {
    //节点到连接池的映射,节点表示"IP:端口",nodes只在初始化时被清空,后面renewClusterSlots只会增加新的节点到这个map中
    private final Map<String, JedisPool> nodes = new HashMap<String, JedisPool>();
    //16454个插槽到连接池的映射关系,因为多个插槽会映射到同一个节点,所以多个插槽可能会对应同一个连接池
    private final Map<Integer, JedisPool> slots = new HashMap<Integer, JedisPool>();

    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    //使用rw锁对nodes和slots的初始化和reshard时进行保护,需要注意,rw锁会不会阻塞用户线程
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();

    //rediscovering变量
    private volatile boolean rediscovering;
    private final GenericObjectPoolConfig poolConfig;

    //连接池创建工厂需要的一些属性
    private int connectionTimeout;
    private int soTimeout;
    private String password;
    private String clientName;

    private static final int MASTER_NODE_INDEX = 2;

    public JedisClusterInfoCache(final GenericObjectPoolConfig poolConfig, int timeout) {
        this(poolConfig, timeout, timeout, null, null);
    }

    public JedisClusterInfoCache(final GenericObjectPoolConfig poolConfig,
                                 final int connectionTimeout, final int soTimeout, final String password, final String clientName) {
        this.poolConfig = poolConfig;
        this.connectionTimeout = connectionTimeout;
        this.soTimeout = soTimeout;
        this.password = password;
        this.clientName = clientName;
    }

    /**
     * 发现集群中插槽和节点的映射关系
     *
     * @param jedis
     */
    public void discoverClusterNodesAndSlots(Jedis jedis) {
        w.lock();

        try {
            reset();
            /**根据当前redis实例,获取集群中master,slave节点信息。包括每个master节点 上分配的数据嘈。
             * slots结果如下(例子):
             *[10923, 16383, [[B@924fda2, 9000], [[B@5b879b5e, 9001]]]
             *[[5461, 10922, [[B@3681fe9a, 7001], [[B@10724c6b, 8000]],
             *[0, 5460, [[B@3ff70d3c, 7000], [[B@7485fef2, 8001]],
             */
            List<Object> slots = jedis.clusterSlots();

            for (Object slotInfoObj : slots) {
                List<Object> slotInfo = (List<Object>) slotInfoObj;

                if (slotInfo.size() <= MASTER_NODE_INDEX) {
                    continue;
                }
                //一批插槽一批插槽的分配
                List<Integer> slotNums = getAssignedSlotArray(slotInfo);

                // hostInfos
                int size = slotInfo.size();
                for (int i = MASTER_NODE_INDEX; i < size; i++) {
                    List<Object> hostInfos = (List<Object>) slotInfo.get(i);
                    if (hostInfos.size() <= 0) {
                        continue;
                    }

                    HostAndPort targetNode = generateHostAndPort(hostInfos);
                    setupNodeIfNotExist(targetNode);
                    if (i == MASTER_NODE_INDEX) {
                        assignSlotsToNode(slotNums, targetNode);
                    }
                }
            }
        } finally {
            w.unlock();
        }
    }

从源码中我们可以看到JedisCluster在初始化时提前构造好了slot到JedisPool的映射关系,利用这些关系,在后面读写key的时候可以很方便的进行节点的路由。 但后面如果redis节点上下线怎么办呢?客户端缓存的映射关系不就是脏数据了吗?我们继续看源码(命令调用流程run方法)怎么解决的

JedisClusterCommand类

/**
 * 该方法采用递归方式,保证在往集群中存取数据时,发生MOVED,ASKing,数据迁移过程中遇到问题,也是一种实现高可用的方式。
 * 该方法中调用execute方法,该方法由子类具体实现
 */
private T runWithRetries(final int slot, int attempts, boolean tryRandomNode, boolean asking) {
    if (attempts <= 0) {
        throw new JedisClusterMaxAttemptsException("No more cluster attempts left.");
    }

    Jedis connection = null;
    try {
        //第一次执行该方法,asking为false。只有发生JedisAskDataException,才asking才设置为true
        if (asking) {//服务器返回ASK
            // TODO: Pipeline asking with the original command to make it
            // faster....
            connection = askConnection.get();
            connection.asking();

            // if asking success, reset asking flag
            asking = false;
        } else {
            if (tryRandomNode) {//从源码来看tryRandomNode貌似永远是false,这个分支先不用管
                connection = connectionHandler.getConnection();
            } else {
                /** 根据key获取分配的嘈数,然后根据数据槽从JedisClusterInfoCache 中获取Jedis的实例
                 **/
                connection = connectionHandler.getConnectionFromSlot(slot);
            }
        }

        return execute(connection);

    } catch (JedisNoReachableClusterNodeException jnrcne) {
        throw jnrcne;
    } catch (JedisConnectionException jce) {
        // release current connection before recursion
        releaseConnection(connection);
        connection = null;

        if (attempts <= 1) {
            //We need this because if node is not reachable anymore - we need to finally initiate slots renewing,
            //or we can stuck with cluster state without one node in opposite case.
            //But now if maxAttempts = 1 or 2 we will do it too often. For each time-outed request.
            //TODO make tracking of successful/unsuccessful operations for node - do renewing only
            //if there were no successful responses from this node last few seconds
            this.connectionHandler.renewSlotCache();//重建插槽和节点关系
        }

        return runWithRetries(slot, attempts - 1, tryRandomNode, asking);
    } catch (JedisRedirectionException jre) {
        // if MOVED redirection occurred,
        if (jre instanceof JedisMovedDataException) {
            // it rebuilds cluster's slot cache
            // recommended by Redis cluster specification
            //返回MOVED,插槽读写已经全部移到其他节点,需要先重构插槽到连接池的关系
            this.connectionHandler.renewSlotCache(connection);//重建插槽和节点关系
        }

        // release current connection before recursion or renewing
        releaseConnection(connection);
        connection = null;

        if (jre instanceof JedisAskDataException) {//返回ASK,集群正在进行插槽迁移,jedis客户端不重构集群状态信息,只是本次key读写,连接targetNode节点进行
            //
            asking = true;
            askConnection.set(this.connectionHandler.getConnectionFromNode(jre.getTargetNode()));
        } else if (jre instanceof JedisMovedDataException) {

        } else {//????
            throw new JedisClusterOperationException(jre);
        }

        return runWithRetries(slot, attempts - 1, false, asking);
    } finally {
        releaseConnection(connection);
    }
}

JedisClusterInfoCache重新发现集群插槽节点关系

    /**
     * 重新分配插槽到连接池的对应关系
     */
    public void renewClusterSlots(Jedis jedis) {
        //类似单例模式
        //If rediscovering is already in process - no need to start one more same rediscovering, just return
        if (!rediscovering) {
            try {
                w.lock();
                rediscovering = true;

                if (jedis != null) {
                    try {
                        discoverClusterSlots(jedis);
                        return;
                    } catch (JedisException e) {
                        //try nodes from all pools
                    }
                }
                //如果上面使用传进来的jedis发现插槽抛出异常,则使用下面的方式,随机一个节点去获取插槽
                for (JedisPool jp : getShuffledNodesPool()) {
                    Jedis j = null;
                    try {
                        j = jp.getResource();
                        discoverClusterSlots(j);
                        return;
                    } catch (JedisConnectionException e) {
                        // try next nodes,可能连接池满了
                    } finally {
                        if (j != null) {
                            j.close();
                        }
                    }
                }
            } finally {
                rediscovering = false;
                w.unlock();
            }
        }
    }
  1. 另外和Jedis和JedisPool的使用对比,发现执行完JedisCluster时没有close方法。这些操作JedisCluster内部就帮我们做了,它通过Command模式将这些固定流程进行了抽象和封装。我们看一下代码
 @Override
 public String set(final String key, final String value) {
     return new JedisClusterCommand<String>(connectionHandler, maxAttempts) {
         @Override
         public String execute(Jedis connection) {
             return connection.set(key, value);
         }
     }.run(key);
 }
  1. 最后我们对各个类的关系和调用顺序做一个总结
    在这里插入图片描述

在这里插入图片描述

四、常见问题

  1. 当出现网络拥塞,或者Redis服务器宕机情况下,业务系统会出现什么情况?
    回答:Jedis内部会抛出JedisConnectionException异常,具体可以看源码
  2. 使用Pipline批量执行redis命令时,redis命令是一次性全部发到服务端,还是一条一条发送?
    回答:Jedis的RedisOutputStream里有一个buf缓冲区。默认大小8KB,发送给服务器端的命令先缓存在buf里,超过8KB就会一次性发送到服务器端,减少系统IO调用,提高性能
  3. 集群模式下,初始化参数IP地址必须和线上IP地址完全一致吗?集群模式下,初始化参数IP地址必须和线上IP地址完全一致吗?集群模式下,初始化参数IP地址必须和线上IP地址完全一致吗?集群模式下,初始化参数IP地址必须和线上IP地址完全一致吗?
    回答:不是,初始化时,只要连上任意一台 redis,通过cluster命令就可以拿到插槽和ip地址的分配关系
  4. 集群模式下,如果增加、删除节点,能否被jedisCluster识别?
    回答:可以,JedisCluster在执行命令请求的时候,会根据RedisServer返回的结果renewClusterSlots
  5. 高并发场景下,会不会有多个线程同时renewClusterSlots,造成大量线程等待w锁,阻塞影响业务系统
    回答:不会,jedis使用了rediscovering这个变量,具体可以看源码
  6. JedisCluster为什么保存键和服务器节点之间的映射信息?
    答:虽然客户端可以自由地向集群中的任何一个节点发送命令请求, 并可以在有需要时, 根据转向错误所提供的信息( -MOVED 或者 -ASK), 将命令转发至正确的节点, 所以在理论上来说, 客户端是无须保存集群状态信息的。但是将键和节点之间的映射信息保存起来, 客户端可以有效地减少可能出现的转向次数, 籍此提升命令执行的效率。(见Redis集群规范Redis集群规范
  7. key散列算法是什么?
    回答:CRC16。key到slot:getCRC16(key) & (16384 - 1)。16384是2的14次方.

五、举一反三

  1. 路由能在哪一层做?
    1.1. 客户端路由
    1.2. 服务端路由
    1.3. proxy代理层路由
  2. 路由算法。其他路由散列算法原理和特性可以参考梅晨做的技术分享
  3. 如果一个对象里的数据有初始化,就要考虑是否有变更,如果变更的过程中还有读的操作,要立刻条件反射是否需要加读写锁。可以将这些行为封装在一个类中
  4. mysql执行流程 VS redis执行流程.
  5. Nagle算法

其他部分源码,有时间介绍一下:

  1. RedisException
  2. JedisDataException:已获得Redis服务器响应,但是结果错误。
    在这里插入图片描述
  3. RedisOutputStream
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值