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

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

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

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