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"
MSET 和 MGET 是原子操作,一次往返完成多个键的读写,网络开销大幅降低。在循环中分别 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 / PX:SET 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 调用频率限制,都可以用 INCR 和 EXPIRE。
阅读量计数器:
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。
误区二:不设过期导致内存溢出
缓存数据一定要设置过期时间,并分散过期点(加随机偏移),避免同一时刻大量缓存同时失效(缓存雪崩)。可以用 EXPIRE 或 SETEX。
误区三:滥用 String 存储复杂对象
将所有对象序列化成 JSON 字符串存入 String 虽简单,但每次修改某个字段都需要整体取出、反序列化、修改、序列化、写回,浪费带宽和 CPU。若需要频繁修改部分字段,应使用 Hash。
误区四:忘记释放锁
使用分布式锁时必须注意客户端崩溃导致的死锁,因此必须设置过期时间,并且程序逻辑中务必使用 try/finally 或上下文管理器确保释放。
7. 动手试试
在你的 Redis 环境中完成以下挑战:
-
使用
MSET一次性设置三个用户属性,再用MGET取回,观察输出。 -
使用
SETEX存储一个 10 秒过期的 token,用TTL观察剩余时间变化。 -
编写一个 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 思维 !