• 详解 mini-redis 复刻 Redis 的 I NCR 指令

详解 mini-redis 复刻 Redis 的 I NCR 指令

2025-04-27 10:39:49 栏目:宝塔面板 46 阅读

因为近期比较忙碌,所以对于mini-redis的复刻基本处于一些指令向的完善,而本文将针对字符串操作中介绍笔者近期所复刻的键值自增指令的落地思路,以帮助读者更好的理解和学习mini-redis。

对象类型前置校验

因为指令是基于字符串操作的,所以在执行INCR或者DECR之前我们都必须针对入参的键值对进行校验,所以对于以下情况,我们都必须采用fail-fast的方式提前将失败暴露,将键值对已存在,对应的值非字符串类型(例如:字典类型),直接响应错误:

基于上述的基本概念,我们给出落地的代码,即位于command.go的incrDecrCommand方法,可以看到我们会优先到redis内存中查看是否存在对应的key,如果存在则进行必要的类型判断,如果非字符串类型即REDIS_STRING则直接响应错误出去,并直接返回:

func incrDecrCommand(c *redisClient, incr int64) {
 var value int64
 var oldValue int64
 var newObj *robj
 //查看键值对是否存在
 o := lookupKeyWrite(c.db, c.argv[1])
 //如果键值对存在且类型非字符串类型,直接响应错误并返回
 if o != nil && checkType(c, o, REDIS_STRING) {
  return
 }
 
 
 //......

}

对此我们也给出checkType的内部逻辑,可以看到当比对类型不一致时会直接输出错误并返回true,读者可以参考注释了解:

func checkType(c *redisClient, o *robj, rType int) bool {
 //如果类型不一致,则输出-WRONGTYPE Operation against a key holding the wrong kind of value
 if o.robjType != rType {
  addReply(c, shared.wrongtypeerr)
  return true
 }
 return false
}

其实笔者这里也想吐槽一句redis对于函数设计的语义的不恰当性,理论性合理的函数进行校验时正确的做法应该是:

  • 逻辑校验失败,输出错误返回false。
  • 逻辑校验正确,返回true。

也只能说因为某些历史原因,或者设计者有着自己的主观编码习惯吧,本着一比一的复刻理念,笔者也沿袭了这样的编码思路。

基于数值池高效完成字符串转换

针对字符串类型(可以转数值的情况下,它也会转数值类型),我们都是通过robj类型创建和维护,因为我们本次所复刻的incr和decr所操作的类型是字符串中可转为数值的对象,所以本着数值类型有迹可循的规律以及空间换时间的思想,我们提出池化思想,即将0-9999数值缓存一份数值池,后续的增减操作后处于该范围的数值都可以直接使用数值池里对应的robj对象,以节约robj对象创建的开销和非必要的内存资源占用:

所以笔者在main.go中声明sharedObjectsStruct 这个结构体中声明了一个integers维护常量池的robj对象:

type sharedObjectsStruct struct {
 //......
 integers       [REDIS_SHARED_INTEGERS]*robj //通用0~9999常量数值池
 //......
}

然后在createSharedObjects方法中完成初始化,后续就可以直接使用了:

func createSharedObjects() {
 //......

 var i int64
 //初始化常量池对象
 for i = 0; i < REDIS_SHARED_INTEGERS; i++ {
  //基于接口封装数值
  num := interface{}(i)
  //生成string对象
  shared.integers[i] = createObject(REDIS_STRING, &num)
  //声明编码类型为int
  shared.integers[i].encoding = REDIS_ENCODING_INT
 }

 //......
}

于是我们就得出了后续的编码逻辑:

  • 将value强转为数值判断是否超出范围,如果超了则抛出异常。反之进入步骤2。
  • 查看取值范围是否大于10000,如果是则自己生成robj对象,反之采用池化数值池的robj。
  • 基于1、2生成的数值对象将键值对更新或者覆盖到内存数据库中。
/**
 针对字符串类型的值进行如下判断的和转换:
 1. 如果为空,说明本次的key不存在,直接初始化一个空字符串,后续会直接初始化一个0值使用
 2. 如果是字符串类型,则转为字符串类型
 3. 如果是数值类型,则先转为字符串类型进行后续的通用数值转换操作保证一致性
 */
 var s string
 if o == nil {
  s = ""
 } else if isString(*o.ptr) {
  s = (*o.ptr).(string)
 } else {
  s = strconv.FormatInt((*o.ptr).(int64), 10)
 }
 //进行类型强转为数值,如果失败,直接输出错误并返回
 if getLongLongFromObjectOrReply(c, s, &value, nil) != REDIS_OK {
  return
 }

 oldValue = value
 //如果累加超范围则报错
 if (incr < 0 && oldValue < 0 && incr < (math.MinInt64-oldValue)) ||
  (incr > 0 && oldValue > 0 && incr > (math.MaxInt64-oldValue)) {
  errReply := "increment or decrement would overflow"
  addReplyError(c, &errReply)
  return
 }
 //基于incr累加的值生成value
 value += incr
 //如果超常量池范围则封装一个对象使用 
 if o != nil &&
  (value < 0 || value >= REDIS_SHARED_INTEGERS) &&
  (value > math.MinInt64 || value < math.MaxInt64) {
  newObj = o

  i := interface{}(value)
  o.ptr = &i
 } else if o != nil {//如果对象存在,且累加结果没超范围则调用createStringObjectFromLongLong获取常量对象
  newObj = createStringObjectFromLongLong(value)
  //将写入结果覆盖
  dbOverwrite(c.db, c.argv[1], newObj)
 } else {//从常量池获取数值,然后添加键值对到数据库中
  newObj = createStringObjectFromLongLong(value)
  dbAdd(c.db, c.argv[1], newObj)
 }

通用结果响应

完成上述操作后就是将结果按照RESP协议规范将结果响应给客户端,按照协议要求数值类型必须用:号开头,所以假设我们累加结果为10,那么响应给客户端的结果就是10 。

对应我们的给出最后的代码段:

//将累加后的结果返回给客户端,按照RESP格式即 :数值
,例如返回10 那么格式就是:10

 reply := *shared.colon + strconv.FormatInt(value, 10) + *shared.crlf
 addReply(c, &reply)

完整的代码实现

我们来小结一下上述的实现思路:

  • 键值对查询与校验。
  • 数值类型转换与越界判断。
  • 字符串类型强转并基于取值范围查看是否通过数值池获取。
  • 更新或覆盖键值对。
  • 将操作结果返回客户端。

完整代码如下:

func incrDecrCommand(c *redisClient, incr int64) {
 var value int64
 var oldValue int64
 var newObj *robj
 //查看键值对是否存在
 o := lookupKeyWrite(c.db, c.argv[1])
 //如果键值对存在且类型非字符串类型,直接响应错误并返回
 if o != nil && checkType(c, o, REDIS_STRING) {
  return
 }
 /**
 针对字符串类型的值进行如下判断的和转换:
 1. 如果为空,说明本次的key不存在,直接初始化一个空字符串,后续会直接初始化一个0值使用
 2. 如果是字符串类型,则转为字符串类型
 3. 如果是数值类型,则先转为字符串类型进行后续的通用数值转换操作保证一致性
 */
 var s string
 if o == nil {
  s = ""
 } else if isString(*o.ptr) {
  s = (*o.ptr).(string)
 } else {
  s = strconv.FormatInt((*o.ptr).(int64), 10)
 }
 //进行类型强转为数值,如果失败,直接输出错误并返回
 if getLongLongFromObjectOrReply(c, s, &value, nil) != REDIS_OK {
  return
 }

 oldValue = value

 if (incr < 0 && oldValue < 0 && incr < (math.MinInt64-oldValue)) ||
  (incr > 0 && oldValue > 0 && incr > (math.MaxInt64-oldValue)) {
  errReply := "increment or decrement would overflow"
  addReplyError(c, &errReply)
  return
 }
 //基于incr累加的值生成value
 value += incr
 //如果超常量池范围则封装一个对象使用
 if o != nil &&
  (value < 0 || value >= REDIS_SHARED_INTEGERS) &&
  (value > math.MinInt64 || value < math.MaxInt64) {
  newObj = o

  i := interface{}(value)
  o.ptr = &i
 } else if o != nil { //如果对象存在,且累加结果没超范围则调用createStringObjectFromLongLong获取常量对象
  newObj = createStringObjectFromLongLong(value)
  //将写入结果覆盖
  dbOverwrite(c.db, c.argv[1], newObj)
 } else { //从常量池获取数值,然后添加键值对到数据库中
  newObj = createStringObjectFromLongLong(value)
  dbAdd(c.db, c.argv[1], newObj)
 }
 //将累加后的结果返回给客户端,按照RESP格式即 :数值
,例如返回10 那么格式就是:10

 reply := *shared.colon + strconv.FormatInt(value, 10) + *shared.crlf
 addReply(c, &reply)

}

递增递减的复用

基于上述函数对应的递增指令INCR就使用incrCommand,入参传1代表加1,而decrCommand则传-1扣减即可:

func incrCommand(c *redisClient) {
 //累加1
 incrDecrCommand(c, 1)
}

func decrCommand(c *redisClient) {
 //递减1
 incrDecrCommand(c, -1)
}

最终效果演示

最后,我们将服务启动进行测试,可以看到指令正常执行:

127.0.0.1:6379> incr k1
(integer) 1
(4.50s)
127.0.0.1:6379> incr k1
(integer) 2
127.0.0.1:6379> incr k1
(integer) 3
127.0.0.1:6379> incr k1
(integer) 4
127.0.0.1:6379> incr k1
(integer) 5
127.0.0.1:6379> incr k1
(integer) 6
127.0.0.1:6379> decr k1
(integer) 5
127.0.0.1:6379> decr k1
(integer) 4
127.0.0.1:6379> decr k1
(integer) 3
127.0.0.1:6379> decr k1
(integer) 2
127.0.0.1:6379> decr k1
(integer) 1
127.0.0.1:6379> decr k1
(integer) 0
127.0.0.1:6379> decr k1
(integer) -1
127.0.0.1:6379>


本文地址:https://www.yitenyun.com/146.html

搜索文章

Tags

数据库 API FastAPI Calcite 电商系统 MySQL 数据同步 ACK Web 应用 异步数据库 双主架构 循环复制 Deepseek 宝塔面板 Linux宝塔 Docker 生命周期 序列 核心机制 JumpServer JumpServer安装 堡垒机安装 Linux安装JumpServer esxi esxi6 root密码不对 无法登录 web无法登录 Windows Windows server net3.5 .NET 安装出错 宝塔面板打不开 宝塔面板无法访问 SSL 堡垒机 跳板机 HTTPS 查看硬件 Linux查看硬件 Linux查看CPU Linux查看内存 无法访问宝塔面板 Windows宝塔 Mysql重置密码 HTTPS加密 连接控制 机制 ES 协同 Oracle 处理机制 Serverless 无服务器 语言 OB 单机版 scp Linux的scp怎么用 scp上传 scp下载 scp命令 Spring SQL 动态查询 技术 存储 缓存方案 缓存架构 缓存穿透 运维 索引 架构 InnoDB 分页查询 日志文件 MIXED 3 Rsync 响应模型 修改DNS Centos7如何修改DNS RocketMQ 长轮询 配置 监控 Redis 电商 系统 HexHub 聚簇 非聚簇 MySQL 9.3 Linux 安全 服务器 异步化 防火墙 黑客 查询 sftp 服务器 参数 group by 自定义序列化 数据 主库 SQLark PostgreSQL 数据库锁 开源 存储引擎 管理口 业务 SQLite-Web SQLite 数据库管理工具 线上 库存 预扣 Doris SeaTunnel MVCC 人工智能 向量数据库 推荐系统 R edis 线程 共享锁 工具 PG DBA ​Redis 机器学习 推荐模型 加密 场景 流量 网络架构 网络配置 AI 助手 Ftp redo log 重做日志 数据备份 信息化 智能运维 B+Tree ID 字段 优化 万能公式 • 索引 • 数据库 RDB AOF Redis 8.0 高可用 Canal Postgres OTel Iceberg Python 缓存 GreatSQL 连接数 云原生 INSERT COMPACT 核心架构 订阅机制 同城 双活 网络故障 Hash 字段 不宕机 自动重启 Web IT运维 微软 SQL Server AI功能 prometheus Alert SVM Embedding Netstat Linux 服务器 端口 大模型 高效统计 今天这篇文章就跟大家 虚拟服务器 虚拟机 内存 向量库 Milvus OAuth2 Token Entity 开发 引擎 性能 分库 分表 DBMS 管理系统 MongoDB 容器 sqlmock 崖山 新版本 ZODB 单点故障 LRU mini-redis INCR指令 JOIN Undo Log 数据集成工具 容器化 窗口 函数 悲观锁 乐观锁 openHalo SpringAI 分布式 集中式 Redisson 锁芯 QPS 高并发 排行榜 排序 Testcloud 云端自动化 磁盘架构 数据脱敏 加密算法 PostGIS Redka Recursive 聚簇索引 非聚簇索引 数据类型 模型 启动故障 大表 业务场景 StarRocks 数据仓库 分页 数据结构 意向锁 记录锁 EasyExcel MySQL8 分布式架构 分布式锁​ Pottery 原子性 R2DBC AIOPS InfluxDB 1 IT Caffeine CP MCP 开放协议 RAG HelixDB 工具链 事务 Java 发件箱模式 SSH 网络 dbt 数据转换工具 部署 池化技术 连接池 Web 接口 字典 filelock 对象 数据分类 传统数据库 向量化 读写 速度 服务器中毒 事务隔离 优化器 单线程 Go 数据库迁移 仪表盘 双引擎 频繁 Codis 分页方案 排版 数据页 Order 线程安全 Crash 代码 LLM 订单 List 类型 事务同步 UUIDv7 主键 日志 Pump Ansible