ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Redis核心数据类型详解:从底层结构到应用场景与避坑指南

Redis核心数据类型详解:从底层结构到应用场景与避坑指南 1. 先搞清楚Redis到底是缓存还是数据结构服务器很多人第一次接触Redis都是因为在项目里拿它做缓存——存个用户Session、存个接口返回值、存个验证码用起来比数据库快得多于是脑子里就形成了一个固有印象Redis就是个性能很好的键值缓存库。这个印象对了一半。Redis确实常被当作缓存使用但它真正厉害的地方是它本质上是一台数据结构服务器。它提供的不是简单的 key - value 字符串映射而是一整套经过精心设计的数据结构字符串、哈希、列表、集合、有序集合以及后来的位图、HyperLogLog、地理坐标、流等扩展类型。每种结构都有自己的存储模型、操作命令、时间复杂度特性对应着完全不同的业务场景。我见过不少同事项目里Redis用得飞起但翻来覆去只会 SET 和 GET偶尔用一下 EXPIRE。遇到稍微复杂一点的需求——比如排行榜、去重计数、延时队列、签到统计——就不知道该用什么结构要么硬生生用 String 拼JSON往里塞要么直接引入一套新的中间件。每次看到这种情况我都觉得挺可惜的Redis明明已经把轮子造好了就摆在那儿只是没人去了解这些轮子各自适合什么路。这篇内容就是围绕Redis 数据类型这个核心展开的。我会把五种基础类型的底层结构、常用命令、典型场景全部拆开讲清楚再补充几种平时用得少但非常关键的扩展类型最后聊几个真实开发中容易踩的坑。不管你是刚接触Redis的新手还是用了很久但对类型理解还停留在表面的开发者这篇内容应该都能帮你把Redis的用法重新梳理一遍。2. 五种基础类型逐个拆解从底层结构到业务场景2.1 String最基础也最容易误用的类型String 是 Redis 里最简单的数据类型key 对应一个 valuevalue 就是一个字符串。但这里的“字符串”概念比很多语言里的 String 更宽泛它可以是普通文本也可以是二进制数据。Redis 的 String value 最大能存 512MB所以理论上你可以往里塞图片、序列化对象、JSON什么都能塞。底层实现上Redis 的 String 有三种编码方式int如果 value 能被解析为整数就用整数编码存储省内存且运算快embstr小于等于 44 字节Redis 3.2 之前是 39 字节的短字符串一次性分配内存raw超过 44 字节的长字符串需要两次内存分配这里有个实用技巧如果纯粹要存数字并做自增自减操作一定要确认 value 被存储为 int 编码。你可以在命令行里通过OBJECT ENCODING key查看某个 key 的编码方式。实测下来整数编码的自增操作性能远远优于字符串编码因为 Redis 直接对整数做运算不需要做字符串转整数的类型转换。String 最典型的应用场景我列一下缓存最常见的用途把数据库查询结果、外部接口响应等序列化成 JSON 或字符串存进去设置过期时间计数器用 INCR / DECR / INCRBY 做自增自减因为 Redis 单线程执行命令所以天然是原子操作不会有并发问题分布式锁用 SETNX 配合 SETEX 实现在 Redis 2.6.12 以后可以直接用SET key value NX EX 10一条命令搞定Session 共享登录状态、购物车信息等以用户 ID 为 keySession 数据序列化后存储一个真实项目中我处理过的场景电商平台的商品库存扣减。最开始团队用数据库行锁控制库存高峰期数据库压力非常大。后来把库存搬到 Redis String 上用 DECR 命令做扣减每次扣减前先检查剩余值是否大于零配合 Lua 脚本保证原子性数据库压力瞬间就降下来了。但这里有一个坑如果扣减出现异常库存数值可能不一致需要引入补偿机制。我在后面第四章会详细讲这个问题。2.2 Hash比 String 更适合存对象别再什么都塞 JSON 了Hash 是一个 string 类型的 field 和 value 的映射表特别适合存储对象。比如一个用户对象有 name、age、email 三个字段用 Hash 来存一个 key 下面挂三个 field清晰明了。为什么比 String 好因为有字段级操作。用 String 存整个 JSON 对象你想单独更新用户邮箱得先 GET 出来反序列化改字段再序列化再 SET 回去还伴随并发覆盖的隐患。用 Hash 的话一条HSET user:1001 email newexample.com就搞定了只更新一个字段性能开销和并发风险都小一个量级。Hash 的底层编码有两种ziplist当 field 数量较少且每个 field 的 value 较短时用压缩列表连续内存块存储省内存但查询是遍历hashtable当数据量变大后升级为真正的哈希表O(1) 读写这个转换是自动的由两个参数控制hash-max-ziplist-entries默认 128和hash-max-ziplist-value默认 64 字节。如果你明确知道 Hash 里的字段数会超过这个阈值可以做一些预估和调优。Hash 的典型应用场景存储对象用户信息、商品信息、配置信息等符合业务模型购物车用户 ID 做 key商品 ID 做 field购买数量做 value配置中心应用的全部配置项放一个 Hash方便统一读取和单独修改这里分享一个实操经验。如果你用 Hash 存储用户信息key 的设计建议用user:{id}这个模式field 则直接用属性名。这样在 Redis Desktop Manager 这类可视化工具里看数据结构和排查问题都非常直观。很多新手习惯用user:{id}:name、user:{id}:age这类 String 格式把每个属性分开存这在读取和更新都频繁的场景下性能更差因为每次都要产生多个 key 的多次网络往返。2.3 List不只是列表更是队列和栈List 是一个双向链表结构可以从头部或尾部插入、弹出元素。这个结构简单但用途极其广泛。底层的 QuickList 设计很有意思它是一个双向链表但每个节点不是单个元素而是一个压缩列表ziplist这样既保留了链表两端操作的高效性又提升了内存利用率和缓存友好度。当你往 List 里推入大量数据时Redis 会自动把节点里的 ziplist 拆分或合并这个细节不需要你用专门的命令控制但理解它有助于你想明白为什么 List 在数据量巨大时性能依然稳定。List 最核心的命令别忘了这几个LPUSH / RPUSH从左侧/右侧推入元素LPOP / RPOP从左侧/右侧弹出元素LRANGE查看范围内的元素LLEN获取长度LINDEX按下标获取元素注意这是 O(N) 操作不要频繁使用尤其是列表很长的时候List 的使用场景我总结得最到位的是三个消息队列简单版用 LPUSH 生产消息BRPOP 消费消息。BRPOP 是阻塞版本队列里没有消息时就阻塞等待避免轮询浪费 CPU。但这只能算一个基础的消息队列没有 ACK 机制、没有消息去重、没有消费者组复杂场景还是要上专业的消息中间件。你用 Redis List 做消息队列就要知道自己用到了一个简化版别指望它解决所有问题时间线/Feed 流发布的内容按时间顺序 LPUSH 到列表用户拉取时用 LRANGE 获取最新的若干条类似微博时间线的核心逻辑最新消息记录比如文章最新评论评论 ID 依次推入 List用 LRANGE 取前 N 条就够了不再需要去数据库做 order by 分页查询我做过一个投票活动就是用 List 记录用户的参与顺序然后按顺序生成奖励发放列表。这里提醒一句List 里的元素是允许重复的它适合存储事件记录、操作日志这类天然带有重复性质的数据。如果你需要去重该用Set而不是List。2.4 Set无序集合去重和关系运算就是它的主场Set 是一个无序的字符串集合跟数学里的集合概念一模一样元素唯一、元素间没有顺序。底层数据结构是哈希表或整数集合当所有元素都是整数时Redis 会采用 intset 编码内存占用极小。Set 的看家本领是集合运算交集SINTER、并集SUNION、差集SDIFF。这三个操作直接对应了很多复杂的业务逻辑。典型的场景去重比如统计一个活动页面的独立访客 IPSADD 两个小时SCARD 一下就知道有多少独立 IP 了共同好友/共同关注两个人的关注列表分别存两个 SetSINTER 一下就得到共同关注抽奖把所有参与用户加入 SetSRANDMEMBER 随机取几个天然保证不重复标签系统一篇文章打上多个标签用 Set 存储然后通过标签做文章聚合Set 的运算在 Redis 服务端完成直接把结果返回给客户端。这比把两个列表拉到内存里做循环比较高效得多也少写很多代码。但这里有个细节我得提醒你如果两个 Set 都很大交集运算会阻塞 Redis 服务因为 Redis 是单线程的一个长时间运行的 SINTERSTORE 会让其他命令全部排队等在那里。所以海量集合的交并差运算最好在低峰期执行或者用SINTERCARD这种直接返回基数而不返回元素本身的命令做初步判断从 Redis 7.0 开始支持。还有一种很巧妙的应用——Set 实现关注关系的存储。比如用follow:{userId}存储这个人关注的所有人 ID用fans:{userId}存储这个人的所有粉丝 ID。当 A 关注 B 时执行 SADD follow:A B 和 SADD fans:B A。要查两个人是否有互相关注直接 SISMEMBER 就行。这种设计在社交类项目里非常常见。2.5 ZSet排行榜功能的唯一正解ZSet 有序集合是在 Set 的基础上为每个元素附加了一个 double 类型的分数。元素按分数从小到大排列分数相同时再按元素字典序排列。底层是跳跃表 哈希表的组合结构。跳跃表这个数据结构值得多说两句。它在很多教科书里都有但实际在生产系统中用在核心链路上的并不多。Redis 选择它而不是平衡树比如红黑树主要是因为跳跃表的实现更简单——一个节点有几个层级指针插入和删除只需要更新相邻节点不像平衡树那样为了保持平衡要做旋转操作。而查询复杂度和红黑树一样都是 O(log N)。用空间换来了实现上的极致简洁和安全可靠这是工程设计里一个非常经典的取舍。ZSet 的核心命令ZADD添加或更新元素及分数ZRANGE / ZREVRANGE按分数从小到大/从大到小取元素ZRANK / ZREVRANK获取某个元素的排名ZINCRBY给某个元素的分数加上增量ZSCORE获取某个元素的分数ZRANGEBYSCORE按分数区间取元素ZSet 的典型应用直接报菜名排行榜游戏积分榜、销售额榜、文章热度榜。分数可以是一个固定值也可以用 ZINCRBY 做动态加减——比如用户每访问一次热度值就加一延时任务调度以执行时间戳作为分数用一个线程轮询 ZRANGEBYSCORE 取出到期的任务去执行。这个玩法比用 List 做消息队列更优雅因为天然支持延时到某个时刻执行滑动窗口限流以时间戳作为分数同一秒内的事件作为 member 存进 ZSet通过 ZREMRANGEBYSCORE 删除窗口之外的数据用 ZCARD 统计窗口内请求量我参与过一个课程平台项目首页热门课程榜就是用 ZSet 做的。分数 课程销售额 0.6 完课率 0.3 好评数 0.1 加权计算。每天凌晨用定时任务重新计算一遍分数更新到 ZSet 里前端取榜单时直接 ZREVRANGE 拿前 20 名整个链路非常短。如果这榜单在之前是用 MySQL 的 ORDER BY 多字段排序做每次查询都要全表扫描排序在高并发场景下早就扛不住了。排行榜还有一个容易忽略的细节同分的排序问题。ZSet 在分数一致时会按 member 的字典序排列如果你希望同分时按先到先得的顺序排可以构造一个复合分数——比如把最终分数算成总分 1000000 参与时间的某种补码让分数比较的同时兼顾时间因素。这个技巧我在实际项目里用过好几次效果不错但要注意分数是 double 类型精度有限超大数据量时需要小心。3. 扩展类型Bitmap、HyperLogLog、Geo、Stream3.1 Bitmap用一个 bit 记录一个状态Bitmap 本质上就是 String 类型只不过把它当位数组来用。一个 bit 只有 0 和 1 两种取值恰恰适合表示是否类型的二值状态。命令包括 SETBIT、GETBIT、BITCOUNT、BITOP位图之间的与、或、异或运算。最常见的应用就是签到功能。用户一年的签到记录如果用数据库行存储要 365 条数据查询汇总都要走索引。用 Bitmap每个用户只需要一个 key365 个 bit 总共不到 50 字节。用户第 100 天签到了就执行SETBIT sign:user:1001 99 1。查累计签到天数直接BITCOUNT sign:user:1001。查某天是否签到就GETBIT sign:user:1001 99。还有在线状态统计。所有用户 ID 映射到 bit 位上在线就 SETBIT 置 1离线置 0。想知道当前在线人数BITCOUNT 一下就行。想知道哪些人同时在线BITOP AND 运算就搞定。这里有一个映射技巧用户的 UID 往往不是从 1 开始的连续数字直接拿 UID 做 bit 位偏移会浪费大量空间。合理做法是给每个用户分配一个自增的序号维护一张 UID 和序号之间的映射表。也不要使用过于稀疏的大 offset 偏移量。3.2 HyperLogLog用极小的内存做海量去重计数HyperLogLog 是一种概率数据结构用于统计基数不重复元素个数。标准误率为 0.81%在 Redis 中每个 HyperLogLog key 最多占用 12KB 内存却能统计 2^64 个元素的去重数。听起来像一个矛盾的东西但实际上它的原理是通过哈希函数把元素分布到桶里通过观察每个桶里前导零的最大长度来估算基数数学上具备严谨的理论支撑。使用场景最典型的是UV 统计。每个用户访问页面时执行PFADD page:uv:20240101 user:1001然后PFCOUNT page:uv:20240101就得到当前的独立访客数。相比 Set 存用户 ID内存占用少了几个数量级。如果存 Set 的话一亿个用户 ID 可能需要几个 GB 内存而 HyperLogLog 只需要 12KB。当然代价是不精确但统计 UV 这种场景0.81% 的误差完全可以在业务上接受。还有一个细节PFADD 是累加式的如果要统计每天、每周、每月的独立访客可以同时维护日 UV 和月 UV 两个 key。有人问月 UV 能不能把 30 天的 HyperLogLog key 直接 PFCOUNT 合并答案是 PFCOUNT 支持传入多个 key 合并计算但十万火急——多个 key 的合并计算在 Redis 内部是逐个 key 独立读取然后做联合近似估算性能比单 key 差得多且多个 key 的内存总量会增大还是要谨慎设计好在每个周期内单独维护对应的 key。这里不展开实践的时候提前规划好存储结构就行。3.3 Geo地理位置检索开箱即用Redis 自 3.2 版本起提供了地理位置相关命令GEOADD、GEOPOS、GEODIST、GEORADIUS 等。底层基于 ZSet 实现使用 geohash 算法将经纬度编码为分数。最常见的应用附近的人用户上报位置时 GEOADD 到集合搜索附近的人就是 GEORADIUS 按距离范围查询门店推荐用户打开 App根据当前定位返回最近的门店。可以限定返回数量、距离排序摇一摇/打卡范围校验判断用户当前位置是否在某个区域范围内GEO 命令使用上有几个注意点经纬度是 double 类型经度范围 -180 到 180纬度范围 -85.05112878 到 85.05112878超出这个纬度范围会报错因为 geohash 算法在这个范围外无法编码GEOADD 的时候key 里的成员member名不要带空格否则会被解析成多个参数从 Redis 6.2 开始GEORADIUS 被标记为废弃推荐用GEOSEARCH和GEOSEARCHSTORE参数更清晰支持多条件组合筛选3.4 Stream真正的消息队列能力Stream 是 Redis 5.0 引入的类型是 Redis 官方为了消息队列场景专门设计的数据结构。它支持消息持久化、消费者组、ACK 确认机制解决了之前用 List 做消息队列的所有短板。Stream 的核心概念有五个消息以键值对形式存储的内容自带一个递增 ID消费者组一个 Stream 可以被多个消费者组消费各消费者组之间互不影响游标每个消费者组维护一个游标 LAST-DELIVERED-ID记录消费进度待确认列表消费者读取了消息但还没 ACK 的话消息会存在于 PELPending Entries List里ACK 机制消费者处理完消息后发送 XACK消息才会从 PEL 移除使用流程大致是生产者用 XADD 向 Stream 追加消息消费者组用 XGROUP CREATE 创建组消费者用 XREADGROUP GROUP 读取分配给自己的消息处理完成后发送 XACK 确认Stream 相比专业消息中间件如 RabbitMQ、Kafka优势在于不引入额外组件Redis 集群里直接能用适合中小规模的异步处理场景。但它没有复杂路由规则、没有消息延迟队列、没有死信队列也别指望在超大吞吐量和严格不丢消息的场景下依赖它。对于中小业务来说够用、简单、快就是最大的优势。4. 实操中的关键取舍和坑踩过的都懂4.1 序列化方式的选择比你想的更重要Redis 存 String 类型时value 可以直接写文本但如果你想存一个对象就得先序列化成 JSON、MessagePack、Protobuf 等格式。这里有个经常被忽略的问题不同客户端库的默认序列化方式可能完全不同。以 Java 的 Jedis 和 Spring Data Redis 为例。Spring Data Redis 默认用的是 JDK 序列化会把对象序列化成一长串以\xAC\xED开头的二进制字节流肉眼完全不可读。你在 Redis Desktop Manager 里看到自己的 key 存了一堆乱码一脸蒙圈大概率就是这个问题。我建议所有非临时性的 key 都统一配置为 JSON 序列化一是可读性极强排查问题方便二是 JSON 可以跨语言通用不会有 Java 类版本升级导致反序列化失败的问题。如果你用了 JSON 序列化注意对象结构变更时的兼容性处理。比如原来有一个字段叫userName后来改成了nickName旧数据反序列化时就会字段缺失。我的做法是线上运行中已存在的 key不再直接修改字段名而是新增字段并兼容读取。或者设计一个版本号字段靠版本号做数据迁移。4.2 过期策略不是设置了就不会出问题Redis 的过期策略有三个层次的机制被动过期当访问某个设置了过期时间的 key 时检查是否过期过期则删除。这是惰性删除主动过期Redis 定时任务会随机抽查一批设置了过期时间的 key删除其中过期的一部分这两个机制设计得再精妙也会有吞吐不掉的过期 key 残留。真正可靠的过期语义是从业务读取角度理解过期而不是依赖物理删除的时机。如果你的业务对 key 过期时机的精确性要求很高比如抢购活动里的限时资格判断靠 Redis 的 EXPIRE 可能不够。更稳的做法是双保险Redis 里设置过期时间业务层在写入时也记录一个数据库过期时间字段读取时双重判断。还有一个大坑过期大 Key 的删除会造成延迟变长。如果某个 key 是一个几百万 field 的 Hash它过期的时候 Redis 会一次性把它从内存里释放掉这个释放操作是阻塞性的可能让 Redis 暂停服务几百毫秒甚至更久。高并发下这会导致连锁的超时和雪崩。解决办法有两个方向一是打散大 key把一个大 Hash 拆成多个小 Hash二是主动删除——不要完全依赖 EXPIRE而是写一个定时任务在低峰期用 SCAN HDEL 慢慢删。4.3 分布式锁的边界条件和正确写法用 Redis 做分布式锁最经典的写法是SET lock:order:1001 uuid_value NX EX 30执行成功表示加锁成功执行失败说明锁被占。释放锁时需要先比较 value 是否是自己当初设置的那个值防止误释放别人的锁if (GET lock:order:1001 uuid_value) then DEL lock:order:1001这个先比较再删除的操作必须用 Lua 脚本保证原子性否则判断通过后、DEL 执行前锁过期了其他线程拿到了锁DEL 就会把别人的锁删掉。这个我写成标准 Luaif redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这是分布式锁的最基础形态。真实场景中还有几个边界问题锁的过期时间设置多少合适设太短业务逻辑还没执行完锁就过期了并发问题又出现设太长持有锁的线程宕机后锁要很久才能被释放。我看过一些项目直接把过期时间设成 30 秒但业务在最慢的情况下要跑 40 秒那这条锁在白给。我建议先预估业务的最大耗时再乘以 3 到 5 作为过期时间同时配合看门狗机制比如 Redisson 的 watchdog自动续期。如果你用的客户端不支持续期就得在业务代码里手动维护一个定时续期任务Redis 主从切换场景下锁会失效吗会。客户端 A 在主节点上获取到锁此时主节点挂掉从节点顶上但锁的数据还没同步过去B 也能获取到锁。这个问题是 AP 型分布式系统的固有限制。要彻底解决需要引入 Redlock 算法向多个独立 Redis 节点同时申请锁超过半数成功才算加锁成功但 Redlock 在实际工程中争议很大实现复杂度也不小。我的建议是大多数内部系统对分布式锁的要求没有那么极端单实例 Redis 加 Lua 脚本和合理过期时间已经够用如果业务真的无法接受双锁风险那就考虑引入 ZooKeeper 或 etcd 这类 CP 系统做强一致锁。这才叫对技术方案有正确的认知边界。可重入问题同一个线程在持有锁期间又尝试获取同一把锁默认实现会阻塞死等如果是不可重入的锁实现需要你在业务层面避免嵌套加锁。4.4 大 Key 和热 Key性能杀手这两个问题是 Redis 生产环境里最常见的性能杀手而且很多是相互关联的。大 Key指的是单个 key 存储的数据量过大比如一个 Hash 有数百万字段、一个 String 存了几十 MB 数据。它会带来几个很实际的问题网络传输开销大单次操作耗时明显变长删除、迁移时阻塞 Redis引发雪崩内存碎片增多内存利用率下降AOF 持久化和主从同步时开销增大因为一个命令就要传输这么多数据如何发现大 Key查询时用--bigkeys参数可以扫描出各类 key 中最大的几个。这个命令执行时也有一点扫描开销我建议在低峰期执行。生产环境还可以用 Redis 自带的内存分析工具结合监控平台来发现。热 Key指的是被大量请求集中访问的 key比如某个明星的粉丝数在发新歌当天被反复读取。这个 key 会瞬间成为 Redis 的单点瓶颈哪怕 Redis 本身性能很好单个 key 的命令处理也受限于单线程。常用解法包括热 Key 数据加本地缓存如 Caffeine降低 Redis 请求量读多写少的场景做多级缓存CDN - Nginx - Redis - DB打散热 Key把一个热点 key 拆成 N 个带后缀的副本 key随机读取。注意写时要同步写所有副本读时有短暂不一致升级到 Redis Cluster 时让热 Key 自然分布到不同节点不过在 Cluster 里单个 key 仍然只存在于一个节点热 key 问题依然存在彻底解决还是靠本地缓存和打散4.5 批量操作的坑MGET、Pipeline、Lua各有各的适用面批量操作在 Redis 里有很多种实现方式性能差异很大选择错了容易误伤服务。先说最简单的 MGET / MSET。它们是一次性获取/设置多个 key 的值适用于无依赖关系的键集合。比如你有 100 个用户的昵称需要一次性渲染到页面发一条 MGET 拿 100 个值远优于发 100 个 GET。但 MGET 一次拿太多 key 也有问题命令本身体积大网络传输长Redis 处理时间变长。同时 MGET 的结果如果有很多空值要注意逐 key 的空值处理逻辑别让 null 把业务逻辑搞崩。Pipeline 是客户端侧的批量发送模式。它把多条命令打包成一次网络请求发出Redis 依次执行后批量返回结果。注意 Pipeline 里的命令不是原子执行的只是减少了网络往返次数如果中间某条命令出错其他命令照常执行。Lua 脚本则完全不同。Redis 执行 Lua 脚本时是原子性的脚本里的所有命令要么全部生效、要么一条都不生效。所以它适合做多步骤且需要原子性的操作比如前面提到的分布式锁释放、库存扣减同时检查限购条件等。但注意 Lua 脚本的执行时间不要太长因为脚本执行期间会阻塞整个 Redis 实例其他请求都得排队。最佳实践是脚本只包含必要的几下 Redis 操作绝不在脚本里做复杂计算或遍历大量数据。5. 数据类型选型速查帮你 10 秒拍板我把每种类型的核心特征、使用场景、优缺点整理成一份速查表遇到需求先对号入座基本不会选错。类型数据模型底层核心结构典型场景关键优势注意限制Stringkey - 字符串/整数/二进制int、embstr、raw缓存、计数器、令牌简单通用操作最丰富无复杂结构对象容易序列化膨胀Hashkey - field - valueziplist / hashtable对象存储、购物车、配置字段可单独修改灵活大 field 消耗内存需注意打散List双向链表quicklist队列、时间线、最新记录两端操作 O(1)天然有序不支持去重随机访问慢Set无序唯一集合intset / hashtable去重、交集运算、抽奖数学集合运算服务端完成无顺序无法排位ZSet分数有序集合skiplist hashtable排行榜、延时队列、限流查询范围和排位高效写入时需维护跳表性能略低Bitmap位数组String 的位视图签到、在线状态、二值标记极省内存映射策略要预先设计HyperLogLog概率去重计数12KB 固定内存UV 统计内存开支极低不精确约 0.81% 误差Geo经纬度点位集合ZSet geohash附近的人、门店推荐开箱即用的位置检索纬度范围受限Stream消息队列radix tree listpack异步解耦、事件流支持消费者组和 ACK规模和能力有限这张表建议你截图存在手机里或者贴在公司内部知识库里以后写方案做技术选型的时候拿出来对照一下。我个人的经验是选型前先回答三个问题——数据有没有唯一性要求需不需要排序是读多还是写多这三个问题的答案基本能帮你锁死两三个候选类型再结合数据量和并发量做最终决定。别一上来就想用 ZSet 做所有事ZSet 跳表的维护成本比 Set 高不需要排序就老老实实用 Set。还有一个内部实用技巧如果你不确定哪种类型在特定业务场景下性能好可以在本地建一个 Redis 实例用redis-benchmark对候选类型做对拍测试。这个工具支持自定义命令序列比如redis-benchmark -t set,get可以测 String 的读写测 ZSet 写入可以写-t zadd。虽然不能完全复刻生产流量的模式但至少能看出数量级差异。用数据说话永远比拍脑袋靠谱。6. 客户端工具选型命令行、桌面管理、Spring 集成6.1 选一个好用的可视化工具Redis 官方其实没有出桌面客户端但社区里有很多优秀的工具。我用过不少从最原始的 redis-cli 到现在各种 GUI简单分享一下感受。如果你是纯命令行流那 redis-cli 必须熟练配合--csv、--scan、--bigkeys这些参数很多东西不用打开 GUI 就能搞定。但如果你要频繁查看数据、构造测试数据、观察 key 分布一个可视化工具能提升不少效率。同类工具里口碑好的有Another Redis Desktop Manager开源免费多平台支持界面干净功能层面支持命令执行、数据浏览、命令行模式。日常使用完全够我认为它比不少商用工具体验都要好Redis Desktop Manager老牌工具但新版本已经开始限制免费功能了需要付费订阅才能完整使用个人项目可以先用免费的旧版Redis InsightRedis 官方出的工具功能最强支持内存分析、慢日志查看、profiling、命令历史等高级功能。适合在生产环境排查问题时用但界面相对重一些命令行工具还有很多人用redis-clijq配合终端画图来监控但这个要自己配脚本维护成本略高我个人现在的主力是 Another Redis Desktop Manager轻量排查线上性能问题时会开 Redis Insight因为它有比较完整的指标和分析视图。工具这事儿没什么绝对的最好顺手、够用就行。6.2 Spring Data Redis 的常见配置问题Java 系项目里大多数人用的都是 Spring Data Redis这个框架封装了连接池、模板方法、序列化等一堆东西开箱即用但几个默认配置我建议一定要做调整。第一序列化器必须显式配置。默认的 JdkSerializationRedisSerializer 前面已经说过生成的数据在 Redis 里完全不可读。推荐配置Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); StringRedisSerializer stringSerializer new StringRedisSerializer(); GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(stringSerializer); template.setValueSerializer(jsonSerializer); template.setHashKeySerializer(stringSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; }Key 用 String 序列化value 用 JSON 序列化。这样在管理工具里看到的 key 和 value 都直观可读。第二连接池的参数调优。Spring Boot 2.x 默认用 Lettuce连接池默认不开启。如果你在配置文件里不显式打开spring.redis.lettuce.pool.enabledtrue每次操作都会新建连接并发一高直接就出性能问题。同时空闲连接回收时间调到 60 秒以上避免频繁建连断开。第三RedisTemplate 的泛型注意。RedisTemplateString, String和RedisTemplateString, Object是两个不同的实例JSON 序列化时记得配置类型信息在 JSON 里带上 class 字段否则反序列化时会直接报类型转换异常。这个坑我踩了不止一次。6.3 macOS / Windows / Linux 本地安装速记网上关于 Redis 安装的教程非常多搜索词热度也高这里给出我实测过的几种方式的最简步骤。macOS 上有两种主流方式Homebrew 一键安装brew install redis装完执行brew services start redis就会作为后台服务常驻编译安装去 Redis 官网下载源码包make make install适合需要指定版本和参数优化的场景Windows 下注意Redis 官方一直没有正式支持 Windows。以前大家用微软维护的分支版本老版本 3.x现在已经不再维护。现在更推荐两条路线WSL2 里装 Linux 版 Redis体验跟原生 Linux 一致最接近生产环境Docker 容器运行docker run -d --name redis -p 6379:6379 redis:7-alpine几秒搞定不想用了直接删容器干净利落Linux 上CentOS / Ubuntu 系最稳的方式是官方源码编译或者通过系统包管理器安装Ubuntu 的apt install redis-serverCentOS 的yum install redis。但包管理器里的版本可能不是最新生产环境我一般是从官网下载稳定版源码编译或直接用 Docker 镜像部署。另外无论什么平台装完第一件事我建议修改配置设置密码修改requirepass修改默认端口避免被人扫描爆破关闭或者限制一些高危命令比如FLUSHALL、KEYS开启持久化至少开启 AOF根据业务需求配置策略这些操作在后面的运维章节我会细讲。7. 运维和监控Redis 不是装好就跑还要照顾好7.1 持久化选 AOF 还是 RDBRedis 默认只开启 RDB快照持久化它会定期把整个内存中的数据保存到磁盘文件里。RDB 的优点是恢复速度快、备份文件紧凑缺点是可能会丢最近几分钟的数据取决于快照策略。默认配置是save 900 1、save 300 10、save 60 10000这种条件触发。AOFAppend Only File则是把每一条写命令追加到文件末尾故障最多丢 1 秒的数据取决于刷盘策略。AOF 文件会随写入增长需要定期 rewrite 压缩但 Redis 的 rewrite 是自动的也不影响读写。我的建议比较明确重要数据场景RDB 和 AOF 同时开启。RDB 用于快速恢复冷启动AOF 用于保障数据不丢。如果你只是拿 Redis 做纯缓存、丢了可以从数据库重新拉可以不开启持久化省下磁盘写入带来的开销。很多人忽略了一个运维细节AOF rewrite 和大 Key 删除都会产生大量磁盘写放大。在机器磁盘 I/O 能力有限的环境下这两个操作同时发生时可能造成 Redis 卡死fork 子进程 大量内存页复制。所以生产环境要有磁盘 I/O 的监控提前做好容量评估。7.2 内存淘汰策略别让 Redis 变成 OOM 炸弹Redis 默认内存上限是物理内存大小如果不设置maxmemory极端情况下会因为内存不足被操作系统杀掉OOM Killer。更常见的场景是内存爬到接近上限后写操作全部失败——你存了用户缓存结果缓存写不进去业务直接报错。生产环境必须显式配置maxmemory并且选择合适的淘汰策略。Redis 支持的策略有这么几档noeviction内存满了还继续写直接报错默认策略不推荐allkeys-lru从所有 key 里按最近最少使用淘汰最常用volatile-lru只从设置了过期时间的 key 里淘汰最少使用的allkeys-lfu按访问频率最低的淘汰如果业务有明显热点数据LFU 效果更好volatile-ttl从设置了过期时间的 key 里挑最快过期的优先删除我建议日常缓存场景直接使用allkeys-lru因为缓存本来就是允许牺牲部分数据来保命。如果缓存对象是系统核心数据宁可拒绝写也不能丢比如库存那就只能用 noeviction 并且要把maxmemory设置得足够宽裕。注意淘汰策略只解决了内存满了之后的行为不代表你可以无限制往里面写——还是要给 Redis 足够的物理内存加上合理的监控告警。7.3 监控指标至少要盯住这几个Redis 的INFO命令可以看到全部指标。生产环境我建议至少监控以下内容used_memory和used_memory_rss内存使用情况后者是实际分配给操作系统的物理内存包含了碎片mem_fragmentation_ratio内存碎片率长期高于 1.5 需要考虑重启或执行MEMORY PURGEconnected_clients连接数异常飙高可能是连接池泄漏instantaneous_ops_per_sec每秒操作数作为整体压力的参考rejected_connections因为 maxclients 限制而被拒绝的连接数keyspace_hits和keyspace_misses缓存命中率。命中率突然大幅下降说明热点数据失效或者 key 设计有变化如果你用的是云厂商的托管 Redis控制台一般都有现成的监控面板不用自己搭。如果是自建Prometheus redis_exporter Grafana 一条龙给你安排好面板网上有大把现成的。7.4 主从复制和哨兵别等到挂了才着急Redis 的高可用方案最常见的是主从复制 哨兵Sentinel。主节点负责写从节点负责读哨兵负责监控主节点的健康状态并在主节点宕机时自动把某个从节点提升为新的主节点。但这套体系有几个注意点复制是异步的主节点写成功了从节点可能还没收到这条数据主挂掉后这块数据就丢了哨兵本身也要高可用至少部署 3 个实例分布在不同机器上主从切换的感知时间大约在几秒到十几秒如果业务在这段时间内不能停机就得提前做好降级方案从节点默认只能读不能写如果你想临时往从节点写数据排查问题切换回主从状态时可能会引发复制冲突如果是新的项目我更建议直接上 Redis Cluster 而不是主从 哨兵。Cluster 天然支持数据分片16384 个槽位、主从自动切换从节点升级、在线扩容缩容。哨兵方案适合单机性能足够、只是需要高可用的场景。这里有个典型误区很多人以为 Redis Cluster 只解决数据量大、单机装不下的问题就一个劲儿把数据往 Cluster 里塞结果分片不均导致某个节点过热反而更难管。架构选型永远要先想清楚自己的业务规模和可用性要求不要盲目跟着热门方案跑。8. 最后分享一点心得Redis 这个系统表面看着简单五条命令就能入门但真正把它的数据结构用对、用好、用出性能是需要时间和踩坑积累的。我今天把五种基础类型和四种扩展类型都梳理了一遍也把选型逻辑、序列化、大 Key、热 Key、持久化这些日常运维中最常遇到的问题都讲到了。这些内容不是从官方文档抄来的每一块都是我在实际项目中踩过、调过、重建过之后总结出来的。你如果全部吃透日常开发里遇到 90% 的 Redis 相关问题应该都能找到明确的方向。我个人的体会是学 Redis 的正确姿势不是背命令而是理解每种数据结构背后的设计意图和适用边界。String 为什么适合计数因为 int 编码和单线程原子性。ZSet 为什么适合排行榜因为跳表天生支持范围查询和排位。HyperLogLog 为什么能省内存因为它用了概率估算而不是精确存储。当你把这些为什么想清楚了遇到新需求时就能自然地想到该用哪个类型根本不需要去翻命令手册。再分享一个小技巧动手之前先在本地把每种类型的 CRUD 命令都敲一遍用OBJECT ENCODING观察不同数据量下编码方式的变化用INFO观察内存的增量用redis-benchmark感受不同操作的耗时差异。这种亲手体验得来的直觉比看多少篇博客都有用。Redis 是一个能伴你整个职业生涯的数据库花点时间打好底子非常值得。
返回列表