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

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

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

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

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

package mainimport ("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 mainimport ("context""github.com/redis/go-redis/v9"
)type contextkey stringvar 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 mainimport ("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)                    // 设置tokenrdb.MSet(ctx, "token2", "abcefghijklmn", "cookie1", "123456") // 设置多个keya := 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] thenreturn redis.call("del",KEYS[1])
elsereturn 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 mainimport ("context""github.com/redis/go-redis/v9"
)var client *redis.Client
var ctx context.Contexttype 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) //添加idreturn 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) //添加到idmapreturn 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 mainimport ("context""fmt""github.com/redis/go-redis/v9"sony "go-redis/sonyflake""strings"
)var client *redis.Client
var ctx context.Contextvar 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) //添加idreturn 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) //添加到idmapreturn true
}func main() {  //测试样例Init() // 初始化 Redis 客户端和 Sonyflakeproduct := &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.mzph.cn/diannao/52530.shtml

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

相关文章

如何在Java Maven项目中使用JUnit 5进行测试

如何在Java Maven项目中使用JUnit 5进行测试 1. 简介 JUnit 5概述 JUnit是Java编程语言中最流行的测试框架之一。JUnit 5是JUnit的最新版本,它引入了许多新特性和改进,使得编写和运行测试更加灵活和强大。 为什么选择JUnit 5 JUnit 5不仅提供了更强…

设计模式反模式:UML图示常见误用案例分析

第一章 引言 1.1 设计模式与反模式概述 在软件开发领域,设计模式与反模式是两种截然不同的概念,它们在软件设计过程中起着至关重要的作用。设计模式是经过验证的最佳实践,用于解决在特定上下文中经常出现的问题,从而提高软件的可…

《黑神话·悟空》是用什么编程语言开发的?

最近火爆全球的国产 3A 大作《黑神话悟空》,你玩了吗?没玩没关系,有人就是对游戏不感冒,我找了个宣发片,一起感受下3A大作的视觉冲击,而且还是我们从小听到大,那猴子🐒的故事。 ‌‌…

【Linux】自动化构建工具makefile

目录 背景 makefile简单编写 .PHONY makefile中常用选项 makefile的自动推导 背景 会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力 ​ ◉ 一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,mak…

Scrapy 项目部署Scrapyd

什么是Scrapyd Scrapyd 是一个用来管理和运行 Scrapy 爬虫的服务。它允许用户将 Scrapy 项目部署到服务器上,然后通过一个简单的 API 来启动、停止和监控爬虫的运行。Scrapyd 可以帮助简化爬虫的部署过程,使得用户不必手动在服务器上运行爬虫&#xff0c…

【测试】JMeter从入门到进阶

本文参考 Jmeter自动化测试工具从入门到进阶6小时搞定,适合手工测试同学学习_哔哩哔哩_bilibili JMeter介绍 JMeter 是 Apache 组织使用 Java 开发的一款测试工具: 1、可以用于对服务器、网络或对象模拟巨大的负载 2、通过创建带有断言的脚本来验证程序…

9个最流行的文本转语音引擎【TTS 2024】

在快速发展的技术世界中,文本转语音 (TTS) 引擎正在取得显著进步。从增强各种应用程序中的用户体验到创建逼真且引起情感共鸣的语音输出,TTS 引擎正变得不可或缺。在这里,我们介绍了 2024 年为行业树立新标准的九款最佳 TTS 引擎。 NSDT工具推…

应用层协议(上)Http(URL、Cookie、Session)内含逻辑图解通俗易懂!

绪论​ “少年没有乌托邦 心向远方自明朗”,本章是应用层常用且重要的协议htttp,没看过应用层建议一定先看那一篇后再看本章才能更好的去从上到下的理解应用层。 话不多说安全带系好,发车啦(建议电脑观看)。 1.Http协…

Mac移动硬盘选什么格式最好 Mac怎么用ntfs移动硬盘

在使用Mac电脑的过程中,很多用户可能有需要扩展存储空间的需求。选择合适的移动硬盘格式对于数据传输的效率和兼容性至关重要。本文将详细介绍Mac移动硬盘选什么格式好,以及Mac怎么用ntfs移动硬盘,帮助用户优化Mac的使用体验。 一、Mac移动硬…

悬浮翻译工具有哪些?工作学习必备的5款悬浮翻译工具

当我们身处异国他乡,或是工作中遇到多语种交流的需求时,语言障碍往往会成为一道难以逾越的高墙。 不过,在这个充满创新的时代里,技术已经为我们准备好了答案——屏幕翻译器app。它们不仅能够即时翻译屏幕上的文字,还能…

电脑回收站清空了怎么恢复?

在日常使用电脑的过程中,不小心清空回收站导致重要文件丢失的情况时有发生。面对这种情况,我们不必过于慌张,因为有多种方法可以尝试恢复被清空的文件。本文将为您详细介绍几种有效的恢复方法,帮助您找回宝贵的文件。 方法一&…

芯片后端之 PT 使用 report_timing 产生报告 之 -nets 选项

今天,我们再学习一点点 后仿真相关技能。 那就是,了解 report_timing 中的 -nets 选项 。 如果我们仅仅使用如下命令,执行后会发现: pt_shell> report_timing -from FF1/CK -to FF2/d -delay_type max 我们使用命令 report_timing 报出的如上路径延时信息,仅仅显示…

Maven的一些相关知识【重修】《包括私服搭建!》

mvnrepository.com Maven 下载jar包的位置! 【该部分有教程】 这是什么nb代码投稿视频-这是什么nb代码视频分享-哔哩哔哩视频 MAVEN 的私服搭建: https://zhuanlan.zhihu.com/p/520107316 2、maven私服搭建及应用(下)_哔哩…

R7RS标准之重要特性及用法实例(三十九)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 新书发布:《Android系统多媒体进阶实战》🚀 优质专栏: Audio工程师进阶系列…

frameworks 之InputReader

frameworks 之InputReader InputManagerService 初始化InputManagerService 启动InputReader 事件的读取设备节点注册和监听设备输入事件的读取 InputReader 事件的处理设备的添加和删除处理触摸事件的处理数据的加工和分发 android 输入事件 主要分 2个流程 事件读取 和 事件…

python的jieba库中文分词词频统计和合并

可能在设置问题模板的时候需要分析已有问句,然后统计词频,根据词频设计问题模板

31套科技风PPT免费分享

目录 部分展示 部分展示 #PPT下载 「科技风模板」链接:https://pan.quark.cn/s/fb2f39a1d343 链接永久有效,点击这里下载,记得给个赞哦

Java生成一个5位的随机验证码(大小写字母和数字)

生成验证码 内容:可以是小写字母,也可以是大写字母,还可以是数字 规则:长度为5 内容中四位字母,一位数字 其中数字只有一位,但是可以出现在任意位置。 package test;impo…

QT error: expected ‘:‘ before ‘slots‘ public slots:

C:\Users\Administrator\Desktop\VideoHill\GikISearch\net.h:10: error: expected : before slots public slots: 先看看头文件里有没有加上引用包含#include <xxxx> 也就是一个引用包含都没有 没有就会报这个。至少一个。 加上后

【架构-24】XML和JSON

XML&#xff08;可扩展标记语言&#xff09;和JSON&#xff08;JavaScript对象表示法&#xff09;是两种常用的数据格式&#xff0c;用于在不同系统之间传输和交换数据。它们各有优点和缺点&#xff0c;适用于不同的场景。下面是对XML和JSON的简要介绍以及它们之间的对比。 XM…