流水线

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
pipe := rdb.Pipeline()

incr := pipe.Incr("pipeline_counter")
pipe.Expire("pipeline_counter", time.Hour)

// Execute
//
//     INCR pipeline_counter
//     EXPIRE pipeline_counts 3600
//
// using one rdb-server roundtrip.
_, err := pipe.Exec()
fmt.Println(incr.Val(), err)

上面的代码也可以写成下面的形式:

1
2
3
4
5
6
7
var incr *redis.IntCmd
_, err := rdb.Pipelined(func(pipe redis.Pipeliner) error {
	incr = pipe.Incr("pipelined_counter")
	pipe.Expire("pipelined_counter", time.Hour)
	return nil
})
fmt.Println(incr.Val(), err)

事务

TxPipeline

TxPipeline的行为类似于Pipeline,但是用MULTI / EXEC包装了排队的命令。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 开启一个TxPipeline事务
pipe := rdb.TxPipeline()
// 执行事务操作,可以通过pipe读写redis
incr := pipe.Incr("tx_pipeline_counter")
pipe.Expire("tx_pipeline_counter", time.Hour)

// 上面代码等同于执行下面redis命令
//
//     MULTI
//     INCR pipeline_counter
//     EXPIRE pipeline_counts 3600
//     EXEC

// 通过Exec函数提交redis事务
_, err := pipe.Exec()

// 提交事务后,我们可以查询事务操作的结果
// 前面执行Incr函数,在没有执行exec函数之前,实际上还没开始运行。
fmt.Println(incr.Val(), err)

Watch

redis乐观锁支持,可以通过watch监听一些Key, 如果这些key的值没有被其他人改变的话,才可以提交事务。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 定义一个回调函数,用于处理事务逻辑
fn := func(tx *redis.Tx) error {
		// 先查询下当前watch监听的key的值
		v, err := tx.Get("key").Result()
		if err != nil && err != redis.Nil {
			return err
		}

		// 这里可以处理业务
		fmt.Println(v)

		// 如果key的值没有改变的话,Pipelined函数才会调用成功
		_, err = tx.Pipelined(func(pipe redis.Pipeliner) error {
			// 在这里给key设置最新值
			pipe.Set("key", "new value", 0)
			return nil
		})
		return err
	}

// 使用Watch监听一些Key, 同时绑定一个回调函数fn, 监听Key后的逻辑写在fn这个回调函数里面
// 如果想监听多个key,可以这么写:client.Watch(fn, "key1", "key2", "key3")
client.Watch(fn, "key")

SCAN

可以使用Iterator方法:

1
2
3
4
5
6
7
iter := rdb.Scan(ctx, 0, "", 0).Iterator()
for iter.Next(ctx) {
	fmt.Println(iter.Val())
}
if err := iter.Err(); err != nil {
	panic(err)
}

Do命令不能与普通命令混用

异常导致tcp断连的代码

1
2
3
4
5
6
7
8
func SpeedLimit(k string, times int, t time.Duration) (e error) {
    r := redisdb
    key := "speed_limit_" + k
    r.Do("MULTI")
    r.Incr(key)
    e = r.Do("EXEC").Err()
    return
}

wireshark抓包

redis tcp建立连接三次握手

redis密码鉴权

req

rsp

可以看到三次握手之后是没有做TLS/SSL 握手的。Redis为了高效,仅提供了最基础的密码验证。当你想把redis-server暴露在公网上面时,仅依靠这个密码验证是远远不够的,就算你把密码设得再长再复杂,加大了暴破的难度,但由于通讯过程不加密,你的“正版”客户端在连接时,还是会有密码泄露的风险。

MULTI

至于redis的通信协议,可以参考 redis协议

req

rsp

INCR

req

rsp

从回包可以看到,redis-server对于incr的命令返回了正确的响应,表示已经入队了

redis客户端主动断开连接

这个时候突然出现了,客户端主动向redis-server发起的FIN信令(redis-server的端口是默认6379),既然已经收到了INCR正确的rsq,为甚这个地方要断连呢。

通过断点调试发现,当收到redis-server的正确回包时,走到下图红框的分支,删除了当前连接

原因为在读返回值时,未找到对应的正确的返回值,可以看到这里只支持,’-‘和’:‘的消息前缀格式

但是收到的入队的reply是以’+‘开头的

分析代码发现,Incr这个函数,new一个IntCmd类型的实例,此类型实现的readReply接口如上,只支持’-‘和’:‘的消息前缀格式

将r.Incr(key) 替换为 r.Do(“incr”, key) ,不会由于读replay err导致断连,代码中Do()返回通用的CMD,支持所有的Redis回包格式。

分析后学习到使用go-redis的接口时,尽量二选一,全部使用Do自己传入redis的命令,或者全部使用redis的封装好的命令接口,两种方式混用可能会导致类似上述的异常。

参考:https://www.yuque.com/alipaydg7i09aywu/nkx0fe/ac66mw