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

news2025/1/12 16:17:08

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

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

go get github.com/redis/go-redis/v9

最后我们尝试一下连接本机的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)
}

输出结果为:

value

同时,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)
}

二.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) //扫描

三. 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())
}

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())

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

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

分布式锁加锁命令如下:

set lock uuid NX PX 10000

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

这样一来,就通过使用 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")

关于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
}

在解决完消息的有序性之后我们要面临的下一个问题就是如何避免重复的处理消息?我们可以给每一个消息加上一个全局唯一 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
}

这里代码主要是添加了全局唯一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
}

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

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)
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2064719.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

扫码点餐系统小程序功能分析

扫码点餐系统小程序通常具备以下核心功能: 用户界面:提供直观易用的界面,方便用户浏览菜单、选择菜品、查看订单状态等 。菜单展示:展示餐厅的菜单,包括菜品图片、价格、描述等信息 。扫码点餐:用户通过…

warning: implicit declaration of function ‘m‘ is invalid in C99

编译报错: implicit declaration of function ‘m’ is invalid in C99 即 函数 “m” 的隐式声明在C99中无效 原因: C语言是过程化的编程语言,程序执行顺序是从上到下。函数调用需要先声明后调用。 C99 默认不允许隐式声明(1999年推出的c语言标准)。 …

悟空,中国神话再一次惊艳世界

大家好,我是小悟 这两天《黑神话:悟空》刚上线,是非常的火。刚进入游戏,犹如揭开一幅绚丽多彩的画卷,展现了国产游戏在技术创新与艺术追求上的新高度。这款游戏不仅仅是一款动作角色扮演游戏,更是技术与艺术…

zoom 会议机器人web例子

一、需要创建zoom app,创建及配置参考:Zoom会议机器人转写例子-CSDN博客 这里直接使用zoom-recall的配置。 二、需要生成签名,参数为:zoom-recall中的Client ID和Client Secret 1、git clone https://github.com/zoom/meetings…

大模型学习笔记 - LLM 之 attention 优化

LLM 注意力机制 LLM 注意力机制 1. 注意力机制类型概述2.Group Query Attention3.FlashAttention4. PageAttention 1. 注意力机制类型概述 注意力机制最早来源于Transformer,Transformer中的注意力机制分为2种 Encoder中的 全量注意力机制和 Decoder中的带mask的…

磁编码器原理与方案

系列文章目录 1.元件基础 2.电路设计 3.PCB设计 4.元件焊接 5.板子调试 6.程序设计 7.算法学习 8.编写exe 9.检测标准 10.项目举例 11.职业规划 文章目录 前言一、磁编码器二、多极磁编三、绝对值磁编四、单极对磁编五、磁游标编码器六、磁刻线编码器七、磁编码器优点八、磁…

46.x86游戏实战-DXX封包实现进入地图房间

免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动! 本次游戏没法给 内容参考于:微尘网络安全 工具下载: 链接:https://pan.baidu.com/s/1rEEJnt85npn7N38Ai0_F2Q?pwd6tw3 提…

windows所有功能都可使用就是电脑黑屏了

运行新任务然后输入explorer.exe勾选上创建任务确定就好了 下次尽量不手欠!!!!

21. Lammps命令学习-6之read_data

来源: “码农不会写诗”公众号 链接:Lammps命令学习-6之read_data read_data file keyword args … 读取包含LAMMPS运行模拟所需信息的data文件   https://docs.lammps.org/read_data.html Syntax read_data file keyword args ...Description 读取包…

推荐算法实战-五-召回(上)

一、传统召回算法 (一)基于物料属性的倒排索引 在离线时,将具有相同属性的物料集合起来,根据一些后验统计指标将物料排序。 当一个用户在线交互发出请求后,提取用户的兴趣标签,根据标签检索相应物料集合…

24吉林成考报名明日开始,注意照片规格

24吉林成考报名明日开始,注意照片规格 #吉林成考 #成考报名 #成考报名照片 #成人高考报名 #成人高考 #成人高考报名照片

CANoe.DiVa的应用——Diva进行诊断自动化测试执行过程详解(三)

🙋‍♂️【Vector CANdelastudio配置CDD】文章合集💁‍♂️点击跳转 ——————————————————————————————————–—— 从0开始学习CANoe使用 从0开始学习车载测试 相信时间的力量 星光不负赶路者,时光不负有心人。 目录 1.工程导入2.查看用…

SpringBoot3 简单集成 Spring AI 并使用

文章目录 准备JDK17api key 创建项目编写配置文件创建controller启动并测试角色预设流式响应\异步响应ChatModel(聊天模型)ImageModel(文生图)文生语音语言翻译多模态Function Calling (函数调用第三方API)…

小折叠手机首获120万次弯折耐久认证,意味着什么?

提到折叠屏手机,很多人都会担心其耐用性。毕竟,频繁的开合对铰链和屏幕都是极大的考验。但华为nova Flip却用实力证明,耐用性不再是问题。凭借玄武水滴铰链技术,首获瑞士SGS 120万次弯折耐久认证的小折叠屏手机。 一、120万次弯折…

springboot中后缀匹配模式useSuffixPatternMatch、useTrailingSlashMatch的源码匹配分析

背景: 上篇文章,已经说了,如果我们直接debug调试没法找到源码中具体的代码,那么就可以通过jd-gui反编译的方式通过搜关键词的方式来找到源码中具体的位置,这次简单说下spring中的两种后缀匹配模式useSuffixPatternMat…

ArcGIS Pro基础:并行处理设置

如上所示,随便打开一个工具,找到【环境】设置,然后就可以找到【并行处理因子】的设置。 ArcGIS 软件里的并行处理因子一般是空的,这是默认设置,表达的是意思由是各个工具【自行决定】使用进程的数量。 其他情况&…

redis面试(十九)读写锁ReadLock

读写锁ReadLock 简单来说就是互斥锁和非互斥锁。多个客户端可以同事加的锁叫读锁,只能有一个客户端加的锁叫写锁。这个理论应该是从数据库中来的,放在这里也是同样的解释。 多个客户端同时加读锁,是不会互斥的,多个客户端可以同…

【Excel】Excel小技巧(实时更新中)- 0.0.1

文章目录 单元格操作多列数据"".join()为一个 END 单元格操作 多列数据"".join()为一个 END

Python保留字符

Python中的保留字。 这些保留字不能用作常数或变数,或任何其他标识符名称。 所有 Python 的关键字只包含小写字母。

Java 中边读数据库边写到文件,可以处理大型数据文件而不会消耗太多内存。

从数据库查询结果集中提取单行数据,并按列格式化为字符串形式。主要功能如下: 遍历结果集的每一列。 根据不同的数据类型转换列值为字符串。 使用StringBuilder拼接各列值,列间以colSeparator分隔。 替换字符串中的换行符\n和回车符\r为空字符…