Redis知识点这个标题听起来很普通但这东西恰恰是后端开发里最容易被轻视、又最容易在面试和线上事故中翻车的一块。这篇文章我打算换一种写法不按官方文档的顺序平铺而是从一个后端老鸟实际怎么用Redis的角度把从安装到上线、从基础类型到持久化选型、从单线程原理到真实项目里的坑一条线串下来。文章会覆盖Redis的全部核心知识点包括环境搭建、五大数据类型以及Bitmap等扩展结构的适用场景、RDB和AOF的取舍逻辑、单线程高性能的底层原因、以及缓存穿透、big key、慢查询这些线上问题的定位思路。适合正在系统学习Redis的初学者也适合那些已经用过Redis但还没想明白为什么的开发者对照查缺补漏。1. 先把Redis装起来源码编译、Docker与配置文件里的关键默认值Redis的安装大概是整个学习链条里最简单、也最容易被随手糊弄过去的一环。很多教程直接让你apt install redis-server或者docker run redis然后就开始讲命令了。但我建议你至少在本地亲手编译一次因为编译过程能让你直观看到Redis对运行时依赖的态度也能让你对后续的配置排查有点体感。1.1 源码编译安装理解Redis的依赖哲学去 redis.io 下载最新的稳定版源码包这个版本号建议留意一下奇数版本通常是大版本之间的过渡版生产环境优先选偶数版本。下载后按这套流程走tar xzf redis-7.0.12.tar.gz cd redis-7.0.12 make -j4 make install编译过程本身几乎不会报错因为Redis对构建依赖的要求极低——只要有gcc和make就够了它不像MySQL那样需要一堆cmake、boost之类的依赖。这背后其实是Redis的设计哲学尽量零外部依赖方便在任何Unix-like系统上快速跑起来。make install会把redis-server和redis-cli装到/usr/local/bin然后你就可以用redis-server启动裸服务用redis-cli ping验证连通返回PONG就算通了。如果你不想污染系统环境make完之后也可以直接在源码目录里用src/redis-server启动效果一样。这种可移植性优先的思路是Redis能遍地开花的基础理解这一点对你之后在容器环境里部署会有帮助——它天然就适合被塞进轻量镜像。1.2 Docker部署注意持久化挂载和网络模式使用Docker是现代部署的主流方式但要记住一个关键点不要在容器里裸跑Redis而不挂载持久化文件。一个完整的docker-compose配置大概是这样的version: 3 services: redis: image: redis:7-alpine container_name: redis restart: always ports: - 6379:6379 command: [redis-server, /etc/redis/redis.conf] volumes: - ./redis.conf:/etc/redis/redis.conf - redis-data:/data sysctls: - net.core.somaxconn1024 volumes: redis-data:这里/data目录对应Redis默认的持久化文件存放位置因为官方镜像的workingDir就是/data。net.core.somaxconn1024是为了绕过容器环境下TCP backlog队列过小导致的连接慢问题。如果你只是临时测试不想搞这么复杂docker run --rm -it -p 6379:6379 redis:7-alpine一行命令也能跑起来但生产环境千万不要省这一步。1.3 配置文件里最容易被忽略的默认值安装完成后sentinel和redis.conf这些文件在Redis源码目录里都有模板。我强烈建议你把模板文件复制出来从头读一遍。以下几个默认值是新手最容易忽略、也最容易在后期引发事故的daemonize yes默认是no前台启动。生产环境建议配合systemd或supervisor管理而不是简单改成yes交给后台。bind 127.0.0.1默认只监听本地回环。如果想让其他机器访问很多人会改成bind 0.0.0.0但这是极其危险的习惯等于把Redis裸奔到公网。正确做法是指定具体内网IP同时搭配requirepass开启密码保护。maxmemory默认是0表示不限制。线上必须设置否则Redis内存会被撑爆触发系统OOM Kill这不是闹着玩的。save相关配置默认有900秒1次修改、300秒10次修改、60秒10000次修改三条RDB触发规则。后面讲持久化时会详细展开。配置文件不是背一遍就能记住的我的建议是每次调一个参数都要在启动日志里观察它的实际效果。用redis-cli config get parameter能看到运行时生效值。比如你改了maxmemory用config get maxmemory立刻就能确认。2. 五种核心数据类型与扩展结构记模型更记适用场景Redis最值钱的知识点不是那些命令本身而是什么数据该用什么结构放。很多新手把Redis当万能缓存一律用String结果遇到排行榜、去重统计、附近的人这类场景就抓瞎还得去外部再搞一套系统。其实这些场景Redis原生的数据结构就能扛。2.1 String最基础但也最容易用错的类型String的底层是SDSSimple Dynamic String不是C语言裸字符串。这个设计的巧妙之处在于Redis可以提前知道字符串长度、避免缓冲区溢出并且在追加操作时通过预分配减少内存重新分配次数。所以你拿它存普通的键值对、计数器、分布式ID都是没问题的。常用的命令里SETEX和SETNX值得单独拎出来说# 带过期时间的set适合会话token SETEX user:token:abc123 3600 token_value # 只有key不存在时才设置成功适合抢锁 SETNX lock:order:1001 1String还能用来做原子计数器例如INCR article:views:1001 DECR stock:product:999这在秒杀、点赞、阅读数场景非常实用。一个容易踩的坑是不要用String存二进制大对象比如图片、序列化后的完整对象除非你有压缩和分片方案否则大key会直接拖垮Redis的响应速度。这个后面在big key部分详细说。2.2 Hash对象数据的首选而不是String序列化我见过不少团队把所有用户信息用set user:1001 {json字符串}来存这种做法的坏处是要改其中一个字段就得读出整个对象、反序列化、修改、再序列化、再写回不仅浪费带宽还容易产生并发覆盖问题。Hash的正确用法是这样HSET user:1001 name 张三 age 30 city 北京 HGET user:1001 name HINCRBY user:1001 age 1这等于在Redis里给对象建了一张字段表缓存热点用户信息、商品信息、购物车数据都极其合适。尤其在某些场景下后台需要动态给用户加字段Hash天生就支持这种演进不需要改数据结构、不需要迁移数据。2.3 List与Set队列和标签关系的经典解法List是双向链表LPUSH配合BRPOP能实现简单的消息队列LRANGE可以做分页查询。但注意不要把List当数组用如果你需要按下标随机访问List的性能就很一般。在这种场景下数据规模不大就直接在应用层处理数据规模大的话就考虑真正的索引结构。Set是去重集合它内置了SADD、SISMEMBER、SINTER、SUNION这些命令天然适合做标签系统、好友关系、共同关注这类集合运算场景。比如给用户打上喜欢数码的标签后要找出同时喜欢数码和摄影的用户一条SINTER搞定不需要拉到应用层做循环遍历。2.4 Sorted Set排行榜与延时队列的逻辑基石ZSet的每一个成员都关联一个double类型的scoreRedis按score排序。这个特性让它成了排行榜场景的标准答案ZADD leaderboard:2024 100 user_a ZADD leaderboard:2024 200 user_b ZINCRBY leaderboard:2024 50 user_a ZREVRANGE leaderboard:2024 0 9 WITHSCORESZINCRBY是热榜更新的神器点赞、热度变化直接原子累加不需要先读后写避免并发覆盖。ZSet还有个隐藏用途是延时队列用score存时间戳轮询时ZRANGEBYSCORE取出到期的任务处理简单可靠。2.5 进阶结构Bitmap、HyperLogLog与Geo这三个扩展结构能解决的问题用基础五类做通常会非常费劲Bitmap按位存储一个用户占一个bit1亿用户在线状态仅需12.5MB内存。SETBIT online 10000001 1就能标记第10000001个用户在线。连续签到功能也是经典用法。HyperLogLog用来做UV统计标准误差0.81%但内存固定12KB左右。如果你用Set存每个访问用户的ID那内存消耗会随用户量线性增长而HyperLogLog用固定内存覆盖海量去重场景。GeoGEOADD、GEORADIUS就可以实现附近的人功能底层是ZSet经纬度编码不需要单独引入地理位置服务。选型逻辑其实就一句话先确认你的数据形态是什么再去挑合适的数据结构而不是反过来拿着一两个命令硬套。3. 持久化选RDB还是AOF数据安全与性能的权衡Redis作为缓存使用时很多人对持久化抱着无所谓的态度觉得数据丢了可以从数据库重新加载。一旦Redis被当作准数据库使用——比如用它存订单状态、存分布式锁、存正在编辑的草稿——持久化策略就直接决定了你数据丢失的窗口期和恢复速度。这个问题是我面过很多人都会问的答得清楚的人不多。3.1 RDB基于快照的持久化RDB是Redis默认开启的持久化方式。它按配置的时间间隔生成一个二进制快照文件默认规则是save 900 1 save 300 10 save 60 10000意思是900秒内至少有1次写操作则触发一次快照、300秒内10次写操作触发、60秒内10000次写操作触发。RDB的好处是文件紧凑、加载速度极快非常适合做备份和灾难恢复的场景。比如凌晨3点生成一个RDB存到云存储真出事了拿来恢复。但它的短板也很明确它是定期快照不是实时记录如果服务器在900秒内崩溃写进去的数据就可能丢失。这里有个内部机制RDB生成时用的是fork子进程的方式不会阻塞主进程的服务但在数据量非常大时fork瞬间的内存开销和CPU消耗也不能完全忽略。3.2 AOF日志追加带来的更小丢失窗口AOFAppend Only File记录的是每个写操作的命令本身有点像MySQL的binlog。它的默认策略是everysec每秒同步一次。这意味着极端情况下最多丢失1秒数据比RDB精细得多。appendonly yes appendfsync everysec生产环境我一般建议开启AOF原因很简单它把数据丢失窗口压缩到了1秒内。代价是AOF文件随时间膨胀会越来越大而且相同数据量下AOF恢复速度比RDB要慢一些。不过Redis提供了BGREWRITEAOF命令和自动触发机制会对AOF里重复的命令做压缩重写比如set k 1、set k 2这样的日志会被合并成最后一条。3.3 我建议的混合持久化方案从Redis 4.0开始aof-use-rdb-preamble yes选项允许AOF文件的前半部分是RDB格式的快速恢复内容后半部分是AOF增量命令。这种方式把RDB的加载速度和AOF的低丢失窗口组合起来是目前主流的配置。我的习惯配置是appendonly yes appendfsync everysec aof-use-rdb-preamble yes同时保留RDB文件作为周期性备份。比如每6小时通过BGSAVE生成一个RDB存到异地作为更底层的数据保险。这样既有了秒级恢复能力又有方案能应对机房级灾难。注意测试环境完全可以关掉持久化以提升性能但任何线上环境都必须开启这不是可选项。4. 单线程为什么还能跑这么快IO多路复用与高效内存设计这是Redis知识体系里最常被问到的问题Redis为什么这么快很多人给出的答案是因为它是单线程的没有上下文切换开销。这个解释不算全错但远远不够。4.1 单线程的核心IO多路复用Redis是单线程处理命令意思是同一时刻只有一个命令在执行。那它如何支撑动辄10万的QPS关键在于它基于事件驱动的IO多路复用机制。它不是在等一个连接的数据读完再去读下一个而是把多个连接的读写事件注册到一个事件循环里由操作系统内核帮忙监听哪个连接有数据来了就立刻处理哪个。类比一下就好理解了你去银行办事单线程的Redis不是一个柜台只服务一个客户直到办完而是一个大堂经理同时盯着门口的所有客户谁填完单子马上接待谁。没有排队干等的情况就靠着这种不断检查谁的请求准备好了的模式单线程也能支撑海量并发。具体到实现Redis使用的是基于epollLinux的事件循环配合内部命令的事件处理器。这里要注意的是单线程指的是执行命令的核心线程而非Redis进程只有一条线程持久化时的fork子进程、过期键清理等辅助任务都是在后台另开的。4.2 单线程模型下哪些命令会害了你单线程模型最大的代价不是性能上限而是一个慢命令拖累所有请求。IO多路复用解决的是并发连接问题但解决不了单个命令执行时间过长的问题。你执行一次KEYS *如果键有几百万个主线程就会卡在匹配和遍历上期间所有其他客户端请求都要排队等着线上就直接表现为大面积超时。所以Redis对单线程的命令执行设计了一个隐含的铁律保证单条命令的时间复杂度足够低。常用命令基本都是O(1)到O(logN)比如GET、SET、HGET、LPUSH、ZADD这些。而KEYS、SMEMBERS、LRANGE全量、ZRANGE全量都是O(N)命令在大数据集下绝对不能随便用。我在实际项目里踩过一次当时一个运维同学用KEYS channel:*去清理一批测试数据几百万key直接让Redis卡了将近一分钟上游业务超时报警不断。后来改成SCAN命令配合MATCH游标迭代才把问题压下去。SCAN和KEYS的区别是它每次只返回一部分key不一次性遍历全量这样即使数据集再大也不至于阻塞主线程。这是生产环境必须掌握的命令之一。4.3 内存层面的提速秘密单线程和高性能IO还解释不了另一个层面的快——压缩列表、快速链表和跳表这些底层编码结构功不可没。Redis的每个键都有一个对象的内部编码方式比如Hash在字段数较少且值较短时会用ziplist这种紧凑内存结构比hashtable更省内存、更快当数据规模变大后自动升级为hashtable。这种按规模选编码的机制在底层极大降低了Redis的内存占用。跳表skip list是ZSet的底层实现之一它用多级索引实现了和平衡树相近的查询效率O(logN)但实现简单得多、并发环境下也更容易维护。Redis追求的不是某个数据结构在所有指标上最强而是在单线程内存数据库这个特定约束下整体最稳。这些原理在面试里会问在线上定位问题时更有价值。比如你去排查大key发现某个Hash有几十万字段内心知道它已经触发编码升级就能更好地判断内存去向。5. 实际项目中踩过的坑缓存穿透、big key与慢查询排查学Redis不能光学会往里面塞数据真正拉开水平的是处理线上问题的思路。这里把我经历过的几个高频坑点整理出来每一个都是真实事故沉淀出的教训。5.1 缓存穿透、击穿与雪崩同一个问题的三种面貌这三个概念总被混在一起其实它们是不同层面的故障。缓存穿透查询一个不存在的key缓存和数据库都没有请求直接穿透缓存砸到数据库。如果有人恶意疯狂请求一个不存在的商品ID数据库会直接被压垮。解决方法是对不存在的key也做缓存但设置较短的过期时间或者用布隆过滤器在缓存前面拦一道快速判断key是否存在。缓存击穿某个热点key的过期时间到了同时大量请求涌进来全部绕过缓存打到数据库。解决方法是热点数据不设置过期时间或者用互斥锁保证只有一个请求去重建缓存其他请求先等。缓存雪崩大量key在同一时间段集中过期导致请求全部落到数据库。解决方法是过期时间加一个随机扰动让过期时间均匀分布或者采用多级缓存架构。这三种问题单独出现都不致命但如果不提前设计策略故障来的时候会一并爆发。我建议在设计缓存层时就做好过期时间的抖动策略加随机数这个习惯成本极低、收益极高。5.2 big key隐藏在慢查询背后的元凶big key是指单个key存储的数据量过大。比如一个Hash里有几十万个字段一个List里有几百万个元素或一个String值有几十MB。这类key的危害链条是这样的读的时候网络传输消耗大占用带宽写的时候内存分配压力大可能触发卡顿删除的时候如果直接用DELRedis主线程需要同步释放大量内存造成短暂阻塞分布式场景下大key还会导致请求倾斜流量全打到同一个分片节点上。定位big key最稳妥的方法是结合redis-cli --bigkeys命令扫描它会按类型汇总出占用最大的key。清理时不要直接用DEL删除而是用UNLINK——这个命令是异步释放内存主线程不会卡住。对大Hash或大Set还可以用HSCAN、SSCAN渐进地分批删。5.3 慢查询与延迟排查的完整思路Redis本身有慢查询日志功能建议在生产环境开启slowlog-log-slower-than 10000 slowlog-max-len 128slowlog-log-slower-than的单位是微秒10000微秒即10毫秒。超过10毫秒的命令会被记入慢日志用SLOWLOG GET可以查看。配置这个一定不要嫌麻烦。排查延迟时一个被忽略的要点是客户端看到的延迟远不止Redis命令执行时间还包括网络往返、客户端连接池不足排队、GC停顿以及本地CPU争抢。我踩过的一个典型例子是服务端慢日志全空但接口RT很高查了半天最后发现是应用侧Redis连接池初始化太小高峰期请求全在等待获取连接。在Redis服务端指标一切正常的情况下务必把排查范围扩展到应用层。redis-cli --latency可以测到服务器本身处理命令的延迟客户端耗时统计可以定位是不是应用侧问题。5.4 过期键删除策略为什么会有内存明明没满但key不见了的困惑Redis的过期键删除是三种策略的组合惰性删除、定时删除和定期删除。惰性删除是指当客户端访问这个key时才去检查是否过期过期就删掉定期删除是指Redis每隔一段时间主动抽查一批过期键清除。这两种策略都不保证过期键被立刻删除。这就解释了一个常见困惑你设置了EXPIRE10秒但10秒后用MEMORY USAGE去查发现内存没降——因为键可能还留着只是逻辑上过期了。当内存压力上来时Redis还有maxmemory-policy allkeys-lru这类淘汰策略兜底但淘汰机制也会带来新的问题比如热数据被误淘汰。所以给热点key设置过期时间时要结合业务容忍度想清楚到底能不能接受这个key在某时刻突然不存在。6. 把Redis用出花来的几个场景分布式锁、轻量队列与排行榜落地前面讲的知识点最终要在场景里验证才有价值。这里挑三个最常见的非纯缓存场景把命令组合和注意事项讲透。6.1 手动实现一个可靠的分布式锁用Redis做分布式锁最简单的版本是这样的SET lock:order:1001 unique_value NX PX 30000NX表示只有key不存在时才设置PX 30000表示30秒自动过期unique_value是客户端生成的唯一标识用于释放锁时确认是自己的锁。释放锁不能直接用DEL——因为你可能持有锁超时后锁已经自动过期并被别人拿到了这时候你删除的就是别人的锁。所以要用Lua脚本实现先比对值、再删除的原子操作if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end用EVAL执行这段脚本。这套方案能应对绝大多数场景但它不是完美的——比如Redis主节点写入了锁但尚未同步到从节点就宕机了从节点顶上后锁就丢了。所以要求极高的一致性场景还得引入Redlock算法不过Redlock本身也存在争议工程上优先通过哨兵、集群的最小化故障窗口来缓解而不是盲目迷信某个锁方案。6.2 用List和Stream实现轻量消息队列典型用法是LPUSH往左边塞消息消费者用BRPOP阻塞地从右边取消息LPUSH task:queue task1 BRPOP task:queue 5BRPOP在队列为空时会阻塞等待最大5秒避免了频繁轮询对Redis的无谓压力。这种方案在业务量不大、不需要消息确认机制的场景下完全够用且零额外依赖。但如果你需要消费者组、消息确认和重试机制那就别用List硬顶了直接换Redis StreamXADD events * user_id 1001 action click XREADGROUP GROUP consumer_group consumer_name COUNT 10 STREAMS events Stream从Redis 5.0加入提供了XACK、XPENDING这些命令来做消息确认和查看未确认消息功能上天花板比我之前用List实现的那套高不少。6.3 排行榜的全链路落地很多排行榜不是单纯按一个数值排序比如综合热度 播放量×0.4 点赞量×0.3 评论量×0.3。ZSet是天然的数据结构不过要注意score是double类型如果你需要高精度计算就不要在Redis里算最终值而是用应用层算好再写入。计算热度变更的典型做法ZINCRBY daily:hot:2024:07:01 40 video_1001 ZINCRBY daily:hot:2024:07:01 30 video_1001每次有人点赞就执行一次ZINCRBY。而且ZSet在增量场景下表现极好不会因为排行数据量大而显著变慢。要取Top100就用ZREVRANGE daily:hot:2024:07:01 0 99 WITHSCORES近似O(logN M)。历史榜单可以按天存不同的key最终发展成一套带过期时间的滚动榜单系统。7. Redis 7的新特性从RESP3到Function别让版本停在原地很多团队用一个Redis版本用到底从3.x、4.x一路用上来其实Redis 7的改进非常值得升级。RESP3协议从Redis 6开始引入相比老旧的RESP2它支持客户端主动推送、服务端事件通知等能力配合CLIENT TRACKING可以实现服务端协助的缓存失效通知——客户端不必轮询服务端缓存一旦被修改就主动告诉客户端。这套机制在应用层做本地缓存时特别省事。Redis 7最大的一个升级是Redis Function。过去你要在客户端组装Lua脚本发给服务端执行管理散落各处的脚本是个噩梦。Redis Function允许你把Lua逻辑作为一等公民部署在服务端就像是数据库的存储过程FUNCTION LOAD #!lua namemylib\nredis.register_function(myfunc, function(keys, args) return redis.call(GET, keys[1]) end)版本升级不是银弹新特性引入的同时也会带来行为变化比如某些命令的O(N)语义修正。我的建议是先在测试环境完整跑一遍业务回归再考虑生产升级。8. 聊聊学Redis的路线从会用命令到能排查事故最后分享一下我对Redis学习路径的真实看法。市面上Redis相关的书和课程很多但我见过不少同行学完还是很虚——问题不是没记住命令而是没建立起几个关键心智模型。第一用命令的时间复杂度来分析一切Redis操作。拿到一条新命令先问它是O(1)还是O(N)这决定了你在生产环境敢不敢用、怎么用。比如HGETALL听起来方便但一个5万字段的Hash会让主线程卡住几百毫秒。第二永远用数据会丢失吗、能恢复多快来审视持久化和高可用配置。Redis的RDB、AOF、主从复制、哨兵、集群所有机制都是为了回答这两个问题。你把这两个问题的答案确定了配置自然就理清了。第三面试和实战都爱考的场景多练手。分布式锁、排行榜、UV统计、附近的人、限流器这五个场景几乎覆盖了Redis最核心的命令组合。哪怕你只是在自己机器上把它们各自实现一遍对Redis的掌握程度都会上一个台阶。尤其限流器用INCR配合EXPIRE实现滑动窗口原理简单却非常实用。我用Redis这些年最大的感受是Redis的设计者把大部分复杂性都藏在了简洁的命令背后但复杂性只是被隐藏并没有消失。真正理解它靠的不是背命令而是把每条命令放到真实场景里去检验它的边界和代价。希望这篇从安装到实战的长文能帮你少走些弯路也欢迎在评论区把你遇到过的Redis怪问题丢出来我们接着聊。
阅读完成 · 觉得有帮助?