【Redis从入门到精通】第25篇:Redis数据库那些事——16个数据库的选择与键空间管理

发布时间:2026/6/1 22:40:40

【Redis从入门到精通】第25篇:Redis数据库那些事——16个数据库的选择与键空间管理 上一篇【第24篇】Redis内存优化完全指南——从对象系统到encoding调优下一篇【第26篇】Redis过期键机制——TTL的生死时钟是怎么走的今天我们来聊一个很多Redis初学者都会好奇的话题Redis到底有几个数据库你可能会脱口而出16个0到15SELECT切换。但如果我再问一句这16个数据库底层是怎么实现的为什么生产环境普遍不推荐用多数据库SCAN又是怎么做到不阻塞遍历整个键空间——可能你就需要往下读了。这篇文章我们把Redis数据库的老底全部翻出来看看。一、16个数据库从哪来当你启动一个Redis实例默认就创建了16个数据库编号从0到15。这个数字来自配置文件中的databases参数# redis.confdatabases16你可以调大这个值但一般没这个必要——原因后面会讲。用SELECT命令在数据库之间切换# 默认在0号数据库127.0.0.1:6379SET key1value1OK# 切换到1号数据库127.0.0.1:6379SELECT1OK127.0.0.1:6379[1]GET key1(nil)# 0号数据库的key在1号数据库里看不到# 切回0号127.0.0.1:6379[1]SELECT0OK127.0.0.1:6379GET key1value1# 又回来了这里的数据库和我们平时说的MySQL、PostgreSQL那种数据库完全是两码事。Redis的数据库更像是命名空间或分区它们之间完全隔离互不干涉。二、redisDb结构体数据库的身份证在Redis源码中每个数据库都是一个redisDb结构体。16个数据库就是16个这样的结构体存储在server.db数组中// Redis源码server.htypedefstructredisDb{dict*dict;// 键空间——存储所有key-value对dict*expires;// 过期字典——存储key的过期时间dict*blocking_keys;// 被BLPOP等阻塞命令等待的keydict*ready_keys;// 阻塞解除后待处理的keydict*watched_keys;// 被WATCH命令监视的keyintid;// 数据库编号0-15longlongavg_ttl;// 平均TTL用于统计unsignedlongexpires_cursor;// 过期键扫描游标list*defrag_later;// 待碎片整理的key列表}redisDb;来看看这16个数据库在内存中的层次结构Redis数据库层次结构16个数据库 server.db[] 数组 ------------------------------------------------------------------- | db[0] | db[1] | db[2] | ... | db[14] | db[15] | ------------------------------------------------------------------- 单个redisDb结构内部 redisDb (以db[0]为例) ---------------------------------------------- | id: 0 | | | | dict (*) 键空间核心 | | ------------------- | | | user:1001 → val | | | | counter → val | | | | msg:queue → val | | | | ... | | | ------------------- | | | expires (*) 过期字典 | | ------------------- | | | user:1001 → 1750| | | | counter → 1800| | | ------------------- | | | blocking_keys (*) 阻塞键集合 | watched_keys (*) WATCH监视的key | | | avg_ttl: 3600 | ----------------------------------------------关键点16个数据库的键空间是完全独立的dict。这就是为什么你在0号库SELECT到1号库后同一个key访问不到——它们在不同的dict里。三、键空间的本质就是你熟悉的老相识——dict键空间keyspace是Redis数据库最核心的组件。它本质上就是一个dict字典/哈希表结构。我们之前讲Hash对象时详细分析过dict这里键空间用的就是同一个东西。键空间dict与数据对象的关系 redisDb.dict (键空间) --------------------------------------------------- | | | dictEntry dictEntry | | ---------- ---------- | | | key |user:1001| key |counter | | | val (*) | | val (*) | | | ---------- | ---------- | | | v v | | ----------- ----------- | | |redisObject| |redisObject| | | |type: HASH | |type:STRING| | | |encoding:ZL| |encoding:INT| | | |ptr →数据 | |ptr → 100 | | | ----------- ----------- | --------------------------------------------------- 层次关系 redisDb → dict → dictht → dictEntry → redisObject → 实际数据用ASCII画清楚这层关系redisDb → dict → dictEntry 的层次结构 redisDb (数据库层) │ ├── dict (键空间 一个dict) │ │ │ ├── dictEntry → redisObject (key1 → value1) │ ├── dictEntry → redisObject (key2 → value2) │ └── dictEntry → redisObject (key3 → value3) │ ├── expires (过期字典 另一个dict) │ ├── dictEntry → 过期时间戳 (key1 → 1750000000) │ └── dictEntry → 过期时间戳 (key3 → 1750086400) │ ├── blocking_keys ├── watched_keys └── ... 要点 1. 键空间就是一个dictkey是字符串value是redisObject指针 2. 每个键对应一个redisObject其中包含type、encoding、实际数据 3. 过期信息单独存在expires字典中不在dict里冷知识正是因为键空间是dictRedis才能做到O(1)的key查找。不管你有1个key还是1亿个keyGET mykey的速度理论上是一样的。四、键的增删改查全过程在键空间dict层面每次键操作不仅要完成基本的增删改查还要附带一系列维护工作。读键时的维护操作// Redis源码db.c lookupKeyReadWithFlags()// 读键时Redis在背后做了什么robj*lookupKeyReadWithFlags(redisDb*db,robj*key,intflags){robj*val;// 1. 检查键是否过期if(expireIfNeeded(db,key)1){// 键已过期处理过期...}// 2. 从dict中查找keyvallookupKey(db,key,flags);// 3. 更新访问统计if(val){// 更新LRU时钟最近访问时间// 对于LFU模式更新访问频率计数}// 4. 更新键空间的命中/未命中统计server.stat_keyspace_hits;// 或 server.stat_keyspace_missesreturnval;}读键操作的背后以 GET user:1001 为例 客户端请求: GET user:1001 │ ├── 1. 检查 user:1001 是否过期 │ ├── 是 → 删除键返回 nil │ └── 否 → 继续 │ ├── 2. 在 db.dict 中查找 keyuser:1001 │ ├── 找到 → 获取 redisObject 指针 │ └── 未找到 → 返回 nil统计misses │ ├── 3. 更新访问统计 │ ├── LRU模式: 更新 redisObject 的 lru 字段 │ └── LFU模式: 更新访问频率计数器 │ ├── 4. 更新 server.stat_keyspace_hits命中统计 │ └── 5. 返回 value 给客户端 注意步骤3发生在返回数据之前这正是LRU/LFU机制的数据来源写键时的维护操作// Redis源码db.c// 写键时Redis在背后做了什么voidsetKey(client*c,redisDb*db,robj*key,robj*val,intflags){// 1. 键是否已存在if(lookupKeyWrite(db,key)NULL){// 2. 新键插入到db.dict中dbAdd(db,key,val);}else{// 3. 已存在覆盖旧值dbOverwrite(db,key,val);// 注意如果旧值使用了共享对象如0-9999的整数// 需要减少引用计数}// 4. 标记数据库为脏有修改server.dirty;// 5. 移除此键的过期时间SET命令会清除过期removeExpire(db,key);// 6. 通知监视此键的客户端WATCH命令相关signalModifiedKey(db,key);// 7. 发送键空间通知Keyspace NotificationsnotifyKeyspaceEvent(NOTIFY_STRING,set,key,db-id);}写键操作的背后以 SET counter 100 为例 客户端请求: SET counter 100 │ ├── 1. 检查counter是否已存在 │ ├── 不存在 → dbAdd()插入新的dictEntry │ └── 存在 → dbOverwrite()更新dictEntry的value指针 │ ├── 2. 清除counter的过期时间SET命令默认行为 │ 除非使用了 SET counter 100 EX 10 KEEPTTL 等选项 │ ├── 3. server.dirty标记数据库有修改 │ 这个计数器影响RDB/AOF持久化 │ ├── 4. signalModifiedKey() │ 通知正在WATCH counter的事务客户端 │ ├── 5. notifyKeyspaceEvent() │ 触发键空间通知事件订阅者可以收到变化消息 │ └── 6. 返回 OK 给客户端 额外操作当键是新键时 ├── 创建新的redisObject ├── 创建新的dictEntry └── 如果key的数量超过了哈希表的负载因子触发rehash五、多数据库的使用建议一句话——别用了虽然Redis提供了16个数据库但真正在生产环境中使用多数据库的场景非常少。不是功能不好而是有几个致命缺陷# 多数据库的主要问题演示# 问题1FLUSHDB/FLUSHALL 很危险127.0.0.1:6379SELECT1OK127.0.0.1:6379[1]FLUSHDB# 只清空当前数据库# 但如果手滑写成FLUSHALL全部16个库都空了# 问题2不同库不能分别认证# Redis的AUTH是整个实例级别的无法给库0设一个密码、库1设另一个密码# 问题3不支持跨库事务127.0.0.1:6379MULTI127.0.0.1:6379SET db0:keyvalue127.0.0.1:6379SELECT1# 事务中不能用SELECT会报错# 问题4RDB/AOF备份是整个实例的# 无法单独备份某个数据库多数据库 vs 多个独立实例对比维度多数据库单实例多个独立实例资源隔离不好共享CPU和内存好各自独立权限控制无法单独设置密码可以为每个实例设置不同密码备份恢复只能全量可按实例独立备份性能隔离互相影响完全隔离运维复杂度简单一个进程稍复杂多个进程适用场景开发测试环境生产环境推荐做法用不同的键前缀来区分业务而不是用不同的数据库。# ❌ 不推荐——用数据库号区分业务SELECT0# 放用户数据SET user:1001... SELECT1# 放订单数据SET order:2001...# ✅ 推荐——用前缀区分业务全在一个库里SET user:1001... SET order:2001... SET product:3001...六、FLUSHDB vs FLUSHALL擦除数据的两种姿势这两个命令负责清空数据库区别很直观命令作用影响范围危险程度FLUSHDB清空当前数据库单个db⚠⚠ 高FLUSHALL清空所有数据库全部16个db⛔ 极高# 当前在0号库127.0.0.1:6379DBSIZE(integer)1000# 只看当前库大小127.0.0.1:6379DBSIZE(integer)1000# FLUSHDB只清0号库127.0.0.1:6379FLUSHDB OK# MOVE把key搬到另一个库127.0.0.1:6379SET key1helloOK127.0.0.1:6379MOVE key15(integer)1127.0.0.1:6379GET key1(nil)# 搬走了127.0.0.1:6379SELECT5OK127.0.0.1:6379[5]GET key1hello# 出现在5号库了Redis 4.0提供了异步的FLUSH# 异步清空——不阻塞主线程127.0.0.1:6379FLUSHDB ASYNC OK# 适用于大数据库的清空避免阻塞踩坑提示FLUSHALL是所有Redis命令里最危险的那个——它没有确认提示执行即生效。建议在生产环境使用CONFIG SET rename-command FLUSHALL 来彻底禁用。七、SCAN命令优雅地遍历键空间如果你用KEYS *来查看所有key那恭喜你你已经在用Redis最危险的操作之一了。KEYS命令会一次性遍历整个键空间并返回所有匹配结果在百万级别的key面前Redis会直接卡死。SCAN是KEYS的优雅版它用游标cursor分批返回结果SCAN游标算法示意 键空间哈希表 (size8, 索引0-7) ---------------------------------------- | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ---------------------------------------- ^ ^ | | 第1次SCAN 0 COUNT 2 第2次SCAN 4 COUNT 2 游标返回: 4 游标返回: 0 (遍历完成) SCAN的工作方式 1. 每次从当前游标位置开始扫描若干哈希桶 2. 返回新游标下次扫描的起点 3. 当游标返回0时表示遍历完成 4. 在扫描过程中新增/删除的key可能被遗漏或重复 SCAN不保证完整一致性这是设计上的取舍来看实际使用# ❌ 危险操作127.0.0.1:6379KEYS user:*# 如果有100万个user:*的keyRedis直接卡住数秒# ✅ 安全操作127.0.0.1:6379SCAN0MATCH user:* COUNT1001)8192# 下一次游标2)1)user:10012)user:1003...共约100个key# 继续扫描127.0.0.1:6379SCAN8192MATCH user:* COUNT1001)163842)1)user:2005...# 最后一轮127.0.0.1:6379SCAN16384MATCH user:* COUNT1001)0# 游标为0遍历结束2)1)user:9999SCAN的三个注意事项COUNT是建议返回数量不是精确值Redis可能返回更多或更少SCAN不能保证在遍历期间不重复不遗漏弱一致性每次SCAN返回的游标可能不是递增的不要自作聪明地判断越往后越接近完成八、键命名规范不写注释也能读懂的设计一个好的键命名规范能让团队沟通成本降低80%。下面是推荐的分层命名结构推荐键命名规范 格式项目名:业务模块:标识[:子标识] 示例 ┌─────────────────────────────────────────────────────┐ │ 键名 │ 含义 │ ├─────────────────────────────────────────────────────┤ │ shop:user:1001 │ 商城-用户模块-用户1001 │ │ shop:user:1001:profile │ 商城-用户模块-1001的资料│ │ shop:order:20260126001 │ 商城-订单-20260126-001 │ │ shop:product:stock:5678 │ 商城-商品-库存-商品5678 │ │ shop:session:token:abc123 │ 商城-会话-token-abc123 │ │ shop:cache:hot:product_list │ 商城-缓存-热点-商品列表 │ └─────────────────────────────────────────────────────┘ 层次项目名 → 业务模块 → 业务对象 → 具体标识命名规范的八大原则原则说明好例子坏例子1. 用冒号分层清晰表达层级关系shop:user:1001shop_user_10012. 项目名前缀区分不同项目的数据crm:contact:123contact:1233. 避免太短见名知意user:session:tokenu:s:t4. 避免太长不要太啰嗦order:20260126order:created:on:2026:01:26:at:friday5. 统一风格全小写或camelCase二选一userProfile或user_profile混用6. 有意义不要用无意义缩写notification:emailntf:em7. 可分组匹配方便SCAN批量操作user:active:*u_*_active8. 包含版本号数据结构变时方便迁移user:v2:1001user:1001无法区分版本小结Redis的数据库和我们平时理解的数据库差别很大它更像是键空间的分组工具。核心要点16个数据库是16个独立的redisDb结构体每个都有自己的dict键空间键空间的本质是dict——所以key查找是O(1)这也是Redis快的根基读写键时有大量维护操作过期检查、LRU/LFU更新、脏键标记、事件通知等生产环境不建议使用多数据库——用键前缀区分业务更灵活SCAN代替KEYS——游标渐进式遍历保护Redis不会因为遍历操作而卡死键命名规范化——项目:模块:对象:标识让key自己说话下一篇我们将深入探讨Redis的过期键删除机制——惰性删除、定期删除到底是怎么配合的过期键的三种命运和内存回收策略一网打尽上一篇【第24篇】Redis内存优化完全指南——从对象系统到encoding调优下一篇【第26篇】Redis过期键机制——TTL的生死时钟是怎么走的

相关新闻