Redis 数据类型入门:全局命令、单线程原理与 String 字符串

一、前言

💬 这一篇讲什么:在正式学习五种数据类型之前,先打好基础;然后深入讲解最核心的数据类型 —— String

🚀 核心内容

  • Redis 的全局命令有哪些?如何对任意 key 进行通用操作?
  • Redis 每种数据类型的底层内部编码是什么?为什么要有多种编码?
  • Redis 单线程模型是怎么运转的?为什么单线程还能这么快?
  • String 类型的所有命令和典型使用场景

上一篇把 Redis 环境搭起来了。从这一篇开始,正式进入 Redis 最核心的内容:数据类型。在开始之前,有一些基础知识必须先铺垫清楚,否则后面很多命令的行为会让人摸不着头脑。


二、全局命令

Redis 有五种数据类型,但不管 value 是哪种类型,key 本身永远是字符串,而且有一批命令是对所有类型的 key 都通用的,这些命令叫做全局命令。先把这些通用命令搞清楚,后面学每种数据类型时会少很多困惑。

2.1 KEYS —— 查找匹配的 key

返回所有满足匹配模式(pattern)的 key。

语法

KEYS pattern

时间复杂度:O(N),N 是数据库中 key 的总数。

支持的通配符

模式 含义 示例
? 匹配任意单个字符 h?llo 匹配 hellohallohxllo
* 匹配任意数量字符(包括零个) h*llo 匹配 hlloheeeello
[ae] 匹配括号内的任意一个字符 h[ae]llo 匹配 hellohallo,不匹配 hillo
[^e] 匹配不在括号内的字符 h[^e]llo 匹配 hallohbllo,不匹配 hello
[a-b] 匹配字符范围 h[a-b]llo 匹配 hallohbllo

示例

redis> MSET firstname Jack lastname Stuntman age 35
"OK"
redis> KEYS *name*
1) "firstname"
2) "lastname"
redis> KEYS a??
1) "age"
redis> KEYS *
1) "age"
2) "firstname"
3) "lastname"

重要警告KEYS * 会遍历整个数据库中所有的 key,时间复杂度是 O(N)。在生产环境中,如果 Redis 存储了大量数据,执行 KEYS * 会造成 Redis 阻塞,期间所有其他客户端的请求都会被卡住。生产环境中禁止使用 KEYS 命令,需要遍历 key 时应使用后面章节会讲到的 SCAN 命令。

2.2 EXISTS —— 判断 key 是否存在

判断指定的一个或多个 key 是否存在。

语法

EXISTS key [key ...]

时间复杂度:O(1)

返回值:存在的 key 的个数(注意:传入多个相同的 key,每个都会被单独计数)。

示例

redis> SET key1 "Hello"
"OK"
redis> EXISTS key1
(integer) 1
redis> EXISTS nosuchkey
(integer) 0
redis> SET key2 "World"
"OK"
redis> EXISTS key1 key2 nosuchkey
(integer) 2

2.3 DEL —— 删除 key

删除指定的一个或多个 key,不存在的 key 会被忽略。

语法

DEL key [key ...]

时间复杂度:O(1)

返回值:实际删除掉的 key 的个数。

示例

redis> SET key1 "Hello"
"OK"
redis> SET key2 "World"
"OK"
redis> DEL key1 key2 key3
(integer) 2

key3 不存在,所以只删掉了 2 个。

2.4 EXPIRE 和 TTL —— 过期时间管理

EXPIRE:设置过期时间

为指定的 key 设置秒级的过期时间(TTL,Time To Live)。

语法

EXPIRE key seconds

时间复杂度:O(1)

返回值:1 表示设置成功,0 表示设置失败(key 不存在)。

示例

redis> SET mykey "Hello"
"OK"
redis> EXPIRE mykey 10
(integer) 1
redis> TTL mykey
(integer) 10
TTL:查询剩余过期时间

获取指定 key 的剩余过期时间,秒级。

语法

TTL key

时间复杂度:O(1)

返回值

  • 正整数:剩余的秒数
  • -1:key 存在但没有设置过期时间
  • -2:key 不存在

示例

redis> SET mykey "Hello"
"OK"
redis> EXPIRE mykey 10
(integer) 1
redis> TTL mykey
(integer) 10
# 等待 10 秒后
redis> TTL mykey
(integer) -2
redis> EXISTS mykey
(integer) 0

键的过期机制如下:设置 key 并执行 EXPIRE 之后,Redis 会在指定秒数后自动淘汰这个 key,之后无论是 GET 还是 EXISTS,都会返回 key 不存在的结果。

💡 EXPIRETTL 都有对应的毫秒版本:PEXPIREPTTL,用法完全一致,只是时间单位换成了毫秒,适合需要高精度过期控制的场景。

2.5 TYPE —— 查询 key 的数据类型

返回 key 对应的 value 的数据类型。

语法

TYPE key

时间复杂度:O(1)

返回值none(key不存在)、stringlistsetzsethashstream

示例

redis> SET key1 "value"
"OK"
redis> LPUSH key2 "value"
(integer) 1
redis> SADD key3 "value"
(integer) 1
redis> TYPE key1
"string"
redis> TYPE key2
"list"
redis> TYPE key3
"set"

三、数据结构与内部编码

3.1 两个层次

TYPE 命令返回的是 Redis 对外暴露的数据结构类型,也就是我们使用时感知到的那一层:string、list、hash、set、zset。

但实际上,Redis 在底层对每种数据结构都有多种内部编码实现,Redis 会根据当前数据的规模和特征,自动选择最合适的那一种:

数据结构 内部编码
string rawintembstr
hash hashtableziplist
list linkedlistziplist(3.2之后合并为 quicklist
set hashtableintset
zset skiplistziplist

可以通过 OBJECT ENCODING 命令查询某个 key 当前使用的内部编码:

127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> object encoding hello
"embstr"
127.0.0.1:6379> lpush mylist a b c
(integer) 3
127.0.0.1:6379> object encoding mylist
"quicklist"

3.2 为什么要这样设计

这个设计有两个核心好处:

好处一:可以升级内部编码而不影响使用者。 用户使用的命令和数据结构完全不变,Redis 内部可以悄悄换掉底层实现。比如 Redis 3.2 引入了 quicklist(结合了 ziplist 和 linkedlist 两者的优势),用户完全无感知,代码一行不用改,性能却提升了。

好处二:不同场景下选用最优实现,兼顾性能和内存。 以 list 为例:元素少时用 ziplist(压缩列表,内存紧凑),元素多时用 linkedlist(链表,读写更快)。Redis 会根据配置的阈值自动切换,用户不需要关心。


四、单线程架构

4.1 单线程模型是什么意思

很多人误解"Redis 是单线程的"这句话。准确的说法是:Redis 处理命令的核心逻辑是单线程的(6.0 之后网络 IO 引入了多线程,但命令执行仍然是单线程)。

来看一个具体的例子。假设有三个客户端同时向 Redis 发命令:

# 客户端 1
SET hello world

# 客户端 2
INCR counter

# 客户端 3
INCR counter

从宏观上看,三个客户端似乎同时在发请求。但在微观上,这些命令到达 Redis 服务端的时间有先后(哪怕只差几微秒),Redis 内部只有一个命令处理队列,所有命令被串行地依次执行,就像银行只有一个服务窗口,所有人排队等候一样。

正因为是串行的,两条 INCR counter 命令无论顺序如何,最终结果一定是 2,不会出现并发竞争导致结果错误的情况。这是单线程模型最大的好处之一。

4.2 单线程为什么还这么快

直觉上,单线程处理能力应该比多线程弱。但 Redis 单线程能达到每秒数万乃至十万次的处理能力,原因有三:

原因一:纯内存操作。 Redis 把所有数据存在内存中,内存访问耗时约 100 纳秒,而磁盘寻道需要 10 毫秒以上,差了 10 万倍。内存操作之快,让单线程的"串行瓶颈"根本不是问题。

原因二:非阻塞 IO(IO 多路复用)。 Redis 使用 epoll 实现 IO 多路复用。简单来说,epoll 允许一个线程同时监听大量网络连接,哪个连接有数据来了就去处理哪个,不会因为等待某个客户端而阻塞其他客户端。Redis 把连接的建立、数据读写、连接关闭全部注册为事件,由事件循环(Event Loop)驱动,在网络 IO 上几乎不浪费时间。

[客户端 1]
[客户端 2]   ──→  epoll 事件监听  ──→  Redis Event Loop(单线程)
[客户端 N]              ↑
                    有数据就触发

原因三:避免了多线程的额外开销。 多线程并不是免费的——线程切换有开销,多个线程竞争共享数据需要加锁(锁的开销有时候非常大),还容易出现死锁等复杂问题。单线程模型彻底规避了这些问题,代码也更简洁。

4.3 单线程的致命弱点

单线程带来了简洁和高效,但也有一个不可忽视的问题:如果某条命令执行时间过长,会阻塞所有其他客户端的请求。

因为只有一个处理窗口,如果某个命令执行了几秒钟,期间所有排队的其他命令都在等待,客户端会超时,用户会感受到明显的卡顿。

这就是为什么 Redis 的命令设计原则是"快":所有命令的时间复杂度都尽量控制在 O(1) 或 O(log N),对于 O(N) 的命令(比如 KEYS *HGETALL 大哈希)需要格外谨慎,在生产环境中要评估数据规模再使用。

结论:Redis 是面向快速执行场景的数据库,命令越快越好,耗时命令要慎用。


五、String 字符串

String 是 Redis 中最基础的数据类型,也是使用最广泛的类型。有几点需要先说清楚:

第一,Redis 所有 key 都是字符串类型。 其他四种数据类型是 value 的类型,key 永远是字符串。

第二,String 类型的 value 可以是以下任意一种:

  • 普通字符串:"hello world"
  • JSON / XML 等格式字符串:'{"id":1,"name":"James"}'
  • 数字(整型或浮点型):423.14
  • 二进制数据(图片、音频等的字节流)

唯一的限制是:单个 String 的值最大不能超过 512 MB

第三,Redis 是按照二进制流存储字符串的,不处理字符集编码问题。 客户端传入什么编码,Redis 原样存储,读出来什么编码是客户端的事。

5.1 基本命令

SET —— 设置值

将 string 类型的 value 设置到 key 中。如果 key 已存在,无论原来是什么类型,都会被覆盖,原有的 TTL 也会清除。

语法

SET key value [EX seconds] [PX milliseconds] [NX|XX]

选项说明

选项 含义
EX seconds 设置秒级过期时间
PX milliseconds 设置毫秒级过期时间
NX 只在 key 不存在时才设置(Not eXists)
XX 只在 key 存在时才设置(eXists)

返回值:设置成功返回 OK;因 NX/XX 条件不满足而未执行则返回 (nil)

示例

# 基本设置
redis> SET mykey "Hello"
OK
redis> GET mykey
"Hello"

# NX:key 不存在才设置
redis> SET mykey "World" NX
(nil)          # mykey 已存在,设置失败

# 先删除
redis> DEL mykey
(integer) 1

# XX:key 存在才设置
redis> SET mykey "World" XX
(nil)          # mykey 不存在,设置失败

# NX 在 key 不存在时成功
redis> SET mykey "World" NX
OK

# 设置带过期时间
redis> SET mykey "Will expire in 10s" EX 10
OK
redis> GET mykey
"Will expire in 10s"
# 10 秒后
redis> GET mykey
(nil)

三种设置模式的执行逻辑:

  • SET key value:无论 key 是否存在,直接覆盖写入。
  • SET key value NX(等价于 SETNX key value):只有 key 不存在时才写入,已存在则不操作。
  • SET key value XX:只有 key 已存在时才覆盖,不存在则不操作。
GET —— 获取值

获取 key 对应的 value。key 不存在返回 nil;如果 value 不是 string 类型会报错。

语法

GET key

时间复杂度:O(1)

示例

redis> GET nonexisting
(nil)
redis> SET mykey "Hello"
"OK"
redis> GET mykey
"Hello"

# 对非 string 类型执行 GET 会报错
redis> HSET mykey name Bob
(integer) 1
redis> GET mykey
(error) WRONGTYPE Operation against a key holding the wrong kind of value
MSET 和 MGET —— 批量操作

MSET 一次性设置多个 key 的值;MGET 一次性获取多个 key 的值。

语法

MSET key value [key value ...]
MGET key [key ...]

时间复杂度:O(N),N 是 key 的数量。

示例

redis> MSET key1 "Hello" key2 "World"
"OK"
redis> MGET key1 key2 nonexisting
1) "Hello"
2) "World"
3) (nil)

为什么批量命令性能更高?

假设网络往返耗时 1ms,每条命令执行耗时 0.1ms:

操作方式 总耗时
执行 1000 次 GET 1000 × 1ms + 1000 × 0.1ms = 1100ms
执行 1 次 MGET(1000 个 key) 1 × 1ms + 1000 × 0.1ms = 101ms

批量操作把 1000 次网络往返压缩成了 1 次,性能提升接近 10 倍。

不过要注意:批量操作一次携带的 key 数量也不能无限多,否则单条命令执行时间过长,会阻塞 Redis 服务端。实践中通常建议单次批量不超过几百到几千个 key,具体根据数据大小评估。

SETNX —— 不存在时才设置

只在 key 不存在的情况下才设置值。等价于 SET key value NX

语法

SETNX key value

返回值:1 表示设置成功,0 表示 key 已存在未设置。

示例

redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"

SETNX 在分布式锁的实现中有重要作用,后续专门讲分布式锁时会详细展开。

5.2 计数命令

Redis 对 String 类型提供了一批专门针对数字值的原子性自增自减命令,这是 Redis 在计数场景下的核心能力。

INCR —— 自增 1

将 key 对应的数字值加 1。如果 key 不存在,视为原值为 0,执行后结果为 1。如果 value 不是整数,或超过 64 位有符号整型范围,则报错。

语法

INCR key

时间复杂度:O(1)

示例

redis> EXISTS mykey
(integer) 0
redis> INCR mykey
(integer) 1      # key 不存在,从 0 开始加 1

redis> SET mykey "10"
"OK"
redis> INCR mykey
(integer) 11

# 非整数会报错
redis> SET mykey "not a number"
"OK"
redis> INCR mykey
(error) value is not an integer or out of range
INCRBY —— 自增指定步长

将 key 对应的数字值加上指定的整数值。

语法

INCRBY key increment

示例

redis> SET mykey "10"
"OK"
redis> INCRBY mykey 3
(integer) 13
redis> INCRBY mykey -5
(integer) 8
DECR 和 DECRBY —— 自减

DECR 将值减 1;DECRBY 将值减去指定的整数。逻辑和 INCR/INCRBY 完全对称。

redis> SET mykey "10"
"OK"
redis> DECR mykey
(integer) 9
redis> DECRBY mykey 3
(integer) 6
INCRBYFLOAT —— 浮点数自增

将 key 对应的值加上指定的浮点数。支持负数(相当于减法)。支持科学计数法。

语法

INCRBYFLOAT key increment

示例

redis> SET mykey 10.50
"OK"
redis> INCRBYFLOAT mykey 0.1
"10.6"
redis> INCRBYFLOAT mykey -5
"5.6"
redis> SET mykey 5.0e3
"OK"
redis> INCRBYFLOAT mykey 2.0e2
"5200"

💡 在很多编程语言里,实现计数器需要用 CAS(Compare And Swap)机制来保证并发安全,这会有一定的 CPU 开销。但在 Redis 里完全不需要这些,因为命令是单线程串行执行的,两条 INCR 命令不可能同时执行,结果天然正确,没有竞态条件。

5.3 字符串操作命令

APPEND —— 追加内容

在 key 对应的 string 末尾追加内容。如果 key 不存在,效果等同于 SET

语法

APPEND key value

返回值:追加后 string 的总长度。

示例

redis> EXISTS mykey
(integer) 0
redis> APPEND mykey "Hello"
(integer) 5
redis> APPEND mykey " World"
(integer) 11
redis> GET mykey
"Hello World"
STRLEN —— 获取字符串长度

获取 key 对应 string 的字节长度(注意是字节数,不是字符数,中文字符在 UTF-8 下占 3 字节)。key 不存在返回 0。

语法

STRLEN key

时间复杂度:O(1)

示例

redis> SET mykey "Hello world"
"OK"
redis> STRLEN mykey
(integer) 11
redis> STRLEN nonexisting
(integer) 0
GETRANGE —— 获取子串

返回 string 的子串,由起始下标 start 和结束下标 end 决定,左闭右闭。支持负数索引:-1 代表最后一个字符,-2 代表倒数第二个,以此类推。下标越界会自动调整。

语法

GETRANGE key start end

时间复杂度:O(N),N 为子串长度,通常视为 O(1)。

示例

redis> SET mykey "This is a string"
"OK"
redis> GETRANGE mykey 0 3
"This"
redis> GETRANGE mykey -3 -1
"ing"
redis> GETRANGE mykey 0 -1
"This is a string"    # 等同于获取整个字符串
redis> GETRANGE mykey 10 100
"string"              # 超出范围自动截断
SETRANGE —— 覆盖子串

从指定的偏移位置开始,用新的 value 覆盖原有字符串的对应部分。

语法

SETRANGE key offset value

返回值:修改后 string 的总长度。

示例

redis> SET key1 "Hello World"
"OK"
redis> SETRANGE key1 6 "Redis"
(integer) 11
redis> GET key1
"Hello Redis"

5.4 命令速查表

命令 执行效果 时间复杂度
SET key value 设置 key 的值 O(1)
GET key 获取 key 的值 O(1)
MSET key value [...] 批量设置多个 key O(k),k 为键个数
MGET key [...] 批量获取多个 key O(k),k 为键个数
SETNX key value key 不存在时才设置 O(1)
INCR key 值 +1 O(1)
DECR key 值 -1 O(1)
INCRBY key n 值 +n O(1)
DECRBY key n 值 -n O(1)
INCRBYFLOAT key n 浮点数值 +n O(1)
APPEND key value 追加内容 O(1)
STRLEN key 获取字符串长度 O(1)
GETRANGE key start end 获取子串 O(n),通常 O(1)
SETRANGE key offset value 覆盖子串 O(n),通常 O(1)

5.5 内部编码

String 类型在底层有三种内部编码实现,Redis 会根据 value 的内容自动选择:

内部编码 使用条件 说明
int value 是 64 位有符号整数 直接用整型存储,最节省内存
embstr value 是长度 ≤ 39 字节的字符串 紧凑存储,只分配一次内存
raw value 是长度 > 39 字节的字符串 常规字符串存储
# int 编码
127.0.0.1:6379> set key 6379
OK
127.0.0.1:6379> object encoding key
"int"

# embstr 编码(≤39 字节的短字符串)
127.0.0.1:6379> set key "hello"
OK
127.0.0.1:6379> object encoding key
"embstr"

# raw 编码(>39 字节的长字符串)
127.0.0.1:6379> set key "one string greater than 39 bytes ........"
OK
127.0.0.1:6379> object encoding key
"raw"

六、String 的典型使用场景

6.1 缓存(Cache)

这是 Redis String 最核心的使用场景。把数据库里的热数据序列化后存入 Redis,后续请求优先从 Redis 读取,命中则直接返回,未命中再查数据库并回填缓存。

典型架构如下:

用户请求
    ↓
先查 Redis 缓存(key = "user:info:{uid}")
    ├── 命中(hit)→ 直接返回数据
    └── 未命中(miss)→ 查 MySQL → 结果写入 Redis(带过期时间)→ 返回数据

伪代码示意:

def get_user_info(uid):
    key = "user:info:" + str(uid)
    
    # 先查缓存
    value = redis.get(key)
    if value is not None:
        return json.loads(value)   # 缓存命中,直接返回
    
    # 缓存未命中,查数据库
    user_info = mysql.query("SELECT * FROM user_info WHERE uid = %s", uid)
    if user_info is None:
        return None   # 用户不存在
    
    # 写入缓存,设置 1 小时过期(防止数据腐烂)
    redis.set(key, json.dumps(user_info), ex=3600)
    
    return user_info

设置过期时间(EX 3600)很重要:即使数据库里的数据发生了变化,缓存最多也只会"过时"一小时,之后自动失效,下次请求会重新从数据库读取最新数据。

💡 键名设计建议:Redis 没有表和字段的概念,建议使用 业务名:对象名:唯一标识:属性 的格式命名键,比如 user:info:1001video:playcount:5253。这样既能防止不同业务的键名冲突,也便于维护。键名过长会影响 Redis 性能,必要时可以使用团队内认可的缩写。

6.2 计数器(Counter)

利用 INCR 系列命令实现原子性计数,天然线程安全,性能极高。

以视频播放量为例:

def increment_play_count(video_id):
    key = "video:playcount:" + str(video_id)
    count = redis.incr(key)   # 原子性 +1,返回最新计数
    return count

每次用户播放视频,调用一次 INCR,Redis 自动完成加 1 操作。可以异步地将 Redis 中的计数定期同步回 MySQL,避免高频写库。

6.3 共享 Session

分布式 Web 服务通常有多台应用服务器,负载均衡会把用户请求打到不同的机器上。如果 Session 存在每台机器本地,用户可能每次请求都被分到不同的机器,导致需要反复登录。

解决方案是把 Session 集中存储在 Redis 中:

用户
 ↓
[负载均衡器]
 ├── [应用服务器 1]  ──→  [Redis:Session 集中存储]
 ├── [应用服务器 2]  ──→
 └── [应用服务器 n]  ──→

无论用户被分到哪台服务器,都从 Redis 里读写 Session,数据始终一致,也不会丢失登录状态。

6.4 手机验证码

验证码场景有两个核心需求:验证码有效期(比如 5 分钟失效)和发送频率限制(比如同一手机号 1 分钟内最多发 5 次)。两个需求都可以用 Redis String 的过期时间和 INCR 优雅地实现。

核心逻辑:

def send_verification_code(phone_number):
    # 频率限制 key,1 分钟内有效
    limit_key = "sms:limit:" + phone_number
    
    # NX 保证第一次设置时才初始化,EX 60 设置 1 分钟过期
    r = redis.set(limit_key, 1, ex=60, nx=True)
    
    if r is None:
        # key 已存在,说明这一分钟内已经发过了
        count = redis.incr(limit_key)
        if count > 5:
            return None   # 超出频率限制,拒绝发送
    
    # 生成随机 6 位验证码
    code = generate_random_code()
    
    # 存入 Redis,5 分钟有效
    code_key = "sms:code:" + phone_number
    redis.set(code_key, code, ex=300)
    
    # 通过短信接口发送
    send_sms(phone_number, code)
    return code

def verify_code(phone_number, input_code):
    code_key = "sms:code:" + phone_number
    stored_code = redis.get(code_key)
    
    if stored_code is None:
        return False   # 验证码已过期或不存在
    
    return stored_code == input_code

整个流程用到了 SET NX EX(原子性设置 + 过期时间)和 INCR(原子性计数),不需要任何锁操作,逻辑清晰,并发安全。


七、总结

现在你已经掌握了:

全局命令KEYS(生产环境禁用)、EXISTSDELEXPIRETTLTYPE

内部编码机制:对外数据结构与内部编码的两层设计,以及这样设计的好处

单线程架构:命令串行执行、IO 多路复用(epoll)、纯内存操作 —— 三者共同造就了 Redis 的高性能

单线程弱点:慢命令会阻塞所有客户端,生产环境要谨慎

String 全部命令:SET/GET/MSET/MGET/SETNX、INCR/DECR 系列、APPEND/STRLEN/GETRANGE/SETRANGE

String 内部编码:int(整数)、embstr(≤39字节)、raw(>39字节)

String 典型场景:缓存、计数器、共享Session、手机验证码

7.1 重点注意事项

注意事项 说明
KEYS * 禁止在生产环境使用 时间复杂度 O(N),会阻塞 Redis
批量命令注意 key 数量 单次批量过多同样会导致阻塞
计数命令只对整数有效 浮点数要用 INCRBYFLOAT
SET 会清除原有 TTL 覆盖写时需注意过期时间是否需要重新设置
键名设计要规范 使用 业务:对象:id 格式,避免冲突

下一篇预告:Redis Hash 哈希类型 —— 全部命令详解、内部编码机制、与字符串存储方式的对比,以及在用户信息存储等场景中的实战应用。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐