ARTICLE DETAIL

资讯详情

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

Redis实战笔记:从数据结构到高并发缓存架构

Redis实战笔记:从数据结构到高并发缓存架构 做了几年后端Redis几乎是每套系统里都绕不开的组件。缓存、分布式锁、排行榜、会话共享、限流、消息队列它都能掺和一脚。这个项目名为“redis笔记”其实就是我这些年使用Redis的一次系统性梳理——从安装部署到底层数据结构从持久化机制到高并发场景下的缓存三大难题再到线上真实踩过的那些坑。我尽量把这些内容整理得能直接上手用而不是仅仅停留在命令文档的层面。这篇笔记适合刚接触Redis不久、想把基础打扎实的开发者也适合已经用了一段时间、但总在某些边界场景上拿不准的运维或后端工程师。如果你正在准备面试这里的不少问题也正好是高频考点。我写的时候尽可能把“为什么这么做”讲清楚因为只记住命令没有用只有理解了背后的设计取舍你才能在遇到新问题时自己做出判断。1. Redis到底是什么为什么大家都用它1.1 从数据结构服务器的本质说起Redis全称是Remote Dictionary Server直译过来就是“远程字典服务器”。面试时如果被问“Redis是什么”不要只说它是个缓存——这个答案太浅了。更准确的说法是Redis是一个基于内存的、单线程模型的、支持多种数据结构的NoSQL数据库。它和传统关系型数据库最大的区别在于数据存哪、怎么存。MySQL这类数据库把数据持久化到磁盘通过索引、缓冲池、执行计划优化来对抗磁盘I/O的慢速度而Redis直接让数据住在内存里读写都是内存操作速度自然快到了毫秒甚至微秒级别。官方公布的benchmark数据是单实例可以轻松达到10万以上的QPS实际生产环境里跑个几万QPS完全是常态。但Redis绝不是靠“快”这一个特点吃饭的。它真正让人离不开的是那套灵活的数据结构。String、Hash、List、Set、ZSet这五种基础类型加上后来的Bitmap、HyperLogLog、Geo、Stream每一种都对应着一类典型的业务场景。这就意味着你可以在Redis里用两三行命令完成传统数据库里要写一堆SQL才能实现的功能比如排行榜、去重统计、附近的人、延迟消息等等。1.2 为什么单线程还这么快不少初学者第一次听到“Redis是单线程”时都会愣一下现在CPU都是多核的单线程怎么可能快这里要先澄清一点Redis 6.0之前核心的命令执行链路确实是单线程的6.0开始网络I/O部分引入了多线程但命令执行依然由单线程完成。单线程模型能成立核心原因是Redis内部的性能瓶颈从来不在CPU而在网络I/O和内存带宽。当数据都在内存里、命令又足够简单时CPU根本来不及成为瓶颈数据就已经返回了。单线程反而带来了两大好处不需要加锁不会出现竞争条件和死锁的问题代码复杂度大幅下降。数据结构的实现可以做得非常高效不需要考虑多线程同步带来的额外开销像SDS动态字符串、跳跃表这些设计都因此受益匪浅。同时Redis在操作系统层面做了大量优化I/O多路复用技术基于epoll/select/kqueue让单线程可以同时处理成千上万的客户端连接命令执行过程使用复用的事件处理器不会因为等待某个客户端而阻塞其他请求。所以永远记住一句话Redis快是因为内存数据结构I/O多路复用而不是因为单线程本身。1.3 适用场景和不适合的场景我一般会把Redis的典型使用场景分成五类缓存层这是最基础也是最广泛的使用方式。热点数据放内存避免请求直接打到数据库。分布式锁基于SETNX或者Redisson实现解决多实例环境下的资源竞争。排行榜和计数器ZSet天然适合排序INCR系列命令适合点赞数、访问量等场景。会话共享多台应用服务器的Session统一存到Redis实现无状态化。消息队列List的阻塞读取、Stream类型都能做简单的队列模型适合中小流量的异步解耦。但Redis并不是万能钥匙。比如它不适合做复杂的关联查询、不适合存超大文件、不适合作为唯一的数据源来承载核心交易数据因为它的持久化能力虽然有RDB和AOF但相比成熟的关系型数据库数据安全性和恢复能力还是有差距的。我见过有的团队把所有业务数据都往Redis塞结果一重启就丢数据这是对Redis定位的误解。2. 环境准备下载、安装与基础配置2.1 安装方式选哪种Redis官网提供源码包Linux的各个发行版仓库里也有现成的包Windows没有官方版本只能找第三方移植。我先说结论生产环境优先用Linux 官方源码编译安装开发环境随便你怎么舒服怎么来。源码编译安装其实比你想象中简单整个过程就是四步# 1. 下载源码包建议去官网 https://redis.io/download 拿最新稳定版 wget https://download.redis.io/releases/redis-7.0.12.tar.gz # 2. 解压并进入目录 tar xzf redis-7.0.12.tar.gz cd redis-7.0.12 # 3. 编译这里不需要configureMakefile已经写好了 make # 4. 安装到系统目录默认装到/usr/local/bin make install如果你不想整个步骤太复杂用包管理器也能装。Ubuntu/Debian下是aptCentOS/RHEL 8以上用dnfmacOS用brew# Ubuntu apt update apt install redis-server # CentOS/RHEL dnf install redis # macOS brew install redis包管理器安装的好处是省事Redis会自动注册成系统服务开机自启都给你配好了。坏处是版本通常滞后比如Ubuntu 20.04仓库里的Redis还停留在5.x很多新特性用不了。所以我的习惯是开发环境图快用brew或者apt测试和生产环境全部源码编译保证版本可控、参数可调。Windows用户我多说一句。官方不支持Windows微软曾经维护过一个移植版后来也放弃了。现在Windows上比较靠谱的方案是WSLWindows Subsystem for Linux在WSL里装一个Ubuntu然后走Linux路线。也有Memurai这种商业兼容层但个人玩票性质可以生产环境不建议冒险。2.2 配置文件里必须关心的几个参数Redis默认配置可以直接启动redis-server回车就算跑起来了监听6379端口。但真要拿它干活至少要修改以下几个配置项。daemonize决定Redis是否以守护进程方式在后台运行。开发时你可以不管生产环境必须设成yes否则终端一关Redis就没了。不过现代系统推荐用systemd托管由systemd管理守护进程配置文件里反而保持daemonize no。bind和protected-mode默认配置下Redis只监听127.0.0.1外部根本连不上这是很安全的设计。如果你确实需要远程访问建议同时做两件事把bind改成你的内网IP再把protected-mode no。但我要警告一下——绝对不要在没有密码保护的情况下把Redis暴露到公网。网上被挖矿病毒入侵的案例十有八九都是Redis裸奔导致的。requirepass设置访问密码。用redis-cli登录后执行AUTH yourpassword或者连接时直接redis-cli -a yourpassword。要注意密码是明文存储在配置文件里的所以配置文件本身的权限要收紧建议chmod 600。maxmemoryRedis能用的最大内存默认是64位系统下不限制这在生产环境是个隐患。如果业务代码有问题突然写入大量数据Redis会把机器内存耗尽触发OOM。在配置文件里根据机器内存大小设置一个合理阈值再配合下面的内存淘汰策略才是完整方案。maxmemory-policy内存满了之后怎么处理新写入的数据。这个配置特别关键后面我会单独讲。先记住最常用的几个allkeys-lru对所有key按LRU淘汰、volatile-lru只对设置了过期时间的key按LRU淘汰、noeviction内存满了就直接报错不淘汰任何数据。改配置文件后记得重启服务或者用redis-cli config get/set动态修改运行期参数。要注意的是运行时CONFIG SET修改的参数如果不执行CONFIG REWRITE重启后会回到配置文件的值。2.3 验证安装是否成功装完之后用redis-cli ping返回PONG就代表服务正常运行了。这是最简单的健康检查命令。再看一下版本和基础信息redis-server --version redis-cli INFO | head -n 20INFO命令会输出一大堆运行时统计信息包括内存占用、连接数、命中率、持久化状态这些排障时非常好用。建议第一次搭好环境后就把INFO的输出从头到尾扫一眼对后续理解各种指标很有帮助。3. 五种核心数据结构以及它们的实战用法3.1 String最基础也是最容易被低估的类型String是Redis里最简单的数据结构一个key对应一个valuevalue最大能存512MB。但千万别因为它简单就小瞧了它实际开发中String的使用频率是最高的。典型场景一热点数据缓存。把从数据库查出来的用户信息、商品详情、配置项序列化成JSON字符串后写入Redis设置一个合理的过期时间下次请求直接读缓存。SET user:10001 {name:张三,level:3} EX 3600 GET user:10001注意EX 3600的意思是一小时后自动过期。如果忘了设置过期时间这个key就会一直留在内存里日积月累就是内存泄漏。典型场景二计数器。INCR和DECR是原子操作即使并发请求再多也不会出现数据错乱。你要做一个文章的阅读量统计直接INCR article:readcount:88读的时候GET就行了。这种原子自增也经常用在分布式环境下的序列号生成。典型场景三分布式锁。Redis官方推荐的简单锁实现就是SET命令带上NX和EX参数SET lock:order:12345 unique_value NX EX 30意思是“只有当key不存在时才写入并且30秒后自动过期”返回值OK代表加锁成功返回nil代表别人已持有锁。释放锁时要注意先比较value是不是自己的再删除避免误删别人的锁。这个场景细节比较多我在后面常见问题里会专门展开。String底层的数据结构值得提一句Redis自研了SDSSimple Dynamic String而不是直接用C语言的char数组。SDS可以O(1)获取长度、杜绝缓冲区溢出、减少修改字符串时内存重新分配的次数这也是为什么Redis能高效处理频繁的字符串修改操作。3.2 Hash更适合存储对象Hash类型相当于Java里的Map、Python里的dict它允许你在一个key下存储多个字段。相比把整个对象序列化成StringHash最大的优势是可以单独操作某个字段不用为了改一个属性就把整个对象读出来重新序列化再写回去。典型场景购物车、用户资料、商品信息。# 商品ID为1001的信息field分别是name、price、stock HSET product:1001 name 机械键盘 price 399 stock 100 HGET product:1001 name HINCRBY product:1001 stock -1 # 下单时库存减一用Hash做对象存储内存效率通常比JSON序列化高尤其是字段多、只访问部分字段的场景优势非常明显。但也有个坑如果一个key的字段数量特别多或者某个字段的值特别大就会出现大key问题后面我会专门讲。另外要注意Hash的每个field的过期时间不能单独设置过期只能针对整个key。3.3 List双向链表消息队列的雏形List存储一个有序的字符串列表可以从头部或者尾部插入和弹出元素。最经典的应用是“最新消息”列表比如用户的时间线每次发新动态都LPUSH到List头部读取时LRANGE取前20条配合LTRIM控制长度内存占用可控。另一个经典用法是做简单的消息队列。生产者LPUSH消息消费者RPOP取出消息# 生产者 LPUSH task:queue job1 # 消费者BRPOP是阻塞式读取没有消息时休眠等待避免CPU空转 BRPOP task:queue 0BRPOP的timeout参数传0代表永远阻塞等待。这里有个实战体会单纯使用BRPOP有一个致命的缺陷——如果消费者处理消息时崩溃了消息就丢了因为已经出队了没有人知道它还没被处理完。如果要可靠投递需要用RPOPLPUSH系列命令把消息备份到一个处理中队列确认处理完成后再删除。不过话说回来如果没有严格的可靠性要求用Redis List做轻量级队列完全够用我自己在中小项目里就这么干过很多次比引入Kafka省事太多。3.4 Set无序集合去重和交集的高手Set是去重的无序集合底层用哈希表实现所以添加、删除、判断是否存在都是O(1)复杂度。它的独门绝技是支持集合运算SINTER交集、SUNION并集、SDIFF差集。典型场景一点赞用户列表。文章ID为101的文章被哪些用户点赞过直接SADD article:101:liked user1 user2判断某个用户是否点过赞用SISMEMBER article:101:liked user1。典型场景二抽奖池。把参与活动的用户ID全部加进Set随机抽出N个中奖者SADD lottery:act001 u10001 u10002 u10003 SRANDMEMBER lottery:act001 2 # 允许重复抽 SPOP lottery:act001 2 # 抽完后移除不重复典型场景三共同好友。两个用户的粉丝集合做交集一条命令就能算出来比如社交App里的“你关注的人也关注了TA”这类功能SINTER一下就行。传统数据库做这样的集合运算要写很复杂的SQLRedis一条命令搞定这也是为什么它的数据模型在特定场景下比关系型数据库更高效。3.5 ZSet有序集合排行榜的默认选型ZSet在Set的基础上给每个元素关联了一个分数score集合按照分数排序。底层实现是跳跃表skiplist加哈希表插入、删除、查排名都非常高效。典型场景排行榜。游戏里按战力排名、电商里按销量排名都是它的拿手好戏。# 给玩家id9527加分到100.5 ZADD leaderboard:game001 100.5 9527 # 获取前三名 ZREVRANGE leaderboard:game001 0 2 WITHSCORES # 获取某玩家的排名 ZRANK leaderboard:game001 9527ZSet还能做延迟队列的思路把任务执行时间作为score轮询时用ZRANGEBYSCORE min_time max_time拉取到期的任务取出后删除。虽然不如专业队列完善但在某些不需要持久化、不想引入重型组件的场景下这个小技巧很实用。使用ZSet有一个容易踩的坑score的精度。score底层是double类型如果你需要记录很大的整数或者对精度要求极高比如金额建议把值转换成整数再存入或者用String类型单独存原始值。否则可能出现分数对不上账的问题。3.6 其他衍生类型Bitmap、HyperLogLog、Geo、Stream这四种类型我用一张表总结它们适合做什么类型适用场景核心命令关键点Bitmap签到、在线状态、布隆过滤SETBIT / BITCOUNT基于位运算极其省内存HyperLogLogUV统计PFADD / PFCOUNT有0.81%的标准误差适合近似统计Geo附近的人、地理位置检索GEOADD / GEORADIUS底层是对经纬度做编码的ZSetStream消息队列支持消费者组XADD / XREADGROUP相比List可靠支持ACK确认比如一个签到功能一年365天只需要365个bit大约46个字节假设有100万用户也就占用几十兆内存用Bitmap简直不要太划算。这些类型不是重点但面试时提一嘴会让别人觉得你不是只会五种基础类型。4. 持久化机制重启不丢数据的保障4.1 RDB快照瘦身快速恢复RDBRedis DataBase是Redis默认开启的持久化方式。它会周期性地把当前内存中的全量数据生成一份快照写入磁盘文件默认叫dump.rdb。恢复时直接把RDB文件载入内存速度非常快。触发RDB的时机由配置文件里的save参数控制默认配置大概是save 900 1 # 900秒内至少1次修改就触发一次快照 save 300 10 # 300秒内至少10次修改触发快照 save 60 10000 # 60秒内至少10000次修改触发快照这条配置的意思是“写入频率越高检查越频繁所以快照生成越快”。生产环境到底怎么设置要看你的数据容忍丢失窗口。比如你设置save 900 1意味着极端情况下最近900秒内的数据可能全部丢失。RDB的快照生成采用的是fork子进程的方式父进程fork出一个子进程子进程负责把内存数据写入临时RDB文件写完后原子替换旧文件。这里有一个非常关键的知识点fork会复制父进程的页表如果Redis占用的内存特别大fork瞬间的阻塞时间会很长。所以生产环境建议给Redis机器预留足够的内存不要让内存使用率达到极限。4.2 AOF日志更安全的增量持久化AOFAppend Only File的原理是把每一次写命令追加到日志文件末尾。恢复时依次执行日志里的命令就能重建数据。相比RDB的全量快照AOF的粒度细得多理论上能实现每秒甚至每次写入都落盘。AOF有三种写回策略策略说明丢数据风险always每次写命令同步刷盘基本不丢everysec每秒刷一次盘最多丢1秒数据no交给操作系统决定何时刷盘可能丢较多数据生产环境我一般推荐everysec性能和安全达到了比较理想的平衡。always对性能影响太大吞吐量会肉眼可见地掉下来no虽然有更好的性能但Redis进程崩溃或者机器断电时会丢失大量数据。AOF最大的问题是日志文件会越来越大。好在Redis提供了日志重写机制rewrite把内存当前的数据状态转换成最精简的写入命令生成一个新的AOF文件替换旧文件。Redis 4.0以后还引入了混合持久化方式重写时先写一份RDB格式的快照内容再追加增量AOF命令。这样恢复时加载RDB快照的速度很快之后重放少量增量命令兼顾了恢复速度和数据完整性我个人在新版本里基本都用混合模式。4.3 到底该选RDB、AOF还是混合我把选择思路总结成一句话能不能容忍丢数据以及你能接受多长的恢复时间。纯缓存场景数据丢了再从数据库重新加载就行用RDB足够简单省心。核心数据比如订单状态、用户余额必须在Redis里也保证一定可靠性开AOF everysec并且开启混合持久化。对数据安全要求极高的场景AOF always但你要接受性能损耗。我自己的经验是大部分业务系统用RDB AOF混合模式Redis 4.0默认配置就是两者都开就已经非常稳了。不要因为害怕丢数据就盲目上always性能瓶颈往往是先出现在你没考虑到的环节上。5. 高并发缓存三大难题穿透、击穿、雪崩5.1 缓存穿透查询不存在的数据缓存穿透的典型表现是请求查询的数据在数据库里也不存在这样缓存永远无法命中所有请求都穿透到数据库。如果有人恶意构造一堆不存在的ID不断请求数据库很快就被打垮了。最常用的解决方案有两个。第一个是缓存空值即使是NULL结果也缓存一段时间比如5分钟。这样可以拦住重复的无效查询但代价是浪费一些内存而且如果这个key之后真有数据了可能要等缓存过期才能看到解决这个问题不复杂写数据时同步删除空缓存即可。第二个是布隆过滤器启动时把所有的合法ID加载到布隆过滤器里请求进来先判断ID是否可能存在不存在直接返回。布隆过滤器的特点是“判断不存在一定不存在”但“判断存在不一定存在”会有一定的误判率需要在空间和准确率之间权衡。5.2 缓存击穿热点key瞬间失效缓存击穿和穿透一字之差但含义完全不同。击穿指的是一个非常热点的key刚好到了过期时间一瞬间大量请求同时发现缓存没有数据于是全部打到数据库上数据库压力瞬间爆炸。解决思路有两个层面。一是永不过期不给热点key设置过期时间改为在value里存储一个逻辑过期时间后台任务异步更新缓存。这个方案适合数据变化不频繁的场景。二是加互斥锁当缓存失效时不是所有请求都去查数据库而是先尝试获取分布式锁只有拿到锁的请求能查数据库并回填缓存其他请求短暂等待后重新尝试读缓存。// 伪代码示例 String value redis.get(key); if (value null) { if (redis.setnx(lock: key, 1, 30)) { try { value database.query(key); redis.set(key, value, 3600); } finally { redis.delete(lock: key); } } else { Thread.sleep(50); return redis.get(key); // 重试 } }两种方案都有人用互斥锁逻辑简单但对热点场景会增加等待时间逻辑过期更优雅但实现复杂度高。我个人的倾向是如果能接受偶尔一次慢响应用互斥锁更省心。5.3 缓存雪崩大量key同时失效雪崩比击穿更严重不是单个key失效而是大量key集中在同一时间失效导致巨量请求穿透到数据库。比如把所有缓存都设置了同一个过期时间零点的批量更新任务一执行所有缓存同时清空数据库直接被冲垮。应对雪崩最典型的办法就是过期时间打散在设置过期时间时加一个随机值比如3600秒上加0到300秒的随机数让key的失效时间错开。另一个办法是加多级缓存本地缓存比如Caffeine Redis缓存本地缓存抗住第一波流量Redis抗第二波数据库扛底。多级缓存虽然增加了实现复杂度但效果确实立竿见影。还有一点容易被忽略缓存预热。在大型活动开始前提前把热点数据加载到缓存里并且保证这些数据的过期时间分散不要人为制造雪崩条件。6. 线上常见故障与排查技巧实录6.1 大key问题内存、阻塞和删除三座大山所谓大key通常指一个key对应的value特别大比如超过几MB的String或者包含几百万个元素的Hash/List/Set/ZSet。大key带来的危害是多方面的读写的耗时变大因为单线程下每次操作都要处理这么多数据。占用大量内存且不好迁移主从同步时全量重同步可能直接拖垮网络。删除大key时尤其危险DEL命令是同步的在单线程模型里删除一个几百万元素的list可能阻塞Redis几百毫秒甚至更久。排查大key用redis-cli --bigkeys命令它会扫描整个实例并输出最大的若干key。这个命令在线上直接跑会影响性能最好在低峰期执行。删除大key的正确姿势不要直接用DEL用UNLINK命令。UNLINK是异步删除先把key从全局字典里摘掉真正的内存回收在后台线程完成不会阻塞主线程。如果没有UNLINK比如老版本Redis可以分批删除比如一个大Hash用HSCAN查出少量字段HDEL删除反复操作直到清空。6.2 慢查询排查单线程模型的阿喀琉斯之踵既然命令执行是单线程任何一个慢命令都会阻塞后面的所有请求。最常见导致慢查询的命令有KEYS、HGETALL、SMEMBERS、ZRANGEBYSCORE等全量操作命令。开发环境用无所谓生产环境一旦数据量大起来这些命令会立刻让Redis的响应时间飙升。排查慢查询第一步看SLOWLOG。SLOWLOG GET 10这条命令会列出最近的10条慢命令以及耗时。还可以通过CONFIG SET调整慢查询阈值CONFIG SET slowlog-log-slower-than 10000 # 超过10ms的命令记录下来我曾经因为在生产环境执行了一条KEYS模糊匹配直接导致线上Redis阻塞了2秒所有业务跟着卡顿。从此以后我在任何代码里都强制要求禁止KEYS命令。需要匹配时要么用SCAN游标遍历要么在业务里维护一个索引集合比如把所有用户ID放在一个Set里从根源上避免全量扫描。6.3 连接数打满和内存不足连接数打满的典型场景是应用层忘记关连接或者连接池配置得过大。Redis默认maxclients是10000看起来很多但如果每个应用实例都开了几百个连接且没有复用几十个应用实例就能打满。排查方法是用INFO clients查看当前连接数用CLIENT LIST看具体连接来源。内存不足的排查就更有意思了。用redis-cli INFO memory看used_memory和maxmemory的关系再用MEMORY USAGE key看某个key占用多少内存。这时候就需要检查maxmemory-policy是否合理。我在前面提到过这个配置它决定了内存满了时的行为noeviction拒绝写入返回OOM错误不淘汰任何数据。这个策略最安全适合不能丢数据的场景。allkeys-lru从所有key中按LRU算法淘汰最久没用的。适合纯缓存场景。volatile-lru只从设置了过期时间的key里淘汰。适合“有部分数据必须常驻”的场景。这里我要提醒一个容易理解错的点LRU在这里是“近似LRU”Redis为了性能和内存开销并不是对每个key都严格记录访问时间而是基于抽样来的。Redis 4.0后还引入了LFU最不经常使用算法适合那种“访问频率比访问时间更能代表热度”的场景。选择策略前先想清楚你的Redis里存的东西到底能不能丢6.4 缓存和数据库双写一致性问题网上关于缓存一致性的讨论特别多各个方案吵得不可开交。我在实际项目里的做法其实很简单先更新数据库再删除缓存。为什么是删除而不是更新缓存因为更新缓存容易产生并发问题两个请求先后更新了缓存后更新的可能把先更新的旧数据覆盖了。删除缓存就没这个问题下次读取时发现没有缓存自然回源数据库拿到最新值再写缓存。但“先更新数据库再删除缓存”也有一个经典的坑如果删缓存失败缓存里的旧数据会一直存在。而且在高并发下可能存在这样的时序请求A读取数据库旧值准备写缓存请求B更新数据库并删除缓存随后请求A把旧值写回缓存。解决这个问题要么用消息队列重试删除要么给缓存设置一个很短的过期时间作为兜底。讲实话要彻底解决缓存一致性最好的办法是不用缓存但用了缓存就得接受这种“最终一致”的现实。给缓存加上合理过期时间是最简单也最有效的兜底策略。6.5 分布式锁的那些细节坑前面提到过用SET NX EX实现分布式锁这个方案看着简单实际用起来细节多得很。完整可用的加锁命令应该是SET lock:order:12345 unique_value NX PX 30000关键点有三个NX保证互斥PX设置自动过期防止死锁unique_value用来在释放时确认锁是自己的。释放锁的逻辑必须是这样的先GET锁里的value和自己持有时的unique_value比较一致才执行DEL。如果把“先比较再删除”拆成两步中间可能被其他线程抢走锁导致误删。还有一个常见问题是锁的过期时间设置业务执行时间如果超过了锁的过期时间锁自动释放了第二个线程就拿到了锁两个线程同时执行临界区代码分布式锁形同虚设。真正生产级方案是用Redisson那样的看门狗机制锁快过期时自动续期。自己实现的话可以起一个定时任务在锁快过期时续期实现不难但一定要想清楚续期的线程模型和异常处理。另外我说句实在话如果业务没那么复杂很多场景下没必要用分布式锁。比如库存扣减用Redis的原子操作或者数据库的行锁就够了。分布式锁的引入带来了复杂度也带来了新的故障可能小而美的方案往往更靠谱。7. 实用运维经验与监控建议7.1 设置监控指标要有重点Redis运维不能全靠出了问题再去翻日志日常监控要盯住几个核心指标。命中率INFO stats里的keyspace_hits和keyspace_misses。命中率低说明缓存设计不合理大量请求在无效访问。内存使用率used_memory接近maxmemory时需要排查是否有大key或者是否有数据没有设置过期时间。瞬时QPSINFO stats里的instantaneous_ops_per_sec峰谷明显波动时要结合业务判断是否正常。连接数connected_clients如果持续在高位重点排查连接池配置和代码里是否存在连接泄漏。持久化相关RDB最近一次快照的时间、AOF重写的耗时和频率。如果AOF重写非常频繁说明写入命令太多要考虑调大重写阈值。7.2 使用Pipeline和Lua脚本提升性能单线程Redis有一个隐藏的性能瓶颈网络往返时间RTT。一次命令的网络开销可能是几十微秒如果业务中要连续执行几十次命令总耗时就会明显上升。这时可以用Pipeline管道把一批命令一次性发给RedisRedis连续执行完后一次性返回结果能大幅减少网络往返。# 正常逐条执行 SET k1 v1 SET k2 v2 SET k3 v3 # 用redis-cli的pipe模式 (echo -e SET k1 v1\nSET k2 v2\nSET k3 v3) | redis-cli --pipeLua脚本则是把多条命令封装成一个脚本在Redis服务端原子执行既避免了多次网络往返又保证了操作的原子性。比如扣减库存用Lua脚本检查库存是否充足再扣减这个操作在单线程模型下天然原子不需要加锁local stock tonumber(redis.call(GET, KEYS[1])) if stock and stock 0 then redis.call(DECR, KEYS[1]) return 1 end return 0Pipeline适合批量操作但对原子性没有强要求的场景Lua适合必须保证原子性的复合操作两者是互补关系。7.3 主从复制和高可用架构单机Redis一旦宕机业务直接不可用。生产环境至少要配置主从复制主节点提供读写从节点同步数据并承担只读流量或者作为冷备。启用主从只需要在从节点的配置文件加一行replicaof 192.168.1.10 6379主从复制的原理是主节点把数据变更写入复制积压缓冲区推送给从节点。如果网络中断从节点会尝试增量同步如果断线时间太长导致积压缓冲区被覆盖就只能全量同步了。再往上就是Redis Sentinel哨兵模式它负责监控主节点状态主节点挂掉时自动把某个从节点提升为主节点实现故障转移。应用层通过Sentinel获取当前主节点地址这样对业务方来说是透明的。在容器化流行的今天很多人直接把Redis部署在Kubernetes里使用Operator管理。但我要提醒一点Redis对磁盘和网络的延迟比较敏感容器编排带来的调度、网络转发开销有时会影响性能。能用裸金属或虚机部署Redis尽量别为了图省事把它放进容器除非你的基础设施已经针对Redis做了很细致的调优。8. 写在最后的一点心得这篇笔记从安装配置写到数据类型、持久化机制、高并发问题、线上排查和运维架构基本覆盖了我这些年工作中真正用到过的Redis知识。写到这里我特别想分享一个自己的体会Redis其实是一个非常简单的软件它之所以难难在使用者是否理解自己的业务场景。同样的数据结构在不同人手里能发挥完全不同的价值同样的配置参数在不同业务下需要完全不同的取值。在搭建Redis环境的时候多花一些时间把配置文件的注释都读一遍遇到奇怪的线上问题多去查官方文档而不是随便搜博客。踩坑之后你会发现Redis官方文档其实写得比大多数二手资料都清楚很多网上互相矛盾的答案看看源码和注释就能真相大白。如果你在按照这篇笔记实操的过程中遇到环境安装、数据类型选择或者持久化策略上的问题欢迎在评论区留言我会尽量以我第一次遇到这些问题时的思考路径来回复。Redis还在一路进化未来会出现更多有意思的特性但核心的数据结构思维和场景抽象能力是无论版本怎么升级都不会过时的基本功。
返回列表