news 2026/10/21 12:56:13

Redis字符串类型深度剖析:从SDS存储结构到核心命令实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis字符串类型深度剖析:从SDS存储结构到核心命令实战

1. Redis 字符串类型:先搞懂它到底在存什么

Redis 的 String(字符串)类型是整个 Redis 体系里最基础、也最容易被低估的数据类型。很多人一听“字符串”就下意识觉得“不就是 key-value 存文本嘛”,实际上它承担了缓存、计数器、分布式锁、限流、Session 共享、对象序列化存储等一整套核心业务场景。作为数据类型系列的第二个部分,这篇重点拆解 Strings 的核心命令和底层存储结构,尽量把“命令为什么要这么设计”“底层为什么选择这种结构”也讲明白,而不只是贴一堆语法。

1.1 字符串类型在 Redis 里的定位

Redis 的数据模型是“一个键对应一个值”,键永远是字符串,值就有多种类型可选,包括 String、Hash、List、Set、ZSet。而 String 是唯一一种“值本身也是字符串”的类型。它看起来最简单,却是其他所有类型的地基,不管是 Hash 的 field、List 的元素、Set 的成员,底层本质上都是字符串。

很多人不知道的是,Redis 的字符串不是传统语言里的“字符数组”,它既能保存普通的文本,也能保存整数,还能保存二进制数据,比如图片的字节流、序列化后的对象。单个字符串的最大容量是 512MB,所以在实际项目里,把一个用户对象序列化成 JSON 再塞进 Redis,或者把一个文件内容直接缓存起来,在容量上完全没问题。

1.2 为什么 Strings 能扛起大半业务场景

我在实际项目里观察过,线上 Redis 的 key 数量里,String 类型常常能占到 60% 以上。原因无非这几点:

  • 读写模型简单:一个 key 一个 value,业务要缓存什么,直接 SET 进去、GET 出来,心智负担极低。
  • 原子操作丰富:INCR、DECR、SETNX 这些命令天然具备原子性,不用额外加锁就能实现计数器、秒杀库存扣减、分布式锁。
  • 过期时间精细:SET 命令可以同时指定过期时间,缓存场景几乎是无缝适配。
  • 二进制安全:存什么都不会被截断,序列化后的 Protobuf、MessagePack 都能直接放进去。

后面会发现,很多看似简单的命令,组合起来能覆盖相当复杂的业务需求。这也是我坚持先写 String 的原因,基础类型没吃透,后面学 Hash、ZSet 时会总觉得缺一块。

2. 核心命令拆解:从最基础的存取到花样操作

2.1 最常用的三件套:SET、GET、DEL

先看最基础的一组:

SET user:name "zhangsan" GET user:name DEL user:name

SET 负责写入,GET 负责读取,DEL 负责删除。这套组合本身没什么好讲的,但有几个细节很容易踩坑:

  • SET 一个已经存在的 key 时,默认会直接覆盖旧值。
  • GET 一个不存在的 key,返回的是 nil,不是空字符串。这个在客户端里要区分清楚,很多新手在这里把 nil 当成空字符串处理,结果判断逻辑出问题。
  • DEL 可以同时删除多个 key,比如DEL key1 key2 key3,返回值是被删除的数量。

实际业务里,我最常用的是这套组合做热点数据的读取,比如先查缓存,缓存没有再查数据库,查询成功后再回写缓存。看起来简单,但配合后面的过期时间和 NX 参数,就能做出防雪崩、防击穿的缓存方案。

2.2 带过期时间的写入:SET 的完整参数

SET 命令不是只有两个参数,它完整语法里有不少可选项:

SET key value [EX seconds] [PX milliseconds] [EXAT timestamp] [PXAT timestamp] [NX | XX] [KEEPTTL]

逐个解释一下:

  • EX / PX:指定过期时间,EX 的单位是秒,PX 的单位是毫秒。
  • EXAT / PXAT:指定一个绝对时间戳,到点自动过期。EXAT 是秒级时间戳,PXAT 是毫秒级时间戳。这个在定时任务场景里很有用,比如设置某个优惠券在 2024 年某月某日失效。
  • NX:只在 key 不存在的时候才设置。这个参数是分布式锁的基石。
  • XX:只在 key 已经存在的时候才设置,适合做“只更新已存在的缓存”。
  • KEEPTTL:重新设置 value 的时候保留原本的过期时间。这个参数特别容易被忽略,很多人 SET 一个已有 TTL 的 key 时,发现过期时间被清掉了,问题就出在这里。

这里有个我切身体会的大坑:如果第一次用SET token "abc" EX 3600设置了过期时间,后来又用SET token "def"去更新值,没有加 EX 也没有 KEEPTTL,那么这个 key 的过期时间会被清除,变成一个永久 key。日志里就会出现“明明设置了过期时间,为什么几百个 token 全不过期”的诡异现象。

2.3 计数器三兄弟:INCR、INCRBY、DECRBY

INCR article:views:10001 INCRBY article:views:10001 10 DECR article:stock:10086 DECRBY article:stock:10086 5

INCR 把 key 对应的数字加 1,INCRBY 加指定步长,DECR 和 DECRBY 则是减。这一组命令有几个非常重要但却常常被忽视的特性:

  • 原子性:INCR 在执行过程中不会被其他命令插队,多个客户端同时调用也不会出现“加两次但只加了一次”的问题。这一点在做秒杀扣库存、点赞数统计时极其关键。
  • 自动初始化:key 不存在时,Redis 会先把它当成 0,再执行加操作。比如INCR view在 view 不存在时,结果直接是 1。
  • 必须是数字:如果 key 的值不是整数,或者超过了 64 位有符号整数范围,Redis 会返回错误。曾经有人把用户手机号存进去再 INCR,直接报错。

我用计数器命令做过一个比较典型的场景:文章阅读数。数据库里存一个基准数,Redis 里存增量,每隔一段时间把增量同步回数据库,然后重置 Redis 里的值。这样数据库不用扛高频写入,阅读数又不会丢。

2.4 批量操作与单命令逻辑:MSET、MGET、APPEND、STRLEN

MSET user:1 "a" user:2 "b" user:3 "c" MGET user:1 user:2 user:3 APPEND user:1 "_suffix" STRLEN user:1

MSET 可以一次写入多个 key,MGET 一次读取多个 key。这样做的价值是减少网络往返,本来需要 N 次请求的,现在一次搞定。在 RTT(Round-Trip Time)很高的场景下,比如跨机房调用,这个优化尤其明显。

APPEND 是在原有字符串末尾追加内容,如果 key 不存在,则等价于 SET。STRLEN 返回字符串的字节长度,不是字符长度。这里要特别注意:如果存的是中文,一个汉字在 UTF-8 编码下占 3 个字节,STRLEN 返回的长度跟肉眼看到的字符数是不一致的。比如STRLEN "你好"返回的是 6,不是 2。

2.5 其他常用但容易忽略的命令

还有一组命令值得提一下:

SETNX key value GETSET key new_value GETDEL key GETEX key EX 60 SETRANGE key offset value GETRANGE key start end
  • SETNX 本质就是 SET key value NX,只在 key 不存在时设置成功,返回 1,否则返回 0。它经常被用作分布式锁的“占坑”动作。
  • GETSET 返回旧值并设置新值,适合做“取出当前值并重置”的操作,常用于流量统计里的“上一次统计周期值”。
  • GETDEL 返回旧值并删除 key,在“消费一次”的场景里很好用,比如一次性 token 的校验。
  • GETEX 可以读取 key 的同时重新设置过期时间,适合做滑动过期,比如用户活跃 session 续期。
  • SETRANGE 和 GETRANGE 则是对字符串指定区间进行操作,实际业务里用得不多,但偶尔做字符串裁剪时非常方便。

3. 存储结构:SDS 与三种编码方式

3.1 为什么 Redis 不用 C 字符串

Redis 是 C 语言写的,但它没有直接用 C 语言传统的char*字符串来存数据,而是自己定义了一个结构叫 SDS(Simple Dynamic String,简单动态字符串)。这个选择不是闲得慌,而是为了解决几个实际问题:

  • O(1) 获取长度:C 字符串要统计长度必须遍历到结尾的\0,SDS 结构里直接记录了长度字段,取长度没有遍历开销。
  • 二进制安全:C 字符串以\0作为结束标志,如果数据里本身包含\0就会被截断。SDS 用长度字段判断结尾,所以存二进制数据、序列化数据都不会出问题。
  • 避免缓冲区溢出:C 字符串拼接时如果没分配足够空间,会把数据写到相邻内存里,造成安全问题。SDS 在修改前会检查空间,不够就先扩容。

SDS 还做了空间预分配和惰性空间释放。简单说,字符串变长时不会只分配刚好够用的内存,而是多预留一些,避免频繁扩容;缩短时也不急着回收内存,方便后续再次追加。这在频繁做 APPEND 操作的场景下能省下不少性能。

3.2 int、embstr、raw 三种编码方式

Redis 底层会根据字符串的内容和长度,选择不同的编码方式。通过OBJECT ENCODING key可以查看当前 key 的编码:

SET num 12345 OBJECT ENCODING num # 返回 int SET short_str "hello" OBJECT ENCODING short_str # 返回 embstr SET long_str "长字符串,超过 44 字节的内容" OBJECT ENCODING long_str # 返回 raw

三种编码的含义:

  • int:当字符串可以解析为 64 位有符号整数时,Redis 直接以整数形式存储,不再保存字符串字节序列。这样省内存,INCR、DECR 命令操作起来也快。
  • embstr:短字符串(通常小于等于 44 字节)使用嵌入式字符串编码,Redis 对象头和数据在连续内存里,一次分配一次释放,内存碎片少,访问快。
  • raw:长字符串使用普通动态字符串编码,redisObject 和 SDS 分开分配,适合保存大文本和二进制数据。

这里的 44 字节阈值经常被搞混。早期版本里阈值是 39 字节,后来版本调整了结构,常见 Redis 3.2 之后就是 44 字节。不同小版本的精确阈值可能略有差异,最好的办法就是实际执行OBJECT ENCODING看看。

还有一点:编码不是固定的,会随着操作动态变化。比如一个 embstr 字符串执行 APPEND 之后,可能直接变成 raw。因为 APPEND 要修改数据,Redis 选择转为 raw 再做追加。一旦变成 raw,就算数据缩短也不会自动回到 embstr。int 类型如果执行 APPEND 变成字符串,也会转为 embstr 或 raw。

3.3 内存与性能的取舍

从存储结构上能看出 Redis 的设计哲学:能省则省,能快则快。在内存型数据库里,同样的 key 数量,编码方式是 int 还是 raw,最终内存占用差距可能非常大。

我实际排查过一个内存增长问题:某个服务把所有订单号都用 String 存,订单号是 18 位纯数字,但因为代码里用了格式化函数,在 Redis 里存成了 18 字节字符串,编码变成 raw。后来改为直接存整数,内存直接降了约 30%。这就是编码方式对内存的真实影响。

再看一个常见的问题是“大 key”。一个 value 达到几百 KB 甚至几 MB,使用 raw 编码存储时没问题,但 GET 和 SET 这种全量操作就会比较耗时,甚至引起 Redis 阻塞。排查时可以用MEMORY USAGE key看一个 key 占用多少内存,也可以用redis-cli --bigkeys扫描全库的大 key。

MEMORY USAGE user:info:10001

返回的是字节数,能直观看到这个 key 吃了多少内存。我在排查线上问题时发现,很多“Redis 内存涨得莫名其妙”的案例,都是因为“当时只是为了方便,把一个巨大的 JSON 直接塞了进去”,这个 JSON 往往还是重复内容。

4. 实战场景:缓存、计数器与简单分布式锁

4.1 缓存场景:写法与过期策略

缓存是 String 最典型的应用。一个标准的缓存读写流程是这样的:

# 尝试读缓存 GET product:info:10001 # 没命中则查数据库,然后写缓存并设置过期时间 SET product:info:10001 '{"name":"demo","price":99}' EX 600

这里有几个关键细节值得展开:

  • 过期时间不能设置太长。太长会导致缓存里的数据长期和数据库不一致;太短又容易击穿数据库。我通常的做法是基础过期时间 10 分钟,再根据业务容忍度调整。
  • 为了防止缓存雪崩,最好不要让所有 key 在同一时刻过期。可以在过期时间上加一个随机值,比如EX 600 + random(0, 300)。这样即使大量缓存同时失效,也不会在同一瞬间全部打到数据库。
  • 防止缓存穿透,可以在查询不存在的 key 时也写一个空值缓存,比如SET product:info:99999 "" EX 60。这样恶意请求反复查不存在的商品,也不会每次都穿透到数据库。
  • 防止缓存击穿,可以用SET key value NX EX做一个互斥锁,只允许一个请求去加载数据库,其他请求等待。

这些组合在实际项目中几乎人人都在用,但很多人写的时候并不清楚背后的原理。比如互斥锁,本质就是利用了 SETNX 的原子性:第一个拿到锁的请求去加载数据库,其他请求短暂等待或返回旧值。

4.2 计数器场景:高并发下的原子性

在 MySQL 里做计数器通常要考虑行锁、事务,但在 Redis 里用 INCR 就是一行命令的事。因为 INCR 是原子操作,多个客户端并发执行时,最终结果一定是准确的,不会出现加少了或者覆盖了的问题。

SET goods:stock:10001 100 INCR goods:stock:10001 # 99 INCR goods:stock:10001 # 98

这里有个应用技巧:如果要做“扣减到 0 就不能再减”的业务,比如库存扣减,只用 INCR 还不够,需要先读再判断再扣,这个流程不是原子的。简单方案是结合 Lua 脚本:

if redis.call('GET', KEYS[1]) <= 0 then return 0 end return redis.call('DECR', KEYS[1])

Lua 脚本在 Redis 里是原子执行的,业务里经常用这种方式实现安全扣减。当然,这只是 String 层面的玩法,真正精密的库存、秒杀系统还需要考虑更多,但 Strings 的原子操作是地基。

4.3 简单分布式锁:SETNX 的正确打开方式

String 实现分布式锁是很多面试题和高并发项目里绕不开的点。基础版本就是:

SET lock:order:10001 random_value NX EX 30
  • NX 保证只有没人持有锁的时候才能设置成功。
  • EX 30 保证即使持有锁的进程挂了,锁也会在 30 秒后自动释放,不会死锁。

这个方案的问题也很明显:如果业务执行时间超过 30 秒,锁自动过期了,另一个进程就能拿到锁,造成并发问题。解决思路通常在两个方向:一是把过期时间调大一点,二是在释放锁时校验随机值,只释放自己持有的锁。释放锁的校验和删除操作要保证原子性,实际也是用 Lua 脚本做的,这里不展开指令细节,但思路要清楚:不能直接 DEL,一定要先判断 value 是否等于自己设的随机值。

我用这个方案做过活动秒杀入口的防并发控制,简单有效。但要注意,它不是一个“完美”的分布式锁,Redis 主从切换、长时间 GC 暂停等极端场景下仍然可能有竞态。所以在要求极高的金融场景里,不建议直接用这个方案,而在绝大多数互联网业务里,它能覆盖 99% 的需求。

4.4 命令选择速查表

业务场景推荐命令关键注意点
基础缓存读写SET / GET记得设置过期时间
只允许新增的缓存SET NX防止覆盖已有数据
缓存续期GETEX key EX实现滑动过期
计数器 / 秒杀库存INCR / DECR天然原子,但扣减到 0 需 Lua
分布式锁SET NX EX释放前要校验随机值
批量读取缓存MGET减少 RTT,但要注意 key 数量
一次性消费 tokenGETDEL取到即删,防止重复使用
统计上一次的值GETSET取出旧值并原子重置

这张表是我平时写代码时经常对照的,业务需求一变,马上能映射到对应的命令,不用重新翻文档。

5. 常见问题与排查实录

5.1 设置了过期时间,key 却永久存在

这个我前面提过,最常见原因是后续 SET 没有带过期时间,也没带 KEEPTTL,把原来的 TTL 覆盖掉了。

SET user:token "abc123" EX 3600 SET user:token "new_token" # 过期时间被清掉

排查时用TTL user:token查看剩余时间,返回值 -1 表示永久有效。修复方法很简单:更新 value 时加上KEEPTTL:

SET user:token "new_token" KEEPTTL

还有一个场景也会造成误导:Redis 集群模式下,如果某台机器时钟不同步,相对过期时间会看起来“不准时”,因为 EX 的计时依赖服务器本地时间。生产环境要对好 NTP,不然会出现缓存提前过期或延迟过期的怪问题。

5.2 大 key 引发阻塞和内存暴涨

大 key 是 Redis 生产事故里最经典的一类。String 类型出现大 key 通常是以下原因:

  • 一次性缓存了很大的 JSON 数据,比如把整个商品列表一次塞进一个 key。
  • 把文件、图片字节直接存进 Redis。
  • 频繁 APPEND 一个 key,让它越长越大。

大 key 带来的影响有两个:一是内存占用高,二是操作耗时高。GET 一个大 key 需要把几十 MB 的数据从内存里取出来,传给客户端,这个操作会阻塞 Redis 主线程,影响所有其他请求。Redis 慢日志里经常能看到这类命令:

SLOWLOG GET 10

如果发现某个 GET 命令耗时几十毫秒甚至上百毫秒,基本可以怀疑是读了大 key。处理思路:

  • 把大 key 拆成多个小 key,用 Hash 结构分段存储,或者业务上分页读取。
  • 不确定哪些 key 大时,用redis-cli --bigkeys扫描。
  • 评估是否真的需要把这么大的数据放 Redis,很多时候可以只缓存摘要,完整内容走对象存储。

5.3 连接数太多导致命令变慢

String 本身不会导致这个问题,但我在用 MGET 批量读取时就遇到过:客户端一次性并发几千个 GET,连接数瞬间打满,命令排队变慢。根源是业务代码没有合理复用连接,或者没有用 pipeline 批量提交。

解决方案是用 MGET 替代多个 GET,或者使用 pipeline 技术把多个命令打包发送。我用 MGET 优化过一个接口,原本 50 次 GET 请求,合并成 1 次 MGET 后,整体耗时从 30ms 降到 3ms 左右,效果立竿见影。

5.4 中文长度和编码统计的坑

STRLEN 统计的是字节数,在中文场景下非常容易迷惑。比如一个昵称字段,用户输入了 5 个汉字,业务上预期长度是 5,但 STRLEN 返回 15。

其实这里的问题本质是字符集。UTF-8 下汉字通常占 3 字节,如果客户端写入时用了 UTF-8 编码,Redis 不会做任何编码转换,存进去是什么就是什么。想要精确的字符数,只能在业务层统计,Redis 本身不关心字符和字节的区别。这个特性在存二进制数据时反而是优点。

6. 一些实操后的心得

Redis String 这部分内容讲完了,最后分享几点我个人在实际项目中攒下的体会。

第一,不要忽视编码检查。线上环境如果内存紧张,先用OBJECT ENCODING和MEMORY USAGE看看哪些 key 是 raw 编码,哪些是 int 编码。很多内存优化,并不需要换数据结构,只需要改变写入方式。

第二,命令组合比单个命令更重要。SETNX 单独用只是一个占位操作,配合 EX、随机值、Lua 脚本释放,就成了一个实用的分布式锁;INCR 单独用只是计数器,配合 Lua 脚本就成了安全扣库存工具。学会组合,才算真正会用 Strings。

第三,任何缓存方案都要想清楚过期和失效。String 的过期时间虽然好用,但“缓存雪崩”“缓存穿透”“缓存击穿”这三座大山,几乎全是围绕过期策略产生的。合理地设置 NX、随机过期时间、空值缓存,比事后加机器更有效。

第四,遇到缓存相关问题,先看慢日志和内存,再改代码。有一次我以为某个接口变慢是数据库问题,查了半天才发现是 Redis 里一个大 key 每次 GET 耗时 80ms。如果一开始就用SLOWLOG GET定位,能省下好几个小时。

字符串类型看起来简单,但它承载的是整个 Redis 生态里最广泛的场景。把这套命令和存储结构吃透,后面学 Hash、List、Set 时会轻松很多,因为它们本质上都建立在 String 的基础机制之上。下一篇可以继续深入 Hash 类型的底层结构与命令陷阱。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 12:56:04

PyTorch模型高并发API部署:Nginx+Gunicorn+Flask+Docker实战

1. 项目概述&#xff1a;为什么一个能扛住每秒上千请求的模型API&#xff0c;往往死在部署这一步&#xff1f; “深度学习模型训练好了&#xff0c;怎么让别人用上&#xff1f;”——这是我在某高校实验室带学生做毕业设计时&#xff0c;被问得最多的一句话。不是模型不准&…

作者头像 李华
网站建设 2026/10/21 12:55:39

Jev决策模型:将高频决策从大模型剥离的轻量架构实践

1. 为什么要把“决策”从大模型里拆出来 第一次看到“把决策从大模型里拆出来”这个说法&#xff0c;我脑子里蹦出来的画面是&#xff1a;一个团队里有个什么都懂但反应慢、还特别贵的顾问&#xff0c;每次遇到芝麻大的事都要把他请出来开会。大模型现在在很多系统里扮演的就是…

作者头像 李华
网站建设 2026/10/21 12:54:54

外卖霸王餐小程序源码拆解:从部署到防刷,掌握任务返现系统核心

简介&#xff1a;这款“省钱兄外卖霸王餐程序”是一套面向本地外卖商家的营销小程序源码&#xff0c;用于搭建支持商家入驻、霸王餐发布、用户评价互动的服务平台&#xff0c;适合有二次开发需求的技术人员或想自建外卖引流渠道的运营者&#xff0c;同时也可作为本地生活类项目…

作者头像 李华
网站建设 2026/10/21 12:54:14

Linux高级存储管理实战:LVM、RAID、快照与配额调优

1. 存储管理的分水岭&#xff1a;为什么基础分区不够用了 很多人学Linux存储&#xff0c;学到 fdisk 分区、 mkfs 格式化、 mount 挂载这三板斧就觉得到头了。我刚开始也这么想&#xff0c;直到有一次帮朋友处理一台运行了半年的文件服务器&#xff0c; /data 分区满了…

作者头像 李华
网站建设 2026/10/21 12:53:45

AI原生备份:面向GPU训练的端到端热备方案

1. 项目概述&#xff1a;这不是一个“备份软件”&#xff0c;而是一套面向AI工作流的业务连续性保障体系“守护 AI 生产力&#xff0c;筑牢业务连续防线”——这句话不是宣传口号&#xff0c;而是我在某AI模型训练平台运维现场连续蹲点三周后&#xff0c;亲手写进SOP里的第一行…

作者头像 李华
网站建设 2026/10/21 12:53:24

Docker存储卷完全指南:数据持久化、挂载方式与备份恢复实战

1. 存储卷到底是什么&#xff0c;先把容器数据丢失的痛点说清楚 先抛一个几乎所有容器初学者都会遇到的问题&#xff1a;容器一删&#xff0c;数据没了。这不是玩笑&#xff0c;也不只是“忘记保存”的问题。容器的读写层是临时的&#xff0c;容器停止、删除之后&#xff0c;这…

作者头像 李华