Redis 从入门到精通:Python 操作 Redis 进阶

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_accexecute() 会抛出 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. 动手试试

  1. Lua 原子操作:用 Lua 实现"抢红包"逻辑,随机生成红包金额,确保总金额不超,并将抢到的金额写入用户的 Hash。

  2. 序列化性能 :分别用 JSON、Pickle(仅供学习,别上生产)、MessagePack 存储一个包含 10 万条简单数据的列表,记录写入耗时和 MEMORY USAGE 大小。

  3. 异步多任务 :用 redis.asyncio 同时执行 50 个 INCR 操作,用 asyncio.gather 收集结果,验证原子性(最后计数器值为 50)。

  4. 封装实战 :基于 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 思维 !

相关推荐
uhakadotcom1 小时前
什么是Mass Assignment(批量赋值)风险
后端·面试·github
XovH1 小时前
Redis 从入门到精通:Python 操作 Redis
后端
HLAIA光子2 小时前
分布式锁与事务:你的微服务可能根本不需要它们
分布式·后端·微服务
砍材农夫2 小时前
物联网实战:Spring Boot + Netty 搭建 MQTT 统一接入层
java·网络·spring boot·后端·物联网·spring
苏三说技术2 小时前
MarkItDown 再次登顶GitHub榜
后端
IT_陈寒2 小时前
SpringBoot这个坑差点让我加班到天亮
前端·人工智能·后端
小小龙学IT2 小时前
Go 后端开发中的并发模式:从 Goroutine 到 Pipeline 实战
开发语言·后端·golang
geovindu2 小时前
go: Coroutines Pattern
开发语言·后端·设计模式·golang·协程模式
阿正的梦工坊3 小时前
【Rust】01-认识 Rust:语言定位、工具链与第一个程序
开发语言·后端·rust