3. Redis 基本数据结构(上):String 类型详解

Redis 提供了五种核心数据结构,其中 String(字符串) 是最基础、应用最广泛的一种。String 类型可以存储字符串、整数、浮点数,甚至二进制数据(如图片序列化后的字节流),最大容量为 512 MB。本章将深入解析 String 类型的底层实现、常用命令及其三大经典应用场景。

3.1 String 类型底层结构

Redis 的 String 并非简单的 C 字符串,而是采用 SDS(Simple Dynamic String,简单动态字符串) 结构。SDS 在 C 字符串的基础上增加了 len(已用长度)和 free(未用空间)字段,实现了 O(1) 长度获取、预分配空间、惰性空间释放等优化,有效避免了缓冲区溢出问题。

核心优势: SDS 是二进制安全的,可以存储包含 \0 的任意二进制数据,而 C 字符串以 \0 结尾,无法处理此类数据。

3.2 常用命令详解

String 类型的命令分为基础操作、数值操作和批量操作三类。下表列出了最常用的命令及其时间复杂度:

命令 语法 时间复杂度 说明
SET SET key value [NX|XX] [EX seconds|PX milliseconds] O(1) 设置指定 key 的值;支持 NX(不存在时设置)、XX(存在时设置)、EX(秒级过期)、PX(毫秒级过期)
GET GET key O(1) 获取指定 key 的值;key 不存在返回 nil
INCR INCR key O(1) 将 key 中存储的数字值加 1;如果 key 不存在,则先初始化为 0 再执行 INCR
DECR DECR key O(1) 将 key 中存储的数字值减 1;如果 key 不存在,则先初始化为 0 再执行 DECR
INCRBY INCRBY key increment O(1) 将 key 中存储的数字值加上指定的增量
DECRBY DECRBY key decrement O(1) 将 key 中存储的数字值减去指定的减量
MSET MSET key1 value1 [key2 value2 ...] O(N) 批量设置多个 key-value 对,原子操作
MGET MGET key1 [key2 ...] O(N) 批量获取多个 key 的值
GETSET GETSET key value O(1) 设置新值并返回旧值,常用于原子性的计数器重置
STRLEN STRLEN key O(1) 返回 key 所存储的字符串值的长度

3.2.1 基础操作示例

# 设置缓存
SET user:1001:name "Alice" EX 3600
# 获取缓存
GET user:1001:name
# 输出: "Alice"

# 使用 NX 选项实现"不存在则设置"
SET lock:order:12345 "locked" NX EX 30
# 如果 key 不存在,设置成功返回 OK;如果已存在,返回 nil

# 批量操作
MSET user:1001:age 30 user:1002:age 25
MGET user:1001:age user:1002:age
# 输出: 1) "30"  2) "25"

3.2.2 数值操作示例

# 初始化计数器
SET page:view:home 0

# 自增
INCR page:view:home
# 输出: (integer) 1

# 指定增量
INCRBY page:view:home 10
# 输出: (integer) 11

# 自减
DECR page:view:home
# 输出: (integer) 10

# 原子性获取并重置
GETSET page:view:home 0
# 输出: "10"  (返回旧值,同时重置为 0)
注意: INCR/DECR 操作是原子性的,底层通过 Redis 单线程模型保证并发安全。但如果 value 不是整数(如包含字母),执行 INCR 会返回错误 (error) ERR value is not an integer or out of range。

3.3 三大经典应用场景

String 类型凭借其简单、高效、原子性的特点,支撑了 Redis 最核心的三种应用模式。下图展示了这三种场景的架构关系:

String 类型三大应用场景架构图 Redis String 缓存 计数器 分布式锁 缓存 • 热点数据临时存储 • 减轻数据库压力 • 支持 EX/PX 过期时间 • 典型:用户 Session、 页面片段、API 响应 计数器 • 原子自增/自减 • 高并发无锁操作 • 支持 INCRBY 自定义步长 • 典型:页面访问量、 点赞数、限流令牌 分布式锁 • SET NX EX 原子命令 • 防止并发重复执行 • 自动过期防死锁 • 典型:秒杀扣库存、 定时任务调度、订单防重

3.4 场景一:缓存

缓存是 Redis 最普遍的应用。String 类型配合 SET 的过期时间参数,可以轻松实现 缓存 + 自动失效 机制。

3.4.1 缓存工作流程

  1. 读请求:先查询 Redis,命中则直接返回;未命中则查询数据库。
  2. 回写缓存:从数据库查询到数据后,写入 Redis 并设置过期时间。
  3. 缓存失效:过期后自动删除,下次请求重新加载。
// 伪代码示例:用户信息缓存
public User getUserById(String userId) {
    // 1. 查询缓存
    String userJson = redis.get("user:" + userId);
    if (userJson != null) {
        return JSON.parse(userJson, User.class);
    }
    // 2. 缓存未命中,查询数据库
    User user = userDao.findById(userId);
    if (user != null) {
        // 3. 回写缓存,过期时间 1 小时
        redis.set("user:" + userId, JSON.stringify(user), "EX", 3600);
    }
    return user;
}
最佳实践: 缓存 key 的命名建议采用 业务:对象类型:ID:字段 的格式,如 user:1001:profile,便于管理和避免冲突。

3.5 场景二:计数器

Redis 单线程模型保证了 INCR/DECR 的原子性,非常适合高并发计数场景。

3.5.1 典型应用

场景 实现方式 关键命令
页面 PV/UV 统计 每个页面一个 key,每次访问 INCR INCR page:pv:home
文章点赞/收藏数 每篇文章一个 key,点赞 INCR,取消点赞 DECR INCR article:like:10086
接口限流(令牌桶) 每个用户/接口一个 key,每次请求 INCR,超过阈值拒绝 INCR rate:limit:user:1001 + EXPIRE
生成唯一递增 ID 使用 INCR 生成全局唯一序列号 INCR global:id:order
# 限流示例:1 分钟内最多允许 100 次请求
SET rate:limit:user:1001 0 NX EX 60   # 初始化,过期时间 60 秒
INCR rate:limit:user:1001              # 每次请求自增
# 判断返回值是否超过 100,超过则拒绝请求

3.6 场景三:分布式锁

在分布式系统中,多个进程需要互斥访问共享资源。Redis 利用 SET NX EX 的原子性,可以高效实现分布式锁。

3.6.1 加锁与解锁流程

# 加锁:key 不存在时设置成功,返回 OK;已存在则失败
SET lock:order:12345 "thread-1" NX EX 30

# 解锁:使用 Lua 脚本保证原子性,防止误删其他线程的锁
# 脚本内容:先判断 value 是否为本线程标识,是则删除
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

3.6.2 分布式锁关键设计点

  • 原子性:SET NX EX 是一条命令,避免了“先判断再设置”的竞态条件。
  • 自动过期:设置过期时间防止死锁(如持有锁的进程崩溃)。
  • 唯一标识:value 存储线程/请求的唯一 ID,解锁时校验,防止误删其他线程的锁。
  • Lua 脚本:解锁操作使用 Lua 脚本保证“判断+删除”的原子性。
生产环境注意: 简单的 SET NX 锁在 Redis 主从切换时可能出现锁丢失问题。生产环境建议使用 Redlock 算法或 Redis 官方推荐的 Redisson 客户端,它们提供了更可靠的分布式锁实现。

3.7 本章小结

String 类型是 Redis 数据结构的基石,其 SDS 底层设计保证了高性能和二进制安全。通过 SET/GET/INCR/DECR 等命令,String 可以灵活支撑缓存、计数器、分布式锁三大核心场景。掌握 String 类型,就掌握了 Redis 应用的一半精髓。