8.1 GO通过redigo读写Redis

8.1.1 以go mod方式引入redigo包

  • step1. 编写redis.conf文件

(base) yanglei@yuanhong section8-1 % cat redis.conf 
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user redis_user on >redis_password ~* &* +@all

# 指定端口
port 6379
  • step2. 启动redis-server

(base) yanglei@yuanhong ~ % redis-server /Users/yanglei/Desktop/StudyRedisBaseOnDocker/conf/chapter8/section8-1/redis.conf
  • step3. 使用go mod初始化项目

(base) yanglei@yuanhong operateRedis % pwd
/Users/yanglei/Desktop/StudyRedisBaseOnDocker/code/chapter8/section8-1/operateRedis
(base) yanglei@yuanhong operateRedis % go mod init operateRedis
go: creating new go.mod: module operateRedis
  • step4. 引入redigo包

编写main.go如下:

/Users/yanglei/Desktop/StudyRedisBaseOnDocker/code/chapter8/section8-1/operateRedis/main.go:

package main

import (
	"github.com/gomodule/redigo/redis"
	"log"
)

func main() {
	conn, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		log.Fatalf("Could not connect: %v\n", err)
	} else {
		log.Println("Connected")
	}
	defer conn.Close()
}

执行go mod tidy

  • step5. 运行

(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/18 13:48:03 Connected
  • step6. 认证并确认连接

  • PING:该命令用于测试与Redis服务器的连接是否仍然活跃.若链接活跃则Redis服务器会回复PONG

修改main.go如下:

package main

import (
	"github.com/gomodule/redigo/redis"
	"log"
)

func main() {
	// 连接到Redis
	conn, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		log.Fatalf("Could not connect: %v\n", err)
		return
	}

	// 认证
	_, err = conn.Do("AUTH", "redis_user", "redis_password")
	if err != nil {
		log.Fatalf("Could not authenticate: %v\n", err)
		return
	}

	// 探活
	reply, err := redis.String(conn.Do("PING"))
	if err != nil {
		log.Fatalf("Could not ping: %v\n", err)
		return
	}

	log.Printf("PING Response = %s\n", reply)

	defer conn.Close()
}

运行结果:

(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/18 14:00:22 PING Response = PONG
  • step7. 封装连接

工程结构如下:

(base) yanglei@yuanhong operateRedis % tree ./
./
├── conn
│   └── redis.go
├── go.mod
├── go.sum
└── main.go

1 directory, 4 files

conn/redis.go:

package conn

import (
	"github.com/gomodule/redigo/redis"
	"log"
)

type Conf struct {
	NetWork  string // NetWork 网络类型
	Address  string // Address Redis地址 格式: ip:port
	User     string // User 用户名
	Password string // Password 密码
}

// NewConn 根据给定的配置 创建Redis连接
func NewConn(conf *Conf) (conn redis.Conn, err error) {
	// 连接到Redis
	conn, err = redis.Dial(conf.NetWork, conf.Address)
	if err != nil {
		log.Fatalf("Could not connect: %v\n", err)
		return
	}

	if conf.User != "" || conf.Password != "" {
		err = auth(conf, conn)
		if err != nil {
			log.Fatalf("Could not authenticate: %v\n", err)
			return
		}
	}

	err = ping(conn)
	if err != nil {
		log.Fatalf("Could not ping: %v\n", err)
		return
	}

	return
}

// auth 根据给定的配置和连接 进行认证
func auth(conf *Conf, conn redis.Conn) (err error) {
	_, err = conn.Do("AUTH", conf.User, conf.Password)
	return err
}

// ping 根据给定的连接 进行探活
func ping(conn redis.Conn) (err error) {
	_, err = conn.Do("PING")
	return err
}

main.go:

package main

import (
	"log"
	"operateRedis/conn"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	_, err := conn.NewConn(&conf)
	if err != nil {
		panic(err)
	}

	log.Print("Connect to Redis success!")
}
  • step8. 测试

(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/18 14:33:15 Connect to Redis success!

8.1.2 通过redigo读写Redis字符串

写入字符串

工程结构如下:

(base) yanglei@yuanhong operateRedis % tree ./
./
├── conn
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
└── operate
    └── set.go

2 directories, 5 files

operate/set.go:

package operate

import "github.com/gomodule/redigo/redis"

func Set(conn redis.Conn, key string, value string) (reply string, err error) {
	return redis.String(conn.Do("SET", key, value))
}

main.go:

package main

import (
	"log"
	"operateRedis/conn"
	"operateRedis/operate"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	redisConn, err := conn.NewConn(&conf)
	if err != nil {
		panic(err)
	}

	reply, err := operate.Set(redisConn, "name", "Peter")
	if err != nil {
		panic(err)
	}
	log.Printf("Set command reply: %s\n", reply)
}

运行结果:

(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/18 14:44:31 Set command reply: OK

读取字符串

工程结构如下:

(base) yanglei@yuanhong operateRedis % tree ./       
./
├── conn
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
└── operate
    ├── get.go
    └── set.go

2 directories, 6 files

operate/get.go:

package operate

import "github.com/gomodule/redigo/redis"

func Get(conn redis.Conn, key string) (reply string, err error) {
	return redis.String(conn.Do("GET", key))
}

main.go:

package main

import (
	"log"
	"operateRedis/conn"
	"operateRedis/operate"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	redisConn, err := conn.NewConn(&conf)
	if err != nil {
		panic(err)
	}

	reply, err := operate.Get(redisConn, "name")
	if err != nil {
		panic(err)
	}
	log.Printf("Get command reply: %s\n", reply)
}

运行结果:

(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/18 14:49:10 Get command reply: Peter

8.1.3 操作各种Redis命令

DEL指令

工程结构如下:

(base) yanglei@yuanhong operateRedis % tree ./
./
├── conn
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
└── operate
    ├── del.go
    ├── get.go
    └── set.go

2 directories, 7 files

operate/del.go:

package operate

import "github.com/gomodule/redigo/redis"

func Del(conn redis.Conn, key string) (reply int, err error) {
	return redis.Int(conn.Do("DEL", key))
}

main.go:

package main

import (
	"log"
	"operateRedis/conn"
	"operateRedis/operate"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	redisConn, err := conn.NewConn(&conf)
	if err != nil {
		panic(err)
	}

	reply, err := operate.Del(redisConn, "name")
	if err != nil {
		panic(err)
	}
	log.Printf("Del command reply: %d\n", reply)
}

运行结果:

(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/18 14:53:03 Del command reply: 1

KEYS指令

工程结构如下:

(base) yanglei@yuanhong operateRedis % tree ./
./
├── conn
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
└── operate
    ├── del.go
    ├── get.go
    ├── keys.go
    └── set.go

2 directories, 8 files

operate/keys.go:

package operate

import "github.com/gomodule/redigo/redis"

func Keys(conn redis.Conn, pattern string) (reply []string, err error) {
	return redis.Strings(conn.Do("KEYS", pattern))
}

main.go:

package main

import (
	"log"
	"operateRedis/conn"
	"operateRedis/operate"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	redisConn, err := conn.NewConn(&conf)
	if err != nil {
		panic(err)
	}

	replies, err := operate.Keys(redisConn, "*a*")
	if err != nil {
		panic(err)
	}

	for _, reply := range replies {
		log.Printf("Keys command reply: %s\n", reply)
	}
}

运行结果:

(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/18 14:57:57 Keys command reply: repeat
2023/08/18 14:57:57 Keys command reply: rename

注:此处这2个key是我通过redis-cli事先添加进去的

EXISTS指令

工程结构如下:

(base) yanglei@yuanhong operateRedis % tree ./
./
├── conn
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
└── operate
    ├── del.go
    ├── exists.go
    ├── get.go
    ├── keys.go
    └── set.go

2 directories, 9 files

operate/exists.go:

package operate

import "github.com/gomodule/redigo/redis"

func Exists(conn redis.Conn, key string) (reply bool, err error) {
	return redis.Bool(conn.Do("EXISTS", key))
}

main.go:

package main

import (
	"log"
	"operateRedis/conn"
	"operateRedis/operate"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	redisConn, err := conn.NewConn(&conf)
	if err != nil {
		panic(err)
	}

	reply, err := operate.Exists(redisConn, "name")
	if err != nil {
		panic(err)
	}
	log.Printf("name exists: %v\n", reply)

	reply, err = operate.Exists(redisConn, "rename")
	if err != nil {
		panic(err)
	}
	log.Printf("rename exists: %v\n", reply)
}

运行结果:

(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/18 15:46:27 name exists: false
2023/08/18 15:46:27 rename exists: true

也能通过该包使用其他Redis命令

8.1.4 以事务的方式操作Redis

工程结构如下:

(base) yanglei@yuanhong operateRedis % tree ./
./
├── conn
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
└── operate
    ├── del.go
    ├── exists.go
    ├── get.go
    ├── keys.go
    ├── set.go
    └── transaction.go

2 directories, 10 files

operate/transaction.go:


package operate

import (
	"errors"
	"github.com/gomodule/redigo/redis"
)

func Transaction(conn redis.Conn) (replies []string, err error) {
	// 1. Watch
	_, err = redis.String(conn.Do("WATCH", "counter"))
	if err != nil {
		return nil, err
	}

	// 若counter的值大于10 则不执行事务
	// 此处假定10为counter的原值
	counter, _ := redis.Int(conn.Do("GET", "counter"))
	if counter > 10 {
		conn.Do("UNWATCH")
		return nil, errors.New("counter value is too high. Not continuing the transaction")
	}

	// 2. Multi
	err = conn.Send("MULTI")
	if err != nil {
		return nil, err
	}

	// 3. Exec
	err = conn.Send("SET", "transaction_key_1", "transaction_value_1")
	if err != nil {
		conn.Do("DISCARD")
		return nil, err
	}

	err = conn.Send("SET", "transaction_key_2", "transaction_value_2")
	if err != nil {
		conn.Do("DISCARD")
		return nil, err
	}

	replies, err = redis.Strings(conn.Do("EXEC"))
	if err != nil {
		conn.Do("DISCARD")
		return nil, err
	}

	return replies, nil
}

main.go:

package main

import (
	"log"
	"operateRedis/conn"
	"operateRedis/operate"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	redisConn, err := conn.NewConn(&conf)
	if err != nil {
		panic(err)
	}

	replies, err := operate.Transaction(redisConn)
	if err != nil {
		panic(err)
	}

	for _, reply := range replies {
		log.Printf("Transaction reply: %v\n", reply)
	}
}

运行结果:

(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/21 16:49:20 Transaction reply: OK
2023/08/21 16:49:20 Transaction reply: OK

8.1.5 redis连接池

如果在项目中有多个GO客户端需要连接并操作Redis对象,那么每一次访问Redis都需要经历创建连接 -> 打开Redis并操作 -> 释放连接的步骤.连接和关闭数据库的操作比较浪费资源,如果频繁操作,会影响Redis甚至整个系统的性能,所以这种场景可以用redigo连接池来管理Redis的连接.

工程结构如下:

(base) yanglei@yuanhong operateRedis % tree ./
./
├── conn
│   ├── pool.go
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
└── operate
    ├── del.go
    ├── exists.go
    ├── get.go
    ├── keys.go
    ├── set.go
    └── transaction.go

2 directories, 11 files

其中,conn/pool.go如下:

package conn

import (
	"github.com/gomodule/redigo/redis"
	"time"
)

type PoolConf struct {
	// MaxIdle 最大空闲连接数
	MaxIdle int
	// MaxActive 最大连接数,0表示无限制
	MaxActive int
	// IdleTimeout 空闲连接超时时间
	IdleTimeout time.Duration
	// Conf Redis连接配置
	Conf Conf
}

var Pool *redis.Pool

func NewPool(poolConf PoolConf) {
	Pool = &redis.Pool{
		MaxIdle:     poolConf.MaxIdle,
		MaxActive:   poolConf.MaxActive,
		IdleTimeout: poolConf.IdleTimeout,
		Dial: func() (redis.Conn, error) {
			return redis.Dial(poolConf.Conf.NetWork, poolConf.Conf.Address)
		},
	}
}

// GetConnFromPool 从连接池中获取一个经过认证后的连接
func GetConnFromPool(poolConf PoolConf) (conn redis.Conn, err error) {
	conn = Pool.Get()
	err = auth(&poolConf.Conf, conn)
	if err != nil {
		return nil, err
	}

	return conn, nil
}

// CloseConnToPool 将该连接返回到连接池中
func CloseConnToPool(conn redis.Conn) error {
	// 关闭一个从连接池中取出的连接时,使用conn.Close()方法
	// 并不会让该连接真正的关闭,而是将该连接返回到连接池中,以便后续复用
	return conn.Close()
}

main.go:

package main

import (
	"fmt"
	"log"
	"operateRedis/conn"
	"operateRedis/operate"
	"time"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	poolConf := conn.PoolConf{
		MaxIdle:     10,
		MaxActive:   100,
		IdleTimeout: time.Hour,
		Conf:        conf,
	}

	// 初始化连接池
	conn.NewPool(poolConf)

	// 从连接池中获取一个连接
	redisConn, err := conn.GetConnFromPool(poolConf)
	if err != nil {
		log.Fatalf("get conn from pool error: %v\n", err)
	}

	// 将该连接返回到连接池中
	defer conn.CloseConnToPool(redisConn)

	// 使用该连接进行操作
	reply, err := operate.Set(redisConn, "name", "Peter")
	if err != nil {
		log.Fatalf("set error: %v\n", err)
	}

	fmt.Printf("set reply: %v\n", reply)
}

运行结果:

(base) yanglei@yuanhong operateRedis % go run main.go 
set reply: OK

8.1.6 用管道的方式提升操作性能

在单个客户端里,如果要读写大量数据,那么可以采用管道(pipeline)的方式.比如要一次性地执行20次的读写,那么每条命令都需要发送到Redis服务器,而每条命令的执行结果都需要返回给客户端.如果采用管道的方式,那么这20条命令会以批量的方式一次性地发送到服务器,而结果也会一次性地返回到客户端.

换言之,在大数据操作的场景里,通过管道的方式能大量节省"传输命令和结果的时间".

工程结构如下:

(base) yanglei@yuanhong operateRedis % tree ./
./
├── conn
│   ├── pool.go
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
└── operate
    ├── del.go
    ├── exists.go
    ├── get.go
    ├── keys.go
    ├── pipeline.go
    ├── set.go
    └── transaction.go

2 directories, 12 files

其中,operate/pipeline.go如下:

package operate

import "github.com/gomodule/redigo/redis"

// Pipeline 以管道方式一次性发送多个命令
func Pipeline(conn redis.Conn) (operateNum int, err error) {
	operateNum = 0
	err = conn.Send("SET", "name", "Peter")
	if err != nil {
		return 0, err
	}
	operateNum++

	err = conn.Send("GET", "name")
	if err != nil {
		return 0, err
	}
	operateNum++

	err = conn.Send("SET", "age", "18")
	if err != nil {
		return 0, err
	}
	operateNum++

	err = conn.Send("GET", "age")
	if err != nil {
		return 0, err
	}
	operateNum++

	// 将缓冲区的命令写入到连接中
	err = conn.Flush()
	if err != nil {
		return 0, err
	}

	return operateNum, nil
}

main.go如下:

package main

import (
	"github.com/gomodule/redigo/redis"
	"log"
	"operateRedis/conn"
	"operateRedis/operate"
	"time"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	poolConf := conn.PoolConf{
		MaxIdle:     10,
		MaxActive:   100,
		IdleTimeout: time.Hour,
		Conf:        conf,
	}

	// 初始化连接池
	conn.NewPool(poolConf)

	// 从连接池中获取一个连接
	redisConn, err := conn.GetConnFromPool(poolConf)
	if err != nil {
		log.Fatalf("get conn from pool error: %v\n", err)
	}

	// 将该连接返回到连接池中
	defer conn.CloseConnToPool(redisConn)

	// 使用该连接进行操作
	operateNum, err := operate.Pipeline(redisConn)
	if err != nil {
		log.Fatalf("pipeline error: %v\n", err)
	}

	// 读取响应
	for i := 0; i < operateNum; i++ {
		reply, err := redis.String(redisConn.Receive())
		if err != nil {
			log.Fatalf("receive error: %v\n", err)
		}

		log.Printf("reply: %v\n", reply)
	}
}

运行结果:

(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/21 16:43:47 reply: OK
2023/08/21 16:43:47 reply: Peter
2023/08/21 16:43:47 reply: OK
2023/08/21 16:43:47 reply: 18

Last updated