实现mini-redis字符串操作

news2025/1/11 0:25:17

写在文章开头

在之前的系列文章中,我们通过命令行模式完成了mini-redis解析和处理指令的执行基调,这篇文章笔者将对mini-redis中存储字符串的set和get指令的设计和实现进行分析讲解,希望对你了解mini-redis有所帮助。

在这里插入图片描述

Hi,我是 sharkChili ,是个不断在硬核技术上作死的技术人,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

详解mini-redis如何设计和实现字符串操作指令

设计思路

要想完全复刻redis的字符串操作指令,我们就必须了解每个指令的作用和含义,先来说说set指令,按照redis官网的说明,set指令大体有5种操作:

  1. 常规set操作:指令示例为set k vkeyk,意为值为v的键值对存入客户端所设置的redis数据库(默认为0)中。
  2. 参数nx:指令示例为set k v nx,当k不存在redis客户端所指向的数据库中时,将kv键值对插入数据库。
  3. 参数xx:指令示例为set k v xx即当k存在于redis客户端所指向的数据库时,修改这个k的值。
  4. 参数ex:指令示例为set k v ex x,通过参数ex设置,存入数据库的key会在x秒后删除。
  5. 参数px:指令示例为set k v px x,通过参数px设置,存入数据库的key会在x毫秒后删除。

get指令就比较简单了,基于redis-cli所指向的数据库查询要查询的key是否存在,如果存在则返回字符串,反之返回nil

了解了所有指令的含义之后,我们就来介绍介绍mini-redis的实现思路了,对于set指令,mini-redis通过字符串解析后得到set指令,从将参数传入到函数setCommand进行处理。而setCommand处理逻辑比较简单,由于索引0、1、2分别对应set key value,所以我们只需从索引3开始遍历查看当前传入的指令是否存在特殊参数(如nx、ex等)并进行标记。

以下图为例,我们redis-cli键入的指令为set k v ex 3,跳过argv数组中0、1、2几个常规set指令必传的字符串,得到ex和键值对存活的数值3,这意味着存储的kv会在3秒后过期。

解析到键值对kv之后,定位到当前客户端指针所指向的数据库db(默认为0),先将其存储到数据库结构体记录键值对的字典dict中,然后基于ex后面的参数得到期时间并以k作为键,到期时间作为value将其其存到expires字典中:

在这里插入图片描述

后续我们在通过get指令获取参数时,首先会看到expires字典中查看这个key是否过期,如果过期则将dict字典中的键值对删除,返回给客户端nil,反之返回键值对的结果:

在这里插入图片描述

落地set指令

经过上述的分析,我们就可以进行代码落地了,这里笔者给出mini-redisset指令处理函数setCommand,可以看到笔者从索引3开始遍历特殊参数,基于各个字符串例如nxxx进行指令特殊处理标识,如果遇到expx则读取后一位参数,以ex为例就算将unit单位设置为秒然后读取后一位参数值,最后将这些参数全部传入setGenericCommand进行内存持久化操作:


func setCommand(c *redisClient) {
	var j uint64
	var expire string
	unit := UNIT_SECONDS
	flags := REDIS_SET_NO_FLAGS
	//traverse the arguments after setting the key-value pair in a set.
	for j = 3; j < c.argc; j++ {
		a := c.argv[j]
		var next string
		if j == c.argc-1 {
			next = ""
		} else {
			next = c.argv[j+1]
		}
		// if the string "nx" is included, mark through bitwise operations that the current key can only be set when it does not exist.
		if strings.ToLower(a) == "nx" {
			flags |= REDIS_SET_NX
		} else if strings.ToLower(a) == "xx" { //if "xx" is included, mark the flags to indicate that the key can only be set if it already exists.
			flags |= REDIS_SET_XX
		} else if strings.ToLower(a) == "ex" { //if it is "ex", set the unit to seconds and read the next parameter.
			unit = UNIT_SECONDS
			expire = next
			j++
		} else if strings.ToLower(a) == "px" { //if it is "px", set the unit to milliseconds and read the next parameter.
			unit = UNIT_MILLISECONDS
			expire = next
			j++
		} else { // Treat all other cases as exceptions.
			addReply(c, shared.syntaxerr)
			return
		}
	}
	//pass the key, value, instruction identifier flags, and expiration time unit into `setGenericCommand` for memory persistence operation.
	setGenericCommand(c, flags, c.argv[1], c.argv[2], expire, unit, "", "")
}

对应我们给出setGenericCommand指令的核心流程,它会基于上一步的入参判断是否存在expire ,如果存在则将其转为整数,后续会基于当前时间计算到期时间并以传入key作为键,到期时间作为值存入expires字典中。随后在进行标记判断,通过上一步位运算的标记,只有符合以下某种情况则不进行键值对插入:

  1. 指令包含nx且调用lookupKeyWrite查看当前数据库包含这个键值对。
  2. 指令包含xx且调用lookupKeyWrite查看数据库不包含这个键值对。

完成这些校验之后,进行键值对、到期时间信息写入内存中:

func setGenericCommand(c *redisClient, flags int, key string, val string, expire string, unit int, ok_reply string, abort_reply string) {
	//initialize a pointer to record the expiration time in milliseconds.
	var milliseconds *int64
	milliseconds = new(int64)
	//if `expire` is not empty, parse it as an int64 and store it in `milliseconds`.
	if expire != "" {
		if getLongLongFromObjectOrReply(c, expire, milliseconds, "") != REDIS_OK {
			return
		}

		if unit == UNIT_SECONDS {
			*milliseconds = *milliseconds * 1000
		}
	}
	/**
	the following two cases will no longer undergo key-value persistence operations:
	   1. if the command contains "nx" and the data exists for this value.
	   2. if the command contains "xx" and the data for this value does not exist.
	*/
	if (flags&REDIS_SET_NX > 0 && *lookupKeyWrite(c.db, key) != nil) ||
		(flags&REDIS_SET_XX > 0 && *lookupKeyWrite(c.db, key) == nil) {
		addReply(c, shared.nullbulk)
		return
	}
	//if `expire` is not empty, add the converted value to the current time to obtain the expiration time. Then,
	//use the passed key as the key and the expiration time as the value to store in the `expires` dictionary.
	if expire != "" {
		c.db.expires[key] = time.Now().UnixMilli() + *milliseconds
	}
	//store the key-value pair in a dictionary.
	c.db.dict[key] = val

	addReply(c, shared.ok)
}

而判断是否存在的函数lookupKeyWrite逻辑比较简单,调用expireIfNeeded查看当前key是否过期如果过期则删除,然后在调用lookupKey返回查询结果:

func lookupKeyWrite(db *redisDb, key string) *interface{} {
	//check if the key has expired, and if so, delete it.
	expireIfNeeded(db, key)
	//query the dictionary for the value corresponding to the key.
	return lookupKey(db, key)
}

对此我们也给出expireIfNeeded的逻辑,可以看到我们会从expires拿到这个key的过期时间,如果这个过期时间大于当前时间,则说明key没有过期直接返回,反之则说明key过期,调用deDelete将dict中的key删除:

func expireIfNeeded(db *redisDb, key string) int {
	//get the expiration time of the key.
	when, exists := db.expires[key]
	if !exists {
		return 0
	}
	if when < 0 {
		return 0
	}
	now := time.Now().UnixMilli()
	//if the current time is less than the expiration time, it means the current key has not expired, so return directly.
	if now < when {
		return 0
	}
	//delete expired keys.
	deDelete(db, key)

	return 1

}

落地get指令

get指令就比较简单了,直接调用getGenericCommand其内部调用lookupKeyReadOrReply检查当前key是否到期,如果到期则将这个键值对删除,然后再到dict中查询键值对是否存在,如果存在则调用addReplyBulk将结果写给客户端:

func getCommand(c *redisClient) {
	getGenericCommand(c)
}

func getGenericCommand(c *redisClient) int {
	//check if the key exists, and if it does not, return a null bulk response from the constant values.
	o := lookupKeyReadOrReply(c, c.argv[1], &shared.nullbulk)
	if *o == nil {
		return REDIS_OK
	}
	//return the value to the client if it exists.
	val := (*o).(string)
	addReplyBulk(c, &val)
	return REDIS_OK
}

lookupKeyRead内部逻辑也是先检查key是否到期,然后调用lookupKey查询返回值:

func lookupKeyRead(db *redisDb, key string) *interface{} {
	//check if the key has expired and delete it.
	expireIfNeeded(db, key)
	val := lookupKey(db, key)
	return val
}

测试落地结果

我们启动mini-redis,通过redis-cli键入常规字符串设置指令,调用get可以得到存储的结果:

127.0.0.1:6379> set k v
OK
127.0.0.1:6379> get k
"v"

设置时效后,key到期后查询就返回nil

127.0.0.1:6379> set key v ex 10
OK
127.0.0.1:6379> get key
"v"
127.0.0.1:6379> get key
(nil)

使用nx插入存在的键值对,直接返回nil:

127.0.0.1:6379> set k v nx
(nil)
127.0.0.1:6379>

小结

自此我们将mini-redis中字符串操作指令的实现思路和落地代码核心部分都进行了详细分析,希望对你阅读笔者的源码有所帮助。这里笔者也给出源码地址:

https://github.com/shark-ctrl/mini-redis

我是 sharkchiliCSDN Java 领域博客专家mini-redis的作者,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

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

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

相关文章

94 、k8s之rbac

一、rbac----安全机制 赋权机制 集群是按照用户名进行登录&#xff0c;按照项目名称进行命名空间的分类。 配电云主站------62天 8个人 高温补贴 一主2从 user pdyzz pdyzz -n pdyzz 资源空间 pod数量 1.1、k8s的安全机制&#xff1a; apiserver------>集群内和外…

S3C2440开发板点亮LED灯+PWM定时器

目录 GPIO引脚和寄存器概述 点亮LED灯步骤 1.配置GPIO 2.点亮LED 设置引脚为输出 控制引脚电平 完整代码 PWM GPIO引脚和寄存器概述 GPIO端口&#xff1a; S3C2440的GPIO引脚可被配置为输入或输出&#xff08;控制LED的引脚通常配置为输出模式&#xff09;。寄存器&#…

从LoRA到QLoRA:量化技术如何改变预训练模型的微调方式

在现代人工智能的发展中&#xff0c;预训练语言模型&#xff08;LLM&#xff09;已成为各种自然语言处理任务中的关键技术。这些模型通常具有数十亿甚至数千亿的参数&#xff0c;因此需要巨大的计算和存储资源来进行微调。QLoRA提出了一种新方法&#xff0c;使得在单个48GB的GP…

“这年头,只依赖上班,是赚不到钱的——揭秘如何利用AI开启赚钱新模式“

苹果&#xff0c;在AI时代终于要有大动作了。 反观国内华为&#xff0c;前段时间刚上线的新款平板MatePad Air也大放异彩&#xff0c;搭载AI助手&#xff0c;创新生产力。 像我这写文案的时常灵感枯竭&#xff0c;打开电脑却迟迟下不了手…而华为小艺帮写功能只需要输入指令就…

【C\C++】Eigen初体验(VS Code编译)

Eigen Eigen 是一个高效的 C 库&#xff0c;专注于线性代数运算。PCL 使用 Eigen 来处理矩阵和向量运算&#xff0c;特别是在点云数据的变换、配准和特征计算等方面。Eigen 提供了高效的矩阵运算功能&#xff0c;使得 PCL 在处理大规模点云数据时能够保持高性能。 Eigen 使用…

如何使用Chainlit让所有网站快速嵌入一个AI聊天助手Copilot

Copilot 副驾驶 Software Copilot 是嵌入到您的应用/产品中的一种新型助手。它们旨在通过提供情境指导并代表用户采取行动来帮助用户充分利用您的应用。 支持的功能 信息流媒体元素声音的询问用户聊天记录聊天资料反馈✅✅✅✅✅❌✅✅ 嵌入 Copilot 首先&#xff0c;确保您…

团队协作必备:2025年10大企业知识库管理系统工具推荐

2025年团队协作知识库工具合集TOP 10 在团队协作和知识管理日益重要的今天&#xff0c;选择一款高效、灵活的知识库工具对于提升工作效率至关重要。以下是精心挑选的2025年团队协作知识库工具合集TOP 10&#xff0c;每款工具都具备独特的功能和优势&#xff0c;以满足不同团队…

基于鸿蒙API10的RTSP播放器(一:基本界面的实现)

ijkplayer简介&#xff1a; ijkplayer 本身是一个开源的 Android 媒体播放库&#xff0c;它主要用于播放视频和音频文件&#xff0c;现在已有前辈将其引入到鸿蒙当中&#xff0c;通过XComponent组件完成适配。向开源致敬&#xff01; 支持格式&#xff1a; 它支持多种格式&…

wine 麒麟系统运行Windows编译的exe

启动终端 首先,我们需要打开终端程序。 在终端中,输入以下命令: sudo apt-get update这个命令通常会要求输入密码,输入你的密码后,就可以继续执行了。 如果你想要切换到 root 用户,可以通过以下方法: su 用户名然后输入 root 用户的密码。 如果你想要重置 root 用户…

网站如何防范BOT流量?

随着互联网的快速发展&#xff0c;BOT流量已成为网络安全领域的一大隐患。BOT&#xff0c;即自动化程序&#xff0c;它们在网络中执行各种任务&#xff0c;包括数据抓取、恶意抢购、暴力破解等。这些行为不仅威胁到网站的正常运营&#xff0c;还可能造成数据泄露、经济损失等严…

day20JS-axios数据通信

1. 什么是axios axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端&#xff0c;简单的理解就是ajax的封装&#xff0c;只不过它是Promise的实现版本。 特性&#xff1a; 从浏览器中创建 XMLHttpRequests从 node.js 创建 http 请求支持 Promise API拦截请求和响应转…

零宽字符应用场景及前端解决方案

零宽字符&#xff08;Zero Width Characters&#xff09;是一类在文本中不可见但具有特定功能的特殊字符。称为零宽字符&#xff0c;也叫幽灵字符。它们在显示时不占据任何空间&#xff0c;但在文本处理和显示中发挥着重要作用。这些字符主要包括零宽度空格、零宽度非连接符、零…

【C++ 高频面试题】STL 你了解多少呢?vector 的底层实现原理

文章目录 1. 常见的 STL 容器2.vector 和 list 的区别3. vector 的底层原理4. push_back() 和 emplace_back() 区别 1. 常见的 STL 容器 &#x1f34e;①序列容器 vector&#xff08;向量&#xff09;&#xff1a;是一个动态数组实现&#xff0c;提供高效的随机访问和在尾部进行…

基于R语言结构方程模型分析与实践技术应用

结构方程模型&#xff08;Sructural Equation Model&#xff09;是一种建立、估计和检验研究系统中多变量间因果关系的模型方法&#xff0c;它可以替代多元回归、因子分析、协方差分析等方法&#xff0c;利用图形化模型方式清晰展示研究系统中变量间的因果网络关系&#xff0c;…

信息安全数学基础(9)素数的算数基本定理

前言 在信息安全数学基础中&#xff0c;素数的算数基本定理&#xff08;也称为唯一分解定理或算术基本定理&#xff09;是一个极其重要的定理&#xff0c;它描述了正整数如何唯一地分解为素数的乘积。这个定理不仅是数论的基础&#xff0c;也是许多密码学算法&#xff08;如RSA…

【算法专场】分治(上)

目录 前言 什么是分治&#xff1f; 75. 颜色分类 算法分析 算法步骤 算法代码 912. 排序数组 - 力扣&#xff08;LeetCode&#xff09; 算法分析 算法步骤 算法代码 215. 数组中的第K个最大元素 - 力扣&#xff08;LeetCode&#xff09; 算法分析 算法步骤 ​编辑…

【Redis入门到精通一】什么是Redis?

目录 Redis 1. Redis的背景知识 2.Redis特性 3.Redis的使用场景 4.Ubuntu上安装配置Redis Redis Redis在当今编程技术中的地位可以说非常重要&#xff0c;大多数互联网公司内部都在使用这个技术&#xff0c;熟练使用Redis已经成为开发人员的一个必备技能。本章将带领读者…

yolov5实战全部流程

本科生阶段除了在中等以上的985和某些特定的CS强项院校&#xff0c;无意愿研究生学习的本科生是不建议学习人工智能这一专业的&#xff0c;保研学生也许可以在实验室打工推荐信学习接触到此类事件&#xff0c;此项blog主要是对yolov5的实践性项目&#xff0c;yolov5作为最具有代…

python 函数 封装

封装 函数的参数是&#xff1a;变量 def 函数(参数):print(参数)if __name__ __main__:函数(参数)函数(参数2)函数的参数是&#xff1a; 字典 import requests# 定义一个字典 data {} 地址 "https://webdriveruniversity.com/" 请求方法 getdata["url"…

【数据结构】选择题错题集

这里注意原本p后面也是有节点的。 这里只有遍历前面的链表找到尾节点连接即可。 快排是交换排序。 不要想象只有这两个节点&#xff0c;还有其他节点省略了。 筛选法就是向下调整算法。用向下调整建堆从最后一个节点的父亲开始。 这里错位相减法是乘4&#xff0c;所以最后要除三…