1 请求回应模式

redis 与 client 之间采用请求回应模式,一个请求包对应一个回应包。但是也有例外,pub/sub 模式下,client 发送 subscribe 命令并收到回应包后,之后被动接收 redis 的发布包。所以若需要使用 pub/sub 模式,那么需要在 client 下创建额外一条连接与 redis 交互。
在这里插入图片描述

2 Redis 协议图

在这里插入图片描述
redis 协议采用特殊字符( \r\n )来分割有意义的数据,redis 使用的是二进制安全字符串(用长度来标识字符串的长度),所以 redis 可以用来存储二进制数据(比如经过压缩算法加密的数据)。

  • 1)例如上图客户端。星号*代表后面紧接着的字段的参数数量,包括关键字本身,\r\n代表该字段结束。$符号代表下一个字段参数的字节数,\r\n代表该字段结束。读到和参数数量的字段后,剩余的就是数据了。
set key val
# 上面的命令用上图的redis协议可以这样表示:
"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\nval\r\n"
  • 2)服务端的同理,+号代表简单字符串的回复。-号代表错误的回复,:号代表整数回复,$号代表有数据回复,和上面## 标题讲的一样。*号同理,看上面即可。
    例如简单字符串回复、错误回复、整数回复的redis例子。
# 整数回复
192.168.1.9:6379> del tyy
(integer) 1
# 简单字符串回复
192.168.1.9:6379> set tyy hi
OK
# 错误回复
192.168.1.9:6379> ZADD tyy 1 one
(error) WRONGTYPE Operation against a key holding the wrong kind of value
192.168.1.9:6379> 

# 二进制字符串的返回可以通过get等命令进行测试。$-1代表返回的是nil值。

# 批量回复可以使用zrange key 0 -1 withscores这个命令进行测试。*0代表返回空数组。*-1代表超时(brpop可以测这个超时)。其它就是正常返回数据。

3 Redigo

golang 的 redis 驱动实现了 go 应用程序与 redis 服务器通信的协议实现,同时实现了与 redis
交互时的数据转换。所以我们可以使用Redigo这个库来进行与redis交互。

1)git 地址:https://github.com/gomodule/redigo.git
2)文档地址:https://pkg.go.dev/github.com/garyburd/redigo@v1.6.2/redis

3.1 命令执行

想要执行redis中的命令,我们可以统一使用Redigo里面的Do函数。

Do(commandName string, args ...interface{}) (reply interface{}, err error)

3.2 参数类型转换

在这里插入图片描述

  • 1)go的字节切片[]byte:因为go的字节切片是使用字节流的方式传输,所以在与redis进行通信时,无须转换。
  • 2)go的string字符串:go的字符串和redis无须转换。
  • 3)go的int、int64:如果go的类型是int、int64时,需要使用redigo的strconv.FormatInt函数进行转换。
  • 4)go的float64浮点数:如果go的类型是float64时,需要使用redigo的strconv.FormatFloat64函数进行转换。
  • 5)go的布尔类型:如果go的类型是bool时,需要转成对应的1和0。
  • 6)go的其它类型:看情况进行转换。具体可以看上面Redigo的文档。

3.3 返回值类型转换

在这里插入图片描述

返回值一般是针对于redis返回给go,所以我们主要分析redis返回的错误如何用go的类型结束即可。

  • 1)redis返回错误类型:redis的错误类型我们可以使用Redigo封装好的redis.Error这个东西获取即可。
  • 2)redis返回integer:go中使用int64接收。
  • 3)redis返回simple string简单字符串:go中使用string接收。
  • 4)redis返回bulk string批量字符串:go中使用字节切片接收。
  • 5)redis返回array数组:go中使用[]interface{}空切片接口这个万能类型去接收。

具体redis的返回的返回值,go该使用什么类型接收同样可以参考上面Redigo的文档。

4 重点

  • 1)redis是一种请求回应模式。
  • 2)redis的string是二进制安全字符串。
  • 3)协议传输过程中,是字节流,对应 go 类型为 []byte 。redis 协议需要将整数、浮点数都会转换为字符串。
  • 4)redis.Args 方便我们组织复杂的参数类型,能将 struct 和 map 通过redis.Args{}.AddFlat 进行展开。
  • 5)如果接收到 redis-server 批量回复(array):
    1. 类型相同的可以使用 redis.Ints redis.Float64s 等类型名后加上 s 来接收返回值。
    2. 类型固定且有关联性,比如接收 hash 结构的返回值,则可以使用 redis.StringMap 、 redis.IntMap 等。
    3. 类型差异大但是有规律,可以使用 redis.Values + redis.ScanStruct 来接收。
    4. 简单批量回复且类型不同,可以使用 redis.Valus + redis.Scan 来接收。
    5. 接收多个结构体,可以使用 redis.Values + redis.ScanSlice 来接收。

但是这些转换的内容无须死记,用到对应的转换时,具体看回下面的操作代码即可。

5 redigo基本命令操作

package main

import (
	"fmt"
	"reflect"

	"github.com/garyburd/redigo/redis"
)

func main() {
	// 1. 连接redis
	//conn, err := redis.Dial("tcp", fmt.Sprintf("%s:%d", "172.20.10.9", 6379))
	conn, err := redis.Dial("tcp", "192.168.1.9:6379", redis.DialPassword("123456"))
	if err != nil {
		panic(err)
	}
	defer (func() {
		fmt.Println("connection close")
		conn.Close()
	})()

	// 2. 执行命令并获取该key的值。
	if false {
		//conn.Do("set", "hello", 1)
		// 如果不加redis.Int,那么conn.DO返回的是切片类型[]type,因为redis和go使用tcp字节流传输
		//rpy, err := conn.Do("get", "hello")
		//rpy, err := redis.Int(conn.Do("get", "hello"))

		conn.Do("set", "hello", "world")
		// 如果不加redis.String,那么conn.DO返回的是字节切片类型[]type(整数和单个字符串一般是[]uint8),因为redis和go使用tcp字节流传输
		//rpy, err := conn.Do("get", "hello")
		rpy, err := redis.String(conn.Do("get", "hello"))
		if err != nil {
			panic(err)
		}
		fmt.Println(rpy, reflect.TypeOf(rpy))
	}

	// 3. 使用redis.Args{}添加多个参数。
	if false {
		conn.Do("del", "list")
		args := redis.Args{}.Add("list").Add("tyy").Add("lqq").Add("hc")
		conn.Do("lpush", args...)
		// 因为Do返回的是批量字符串,所以我们可以使用Strings来转成[]string类型的切片。切片可以保存二进制数据。
		rpy, _ := redis.Strings(conn.Do("lrange", "list", 0, -1))
		fmt.Println(rpy, reflect.TypeOf(rpy))
	}

	// 4. 测试brpop,并且使用[]string字符串切片进行接收。
	// 测试方法:if条件改成true,运行程序,然后xshell开启redis-cli往这个list输入个数据即可。
	// 例如命令为:lpush list lqq tyy hc。注意如果一次lpush多个,下面的brpop只会拿走一个就返回,
	// brpop和rpop的区别就在于为空时是否阻塞。
	// 例如结果:[list lqq]
	// 			<nil>
	// 			connection close
	// list代表key,lqq代表获取到的名字,<nil>和connection close只是err的值,返回nil代表没错误,然后连接关闭。
	if false {
		conn.Do("del", "list")
		vals, err := redis.Strings(conn.Do("brpop", redis.Args{}.Add("list").Add(20)...))
		if err != redis.ErrNil {
			fmt.Println(vals)
			fmt.Println(err)
		}
	}

	// 5. 同样测试brpop,但使用map[string]string进行接收。大家可以按照需求来使用不同方式接收。
	// 测试方法:同第4点。
	if false {
		conn.Do("del", "list")
		vals, err := redis.StringMap(conn.Do("brpop", redis.Args{}.Add("list").Add(20)...))
		if err != redis.ErrNil {
			fmt.Println(vals)
			fmt.Println(err)
		}
	}

	// 6. 如果redis返回的是 批量数组 且 类型不同的内容,且 我们知道它具体的返回类型 时,满足这3个条件,
	//		那么可以使用redis.Values + redis.Scan组合。
	if false {
		conn.Do("del", "list")
		conn.Do("lpush", "list", "tyy", 100) // 往list中push字符串和整型。
		vals, _ := redis.Values(conn.Do("lrange", "list", 0, -1))
		var name string
		var score int
		redis.Scan(vals, &score, &name) // 注意顺序,因为先返回100,再返回tyy,使用参数2位score,参数3为name
		fmt.Println(name, score)
	}

	// 7. 结构体、map作为redis的命令数据传入,并且利用结构体重新从redis中读取返回的key-value数据。
	if true {
		// 7.1 将结构体展开并当做redis的参数,传输到redis中执行。
		var p1, p2 struct {
			// 使用strurt_tag,类似json(`json:"company"`),那么字段名Name会变成name。
			Name string `redis:"name"`
			Age  string `redis:"age"`
			Sex  string `redis:"sex"`
		}
		p1.Age = "18"
		p1.Name = "tyy"
		p1.Sex = "male"

		// AddFlat的作用是将结构体的字段名和字段值进行展开,例如展开p1之后:age 18 name tyy sex male
		args1 := redis.Args{}.Add("role:10001").AddFlat(&p1)
		if _, err := conn.Do("hmset", args1...); err != nil {
			fmt.Println(err)
			return
		}

		// 7.2 将map展开并当做redis的参数,传输到redis中执行。
		m := map[string]string{
			"name": "lqq",
			"age":  "20",
			"sex":  "female",
		}
		args2 := redis.Args{}.Add("role:10002").AddFlat(m)
		if _, err := conn.Do("hmset", args2...); err != nil {
			fmt.Println(err)
			return
		}

		// 7.3 使用结构体从redis中获取key-value的内容。
		// 由于redis返回的是key-value形式的批量数组,那么我们可以使用 redis.Values + redis.ScanStruct方式获取内容。
		// 参考redis命令行输入会返回以下内容:
		/*
			192.168.1.9:6379> HGETALL role:10001
			1) "name"
			2) "lqq"
			3) "age"
			4) "20"
			5) "sex"
			6) "female"
		*/
		for _, id := range []string{"role:10001", "role:10002"} { // id是"role:10001", "role:10002"
			fmt.Println(id)
			v, err := redis.Values(conn.Do("HGETALL", id))
			if err != nil {
				fmt.Println(err)
				return
			}

			if err := redis.ScanStruct(v, &p2); err != nil {
				fmt.Println(err)
				return
			}

			fmt.Printf("%+v\n", p2)
		}
	}
}

上面代码步骤依次对应的结果截图(4、5的brpop需要在shell的命令行往list中lpush后,程序才能不超时返回):

  • 2)
    在这里插入图片描述

  • 3)
    在这里插入图片描述

  • 4)
    在这里插入图片描述

  • 5)
    在这里插入图片描述

  • 6)
    在这里插入图片描述

  • 7)
    在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐