-
redigo 介绍
Go语言实现的开源 redis 客户端。 -
特性
- 提供类似 print 函数风格(Print-like)的 API,支持所有的 redis 命令;
- 支持流水线事务(pipelined transaction);
- 支持发布/订阅机制;
- 支持使用连接池,提高并发操作;
- Lua 脚本辅助类型(script helper type),经过优化的 EVALSHA 功能;
- 应答辅助函数(helper functions),通过 Bool,Int,Bytes,String,Strings 和 Values 函数将应答转化为特定类型值。
- 基本使用
- redis.Dial() : 建立与 redis 服务器的连接,成功则返回一个 redis.Conn 对象表示这个连接,后续 redis 命令都通过该对方操作。退出时需要 Close()释放该连接。
- Conn.Do() : 使用该方法可以发送 redis 命令到服务器,并同步返回命令执行结果。
- 源码分析
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 服务器的连接。
- 主要数据结构
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() 方法。
- 主要数据结构
// 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 结构体