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

IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。

在前五篇文章里,我们一直在用 redis-cli 和命令行探索 Redis 的五大数据结构外加三个高级类型。你可能会问:在 Python 项目里怎么操作 Redis?怎么管理连接?怎么提升性能?

从本文开始,我们将全面切换到 Python 视角 ,用 redis-py 这个官方推荐的客户端库来武装我们的 Python 应用。本文将手把手教你安装、建立连接、操作五大基础数据结构,并深入讲解连接池和 Pipeline 两大工程化利器。读完你会发现,用 Python 调用 Redis 原来可以这么简洁优雅。

1. 环境准备:安装 redis-py

redis-py 是 Redis 官方推荐的 Python 客户端,纯 Python 实现,API 简洁且紧跟 Redis 最新命令。安装只需一条命令:

预期输出:

bash 复制代码
Collecting redis
  Downloading redis-5.0.0-py3-none-any.whl (252 kB)
     ... 省略 ...
Installing collected packages: redis
Successfully installed redis-5.0.0

⚠️ 本文以 redis-py 5.x 版本为例,部分 API 可能与 4.x 略有差异。建议使用 4.0 及以上版本,以获取完整类型提示和更好的异步支持。检查版本:pip show redis

2. 创建连接:从简单到规范

2.1 最简单的连接

bash 复制代码
import redis

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

# Ping 测试
print(r.ping())  # True

参数解释

  • host:Redis 服务器地址,默认 localhost

  • port:端口,默认 6379

  • decode_responses=True强烈建议开启 ,让返回的数据自动解码为 Python 字符串,而不是 bytes。否则你得到的是 b'value',需要自己 .decode()

2.2 带密码和 db 的连接

bash 复制代码
r = redis.Redis(
    host='192.168.1.100',
    port=6379,
    password='your_password',
    db=1,
    decode_responses=True
)

db 参数指定数据库编号(Redis 默认有 0~15 共 16 个数据库,可通过配置修改)。

2.3 使用连接 URL(更现代)

redis-py 支持从 URL 字符串创建连接,适合从配置文件读取:

bash 复制代码
# 格式: redis://[:password@]host:port/db
r = redis.from_url('redis://:mypassword@localhost:6379/0', decode_responses=True)
print(r.ping())

💡 最佳实践 :永远不要在代码中硬编码密码!使用环境变量:redis.from_url(os.getenv('REDIS_URL'))

3. 五大基础数据类型 API 速查

redis-py 的 API 与 redis-cli 命令几乎一一对应,方法名小写,参数顺序一致。下面我们快速过一遍每种数据类型的 Python 调用方式,并附上输出。

3.1 String 操作

bash 复制代码
import redis
r = redis.Redis(decode_responses=True)

# SET / GET / MSET / MGET
r.set('name', 'Alice')
print(r.get('name'))                     # Alice

r.mset({'age': 30, 'city': 'Beijing'})
print(r.mget(['age', 'city']))           # ['30', 'Beijing']

# SET 可选参数(ex, nx, px 等)
r.set('token', 'abc', ex=10)             # 10秒过期
r.set('lock', '1', nx=True, ex=30)       # 仅在不存在时设置

# 数字操作
r.set('counter', 0)
r.incr('counter')
r.incrby('counter', 5)
print(r.get('counter'))                  # 6

# 字符串操作
r.set('greet', 'Hello')
r.append('greet', ' Redis')
print(r.get('greet'))                    # Hello Redis
print(r.strlen('greet'))                 # 11
print(r.getrange('greet', 0, 4))         # Hello

3.2 Hash 操作

bash 复制代码
# HSET / HGET / HMSET (已废弃,直接用 hset)
r.hset('user:1001', mapping={'name': 'Bob', 'age': '25', 'city': 'Shanghai'})
print(r.hgetall('user:1001'))            # {'name': 'Bob', 'age': '25', 'city': 'Shanghai'}

# 获取单个字段
print(r.hget('user:1001', 'name'))       # Bob

# 批量获取
print(r.hmget('user:1001', ['name', 'age']))  # ['Bob', '25']

# 自增
r.hincrby('user:1001', 'age', 1)
print(r.hget('user:1001', 'age'))        # 26

# 字段是否存在/删除/长度
print(r.hexists('user:1001', 'city'))    # True
r.hdel('user:1001', 'city')
print(r.hlen('user:1001'))               # 2

3.3 List 操作

bash 复制代码
# 左右插入
r.lpush('queue', 'task1', 'task2')       # task2, task1
r.rpush('queue', 'task3')                # task2, task1, task3

# 范围查看
print(r.lrange('queue', 0, -1))          # ['task2', 'task1', 'task3']

# 弹出
print(r.lpop('queue'))                   # task2
print(r.rpop('queue'))                   # task3

# 阻塞弹出
# r.brpop('queue', timeout=5)            # 返回 (queue_name, value) 或 None

# 长度与索引
print(r.llen('queue'))                   # 1
print(r.lindex('queue', 0))              # task1

# 修剪
r.lpush('log', *['a','b','c','d','e'])
r.ltrim('log', 0, 2)
print(r.lrange('log', 0, -1))            # ['e', 'd', 'c']

3.4 Set 操作

bash 复制代码
# 添加 / 查看 / 删除
r.sadd('tags:python', 'redis', 'django', 'flask')
print(r.smembers('tags:python'))         # {'flask', 'django', 'redis'} 无序

print(r.sismember('tags:python', 'django'))  # True
print(r.scard('tags:python'))            # 3
r.srem('tags:python', 'flask')

# 集合运算
r.sadd('tags:web', 'django', 'fastapi')
print(r.sinter('tags:python', 'tags:web'))   # {'django'}
print(r.sunion('tags:python', 'tags:web'))   # {'redis', 'django', 'fastapi'}
print(r.sdiff('tags:python', 'tags:web'))    # {'redis'}

# 随机弹出
print(r.spop('tags:python'))              # 随机一个元素

3.5 Sorted Set 操作

bash 复制代码
# 添加 / 查看
r.zadd('leaderboard', {'Alice': 100, 'Bob': 85, 'Charlie': 92})
print(r.zrange('leaderboard', 0, -1, withscores=True))
# [('Bob', 85.0), ('Charlie', 92.0), ('Alice', 100.0)]

# 反向获取 Top N
print(r.zrevrange('leaderboard', 0, 1, withscores=True))
# [('Alice', 100.0), ('Charlie', 92.0)]

# 增减分数
r.zincrby('leaderboard', 10, 'Bob')
print(r.zscore('leaderboard', 'Bob'))     # 95.0

# 排名
print(r.zrank('leaderboard', 'Bob'))      # 0 (从低到高)
print(r.zrevrank('leaderboard', 'Bob'))   # 2 (从高到低)

# 按分数范围查询
print(r.zrangebyscore('leaderboard', 90, 100))
# ['Charlie', 'Alice']

🧪 动手试试 :把上面的代码逐段复制到你的 python 交互环境(或脚本)运行,对比输出是否与预期一致。试着修改几个参数,比如给 String 加 ex=5,然后用 ttl 观察倒计时。

4. 连接池管理:告别频繁握手

4.1 为什么要用连接池?

Redis 是基于 TCP 的,每次创建新连接都需要三次握手、认证等开销。如果你的应用在高并发场景下每次请求都 redis.Redis() 新建连接,不仅浪费性能,还可能耗尽 Redis 的 maxclients 限制。

连接池(ConnectionPool) 就是提前创建好一定数量的连接,放在池子里,用时借、用完还,连接不断开。

4.2 创建连接池

bash 复制代码
import redis

# 显式创建连接池
pool = redis.ConnectionPool(
    host='localhost',
    port=6379,
    decode_responses=True,
    max_connections=20,    # 最大连接数
    # 更多参数如 password, db 等
)

# 多个 Redis 实例共享一个连接池
r1 = redis.Redis(connection_pool=pool)
r2 = redis.Redis(connection_pool=pool)

# 验证共享
print(r1.ping())   # True
print(r2.ping())   # True

# 使用完可以断开所有连接(应用退出时)
pool.disconnect()

max_connections 设定了池中最多保持多少个连接。当并发请求超过此数时,获取连接的请求会阻塞,直到有连接归还。

4.3 生产环境配置建议

bash 复制代码
import redis
import os

def get_redis_client():
    """生产级 Redis 客户端工厂函数"""
    pool = redis.ConnectionPool(
        host=os.getenv('REDIS_HOST', 'localhost'),
        port=int(os.getenv('REDIS_PORT', 6379)),
        password=os.getenv('REDIS_PASSWORD', None),
        db=int(os.getenv('REDIS_DB', 0)),
        decode_responses=True,
        max_connections=int(os.getenv('REDIS_MAX_CONNS', 50)),
        socket_timeout=5,              # 连接超时
        socket_connect_timeout=5,      # 连接建立超时
        health_check_interval=30,      # 健康检查间隔(秒)
    )
    return redis.Redis(connection_pool=pool)

client = get_redis_client()

⚠️ 常见误区:在多线程环境中,每个线程反复创建 redis.Redis() 而不共享连接池。正确做法是全局创建一个连接池,所有线程共享同一个 redis.Redis 实例(线程安全)。在多进程(如 gunicorn)中,每个 worker 进程应创建自己的连接池。

5. Pipeline:批量命令的加速器

5.1 为什么需要 Pipeline?

Redis 的命令执行是 C/S 模式,每一次命令都要经历:客户端发送命令 → 网络传输 → 服务端处理 → 网络返回 → 客户端接收。如果我们有 N 个命令,就有 N 次网络往返(RTT)。

Pipeline(管道) 允许我们把多个命令打包,一次性发给 Redis 服务端,服务端批量执行后一次性返回结果。这样 N 个命令只花费 1 次 RTT,性能提升非常显著。

5.2 Pipeline 基础用法

bash 复制代码
import redis
import time

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

# 不使用 Pipeline
start = time.perf_counter()
for i in range(1000):
    r.set(f'key:{i}', f'value:{i}')
print(f'无 Pipeline 耗时: {time.perf_counter() - start:.3f}s')

# 使用 Pipeline
r.flushdb()  # 清空测试库
start = time.perf_counter()
pipe = r.pipeline()
for i in range(1000):
    pipe.set(f'key:{i}', f'value:{i}')
pipe.execute()  # 一次性发送所有命令
print(f'使用 Pipeline 耗时: {time.perf_counter() - start:.3f}s')

输出示例(本地测试):

bash 复制代码
无 Pipeline 耗时: 0.532s
使用 Pipeline 耗时: 0.018s

性能差距可达 30 倍以上!在跨机房、高延迟网络环境下差距更明显。

5.3 Pipeline 的高级用法

Pipeline 也可以链式调用,并且支持获取每个命令的返回值:

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

# 链式写入
pipe.set('name', 'Alice').incr('counter').get('name')

# execute() 返回每个命令的结果列表
results = pipe.execute()
print(results)  # [True, 1, 'Alice']

Pipeline + 事务

Pipeline 默认不是事务,命令只是打包发送,不会保证原子性。如果要事务,使用 transaction=True(下一章会深入讲解 Lua 和事务)。

bash 复制代码
pipe = r.pipeline(transaction=True)
pipe.set('balance', 100)
pipe.incrby('balance', 50)
pipe.execute()   # 在 MULTI/EXEC 中执行

5.4 最佳实践

  • 批量读写 :循环中的 GET/SET 应改为 PipelineMGET/MSET

  • 批量操作但不同键PipelineMSET 更灵活,可以混合不同类型命令。

  • 控制批量大小:一次 Pipeline 发送的命令数不要过大(建议 1000 以内),避免客户端内存爆涨和服务端长时间阻塞(虽然 Redis 单线程会一次性执行完这批命令,但命令过多会阻塞其他请求)。

  • pipeline 配合连接池:Pipeline 从连接池获取一个连接并独占,执行完毕后归还,确保连接被正确复用。

6. 异常处理与重试

网络抖动不可避免,健壮的应用需要容错机制:

bash 复制代码
from redis.exceptions import ConnectionError, TimeoutError
import time

def safe_incr(r, key, retries=3):
    for attempt in range(retries):
        try:
            return r.incr(key)
        except (ConnectionError, TimeoutError) as e:
            print(f'第 {attempt+1} 次重试,错误: {e}')
            time.sleep(0.1 * (attempt + 1))  # 退避
    raise Exception(f'操作失败,已重试 {retries} 次')

更完善的方案可使用 retry 库或 redis-py 内置的 retry_on_timeout 支持(4.x+ 版本):

bash 复制代码
r = redis.Redis(
    host='localhost',
    retry_on_timeout=True,
    retry_on_error=[ConnectionError, TimeoutError],
    health_check_interval=30,
)

7. 动手试试

在本地完成以下实战练习:

  1. 连接池挑战 :创建一个 max_connections=5 的连接池,启动 10 个线程同时执行 GET 操作,观察是否会阻塞?用 time.sleep 模拟长操作,验证连接池行为。

  2. Pipeline 性能对决 :准备 5000 个键值对,分别用 for 循环 SETPipeline 写入,打印耗时对比。然后将数据用两种方式读取出来(GET 循环 vs MGET),观察性能。

  3. 模拟缓存读写 :编写一个函数 get_cached_data(key),先从 Redis 读取,不存在时模拟从 DB 查询(返回字符串),然后写入 Redis 并设 TTL,返回数据。验证第二次调用时是否命中缓存。

预期效果:Pipeline 写入速度是循环写入的数十倍;MGET 读取同样秒杀逐条 GET;缓存命中时无需调用 DB 函数。

8. 总结

本文是 Python 操作 Redis 的开篇基石,我们掌握了:

  • 安装与连接pip install redisredis.Redis()、连接 URL。

  • 五大数据类型 API :方法与命令行一一对应,decode_responses 自动转字符串。

  • 连接池:避免频繁 TCP 握手,全局共享,线程安全。

  • Pipeline:批量打包命令,大幅降低网络 RTT,性能提升数十倍。

  • 异常处理与重试:让应用在网络抖动下依然可靠。

你已经可以从 redis-cli 无缝切换到 Python 代码,并能写出工程级别的 Redis 交互逻辑。下一篇,我们将继续深入 Python 操作 Redis 的进阶技巧:事务、Lua 脚本、序列化方案、异步 redis.asyncio 以及工具类封装,真正把 Redis 用出"高级感"。

想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !

相关推荐
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
一条泥憨鱼3 小时前
苍穹外卖【day5|Redis与店铺营业状态设置】
java·后端·mybatis·苍穹外卖