
1. 先从一次线上事故说起为什么必须吃透Redis的基础和细节大概半年前我们一个营销活动零点上线头一分钟流量直接打垮了数据库。排查到最后原因并不复杂所有奖品的库存key都设置了同一时刻过期缓存一失效请求全量穿透到MySQL连接数瞬间打满。那次事故之后我把团队里关于Redis的使用规范重新梳理了一遍从基础命令到部署参数再到策略选型认认真真补了一课。今天这篇就当作一次完整复盘把Redis是什么、常用命令有哪些、以及线上优化怎么做一次性讲透。Redis是现在后端服务里几乎绕不开的组件。它的定位是一个基于内存的Key-Value存储系统把数据放在内存里读写所以速度极快。官方给的基准数据一般是单实例10万级QPS具体和机器性能、数据结构大小、网络开销都有关系。和传统数据库相比Redis最大的差异在于它把数据模型设计成了多种数据结构而不是只有二维表。String、Hash、List、Set、ZSet这五种类型基本覆盖了缓存、计数、队列、去重、排行榜这些常见场景。很多人学Redis停留在“会SET和GET”的程度功能能跑通但一到线上就出问题。比如key没设置过期时间导致内存涨到OOM或者用KEYS命令把整个实例卡住再或者主从切换后数据回滚导致缓存错乱。这些问题本质上不是因为Redis难而是使用者对它的底层逻辑和命令边界理解不够。这篇文章我会从三个层面展开。第一层是基础和命令覆盖五种核心数据类型和它们的高频用法这部分适合刚入门或者用过但没系统梳理过的读者。第二层是进阶场景比如分布式锁、管道批量操作、事务与Lua脚本的执行规则。第三层是优化包括慢查询排查、大key处理、内存策略选择、缓存穿透与雪崩的应对方案。无论你是后端开发、运维还是自己写点小项目想提升性能这篇文章都可以当作一份能拿来就用的操作手册。2. 安装部署和基础配置这些参数不调好后面全白搭Redis在不同系统下的安装方式差异比较大很多新手第一次装就卡在配置文件上。我建议不管用什么平台装完以后先做三件事确认版本、确认端口、确认持久化开关。版本直接决定你能用哪些命令比如Redis 6.0之后才支持多线程IO和ACL7.0之后引入了Function功能。端口默认6379除非有明确的防火墙要求不然不用特意改但一定要设置访问密码不要裸奔在内网里。2.1 Linux下的安装步骤与启动方式下载源码包例如redis-7.2.4.tar.gz放到/opt目录下解压。执行make编译如果机器上没有gcc需要先yum install -y gcc或者apt install -y gcc否则会报错。编译完成后在源码目录下执行make install PREFIX/usr/local/redis把二进制文件装到指定目录。复制配置文件cp redis.conf /etc/redis/6379.conf也可以直接用默认配置启动但不推荐因为默认配置禁用了很多保护机制。修改关键配置后使用/usr/local/redis/bin/redis-server /etc/redis/6379.conf启动。配置文件里第一优先级是daemonize yes让Redis在后台运行。第二是requirepass设置访问密码。第三是appendonly yes开启AOF持久化。如果这几项都没改那Redis进程一关缓存数据可能就找不回来了。很多时候开发环境看不出问题因为每次重启数据都能重新加载但线上绝对不能这样。2.2 Docker方式部署Docker部署相对省心但要注意挂载数据目录和配置文件。直接启动一个裸容器容器删了数据也就没了。docker run -d --name redis \ -p 6379:6379 \ -v /data/redis/redis.conf:/etc/redis/redis.conf \ -v /data/redis/data:/data \ redis:7.2 \ redis-server /etc/redis/redis.conf-v把宿主机上的配置文件和持久化目录映射进容器这样就算容器重建数据也还在。注意要提前在宿主机创建好/data/redis目录并在配置文件里把dir /data这一项设置好否则RDB快照写入时会报权限错误。Windows环境下开发调试可以直接用微软维护的Redis发行版也可以借助WSL跑Linux版本用法都差不多核心思路是一样的先保证一个稳定干净的运行环境再去折腾业务代码。2.3 客户端连接工具的选择命令行的redis-cli是最轻量的排查工具支持redis-cli -a 密码 ping这种快速连通性测试。图形化工具用过不少目前比较主流的还是Redis Desktop Manager和Another Redis Desktop Manager。前者老版本对Redis 6之后的ACL支持不太好后者更轻快适合日常查看key列表和过期时间。不过我还是建议养成用命令行做线上操作的习惯图形工具适合观察和简单操作生产环境里精确执行命令命令行永远是最可控的。3. 五种核心数据类型命令速查和场景对照Redis之所以不叫“Redis数据库”而叫“Redis数据结构服务器”是因为它每种类型的操作都针对特定的业务需求做了优化。掌握命令不能靠死记硬背要按场景去理解。我把常用命令整理成了一份速查表标注了它们最典型的用途以及容易踩坑的细节。3.1 String最简单的类型却藏着不少边界问题String是Redis里最基础的类型value可以是普通字符串、数字字符串或者二进制数据最大512MB。它最常见的场景是缓存对象、计数器、限流。核心命令如下命令作用典型业务场景SET key value设置key缓存用户信息GET key获取value读取缓存SETNX key value只有key不存在时才设置分布式锁占位INCR key数值加1点击量、库存扣减DECR key数值减1库存回补EXPIRE key seconds设置过期时间防止缓存永不失效TTL key查看剩余过期秒数排查key何时消失MSET k1 v1 k2 v2批量设置减少网络往返实际开发里最常见的错误有两个。一个是给没有设置过期时间的key持续写入比如把验证码这类数据也存成长期有效日积月累就把内存堆满了。另一个是使用INCR做扣减操作时忘记在扣减前判断是否存在结果把负数库存也INCR成了新值。比如扣库存这段逻辑很多人会直接写if redis.call(GET, key) false then return -1 end local stock redis.call(INCR, key) if stock 0 then return 1 else return -1 end这个写法有并发问题因为GET和INCR不是原子操作后面讲Lua脚本时会再展开。这里先记住一个原则凡是“先读、再判断、再写”这三个动作连在一起的就不能用普通命令序列必须用Lua保证原子性。3.2 Hash对象存储的天然选择Hash把多个字段组织在同一个key下适合用来存“对象”。比如用户信息包含name、level、score三个字段用String就得拼出三个key而Hash只需要一个key加上三个field。命令作用示例HSET key field value设置字段值HSET user:1001 name zhangsanHGET key field获取字段值HGET user:1001 nameHGETALL key获取所有字段与值查询整个用户对象HMGET key field1 field2批量获取字段只取name和score两个字段HINCRBY key field n字段值增加给score加10分HDEL key field1删除字段移除某个属性Hash类型的优势在于当只需要读取对象里少数几个字段时不必像String那样把整个value都取回来再反序列化。但它也有坑如果字段过多、对象过大HGETALL同样会产生大key问题阻塞其他操作。我们的经验是Hash里的field数量控制在1000个以内比较稳妥超过这个量就要考虑拆分key。3.3 List不只可以用来做队列List在底层是链表结构头尾操作是O(1)中间索引访问是O(n)。常用命令里LPUSH和RPUSH代表从头部还是尾部写入LPOP和RPOP则对应弹出。业务里最广为人知的是做消息队列一边LPUSH另一边BRPOP阻塞消费。命令作用场景LPUSH key value从头部放入最新消息列表RPUSH key value从尾部放入普通队列LPOP key从头部弹出取消息RPOP key从尾部弹出取消息LRANGE key start stop读取区间分页查列表LLEN key获取长度消息堆积量BRPOP key timeout阻塞弹出消费者等待消息List类型有个典型误用场景用LRANGE做分页查询起始下标很大比如LRANGE list 1000000 1000020底层需要从头遍历到100万个节点性能会迅速劣化。所以List的长度需要控制在合理范围。长时间不清空的列表要么定期裁剪掉旧数据要么在写入时就用LTRIM保留最近N条。3.4 Set去重和随机抽奖都靠它Set是去重集合往里面重复添加同一个元素只会存在一份。可以用来做点赞用户去重、已读消息记录或者利用交集并集做推荐系统里的标签匹配。常用命令命令作用场景SADD key member添加元素记录用户IDSREM key member删除元素取消点赞SISMEMBER key member判断是否存在检查是否已参与SMEMBERS key取出所有元素查询全部粉丝SCARD key获取元素数量统计点赞数SUNION key1 key2并集多群聊合并SINTER key1 key2交集共同好友SPOP key count随机弹出抽奖有一个容易忽略的点SMEMBERS会取出全量元素如果Set有几十万个成员这个操作可能阻塞Redis。线上不要直接执行SMEMBERS推荐用SSCAN分批遍历。判断某个用户是否在集合里用SISMEMBER官方会提示一个问题。3.5 ZSet排行榜功能的最强拍档ZSet在Set的基础上给每个元素增加了一个score分值Redis会根据score自动排序分值可以重复。它最大的价值是在做排行榜、延时队列、滑动窗口限流这类需要排序和区间查询的场景。命令作用场景ZADD key score member添加或更新成员写入玩家分数ZINCRBY key n member增加分数分数变动ZSCORE key member查看分数查询单用户总分ZRANK key member按分数从小到大排名查看名次ZREVRANK key member按分数从大到小排名查最高排名ZRANGE key start stop按下标范围正向取查看从低到高ZREVRANGE key start stop按分数大小反向取排行榜前N名ZRANGEBYSCORE key min max按分数区间取筛选及格学员ZREM key member删除成员移除用户这里有个新手极易混淆的细节ZRANGE返回的是按score从小到大ZREVRANGE返回的是从大到小。做排行榜时很多人直接用ZRANGE结果发现第一页排的是最低分想要的前三名反而在最后一页。一定要先想清楚方向。另外要注意Zset的底层结构是跳表加哈希表当成员数量多且需要频繁插入时性能仍然不错但如果score都相同它就会退化成按字典序排列选择score时尽量设计成业务上需要排序的数值而不是填一个随便的固定值。4. 进阶操作里最容易出错的三块管道、事务、分布式锁很多人用Redis止步在“能存取数据”这个层面没有深挖它的原子操作能力。但真实业务中的一次性批量写入、库存扣减、分布式环境下的互斥控制全都依赖于这三个进阶特性。用好了它们能让应用性能和正确性上一个台阶用不好各种隐性问题随后就来了。管道Pipeline解决的是一次请求多条命令的网络开销问题。如果没有管道执行N条命令需要N次网络往返。管道模式下客户端把所有命令打包一次发到RedisRedis逐条执行后把结果一次性返回。在批量缓存预热、批量写入用户标签时管道能把耗时降到原来的十分之一甚至更低。但要注意管道并没有把命令打包成“事务”它只是减少了RTT不保证中间某条命令失败后前面的命令能回滚。事务通过MULTI、EXEC、DISCARD、WATCH实现。Redis事务和关系型数据库事务不同它不保证原子回滚而是保证在EXEC之前所有命令排好队EXEC之后一起执行中间不会插入别的客户端命令但某一条命令执行失败其他命令照常执行。这个特性对“要么全成功、要么全失败”的业务来说需要格外小心。分布式锁是最常见的需求之一最朴素的实现就是SET key value NX PX 30000NX表示key不存在时才设置成功PX设置过期时间。问题是锁的释放阶段一个人拿到锁之后业务还没执行完锁已经因为过期自动释放了另一个线程拿到了锁前一个线程在这时执行DEL把别人的锁删了。正确做法是value存一个唯一标识删除前先用Lua脚本比对value是否一致if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这个脚本保证了“检查持有者”和“删除锁”两步是原子操作不会出现删错锁的问题。此外过期时间如何设置也需要根据业务耗时来估算不能拍脑袋填个十秒。如果业务确实可能超过过期时间就需要引入“看门狗”机制定期续期。好在像Redisson这类客户端已经封装好了这些能力项目里直接用它的RLock就行不必自己手写续期逻辑。我在代码评审中见过很多人手写分布式锁实现了一个版本看起来能用但仔细想总有一两个边界问题的。比如没有设置过期时间进程挂了锁永远不会释放又比如把锁的过期时间写死成1秒业务执行超过1秒后就失去保护。踩过几次这类问题的坑后我的建议是能用成熟库就用成熟库自己写的锁只能作为辅助手段去配合数据库唯一约束之类的最后兜底。5. 线上优化慢查询、大key、内存策略与缓存异常治理Redis优化不像SQL优化那样有一个明确的执行计划可以看它更需要从数据特征、命令模式、内存使用和并发访问四个维度去做体检。下面这几块内容是运维和生产环境故障排查中最常用的一条条说清楚。5.1 先搞清楚慢查询从哪来Redis的慢查询日志记录了执行时间超过阈值的命令。通过它定位慢命令是最直接的手段。先确认当前配置slowlog-log-slower-than 10000 slowlog-max-len 128单位是微秒默认10000微秒也就是10毫秒。这个阈值线上一般可以设成5000甚至2000因为Redis绝大多数命令在O(1)或O(log n)复杂度下应该远低于5毫秒。如果某条命令超过这个值说明它很可能碰到大key或复杂操作了。查看最近慢查询SLOWLOG GET 50 SLOWLOG RESET一次线上排查里我们发现最近一条慢查询是LRANGE user:timeline 0 -1timeline列表已经积累到了几十万条消息。改用只保留最近100条的策略并在读取时限定最大条数后慢查询立刻消失了。这个问题的本质是命令复杂度不随数据规模变化而变化是设计时就要避免的。5.2 大key是万恶之源大key不是指key本身字符长而是指value体量大。判断大key有几种途径用redis-cli --bigkeys命令做一次全库扫描它会对每种类型找出最大的几个key也可以基于DEBUG OBJECT key查看序列化长度但在生产环境频繁执行会带来额外开销。大key的危害非常明显一是删除时容易阻塞Redis进程因为删除一个包含几百万元素的Hash或List释放内存的过程是同步的可能卡住几十毫秒到秒级别。二是读写大key都会拖慢单线程的处理速度。三是主从同步时大key的传输也会加大网络和内存压力。处理大key的常用原则是拆分也就是把一个大集合拆成多个小集合用小key加hash取模的方式散列存储。比如原来有一个Hash存了用户所有好友关系可以按用户ID分桶到100个Hash里每个Hash只存一部分好友这样单key体积就下降了。删除大key时不要直接DEL用渐进式删除命令更安全。Redis 4.0以后提供了UNLINK命令它把释放内存的操作放在后台异步执行不会阻塞主线程。5.3 内存优化淘汰策略和数据编码内存淘汰策略是Redis在达到maxmemory上限时如何处理新写入的一种机制。配置项是maxmemory-policy常用策略有策略行为适合场景noeviction不淘汰写入直接报错你需要严格保证缓存数据不丢失allkeys-lru从所有key中按最近最少使用淘汰常见纯缓存场景volatile-lru从设置了过期时间的key中按最近最少使用淘汰大部分key都设了过期时间volatile-ttl从设置了过期时间的key中优先淘汰剩余时间短的适合做大量短生命周期缓存allkeys-random随机淘汰任意key缓存数据访问无明显热点这里需要分清一个误区不是设置了过期时间的key才能被淘汰。allkeys-lru同样可以淘汰没有过期时间的key如果业务里有些key想做永不过期的懒加载必须确认自己设置的是volatile开头的那几个策略。编码优化方面Redis对每种类型都有内部编码方式比如Hash和ZSet在元素少的时候用ziplist压缩列表超过阈值后转成hashtable或skiplist。这种编码转换是自动的但也会减少内存碎片。实际开发中我们常做的一个优化是把大量小对象从String切换成Hash可以降低内存。比如用户姓名信息原来用user:1:name zhangsan这种String存储所有用户加起来会生成海量key改为HSET user:1 name zhangsan后一个key内就能存多个属性内存占用能下降不少。5.4 缓存三兄弟穿透、击穿、雪崩这三个概念面试里常问实际运维中也真的会遇到分开说。缓存穿透是请求了一个缓存和数据库里都不存在的数据比如查询一个不存在的用户ID每次都绕过缓存打到数据库上。解决思路有两个方向一是布隆过滤器先行判断用位图结构快速过滤掉大部分不存在的key二是把空值也缓存到Redis里设置一个较短的过期时间比如60秒防止同一不存在的key反复击穿数据库。缓存击穿是指某个热点key在过期的一瞬间大量并发请求同时进入全部去数据库加载。解决办法是在热点key过期前做主动续期比如用一个定时任务刷新它的TTL或者配合分布式锁只让一个请求去查数据库其他请求等锁释放后从缓存里读取。缓存雪崩是指大量key在同一时段集体失效导致后端数据库压力暴涨。上面说的我们那次线上事故就是这种情况。解决办法是给过期时间加一个随机扰动比如原来全部设置在午夜零点过期改为零点前后各15分钟内随机这样过期时间被分散开就不会出现瞬间的全量穿透。5.5 别用KEYS慎用SMEMBERSSCAN的合理替代新手总喜欢用KEYS去模糊匹配key比如KEYS user:abc:*这在本地几万key时感觉还行到生产环境几十万key时会直接阻塞Redis。因为KEYS会全量扫描整个键空间。替代方案是SCAN它采用游标方式分批遍历。虽然每批返回的key不一定是最新一致的但对于键名扫描、定时清理分布式锁这类场景已经完全够用。例如每次扫描1000个key循环直到游标回到0redis-cli --scan --pattern user:abc:* --count 1000同理前面提到的SMEMBERS也不适合大集合全量拉取。用SSCAN代替。SCAN和SSCAN都支持模糊匹配和分批返回副作用小得多。5.6 持久化怎么选RDB还是AOF关于持久化我说点管理上的直观经验。RDB是定期生成全量快照恢复快但可能在最后一次快照之后丢数据。AOF是追加写命令日志最多丢1秒内的数据但文件体积会比RDB大。如果项目对数据丢失容忍度很低比如用户余额、订单状态这类重要数据也放在Redis里做短暂存储应该开启AOF并设置appendfsync everysec。如果Redis只是作为纯缓存重启后可以通过回源数据库全量加载那么可以关闭AOF只保留RDB甚至干脆禁用持久化来换取更高的吞吐。生产环境我更推荐两者同时开启RDB做定期快照AOF做崩溃恢复的细粒度保障。这样既兼顾了崩溃恢复的速度又能保证绝大多数数据不丢失。6. 一套可供参考的日常运维自检清单这里分享一份我在项目里经常用的Redis健康度自检清单不是官方文档的顺序而是踩坑踩出来的。“查状态、查内存、查命令、查主从”四步走基本能发现90%的常见隐患。第一步查状态。INFO server看版本和运行时间INFO replication看主从角色和复制偏移量。如果从库复制偏移量和主库相差持续扩大说明同步跟不上需要检查网络或者补做全量同步。INFO persistence能看出最近一次RDB是否成功、AOF重写是否正常。第二步查内存。INFO memory看used_memory和used_memory_human再看maxmemory配置。把used_memory除以自身业务数据量估算一下如果内存使用率长期超过70%就要尽早考虑扩容或优化存储结构。还有MEMORY DOCTOR命令它会在Redis 4.0以上版本给出内存诊断建议例如是否存在内存碎片率过高的问题不过输出信息较多读起来比较费劲我更习惯直接用INFO memory里的mem_fragmentation_ratio这个比值接近1表示内存碎片很少如果超过1.5说明碎片率偏高可以考虑用ACTIVE DEFRAG整理碎片或者重启实例重新分配内存。第三步查命令。除了前面说的SLOWLOG还可以打开CONFIG SET latency-monitor-threshold 100用LATENCY LATEST查一下实例里各个事件的最大延迟时延。结合慢查询日志定位高耗时命令。第四步查主从。使用INFO replication查看从库是否都在线再用redis-cli --replicaof 主库IP 端口。关于主从切换和安全注意事项建议Redis 6.0之后的部署统统使用ACL功能给不同业务线分配不同权限避免一个应用拿着管理员权限操作所有库。还有一点容易被忽略定期检查key的过期时间设计。比如某个业务的key本来设置了30分钟过期但因为需求变更改成了永久有效时间一长内存里堆了几十万个无用key。我用一个简单的定时脚本每天扫描全库统计不存在过期时间的key的个数和占用内存再逐个业务线确认是否需要设置过期时间能省下不少内存容量。7. 写在最后Redis学得好不好看排查问题的动作就知道Redis这条技术线说深很深底层涉及网络模型、内存分配、持久化协议说浅也浅日常开发用好五大数据类型和几个核心配置就已经能覆盖绝大多数业务场景。但我觉得真正区分“会用”和“用得稳”的不是背了多少命令而是遇到线上故障时你能不能快速判断出问题出在缓存设计、命令使用还是部署架构上。回到开头那次事故。我们把营销活动的缓存key按分钟级别打散过期时间同时给数据库查询接口加了限流和空值缓存从那以后再没出现过同类的全量穿透。复盘的经验只有一条Redis优化不是故障发生后的补丁而是在设计缓存结构、选择数据类型、配置内存策略时就要提前想到系统在流量峰值下会经历什么。这篇文章把命令和优化手段整理出来希望能让你少踩几次我们踩过的坑。最后说个实际操作中的习惯。我每次接手一个Redis相关项目一定会做的第一件事是打印出当前实例的config配置把maxmemory、maxmemory-policy、appendonly、save策略这些关键项逐条核对一遍。这个动作花费不到五分钟但比任何性能测试工具都更能暴露隐患。下次如果你发现Redis运行得没那么稳不妨也从这个动作开始。