Redis 从入门到精通:数据结构String 与键管理

IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。

在 Redis 的五种基础数据结构中,String 是最简单、最基础,却又是最万能的一种。无论是缓存对象、记录计数、存储序列化数据,还是实现分布式锁,背后几乎都能看到 String 的身影。但"简单"并不意味着"浅薄"------String 在 Redis 内部拥有精巧的编码优化,配合丰富的命令和键管理技巧,能帮你解决很多实际问题。

本文会从内部编码讲起,逐一拆解 String 的核心命令,然后结合 Python 代码,带你实战缓存、计数器和分布式锁三个经典场景。读完你会发现,一个小小的 String,也能玩出千般花样。

1. String 的内部编码:int、embstr、raw

你可能以为 Redis 的 String 就是一段字符串,其实不然。为了最大化节省内存和提升性能,Redis 对 String 采用了三种内部编码:

  • int :当值可以解释为 64 位有符号整数时,Redis 直接用整数存储,不分配字符串空间。这样,INCR 等操作直接在整型值上进行,时间复杂度 O(1)。

  • embstr:当字符串长度小于等于 44 字节(Redis 3.2+)时,使用嵌入式字符串。字符串对象和底层 SDS(简单动态字符串)内存连续分配,只需一次内存分配和释放,性能更高。

  • raw:长度超过 44 字节时,使用原始字符串,对象和 SDS 分开分配,适合大字符串。

我们可以用 OBJECT ENCODING 查看键的内部编码:

bash 复制代码
127.0.0.1:6379> SET age 30
OK
127.0.0.1:6379> OBJECT ENCODING age
"int"

127.0.0.1:6379> SET name "IT策士"
OK
127.0.0.1:6379> OBJECT ENCODING name
"embstr"

127.0.0.1:6379> SET longstr "这是一段超过四十四字节的字符串,用来展示raw编码的区别..."
OK
127.0.0.1:6379> OBJECT ENCODING longstr
"raw"

有了这层认识,以后操作 Redis 时就能意识到,短字符串和整数操作的效率往往超出你的预期。

2. String 核心命令速览

String 的命令虽然简单,但组合起来威力巨大。我们按使用场景,将常用命令分成几组。

2.1 基本读写:SET / GET / MSET / MGET

bash 复制代码
127.0.0.1:6379> SET user:1001:name "Alice"
OK
127.0.0.1:6379> GET user:1001:name
"Alice"

# 批量设置与批量获取,减少网络开销
127.0.0.1:6379> MSET user:1001:age 30 user:1001:city "Beijing"
OK
127.0.0.1:6379> MGET user:1001:age user:1001:city
1) "30"
2) "Beijing"

MSETMGET 是原子操作,一次往返完成多个键的读写,网络开销大幅降低。在循环中分别 SET 远远不如一次 MSET

2.2 条件设置:SETNX / SETEX / SET ... NX EX

  • SETNX key value:仅当 key 不存在时设置,常用来实现分布式锁。注意,SETNX 在 Redis 2.6.12 后可用 SET key value NX 代替,且支持同时设置过期。

  • SETEX key seconds value:原子地设置值并指定过期时间(秒)。

  • SET ... EX / PXSET key value EX 10 表示 10 秒过期。

bash 复制代码
127.0.0.1:6379> SET lock:task1 "locked" NX EX 30
OK
127.0.0.1:6379> SET lock:task1 "locked" NX EX 30
(nil)   # 第二次设置失败,因为键已存在

这种原子性的"若不存在则设置,且带过期"的语义,是实现分布式锁的核心基石。

2.3 数字操作:INCR / DECR / INCRBY / DECRBY

String 存储数字字符串时,Redis 允许直接进行原子加减,内部会自动处理编码为 int。

bash 复制代码
127.0.0.1:6379> SET article:101:views 0
OK
127.0.0.1:6379> INCR article:101:views
(integer) 1
127.0.0.1:6379> INCRBY article:101:views 10
(integer) 11
127.0.0.1:6379> DECR article:101:views
(integer) 10

计数器是 String 的经典用途,因为操作是原子的,无需担心并发覆盖问题。

2.4 字符串操作:APPEND / GETRANGE / SETRANGE / STRLEN

bash 复制代码
127.0.0.1:6379> SET slogan "Hello"
OK
127.0.0.1:6379> APPEND slogan " Redis"
(integer) 11
127.0.0.1:6379> GET slogan
"Hello Redis"
127.0.0.1:6379> GETRANGE slogan 0 4
"Hello"
127.0.0.1:6379> SETRANGE slogan 6 "World"
(integer) 11
127.0.0.1:6379> GET slogan
"Hello World"
127.0.0.1:6379> STRLEN slogan
(integer) 11

这些命令让你能像操作字符串一样,对 Redis 中的值进行部分读写,避免先 GET、修改、再 SET 的多次往返。

3. 键命名规范

随着系统壮大,键的管理会成为噩梦。遵循一套清晰的命名规范至关重要。

推荐格式业务模块:对象类型:对象ID:属性

例如:

  • user:profile:1001:name

  • order:cache:202406091234

  • lock:seckill:iphone15

要点:

  • 使用冒号 : 作为分隔符,Redis 的 GUI 工具和某些监控系统会自动按冒号分组展示。

  • 长度不宜过长,但需要明确语义。键本身也会占用内存。

  • 避免使用不符合规范的字符,如空格、特殊符号(. 可以使用,但 : 更通用)。

  • 批量操作时,可以利用 SCAN 命令配合通配符模式匹配,如 SCAN 0 MATCH user:profile:*

⚠️ 不要在键名中拼接用户输入的原始数据,否则可能引发安全风险或导致键名爆炸。

4. 过期与 TTL

过期为键赋予了"生命周期",是缓存和临时数据控制的基础。

相关命令

  • EXPIRE key seconds:设置过期时间(秒)。

  • PEXPIRE key milliseconds:毫秒级别过期。

  • TTL key:查看剩余秒数,返回 -1 表示无过期,-2 表示键不存在。

  • PERSIST key:移除过期时间,使键永久存在。

bash 复制代码
127.0.0.1:6379> SET temp "data" EX 20
OK
127.0.0.1:6379> TTL temp
(integer) 17
127.0.0.1:6379> PERSIST temp
(integer) 1
127.0.0.1:6379> TTL temp
(integer) -1

过期的底层实现采用惰性删除 + 定期删除,后续在内存管理一章会深入探讨。这里只需记住:设置合理的过期时间,防止内存无限增长

5. Python 实战 String

我们先安装 Python 客户端(如果未安装):

5.1 基础 String 操作的 Python 版本

bash 复制代码
import redis

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

# SET / GET
r.set('user:1001:name', 'Alice')
print(r.get('user:1001:name'))  # Alice

# MSET / MGET
r.mset({'user:1001:age': 30, 'user:1001:city': 'Beijing'})
print(r.mget(['user:1001:age', 'user:1001:city']))  # ['30', 'Beijing']

# 条件设置 (NX + EX)
success = r.set('lock:task1', 'locked', nx=True, ex=30)
print(f"第一次获取锁: {success}")  # True
success = r.set('lock:task1', 'locked', nx=True, ex=30)
print(f"第二次获取锁: {success}")  # False

# 数字操作
r.set('article:101:views', 0)
r.incr('article:101:views')
r.incrby('article:101:views', 10)
print(r.get('article:101:views'))  # 11

5.2 实战场景一:缓存

业务开发中最常见的用法:先从 Redis 读取数据,如果不存在则查询数据库,并将结果写入 Redis 并设置过期时间。

模拟数据库查询函数:

bash 复制代码
import time

def get_user_from_db(user_id):
    """模拟从数据库查询用户信息,耗时 200ms"""
    time.sleep(0.2)
    return {'id': user_id, 'name': f'User_{user_id}', 'age': 25}

def get_user(r, user_id):
    """带缓存的用户查询"""
    cache_key = f'user:cache:{user_id}'
    # 尝试从缓存获取
    user_json = r.get(cache_key)
    if user_json:
        print('缓存命中')
        import json
        return json.loads(user_json)
    
    print('缓存未命中,查询数据库...')
    user = get_user_from_db(user_id)
    # 写入缓存,序列化为 JSON,过期 60 秒
    r.setex(cache_key, 60, json.dumps(user, ensure_ascii=False))
    return user

# 测试
r = redis.Redis(host='localhost', port=6379, decode_responses=True)

user = get_user(r, 1001)
print(user)  # 缓存未命中,查询数据库...
user = get_user(r, 1001)  # 第二次,命中缓存
print(user)

输出示例:

bash 复制代码
缓存未命中,查询数据库...
{'id': 1001, 'name': 'User_1001', 'age': 25}
缓存命中
{'id': 1001, 'name': 'User_1001', 'age': 25}

实际开发中,需要根据数据一致性要求选择过期时长,或者配合主动失效机制。

5.3 实战场景二:计数器

比如实现文章阅读量计数、API 调用频率限制,都可以用 INCREXPIRE

阅读量计数器

bash 复制代码
def incr_view(r, article_id):
    key = f'article:{article_id}:views'
    return r.incr(key)

# 模拟阅读
for _ in range(5):
    print(f"阅读量: {incr_view(r, 102)}")

输出:

bash 复制代码
阅读量: 1
阅读量: 2
阅读量: 3
阅读量: 4
阅读量: 5

简单频率限制器(滑动窗口粗糙版)

bash 复制代码
def is_rate_limited(r, user_id, limit=10, window=60):
    key = f'rate:{user_id}'
    current = r.get(key)
    if current and int(current) >= limit:
        return True
    # 原子增加,并设置窗口(仅首次)
    pipe = r.pipeline()
    pipe.incr(key)
    pipe.expire(key, window)
    count, _ = pipe.execute()
    return int(count) > limit

# 测试
for i in range(12):
    if is_rate_limited(r, 'user123', limit=10):
        print(f'第{i+1}次请求:触发限流')
    else:
        print(f'第{i+1}次请求:正常')

这里用了 Pipeline 将两个命令打包执行,减少网络开销,虽然并非事务原子,但 incr 本身是原子的。

5.4 实战场景三:分布式锁基础

利用 SET key value NX EX seconds 可以在 Redis 单实例上实现一个简单的分布式锁。虽然生产级还需要 Redlock 或续期机制(后续章节详解),但这里先打下基础。

Python 上下文管理器实现:

bash 复制代码
import uuid

class RedisLock:
    def __init__(self, client, lock_name, expire=30):
        self.client = client
        self.lock_name = f'lock:{lock_name}'
        self.expire = expire
        self.lock_value = str(uuid.uuid4())  # 唯一值,用于安全释放

    def acquire(self):
        return self.client.set(self.lock_name, self.lock_value, nx=True, ex=self.expire)

    def release(self):
        # 使用 Lua 保证比较和删除的原子性(避免误删其他客户端的锁)
        script = """
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
        """
        self.client.eval(script, 1, self.lock_name, self.lock_value)

    def __enter__(self):
        if self.acquire():
            return self
        else:
            raise Exception("获取锁失败")

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.release()

# 使用示例
r = redis.Redis(host='localhost', port=6379, decode_responses=True)

try:
    with RedisLock(r, 'order:1001', expire=10) as lock:
        print("获取锁成功,执行业务...")
        # 模拟业务处理
        time.sleep(2)
        print("业务完成")
except Exception as e:
    print(e)

输出(第一次运行):

同时再次运行(如果锁未释放):

这里释放锁时用了一小段 Lua 脚本,确保只有持有者才能删除,避免了直接 DEL 带来的原子性问题。Lua 的详细用法我们会在后续章节展开。

6. 常见误区与最佳实践

误区一:大 Key 导致阻塞

由于 Redis 是单线程执行命令,一个巨大的 String(比如几 MB 的 JSON)使用 GET 会造成网络和内存带宽瓶颈,同时阻塞其他请求。建议将大对象拆分 Hash,或进行压缩处理。一般来说,单个 value 应控制在 10KB 以内,特殊场景也不宜超过 1MB。

误区二:不设过期导致内存溢出

缓存数据一定要设置过期时间,并分散过期点(加随机偏移),避免同一时刻大量缓存同时失效(缓存雪崩)。可以用 EXPIRESETEX

误区三:滥用 String 存储复杂对象

将所有对象序列化成 JSON 字符串存入 String 虽简单,但每次修改某个字段都需要整体取出、反序列化、修改、序列化、写回,浪费带宽和 CPU。若需要频繁修改部分字段,应使用 Hash。

误区四:忘记释放锁

使用分布式锁时必须注意客户端崩溃导致的死锁,因此必须设置过期时间,并且程序逻辑中务必使用 try/finally 或上下文管理器确保释放。

7. 动手试试

在你的 Redis 环境中完成以下挑战:

  1. 使用 MSET 一次性设置三个用户属性,再用 MGET 取回,观察输出。

  2. 使用 SETEX 存储一个 10 秒过期的 token,用 TTL 观察剩余时间变化。

  3. 编写一个 Python 脚本,模拟 100 个并发请求对同一个计数器执行 INCR(可使用 concurrent.futures 线程池),验证结果是否为 100。

预期效果:计数器最终准确为 100,因为 Redis 单线程保证了原子性。

8. 总结

String 看似简单,但通过精巧的内部编码和丰富的操作命令,它成为了 Redis 中最灵活的数据结构。我们学习了:

  • 三种内部编码(int、embstr、raw)及其优化目的;

  • 常用命令:SET/GET/MSET/MGET、SETNX/SETEX/条件设置、INCR/DECR、字符串局部操作;

  • 科学的键命名规范;

  • 过期和 TTL 管理;

  • 并亲手用 Python 实现了缓存、计数器和分布式锁的基础版本。

掌握了 String,你就掌握了 Redis 的"通用语言",可以应对绝大多数的缓存、计数和简单加锁需求。从下一篇开始,我们将深入更高级的数据结构------Hash 和 List,继续武装我们的 Redis 技能库。

想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !

相关推荐
小蒋学算法2 小时前
算法-计算右侧小于当前元素的个数-分治&归并思想
java·数据结构·算法
AC赳赳老秦2 小时前
技术文章素材收集自动化:用 OpenClaw 自动爬取行业资讯、技术热点、优质文章
运维·开发语言·python·自动化·wpf·deepseek·openclaw
j7~2 小时前
【C++】模板初阶--函数模板,类模板详解
数据结构·c++·算法·函数模板·类模板·函数模板实例化
闪电悠米2 小时前
黑马点评-Redis 消息队列-01_why_redis_mq
java·数据库·spring boot·redis·缓存·junit·消息队列
IT策士2 小时前
Redis 从入门到精通:初识 Redis
数据库·redis·缓存
IT策士2 小时前
Redis 从入门到精通:数据结构Hash 与 List
数据结构·redis·哈希算法
加号32 小时前
【WPF】 Storyboard 故事板动画设计深度解析
wpf
HZ·湘怡3 小时前
数据结构之排序算法 (1)--插入排序
c语言·数据结构·算法·排序算法
阿旭超级学得完3 小时前
Linux基础指令 四(apt,vim,git,cgdb)
linux·服务器·开发语言·数据结构·c++·git·vim