第六篇了。前面几篇我把 redis-py 的字符串、哈希、列表、集合、管道、事务这些业务命令都过了一遍。按理说,这种时候最该静下心来写的,反而是那些“不产生业务数据”的命令——服务控制与状态监控。原因很简单:很多人用 redis-py 写业务代码写得飞起,真到了线上报警、实例卡顿、内存告警的时候,只剩两种姿势——掏出 Redis Desktop Manager 点点点,或者抱着 redis-cli 一条条敲。这不是技术能力问题,是你手里没有一套属于自己团队的辅助函数库,没有把 Redis 的服务控制与状态监控能力沉淀成可复用的代码。
这篇文章我就把 redis-py 里跟服务生命周期、实例级数据操作、状态信息采集、慢查询诊断、内存评估相关的辅助函数全部拆开讲。重点不是罗列文档,而是告诉你每个操作背后有哪些坑、返回结构长什么样、什么时候该用、什么时候千万别用。最后我会给一个可以直接抄走的巡检脚本骨架,落地难度比你想象的小得多。
1. 服务控制与状态监控在 redis-py 里的真实位置
1.1 不要把它当成 redis-cli 的克隆体
很多人写 redis-py 代码时,习惯把它当成 redis-cli 的“同款封装”,觉得r.info()就是控制台敲INFO,r.config_get()就是敲CONFIG GET。方向上没错,但理解太粗了。
redis-py 在这些管理命令上做了几件 cli 不会替你做的事:
- 把服务端返回的文本协议解析成 Python 原生类型。
DBSIZE返回的是数字,INFO返回的是嵌套 dict,CLIENT LIST返回的是 list of dict,而不是一坨要自己 split 的字符串。 - 把 Redis 的“错误返回”统一成异常。比如对不存在的 key 执行
MEMORY USAGE,cli 会返回一个 nil,redis-py 直接给你None;对没有权限执行的命令,直接抛ResponseError。 - 连接层面的细节被隐藏了。你调用
bgsave()时,不需要关心这条命令该走读连接还是写连接,连接池会帮你安排。
所以,redis-py 的管理类 API 不是简单的命令转发,而是一套已经做了类型归一化和异常收敛的接口。我们要做的是在这之上再包一层,让它更贴近自己的运维场景。
1.2 值得封装的辅助函数清单
先拉一张总表,后面每一类我都会单独展开。
| 分类 | 主要函数 | 典型用途 |
|---|---|---|
| 服务生命周期 | save()bgsave()bgrewriteaof()shutdown() | 持久化、重启、备份 |
| 实例级数据操作 | flushdb()flushall()swapdb()select()randomkey() | 清理数据、切换库、抽样 |
| 状态采集 | ping()dbsize()info()lastsave()time() | 健康检查、巡检、时钟校准 |
| 配置管理 | config_get()config_set()config_rewrite()config_resetstat() | 在线调参、配置持久化 |
| 慢查询与连接 | slowlog_get()slowlog_len()slowlog_reset()client_list()client_kill()client_getname() | 慢命令定位、连接治理 |
| 内存诊断 | memory_usage()memory_stats()memory_doctor() | 大 key 排查、碎片率评估 |
这张表整体呈现出 redis-py 管理面 API 的全貌,下面我会一层一层拆开讲。你会发现,真正麻烦的不是“函数怎么调”,而是“返回值怎么用”“边界条件怎么处理”。
2. 服务生命周期控制:save、bgsave、shutdown 的封装与坑
2.1 save 和 bgsave 的真实选择逻辑
SAVE是同步持久化,Redis 主进程会阻塞,直到 RDB 文件写完才恢复对外服务。BGSAVE是 fork 一个子进程去写 RDB,主进程继续服务请求。从 redis-py 的角度看,两个方法都返回True,看起来人畜无害,但生产环境绝对不能乱调save()。
我见过一个真实案例:有人写了个定时任务,每天凌晨用redis-py调一次save()做备份,理由是“这样保险”。结果实例在主进程阻塞的几秒里堆积了大量请求,超时告警一堆。后来改成了bgsave(),问题立刻消失。
封装的时候,我的建议是:
def safe_bgsave(client, wait_for_save=False, timeout=60): """ 触发后台持久化。 wait_for_save=True 时,会轮询 lastsave() 直到时间戳变化,确认保存完成。 """ before = client.lastsave() ok = client.bgsave() if not ok: return False if not wait_for_save: return True deadline = time.time() + timeout while time.time() < deadline: if client.lastsave() > before: return True time.sleep(0.5) return False这里有个容易被忽略的点:BGSAVE执行之后,不能只看返回值。BGSAVE的返回值只能说明命令被接收了,子进程是否成功生成 RDB 文件,需要通过lastsave()的时间戳变化或者INFO persistence里的rdb_last_bgsave_status来确认。上面的封装用lastsave()做轮询,是个轻量可靠的方案。
对于 AOF 场景,对应的触发函数是bgrewriteaof()。它同样返回True,但 AOF 重写可能耗时较长,也需要轮询INFO persistence里的aof_last_bgrewrite_status来确认结果。
2.2 shutdown:客户端必须处理断连
shutdown()是所有管理命令里最“危险”的一个。redis-py 的签名是shutdown(nosave=False),对应 Redis 原生命令SHUTDOWN和SHUTDOWN NOSAVE。
调用之后会发生什么?Redis 服务端会关闭连接,然后进程退出。这个时候,你手里的 redis-py 连接会抛出一个ConnectionError,因为服务端把 socket 关了。很多人没意识到这一点,封装的时候不做异常处理,程序直接崩掉。
一个相对安全的重启前准备函数可以这样写:
def safe_shutdown(client, save=False, timeout=5): """ 安全关闭 Redis 实例。 save=True 对应 SHUTDOWN SAVE,save=False 对应 SHUTDOWN NOSAVE。 """ if save: try: client.save() except Exception as e: print(f"save failed before shutdown: {e}") try: client.shutdown(nosave=not save) except ConnectionError: # 服务端正常关闭连接,这是预期行为 print("connection closed as expected") except Exception as e: print(f"unexpected error during shutdown: {e}")关键点在于:把ConnectionError当成预期路径来处理,而不是把它打进异常告警里。否则每次正常重启都会收到一条假的“连接错误”报警。
2.3 在线调完配置以后记得 config_rewrite
说一个我踩过不少次的坑:config_set()只改内存配置,不改磁盘上的 redis.conf。如果实例重启,配置会回到老值。想要持久化,必须再调一次config_rewrite()。
def apply_config(client, key, value, rewrite=True): old = client.config_get(key) client.config_set(key, value) if rewrite: client.config_rewrite() new = client.config_get(key) return {"old": old, "new": new}这里要注意,不是所有配置项都支持CONFIG REWRITE,比如requirepass这类安全相关配置,Redis 在交互输入时有特殊处理,config_rewrite()会拒绝写入。封装的时候建议把ResponseError捕获住,不要把整个巡检脚本搞挂。
3. 实例级数据操作:flushdb、flushall、swapdb、select 的安全边界
3.1 flushdb / flushall 的同步刷新与异步刷新
flushdb()清当前库,flushall()清所有库。这是数据恢复领域最经典的“手滑”操作。redis-py 从 4.x 开始统一了异步参数:flushdb(asynchronous=False),flushall(asynchronous=False)。在 Redis 6.2 及以上版本,设成True会执行FLUSHDB ASYNC/FLUSHALL ASYNC,由后台线程释放内存,避免主线程卡死。
老版本 redis-py 里还有单独的async_flushdb()、async_flushall()方法,如果你是升级上来的老项目,先看清楚版本,别一上来就传asynchronous结果莫名报错。
我封装这类“毁灭级”操作时,一定会加确认令牌:
def flush_all(client, token: str, asynchronous=False): if token != "YES-CLEAR-ALL": raise ValueError("token mismatch, operation aborted") client.flushall(asynchronous=asynchronous)在自动化运维平台里,这层保护极其重要。宁可让操作者多确认一步,也别让自己成为生产事故的制造者。
3.2 swapdb 的原子交换
swapdb(db1, db2)是我比较偏爱的冷门命令。它原子地交换两个 db 的全部数据,时间复杂度 O(1)。在灰度发布、切流场景下很好用:新数据先写到 db1,验证没问题后把 db1 和 db0 交换,实现“热切换”;发现问题再 swap 回来,几乎无损。
redis-py 里的调用方式就是client.swapdb(0, 1),返回True。
封装时可以记录交换前后的dbsize()作为审计信息:
def swap_db(client, db_a, db_b, track=True): size_a = client.dbsize() if track and db_a == 0 else None size_b = client.dbsize() if track and db_b == 0 else None client.swapdb(db_a, db_b) return {"swapped": (db_a, db_b), "size_before_swap": {"db_a": size_a, "db_b": size_b}}别觉得这个封装没用。一旦出问题,你至少知道交换前的 db 规模,能判断是不是数据量异常导致的。
3.3 select 在连接池里的经典陷阱
这是很多人栽过跟头的地方。redis-py 里select(db)是存在的,但它和你在业务代码里“切库”的直觉不一样。
redis-py 的连接池在初始化连接时,通过connection_kwargs里的db参数来决定新连接默认选哪个库。如果你用Redis(host='localhost', db=0)创建客户端,然后手动调了一次select(1),会发生什么?手动select只对当前从池子里取到的这一条连接生效,这条连接归还到连接池之后,池子里还可能有其他连接仍然停留在 db 0。你接下来的命令可能走 db 0,也可能走 db 1,行为完全看连接池的脸色。
所以我的结论很直接:不要用select()来切换业务库。要访问另一个库,就创建一个新的 Redis 实例,db 参数传对应的库号,连接池可以复用同一个ConnectionPool:
pool = redis.ConnectionPool(host='localhost', port=6379) client_db0 = redis.Redis(connection_pool=pool, db=0) client_db1 = redis.Redis(connection_pool=pool, db=1)这样每条命令在取连接时就知道该选哪个库,干干净净,没有状态残留。
4. 状态监控辅助函数:info、config_get、dbsize、lastsave 的组合用法
4.1 info 的 section 参数与解析策略
info()是 Redis 状态监控的核心入口。不带参数时,它返回所有 section,数据量大,网络传输和解析成本都不小;带上 section 参数后,只返回指定部分,效率提升明显。
redis-py 4.x/5.x 中,info()返回的是按 section 嵌套的 dict,取值时要先定位到 section:
info_all = client.info() # 所有 info_mem = client.info('memory') # 只看内存 info_rep = client.info('replication') # 只看主从注意新老版本解析结构的差异。老版本里字段可能是扁平的,直接info['used_memory_human']就能取到;新版本里需要info['Memory']['used_memory_human']。如果你接手的是老代码,一不小心就 KeyError。
我习惯于封装一个带默认值的访问方法:
def get_info_metric(client, section, key, default=None): info = client.info(section) return info.get(section, {}).get(key, default)然后就可以这样用:
used_mem = get_info_metric(client, 'Memory', 'used_memory_human', 'unknown') connected_clients = get_info_metric(client, 'Clients', 'connected_clients', 0)结合热搜词里的“redis缓存治理”,日常巡检最该盯的其实是INFO stats里的keyspace_hits和keyspace_misses,算一下缓存命中率。低于某个阈值说明缓存设计可能有问题,而不是去盲目加容量。
4.2 config_get / config_set / config_rewrite 的配置管理组合
config_get(pattern)支持通配符。config_get('maxmemory*')能把maxmemory和maxmemory-policy一起查出来。返回值永远是个 dict,这是 redis-py 做了解析的结果,别当成 list 处理。
配置管理的封装里,我建议大家至少记录“变更前后对比”:
def set_config(client, key, value, rewrite=True): before = client.config_get(key) client.config_set(key, value) after = client.config_get(key) if rewrite: try: client.config_rewrite() except redis.ResponseError as e: print(f"config rewrite failed: {e}, will not apply after restart") return {"key": key, "before": before, "after": after}你可能会问:为什么config_get('maxmemory')返回的 value 是字符串而不是数字?因为协议层面所有配置值都是字符串,redis-py 为了保持一致性没有做类型强转。如果你需要比较数值,记得自己int()转换,否则'100mb' > '90mb'这种字符串比较会得出错误结果。
4.3 dbsize、lastsave、time 的巡检意义
这三个函数在监控里的作用经常被小看。
dbsize()返回当前库的 key 总量,单位是 int。它是判断数据倾斜、key 增长趋势的第一指标。注意它是精确值,不是近似值,大实例上执行有瞬时开销,巡检频率别太高。lastsave()返回最后一次成功生成 RDB 文件的时间戳(Unix 秒)。如果这个时间离现在太久,说明持久化可能出了问题。拿它跟time()返回的服务端时间做差值,就能算出“距离上次持久化已经过去多久”。time()返回服务端当前时间,是一个[seconds, microseconds]的列表。用它和客户端本地时间做对比,可以判断是否存在时钟偏移。Redis 主从复制、过期 key 清理都对时钟敏感,时间不同步会引发一堆诡异问题。
组合起来的巡检逻辑大概是:
server_time = client.time() last_save_ts = client.lastsave() ts_now = server_time[0] + server_time[1] / 1000000 staleness = ts_now - last_save_ts if staleness > 3600: print(f"persistence stale for {staleness}s")5. 慢查询定位与客户端连接排查:slowlog_get、client_list 的实战价值
5.1 slowlog 的耗时单位是微秒,不是毫秒
Redis 慢查询日志SLOWLOG GET记录的是超过slowlog-log-slower-than阈值的命令。redis-py 里用slowlog_get(num=None)读取,返回一个 list,每条记录通常包含这些字段:
id:慢查询记录编号start_time:Unix 秒duration:命令执行耗时,单位是微秒command:命令及其参数组成的列表client_addr、client_name:客户端来源
最容易踩的坑就是把duration当成毫秒去告警,结果阈值设小了,告警刷屏。Redis 的 slowlog 单位官方就是微秒,1,000,000 微秒才等于 1 秒。如果你习惯看毫秒,要自己除以 1000。
一段实用的慢查询封装:
def get_recent_slowlogs(client, limit=10, slow_ms=100): logs = client.slowlog_get(limit) result = [] for item in logs: duration_ms = item['duration'] / 1000.0 if duration_ms >= slow_ms: result.append({ 'id': item['id'], 'start_time': item['start_time'], 'duration_ms': round(duration_ms, 2), 'command': ' '.join( arg.decode() if isinstance(arg, bytes) else str(arg) for arg in item['command'] ), 'client_addr': item.get('client_addr'), }) return result正常情况下 Redis 单个命令耗时都是微秒级,能进 slowlog 的命令已经值得警惕了。如果频繁出现KEYS、HGETALL大哈希、SMEMBERS大集合,说明业务侧代码有问题,光加慢查询日志解决不了,得回去改数据结构设计。
5.2 client_list:定位连接来源与闲置连接
client_list()返回的是 list of dict,每条对应一个客户端连接,字段名在不同版本略有差异,但常见的addr、name、age、idle、db、cmd、tot_mem基本都有。
idle字段表示连接空闲秒数。如果空闲时间非常长,说明有连接泄漏,或者用了连接池但池大小配得过大。实践中我写过这样的辅助函数:
def kill_idle_connections(client, max_idle_seconds=300, whitelist=None): whitelist = whitelist or set() killed = [] for conn in client.client_list(): addr = conn.get('addr', '') if addr in whitelist: continue idle_sec = int(conn.get('idle', 0) or 0) if idle_sec > max_idle_seconds: client.client_kill(addr) killed.append(addr) return killed注意client_kill()的参数在不同版本有变化:老版本传地址字符串,新版本 redis-py 推荐用 filter 方式,比如client_kill(filter='addr', addr='...')。封装前先确认你用的 redis-py 版本。
很多公司排查“连接数被打满”问题时,第一反应是扩充maxclients,其实更应该先跑一遍client_list()看看是否有大量闲置连接长期占用。对长连接型应用,连接池空转问题是常态。
5.3 实际排查链路:慢命令背后往往是数据结构问题
我处理过一次线上实例 CPU 飙高的排查。第一步用slowlog_get(20)拿到最近 20 条慢命令,发现大量SMEMBERS操作,操作对象是一个大集合。第二步用client_list()定位到这些命令来自一个推荐服务。第三步用memory_usage()评估该 key 的大小,果然占了近 300MB。最后是业务侧把集合拆成多个小 key,CPU 立刻回落。
整个排查过程没有用到任何黑科技,就是围绕 slowlog、client_list、memory_usage 这三个辅助函数反复组合。这也是我在文章开头强调“沉淀自己的辅助函数库”的原因——排查能力的高低,其实就是你把这些函数组合起来的能力。
6. 内存诊断与容量评估:memory_usage、memory_stats 的封装实践
6.1 memory_usage 评估单 key 内存
memory_usage(key, samples=5)是 Redis 4.0 引入的命令,用来估算某个 key 占用的内存字节数。samples参数在 key 是集合类型(list、set、zset、hash)时有效,值越大估算越精确,开销也越大。
返回None表示 key 不存在。注意,它返回的是字节数,不是人类友好格式。封装时我喜欢顺手转一下:
def memory_usage_human(client, key, samples=5): size = client.memory_usage(key, samples=samples) if size is None: return None for unit in ('B', 'KB', 'MB', 'GB'): if size < 1024 or unit == 'GB': return f"{size:.2f}{unit}" size /= 1024配合randomkey()可以做一个“随机抽样”的大 key 扫描:
def sample_large_keys(client, sample_count=1000, threshold_mb=10): threshold = threshold_mb * 1024 * 1024 large = [] for _ in range(sample_count): key = client.randomkey() if key is None: break size = client.memory_usage(key) if size and size > threshold: large.append((key, size)) return large这个方案比KEYS *安全得多,但randomkey()在超大库上的随机性足够做初步筛查,精细的大 key 治理还是建议用scan_iter()配合memory_usage(),并且在业务低峰期执行。
6.2 memory_stats 与 info memory 的分工
memory_stats()返回的是内存分配的统计信息,字段像total.allocated、peak.allocated、startup.allocated、replication.backlog、clients.normal、clients.slaves之类。它和info('memory')的区别在于,info memory偏整体运行指标,memory_stats偏分配出处分解。
实际巡检里我会这样算内存碎片率:
stats = client.memory_stats() total_allocated = stats.get('total.allocated', 0) info_mem = client.info('memory') used_memory = info_mem.get('Memory', {}).get('used_memory', 0) if total_allocated and used_memory: frag_ratio = total_allocated / used_memory print(f"memory fragment ratio: {frag_ratio:.2f}")碎片率长期高于 1.5 或者低于 0.8,都是值得关注的信号。前者说明内存碎片多,后者说明可能出现内存异常占用或过度压缩。
6.3 大 key 删除的正确姿势
很多新人在发现大 key 后第一反应是del,这个行为很危险。删除一个几 GB 的 key 时,主线程会阻塞,线上服务直接抖一下。正确做法是慢慢删除:
- 如果 values 是 list/set/hash,用
ltrim、srem、hdel分批删。 - Redis 4.0 之后可以直接用
UNLINK命令,redis-py 里对应unlink(key),它是异步释放内存的,不会阻塞主线程。
配合记忆里的“redis分布式锁”和“redis面试题”这些背景,面试时常考的就是“如何安全删除大 key”。面试官想听到的回答就是UNLINK,以及为什么不能直接DEL。这个细节,写代码的时候一样适用。
7. 把辅助函数组装成巡检脚本:一个可落地的 mini 方案
7.1 巡检脚本骨架
下面这个脚本可以定时执行,把 Redis 实例的健康状态落成日志。它把我前面讲到的辅助函数组合成了一个最小可用集合。
import redis import json import time from datetime import datetime class RedisHealthCheck: def __init__(self, connection_params, timeout=3): self.client = redis.Redis( host=connection_params['host'], port=connection_params['port'], password=connection_params.get('password'), db=connection_params.get('db', 0), socket_connect_timeout=timeout, socket_timeout=timeout, ) def run(self): result = { 'ts': datetime.now().isoformat(), 'ping': self._ping(), 'basic': self._basic_info(), 'memory': self._memory_info(), 'slowlogs': self._slowlogs(), 'clients': self._client_stats(), 'persistence': self._persistence_info(), } return result def _ping(self): try: return self.client.ping() except redis.RedisError: return False def _basic_info(self): info = self.client.info('Server') return { 'redis_version': info.get('Server', {}).get('redis_version'), 'uptime_in_seconds': info.get('Server', {}).get('uptime_in_seconds'), } def _memory_info(self): mem = self.client.info('Memory') return { 'used_memory_human': mem.get('Memory', {}).get('used_memory_human'), 'peak_memory_human': mem.get('Memory', {}).get('used_memory_peak_human'), 'maxmemory_human': mem.get('Memory', {}).get('maxmemory_human'), } def _slowlogs(self): logs = self.client.slowlog_get(5) return [{ 'duration_ms': round(item['duration'] / 1000.0, 2), 'command': ' '.join( arg.decode() if isinstance(arg, bytes) else str(arg) for arg in item['command'] ), } for item in logs] def _client_stats(self): clients = self.client.client_list() total = len(clients) idle_gt_300 = sum( 1 for c in clients if int(c.get('idle', 0) or 0) > 300 ) return {'total': total, 'idle_gt_300': idle_gt_300} def _persistence_info(self): try: info_ps = self.client.info('persistence') return { 'rdb_last_bgsave_status': info_ps.get('Persistence', {}).get('rdb_last_bgsave_status'), 'aof_last_bgrewrite_status': info_ps.get('Persistence', {}).get('aof_last_bgrewrite_status'), } except redis.ResponseError: return {'error': 'persistence section unavailable'}这个结构很简单,但它把最核心的几个指标都覆盖了:连通性、版本、运行时长、内存占用、慢查询、连接闲置、持久化状态。
7.2 巡检脚本的风险控制和执行策略
巡检脚本本身也不能瞎写,有三条红线:
第一,socket_timeout 一定要设置。如果 Redis 实例卡死,没有超时控制的脚本会永远等下去,连带监控系统一起挂掉。
第二,避免在巡检脚本里执行重量级命令。info()尽量带 section,slowlog_get()限制条数,client_list()在连接数很大的实例上有解析开销,不要秒级执行。
第三,不要用巡检脚本代替真实的监控系统。这类脚本的价值在于故障定位和快速现场留痕,真正的指标采集还是交给专业的监控平台。
我还建议把巡检结果和告警通知打通。脚本每次运行把结果序列化成 JSON,推到日志系统,出现异常时能立刻看到故障时刻的现场快照。
7.3 后续可以怎么扩展
这套辅助函数库再往下走,可以扩展的方向不少。比如加上哨兵或多实例的支持,从SENTINEL或集群的CLUSTER INFO里拉取节点状态;再比如把配置变更记录落到独立表,方便审计;还可以把slowlog历史做周期性归档,用于追踪性能退化趋势。
就拿多实例巡检来说,最省事的办法是在上面RedisHealthCheck的基础上套一层循环:
instances = [ {'host': '10.0.0.1', 'port': 6379}, {'host': '10.0.0.2', 'port': 6379}, ] for params in instances: try: result = RedisHealthCheck(params).run() print(json.dumps(result, ensure_ascii=False)) except Exception as e: print(json.dumps({'host': params['host'], 'error': str(e)}))这种组合方式没有把复杂的东西暴露给外面,就是一个个类方法堆叠,但是可读性、可维护性都很好。
说实话,这篇文章里没有一条命令是文档里查不到的。真正值钱的,是你在什么场景下选择哪条命令,以及你把这些命令组合成辅助函数时踩过的坑。我在生产上把上面的逻辑整理成了一个health_check模块,每次预警都会自动拉一份信息快照,再决定要不要人工介入。你可以从最小的一行dbsize()开始,逐步把自己对 Redis 的服务控制与状态监控经验,也沉淀成能复用的代码。