Redispy 源码学习(一) --- 概览

Redis是一个高性能的Nosql内存数据库。代码精简,性能和扩展性强,被广泛用于互联网应用之中。许多语言也都支持redis,并实现了其客户端驱动。Python的redis驱动写得非常好(以下简称redis.py),通过阅读redis.py可以学习redis的通信协议,网络客户端的编程以及连接池管理等技术,我们也将通过对这三部分的逐一解析来学习redis.py。

目录:

  1. Redispy 源码学习(一) --- 概览
  2. Redispy 源码学习(二) --- RESP协议简介
  3. Redispy 源码学习(三) --- RESP协议实现--编码
  4. Redispy 源码学习(四) --- 创建连接
  5. Redispy 源码学习(五) --- RESP协议实现--解码
  6. Redispy 源码学习(六) --- 连接池
  7. Redispy 源码学习(七) --- 客户端接口
  8. Redispy 源码学习(八) --- 多线程和阻塞连接池

Redis 通信协议

redis设计了一个非常简单高效的通信协议RESP---REdis Serialization Protocol,该协议基于TCP的应用层协议。在编码RESP协议的时候,我们需要学习字符的编码与解码。由于TCP是流(stream)模式,并没有边界,因此关于抽象出来的的边界,将会是RESP协议的重点处理方式。同时也会发现RESP设计比较巧妙。

网络通信

redis.py是redis的python客户端驱动,因此我们只需要实现客户端的逻辑,服务端当然就是redis服务器本身。简而言之,就是我们需要使用python实现一个redis-cli。虽然客户端的socket编码不比服务端的复杂,可是要是处理不当,同样也会带来诸多问题。构建一个健壮的客户端是写好服务端的基础。

学习了RESP的编码与解码之后,我们就需要借助socket把网络数据发送给redis服务器,同时介绍服务器的应答,完成客户端对数据库的操作。

连接池

redis.py是一个redis的客户端,主要任务是发送命令到redis服务器。对于客户端而言,与服务器的通信基于tcp的socket,客户端的生命周期,自然而然就需要创建连接,发送数据,关闭连接等基本操作。

连接的频繁创建与销毁也会消耗资源,引入连接池管理连接将会是一种比较好的解决方式。redis.py的连接池写得很不错,我们也会从中受益良多。

redis.py的软件架构

一个大型的系统需要一个良好的设计和架构。小的软件或者脚本也离不开好的设计结构。redis.py作为python的客户端,封装了很多redis命令的接口。因此在python中使用redis将非常方便和优雅。

分布式

redis提供分布式功能,我们也会针对其分布式实现和使用解释其原理。

阅读方式

在阅读redis.py源码的时候,尝试自己实现一个驱动将会对学习理解提供莫大帮助,同时也能带来成就感。因此我们将使用Python3的编码环境,以单文件为基础,实现一个简易的redis-like.py。

redis.py 的架构概览

软件结构

下面我们就先对redis.py做一个简单地概览。redis.py已经2.10版本。其文件结构如下:

☁  redis  tree
.
├── __init__.py
├── _compat.py
├── client.py
├── connection.py
├── exceptions.py
├── lock.py
├── sentinel.py
└── utils.py
  • _compat.py 用于处理python2和python3不兼容的函数,封装并提供统一的接口。
  • client.py 该文件提供接口给python代码使用。
  • connection.py 该文件非常重要,实现了对redis服务器的连接创建销毁和socket收发过程。
  • exceptions.py 自定义异常
  • lock.py sentinel.py 用于分布式相关的操作
  • utils.py 工具函数库

redis.py的作者把软件设计很清晰。不过我们可以先忽略这些结构,基于一个文件实现上面的功能。把核心功能实现之后,再拆分和组织代码结构。

创建客户端,初始化连接池

下面一段简单的使用代码:

import redis

rc = redis.StrictRedis(host='127.0.0.1', port=6379, db=0)
print(rc.ping())
print(rc.get('hello'))

StrictRedis创建一个客户端 rc(redis_cli),其内部创建一个连接池,当调用ping方法的时候,rc才会创建连接。再次调用get方法的时候,rc会从连接池中读取连接,执行命令。当连接池没有可用连接,rc又会自动创建连接。总之,redis在执行命令的时候,一旦连接坏了,就会清理释放连接,然后重建新连接,并重新执行命令。

与服务器通信入口

无论ping还是get方法,调用的都是StrictRedisexecute_command方法。

def execute_command(self, *args, **options):
       
        pool = self.connection_pool
        command_name = args[0]
        connection = pool.get_connection(command_name, **options)
        try:
            connection.send_command(*args)
            return self.parse_response(connection, command_name, **options)
        except (ConnectionError, TimeoutError) as e:
            connection.disconnect()
            if not connection.retry_on_timeout and isinstance(e, TimeoutError):
                raise
            connection.send_command(*args)
            return self.parse_response(connection, command_name, **options)
        finally:
            pool.release(connection)

execute_command方法中,先从连接池中get一条连接,然后调用send_command发送命令给redis服务器,接着调用parse_response方法读取redis的返回结果。

如果执行发送命令或者读取结果的时候发生异常,将会主动disconnect,即释放客户端的连接资源(如果连接已经断开,就清理对象)。然后再重新发送。等到通信完毕之后,再把连接释放回连接池。

之所以要清理连接对象,是因为在python代码上下文中,逻辑连接还是正常,只不过实际上的tcp连接已经close了。此时要同步逻辑连接和实际的连接。

发送命令

send_command是连接对象的方法,执行该方法之前将会把命令参数按照RESP协议编码。并调用send_packed_comand方法,后者会检查连接是是否存在,如果不存在,将会创建连接。这一步就是rc的惰性创建连接入口。

def send_packed_command(self, command):
        if not self._sock:
            self.connect()
        try:
            if isinstance(command, str):
                command = [command]
            for item in command:
                self._sock.sendall(item)
        except socket.timeout:
            self.disconnect()
            raise TimeoutError("Timeout writing to socket")
        except socket.error:
            e = sys.exc_info()[1]
            self.disconnect()
            if len(e.args) == 1:
                errno, errmsg = 'UNKNOWN', e.args[0]
            else:
                errno = e.args[0]
                errmsg = e.args[1]
            raise ConnectionError("Error %s while writing to socket. %s." % (errno, errmsg))
        except:
            self.disconnect()
            raise

该方法会调用self._sock.sendall(item)将redis命令发送到服务器。

连接与连接池

调用send_packed_command之前就从连接池中读取连接。

def get_connection(self, command_name, *keys, **options):
        "Get a connection from the pool"
        self._checkpid()
        try:
            connection = self._available_connections.pop()
        except IndexError:
            connection = self.make_connection()
        self._in_use_connections.add(connection)
        return connection

可以该方法会从可用的连接池对象中pop一个连接,如果连接不存在,那么就调用make_connection创建连接并返回。然后才能使用send_packed_command发送数据。

读取响应

发送的过程并不复杂,接收的过程则比较讲究。后面的我们会详细分析,在此只需要有个大概认识就行了。

parse_response方法会调用连接对象的read_response方法,后者会调用self._parser.read_response()。这个 _parser对象为了兼容Hiredis而做的一个适配器。主要功能就是封装hiredis,提供统一的处理连接管理和数据缓冲的接口。默认使用PythonParse类。

_parser对象有一个_buffer属性,后者是一个SocketBuffer类,主要封装了对socket的接收功能,即从socket的缓冲区读取数据,通过BytesIO写入到内存,然后从内存中读取数据。通过计算对比内存中的数据和读取的数据,控制从socket中读取的数据。这个精妙的设计我们后面会详细介绍。

再看_parse对象read_response方法:

def read_response(self):
        response = self._buffer.readline()
        if not response:
            raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)

        byte, response = byte_to_chr(response[0]), response[1:]

        if byte not in ('-', '+', ':', '$', '*'):
            raise InvalidResponse("Protocol Error: %s, %s" %
                                  (str(byte), str(response)))

        if byte == '-':
            response = nativestr(response)
            error = self.parse_error(response)
            if isinstance(error, ConnectionError):
                raise error
                return error
        # single value
        elif byte == '+':
            pass
        # int value
        elif byte == ':':
            response = long(response)
        # bulk response
        elif byte == '$':
            length = int(response)
            if length == -1:
                return None
            response = self._buffer.read(length)
        # multi-bulk response
        elif byte == '*':
            length = int(response)
            if length == -1:
                return None
            response = [self._buffer.read(length)() for i in xrange(length)]
        if isinstance(response, bytes) and self.encoding:
            response = response.decode(self.encoding)
        return response

调用self._buffer.readline()方法从socket读取数据。然后根据RESP协议处理redis的回复类型,接着逐一解析返回的数据,并返回。注意,遇到RESP中的批量回复(bulk response)和多批量回复(multi-bulk response),还需要调用self._buffer.read()和递归调用self._buffer.read(length)解析。

根据self._buffer.readline()只能读取返回一行的数据,因为redis是用\r\n区分数据,因此调用readline的时候,在批量回复和多批量回复的情况下,只能读取最前面的参数,后面的socket数据还在socket的缓冲区,所以需要继续调用read方法读取解析。后面的分析我们将会了解,多批量回复可以分解为多个批量回复,因此就与了迭代response的递归调用response = [self.read_response() for i in xrange(length)]。这也是经典的tcp流无边界问题的处理方式。

断开连接

读取响应之后,交互就完成了使命。redis维护的是一个连接,也就是根据redis的timeout参数来决定连接的空闲时间。默认配置是0,即如果redis不主动close这个连接,连接将会一直存在。所以客户端不会主动disconnect连接,而是释放其回到连接池pool.release(connection)。前面我们已经提到,只要交互过程中发生了异常,客户端才会主动调用disconnect方法释放与连接相关的资源和对象。

如果设置了redis连接的最大空闲时间: CONFIG SET TIMEOUT 30。那么每个redis的连接在30s之后,服务器都会主动close。此时的客户端还认为连接是正常的,执行收发数据的时候将会抛异常。这时就需要同步客户端和服务器的连接状态。

上面的过程可以用下面的流程图简要的说明:


                                    +-----------------------+
      +------------+                |         pool          |
      |            |                |-----------------------|
      |  client    |    1           |                       |
      |            ++-------------> |    connection_pool    |
      +-----+------+                |                       |
            |           2           |                       |
            +---------------------> |  execute_command      |
                                    +----------+------------+
                                               |
                                               |3
                                               |
                                               |
                                               v
                                    +-----------------------+          +---------------------+              +--------------------+
                                    |       pool            |          |       pool          |              |       connection   |
                                    |-----------------------|   4      |---------------------|              |--------------------|
                                    |     get_conncetion    ++-------->|                     |      6       |                    |
                                    |                       |          |     pop connection  | <-----------+|    init a conn     |
                                    |     send_command      +-----+    |                     |     5        |                    |
                      16            |                       |     |    |     make_connection |+------------>|    conect          |
                +-------------------+     parse_response    |     |    |                     |              |                    |
                |                   |                       |     |    +---------------------+              +------------+-------+
                |                   |     release           |     |                                          ^           |
                |                   +-----------------------+     +7                                         |           |
                |                                                 |                                          |           |10
                |                                                 |                                          |           |
                |                                                 |                                          |           |
    +--------------------------+                                  v                                          |           v
    |     connection           |    +-------------------+        +---------------------------------+         |   +-------------------+        +---------------+
    |--------------------------|    |                   |        |           connection            |         |   |      connection   |        |    connection |
    |                          |    |                   |        |---------------------------------|         |   |-------------------|        |---------------|
    |     _parse.read_response |    |                   |  8     |                                 |         |   |                   |        |               |
    |                          |    |    pack command   |<------+|        pack_command             |         |   |                   |   11   |               |
    +-----------+--------------+    |                   |------->|                                 |         |   |     create socket +--------|    on_connect |
                |                   +-------------------+        +----------------+----------------+         |   |                   |        |               |
                |                                                                 |                          |   |                   |        |               |
                |17                                                               v                          |   +--------+----------+        +------+--------+
                v                                                +---------------------------------+         |            |                          +
    +--------------------------+                                 |          connection             |         |            |                          |
    |       pythonparse        |                                 |---------------------------------|         |            |                          |
    |--------------------------|                                 |                                 |   9     |            |                          |12
    |                          |      18                         |        check sock connect       |+--------+            |                          |
    |      _buffer.readline    +------------+                    |                                 |<---------------------+                          |
    |                          |            |                    +        sock sendall             |                                                 v
    |                          |            |                    |                                 |<--------------------------------+        +----------------+
    |      handle response     |            |                    +-----------------+---------------+                                 |        | PythonParse    |
    |                          |            |                                      |                                                 |        |----------------|
    |                          |            |                                      |15                                               |        |                |
    +----------+---------------+            |                                      |                                                 |        |   on_connect   |
               |                            |                                      v                                                 |        |                |
               |                            |                    +-----------------+----------------+                                |        |                |
               |                            |                    |                                  |                                |        +-------+--------+
               |                            |                    |           end                    |                                |                |
               |19                          |                    |                                  |                                |14              |
               |                            |                    +----------------------------------+                                |                |
               v                            v                                                                                        |                |13
    +---------------------------+       +----------------------------+                  +-------------------------------+            |                |
    |       pythonparse         |       |      ScoketBuffer          |                  |         SocketBuffer          |            |                v
    |---------------------------|       |----------------------------|                  |-------------------------------|            |       +------------------+
    |                           |       |                            |                  |                               |            |       |                  |
    |       _buffer.read        |       |    readline                |       21         |                               |            |       |------------------|
    |                           |  20   |                            |+---------------->|      read from socket         |            |       |                  |
    |                           |------>|    read                    |                  |                               |            |       |
    |                           |       |                            |                  |                               |            +-------+ init SocketBuffer|
    +---------------------------+       +----------------------------+                  +-------------------------------+                    |                  |
                                                                                                                                             +------------------+
  1. StrictRedis创建客户端对象,并初始化连接池
  2. 执行ping命令
  3. 调用execute_command 从使用 get_connection 从连接池读取连接,然后发送命令,接着解析返回的响应,最后释放连接。
  4. get_connection 调用,要不重连接池中pop一个连接,如果连接不存在,则调用make_connection 方法创建连接。
  5. 初始化连接对象。
  6. 返回连接对象
  7. 执行send_command 函数,打包编码resp协议的命令。
  8. 编码resp过程。
  9. 检查连接是否存在,如果存在则发送socket数据。如果不存在,则调用connect方法创建连接对象。
  10. 创建socket,用于网络通信。
  11. 连接创建之后,调用连接的on_connect方法。
  12. 调用 pythonparse的on connect方法。初始化socketbuffer对象,用于接受数据时候的socket通信。
  13. 初始化 socketbuffer对象。
  14. 逐步返回连接对象,直到可以sendall数据到服务器。
  15. 结束发送过程。
  16. 调用parse_response 方法,用于读取服务器返回的响应数据
  17. 逐步回溯调用pythonparse封装的方法读取一行数据。
  18. 通过socketbuffer读取一行数据
  19. 遇到批量回复或多批量回复,调用read读取除token之后的数据。
  20. 与19类似,递归处理多批量回复。
  21. 从socket读取数据。

总结

经过上面的简述,我们对redis.py的大致框架和功能有了初步的了解,接下来就是针对上面所提及的三个方面深入解析。

RESP协议的学习比较简单,连接池的设计也不会很难,比较核心的关键是网络通信相关的处理。收发数据是我们的核心重点,连接管理也是举足轻重。一个经典的问题就是客户端的代码逻辑上的连接还存在,可是实际的tcp连接已经close,此时的收发数据该如何处理和管理呢?这将成为我们接下来阅读redispy的关键。

接下来将会在分别介绍redispy源码的时,提供文中使用的客户端测试代码,于文末的gist提供。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容