IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。
上一篇我们学会了用 redis-py 连接 Redis、操作五大基础数据类型,还掌握了连接池和 Pipeline 两大性能神器。但生产环境远比"读写键值"复杂:多个命令需要原子执行怎么办?怎样高效存储 Python 对象?高并发异步项目如何接入?代码中到处散落的 Redis 调用又该如何优雅封装?
本篇作为 Python 操作 Redis 的进阶篇,将逐一攻克这些难题:事务与 Lua 脚本的 Python 调用 、对象序列化方案 、异步 Redis(redis.asyncio) 以及工具类封装。掌握它们,你的 Redis 代码就能从"能用"跃升至"专业"。
1. 事务:让多个命令原子执行
Redis 的事务通过 MULTI/EXEC 实现,能将一组命令打包,在执行期间不会被其他客户端的命令插队。redis-py 中使用 Pipeline(transaction=True) 即可开启事务模式。
1.1 基础事务------转账
模拟账户 A 给账户 B 转账 50 元,需要扣减 A、增加 B。这两个操作必须原子化。
bash
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# 初始化余额
r.set('account:A', 100)
r.set('account:B', 200)
def transfer(from_account, to_account, amount):
pipe = r.pipeline(transaction=True)
pipe.decrby(from_account, amount)
pipe.incrby(to_account, amount)
results = pipe.execute()
return results
print("转账前: A =", r.get('account:A'), "B =", r.get('account:B'))
transfer('account:A', 'account:B', 50)
print("转账后: A =", r.get('account:A'), "B =", r.get('account:B'))
输出:
bash
转账前: A = 100 B = 200
转账后: A = 50 B = 250
pipe.execute() 返回一个列表 [50, 250],分别对应两个命令的执行结果。中间不会出现 A 扣了钱 B 没加的中间态。
1.2 乐观锁------WATCH 实现 CAS
事务不提供回滚,但可以通过 WATCH 监听键,若事务执行前键被修改,则取消事务,实现乐观锁。
bash
def safe_transfer(from_acc, to_acc, amount):
while True:
r.watch(from_acc)
balance = int(r.get(from_acc))
if balance < amount:
r.unwatch()
raise ValueError('余额不足')
pipe = r.pipeline(transaction=True)
pipe.decrby(from_acc, amount)
pipe.incrby(to_acc, amount)
try:
pipe.execute()
break
except redis.WatchError:
print('冲突重试...')
continue
r.unwatch()
# 测试
r.set('account:A', 100)
r.set('account:B', 200)
safe_transfer('account:A', 'account:B', 30)
print("A =", r.get('account:A'), "B =", r.get('account:B'))
若并发修改 from_acc,execute() 会抛出 WatchError,循环重试即可。这适用于竞争不激烈的场景。
💡 事务在
EXEC执行时才真正运行,MULTI期间的命令只是排队。如果原子性要求极高且逻辑复杂,推荐直接使用 Lua 脚本。
2. Lua 脚本:原子性的终极武器
Lua 脚本在 Redis 服务端执行,天然原子,且能减少网络往返。复杂的判断、循环、条件更新,一个脚本搞定。
2.1 库存扣减------防超卖
需求:扣减商品库存,库存不足时返回失败。
bash
# Lua 脚本:原子扣减库存
lua_inventory = """
local key = KEYS[1]
local delta = tonumber(ARGV[1])
local current = redis.call('GET', key)
if current == false then
return -1 -- 键不存在
end
current = tonumber(current)
if current < delta then
return 0 -- 库存不足
end
redis.call('DECRBY', key, delta)
return 1 -- 成功
"""
# 注册脚本(缓存 SHA)
inventory_script = r.register_script(lua_inventory)
# 初始化库存
r.set('product:1001:stock', 10)
# 模拟扣减
result = inventory_script(keys=['product:1001:stock'], args=[3])
print("扣减结果:", result) # 1 成功
print("剩余库存:", r.get('product:1001:stock')) # 7
# 尝试超量扣减
result = inventory_script(keys=['product:1001:stock'], args=[10])
print("超量扣减:", result) # 0 库存不足
# 不存在的商品
result = inventory_script(keys=['product:9999:stock'], args=[1])
print("不存在:", result) # -1
输出:
bash
扣减结果: 1
剩余库存: 7
超量扣减: 0
不存在: -1
register_script() 会自动缓存脚本的 SHA 值,后续调用使用 EVALSHA 避免重复传输脚本内容,高效又简洁。
2.2 Lua 脚本的传参细节
-
KEYS:键名列表,通过keys关键字传入。 -
ARGV:参数列表,通过args关键字传入。 -
返回值:Lua 中的
return会直接返回给 Python,支持数字、字符串、表(转为列表)。
bash
script = """
local name = redis.call('GET', KEYS[1])
local count = redis.call('INCR', KEYS[2])
return {name, count}
"""
r.set('username', 'Alice')
result = r.register_script(script)(keys=['username', 'counter'], args=[])
print(result) # ['Alice', 1]
⚠️ Lua 脚本执行期间会阻塞其他命令,务必轻量快速,避免死循环或长时间计算。
3. 序列化方案:如何优雅存储 Python 对象
Redis 值只能存字符串或字节,遇到 Python 对象就需要序列化。常见三大方案:
3.1 JSON 方案(推荐优先使用)
bash
import json
def cache_user_json(r, user_id, user_dict):
r.set(f'user:{user_id}', json.dumps(user_dict, ensure_ascii=False), ex=300)
def get_user_json(r, user_id):
data = r.get(f'user:{user_id}')
return json.loads(data) if data else None
user = {'id': 1001, 'name': 'Alice', 'tags': ['redis', 'python']}
cache_user_json(r, 1001, user)
cached = get_user_json(r, 1001)
print(cached) # {'id': 1001, 'name': 'Alice', 'tags': ['redis', 'python']}
优点:跨语言、可读、安全。缺点:速度一般,datetime 等类型需手动处理。
3.2 MessagePack 方案(高性能场景)
安装:pip install msgpack
bash
import msgpack
def cache_user_msgpack(r, user_id, user_dict):
# use_bin_type=True 保证 bytes 类型正确
packed = msgpack.packb(user_dict, use_bin_type=True)
r.set(f'user:{user_id}', packed, ex=300)
def get_user_msgpack(r, user_id):
data = r.get(f'user:{user_id}')
return msgpack.unpackb(data, raw=False) if data else None
cache_user_msgpack(r, 1002, user)
cached = get_user_msgpack(r, 1002)
print(cached) # {'id': 1001, 'name': 'Alice', 'tags': ['redis', 'python']}
性能对比(1000次序列化/反序列化,粗略测试):
bash
import time, pickle
data = {'id': 1, 'name': 'Alice', 'tags': ['redis', 'python'] * 10}
# JSON
start = time.perf_counter()
for _ in range(10000):
packed = json.dumps(data)
unpacked = json.loads(packed)
print(f'JSON: {time.perf_counter() - start:.3f}s')
# Pickle
start = time.perf_counter()
for _ in range(10000):
packed = pickle.dumps(data)
unpacked = pickle.loads(packed)
print(f'Pickle: {time.perf_counter() - start:.3f}s')
# MessagePack
start = time.perf_counter()
for _ in range(10000):
packed = msgpack.packb(data)
unpacked = msgpack.unpackb(packed)
print(f'MsgPack: {time.perf_counter() - start:.3f}s')
输出示例:
bash
JSON: 0.045s
Pickle: 0.021s
MsgPack: 0.018s
MessagePack 速度和体积都有优势,适合高吞吐缓存。
🚫 避免 Pickle:Pickle 可执行任意代码,攻击者若控制 Redis 写入恶意 Pickle 数据,反序列化时可执行系统命令。且不同 Python 版本兼容性差。生产环境绝对禁用!
3.3 最佳实践
-
默认使用 JSON ,配合
ensure_ascii=False节省空间,处理好datetime等特殊类型。 -
对性能要求极高且数据量大,使用 MessagePack。
-
序列化/反序列化统一封装,方便后续切换方案。
-
避免存储超大数据对象(>10MB),会阻塞 Redis,消耗带宽。
4. 异步 Redis:redis.asyncio 高并发利器
Python 异步框架(FastAPI、Sanic、asyncio)日益流行,使用同步 redis-py 会阻塞事件循环。redis-py 4.2+ 内置了 redis.asyncio 模块,API 与同步版高度一致。
4.1 基本用法
bash
import asyncio
import redis.asyncio as aioredis
async def main():
# 创建异步客户端
r = await aioredis.from_url(
'redis://localhost:6379/0',
decode_responses=True
)
# 基本操作
await r.set('async_key', 'Hello Async')
value = await r.get('async_key')
print(value) # Hello Async
# Pipeline
async with r.pipeline() as pipe:
pipe.set('a', '1')
pipe.set('b', '2')
results = await pipe.execute()
print(results) # [True, True]
# 关闭连接
await r.close()
asyncio.run(main())
注意所有命令都要 await,Pipeline 使用 async with 上下文。
4.2 并发请求对比
模拟 100 个并发 GET 请求:
bash
import time
import redis
import redis.asyncio as aioredis
# 同步版本
def sync_bench():
r = redis.Redis(decode_responses=True)
r.set('test', 'value')
start = time.perf_counter()
for _ in range(100):
r.get('test')
print(f'同步耗时: {time.perf_counter() - start:.4f}s')
# 异步版本
async def async_bench():
r = await aioredis.from_url('redis://localhost', decode_responses=True)
await r.set('test', 'value')
start = time.perf_counter()
tasks = [r.get('test') for _ in range(100)]
await asyncio.gather(*tasks)
print(f'异步并发耗时: {time.perf_counter() - start:.4f}s')
await r.close()
sync_bench()
asyncio.run(async_bench())
输出示例(本地):
bash
同步耗时: 0.0382s
异步并发耗时: 0.0091s
并发场景下异步优势明显,且不会阻塞主线程的其他任务。
4.3 连接池管理
异步连接池同样重要:
bash
async def create_pool():
pool = aioredis.ConnectionPool.from_url(
'redis://localhost',
max_connections=20,
decode_responses=True
)
client = aioredis.Redis(connection_pool=pool)
return client
# 使用
async def worker():
r = await create_pool()
# ... 操作 ...
await r.close()
💡
from_url()方法实际上会创建默认连接池,简单场景无需手动管理。但在多协程共享 client 时,手动创建连接池并传入多个Redis实例会更清晰。
5. 工具类封装:生产级 RedisHelper
项目中到处散落 Redis 调用不易维护。我们封装一个 RedisClient,集成连接池、序列化、缓存穿透保护、异常处理等。
bash
import json
import redis
from redis.exceptions import RedisError
from typing import Optional, Any
class RedisClient:
"""生产级 Redis 工具类"""
def __init__(self, host='localhost', port=6379, db=0, password=None,
max_connections=20, decode_responses=False):
self.pool = redis.ConnectionPool(
host=host, port=port, db=db, password=password,
max_connections=max_connections,
decode_responses=decode_responses,
socket_timeout=5, socket_connect_timeout=5,
health_check_interval=30
)
self.client = redis.Redis(connection_pool=self.pool)
def set_json(self, key: str, value: dict, ex: int = None):
"""存储 JSON 对象"""
self.client.set(key, json.dumps(value, ensure_ascii=False), ex=ex)
def get_json(self, key: str) -> Optional[dict]:
"""获取 JSON 对象,不存在或异常返回 None"""
try:
data = self.client.get(key)
return json.loads(data) if data else None
except (RedisError, json.JSONDecodeError) as e:
print(f'[Redis] get_json error: {e}')
return None
def get_or_set(self, key: str, func, ex: int = 60):
"""缓存穿透保护:存在直接返回,不存在则调用 func 获取并缓存"""
cached = self.get_json(key)
if cached is not None:
return cached
# 查询数据源
data = func()
if data is not None:
self.set_json(key, data, ex)
return data
def delete(self, key: str):
self.client.delete(key)
def pipeline(self):
return self.client.pipeline()
def close(self):
self.pool.disconnect()
# ---------- 使用示例 ----------
cache = RedisClient(decode_responses=False)
# 存储用户信息
user = {'id': 1001, 'name': 'IT策士', 'score': 100}
cache.set_json('user:1001', user, ex=120)
# 获取
print(cache.get_json('user:1001'))
# 利用 get_or_set 实现缓存读取模板
def query_db():
# 模拟从数据库查询
print('查询数据库...')
return {'id': 2001, 'name': 'Bob', 'score': 85}
print(cache.get_or_set('user:2001', query_db, ex=60)) # 第一次查库
print(cache.get_or_set('user:2001', query_db, ex=60)) # 命中缓存
输出:
bash
{'id': 1001, 'name': 'IT策士', 'score': 100}
查询数据库...
{'id': 2001, 'name': 'Bob', 'score': 85}
{'id': 2001, 'name': 'Bob', 'score': 85}
后续可根据需要扩展 lua 方法、incr_json_field 等,形成团队标准库。
6. 动手试试
-
Lua 原子操作:用 Lua 实现"抢红包"逻辑,随机生成红包金额,确保总金额不超,并将抢到的金额写入用户的 Hash。
-
序列化性能 :分别用 JSON、Pickle(仅供学习,别上生产)、MessagePack 存储一个包含 10 万条简单数据的列表,记录写入耗时和
MEMORY USAGE大小。 -
异步多任务 :用
redis.asyncio同时执行 50 个INCR操作,用asyncio.gather收集结果,验证原子性(最后计数器值为 50)。 -
封装实战 :基于
RedisClient扩展一个hash_get_or_set方法,利用 Hash 做对象缓存,支持部分字段更新。
预期效果:Lua 红包总额一分不差;MsgPack 体积最小;异步并发速度快;工具类让后续代码量减少 50%。
7. 总结
本篇我们从工程化角度,将 Python 操作 Redis 的能力提升了一个台阶:
-
事务 :用
Pipeline(transaction=True)实现原子批处理,WATCH做乐观锁。 -
Lua 脚本 :
register_script让复杂逻辑在服务端原子执行,性能与安全兼得。 -
序列化:JSON 通用首选,MessagePack 高性能场景利器,坚决避开 Pickle。
-
异步 :
redis.asyncio无缝接入异步生态,并发优势显著。 -
工具类封装:统一连接管理、序列化和缓存模板,提升代码复用与可维护性。
至此,Python 操作 Redis 的核心技能你已经掌握。下一章,我们将跳出数据操作,进入 Redis 的发布订阅与消息队列初探,看看 Redis 如何扮演消息中间件的角色。
想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !