Golang 操作Redis

Golang 操作Redis

杰子学编程 94 2022-06-01

前言

使用redis首先要部署redis,载个安装包,部署下即可,本文不赘述了。redis官网:https://redis.io/
接着要下载golang的redis资源包,golang官方推荐的有redisgo和go-reids,个人认为go-redis的封装更加人性化,redisgo的调用是基于命令的,go-redis是基于方法的,所以本文先来介绍go-redis的使用。
2行代码来比较下2种资源包的调用方式:

redisgo: client.Do("SET", "mykey", "我是数据", "EX", "3600")
go-redis:client.Set("mykey", "我是数据", time.Hour)

同样是存储一个1小时后过期的数据,go-redis的调用方式明显更友好。

导入go-redis包

image
我们在cache包中创建个init函数,内容如下:

var RedisCache = &redis.Client{}

func init() {
    RedisCache = redis.NewClient(&redis.Options{
        Addr:     "127.0.0.1:6379",
        Password: "125846whj",
        DB:       0,
    })
    //ping
    pong, err := RedisCache.Ping().Result()
    if err != nil {
        fmt.Println("ping error", err.Error())
        return
    }
    fmt.Println("ping result:", pong)
}

解析:通过 redis.NewClient创建个redisClient,构建参数这里指定了redis服务地址、链接密码和数据库号。redis.Options 还有更多参数我们简单看下:

type Options struct {
	// 网络类型,tcp或unix。
	// 默认Tcp
	Network string
	// host:port address.
	Addr string
	// Dialer创建新的网络连接,并具有优先级
	// 网络和地址选项。
	Dialer func() (net.Conn, error)
	// 建立新连接时调用的钩子。
	OnConnect func(*Conn) error

	// 可选的密码。中指定的密码必须匹配
	// requirepass server configuration option.
	Password string
	// 连接到服务器后选择的数据库 0~15
	DB int
	// 失败链接前最大重试次数
	// 默认值是不重试失败的链接
	MaxRetries int
	// Minimum backoff between each retry.
	// Default is 8 milliseconds; -1 disables backoff.
	MinRetryBackoff time.Duration
	// Maximum backoff between each retry.
	// Default is 512 milliseconds; -1 disables backoff.
	MaxRetryBackoff time.Duration

	// Dial timeout for establishing new connections.
	// Default is 5 seconds.
	DialTimeout time.Duration
	// 套接字读取超时。如果到达,命令将失败
	// with a timeout instead of blocking. Use value -1 for no timeout and 0 for default.
	// Default is 3 seconds.
	ReadTimeout time.Duration
	// Timeout for socket writes. If reached, commands will fail
	// with a timeout instead of blocking.
	// Default is ReadTimeout.
	WriteTimeout time.Duration

	// Maximum number of socket connections.
	// Default is 10 connections per every CPU as reported by runtime.NumCPU.
	PoolSize int
	// Minimum number of idle connections which is useful when establishing
	// new connection is slow.
	MinIdleConns int
	// Connection age at which client retires (closes) the connection.
	// Default is to not close aged connections.
	MaxConnAge time.Duration
	// Amount of time client waits for connection if all connections
	// are busy before returning an error.
	// Default is ReadTimeout + 1 second.
	PoolTimeout time.Duration
	// Amount of time after which client closes idle connections.
	// Should be less than server's timeout.
	// Default is 5 minutes. -1 disables idle timeout check.
	IdleTimeout time.Duration
	// Frequency of idle checks made by idle connections reaper.
	// Default is 1 minute. -1 disables idle connections reaper,
	// but idle connections are still discarded by the client
	// if IdleTimeout is set.
	IdleCheckFrequency time.Duration

	// Enables read only queries on slave nodes.
	readOnly bool

	// TLS Config to use. When set TLS will be negotiated.
	TLSConfig *tls.Config
}

1、String类型

字符串作为Redis最简单的类型,其底层实现只有一种数据结构,就是简单动态字符串(SDS)。作为万金油的字符串类型,可以支持struct结构,基本上string类型在传统系统可以解决80%以上的问题。我们看下golang如何使用字符串类型。

// Get 获取缓存数据
func Get(key string) (string, error) {
	result, err := RedisCache.Get(key).Result()
	return result, err
}

// Set 设置数据 过期时间默认24H
func Set(key, value string) error {
	err := RedisCache.Set(key, value, time.Hour*24).Err()
	return err
}

可以看到,string类型非常简单和其他sdk没有什么区别,可以直接调用 Set 方法。

Set(key string, value interface{}, expiration time.Duration)

参数分别为 key、value、expiration过期时间。
获取数据可以通过Get方法获取,返回数据类型及string类型。我们可以测试下,在main方法中定义一下内容:

func main() {
	key := "string:key"
	if cache.Set(key, "字符串作为Redis最简单的类型,其底层实现只有一种数据结构,就是简单动态字符串(SDS)。") != nil {
		fmt.Println("缓存设置错误")
	}
	value, err := cache.Get(key)
	if err != nil {
		fmt.Println("get 缓存出错")
	}
	fmt.Printf("获取到缓存值: %s\n", value)
}

看下结果:
String
这里ping result : PONG是测试redis 链接是否成功,通过客户端发送 PING 服务端回复 PONG 的方式确认链接是否成功,成功后,设置key和value,设置成功后,通过Get取出对应的值。

2、struct 结构

存储结构其实也是存储string,只是把struc序列化成json,等读取的时候再反序列化成struct;
序列化:json.Marshal
反序列化:json.Unmarshal
我们看下demo:

type User struct {
	Name  string `json:"name"`
	Phone string `json:"phone"`
	Age   int64  `json:"age"`
}

func main() {
	key := "string:user"
	user := User{Name: "张三", Phone: "18234566897", Age: 28}
	userJson, _ := json.Marshal(user)
	if cache.Set(key, userJson) != nil {
		fmt.Println("缓存设置错误")
	}
	value, err := cache.Get(key)
	if err != nil {
		fmt.Println("get 缓存出错")
	}
	fmt.Printf("获取到缓存值: %s\n", value)
	var user2 User
	json.Unmarshal([]byte(value), &user2)
	fmt.Println("user2", user2)
}

我们定义一个User的struct,里面包含姓名、手机号、年龄三个属性,定义strng:user key。我先实例化一个user对象,赋值张三、18234566897、28岁。将user对象序列化成json字符串,并通过Set方法设置到缓存中。
缓存设置成功后,我们通过Get方法将值取出,并打印取出的值,通过反序列化方式,将字符串反序列化成
user2对象。

3、List 类型

List本身是按先进先出的顺序对数据进行存取的,可以通过Lpush从一端存入数据,通过RPop从一端消费数据。同时为了解决RPop在消费数据解决while(1)循环,导致消费者CPU一直消耗,Redis引入了新的方法BRPop,及阻塞式读取,客户端在没有读取到队列数据时,自动阻塞,直到有新的数据写入队列,在开始读取新数据。
我们在使用List类型时需要注意一个问题,及生产速度大于消费速度,这样会导致List中的数据越来越多,给Redis的内存带来很大压力,所以我们在使用List类型时需要考虑生产消费的能力。
这里我们重点将几个常用的方法Lpush、Rpop、BRpop、LLen、LRange;

func main() {
	key := "string:list"
	err := cache.RedisCache.LPush(key, "A", "B", "C", 20, "D", "E", "F").Err()
	if err != nil {
		fmt.Println("缓存设置错误", err)
	}
	value, err := cache.RPop(key)
	if err != nil {
		fmt.Println("get 缓存出错")
	}
	fmt.Printf("获取到缓存值: %s\n", value)
}

List

BRpop

func main() {
	key := "string:list"
	err := cache.RedisCache.LPush(key, "A", "B", "C", 20, "D", "E", "F").Err()
	if err != nil {
		fmt.Println("缓存设置错误", err)
	}
	for {
		value, err := cache.BRPop(time.Second*10, key)
		if err != nil {
			fmt.Println("get 缓存出错", err)
			break
		}
		fmt.Printf("获取到缓存值: %s\n", value)
	}
	fmt.Println("结束等等")
}

查看控制台结果:
BList
阻塞住了,等待返回,这里我们设置了阻塞10s,10s后还没有取到值,我们就退出。

LLen、LRange;

这两个方法比较简单,我们看下。

func main() {
	key := "string:list"
	err := cache.RedisCache.LPush(key, "A", "B", "C", 20, "D", "E", "F").Err()
	if err != nil {
		fmt.Println("缓存设置错误", err)
	}
	lLen, _ := cache.LLen(key)
	fmt.Printf("集合数据长度:%d\n", lLen)
	lRange, _ := cache.LRange(key, 0, 3)
	fmt.Println(lRange)
}

ListSize

4、Hash类型

Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)。
Hash类型对应的底层数据结构是Hash表和压缩列表。
我们看下Hash类型常用的操作:HSet、HGet、HAll、HDel、HExists

HSet

用于同时将多个 field-value (字段-值)对设置到哈希表中,此方法会覆盖哈希表中已存在的字段。如果哈希表不存在,会创建一个空哈希表。

func main() {
	key := "string:hash"
	cache.HSet(key, "name", "张三")
	cache.HSet(key, "phone", "18234554345")
	cache.HSet(key, "age", "28")
	//获取全部hash对象
	all, _ := cache.HGetAll(key)
	fmt.Println(all)
	//修改已存在的字段
	cache.HSet(key, "name", "李四")
	//获取指定字段
	name, _ := cache.HGet(key, "name")
	fmt.Println(name)
	existsName, _ := cache.HExists(key, "name")
	existsId, _ := cache.HExists(key, "id")
	fmt.Printf("name 字段是否存在 %v\n", existsName)
	fmt.Printf("id 字段是否存在 %v\n", existsId)
	cache.HDel(key, "name")
	existsName, _ = cache.HExists(key, "name")
	fmt.Printf("name 字段是否存在 %v\n", existsName)
	getAll, _ := cache.HGetAll(key)
	fmt.Println(getAll)
}

执行结果:
Hset
结果分析:
开始通过HSet方法设置name、phone、age三个字段,
通过HGetAll获取全部信息,控制台打印了map[age:28 name:张三 phone:18234554345]
修改name字段的值,改为李四,我们再次通过HGet获取name字段,可以看到现在取到的值为李四。张三被覆盖了。
通过HExists判断name和id字段判断是否存在,可以看到id不存在,name是存在的。
通过HDel方法,将name字段移除,在判断name字段是否存在,可以看到当前name字段不存在。
在次通过HGetAll打印信息,可以看到name已经没有了,只有两个字段了。

5、SET

Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
SET常用方法:

  • SADD:向集合添加一个或多个成员
  • SCard: 获取集合的成员数
  • SMembers:获取集合的所有成员
  • SRem: 移除集合里的某个元素
  • SPop: 移除并返回set的一个随机元素(SET是无序的)
  • SDiff: 返回第一个集合与其他集合之间的差异。
  • SDIFFSTORE: 返回给定所有集合的差集并存储在 destination 中
  • SInter: 返回所有给定集合的交集
  • Sunion: 返回所有给定集合的并集
func main() {
	key := "string:set"
	cache.RedisCache.SAdd(key, "phone")
	err2 := cache.RedisCache.SAdd(key, "hahh").Err()
	if err2 != nil {
		fmt.Println(err2)
		return
	}
	//获取全部hash对象
	all, _ := cache.SCard(key)
	fmt.Println(all)
	members, err2 := cache.SMembers(key)
	fmt.Println(members)
}

执行结果:
ping result: PONG
7
[张三 hahh 18234554345 28 name phone age]

这里只演示操作了三个函数:SAdd、SCard、Smember其他函数可以参考官方API。

6、有序集合(sorted set)

Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
sorted set 函数有很多,这里我们主要演示几个函数,通过sorted set 查看编程语言排行榜热度。

func main() {
	key := "string:zset"
	set := []redis.Z{
		{Score: 80, Member: "Java"},
		{Score: 90, Member: "Python"},
		{Score: 95, Member: "Golang"},
		{Score: 98, Member: "PHP"},
	}
	err := cache.ZAdd(key, set)
	if err != nil {
		fmt.Println(err)
	}
	scores, _ := cache.ZRevRangeWithScores(key, 0, 2)
	fmt.Println(scores)
	cache.ZIncrBy(key, 5, "Golang")
	scores, _ = cache.ZRevRangeWithScores(key, 0, 2)
	fmt.Println("加分后----")
	fmt.Println(scores)
}

输出结果:
ZSet
解析:我们初始化四门编程语言分别给定初始分数,获取此时排名前三名的编程语言;之后我们将golang语言增加5分,再次获取前三名编程语言,可以看到golang排在第一。

7、设置过期时间

操作string数据的时候,可以在方法里直接传入过期时间。但list、hash、set、zset都没有直接提供相应参数,但redis可以额外设置key的过期时间。

// Expire 给指定key 设置过期时间
func Expire(key string, duration time.Duration) error {
	err := RedisCache.Expire(key, duration).Err()
	return err
}

// ExpireAt 给指定Key 设置过期时间,时间格式为UNIX时间
func ExpireAt(key string, duration time.Time) error {
	err := RedisCache.ExpireAt(key, duration).Err()
	return err
}

// TTL 获取key的生存时间
func TTL(key string) (time.Duration, error) {
	result, err := RedisCache.TTL(key).Result()
	return result, err
}

8、完整代码

package cache

import (
	"fmt"
	"github.com/go-redis/redis"
	"time"
)

var RedisCache = &redis.Client{}

func init() {
	RedisCache = redis.NewClient(&redis.Options{
		Addr:     "127.0.0.1:6379",
		Password: "125846whj",
		DB:       0,
	})
	//ping
	pong, err := RedisCache.Ping().Result()
	if err != nil {
		fmt.Println("ping error", err.Error())
		return
	}
	fmt.Println("ping result:", pong)
}

// Get 获取缓存数据
func Get(key string) (string, error) {
	result, err := RedisCache.Get(key).Result()
	return result, err
}

// Set 设置数据 过期时间默认24H
func Set(key string, value interface{}) error {
	err := RedisCache.Set(key, value, time.Hour*24).Err()
	return err
}

// LPush RPush 使用RPush命令往队列右边加入
func LPush(key string, value ...interface{}) error {
	err := RedisCache.LPush(key, value).Err()
	return err
}

// RPop LPop 取出并移除左边第一个元素
func RPop(key string) (interface{}, error) {
	result, err := RedisCache.RPop(key).Result()
	return result, err
}

// BRPop BLPop 取出并移除左边第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
func BRPop(timeout time.Duration, key string) (interface{}, error) {
	result, err := RedisCache.BRPop(timeout, key).Result()
	return result, err
}

// LLen 获取数据长度
func LLen(key string) (int64, error) {
	result, err := RedisCache.LLen(key).Result()
	return result, err
}

// LRange 获取数据列表
func LRange(key string, start, end int64) ([]string, error) {
	result, err := RedisCache.LRange(key, start, end).Result()
	return result, err
}

// HSet hash相关操作
//set hash 适合存储结构
func HSet(hashKey, key string, value interface{}) error {
	err := RedisCache.HSet(hashKey, key, value).Err()
	return err
}

// HGet get Hash
func HGet(hashKey, key string) (interface{}, error) {
	result, err := RedisCache.HGet(hashKey, key).Result()
	return result, err
}

// HGetAll 获取所以hash ,返回map
func HGetAll(hashKey string) (map[string]string, error) {
	result, err := RedisCache.HGetAll(hashKey).Result()
	return result, err
}

// HDel 删除一个或多个哈希表字段
func HDel(hashKey string, key ...string) error {
	err := RedisCache.HDel(hashKey, key...).Err()
	return err
}

// HExists 查看哈希表的指定字段是否存在
func HExists(hashKey, key string) (bool, error) {
	result, err := RedisCache.HExists(hashKey, key).Result()
	return result, err
}

// SAdd -----------------Set------------------------
// 添加Set
func SAdd(key string, values ...interface{}) error {
	err := RedisCache.SAdd(key, values).Err()
	return err
}

// SCard 获取集合的成员数
func SCard(key string) (int64, error) {
	result, err := RedisCache.SCard(key).Result()
	return result, err
}

// SMembers 获取集合的所有成员
func SMembers(key string) ([]string, error) {
	result, err := RedisCache.SMembers(key).Result()
	return result, err
}

// SRem 移除集合里的某个元素
func SRem(key string, value interface{}) error {
	err := RedisCache.SRem(key, value).Err()
	return err
}

// SPop 移除并返回set的一个随机元素(SET是无序的)
func SPop(key string) (interface{}, error) {
	result, err := RedisCache.SPop(key).Result()
	return result, err
}

// ZAdd ------------------ZSet-------------------------
func ZAdd(key string, values []redis.Z) error {
	err := RedisCache.ZAdd(key, values...).Err()
	return err
}

// ZIncrBy 给指定的key和值加分
func ZIncrBy(key string, score float64, value string) error {
	err := RedisCache.ZIncrBy(key, score, value).Err()
	return err
}

// ZRevRangeWithScores 取zSet里的前n名热度的数据
func ZRevRangeWithScores(key string, start, end int64) ([]redis.Z, error) {
	result, err := RedisCache.ZRevRangeWithScores(key, start, end).Result()
	return result, err
}

// Expire 给指定key 设置过期时间
func Expire(key string, duration time.Duration) error {
	err := RedisCache.Expire(key, duration).Err()
	return err
}

// ExpireAt 给指定Key 设置过期时间,时间格式为UNIX时间
func ExpireAt(key string, duration time.Time) error {
	err := RedisCache.ExpireAt(key, duration).Err()
	return err
}

// TTL 获取key的生存时间
func TTL(key string) (time.Duration, error) {
	result, err := RedisCache.TTL(key).Result()
	return result, err
}


# Golang # Redis