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

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

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

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