Python 中间件系列:redis 深入浅出

前言

在 Python 后端开发中,Redis 基本属于"高频刚需型中间件"。只要系统里出现缓存、登录态、验证码、排行榜、消息通知、限流、分布式锁、异步任务队列等场景,Redis 往往都会出场。

Redis 官方文档把 Redis 定位为一种数据结构服务器,提供 String、Hash、List、Set、Sorted Set 等多种原生数据类型,可用于缓存、队列、事件处理等场景。Python 里常用的 Redis 客户端是 redis-py,官方文档推荐使用它连接 Redis,并且同时支持同步和异步 API。

一、Redis 在 Python 项目中的常见定位

Redis 在 Python 项目里通常不是"数据库替代品",而是一个高性能的辅助型中间件。

常见的职责包括:

  • 缓存热点数据:例如用户信息、商品详情、配置数据、接口返回结果。

  • 保存临时状态:例如短信验证码、登录 token、图形验证码、一次性链接。

  • 计数器:例如文章阅读量、接口访问次数、点赞数、库存扣减前的预校验。

  • 排行榜:例如游戏积分榜、销售榜、热门文章榜。

  • 简单消息队列:例如用 List 实现任务队列,用 Pub/Sub 做消息通知。

  • 分布式锁:例如防止重复提交、防止多个服务同时执行定时任务。

  • 限流:例如同一个用户 1 分钟内最多请求 60 次。

Redis 的优点是速度快、数据结构丰富、操作简单;缺点是不能无脑当主数据库使用,尤其是涉及强一致性、复杂事务、复杂查询时,还是应该交给 MySQL、PostgreSQL 等关系型数据库。

二、环境准备

1. 安装 Python Redis 客户端

官方推荐安装方式:

如果希望获得更好的解析性能,可以安装 hiredis 支持:

bash 复制代码
pip install "redis[hiredis]"

如果安装了 hiredis >= 1.0,redis-py 会尝试使用它作为响应解析器,通常不需要修改业务代码。

2. 启动 Redis

本地使用 Docker 启动 Redis:

bash 复制代码
docker run -d \
  --name redis-demo \
  -p 6379:6379 \
  redis:7

查看容器:

输出示例:

bash 复制代码
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS                    NAMES
f4b19e8a23c1   redis:7   "docker-entrypoint.s..."   12 seconds ago   Up 11 seconds   0.0.0.0:6379->6379/tcp   redis-demo

测试连接:

bash 复制代码
docker exec -it redis-demo redis-cli ping

输出:

三、Python 连接 Redis

1. 最简单连接方式

bash 复制代码
import redis

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

result = r.ping()
print(result)

输出:

重要参数decode_responses=True

如果不加该参数,Redis 返回的字符串通常是 bytes 类型。

bash 复制代码
import redis

r = redis.Redis(host="localhost", port=6379, db=0)

r.set("name", "vito")
print(r.get("name"))

输出:

加上 decode_responses=True 后:

bash 复制代码
import redis

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

r.set("name", "vito")
print(r.get("name"))

输出:

在大多数业务代码中,建议加上 decode_responses=True,否则后续处理字符串时经常会遇到 bytes 和 str 混用的问题。

四、String 操作:缓存、计数器、验证码

String 是 Redis 最常用的数据类型,可以存普通字符串、JSON 字符串、数字等。

1. set / get

bash 复制代码
import redis

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

r.set("user:1001:name", "张三")

name = r.get("user:1001:name")
print("用户名称:", name)

输出:

2. 设置过期时间

常见场景:短信验证码 5 分钟过期。

bash 复制代码
import redis

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

r.set("sms:code:13800138000", "9527", ex=300)

code = r.get("sms:code:13800138000")
ttl = r.ttl("sms:code:13800138000")

print("验证码:", code)
print("剩余过期时间:", ttl)

输出示例:

过几秒后再次执行:

3. 自增计数器

bash 复制代码
import redis

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

r.delete("article:1001:views")

print(r.incr("article:1001:views"))
print(r.incr("article:1001:views"))
print(r.incr("article:1001:views"))

输出:

非常适合做访问量统计。

4. 接口访问次数统计

bash 复制代码
import redis

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

user_id = 1001
key = f"api:visit:{user_id}"

count = r.incr(key)

if count == 1:
    r.expire(key, 60)

print(f"用户 {user_id} 当前 60 秒内访问次数: {count}")

连续运行几次:

bash 复制代码
用户 1001 当前 60 秒内访问次数: 1
用户 1001 当前 60 秒内访问次数: 2
用户 1001 当前 60 秒内访问次数: 3

这就是一个最基础的限流雏形。

五、Hash 操作:保存对象数据

Hash 很适合保存一个对象的多个字段,例如用户信息、商品信息、订单摘要。

1. hset / hget / hgetall

bash 复制代码
import redis

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

user_key = "user:1001"

r.hset(user_key, mapping={
    "name": "张三",
    "age": "28",
    "city": "深圳",
    "role": "tester"
})

print("姓名:", r.hget(user_key, "name"))
print("年龄:", r.hget(user_key, "age"))
print("完整用户信息:", r.hgetall(user_key))

输出:

bash 复制代码
姓名: 张三
年龄: 28
完整用户信息: {'name': '张三', 'age': '28', 'city': '深圳', 'role': 'tester'}

2. 修改单个字段

bash 复制代码
r.hset("user:1001", "city", "广州")

print(r.hgetall("user:1001"))

输出:

bash 复制代码
{'name': '张三', 'age': '28', 'city': '广州', 'role': 'tester'}

3. 判断字段是否存在

bash 复制代码
print(r.hexists("user:1001", "name"))
print(r.hexists("user:1001", "email"))

输出:

4. 删除字段

bash 复制代码
r.hdel("user:1001", "role")

print(r.hgetall("user:1001"))

输出:

bash 复制代码
{'name': '张三', 'age': '28', 'city': '广州'}

5. Hash 适合什么场景?

适合

  • user:1001 -> name, age, city

  • product:2001 -> title, price, stock

  • order:3001 -> status, amount, create_time

不适合

  • 复杂嵌套对象

  • 大量多层 JSON 查询

  • 需要根据字段做复杂筛选

Redis Hash 是简单对象存储,不是关系型数据库。

六、List 操作:队列、栈、任务列表

Redis List 是字符串值组成的链表,经常用于实现栈、队列和后台任务队列。

1. 使用 List 实现队列

生产任务

bash 复制代码
import redis
import json

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

queue_key = "task:queue"

r.delete(queue_key)

tasks = [
    {"task_id": 1, "type": "send_email", "email": "a@example.com"},
    {"task_id": 2, "type": "send_sms", "phone": "13800138000"},
    {"task_id": 3, "type": "generate_report", "report_id": 9001},
]

for task in tasks:
    r.rpush(queue_key, json.dumps(task, ensure_ascii=False))
    print("任务入队:", task)

输出:

bash 复制代码
任务入队: {'task_id': 1, 'type': 'send_email', 'email': 'a@example.com'}
任务入队: {'task_id': 2, 'type': 'send_sms', 'phone': '13800138000'}
任务入队: {'task_id': 3, 'type': 'generate_report', 'report_id': 9001}

消费任务

bash 复制代码
import redis
import json

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

queue_key = "task:queue"

while True:
    task_json = r.lpop(queue_key)

    if task_json is None:
        print("队列为空,暂无任务")
        break

    task = json.loads(task_json)
    print("开始处理任务:", task)

输出:

bash 复制代码
开始处理任务: {'task_id': 1, 'type': 'send_email', 'email': 'a@example.com'}
开始处理任务: {'task_id': 2, 'type': 'send_sms', 'phone': '13800138000'}
开始处理任务: {'task_id': 3, 'type': 'generate_report', 'report_id': 9001}
队列为空,暂无任务

这里使用 rpush 入队,lpop 出队,符合先进先出队列逻辑。

2. 查看队列长度

bash 复制代码
length = r.llen("task:queue")
print("当前队列长度:", length)

输出:

3. 阻塞式消费

普通 lpop 如果队列没有数据,会马上返回 None。如果希望消费者阻塞等待,可以用 blpop

bash 复制代码
import redis
import json

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

queue_key = "task:queue"

print("等待任务中...")

result = r.blpop(queue_key, timeout=10)

if result is None:
    print("10 秒内没有任务,退出")
else:
    key, task_json = result
    task = json.loads(task_json)
    print("收到任务:", task)

如果 10 秒内没有任务:

如果期间有任务入队:

bash 复制代码
等待任务中...
收到任务: {'task_id': 4, 'type': 'sync_data', 'table': 'user'}

七、Set 操作:去重、标签、集合关系

Set 是无序不重复集合,适合去重、标签、共同好友、权限集合等场景。

1. 添加元素

bash 复制代码
import redis

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

key = "article:1001:like_users"

r.delete(key)

r.sadd(key, "user_1", "user_2", "user_3")
r.sadd(key, "user_2")

print("点赞用户:", r.smembers(key))
print("点赞人数:", r.scard(key))

输出:

bash 复制代码
点赞用户: {'user_2', 'user_1', 'user_3'}
点赞人数: 3

虽然添加了两次 user_2,但 Set 会自动去重。

2. 判断是否存在

bash 复制代码
print(r.sismember("article:1001:like_users", "user_1"))
print(r.sismember("article:1001:like_users", "user_9"))

输出:

3. 共同好友

bash 复制代码
r.delete("user:1:friends", "user:2:friends")

r.sadd("user:1:friends", "张三", "李四", "王五")
r.sadd("user:2:friends", "李四", "王五", "赵六")

common = r.sinter("user:1:friends", "user:2:friends")

print("共同好友:", common)

输出:

4. 差集

bash 复制代码
diff = r.sdiff("user:1:friends", "user:2:friends")

print("用户1有但用户2没有的好友:", diff)

输出:

八、Sorted Set 操作:排行榜

Sorted Set(ZSet)是带分数的有序集合,非常适合做排行榜。

1. 添加排行榜数据

bash 复制代码
import redis

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

rank_key = "game:rank"

r.delete(rank_key)

r.zadd(rank_key, {
    "张三": 1200,
    "李四": 1800,
    "王五": 1500,
    "赵六": 2100,
    "钱七": 900,
})

top_users = r.zrevrange(rank_key, 0, 2, withscores=True)

print("游戏积分 Top3:")
for index, item in enumerate(top_users, start=1):
    name, score = item
    print(f"第 {index} 名: {name}, 分数: {int(score)}")

输出:

bash 复制代码
游戏积分 Top3:
第 1 名: 赵六, 分数: 2100
第 2 名: 李四, 分数: 1800
第 3 名: 王五, 分数: 1500

2. 给用户增加分数

bash 复制代码
new_score = r.zincrby(rank_key, 300, "张三")

print("张三最新分数:", int(new_score))

输出:

再次查看排行榜:

bash 复制代码
top_users = r.zrevrange(rank_key, 0, 4, withscores=True)

for index, item in enumerate(top_users, start=1):
    name, score = item
    print(f"第 {index} 名: {name}, 分数: {int(score)}")

输出:

bash 复制代码
第 1 名: 赵六, 分数: 2100
第 2 名: 李四, 分数: 1800
第 3 名: 张三, 分数: 1500
第 4 名: 王五, 分数: 1500
第 5 名: 钱七, 分数: 900

3. 查询某个用户排名

bash 复制代码
rank = r.zrevrank(rank_key, "张三")
score = r.zscore(rank_key, "张三")

print("张三排名:", rank + 1)
print("张三分数:", int(score))

输出:

zrevrank 返回从 0 开始的排名,展示时通常要 +1。

九、Key 过期时间操作

1. expire 设置过期

bash 复制代码
r.set("login:token:abc123", "user_1001")
r.expire("login:token:abc123", 3600)

print("token:", r.get("login:token:abc123"))
print("ttl:", r.ttl("login:token:abc123"))

输出:

bash 复制代码
token: user_1001
ttl: 3600

2. ttl 返回值含义

bash 复制代码
print(r.ttl("login:token:abc123"))
print(r.ttl("not_exists_key"))

可能输出:

常见含义:

  • 大于 0:剩余过期秒数

  • -1:key 存在但没有设置过期时间

  • -2:key 不存在

3. persist 移除过期时间

bash 复制代码
r.set("system:config", "v1")
r.expire("system:config", 60)

print("设置过期后 ttl:", r.ttl("system:config"))

r.persist("system:config")

print("移除过期后 ttl:", r.ttl("system:config"))

输出:

bash 复制代码
设置过期后 ttl: 60
移除过期后 ttl: -1

十、Pipeline:批量操作提升性能

如果一次要执行很多 Redis 命令,逐条发送会产生较多网络往返开销。Pipeline 可以把多个命令一起发送给服务端,减少网络和处理开销。

1. 普通循环写入

bash 复制代码
import redis
import time

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

start = time.time()

for i in range(1000):
    r.set(f"normal:key:{i}", i)

end = time.time()

print("普通写入耗时:", round(end - start, 4), "秒")

输出示例:

2. Pipeline 批量写入

bash 复制代码
import redis
import time

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

start = time.time()

pipe = r.pipeline()

for i in range(1000):
    pipe.set(f"pipe:key:{i}", i)

result = pipe.execute()

end = time.time()

print("Pipeline 写入结果数量:", len(result))
print("Pipeline 写入耗时:", round(end - start, 4), "秒")

输出示例:

bash 复制代码
Pipeline 写入结果数量: 1000
Pipeline 写入耗时: 0.0186 秒

Pipeline 在大量命令场景下通常明显更快。

3. Pipeline 返回值

bash 复制代码
pipe = r.pipeline()

pipe.set("user:1:name", "张三")
pipe.get("user:1:name")
pipe.incr("counter:test")
pipe.ttl("user:1:name")

result = pipe.execute()

print(result)

输出:

返回值按照命令加入 Pipeline 的顺序返回。

十一、事务:MULTI / EXEC 思想

redis-py 的 Pipeline 默认会把命令包在事务里执行,也可以通过参数关闭事务模式。

1. 简单事务示例

bash 复制代码
import redis

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

r.set("account:1001:balance", 1000)
r.set("account:1002:balance", 500)

pipe = r.pipeline(transaction=True)

pipe.decrby("account:1001:balance", 100)
pipe.incrby("account:1002:balance", 100)

result = pipe.execute()

print("事务执行结果:", result)
print("账户1001余额:", r.get("account:1001:balance"))
print("账户1002余额:", r.get("account:1002:balance"))

输出:

bash 复制代码
事务执行结果: [900, 600]
账户1001余额: 900
账户1002余额: 600

2. 注意:Redis 事务不是关系型数据库事务

Redis 的事务更多是"命令队列化 + 顺序执行",并不等同于 MySQL 那种复杂事务。金额、订单、库存等核心数据的主流程仍应放在数据库中,Redis 可做缓存、预扣、限流或辅助判断。

十二、发布订阅 Pub/Sub

Pub/Sub 适合做轻量级实时通知,例如:

  • 服务 A 发布消息

  • 服务 B、C、D 订阅消息

  • 收到消息后执行对应动作

1. 订阅者

新建 subscriber.py

bash 复制代码
import redis

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

pubsub = r.pubsub()
pubsub.subscribe("order_channel")

print("订阅 order_channel 成功,等待消息...")

for message in pubsub.listen():
    print("收到原始消息:", message)

    if message["type"] == "message":
        print("收到业务消息:", message["data"])

运行:

输出:

bash 复制代码
订阅 order_channel 成功,等待消息...
收到原始消息: {'type': 'subscribe', 'pattern': None, 'channel': 'order_channel', 'data': 1}

2. 发布者

新建 publisher.py

bash 复制代码
import redis
import json

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

message = {
    "order_id": "O202605100001",
    "status": "paid",
    "amount": 199.9
}

count = r.publish("order_channel", json.dumps(message, ensure_ascii=False))

print("消息已发布")
print("接收客户端数量:", count)

运行:

输出:

订阅者终端会看到:

bash 复制代码
收到原始消息: {'type': 'message', 'pattern': None, 'channel': 'order_channel', 'data': '{"order_id": "O202605100001", "status": "paid", "amount": 199.9}'}
收到业务消息: {"order_id": "O202605100001", "status": "paid", "amount": 199.9}

3. Pub/Sub 的缺点

Pub/Sub 很轻量,但不是可靠消息队列。如果订阅者当时不在线,消息不会自动保存。因此适合在线通知、临时广播、实时刷新等场景,但不适合必须保证消费成功的可靠异步任务,这种场景建议使用 Kafka、RabbitMQ、RocketMQ 或 Redis Stream。

十三、分布式锁

分布式锁常见场景:

  • 防止重复提交

  • 防止定时任务多实例重复执行

  • 防止同一资源被多个服务同时修改

1. 使用 redis-py 自带 Lock

bash 复制代码
import redis
import time

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

lock = r.lock(
    name="lock:daily_report",
    timeout=10,
    blocking_timeout=3
)

try:
    acquired = lock.acquire()

    if not acquired:
        print("没有获取到锁,任务不执行")
    else:
        print("获取锁成功,开始执行任务")
        time.sleep(2)
        print("任务执行完成")

finally:
    if lock.owned():
        lock.release()
        print("锁已释放")

输出:

2. 手写简化版锁

理解原理可用此示例:

bash 复制代码
import redis
import uuid
import time

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

lock_key = "lock:submit:order:1001"
lock_value = str(uuid.uuid4())

locked = r.set(lock_key, lock_value, nx=True, ex=10)

if locked:
    try:
        print("获取锁成功:", lock_value)
        print("开始处理订单提交...")
        time.sleep(2)
        print("订单提交处理完成")
    finally:
        current_value = r.get(lock_key)
        if current_value == lock_value:
            r.delete(lock_key)
            print("释放锁成功")
        else:
            print("锁已过期或被其他请求持有,不释放")
else:
    print("请求正在处理中,请勿重复提交")

核心思想:set key value nx ex ------ 不存在才设置,设置成功代表拿到锁,设置过期时间防止死锁,释放前确认 value 是自己的。生产环境建议使用成熟实现。

十四、缓存封装实战:查询用户信息

业务需求:查询用户信息,优先查 Redis,没有则查数据库,查到后写入 Redis。

1. 模拟数据库

bash 复制代码
import time

USER_DB = {
    1001: {"id": 1001, "name": "张三", "city": "深圳", "role": "测试工程师"},
    1002: {"id": 1002, "name": "李四", "city": "广州", "role": "开发工程师"},
}

def query_user_from_db(user_id: int):
    print(f"正在查询数据库,user_id={user_id}")
    time.sleep(0.2)
    return USER_DB.get(user_id)

2. Redis 缓存封装

bash 复制代码
import redis
import json
import time

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

USER_DB = {
    1001: {"id": 1001, "name": "张三", "city": "深圳", "role": "测试工程师"},
    1002: {"id": 1002, "name": "李四", "city": "广州", "role": "开发工程师"},
}

def query_user_from_db(user_id: int):
    print(f"正在查询数据库,user_id={user_id}")
    time.sleep(0.2)
    return USER_DB.get(user_id)

def get_user(user_id: int):
    cache_key = f"cache:user:{user_id}"

    cache_value = r.get(cache_key)

    if cache_value:
        print("命中 Redis 缓存")
        return json.loads(cache_value)

    print("Redis 未命中")
    user = query_user_from_db(user_id)

    if user is None:
        print("数据库也没有查到用户")
        return None

    r.set(cache_key, json.dumps(user, ensure_ascii=False), ex=300)
    print("用户信息已写入 Redis,过期时间 300 秒")

    return user

print("第一次查询:")
print(get_user(1001))

print("\n第二次查询:")
print(get_user(1001))

输出:

bash 复制代码
第一次查询:
Redis 未命中
正在查询数据库,user_id=1001
用户信息已写入 Redis,过期时间 300 秒
{'id': 1001, 'name': '张三', 'city': '深圳', 'role': '测试工程师'}

第二次查询:
命中 Redis 缓存
{'id': 1001, 'name': '张三', 'city': '深圳', 'role': '测试工程师'}

这就是最典型的缓存旁路模式(Cache Aside Pattern)。

十五、缓存穿透、击穿、雪崩

1. 缓存穿透

缓存穿透指请求的数据 Redis 和数据库都没有,每次请求都会打到数据库。解决方式:参数校验、缓存空值、布隆过滤器。

示例:缓存空值

bash 复制代码
import redis
import json
import time

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

USER_DB = {
    1001: {"id": 1001, "name": "张三"}
}

def query_user_from_db(user_id):
    print(f"查询数据库 user_id={user_id}")
    time.sleep(0.1)
    return USER_DB.get(user_id)

def get_user(user_id):
    cache_key = f"cache:user:{user_id}"

    cache_value = r.get(cache_key)

    if cache_value is not None:
        if cache_value == "":
            print("命中空值缓存")
            return None
        print("命中正常缓存")
        return json.loads(cache_value)

    print("缓存未命中")
    user = query_user_from_db(user_id)

    if user is None:
        r.set(cache_key, "", ex=60)
        print("数据库无数据,写入空值缓存 60 秒")
        return None

    r.set(cache_key, json.dumps(user, ensure_ascii=False), ex=300)
    return user

print(get_user(9999))
print(get_user(9999))

输出:

bash 复制代码
缓存未命中
查询数据库 user_id=9999
数据库无数据,写入空值缓存 60 秒
None
命中空值缓存
None

2. 缓存击穿

热点 key 过期,大量请求同时打向数据库。解决方式:热点 key 不设过期、互斥锁回源、提前异步刷新。

简化版互斥锁示例

bash 复制代码
import redis
import json
import time

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

def query_hot_data_from_db():
    print("查询数据库中的热点数据")
    time.sleep(0.5)
    return {"id": 1, "title": "热门商品", "price": 99.9}

def get_hot_data():
    cache_key = "cache:hot:product:1"
    lock_key = "lock:hot:product:1"

    cache_value = r.get(cache_key)

    if cache_value:
        print("命中热点缓存")
        return json.loads(cache_value)

    locked = r.set(lock_key, "1", nx=True, ex=10)

    if locked:
        try:
            print("获取互斥锁成功,回源数据库")
            data = query_hot_data_from_db()
            r.set(cache_key, json.dumps(data, ensure_ascii=False), ex=300)
            return data
        finally:
            r.delete(lock_key)
            print("释放互斥锁")
    else:
        print("其他线程正在重建缓存,稍后重试")
        time.sleep(0.1)
        cache_value = r.get(cache_key)
        if cache_value:
            return json.loads(cache_value)
        return None

print(get_hot_data())

输出:

bash 复制代码
获取互斥锁成功,回源数据库
查询数据库中的热点数据
释放互斥锁
{'id': 1, 'title': '热门商品', 'price': 99.9}

3. 缓存雪崩

大量 key 同时过期,请求全部打到数据库。解决方式:过期时间加随机值、分批刷新、本地缓存、限流降级、Redis 高可用。

过期时间增加随机值

bash 复制代码
import random
import redis
import json

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

data = {"name": "张三"}

base_ttl = 300
random_ttl = random.randint(0, 60)
ttl = base_ttl + random_ttl

r.set("cache:user:1001", json.dumps(data, ensure_ascii=False), ex=ttl)

print("缓存写入成功,过期时间:", ttl)

输出示例:

十六、Redis 限流实战

需求:同一个用户 60 秒内最多请求 5 次。

bash 复制代码
import redis

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

def check_rate_limit(user_id: int):
    key = f"rate_limit:user:{user_id}"

    count = r.incr(key)

    if count == 1:
        r.expire(key, 60)

    ttl = r.ttl(key)

    if count > 5:
        return False, count, ttl
    return True, count, ttl

for i in range(1, 8):
    allowed, count, ttl = check_rate_limit(1001)

    if allowed:
        print(f"第 {i} 次请求:允许,当前次数={count},剩余时间={ttl}s")
    else:
        print(f"第 {i} 次请求:拒绝,当前次数={count},剩余时间={ttl}s")

输出:

bash 复制代码
第 1 次请求:允许,当前次数=1,剩余时间=60s
第 2 次请求:允许,当前次数=2,剩余时间=60s
...
第 6 次请求:拒绝,当前次数=6,剩余时间=60s
第 7 次请求:拒绝,当前次数=7,剩余时间=60s

此为固定窗口限流,更精细可用滑动窗口、令牌桶等算法。

十七、Redis 连接池封装

生产项目建议统一封装连接池,避免到处创建连接。

bash 复制代码
import redis

class RedisClient:
    _pool = None

    @classmethod
    def get_client(cls):
        if cls._pool is None:
            cls._pool = redis.ConnectionPool(
                host="localhost",
                port=6379,
                db=0,
                decode_responses=True,
                max_connections=20
            )
        return redis.Redis(connection_pool=cls._pool)

if __name__ == "__main__":
    r = RedisClient.get_client()
    r.set("app:name", "redis-demo")
    print(r.get("app:name"))

输出:

十八、FastAPI 中使用 Redis

1. 安装依赖

bash 复制代码
pip install fastapi uvicorn redis

2. 示例代码

bash 复制代码
from fastapi import FastAPI
import redis
import json

app = FastAPI()

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

USER_DB = {
    1001: {"id": 1001, "name": "张三", "city": "深圳"},
    1002: {"id": 1002, "name": "李四", "city": "广州"},
}

@app.get("/users/{user_id}")
def get_user(user_id: int):
    cache_key = f"cache:user:{user_id}"

    cache_value = r.get(cache_key)

    if cache_value:
        return {"source": "redis", "data": json.loads(cache_value)}

    user = USER_DB.get(user_id)

    if user is None:
        return {"source": "db", "data": None, "message": "用户不存在"}

    r.set(cache_key, json.dumps(user, ensure_ascii=False), ex=300)

    return {"source": "db", "data": user}

启动:

bash 复制代码
uvicorn main:app --reload

第一次请求 curl http://127.0.0.1:8000/users/1001 返回 "source": "db",第二次返回 "source": "redis"

十九、Django 中使用 Redis 的思路

可直接封装 redis-py:

bash 复制代码
# utils/redis_client.py
import redis

redis_client = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

业务中使用:

bash 复制代码
from utils.redis_client import redis_client

def get_verify_code(phone):
    key = f"sms:code:{phone}"
    return redis_client.get(key)

若需全站缓存、session 缓存,可考虑 Django 的缓存框架并配置 Redis 后端。

二十、常见异常处理

1. 连接失败

bash 复制代码
import redis

r = redis.Redis(
    host="localhost",
    port=6378,
    db=0,
    decode_responses=True,
    socket_connect_timeout=2
)

try:
    print(r.ping())
except redis.exceptions.ConnectionError as e:
    print("Redis 连接失败:", e)

2. 命令执行超时

bash 复制代码
r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True,
    socket_timeout=1
)

try:
    r.get("test")
except redis.exceptions.TimeoutError as e:
    print("Redis 操作超时:", e)

3. 类型错误

bash 复制代码
r.set("name", "张三")
try:
    r.hset("name", "age", "18")
except redis.exceptions.ResponseError as e:
    print("Redis 类型错误:", e)

输出:WRONGTYPE Operation against a key holding the wrong kind of value

二十一、Redis Key 命名规范

建议使用风格:业务模块:业务含义:唯一标识

例如:

bash 复制代码
user:1001
user:1001:profile
sms:code:13800138000
cache:product:2001
lock:order:submit:3001
rate_limit:user:1001
task:queue:email
game:rank:season:2026

避免无意义的 namedatainfo 等,项目变大后难以维护。

二十二、Redis 使用原则

  1. 不要把 Redis 当永久数据库,核心数据应存于关系型数据库。

  2. 所有缓存都应考虑过期策略,避免脏数据或空间无限增长。

  3. 谨慎处理大 key,可能带来删除慢、迁移慢、阻塞风险。

  4. 慎用 keys * ,生产环境使用 scan 代替。

    bash 复制代码
    cursor = 0
    while True:
        cursor, keys = r.scan(cursor=cursor, match="cache:user:*", count=100)
        for key in keys:
            print(key)
        if cursor == 0:
            break
  5. 注意缓存与数据库一致性,常见策略:先更新数据库,再删除缓存。

    bash 复制代码
    def update_user_name(user_id, new_name):
        # update db set name = new_name where id = user_id
        cache_key = f"cache:user:{user_id}"
        r.delete(cache_key)

二十三、完整实战:商品详情缓存

整合了缓存命中、空值缓存、随机过期时间的完整示例:

bash 复制代码
import redis
import json
import random
import time
from typing import Optional, Dict, Any

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

PRODUCT_DB = {
    2001: {"id": 2001, "name": "无线鼠标", "price": 59.9, "stock": 120},
    2002: {"id": 2002, "name": "机械键盘", "price": 299.0, "stock": 35}
}

def query_product_from_db(product_id: int) -> Optional[Dict[str, Any]]:
    print(f"[DB] 查询商品,product_id={product_id}")
    time.sleep(0.2)
    return PRODUCT_DB.get(product_id)

def get_product(product_id: int) -> Optional[Dict[str, Any]]:
    cache_key = f"cache:product:{product_id}"

    cache_value = r.get(cache_key)

    if cache_value is not None:
        if cache_value == "":
            print("[Redis] 命中空值缓存")
            return None
        print("[Redis] 命中商品缓存")
        return json.loads(cache_value)

    print("[Redis] 未命中商品缓存")

    product = query_product_from_db(product_id)

    if product is None:
        r.set(cache_key, "", ex=60)
        print("[Redis] 写入空值缓存,过期时间 60 秒")
        return None

    ttl = 300 + random.randint(0, 60)
    r.set(cache_key, json.dumps(product, ensure_ascii=False), ex=ttl)
    print(f"[Redis] 写入商品缓存,过期时间 {ttl} 秒")

    return product

if __name__ == "__main__":
    print("第一次查询商品 2001")
    print(get_product(2001))
    print("\n第二次查询商品 2001")
    print(get_product(2001))
    print("\n查询不存在的商品 9999")
    print(get_product(9999))
    print("\n再次查询不存在的商品 9999")
    print(get_product(9999))

二十四、完整实战:接口幂等控制

基于 Redis 的 set nx 实现唯一请求 ID 的幂等:

bash 复制代码
import redis
import time

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

def submit_order(user_id: int, request_id: str):
    key = f"idempotent:order:{user_id}:{request_id}"

    success = r.set(key, "processing", nx=True, ex=60)

    if not success:
        print("重复请求,拒绝处理")
        return {"success": False, "message": "请勿重复提交"}

    try:
        print("首次请求,开始创建订单")
        time.sleep(0.5)
        order_id = "O202605100001"
        r.set(key, f"success:{order_id}", ex=300)
        print("订单创建成功:", order_id)
        return {"success": True, "order_id": order_id}
    except Exception as e:
        r.delete(key)
        raise e

print(submit_order(1001, "REQ-ABC-001"))
print(submit_order(1001, "REQ-ABC-001"))

二十五、完整实战:验证码发送限制

60 秒内限制发送频率,验证码 5 分钟有效。

bash 复制代码
import redis
import random

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True
)

def send_sms_code(phone: str):
    limit_key = f"sms:limit:{phone}"
    code_key = f"sms:code:{phone}"

    limited = r.set(limit_key, "1", nx=True, ex=60)

    if not limited:
        ttl = r.ttl(limit_key)
        return {"success": False, "message": f"发送太频繁,请 {ttl} 秒后再试"}

    code = str(random.randint(100000, 999999))
    r.set(code_key, code, ex=300)
    print(f"模拟发送短信:手机号={phone},验证码={code}")

    return {"success": True, "message": "验证码发送成功"}

def verify_sms_code(phone: str, input_code: str):
    code_key = f"sms:code:{phone}"

    real_code = r.get(code_key)

    if real_code is None:
        return {"success": False, "message": "验证码不存在或已过期"}
    if real_code != input_code:
        return {"success": False, "message": "验证码错误"}

    r.delete(code_key)
    return {"success": True, "message": "验证码校验成功"}

print(send_sms_code("13800138000"))
print(send_sms_code("13800138000"))

二十六、生产环境建议

  1. 密码不要硬编码,使用环境变量:

    bash 复制代码
    import os
    import redis
    
    r = redis.Redis(
        host=os.getenv("REDIS_HOST", "localhost"),
        port=int(os.getenv("REDIS_PORT", "6379")),
        password=os.getenv("REDIS_PASSWORD"),
        db=int(os.getenv("REDIS_DB", "0")),
        decode_responses=True
    )
  2. 设置连接超时socket_connect_timeout=3, socket_timeout=3,避免业务接口卡死。

  3. 重要接口要有降级逻辑 :Redis 异常时降级查数据库,并包装 safe_get_cache 方法捕获 RedisError

  4. 缓存数据必须可重建,缓存不是主数据。

  5. 监控 Redis:关注内存、连接数、慢查询、命中率、主从同步状态等。

二十七、总结

Redis 在 Python 项目中非常实用,尤其适合缓存、验证码、Token、计数器、排行榜、限流、分布式锁、简单队列、发布订阅、接口幂等等场景。实际使用时需注意:

  • 不要把 Redis 当唯一数据库

  • 缓存必须考虑过期策略

  • 防穿透、击穿、雪崩

  • 分布式锁要设置过期时间

  • 生产环境避免 keys *

  • 重要业务考虑异常降级

一句话总结:Redis 很快,但快不是乱用的理由。用得好,它是系统性能加速器;用不好,它就是凌晨三点叫醒你的"红色报警器"。

相关推荐
小猿姐1 小时前
GitLab on Kubernetes:使用 KubeBlocks 部署生产级高可用 PostgreSQL 和 Redis
redis·postgresql·kubernetes
Dxy12393102161 小时前
Python Pillow库:`img.format`与`img.mode`的区别详解
开发语言·python·pillow
༒࿈南林࿈༒2 小时前
刺猬猫小说下载
python·js逆向
.柒宇.2 小时前
AI-Agent入门实战-AI私厨
人工智能·python·langchain·agent·fastapi
默子昂2 小时前
langchain 基本使用
开发语言·python·langchain
SilentSamsara2 小时前
生成器实战:处理大文件、流水线模式与无限序列
vscode·python·青少年编程·pycharm
phltxy2 小时前
Redis 常见数据类型之全局通用命令详解
数据库·redis·bootstrap
yaoxin5211232 小时前
402. Java 文件操作基础 - 读取二进制文件
java·开发语言·python
難釋懷3 小时前
Redis网络模型-用户空间和内核态空间
网络·arm开发·redis