redis实战——go-redis的使用与redis基础数据类型的使用场景(一)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 本文档介绍了如何使用 Go 语言中的 `go-redis` 库操作 Redis 数据库

一.go-redis的安装与快速开始

这里操作redis数据库,我们选用go-redis这一第三方库来操作,首先是三方库的下载,我们可以执行下面这个命令:

go get github.com/redis/go-redis/v9
AI 代码解读

最后我们尝试一下连接本机的redis数据库,以及执行一个简单的redis操作:

package main

import (
    "context"
    "github.com/redis/go-redis/v9"
)

func main() {
   
   
    rdb := redis.NewClient(&redis.Options{
   
   
        Addr:     "localhost:6379",
        Password: "",
        DB:       0,
    })

    //go-redis支持Context,我们可以用它来控制超时会传递数据
    ctx := context.Background()
    rdb.Set(ctx, "key", "value", 100)
    val, err := rdb.Get(ctx, "key").Result()
    if err != nil {
   
   
        panic(err)
    }
    println(val)
}
AI 代码解读

输出结果为:

value
AI 代码解读

同时,go-redis还支持Context,我们可以用这个机制来实现一些我们想要的功能,比如传递数据和设置超时时间:

package main

import (
    "context"
    "github.com/redis/go-redis/v9"
)

type contextkey string

var userIDKey contextkey = "userID"

func main() {
   
   
    rdb := redis.NewClient(&redis.Options{
   
   
        Addr:     "localhost:6379",
        Password: "",
        DB:       0,
    })

    //go-redis支持Context,我们可以用它来控制超时会传递数据
    ctx := context.WithValue(context.Background(), userIDKey, "123")

    //利用上下文来传递数据
    userID := ctx.Value(userIDKey).(string)

    rdb.Set(ctx, "ID", userID, 0)
    val, err := rdb.Get(ctx, "ID").Result()
    if err != nil {
   
   
        panic(err)
    }
    //// 设置超时时间为 2 秒
    //ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    //defer cancel() // 确保在函数结束时取消上下文
    println(val)
}
AI 代码解读

二.go-redis的基本操作

rdb.Del(ctx, "ID") //删除键
rdb.Expire(ctx, "ID", 100) //设置过期时间
rdb.Persist(ctx, "ID") //取消过期时间
rdb.TTL(ctx, "ID") //获取ID的过期时间
rdb.PTTL(ctx, "ID") //获取ID的剩余过期时间
rdb.Type("ID")  //查询类型
rdb.Scan(0,"",4) //扫描
AI 代码解读

三. go-redis操作字符串

首先对字符串的操作还是很简单的:

package main

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

func main() {
   
   
    rdb := redis.NewClient(&redis.Options{
   
   
        Addr:     "localhost:6379",
        Password: "",
        DB:       0, // use default DB
    })
    ctx := context.Background()
    rdb.Set(ctx, "token1", "abcefghijklmn", 0)                    // 设置token
    rdb.MSet(ctx, "token2", "abcefghijklmn", "cookie1", "123456") // 设置多个key
    a := rdb.MGet(ctx, "token1", "token2", "cookie1").Val()
    fmt.Println(a)

    //数字增减操作
    rdb.Set(ctx, "age", "1", 0)
    fmt.Println(rdb.Get(ctx, "age").Val())
    rdb.Incr(ctx, "age")
    fmt.Println(rdb.Get(ctx, "age").Val())
    rdb.IncrBy(ctx, "age", 5)
    fmt.Println(rdb.Get(ctx, "age").Val())
    rdb.Decr(ctx, "age")
    fmt.Println(rdb.Get(ctx, "age").Val())
    rdb.DecrBy(ctx, "age", 5)
    fmt.Println(rdb.Get(ctx, "age").Val())
}
AI 代码解读

String可以算是Redis使用最为频繁的基础数据类型了,它的作用非常广泛,简单的它可以实现一个计数器,向这样:

    rdb.Set(ctx, "age", "1", 0)
    fmt.Println(rdb.Get(ctx, "age").Val())
    rdb.Incr(ctx, "age")
    fmt.Println(rdb.Get(ctx, "age").Val())
    rdb.IncrBy(ctx, "age", 5)
    fmt.Println(rdb.Get(ctx, "age").Val())
    rdb.Decr(ctx, "age")
    fmt.Println(rdb.Get(ctx, "age").Val())
    rdb.DecrBy(ctx, "age", 5)
    fmt.Println(rdb.Get(ctx, "age").Val())
AI 代码解读

它也可以用来实现分布式锁,后面我们会详细的探讨分布式锁的原理,这里我们就简单的介绍一下什么是分布式锁:
我们在微服务中会在一个服务中部署多个进程,需要我们操作多个进程,在多进程中为了避免同时操作一个共享变量产生数据问题,通常会使用一把锁来互斥以保证共享变量的正确性,但是单机锁发挥作用是单进程中使用,我们应该如何给多个进程上锁呢?
我们这里可以选择第三方来做裁判,这里我们一般会使用Zookeeperredis来作为第三方,所有进程都去这个裁判这里申请加锁。而这个外部系统,必须要实现互斥能力,即两个请求同时进来,只会给一个进程加锁成功,另一个失败,接下来我们来看一下这个分布式锁怎么来实现:
set命令中通过NX参数我们可以实现key 不存在才插入,我们可以用它来实现分布式锁:

  • 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
  • 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。

分布式锁加锁命令如下:

set lock uuid NX PX 10000
AI 代码解读

lock:key键
uuid:客户端生成的唯一的标识
NX: 只在 lock 不存在时,才对 lock 进行设置操作
PX:设置锁的过期时间

而解锁就是删除lock键,但是这个命令不能随便删,我们要保证执行该操作的客户端是加了锁的,这就导致我们删锁的操作分为以下两步:

  • 判断锁的 uuid 是否为加锁客户
  • 删除锁
    但是由于是俩个操作,这就导致删锁的操作不具有原子性,所以需要我们借助Lua脚本来实现操作的原子性,Lua脚本如下:
    if redis.call("get",KEYS[1]) == ARGV[1] then
      return redis.call("del",KEYS[1])
    else
      return 0
    end
    
    AI 代码解读
    这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。

最后我们还可以用Redis共享 Session 信息:
在我们写后台管理系统的时候,我们一般需要存储用户的Jwt或者Session来保存用户的登录状态,单服务器下Session 信息会被保存在服务器端,但是分布式系统下,用户一的 Session 信息被存储在服务器一,但第二次访问时用户一被分配到服务器二,这个时候服务器并没有用户一的 Session 信息,就会出现需要重复登录的问题,问题在于分布式系统每次会把请求随机分配到不同的服务器,而为了解决这个问题我们就会选择redis服务器来对这些 Session 信息进行统一的存储和管理,这样无论请求发送到那台服务器,服务器都会去同一个 Redis 获取相关的 Session 信息,进而解决了分布式系统下 Session 存储的问题,结构示例如下:
在这里插入图片描述

go-redis操作list

首先我们来看一下一些常用的命令:

// 左边添加
redisClient.LPush("list", "a", "b", "c", "d", "e")
// 右边添加
redisClient.RPush("list", "g", "i", "a")
// 在参考值前面插入值
redisClient.LInsertBefore("list", "a", "aa")
// 在参考值后面插入值
redisClient.LInsertAfter("list", "a", "gg")
// 设置指定下标的元素的值
redisClient.LSet("list", 0, "head")
//访问列表长度
redisClient.Len("list")
// 左边弹出元素
redisClient.LPop("list")
// 右边弹出元素
redisClient.RPop("list")
// 访问指定下标的元素
redisClient.LIndex("list", 1)
// 访问指定范围内的元素
redisClient.LRange("list", 0, 1)
// 保留指定范围的元素
redisClient.LTrim("list", 0, 1)
// 删除指定元素
redisClient.LRem("list", 0, "a")
AI 代码解读

关于List的使用场景我也没有找到太多的案例,但是博主找到了一个比较有趣的实践:基于List这一数据结构来实现一个简单的消息队列,接下来博主将尝试写一个简单的消息队列来作为我们List数据结果的实践:

一个合格的消息队列应该满足下面几个要求:

  • 消息的保序性
  • 如何处理重复的消息
  • 保证消息的可靠性

首先我们如何保证消息的有序性呢?由于我们是用List这一数据结构来实现对消息队列的模拟,所以不生就可以实现对消息的保序性了,我们现在要完成的就是生产者基于Push操作完成对消息的生产,消费者基于Pop完成对信息地消费即可,一般来说下面这个组合就可以了

  • LPUSH+RPOP
  • RPUSH+LPOP

但是现在有一个问题:List本身是不会去提醒消费者有新消息写入,如果消费者想要及时处理消息,我们应该怎么做呢?首先我们的想法应该是让消费者程序不断去执行RPOP操作,如果有新消息写入,RPOP 命令就会返回结果,否则,RPOP 命令返回空值,再继续循环。但是这样一来消费者程序的 CPU 一直消耗在执行 RPOP 命令上,带来不必要的性能损失,所以这里我们可以选择使用BRPOP操作,执行该操作时,客户端在没有读到队列数据的时候会自动阻塞,直到有新的数据写入队列,再开始读取新数据。和消费者程序自己不停地调用 RPOP 命令相比,这种方式能节省 CPU 开销。

示例代码如下:

package main

import (
    "context"
    "github.com/redis/go-redis/v9"
)

var client *redis.Client
var ctx context.Context

type Custom struct {
   
   
}

type Product struct {
   
   
}

func Init() {
   
   
    client = redis.NewClient(&redis.Options{
   
   
        Addr:     "localhost:6379",
        Password: "",
        DB:       0,
    })
    ctx = context.Background()
}

func (product *Product) AddMessage(key string, value any) error {
   
    //生产消息
    return client.LPush(ctx, key, value).Err()
}

func (custom *Custom) ConsumerMessage(key string) ([]string, error) {
   
   
    message, err := client.BRPop(ctx, 0, key).Result()
    return message, err
}
AI 代码解读

在解决完消息的有序性之后我们要面临的下一个问题就是如何避免重复的处理消息?我们可以给每一个消息加上一个全局唯一 ID,这样消费者在消费时可以把已经消费的消息id记录下来,每次即将消费新消息的时候进行对比,避免对已经处理的消息进行重复操作,这里我采用了雪花算法生成分布式id的方式来实现对全局唯一id的生成,有关雪花算法的相关操作就不赘述了,我之前的文章中也有所介绍,具体可以参考:go语言后端开发学习(六) ——基于雪花算法生成用户ID

我们来看一下具体的代码可以怎么写:

func Init() {
   
   
    client = redis.NewClient(&redis.Options{
   
   
        Addr:     "localhost:6379",
        Password: "",
        DB:       0,
    })
    ctx = context.Background()
    err := sony.Init()
    if err != nil {
   
   
        fmt.Println("Init sonyflake failed,err:", err)
    }
}

func (product *Product) AddMessage(key string, value any) error {
   
    //生产消息
    id, err := sony.GetID()
    if err != nil {
   
   
        return err
    }
    value = fmt.Sprintf("%d:%v", id, value) //添加id
    return client.LPush(ctx, key, value).Err()
}

func (custom *Custom) ConsumerMessage(key string) ([]string, error) {
   
   
    message, err := client.BRPop(ctx, 0, key).Result()
    id := custom.SplitMessage(message)
    if !custom.CheckId(id) {
   
   
        err := fmt.Errorf("id:%s is already processed", id)
        return nil, err
    }
    return message, err
}

func (custom *Custom) SplitMessage(message []string) string {
   
   
    str := strings.Split(message[1], ":")
    return str[0]
}

func (custom *Custom) CheckId(id string) bool {
   
    //检测id是否已经处理过
    for _, v := range idmap {
   
   
        if v == id {
   
   
            return false //该消息已经处理过了
        }
    }
    idmap = append(idmap, id) //添加到idmap
    return true
}
AI 代码解读

这里代码主要是添加了全局唯一id的生成,以及对id的解析与判断。

最后我们如何保证消息的可靠性呢?大家乍一听这个可能有点懵,这是什么意思?现在有一个情况,如果消费者程序从 List 中读取一条消息后,List 就不会再留存这条消息了。所以,如果消费者程序在处理消息的过程出现了故障或宕机,就会导致消息没有处理完成,那么,消费者程序再次启动后,就没法再次从 List 中读取消息了,那么我们如何解决就这样情况呢?

为了留存消息,List 类型提供了 BRPOPLPUSH命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存。
这样一来,如果消费者程序读了消息但没能正常处理,等它重启后,就可以从备份 List 中重新读取消息并进行处理了,实现也非常简单:

func (custom *Custom) ConsumerMessage(key1,key2 string) (string, error) {
   
   
    message, err := client.BRPopLPush(ctx, key1,key2,0).Result()
    id := custom.SplitMessage(message)
    if !custom.CheckId(id) {
   
   
        err := fmt.Errorf("id:%s is already processed", id)
        return "", err
    }
    return message, err
}
AI 代码解读

最后就有了我们最后的代码:

package main

import (
    "context"
    "fmt"
    "github.com/redis/go-redis/v9"
    sony "go-redis/sonyflake"
    "strings"
)

var client *redis.Client
var ctx context.Context

var idmap []string //不暴露到包外,避免被修改

type Custom struct {
   
   
}

type Product struct {
   
   
}

func Init() {
   
   
    client = redis.NewClient(&redis.Options{
   
   
        Addr:     "localhost:6379",
        Password: "",
        DB:       0,
    })
    ctx = context.Background()
    err := sony.Init()
    if err != nil {
   
   
        fmt.Println("Init sonyflake failed,err:", err)
    }
}

func (product *Product) AddMessage(key string, value any) error {
   
    //生产消息
    id, err := sony.GetID()
    if err != nil {
   
   
        return err
    }
    value = fmt.Sprintf("%d:%v", id, value) //添加id
    return client.LPush(ctx, key, value).Err()
}

func (custom *Custom) ConsumerMessage(key1, key2 string) (string, error) {
   
   
    message, err := client.BRPopLPush(ctx, key1, key2, 0).Result()
    id := custom.SplitMessage(message)
    if !custom.CheckId(id) {
   
   
        err := fmt.Errorf("id:%s is already processed", id)
        return "", err
    }
    return message, err
}

func (custom *Custom) SplitMessage(message string) string {
   
   
    str := strings.Split(message, ":")
    return str[0]
}

func (custom *Custom) CheckId(id string) bool {
   
    //检测id是否已经处理过
    for _, v := range idmap {
   
   
        if v == id {
   
   
            return false //该消息已经处理过了
        }
    }
    idmap = append(idmap, id) //添加到idmap
    return true
}

func main() {
   
     //测试样例
    Init() // 初始化 Redis 客户端和 Sonyflake

    product := &Product{
   
   }
    custom := &Custom{
   
   }

    // 测试数据
    testKey1 := "test-queue"
    testKey2 := "test-queue2"
    testValue := "Hello, world!"

    // 生产消息
    err := product.AddMessage(testKey1, testValue)
    if err != nil {
   
   
        fmt.Println("Failed to add message: %v", err)
    }

    // 消费消息
    message, err := custom.ConsumerMessage(testKey1, testKey2)
    if err != nil {
   
   
        fmt.Println("Failed to consume message: %v", err)
    }
    id := custom.SplitMessage(message)
    fmt.Println(id)
}
AI 代码解读

用List模拟消息队列缺点是比较多的,比如它不支持多个消费者消费同一条消息,因为一旦消费者拉取一条消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费,在后面我们会介绍Stream这一数据类型,我们到时候会基于它实现功能更加强大的消息队列。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
Go入门实战:并发模式的使用
本文详细探讨了Go语言的并发模式,包括Goroutine、Channel、Mutex和WaitGroup等核心概念。通过具体代码实例与详细解释,介绍了这些模式的原理及应用。同时分析了未来发展趋势与挑战,如更高效的并发控制、更好的并发安全及性能优化。Go语言凭借其优秀的并发性能,在现代编程中备受青睐。
116 33
Go语言实战指南 —— Go中的反射机制:reflect 包使用
Go语言中的反射机制通过`reflect`包实现,允许程序在运行时动态检查变量类型、获取或设置值、调用方法等。它适用于初中级开发者深入理解Go的动态能力,帮助构建通用工具、中间件和ORM系统等。
157 63
分布式爬虫框架Scrapy-Redis实战指南
本文介绍如何使用Scrapy-Redis构建分布式爬虫系统,采集携程平台上热门城市的酒店价格与评价信息。通过代理IP、Cookie和User-Agent设置规避反爬策略,实现高效数据抓取。结合价格动态趋势分析,助力酒店业优化市场策略、提升服务质量。技术架构涵盖Scrapy-Redis核心调度、代理中间件及数据解析存储,提供完整的技术路线图与代码示例。
414 0
分布式爬虫框架Scrapy-Redis实战指南
Redis 实操要点:Java 最新技术栈的实战解析
本文介绍了基于Spring Boot 3、Redis 7和Lettuce客户端的Redis高级应用实践。内容包括:1)现代Java项目集成Redis的配置方法;2)使用Redisson实现分布式可重入锁与公平锁;3)缓存模式解决方案,包括布隆过滤器防穿透和随机过期时间防雪崩;4)Redis数据结构的高级应用,如HyperLogLog统计UV和GeoHash处理地理位置。文章提供了详细的代码示例,涵盖Redis在分布式系统中的核心应用场景,特别适合需要处理高并发、分布式锁等问题的开发场景。
147 38
Redis数据类型面试给分情况
Redis常见数据类型包括:string、hash、list、set、zset(有序集合)。此外还包含高级结构如bitmap、hyperloglog、geo。不同场景可选用合适类型,如库存用string,对象存hash,列表用list,去重场景用set,排行用zset,签到用bitmap,统计访问量用hyperloglog,地理位置用geo。
19 5
Go语言实战案例-判断回文字符串-是不是正着念反着念都一样?
本案例实现判断回文字符串程序,支持中文、英文、标点及空格处理。通过双指针法与Unicode字符比较,帮助读者掌握字符串处理技巧与逻辑编程基础。
Go语言实战案例 - 找出切片中的最大值与最小值
本案例通过实现查找整数切片中的最大值与最小值,帮助初学者掌握遍历、比较和错误处理技巧,内容涵盖算法基础、应用场景及完整代码示例,适合初学者提升编程能力。
Go语言实战案例-读取用户输入并打印
本案例介绍如何使用Go语言的`fmt`包读取终端输入并输出信息。通过编写简单的交互程序,学习`fmt.Scanln()`、`fmt.Print()`和字符串拼接等基础I/O操作,适合初学者掌握命令行交互编程。
Go语言实战案例-字符串反转
本案例通过“字符串反转”任务,帮助初学者理解Go语言中字符串的本质、Unicode字符处理、切片操作及基本算法思想(如双指针法)。内容涵盖字符串反转的多种应用场景,如回文判断、加密解密等,并提供完整代码实现与解析,支持中文及特殊字符处理,避免乱码问题。同时介绍了错误示范与进阶优化方法,如封装成函数及泛型版本,适合拓展练习与深入学习。
|
6天前
|
Redis基本数据类型及Spring Data Redis应用
Redis 是开源高性能键值对数据库,支持 String、Hash、List、Set、Sorted Set 等数据结构,适用于缓存、消息队列、排行榜等场景。具备高性能、原子操作及丰富功能,是分布式系统核心组件。
91 2
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问