
最近不少朋友在折腾Python后端的时候都会碰到同一个需求把数据写进Redis再从Redis里读出来用。Redis本质上就是一张住在内存里的超级大表读写速度能到毫秒级而Python连Redis最主流的方案就是redis-py这个库。这篇文章我不想只贴几个API了事而是打算把从环境准备开始到连接方式、参数选型、五种数据类型怎么存、存的时候有什么坑再到线上常见的故障排查一次讲透。不管你是刚把Python装好的新手还是已经写过好几个接口的进阶选手照着这篇文章把代码跑一遍基本就能在自己的项目里独立接Redis了。1. 环境准备先把Redis和Python的底座打好1.1 Redis服务端安装与启动先明确一件事Redis是一个独立的服务端程序Python只是它的客户端。你在Python里写import redis不代表Redis就跑起来了。所以第一步永远是先把Redis服务端装好。在Linux上最省事直接用包管理器# Ubuntu / Debian sudo apt update sudo apt install redis-server # CentOS / RHEL sudo yum install redismacOS用户如果装了Homebrew一行就搞定brew install redis装完之后启动也不同平台有别# Linux多数发行版安装后就自动注册成服务 sudo systemctl start redis sudo systemctl enable redis # macOS 用 brew services 管理 brew services start redisWindows这边要单独说。Redis官方早就不提供Windows原生版本了我试过网上那些第三方编译版版本旧、还时不时出点诡异问题。现在最靠谱的两条路一条是装WSL2后在Linux子系统里装另一条是直接用Docker跑docker run -d --name redis -p 6379:6379 redis:7启动之后无论如何先验证一下服务端是否真的活着。Redis自带一个极简的测试命令redis-cli ping如果返回PONG说明服务端已经正常待命这时候再进入Python环节才有意义。1.2 Python侧安装redis-pyPython连接Redis的库有很多redis-py是维护最活跃、文档最全、社区用户最多的一个也是事实上的标准。安装没有任何悬念pip install redis装完确认一下版本不同大版本的API风格差异挺多的python -c import redis; print(redis.__version__)我这篇文章里用的示例代码基于redis-py 4.x或5.x如果你装的是更老的2.x、3.x建议直接升到新版。老版本的连接方式也能跑但很多参数名、默认行为都不一样照着新教程写容易踩踩版本差异的坑。顺带说一句在虚拟环境里安装是最佳实践别图省事直接往全局环境里塞。用python -m venv venv建一个隔离环境再激活后安装能少掉未来依赖冲突的一堆麻烦。2. 连接Redis的核心方式与参数拆解2.1 从最基础的连接说起redis-py最基础的用法是直接创建一个Redis实例import redis r redis.Redis(host127.0.0.1, port6379, db0) r.set(hello, world) print(r.get(hello)) # bworld这段代码的含义是告诉客户端去连接127.0.0.1这台机器上的6379端口选第0号逻辑数据库然后执行一次写入、一次读取。这里有个新手最容易犯迷糊的点r.get(hello)返回的是bworld而不是world。原因在于redis-py默认把返回值当成字节串来处理因为它不知道你到底存的是文本还是二进制数据。想让它自动解码成字符串需要在创建客户端时传入decode_responsesTruer redis.Redis(host127.0.0.1, port6379, db0, decode_responsesTrue) print(r.get(hello)) # world这个参数是新手遇到的第一座山后面排查章节我还会专门展开讲。2.2 连接对象与连接池的区别直接创建redis.Redis()对象做少量操作没问题但在高并发项目里每次操作都新建连接、用完再断资源开销是非常大的。Redis是基于TCP的握手、认证都有成本每秒几千次写入的场景下频繁建连会直接拖垮性能。正确的做法是用连接池。连接池的原理和数据库连接池一样预先创建一批连接放在池子里用的时候借一条用完归还循环复用pool redis.ConnectionPool( host127.0.0.1, port6379, db0, decode_responsesTrue, max_connections50, ) r redis.Redis(connection_poolpool)创建redis.Redis时如果传了connection_pool它就不会自己单独建连而是从池子里借。max_connections控制池里最多能有多少条连接。默认值是2的31次方减1相当于不限但生产环境我强烈建议手动指定一个合理上限否则突发流量会把Redis的连接数拉到峰值反过来影响服务端性能。还有一个隐藏细节同一个连接池可以被多个Redis实例共享也就是说两个业务模块可以共用一个池子只要它们访问的是同一个Redis实例、同一个db。2.3 连接参数逐个说清楚很多人在配置连接时只看得懂host和port其他参数一律不填。这些参数平时确实用不上但遇到问题、做调优的时候它们就是救命稻草。host和portRedis服务端地址和端口默认是localhost和6379。填127.0.0.1和填localhost在大部分机器上等效但如果本机做了域名解析的特殊配置比如IPv6优先两者就可能出现行为差异建议直接写IP。password服务端设置了密码时必填没设就不填。密码对应Redis配置里的requirepass。dbRedis默认有16个逻辑数据库编号0到15db0指默认库。逻辑库的作用是数据隔离比如同一个Redis实例里缓存数据放db0、业务数据放db1。不过话说回来逻辑库只是简单的编号隔离并没有真正的权限控制如果项目规模上来了更推荐用不同的Redis实例来做真正的隔离。socket_timeout单次读写操作的超时时间单位秒。默认是不超时意味着如果Redis端卡住了你的程序就可能一直等下去。我一般建议设置成3到5秒避免一个慢查询把整个服务线程拖死。socket_connect_timeout建立TCP连接的超时默认一样不限制。设置成2到3秒比较合理Redis挂掉的时候快速失败而不是卡在connect上让请求堆积。socket_keepalive是否开启TCP保活默认不开。如果客户端和Redis之间有NAT设备或防火墙空闲连接可能被静默掐断开启保活并配合下面的健康检查能减少这种幽灵断连。health_check_interval连接空闲多久后在下次使用前进行一次PING检查直到失败就重连。默认是0表示不启用。生产环境我建议设30秒能有效规避连接被中间网络设备悄悄关闭导致的报错。retry_on_timeout超时后是否自动重试。默认False。打开之后副作用是可能造成命令重复执行所以只建议在幂等操作场景开启。这些参数看起来不起眼但线上很多连接莫名其妙断了、程序假死的案子最后都能追溯到某几个参数没配置好。3. Redis五大数据类型的存储实操3.1 String类型万能的缓存和王牌计数器String是Redis里最基础、最常用的类型。一个key对应一个valuevalue本身是字符串但你可以通过命令把它当整数来做自增运算。最常见的场景就是缓存一段文本比如把接口返回的JSON存进去设置过期时间import json user_info {name: 张三, age: 30} r.setex(user:1001, 3600, json.dumps(user_info))setex的三个参数分别是key、过期秒数、value。这样写入后1小时内这个key自动失效非常适合缓存场景。取出来记得要json.loads还原data r.get(user:1001) if data: user_info json.loads(data)另一个经典用法是计数器比如统计接口调用次数、限流用的滑动窗口计数。Redis的incr是原子操作在高并发下不会丢数据r.incr(request:count:20250101) count r.get(request:count:20250101)我之前处理过一个活动抽奖的限流需求就是靠incr加expire实现的。每次用户抽奖先incr一个按用户ID和时间片组成的key第一次自增后顺手设置过期时间值超过阈值就拒绝。这套逻辑在单机Redis下性能非常稳定。3.2 Hash类型存对象数据的天然选择Hash可以理解成一个key下面挂了一个小型的字段-值映射表。你存一个用户的多个属性时用Hash比用String存一整段JSON要灵活得多因为你可以只更新其中一个字段不需要取出来反序列化整个对象再写回。r.hset(user:1001, mapping{ name: 张三, age: 30, city: 北京, }) r.hget(user:1001, name) # 张三 r.hgetall(user:1001) # {name: 张三, age: 30, city: 北京}注意一点hgetall会一次性把所有字段取回来字段很多且只需要其中一两个时用hmget指定字段更省流量r.hmget(user:1001, name, city) # [张三, 北京]Hash在存储对象属性、购物车、配置项这类一物多属性的数据时特别好用。我见过有人把用户的积分、等级、最近登录时间全塞进一个Hash里更新其中一个字段时整个对象的数据一致性依然有保障代价也小。3.3 List、Set、ZSet队列、去重和排行榜List类型是双向链表特别适合做简单的消息队列。生产者从左边塞消费者从右边取r.lpush(task:queue, job_1) r.lpush(task:queue, job_2) while True: job r.rpop(task:queue) if job is None: break print(f处理任务: {job})这里有个讲究用brpop替代rpop可以阻塞等待队列为空时挂起而不是立刻返回None这样就不用在轮询里空转浪费CPU。Set类型是去重的无序集合适合做标签、关注关系、去重判断r.sadd(user:1001:tags, python, backend, python) r.smembers(user:1001:tags) # {backend, python}上面连续sadd了两次python第二次不会重复写入这是Set天然的约束。ZSet是有序集合每个成员带一个分数Redis按分数排序典型用途就是排行榜r.zadd(leaderboard:2025, mapping{ player_1: 100, player_2: 80, player_3: 120, }) r.zrevrange(leaderboard:2025, 0, 2, withscoresTrue)zrevrange按分数从高到低取前三名配合withscoresTrue顺便拿到分数。排行榜这种需求用关系数据库查起来要order by加limit数据量大时还得考虑索引放到Redis里就是一条命令的事。3.4 过期时间的进阶玩法Redis的过期机制是懒过期加主动过期结合。懒过期是读的时候发现过期就删主动过期是后台每100毫秒采样一批过期key进行清理。这意味着一个key到了过期时间并不会被立刻物理删除你刚好在那之前读了它就可能读到本应失效的数据——虽然概率很低但设计缓存时要留个心眼。给已经写好的key添加过期时间是常规操作r.expire(user:1001, 3600) ttl r.ttl(user:1001) # 剩余秒数-1表示永不过期-2表示key不存在对于批量缓存更新我习惯在写入时就带上过期时间而不是写入后再用expire补少一次RTT不说逻辑上也更清晰。4. 完整实战写一个可复用的Redis存储模块4.1 从零封装一个连接模块把前面的知识点串起来生产环境里我习惯封装成一个单独的模块业务方只负责传key和value不关心连接池怎么创建、序列化怎么处理。下面给一个可直接抄作业的示例文件名叫redis_client.pyimport json import redis class RedisClient: def __init__(self, host: str 127.0.0.1, port: int 6379, password: str None, db: int 0, max_connections: int 50): self.pool redis.ConnectionPool( hosthost, portport, passwordpassword, dbdb, decode_responsesTrue, socket_timeout3, socket_connect_timeout3, socket_keepaliveTrue, health_check_interval30, max_connectionsmax_connections, ) self.client redis.Redis(connection_poolself.pool) def set_json(self, key: str, obj, expire: int None) - bool: value json.dumps(obj, ensure_asciiFalse) return self.client.set(key, value, exexpire) def get_json(self, key: str): value self.client.get(key) if value is None: return None return json.loads(value) def delete(self, key: str): self.client.delete(key) def get_client(self) - redis.Redis: return self.client这个工具的写法有几个关键设计说给各位参考decode_responsesTrue放在连接池构造里那么所有从连接池借出来的连接都默认字符串解码省去到处转码的麻烦。socket_timeout3保证单个命令最多等3秒Redis无响应时快速失败不会把请求线程挂死。set_json和get_json专门处理JSON序列化内部自动把Python对象转成JSON字符串、再转回来。业务层调用时感知不到序列化细节。使用方式就简单了from redis_client import RedisClient cache RedisClient(host127.0.0.1, db1) cache.set_json(user:1001, {name: 张三, age: 30}, expire3600) user cache.get_json(user:1001)4.2 序列化选型JSON是默认答案别轻易用pickle前面示例用了json.dumps这是最稳妥的选择。原因是JSON可读性强、跨语言通用前端或者其他语言的服务也能毫无障碍地读取。需要注意中文会被转成unicode转义序列所以我在set_json里加了ensure_asciiFalse存进去的字符串直接显示中文排查问题的时候一目了然。有同学图方便想用pickle直接把Python对象二进制序列化存进去我强烈不建议。两个理由一是pickle格式只有Python能读其他语言完全没法解析二是pickle在反序列化时存在任意代码执行风险如果数据源不可信等于把后门开给攻击者。除非你明确知道自己存的对象没法简单转JSON比如含有datetime、Decimal才考虑用json.dumps加一个自定义default函数来处理特殊类型。日常项目里90%的场景JSON都够用了。4.3 缓存过期时间的策略设计expire设置多少合适看起来是拍脑袋的事实际有讲究。缓存热点数据过期时间建议加上一个随机抖动。比如基准3600秒再随机加0到300秒避免大量key在同一秒集体过期造成缓存雪崩。缓存防穿透如果key对应的数据在数据库里根本不存在可以考虑把这个空结果也缓存一小段时间比如60秒避免恶意请求反复穿透到数据库。更新策略更新数据库后先更新缓存还是先删缓存业界讨论很多。我的习惯是先更新数据库再删除缓存下次读取时缓存缺失再回源数据库重建。这个方案在绝大多数场景下简单可靠比先写缓存再更新数据库的脏数据概率要低得多。5. 常见故障排查与避坑实录5.1 连接被拒绝不是代码的问题是环境的问题我最常收到的问题就是为什么我代码连不上Redis报错ConnectionError: Error while reading from socket。第一步永远是先确认Redis服务端真的在跑redis-cli ping如果返回Could not connect to Redis at 127.0.0.1:6379: Connection refused那就是服务端压根没启动或者启动后崩了。去服务端日志看一眼常见原因是配置了bind 127.0.0.1但你的代码从另一台机器连过来自然被拒绝。这时候修改Redis配置把bind改成需要的IP或0.0.0.0同时注意防火墙和网络安全然后重启服务。还有一类情况是Redis启动了但处于保护模式protected mode。默认配置下Redis只能被本机访问外部IP连接会被明确拒绝。如果确实是内网测试环境需要其他机器连需要修改配置或者显式设置密码后关闭保护模式。5.2 认证失败密码不匹配不是因为redis-py有bug如果Redis配置了requirepass而连接代码没带密码或者带错了会报AuthenticationError。这个排查思路很直接# 命令行带密码验证 redis-cli -a your_password ping命令行能通说明密码没问题那就是代码里的password参数写错了。命令行也不通直接去Redis配置文件里找requirepass那一行确认密码。需要注意的是密码里如果带有特殊字符命令行里可能要加引号。代码里则老实写成字符串注意别把空格、换行这些不可见字符带进去。5.3 存进去是字符串取出来是bytes到底怎么回事这是新手最高频的困惑也是decode_responses这个参数的知识盲区。redis-py默认把所有value当成二进制数据返回所以r.get(key)得到bvalue。如果连接配置了decode_responsesTrue它才会自动转成str。但有一个坑经常被忽略decode_responses只影响连接层的自动解码它不会帮你把JSON字符串转成Python对象。你存的是JSON字符串取回来decode后得到JSON字符串还得自己json.loads。所以在封装工具类时get_json里不仅要依赖decode更要显式处理反序列化。5.4 命令超时和连接池耗尽线上偶发redis.exceptions.TimeoutError或者ConnectionPoolError: Connection pool exhausted这两类问题的根源完全不同但都和连接配置有关。超时命令执行超过了socket_timeout的阈值。先看Redis的慢日志SLOWLOG GET能告诉你哪些命令耗时高。如果确实有大key导致的慢命令拆key是正路。如果Redis本身很快但网络有抖动适当调大socket_timeout也能缓解。连接池耗尽池子里所有连接都被借出且未归还。常见原因是业务代码用pipeline或订阅发布时忘了释放或者某个线程被慢查询卡在线程里迟迟不归还连接。排查时可以用CLIENT LIST看当前连接数再对照池子的max_connections基本能定位是哪个模块把连接占满了。我之前遇到过一例比较典型的有个定时任务每5分钟跑一次全量数据同步每次开50个并发线程同时从池子里借连接正常没问题。某天数据量翻倍单次同步耗时从1分钟变成10分钟池子里的50条连接全被占着不放其他业务立刻报连接池耗尽。后来把同步任务改成分批处理控制并发数问题再没出现过。附一个排查速查表方便对照报错现象可能原因建议动作Connection refusedRedis未启动、端口错误检查redis-cli ping与服务端日志AuthenticationError密码错误或未传password用redis-cli -a验证密码TimeoutErrorRedis慢命令、网络抖动看SLOWLOG视情况调大socket_timeoutConnection pool exhausted连接未归还、并发过高检查业务代码控制并发调大池子上限socket read timeout网络不稳定、连接被中间设备掐断开启socket_keepalive与health_check_intervalResponseError: WRONGTYPEkey类型与命令不匹配TYPE key查看类型删除或改名重建5.5 存量key的类型冲突还有一种问题特别让人抓狂明明代码是对的一执行就报WRONGTYPE Operation against a key holding the wrong kind of value。原因是同一个key之前被用别的类型写过比如以前用set存过字符串后来你又拿它去hsetRedis不会自动帮你转换类型直接拒绝。遇到这种情况TYPE key查一下类型DEL key删掉重建就行。存key的命名规范从源头做好能避免绝大多数这类问题。6. 一些生产环境的实操心得最后分享几个我在实际项目里总结的做法不是什么高深理论但都是踩过坑换来的。第一个心得是Redis的key一定要有命名规范。我常用的格式是业务名:实体名:ID例如user:info:1001、order:status:8888。好处是排查问题时能通过前缀快速定位是一类数据也可以用SCAN user:*遍历相关key。别小看这件事等Redis里有几千万个key的时候连命名都乱成一团线上事故处理起来会非常煎熬。第二个心得是Redis里真的只放值得放的数据。Redis是内存数据库内存比磁盘贵得多放进去的数据越大成本越高。那些动辄几百KB的文本、大列表放之前认真想想能不能换一种存储方案。我在一个项目里见过有人把整个文件内容以base64形式塞进Redis一个key就占几十MB这种行为基本等同于花钱买罪受。第三个心得是监控一定要提前做。接入Redis的同时用INFO memory关注内存使用量、INFO stats关注命中率设置好告警。Redis宕机不可怕可怕的是它内存撑爆了、swap开始拖垮整机性能而你完全不知道。第四个心得是关于发布订阅和分布式锁这类进阶能力。热词里经常能看到redis分布式锁这个确实是刚需但用的时侯一定要注意锁的过期时间与业务执行时间的匹配避免锁提前释放导致并发进入临界区。业界成熟的做法是用红锁算法或者引入更专门的协调服务单机Redis的分布式锁只能满足多数普通场景。不要因为Redis功能多就把它当万能数据库用有些一致性要求很高的数据关系数据库的主键约束和事务机制是不可替代的。文章写到这里正好把连接Redis从安装、连接方式、数据类型到线上问题排查整个链路都过了一遍。如果你照着示例代码把连接池封装和JSON读写跑通了接下来完全可以自己动手扩展比如做接口级的缓存中间件或者写一个基于ZSet的简易排行榜服务。我自己第一次真正吃透Redis就是从封装一个RedisClient开始的把每个参数、每种数据类型都亲手验证一遍后面再碰到生产和性能问题心里就有底得多。这套东西并不难难的是迈出第一步亲手敲一遍代码。