最近一位朋友问到:既然Redis是单线程的工作模式,那像BLPOP这样的阻塞操作又是怎样实现的呢?
接下来分别从服务端和客户端来阐述这一逻辑的实现原理。
Redis Server:
redis实现了一套事件触发模型,主要处理两种事件:I/O事件(文件事件)和定时事件。而处理它们的就靠一个EventLoop线程。同时redis还提供了丰富的数据结构,今天我们要分析的主要是List数据结构中的阻塞命令。
先来看看BLPOP的源码(做了精简,只看主要的部分,详细的可以看文尾提供的参考链接):
t_list.c
t_list.c_1.png
t_list.c_2.png
上面代码表明:如果客户端发来一个blpop key命令,redis先找到对应的key的list,如果list不为空则pop一个数据返回给客户端;如果对应的list不存在或者里面没有数据,就将该key添加到一个blockling_keys的字典中,value就是想订阅该key的client链表。此时对应的client的为block状态,且i/o channel里面没有写入数据。
既然是list数据结构,当然有push数据的操作:
同样是t_list.c
t_list.c_3.png
t_list.c_4.png
上面代码表明:如果客户端发来一个push key value命令,先从blocking_keys中查找是否存在对应的key,如果存在就往ready_keys这个链表中添加该key;同时将value插入到对应的list中,并响应客户端。
从上面的分析来看,主要是blocking_keys和ready_keys的作用,那何时才会处理它们呢?我们知道redis全靠EventLoop来处理所有的I/O事件,我们来看看所有命令的处理入口:
redis.c
redis.c_1.png
t_list.c_5.png
t_list.c_6.png
上面代码表明:每次处理完客户端命令后都会遍历ready_keys,并通过blocking_keys找到对应的client,依次将对应list的数据pop出来并响应对应的client;同时检查是否需要再次block。
这样一来整个流程就清晰了。redis就是通过blocking_keys和ready_keys两个数据结构来实现的阻塞操作。但整个阻塞并没有阻塞EventLoop本身,从而实现命令的快速响应。算是一个典型的空间换时间的设计思路。
接下来再看看客户端如何实现一个阻塞的I/O请求。
Client:
这里我们分两种I/O模型来阐述:阻塞I/O(BIO)和非阻塞I/O(NIO)。
BIO,以Jedis为例。
BinaryJedis.java
BinaryJedis.java.png
可以看出一个blpop请求会向redis发起两次I/O请求,一次向redis发送BLPOP key命令,一次从对应的链接管道(channel)中读取数据。由于BIO的特性当channel中没有数据时会一直阻塞,直到有新数据为止。这样就实现了客户端的阻塞效果。
注意:这里的链接是被独享的,不然会有数据干扰。
NIO的实现就稍微复杂一些,这里分两种情况(以netty为例):
不带RquestID的实现方式
伪代码如下:
不带RquestID.png
由于NIO的特性read和write是两个I/O事件,要分别等待selector来触发,所以不能像BIO那样连续发起两次I/O操作。再加上没有requesID,当read到数据时无法找到之前对应的发起者,所以这里的链接也必须是独享的,同时由一个只能包含一个元素的阻塞队列LinkedBlockingQueue来实现阻塞的效果。
带RequestID的实现方式
伪代码如下:
带RequestID.png
这里因为reqeust和response数据结构里都有带上了requestId,并且在链接对象上缓存了requestId和响应future的对应关系,因此链接可以不用独享。
到处,整个阻塞的实现原理分析完毕。
https://www.jianshu.com/p/xsMzfn
参考链接:
带注解的redis源码:
https://github.com/huangz1990/annotated_redis_source/blob/unstable/src/t_list.c https://github.com/huangz1990/annotated_redis_source/blob/unstable/src/redis.c
IO - 同步,异步,阻塞,非阻塞 (亡羊补牢篇)
http://blog.csdn.net/historyasamirror/article/details/5778378