ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python连接Redis存储数据全攻略:从入门到排障

Python连接Redis存储数据全攻略:从入门到排障 如果你在一个 Python 项目里接 Redis只是为了把一个字符串写进去、再读出来那确实花不了两分钟。但真实业务里Redis 的定位通常不是“一个键值存储”这么简单它往往是缓存层、会话中心、计数器、消息队列甚至要承担分布式锁。一旦承担这些角色“连接 Redis 数据库并存储数据”这件事的复杂度就会立刻上来连接参数、数据类型、过期策略、序列化、连接池任何一个环节没处理好线上就是一次事故。我自己带团队这些年遇到过连接超时、键被莫名淘汰、读出来的数据带着 b 前缀导致整条业务线报错也遇到过一个大 Key 把实例 CPU 打满的情况。所以想认真写一篇从头到尾讲 Python 连 Redis 并存储数据的文章从环境准备、客户端选型到五种核心数据类型的实操再到排障思路和可复用封装把这条链路彻底讲透。不管你是刚把 Redis 装好的新手还是已经在项目里用了很久但总被细节绊倒的开发者按顺序过一遍基本不会再有“连不上”和“存不进去”的困惑。1. 连接前的准备确认场景、装好服务、选对客户端1.1 先确认你存进 Redis 的数据属于哪一类我见过很多人一上来就问“Python 怎么用 Redis 存数据”但真正应该先回答的是你要存的那份数据到底是必须持久化的业务数据还是丢了可以重算的缓存数据这两类数据的用法完全不同。Redis 是典型的基于内存的键值数据库读写延迟通常在亚毫秒级这是它最大的优势也是它不能替代 MySQL 这类关系型数据库的原因。如果你要存的是订单、账户余额这种不能丢、需要强一致的事务性数据那落库应该落到关系型数据库上Redis 顶多在前面做一层加速。反过来像用户登录态、接口响应缓存、短信验证码、实时排行榜这类数据读写频率极高、允许短时间丢失、甚至需要自动过期那放 Redis 就是最合理的方案。明确了这个边界后面的设置项你才知道哪些必须开、哪些可以偷懒。顺带说一下Redis 官方文档里一直称自己为 database很多人搜“redis 数据库”其实就是把它当内存数据库用。用 Python 连接它的时候底层走的协议是 RESPredis-py 这个库把这层协议封装成了你熟悉的方法调用所以你不用关心协议细节但要知道背后每次命令都是一次 TCP 通信。1.2 服务端启动Docker、Windows、Linux 三条路线在写任何 Python 代码之前你得先有一个能连上的 Redis 服务端。这一步卡住的概率其实比写代码高因为各系统安装方式不一样。Windows官方不维护 Windows 版本的 Redis 服务端最省事的方式是 Docker 或者 WSL。如果非要用原生版本可以下载开源社区维护的 Memurai 或者微软老版本分支但我一般不建议在生产环境这么干。macOS有 Homebrew 的话直接brew install redis然后redis-server启动即可。Linux以 Ubuntu 为例一般是apt install redis-server然后通过systemctl enable --now redis-server开机自启。CentOS 用yum install redis或者编译安装都可以。但我个人最推荐的方式还是 Docker尤其是开发环境。原因很简单可以用一条命令启动并且可以指定版本换项目时再起一个容器互不干扰不会把依赖装进你的系统里。比如docker run --name redis-dev -p 6379:6379 -d redis:7.2启动后验证是否可用执行redis-cli -h 127.0.0.1 -p 6379 ping如果返回PONG说明服务端已经就绪。很多人搜“redis 安装教程”时还会看到 docker 搭建主从的玩法那是后面为了高可用才需要考虑的事新手阶段先把单机跑通。另外有一点要特别提醒默认配置下 Redis 没有密码只监听 127.0.0.1。这在本地开发没问题但如果你的服务器对公网开放了 6379 端口极容易被扫描到然后被恶意写入。生产环境必须做两件事一是在配置文件里设置requirepass二是搞清楚protected-mode的行为必要时用防火墙或安全组把端口限制在内网。1.3 客户端选型redis-py 为何是默认答案Python 生态里连 Redis 的库90% 的场景用 redis-py 就够了。它是 Redis 官方推荐的 Python 客户端GitHub 上的 star 数也遥遥领先同步接口和 asyncio 接口都提供还支持连接池、管道、发布订阅这些高级特性。安装一句话pip install redis装好之后可以用pip show redis看一眼版本。现在 redis-py 已经到 5.x接口相对稳定网上老教程里有些写法比如StrictRedis在新版本里已经被统一收进Redis照着新写法写就行。另外提一句不用急着装可视化工具。我见过不少初学者把 Redis Desktop Manager 当成必需品其实前期你更需要的是redis-cli命令行因为它能直接验证连接、看 key 类型、查 TTL、跑慢日志这些都是排障时最顺手的手段。可视化工具更适合用来看数据长啥样、确认 key 前缀规则是不是合理。等命令行的基本操作熟练了再按需装一个也不迟。以后要连 Redis Clusterredis-py 也支持用redis.cluster.RedisCluster就能以集群模式连接但新手阶段先无视这个等单机 Redis 的存储模式熟练了再升级。2. 建立第一条连接从 ping 到 set/get 的完整链路与参数细节2.1 最小可运行代码与 db 的划分逻辑服务端起来了客户端装好了就可以写第一条连接代码import redis r redis.Redis(hostlocalhost, port6379, db0) print(r.ping()) r.set(hello, redis) value r.get(hello) print(value)运行之后第一次看到True和redis输出你就打通了 Python 到 Redis 的全链路。这里有个细节放在前面说db0表示选择第 0 号逻辑库Redis 默认给每个实例划分了 16 个 db编号从 0 到 15。很多新手会问什么时候用 db1、什么时候用 db2。我的建议是开发环境里你可以拿 db 隔离不同的业务模块比如 db0 放缓存、db1 放会话信息这样调试时互不干扰也方便FLUSHDB只清空其中一个库。但生产环境我并不推荐把大量业务分散在多个 db 上因为 Redis 是单线程模型db 只是逻辑隔离并不提供真正的性能隔离一个 db 里出现慢命令照样会影响同一实例上的其他部分。更稳妥的做法是不同团队或不同业务互不信任时就拆独立实例否则统一放在同一个 db然后用带业务前缀的 key 来区分。2.2 decode_responses、超时、健康检查连接参数里最值得提前掌握的几个连接 Redis 的代码虽短参数却不少。第一次写的人很容易只传 host 和 port然后被返回值搞懵。下面这几个参数是实际开发里最容易踩坑、也最值得提前设置好的参数默认值作用与建议decode_responsesFalse决定读取到的字符串是str还是bytes。默认是bytes绝大多数项目建议设为Truesocket_connect_timeoutNoneTCP 建立连接的超时时间生产环境建议设 2~5 秒socket_timeoutNone单次命令读写超时不设置时命令有可能长时间阻塞生产环境建议设置health_check_interval0每隔多少秒对空闲连接做一次健康检查防止连接被中间设备断开max_connections2**31连接池上限按实际并发合理设置以decode_responses为例如果你不设置r.get(hello)返回的是bredis在打印时人眼看不出差别但只要拿去和redis做等于判断、或者拼进字符串模板立刻就会出问题。很多线上偶发 bug 就是这种字节串和字符串的隐形不一致导致的。我建议从第一个连接开始就统一写成r redis.Redis( hostlocalhost, port6379, db0, decode_responsesTrue, socket_connect_timeout3, socket_timeout3, )这样后面所有的代码拿到的都是正常字符串心智负担会小很多。2.3 连接池与 from_url生产环境推荐写法除了上面的写法redis-py 还提供一种基于 URL 的初始化方式r redis.Redis.from_url( redis://localhost:6379/0, decode_responsesTrue, )URL 格式是redis://[:password]host:port/db。如果你在配置里设置了requirepass就把密码放在user:pass的位置。用 URL 的好处是连接信息可以整体存到环境变量里部署时不用改代码。还有一个容易忽略的点每次创建redis.Redis()实例就等于创建了一个新的连接池。很多人图省事在函数里临时redis.Redis()一下用完就丢并发量上来之后会发现连接数暴涨、端口被占满、性能直线下降。正确做法是模块级创建单例保证整个进程复用同一个连接池。redis-py 内部从连接池取连接、执行命令、归还连接的流程都已经封装好了我们只需要守住“全局只初始化一次”这一条纪律。3. 五大数据类型存储实操不只是把一个字符串塞进去3.1 String验证码、计数器、缓存的基础设施String 是 Redis 里最基础的数据类型底层就是一个安全的二进制字符串可以存普通字符串、数字、甚至序列化之后的对象。它的用途远不止“存一个值”缓存接口返回SET配合过期时间避免每次请求都查数据库。短信验证码SETEX verify:code:13700000000 300 4829135 分钟自动过期。计数器INCR api:calls:2025自增 1原子操作不用怕并发问题。代码示例r.setex(verify:code:13700000000, 300, 482913) r.incr(api:calls:2025) r.decr(api:calls:2025)注意setex和set的区别setex在写入的同时指定过期秒数是“带 TTL 写入”的推荐方式。如果你先set之后再单独调expire中间至少有一次网络往返的空档理论上存在 key 永远不过期的风险窗口。另外setnx只在 key 不存在时写入是实现分布式锁的原始雏形但生产上要谨慎使用锁要带随机值、要设置过期时间防止死锁否则只靠setnx很容易锁不释放这个后面排错部分会再提到。3.2 Hash存储对象字段而不是一坨 JSONHash 类型用来存储一个对象的多个字段非常适合表示一条记录。比如用户信息用 String 存 JSON 也能做到但问题是你只改了用户昵称这一个字段也得把整个 JSON 读出来、反序列化、改完再写回去。而用 Hash可以直接对单个字段操作r.hset(user:1001, mapping{name: 张三, age: 30, city: 北京}) r.hset(user:1001, city, 上海) name r.hget(user:1001, name) all_info r.hgetall(user:1001) r.hincrby(user:1001, age, 1)这里面有几个实用细节hgetall返回的是一个 dict但由于 Hash 的存储结构特性这个 dict 里的字段顺序是不确定的需要排序的展示逻辑不要依赖它的顺序hincrby适合做字段级的计数比如文章点赞数、库存数比“先读后写”的方式安全得多。当字段数很小比如几十个以内Hash 的内部编码是 ziplist非常节省内存字段多了会自动切换成 hashtable这也是为什么存对象优先选 Hash 而不是 String。3.3 List简单消息队列与最新列表的两种正确姿势List 在 Redis 里是双向链表头尾操作都是 O(1)所以它天生适合两种场景。第一种是当消息队列用。生产者RPUSH进队消费者BLPOP阻塞等待实现一个简单的任务分发import redis r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) # 生产者 r.rpush(task:queue, task_1, task_2) # 消费者 _, task r.blpop(task:queue, timeout5) print(task)blpop如果队列为空会阻塞最多 timeout 秒超时后返回None。用这种方式做异步任务比轮询节省 CPU也比自己写 Socket 省心。但要注意List 消息队列没有“确认机制”消费者拉走消息后如果崩溃消息就丢了。对可靠性要求高的场景应该换用专门的消息中间件或者自己补一层处理表。第二种是最新列表比如最新的公告、最近浏览记录。用LPUSH往左插入然后LTRIM截断长度这样永远不会无限增长r.lpush(latest:notices, 公告A, 公告B) r.ltrim(latest:notices, 0, 9) # 只保留前10条3.4 Set 与 ZSet去重、关系计算和排行榜Set 是无序不重复集合适合做去重和关系运算。比如记录用户标签、统计访问过某页面的独立用户数或者算两个集合的共同部分r.sadd(user:1001:tags, python, redis, database) r.sadd(user:1002:tags, redis, backend) r.sinter(user:1001:tags, user:1002:tags) # 交集ZSet 是带分数score的有序集合常用来做排行榜。ZADD写入ZINCRBY增加分数ZREVRANGE取分数从高到低的前 N 名r.zadd(rank:reads, {article_1: 120, article_2: 90}) r.zincrby(rank:reads, 10, article_1) top r.zrevrange(rank:reads, 0, 2, withscoresTrue)排行榜、热榜、延时队列都是 ZSet 的经典场景。这里提醒一句withscoresTrue时返回的分数是字符串形式的要参与计算记得先转成 float。很多人搜“redis 数据类型”其实是在找这五种类型的区别别死记命令拿这张表当对照参考在项目里用过一遍自然就记住了。4. 存储前必须想清楚的三个问题过期策略、序列化与管道4.1 TTL让每个键的生命周期都可预期Redis 的内存不是无限的尤其作为缓存使用时必须给每个 key 规划好生命周期。过期操作相关命令很容易上手r.setex(cache:user:1001, 3600, data) r.expire(cache:user:1001, 3600) r.ttl(cache:user:1001) # 返回剩余秒数-1 表示没有过期时间-2 表示 key 不存在但这里有个更大的话题是内存淘汰策略。Redis 配置里的maxmemory-policy决定了内存到达上限时的行为策略行为noeviction不淘汰写命令直接报错allkeys-lru在所有 key 中按 LRU 淘汰最少使用的volatile-lru只在设置了过期时间的 key 中按 LRU 淘汰allkeys-random随机淘汰任意 keyvolatile-random随机淘汰带过期时间的 key最容易被坑的是如果生产环境设置了allkeys-lru那些你打算“永久保存”的 key 也随时可能被淘汰而且你根本感知不到直到某一天发现数据没了。所以我的习惯是缓存类 key 一定带 TTL被当作存储用的 key要么不设置过期但依赖持久化兜底要么明确接受淘汰风险绝不做“既不设 TTL 又指望不被淘汰”的假设。4.2 序列化方案json、pickle、msgpack 怎么选Redis 的五种数据类型最终都只能存字符串或 bytes所以往 Redis 里写 Python 对象dict、list、自定义对象之前必须先序列化。最常见的三种方案方案优点缺点适用场景json可读性好跨语言通用datetime/set 等类型需要额外处理对外接口、缓存结构体pickle什么 Python 对象都能存只限 Python有反序列化安全风险内部工具、管道重试任务msgpack体积小速度比 json 快生态不如 json 统一对存储空间敏感的批量缓存我在业务项目里主用 json因为它可读性强、出了问题能直接用 Redis Desktop Manager 看内容排查成本低。一个常见的写法是封装两个小函数import json, redis r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) def set_cache(key, value, exNone): r.set(key, json.dumps(value, ensure_asciiFalse, defaultstr), exex) def get_cache(key): data r.get(key) return json.loads(data) if data else Noneensure_asciiFalse是为了让中文肉眼可读defaultstr是为了处理 datetime、Decimal 这些 json 默认序列化不了的类型。注意如果你的数据包含 bytes、自定义类json 仍然会束手无策这种场景才需要上 pickle 或 msgpack。4.3 Pipeline批量写数据的网络开销优化初学者写批量数据最常见的做法是 for 循环里一条一条set。数据量小无所谓但如果你要写几千上万条每条命令都是一次完整的网络往返耗时会被 RTT 放大几十倍。Redis 提供了 Pipeline 管道把多条命令一次性打包发给服务端服务端也一次性返回全部结果pipe r.pipeline(transactionFalse) for i in range(10000): pipe.set(fbulk:{i}, i) pipe.execute()这里有个参数需要理解pipeline()默认transactionTrue会用 MULTI/EXEC 包裹所有命令相当于在一次事务中执行。如果你只是想减少往返、不需要事务语义就显式传transactionFalse。我实际测过写 1 万个小 key普通循环要几秒Pipeline 方式通常几百毫秒就能完成。不过 Pipeline 也不是银弹它会把所有命令缓存在内存里一次性 execute 时数据量太大反而可能撑爆内存或阻塞服务端。超大数据的导入更稳妥的做法是分批每批 1000 到 5000 条或者考虑 Redis 的MSET、HMSET这类原生批量命令。5. 实测踩坑排错从“连不上”到“数据悄然消失”的 5 条完整排查链路5.1 ConnectionError先判断是服务端没起床还是客户端找不到门“连不上 Redis”是最高频的问题。很多人第一反应是改代码、换端口但这过程太低效。我排错时基本按下面这条链路走先在能访问 Redis 的机器上执行redis-cli -h 127.0.0.1 -p 6379 ping确认服务端是否存活、本机能否连通。如果本机能通、程序不通检查连接参数是不是把 host 写成了localhost而服务端绑定了127.0.0.1或者反过来。检查 Redis 配置里的bind。默认配置通常只监听127.0.0.1别的机器自然连不上。要让内网其他机器连接需要把bind改成服务器内网 IP而不是直接设成0.0.0.0暴露公网。检查protected-mode。这个选项在默认配置下是yes当 Redis 没有设置密码且监听在非本机地址时它会拒绝外部连接这是安全保护不熟悉的容易误判成“连不上”。检查网络层云服务器安全组是否放行 6379 端口、交换机/防火墙是否拦截、容器端口映射是否正确。按照这个顺序90% 的 ConnectionError 都能定位。真正要改代码的情况很少。5.2 字节串引发的连锁 bugdecode_responses 到底要不要开有次我排查一个接口现象是缓存命中率极低代码逻辑看起来没问题先r.get(key)拿到值以后判断if value is not None然后返回。但用户反馈数据一直不更新。后来抓日志才发现Redis 里存的值读取出来是b{code:0}而代码里对内容的处理全是按字符串写的于是所有比对都失败缓存形同虚设。这种问题特别隐蔽因为默认情况下decode_responsesFalseRedis 返回的一律是 bytes。打印出来人眼看是正常的但只要参与字符串拼接、正则、JSON 解析、等于比较就会出幺蛾子。解决办法有两个一是全局开decode_responsesTrue这是我最推荐的方式二是每条命令读出来之后手动.decode()但那样容易漏而且一旦漏了又是下一个 bug。如果你用连接池还要注意不要在同一个进程里混用不同 decode 配置的客户端否则代码之间拿到的类型不一致问题会变得极其难排查。5.3 连接池耗尽并发一上来就 TimeoutError我在代码里抓到的真实元凶之前有个线上服务平时很稳一到活动大促就大量报TimeoutError: Timeout reading from socket。一开始以为是 Redis 服务端扛不住加配置、换实例都没用。后来看监控发现一个惊人事实Redis 服务端连接数飙到几千但每秒 qps 并不高。顺着代码翻下去发现问题出在一个业务函数里def update_data(key, value): r redis.Redis(host..., port6379) r.set(key, value)调用方很多每个请求都会创建新的Redis实例、新建连接池。请求处理完实例本该被垃圾回收但高并发下连接还没来得及释放新的连接又建立起来最终把服务端的连接数打满。正确做法非常简单在模块加载时就创建单例import redis r redis.Redis( host..., port6379, decode_responsesTrue, socket_timeout3, socket_connect_timeout3, max_connections100, )如果确需动态控制连接数可以显式创建ConnectionPool再传给Redis(connection_poolpool)。记住redis-py 的实例本身是线程安全的全局共用一个实例完全没问题没有必要每个请求都 new 一个。5.4 大 Key 与慢命令一个超长 List 拖垮整个实例的复盘还有一个经典事故排查某服务 CPU 居高不下redis-cli --bigkeys一查发现一个 List key 里积压了上百万条数据。因为这个业务把 Redis 当日志存储只往里面rpush从不清理。表面看每条操作都是 O(1)但问题是量级上来之后任何需要遍历这个 List 的命令都会一次占用大量内存和 CPU比如有人图省事执行了LRANGE key 0 -1直接把实例拖到 OOM。Redis 是单线程执行命令的一条慢命令会阻塞后面所有命令这就是为什么“大 Key”会被当成生产隐患。排查手段主要靠redis-cli --bigkeys扫描和SLOWLOG GET看慢命令。修复思路是不让 List 无限增长用LTRIM截断或者按时效拆 key按天/按小时分区存储查询时只取需要的区间不要全量取出。5.5 重启后数据“消失”持久化和内存淘汰策略在起作用这类问题的典型描述是服务一重启Redis 里某些 key 没了。排错时不要急着怀疑代码先分清两个机制。第一Redis 默认持久化方式叫 RDB 快照它会按配置的时间间隔把内存数据写入磁盘比如默认的 save 规则会在 900 秒内至少有 1 次写入就落一次盘。但如果服务在两次快照之间意外宕机中间的数据就会丢失。对不能接受丢失的业务要开启 AOF 持久化appendonly yesAOF 会把每次写命令追加到日志文件重启时通过回放恢复数据。第二即使配置了持久化如果内存达到maxmemory上限淘汰策略也会把 key 清掉。我见过一个案例某团队把 Redis 当永久存储用但实例 maxmemory 设置得很小策略是allkeys-lru结果热数据不断把“永久 key”挤掉看起来就是数据无规律消失。定位方法很简单看 Redis 日志里有没有evicted相关记录或者用INFO stats里的evicted_keys计数。真正解决要看数据定位缓存数据做好 TTL 和淘汰策略的匹配存储型数据要么换到磁盘数据库要么给 Redis 扩大内存并选择noeviction或只淘汰带 TTL 的 key。6. 业务落地把缓存用户信息做成项目里可复用的模块6.1 旁路缓存的标准写法防穿透的完整代码把前面这些知识串起来看一个真实的业务模板用旁路缓存模式Cache Aside缓存用户信息。流程是先读 Redis命中直接返回没命中就查 MySQL查到之后回填 Redis并设置 TTLMySQL 里也没有就把一个短时间的空值缓存起来防止恶意请求反复打数据库也就是防穿透。import json import redis r redis.Redis( hostlocalhost, port6379, db0, decode_responsesTrue, socket_timeout2, ) def get_user_info(user_id): cache_key fuser:info:{user_id} cached r.get(cache_key) if cached is not None: if cached __EMPTY__: return None return json.loads(cached) user query_user_from_mysql(user_id) if user is None: r.setex(cache_key, 60, __EMPTY__) return None r.setex(cache_key, 3600, json.dumps(user, ensure_asciiFalse, defaultstr)) return user这个模板看似简单其实包含了三个关键决策一是使用了带 TTL 的setex回填数据时直接指定过期时间避免先写再设置过期带来的窗口二是对空结果也做了缓存时间是 60 秒防止缓存穿透把压力全打给数据库三是 key 按业务:类型:ID的粒度组织后续查问题、按前缀批量清理都很方便。6.2 连接统一管理单例客户端、配置抽离和慢命令监控最后为了让上文里的所有经验落到真实工程里我建议把 Redis 连接独立成一个模块业务代码统一从这个模块拿客户端。最简单的方式是import os import redis REDIS_URL os.getenv(REDIS_URL, redis://localhost:6379/0) r redis.Redis.from_url(REDIS_URL, decode_responsesTrue)这里用from_url的意义在于连接信息完全由环境变量控制本地开发、测试环境、生产环境可以通过部署配置切换而不需要改动代码。同时这个模块里可以做一层很薄的日志包装统计每条命令的耗时超过阈值时记录到监控系统这样大 Key 和慢命令问题能在早期暴露。如果需要更规范的慢命令排查还可以在 Redis 侧开启SLOWLOG配置比如设置slowlog-log-slower-than 10000记录执行超过 10 毫秒的命令然后用redis-cli slowlog get查看。把侧边数据留好再遇到“Redis 偶发卡顿”就不用靠猜了。我个人在实际项目里还会加一条纪律业务代码不允许直接import redis去操作所有操作必须经过封装的模块。这么做不是为了限制自由而是当你要更换连接池参数、增加监控、统一序列化规则时只需要改一个文件全项目立刻生效。Redis 本身很稳定真正不稳定的是使用方式。把这套链路从连接到存储都理清之后你再去翻那些“redis 面试题”里的连接管理、缓存穿透、数据淘汰都会觉得其实都是讲同一件事。
返回列表