开源 redis 客户端 redigo 源码学习

  1. redigo 介绍
    Go语言实现的开源 redis 客户端。

  2. 特性

  • 提供类似 print 函数风格(Print-like)的 API,支持所有的 redis 命令;
  • 支持流水线事务(pipelined transaction);
  • 支持发布/订阅机制;
  • 支持使用连接池,提高并发操作;
  • Lua 脚本辅助类型(script helper type),经过优化的 EVALSHA 功能;
  • 应答辅助函数(helper functions),通过 Bool,Int,Bytes,String,Strings 和 Values 函数将应答转化为特定类型值。
  1. 基本使用
  • redis.Dial() : 建立与 redis 服务器的连接,成功则返回一个 redis.Conn 对象表示这个连接,后续 redis 命令都通过该对方操作。退出时需要 Close()释放该连接。
  • Conn.Do() : 使用该方法可以发送 redis 命令到服务器,并同步返回命令执行结果。
  1. 源码分析
    redis.go|conn.go
  • 主要数据结构
// Conn represents a connection to a Redis server.
type Conn interface {
	// Close closes the connection.
	Close() error

	// Err returns a non-nil value when the connection is not usable.
	Err() error

	// Do sends a command to the server and returns the received reply.
	Do(commandName string, args ...interface{}) (reply interface{}, err error)

	// Send writes the command to the client's output buffer.
	Send(commandName string, args ...interface{}) error

	// Flush flushes the output buffer to the Redis server.
	Flush() error

	// Receive receives a single reply from the Redis server
	Receive() (reply interface{}, err error)
}

// Use the DoWithTimeout and ReceiveWithTimeout helper functions to simplify
// use of this interface.
type ConnWithTimeout interface {
	Conn

	// Do sends a command to the server and returns the received reply.
	// The timeout overrides the read timeout set when dialing the
	// connection.
	DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (reply interface{}, err error)

	// Receive receives a single reply from the Redis server. The timeout
	// overrides the read timeout set when dialing the connection.
	ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error)
}

// conn is the low-level implementation of Conn
type conn struct {
	// Shared
	mu      sync.Mutex
	pending int    //未收到应答的未决命令个数
	err     error
	conn    net.Conn

	// Read
	readTimeout time.Duration    //等待 redis 命令应答的超时时间
	br          *bufio.Reader

	// Write
	writeTimeout time.Duration    //redis 命令发送的超时时间
	bw           *bufio.Writer

	// Scratch space for formatting argument length.
	// '*' or '$', length, "\r\n"
	lenScratch [32]byte

	// Scratch space for formatting integers and floats.
	numScratch [40]byte
}

Conn 是一个接口类型,用于表示一个与 redis 服务器的连接,真正实现该接口的是 conn.go 中的私有结构体指针 *conn ,*conn 所实现的以上接口方法执行实际的操作。*conn 同时实现了 ConnWithTimeout 接口,用于简化 Conn 接口的使用。

// DialOption specifies an option for dialing a Redis server.
type DialOption struct {
	f func(*dialOptions)
}

type dialOptions struct {
	readTimeout  time.Duration
	writeTimeout time.Duration
	dialer       *net.Dialer
	dial         func(network, addr string) (net.Conn, error)
	db           int
	password     string
	clientName   string
	useTLS       bool
	skipVerify   bool
	tlsConfig    *tls.Config
}

DialOption 用于指定连接 redis服务器时的连接参数,其中包含了一个函数变量 f ,通过一系列诸如 DialReadTimeout, DialPassword 等 DialXXX 函数返回包含了特定用于 dialOptions 内部某个成员的设置函数的 DialOption 变量。例如 DialPassword() 用于设置 dialOptions.password 的值。
这里包含了一种好的设计思想

如果不希望将内部数据结构(这里是 dialOptions)暴露给外部直接访问,但同时又需要支持外部可修改该数据结构内部成员,一种做法是在包内定义一个全局变量,再定义相关的 Set 函数对该全局变量进行操作,但这种做法不适合并发操作场景。redigo 采用的方式是通过 DialXXX 系列函数返回的 DialOption 变量(内部包含匿名函数,对 *dialOptions 进行操作) 作为入参传入到 Dial()函数内,然后在 Dial()函数内定义一个 dialOptions 局部变量,通过调用 DialOption 的 f()函数对 dialOptions 局部变量进行修改。这样既隐藏了内部数据结构,又统一了 Dial()的入参风格,保证了代码的优雅和可读性。

  • 核心函数

func Dial(network, address string, options ...DialOption) (Conn, error)
以上函数负责建立与 redis 服务器的连接,如果设置了 redis 密码,则发送 AUTH 命令;如果设置了连接名,则发送了 CLIENT SETNAME 命令;如果指定了要连接的 redis 数据库,则发送 SELECT 命令,最后返回一个连接对象 Conn ,实际上具体类型为 *conn 。

func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error)
func (c *conn) DoWithTimeout(readTimeout time.Duration, cmd string, args ...interface{}) (interface{}, error)

Do()函数用于发送 redis 命令给服务器,同步等待命令应答。是 Send/Flush/Receive 三合一版本。该函数实际只是调用 DoWithTimeout() 而已。
DoWithTimeout()函数首先会检查发送的命令是否为空,若不为空,则先写命令到网络 IO 缓冲(见 writeCommand),然后立即调用 Flush 将请求命令发送出去,接着同步接收命令应答(见 readReply)(会将之前尚未收到应答的未决命令也一并处理掉,扔掉了这些命令的应答);若待发送的命令为空,表示本次只是单纯接收之前发出的所有未决命令的应答,不发送额外的命令出去。

func (c *conn) writeCommand(cmd string, args []interface{}) error
以上函数用于将 redis 命令写入网络 IO 缓冲区。会将 redis 命令按照 redis 私有通信协议打包。

func (c *conn) readLine() ([]byte, error)
func (c *conn) readReply() (interface{}, error)

readLine()从输入中读取一行,去除了行尾的\r\n.
readReply()利用 readLine()从连接中读取一行 redis 命令应答报文,按照 redis 通信协议解析报文,对不同类型的应答作不同的校验及数据处理,最后返回应答报文。

func (c *conn) Send(cmd string, args ...interface{}) error
以上函数只是调用 writeCommand 将 redis 命令放入 IO 缓冲区,并未立即发送出去。此时未决命令(未收到应答)个数加 1.

func (c *conn) Flush() error
以上函数将缓冲区未发送的命令立即发送出去。

func (c *conn) Receive() (interface{}, error) 
func (c *conn) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error)

Receive()是对 ReceiveWithTimeout 的封装。
ReceiveWithTimeout()负责从连接中读取一条 redis 命令应答。读取成功后同步将未决命令(未收到应答)个数减 1.

func (c *conn) Close() error
以上函数正常断开与 redis 服务器的连接。


pool.go|commandinfo.go

  • 主要数据结构
type Pool struct {
	// Dial is an application supplied function for creating and configuring a
	// connection.
	//
	// The connection returned from Dial must not be in a special state
	// (subscribed to pubsub channel, transaction started, ...).
	Dial func() (Conn, error)

	// DialContext is an application supplied function for creating and configuring a
	// connection with the given context.
	//
	// The connection returned from Dial must not be in a special state
	// (subscribed to pubsub channel, transaction started, ...).
	DialContext func(ctx context.Context) (Conn, error)

	// TestOnBorrow is an optional application supplied function for checking
	// the health of an idle connection before the connection is used again by
	// the application. Argument t is the time that the connection was returned
	// to the pool. If the function returns an error, then the connection is
	// closed.
	TestOnBorrow func(c Conn, t time.Time) error

	// Maximum number of idle connections in the pool.
	MaxIdle int

	// Maximum number of connections allocated by the pool at a given time.
	// When zero, there is no limit on the number of connections in the pool.
	MaxActive int

	// Close connections after remaining idle for this duration. If the value
	// is zero, then idle connections are not closed. Applications should set
	// the timeout to a value less than the server's timeout.
	IdleTimeout time.Duration

	// If Wait is true and the pool is at the MaxActive limit, then Get() waits
	// for a connection to be returned to the pool before returning.
	Wait bool

	// Close connections older than this duration. If the value is zero, then
	// the pool does not close connections based on age.
	MaxConnLifetime time.Duration  //每个连接的最大寿命

	chInitialized uint32 // set to 1 when field ch is initialized

	mu           sync.Mutex    // mu protects the following fields
	closed       bool          // set to true when the pool is closed.
	active       int           // the number of open connections in the pool  当前总连接数
	ch           chan struct{} // limits open connections when p.Wait is true  
	idle         idleList      // idle connections
	waitCount    int64         // total number of connections waited for.
	waitDuration time.Duration // total time waited for new connections.
}

// PoolStats contains pool statistics.
type PoolStats struct {
	// ActiveCount is the number of connections in the pool. The count includes
	// idle connections and connections in use.
	ActiveCount int
	// IdleCount is the number of idle connections in the pool.
	IdleCount int

	// WaitCount is the total number of connections waited for.
	// This value is currently not guaranteed to be 100% accurate.
	WaitCount int64

	// WaitDuration is the total time blocked waiting for a new connection.
	// This value is currently not guaranteed to be 100% accurate.
	WaitDuration time.Duration
}

// 双向链表
type idleList struct {
	count       int  //空闲连接个数
	front, back *poolConn
}

type poolConn struct {
	c          Conn
	t          time.Time  //进入空闲列表的时间,用于判断是否达到空闲自动断开条件
	created    time.Time  //连接创建时间
	next, prev *poolConn
}

type activeConn struct {
	p     *Pool
	pc    *poolConn
	state int  //用于标识当前发送的命令是否为事务模式或发布订阅相关命令。
}

Pool 结构体维护了多个与 redis 服务器的连接,即连接池。由应用自行指定连接建立函数、最大连接数、最大空闲连接数、空闲连接自动断开等待时间、获取不到可用连接时的行为(等待或报错)等。而 PoolStats 只是将 Pool 内部记录的连接池状态信息暴露出去的一个媒介。新建连接池时,并不会立即创建与 redis 服务器的连接,而是在调用 Get() 从连接池获取连接时才创建连接。连接使用完后需要用户主动调用 Close() 将连接归还到连接池,同时该连接会进入空闲列表,下次获取连接时会优先从空闲列表上获取。空闲列表是一个双向链表,表头存放最新连接,会根据设定的空闲连接自动断开等待时间参数(IdleTimeout)自动剔除表尾的旧空闲连接。如果设定了获取不到可用连接时的行为是等待(Wait=true),则会利用带缓冲的 channel 作为信号量,连接池每分配一个连接,信号量减 1,每回收一个连接,信号量加 1,信号量为 0 时,Get()方法阻塞直到有可用连接。
activeConn 表示一个已分配出去的活跃连接。它的指针类型实现了 Conn 接口,因此应用层调用 Do()发送命令都是由它提供的方法完成的。

  • 核心函数
func (p *Pool) Get() Conn 
func (p *Pool) get(ctx context.Context) (*poolConn, error) 

Get()从连接池获取一个连接,实际执行的是 get()函数。get()主要逻辑:1、如果 Wait 为 true 并且最大连接数大于 0,则初始化信号量的值为最大连接数,保证同时可分配的连接数不超过最大连接数,若全部分配完仍有新的连接分配请求,则阻塞在信号量的获取上(读 channel);2、如果设置了 IdleTimeout ,则自动将空闲列表尾部超时的空闲连接释放掉;3、优先从空闲列表的头部分配连接,并且检查该连接是否仍可用(TestOnBorrow),如可用,则直接返回该连接;4、创建全新连接。

func (p *Pool) Close()
以上方法释放连接池资源。

func (p *Pool) put(pc *poolConn, forceClose bool) error
func (ac *activeConn) Close() error

Close()方法用于将连接归还给连接池,如果该连接上启动了事务,则要先取消事务,如果该连接上订阅了消息,则也需要取消订阅。最后调用连接池的 put()方法将连接加入空闲列表。

func (ac *activeConn) Do(commandName string, args ...interface{}) (reply interface{}, err error)
以上方法会先检查待发送命令是否为事务相关命令或发布/订阅相关命令,如果是,会打上相应的状态标志(见 lookupCommandInfo()),再调用底层 *conn 的 Do() 方法。


pubsub.go|scan.go|reply.go

  • 主要数据结构
// Subscription represents a subscribe or unsubscribe notification.
type Subscription struct {
	// Kind is "subscribe", "unsubscribe", "psubscribe" or "punsubscribe"
	Kind string

	// The channel that was changed.
	Channel string

	// The current number of subscriptions for connection.
	Count int
}

// Message represents a message notification.
type Message struct {
	// The originating channel.
	Channel string

	// The matched pattern, if any
	Pattern string

	// The message data.
	Data []byte
}

// Pong represents a pubsub pong notification.
type Pong struct {
	Data string
}

// PubSubConn wraps a Conn with convenience methods for subscribers.
type PubSubConn struct {
	Conn Conn
}

PubSubConn 封装了 Conn 结构体,并且提供了一些专用于订阅相关的函数,如 Subscribe/Unsubscribe/Receive 等,注意并未提供发布函数。发布和订阅不能使用同一连接,在一个连接上发送了 redis 订阅命令后,该连接就处于阻塞状态,只能接收消息或者发送取消订阅命令。因此,通常应该使用连接池来实现同一客户端的发布订阅机制。消息发布 publish 命令和其他常规 redis 命令一样发送即可。订阅某个频道(channel)的消息后,该客户端使用 Receive 函数持续地接收该频道上收到的消息,会收到几种类型的消息:subscribe/unsubscribe/message/pong ,redigo 将其封装为 Subscription/Message/Pong 结构体与之对应。

  • 核心函数

func (c PubSubConn) Subscribe(channel ...interface{}) error
以上函数向 redis 服务器发送一个 SUBSCRIBE 命令,订阅指定频道

func (c PubSubConn) PSubscribe(channel ...interface{}) error
以上函数向 redis 服务器发送一个 PSUBSCRIBE 命令,按特定模式订阅匹配该模式的频道

func (c PubSubConn) Receive() interface{}
func (c PubSubConn) receiveInternal(replyArg interface{}, errArg error) interface{}

Receive()调用底层 Conn 的 Receive()接收原始的返回报文,再将其传给 receiveInternal(),对订阅模式的返回报文进行特殊解析。
receiveInternal()按照 redis 私有通信协议解析返回报文,先使用 Scan()得到消息类型,再根据消息类型使用 Scan() 解析消息后面部分到 Message 或 Subscription 结构体

  1. 参考资料
    通信协议(protocol)
    发布与订阅(pub/sub)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值