艾体宝干货 | Redis Python 开发系列#2 核心数据结构(上)
前言
继上篇文章,成功连接 Redis 之后,我们直面其核心:数据结构。Redis 的强大,绝非仅是简单的键值存储,而是其精心设计的多种数据结构所能解决的各种业务场景。
本篇读者收益
- 精通 String 类型的全部核心命令,掌握其在缓存、计数器、分布式锁中的应用。
- 精通 Hash 类型的全部核心命令,掌握其高效存储对象、进行分组统计的技巧。
- 深刻理解 String 和 Hash 的底层差异与内存效率,能根据场景做出正确选择。
- 了解生产环境中使用这两种结构时的常见“坑”与最佳实践。
先修要求
本文假设读者已掌握如何使用 redis-py 建立连接(详见系列第一篇)。
关键要点
- String 是万金油,可存文本、数字、序列化数据,INCR 命令是原子操作的典范。
- Hash 适合存储对象,能单独操作字段,内存效率更高(使用 ziplist 编码时)。
- MSET/MGET 和 HMSET(已弃用,用 HSET 替代)/HMGET 是提升批量操作性能的关键。
- 选择 String 还是 Hash 存储对象,是一场 序列化开销 vs 字段管理复杂度 的权衡。
背景与原理简述
Redis 提供了五种核心数据结构,本篇聚焦最基础也最常用的两种:String(字符串) 和 Hash(哈希散列) 。
- String: 最简单的类型,一个 Key 对应一个 Value。虽然是字符串,但可以存储任何二进制安全的数据,包括图片、序列化后的对象等。它是实现其他复杂功能的基石。
- Hash: 一个 Key 对应一个 Field-Value 的映射表。非常适合用来存储对象(如用户信息、商品属性),你可以单独获取、更新对象的某个字段,而无需操作整个对象。
理解它们的底层实现和适用场景,是写出高效 Redis 应用的关键。
环境准备与快速上手
假设阅读本篇时你已安装 redis-py 并能够成功连接 Redis 服务器。本篇所有示例将基于以下连接客户端展开:
# filename: setup.py
import os
import redis
from redis import Redis
# 使用连接池创建客户端(推荐方式,详见第一篇文章)
pool = redis.ConnectionPool(
host=os.getenv('REDIS_HOST', 'localhost'),
port=int(os.getenv('REDIS_PORT', 6379)),
password=os.getenv('REDIS_PASSWORD'), # 若无密码可注释此行
decode_responses=True, # 自动解码,省去 .decode()
max_connections=10
)
r = Redis(connection_pool=pool)
# 简单的连接测试
assert r.ping() is True
print("连接成功,开始操作 String 和 Hash!")
核心用法与代码示例
String (字符串) 操作
基本操作与应用场景
# filename: string_operations.py
def string_basic_operations():
"""String 基本操作:缓存、存值、取值"""
# 1. 简单设置与获取 (SET/GET)
# 应用场景:简单缓存、存储配置项
r.set('username', 'alice')
username = r.get('username') # 返回 'alice' (因为设置了 decode_responses=True)
print(f"Username: {username}")
# 2. 设置过期时间 (SETEX)
# 应用场景:手机验证码、临时会话、限时优惠券
r.setex('sms_code:13800138000', 300, '123456') # 300秒后自动过期
code = r.get('sms_code:13800138000')
print(f"SMS Code: {code}")
ttl = r.ttl('sms_code:13800138000') # 查看剩余生存时间
print(f"TTL: {ttl} seconds")
# 3. 仅当键不存在时设置 (SETNX)
# 应用场景:分布式锁、首次初始化
success = r.setnx('initialized', 'true')
if success:
print("系统初始化标记设置成功!")
else:
print("系统已初始化过。")
# 4. 批量操作 (MSET/MGET) - 大幅减少网络往返
# 应用场景:批量初始化配置、批量获取用户状态
r.mset({"config:theme": "dark", "config:language": "zh-CN", "config:notifications": "on"})
configs = r.mget(["config:theme", "config:language", "config:notifications"])
print(f"Batch configs: {configs}") # ['dark', 'zh-CN', 'on']
# 运行示例
string_basic_operations()
数值操作与应用场景
# filename: string_counter.py
def string_counter_operations():
"""String 数值操作:计数器"""
# 初始化一个计数器
r.set('page_views', 0)
# 1. 递增 (INCR/INCRBY)
# 应用场景:文章阅读量、用户点赞数、秒杀库存
new_views = r.incr('page_views') # +1,返回 1
new_views = r.incr('page_views') # +1,返回 2
new_views = r.incrby('page_views', 10) # +10,返回 12
print(f"Page views: {new_views}")
# 2. 递减 (DECR/DECRBY)
# 应用场景:扣减库存、撤销操作
stock = r.decrby('product:1001:stock', 5) # 扣减5个库存
print(f"Current stock: {stock}")
# 3. 浮点数操作 (INCRBYFLOAT)
# 应用场景:金额、分数、权重
r.set('account:balance', 100.5)
new_balance = r.incrbyfloat('account:balance', 20.8) # 增加 20.8
print(f"New balance: {new_balance}") # 121.3
# 运行示例
string_counter_operations()
Hash (哈希散列) 操作
基本操作与应用场景
# filename: hash_operations.py
def hash_basic_operations():
"""Hash 基本操作:对象存储"""
user_id = 1001
# 1. 设置和获取字段 (HSET/HGET)
# 应用场景:存储对象属性
r.hset(f'user:{user_id}', 'name', 'Alice')
r.hset(f'user:{user_id}', 'email', 'alice@example.com')
user_name = r.hget(f'user:{user_id}', 'name')
print(f"User name: {user_name}")
# 2. 批量设置和获取字段 (HMSET is deprecated, use HSET with mapping)
# 应用场景:一次性设置或获取对象的所有属性
user_data = {
'age': '30', # Note: Hash field values are always strings
'city': 'Beijing',
'occupation': 'Engineer'
}
r.hset(f'user:{user_id}', mapping=user_data) # 批量设置
# 批量获取多个字段
fields = ['name', 'email', 'age', 'city']
user_info = r.hmget(f'user:{user_id}', fields)
print(f"User info (list): {user_info}") # ['Alice', 'alice@example.com', '30', 'Beijing']
# 3. 获取所有字段和值 (HGETALL)
# 小心使用!如果Hash很大,可能会阻塞服务器或消耗大量网络带宽。
all_user_data = r.hgetall(f'user:{user_id}')
print(f"All user data (dict): {all_user_data}") # {'name': 'Alice', 'email': 'alice@example.com', ...}
# 4. 获取所有字段名或值 (HKEYS/HVALS)
field_names = r.hkeys(f'user:{user_id}')
field_values = r.hvals(f'user:{user_id}')
print(f"Field names: {field_names}")
print(f"Field values: {field_values}")
# 5. 判断字段是否存在 (HEXISTS) 和 删除字段 (HDEL)
if r.hexists(f'user:{user_id}', 'email'):
print("Email field exists.")
r.hdel(f'user:{user_id}', 'occupation') # 删除一个字段
print(f"Fields after deletion: {r.hkeys(f'user:{user_id}')}")
# 运行示例
hash_basic_operations()
数值操作与应用场景
# filename: hash_counter.py
def hash_counter_operations():
"""Hash 字段的数值操作"""
product_id = 2001
key = f'product:{product_id}'
# 初始化
r.hset(key, 'price', '99.9')
r.hset(key, 'views', '0')
# 哈希字段的递增递减 (HINCRBY/HINCRBYFLOAT)
# 应用场景:商品价格调整、独立计数器(如商品浏览量)
new_views = r.hincrby(key, 'views', 1) # 整数字段 +1
new_price = r.hincrbyfloat(key, 'price', -10.5) # 浮点字段 -10.5
print(f"Product views: {new_views}, New price: {new_price}")
# 运行示例
hash_counter_operations()
性能优化与容量规划
String vs. Hash:如何选择?
存储对象时这是一个常见的设计决策。其实对于 Redis 上的对象存储,更推荐使用 RedisJSON 拓展进行直接存储,当然这不在本篇的讨论范围内,就 String 与 Hash 的选用上,给出参考如下。
使用 String (存储 JSON):
import json
user_data = {'name': 'Alice', 'age': 30, 'city': 'Beijing'}
# 写入
r.set('user:1001', json.dumps(user_data))
# 读取(无法部分更新,必须读取整个对象)
data = json.loads(r.get('user:1001'))
- 优点: 简单直观,可利用 JSON 的复杂结构。
- 缺点: 无法原子性地更新单个字段。每次修改任何属性都需要序列化并写入整个对象,网络和CPU开销大。读取任何属性也需反序列化整个对象。
使用 Hash (存储字段):
# 写入
r.hset('user:1001', mapping={'name': 'Alice', 'age': '30', 'city': 'Beijing'})
# 读取单个字段(高效)
name = r.hget('user:1001', 'name')
# 更新单个字段(原子高效)
r.hset('user:1001', 'age', '31')
- 优点: 可以原子性地、独立地访问和修改每个字段,非常高效。内存优化更好(使用 ziplist 编码时)。
- 缺点: 无法直接存储嵌套结构,字段值只能是字符串。
对于需要频繁部分读写、字段较多的扁平化对象(如用户配置、商品属性),Hash 是更优选择。对于读写不频繁或结构复杂嵌套的对象,String + JSON 也是一种可选方案。
内存优化:ziplist 编码
Redis 在存储小的 Hash 时,会使用一种叫 ziplist(压缩列表) 的紧凑编码,这比使用标准的哈希表更节省内存。当以下两个配置阈值被突破时,编码会转换为 hashtable:
hash-max-ziplist-entries: Hash 中字段数量的阈值(默认 512)。hash-max-ziplist-value: 每个字段值的最大长度阈值(默认 64 字节)。
最佳实践:根据你的业务数据特点,在 redis.conf 中适当调整这两个参数,可以在内存和性能之间取得更好的平衡。
批量操作
无论是 String 的 MSET/MGET 还是 Hash 的 HMSET(已弃用)/HMGET,批量操作都能极大减少网络往返次数(RTT) ,是提升性能的最有效手段之一。
安全与可靠性
-
大 Key 风险: 避免使用一个巨大的 String(通常超过 10KB 被定义为 Big Key)或一个包含成千上万个字段的 Hash。这类 Key 在持久化、迁移、删除时可能会阻塞 Redis 服务。对 Hash,定期检查 HLEN。
-
命令复杂度:
- HGETALL、HKEYS、HVALS 这些 O(n) 复杂度的命令,在 Hash 很大时会非常慢,在生产环境中应谨慎使用。优先使用 HGET 或 HMGET 获取你真正需要的字段。
KEYS *是 O(n) 且会阻塞服务,绝对禁止在生产环境使用。使用 SCAN 命令族进行增量迭代(后续文章会详述)。
常见问题与排错
-
redis.exceptions.DataError: 尝试对非数字值的 String 或 Hash 字段执行 INCR 等操作。确保操作前值是数字或键不存在。 -
字段值类型错误: Hash 的字段值总是字符串。存储数字后,取回来也是字符串形式(如 '30'),需要客户端自己转换(
int(),float())。 -
HGETALL 返回类型**: 在 redis-py 中,HGETALL 返回的是一个 Python dict,但在其他一些客户端中可能返回列表。
-
内存增长过快:
- 检查是否滥用 String 存储了大对象。
- 检查 Hash 的字段数量是否过多,考虑是否可用多个 Hash 进行分片。
实战案例/最佳实践
案例:用户会话(Session)存储
# filename: session_manager.py
import uuid
import time
class SessionManager:
def __init__(self, redis_client):
self.r = redis_client
def create_session(self, user_id, user_agent, **extra_data):
"""创建一个新的用户会话(使用Hash存储)"""
session_id = str(uuid.uuid4())
session_key = f'session:{session_id}'
session_data = {
'user_id': str(user_id),
'user_agent': user_agent,
'created_at': str(time.time()),
'last_activity': str(time.time()),
**extra_data
}
# 使用Hash存储会话数据,并设置30分钟过期
self.r.hset(session_key, mapping=session_data)
self.r.expire(session_key, 30 * 60) # 30分钟TTL
return session_id
def get_session(self, session_id):
"""获取会话信息(只获取需要的字段,避免使用HGETALL)"""
session_key = f'session:{session_id}'
# 高效地获取特定字段,而不是全部
user_id = self.r.hget(session_key, 'user_id')
if not user_id:
return None # Session不存在或已过期
# 更新最后活动时间
self.r.hset(session_key, 'last_activity', str(time.time()))
self.r.expire(session_key, 30 * 60) # 刷新过期时间
# 按需获取其他字段
user_agent = self.r.hget(session_key, 'user_agent')
# ... 获取其他需要的字段
return {'user_id': user_id, 'user_agent': user_agent}
def update_session_field(self, session_id, field, value):
"""更新会话的单个字段(Hash的优势)"""
session_key = f'session:{session_id}'
self.r.hset(session_key, field, value)
self.r.expire(session_key, 30 * 60) # 刷新过期时间
# 使用示例
session_mgr = SessionManager(r)
sid = session_mgr.create_session(1001, 'Mozilla/5.0', theme='dark')
session_data = session_mgr.get_session(sid)
print(session_data)
小结
String 和 Hash 是 Redis 最基础、最常用的两种数据结构。String 灵活万能,是缓存和计数器的首选;Hash 字段独立,是存储扁平化对象、实现高效部分更新的最佳选择。
浙公网安备 33010602011771号