ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Redis学习从入门到实战:数据结构、持久化、分布式锁与集群踩坑全记录

Redis学习从入门到实战:数据结构、持久化、分布式锁与集群踩坑全记录 Redis学了这段时间从最开始只会set/get到后面慢慢啃数据结构、持久化、分布式锁、集群部署中间踩了不少坑也补了不少知识点。这篇日志不是保姆级教程也不打算一次讲透所有命令而是把我从入门到能独立部署、排查问题的整个学习路径记录下来每个阶段遇到什么问题、怎么解决的、参考资料怎么选、还有哪些坑是常规文档里不会写但我实际撞上的都会尽量交代清楚。如果你正在学Redis或者刚接触但不知道怎么往前推进这份日志可以帮你少走点弯路。1. 环境准备不同安装方式的取舍与验证1.1 为什么先从单机版开始而不是直接上集群很多初学者一上来就想搭集群觉得那才叫生产环境。我的建议是先单机、再主从、然后哨兵或Cluster这个顺序不要跳。原因很简单Redis的很多核心机制持久化、复制、过期策略都是建立在单机实例之上的如果单机的基本行为你都不清楚遇到集群里的诡异现象比如主从切换后丢数据、槽位迁移期间请求失败会完全摸不着头脑。单机版的学习目标是搞清楚三件事Redis是怎么跑起来的、数据是怎么落的盘或没落盘、客户端是怎么连上来的。只要这三件事在你脑子里形成了清晰的回路后面再聊集群就轻松多了。我自己在本地机器上把三种装法都试了一遍macOS上用Homebrew装的系统级RedisWindows机器上用解压版跑起来过另外也通过Docker容器跑过带持久化和密码的实例。三种方式各有适用场景但学习阶段我强烈推荐前两者——因为你能更直接地感知进程、配置文件和数据文件。1.2 macOS和Windows安装的实操细节macOS上安装是最省心的brew install redis redis-server --version brew services start redis装完以后配置文件在/opt/homebrew/etc/redis.confApple Silicon芯片路径默认端口是6379。用brew services start会把Redis注册为后台服务开机自启适合日常开发用如果只是临时启动一个实例验证功能直接跑redis-server就好连配置文件都能省会用默认配置启动一个前台进程CtrlC就能停。Windows上就不能走官方源码编译那套了因为Redis官方其实不提供Windows原生版本。目前主流做法是去GitHub上找第三方维护的Windows移植版比如tporadowski/redis这个仓库下的release包下下来解压就能用。目录里一般直接有redis-server.exe、redis-cli.exe双击启动或者在命令行里启动都行。注意Windows解压版默认不注册为服务。想开机自启的话可以用sc create手动注册成Windows服务或者丢到任务计划程序里。我当时用sc create Redis binPath C:\redis\redis-server.exe --service-install折腾了一阵后来发现直接用项目自带的redis-server --service-install更省事安装完服务以后用redis-server --service-start启动--service-stop停止。而Docker方式会更接近生产环境的样子docker run -d --name redis-learn \ -p 6379:6379 \ -v $(pwd)/redis-data:/data \ -v $(pwd)/redis.conf:/etc/redis/redis.conf \ redis:7.2 redis-server /etc/redis/redis.conf这里有个细节很容易被忽视如果不把/data挂载出来容器一删你写入的数据全没了。Redis容器镜像默认把数据目录放在容器内部的/data持久化文件RDB和AOF都会写到那里所以一定记得挂载。另外配置文件挂载进去以后要注意容器里的Redis默认是否开启了daemonize no——这和宿主机不一样容器内Redis必须是前台进程不然容器会直接退出。1.3 启动后第一件事用好redis-cli和可视化工具装完Redis第一件事不是配密码而是先用redis-cli跑通几个基础命令确认服务正常redis-cli ping # 返回 PONG 就说明服务起来了 redis-cli set hello world redis-cli get hello命令行能通说明内核是好的。接下来我建议再装一个GUI工具辅助日常查看毕竟生产环境排查问题的时候redis-cli配合monitor命令永远是终极手段但日常开发调试时一个直观的界面能省很多时间。我目前常用的可视化工具有三个Redis官方出的RedisInsight功能全支持集群管理和分析、Another Redis Desktop Manager跨平台界面清爽支持SSH隧道和哨兵模式、以及开源的Redis Desktop Manager老牌但后续维护没前两者活跃。我的使用习惯是日常看key列表用ARDM分析内存和慢查询用RedisInsight写脚本、做压测、精细排查问题则回到redis-cli。2. 数据结构与命令从能存到会用的关键跨越2.1 五种基本数据类型的真实应用场景Redis的String、List、Hash、Set、ZSet这五种数据结构官方的文档非常清晰但知道每种结构能存什么和知道每种结构该在什么场景下用是完全两回事。我的学习方式是给每个结构找一个现实场景去对号入座String最基础也最常用。除了缓存用户信息、存JSON外真正的杀手锏是自增自减操作。做个计数器、限流器、分布式生成唯一ID都用INCR/DECR。计数器场景要特别注意INCR是原子操作高并发下不会出现并发覆盖的问题这一点比MySQL里的UPDATE count count 1在高并发下要好处理得多。List底层是链表可以从头部或尾部push、pop。典型的应用是做简单的任务队列一个进程从左边塞任务多个消费者从右边阻塞取任务BRPOP。需要注意不要把Redis的List当成Kafka、RocketMQ这种专业MQ来用它没有acker机制、没有消息轨迹、没有延时消息只适合不要求强可靠性的轻量场景。Hash适合存一个对象的多个字段。比如用户表记录id、name、email用HSET/HGETALL比把整个对象序列化成一个字符串存进String更高效可以只更新其中一个字段不用整个对象读出来再反序列化再写回去。Set无序不重复的集合。最经典的三个场景去重比如判断用户是否已经参与活动、抽奖SRANDMEMBER随机获取成员、共同好友/共同关注SINTER求交集。ZSet有序集合每个成员带一个分数。排行榜是它的主场比如游戏分数排名、热门文章榜。也可以用score存时间戳当作带过期时间的定时任务去用虽然Redis官方不推荐在队列场景这么玩但中小项目里偶尔可以当轻量级延时队列。上述五种结构里我实际项目中用到最多的是String和Hash其次是ZSet。List用得少因为一般需要考虑可靠性的队列都会选正经MQ。Set则主要用在去重和鉴权场景。2.2 高频命令背后的几个关键机制学命令不能死记硬背要看它背后的机制。我很长一段时间都不理解为什么有人会在生产环境用KEYS命令——直到有一次我在一个只有几万key的开发环境里执行了一次KEYS *卡了一瞬间我才意识到问题的严重性。KEYS会遍历整个键空间阻塞Redis单线程如果key上了百万级线上服务直接原地卡死。所以生产环境查key一律用SCAN它通过游标的方式分批遍历不会长时间阻塞。另一个容易被忽略但极其重要的命令是EXPIRE以及它的缩写SETEX。给key设置过期时间是Redis作为缓存的核心能力之一没有过期时间的话内存会被写满然后触发内存淘汰策略maxmemory-policy。默认策略是noeviction内存满了以后写入直接报错——很多人调试时遇到OOM command not allowed when used memory就是吃了这个亏。序列化选择这块也要提一下。用RedisTemplate操作Redis时如果直接用默认的JdkSerializationRedisSerializer存进去的key会在Redis里变成一串二进制乱码redis-cli里没法直接看到明文的key。这会导致一个很尴尬的局面你用Java写进去的数据用命令行查的时候像天书一样。我的建议是统一用StringRedisSerializer做key序列化value用GenericJackson2JsonRedisSerializer或FastJson2JsonRedisSerializer至少保证可视化工具里能看到可读的结构。当然如果追求极致性能并且不介意可读性用Protobuf也是很多大厂的选择——但学习阶段别整这些先保证你能看清自己存进去的数据。2.3 可视化工具对比到底该常驻哪个前面提到了几款工具我按场景细说一下工具优势劣势建议场景RedisInsight官方出品、支持内存分析、慢日志、集群拓扑、支持自定义命令调试界面稍重、有版本迭代频繁的困扰分析复杂问题、内存排查、集群管理Another Redis Desktop Manager轻量、跨端、支持SSH隧道和哨兵连接部分深度分析功能不如官方日常开发调试、多环境切换redis-cli最稳定、最可靠、没有UI需要记命令、看不了报表任何时候的最终兜底我的结论是GUI工具只辅助查看绝不能替代命令行。排查问题的时候命令行提供的能力和可控感是GUI工具给不了的。比如要看一个key的内部编码方式OBJECT ENCODING key这一条命令就能直接把int、embstr、raw的底层结构给看出来很直观。3. 进阶主题持久化、分布式锁与缓存穿透治理3.1 持久化机制RDB和AOF到底怎么选Redis在内存中操作数据但只要进程重启内存里的数据会全部消失。要想不丢数据就必须借助持久化。Redis提供了两种机制RDB快照把某个时间点的全量数据写入一个二进制文件dump.rdb。优点是文件小、恢复速度快、适合跨机器迁移缺点是在两次快照之间的数据会丢失而且生成快照的过程尤其是save命令阻塞式触发可能造成服务停顿。RDB触发的默认条件是900秒内1次变更、300秒内10次变更、60秒内10000次变更满足任一就会在后台生成快照。AOF追加文件把每次写操作的命令追加到日志文件里。优点是数据安全性高可以把数据丢失窗口缩到1秒甚至更小缺点是AOF文件通常比RDB大重启恢复时逐条重放命令耗时更长。Redis 4.0之后提供了混合持久化也就是RDB做全量快照增量操作写入AOF尾部兼顾了重启速度和数据完整性。我个人在生产环境里的经验是如果数据允许丢失几十秒开RDB就够如果数据不能丢比如秒杀库存必须开AOF并且把appendfsync设置为everysec——默认配置就是1秒刷盘一次性能和安全的折中方案很少有人用always因为性能损耗太大用never又等于自杀式放弃安全。这里有一个文档里不太强调但很重要的细节appendfsync everysec并不是绝对不丢数据。操作系统把数据写入文件系统缓冲区再异步刷盘如果这时候机器断电或进程崩溃最后1秒的数据还是有丢的可能。所以如果业务对数据要求到了丢一秒都不能接受的程度那需要考虑的就不只是Redis配置了而是跨机房的实时同步方案比如主从加多副本。3.2 分布式锁SET NX EX的正确姿势与常见误用分布式锁是面试必考、实战必踩的一个点。最基础的实现是先SETNX再EXPIRE两步走——这其实是个错误示范因为SETNX成功和EXPIRE设置过期时间之间如果进程崩了锁永远不会释放其他线程就会被卡死。正确做法是用一条原子命令完成加锁和过期时间设置SET lock_token unique_value NX EX 30NX表示只有key不存在时才设置成功EX 30表示30秒自动过期。释放锁用Lua脚本保证原子性先校验value是不是自己的再删除避免误删别人的锁if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这个Lua判断的value是什么加锁时生成一个唯一ID比如UUID释放时拿这个ID比对只有是自己加的锁才删得动。否则一个线程的锁超时自动过期之后另一个线程加锁成功第一个线程却把别人的锁删了锁就形同虚设。更进一步的坑是锁的续期业务执行时间超过了锁的过期时间锁自动释放其他线程趁虚而入。解决办法是开启Redisson的看门狗机制watchdog它默认会每过锁有效期的三分之一时间默认10秒/3≈3.3秒自动续期直到业务执行完主动释放。这个机制确实好用但要清楚它带来的风险如果Redis主节点宕机锁可能因为主从异步复制而丢失导致锁失效。RedLock算法试图解决这个问题但它在实际生产环境里也有争议很多大厂是直接放弃RedLock、改用第三方协调服务比如ZooKeeper来做强一致性锁的。我的建议是绝大多数业务场景用Redisson的看门狗就够了别为那极小概率的故障场景引入一套复杂度爆炸的方案。3.3 缓存穿透、击穿与雪崩三个容易混淆的治理方案这三个名词经常被混在一起但实际上是完全不同的三个故障形态治理手段也完全不同缓存穿透查询一个根本不存在的数据缓存里没有数据库里也没有每次请求都直接打到数据库。恶意攻击或大量无效请求会瞬间把数据库压垮。治理方式有三种一是把空值也缓存起来比如NULL存一份过期时间短一点比如30~60秒二是用布隆过滤器在缓存层前置拦截不存在的数据直接返回空不再向下查三是参数合法性校验挡掉明显不合理的请求。我实际用过布隆过滤器注意它存在误判率——可以接受把存在的数据误判为不存在这种小概率事件吗不行因为那会造成真实数据的漏查。所以布隆过滤器在Redis缓存场景的定位是拦截掉绝大多数不存在的key而不是过滤所有不存在的key。缓存击穿一个热点key在缓存过期的那一瞬间涌入大量请求同时打到数据库把一个key变成单点故障。治理方式一是互斥锁缓存重建时只允许一个线程去查数据库其他线程自旋等待二是逻辑过期时间在value里额外存一个逻辑过期时间单独线程负责异步重建缓存用户读到旧数据但上层几乎无感知。互斥锁的实现要小心死锁和重建线程的崩溃我更喜欢逻辑过期方案因为它在高并发下性能更好。缓存雪崩大量key在同一时间集体过期导致所有请求都压到数据库。这往往是设置了统一的过期时间比如所有数据都固定缓存1小时引起的。治理方式简单粗暴给过期时间加一个随机值比如1小时±10分钟让过期时间错开极端情况下还可以做主从组合主库扛写、从库扛读配合限流和降级兜底。4. 集群与高可用主从、哨兵、Cluster的演进路径4.1 主从复制从单机走向高可用的第一步一台Redis实例宕机对业务的影响是致命的所以需要多台实例冗余主从复制就是最基本的冗余手段。一个主节点master可以挂多个从节点slave从节点实时同步主节点的数据。写操作只在主节点执行读操作可以分流到从节点这样既提高了可用性又分担了读压力。主从同步的核心机制是从节点启动时向主节点发送PSYNC命令主节点保存当前数据快照RDB发给从节点同时把快照生成期间的新写操作记录在缓冲区里快照传完后把缓冲区增量操作再发给从节点之后就是持续不断的增量同步了。这个过程在早期版本里是SYNC每次断线重连都要全量同步2.8版本引入PSYNC之后才支持断点续传极大降低了网络抖动时的数据同步开销。# 在从节点上执行或者写在redis.conf里 replicaof 192.168.1.10 6379这里有个细节主从复制默认是异步的从节点的数据存在最多秒级延迟。如果业务要求强一致读比如刚写入立刻要能读到不能靠从节点读必须读主节点或者引入读写分离时必须考虑这个延迟。4.2 哨兵机制自动故障转移的核心有了主从还需要解决主节点挂了谁来接管的问题。Redis Sentinel哨兵就是干这个的它独立于Redis实例运行持续监控主节点和从节点的状态当主节点挂了哨兵集群会选举出一个哨兵作为Leader从存活从节点里选一个升级为新的主节点并修改其他从节点的配置让它们指向新的主节点。业务客户端也要通过哨兵获取当前主节点地址如果主节点切换了客户端自动连到新的主节点整个过程对外几乎无感知。Sentinel的部署要求是高可用本身要冗余——哨兵至少部署3个实例因为它的故障转移需要多数派投票quorum。我见过有人只部署1个哨兵节点哨兵自己挂了就无人监控这样其实跟没有高可用没区别属于典型的自己骗自己。4.3 Redis Cluster真正意义上的横向扩展哨兵解决了高可用但单节点的内存和吞吐量仍然有上限。Redis Cluster把一个数据集分成16384个哈希槽通过CRC16(key) % 16384计算出每条key应该落在哪个槽然后由不同的节点负责不同的槽区间。这样数据分散在多节点上写压力、内存容量、读吞吐都能水平扩展。Cluster模式有几个和单机模式完全不同的行为学习的时候要先有心理准备只有0号数据库可用集群模式下多个数据库select 1/2/3是不存在的。某些多key操作例如MGET只有当所有key在同一个槽时才支持跨槽操作要么用Hash Tag把key里用{}包起来的那一部分作为哈希计算对象要么业务上尽量避免跨key事务。节点故障时该节点负责的槽区间会变为不可用状态直到从节点完成升级接管。这期间相关key的读写会直接报CLUSTERDOWN。我实际用Docker搭过一套三主三从的Cluster最推荐用--cluster-enabled yes开启集群然后用redis-cli --cluster create一键完成槽分配。工具会提示你如何分配主从节点关系不用手算槽位。但是真正容易踩坑的不是搭建而是容器网络——如果你用Docker Compose搭集群节点之间会通过容器内部的hostname互通这时候必须给节点配置--cluster-announce-ip、--cluster-announce-port不然客户端通过外部IP连上来时会拿不到正确的节点映射关系报MOVED错误的概率极高。5. 踩坑记录几个真实问题的完整排查链路5.1 Redis command timed outLettuce客户端的隐性问题有一次线上环境里突然报出很多Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException。第一反应是Redis服务出问题了但登上去看redis-cli ping秒回CPU和内存都很正常。查了半天才发现问题在客户端侧。Lettuce默认的连接超时是60秒但这个超时指的是命令发出后等待响应的时间并不是连接建立的超时。在并发量高的情况下如果所有线程共用同一个连接池Lettuce默认使用单个连接连接数较少大量请求会在连接池处排队一旦排队时间超过等待响应时间就会报command timed out。排查链路大致是先看Redis实例自身负载redis-cli --latency观察延迟是否正常INFO命令看connected_clients确认服务端没有异常。再看慢日志SLOWLOG GET 20如果服务端有慢命令比如大key的KEYS、HGETALL大Hash客户端会集体等它就难免超时。最后查客户端配置Spring Boot 2.x默认使用Lettuce它的默认连接池大小是无限的其实不是——默认每个连接最多可并发请求数受spring.redis.lettuce.pool.max-active影响。当并发不够、线程排队时超时就是在客户端这一侧发生的。解决方案分几路调大max-active和max-wait把超时时间从默认值60秒调到一个业务能接受的合理值比如3秒或者把某些大key拆小、禁止KEYS等慢命令。这个问题的本质不是Redis慢而是客户端侧的连接模型和配置与服务端的并发模型不匹配排查方向一开始就搞错的话会浪费大量时间。5.2 Docker部署时遇到的docker search redis返回500用docker search redis的时候报了一个request returned 500 internal server error for API route的错。这其实和Redis完全无关是Docker Desktop本身出了状况。我当时是在Windows上装了Docker Desktopdocker引擎服务没起来或者处于一个异常状态。处理方式重启Docker Desktop记得在Windows右下角托盘图标里彻底退出再启动、确认WSL2后端正常、然后清理一下Docker的缓存数据。更彻底的方式是执行docker system prune -a清掉所有无用镜像和容器数据但不建议一上来就清理因为可能会误删本地的镜像缓存。不过这个错误也提醒了我一个事尽量用官方镜像。拉Redis镜像用redis:7.2-alpine这种官方tag别用第三方个人维护的镜像后者可能存在配置隐患也难追溯安全更新。5.3 Windows上Redis开机自启的坑Windows解压版Redis不像Linux那样可以通过systemd管理默认就是一个普通进程。我在给一台Windows测试服务器配Redis自启时踩了好几次坑方案一任务计划程序启动时运行redis-server.exe。配置简单但是要注意程序路径不能带空格工作目录要设定正确不然Redis配置文件和数据文件路径都会不对。方案二注册服务。用自带的redis-server --service-install安装Windows服务再用redis-server --service-start启动。这个命令的本质是把Redis封装成Windows服务比任务计划更稳。坑点在于Windows服务默认以SYSTEM账户运行但Redis的dir参数数据文件存放目录必须对SYSTEM账户有写权限。很多人把Redis放在C盘Program Files里SYSTEM账户确实有权限但如果你想放到D盘某个自定义目录忘记赋予D盘目录写权限Redis启动后写持久化文件时就会静默失败日志里没有直接报错但重启后数据全没了。6. 学习路线与面试经验沉淀6.1 从使用到原理循序渐进的学习路线如果你要我给一条Redis学习路线我会按下面这个顺序来基础阶段安装、启动、五种数据结构、常用命令。目标是能熟练地用redis-cli操作数据知道什么场景该选什么结构。进阶阶段持久化机制、过期策略、内存淘汰策略、事务与Lua脚本、管道Pipeline。这阶段要把配置项和系统行为对应起来理解每个配置项背后的风险和收益。高可用阶段主从复制、哨兵、Cluster部署与运维。这阶段动手搭一遍就胜过看十篇文章。实践阶段结合Spring Boot等技术栈操作模板踩缓存穿透、缓存击穿、缓存一致性、分布式锁等真实的工程问题。每一阶段我都建议用一个真实需求来驱动。比如学ZSet的时候实现一个排行榜或者最近热门的文章TOP20学主从的时候搭一套读写分离。纯粹看文档学Redis坚持不了几天有场景驱动就不一样了。6.2 面试经常问的高频问题我的回答框架从面试的角度看Redis相关的核心问题其实高度集中。以下是我自己整理的一版高频题回答框架为什么快纯内存操作 单线程避免上下文切换和锁竞争 I/O多路复用底层用epoll。注意单线程指的是命令执行线程不是整个进程只有一个线程。和Memcached的区别数据结构丰富度、持久化、集群方案、复制机制。Memcached在简单KV场景里性能不输但Redis的功能性价比更高。缓存用了Redis数据库和缓存如何避免不一致核心思路是先更新数据库再删除缓存Cache Aside Pattern再配合延迟双删或者订阅数据库binlog异步删除缓存。需要接受一个事实绝对一致性在缓存场景下做不到只能做到最终一致。Redis的事务和Lua脚本的关系MULTI/EXEC的原子性有限执行阶段出错不会回滚只是不会被打断执行。真正的原子操作需要用Lua脚本Redis会在执行脚本期间阻塞其他命令。Redis做消息队列靠谱吗简单场景用List的BRPOP可以要求可靠投递和消息不丢就得用Stream的XADD/XREADGROUP和消费者组机制。但复杂场景事务消息、死信队列、消息轨迹、海量堆积建议还是交给专业MQ。面试题往往问的不是你会不会用而是你有没有在真实场景里思考过为什么这样用。比如为什么不建议在生产环境用KEYS你说一句因为它会阻塞其他命令很容易但如果你补充因为Redis是单线程执行模型KEYS要遍历全部key执行期间其他请求全部排队等待所以必须用SCAN分批处理这就体现出了对单线程模型的理解深度。7. 写在最后我的几个亲身体会回顾学习Redis这个过程我最想分享的几点体会第一一定不要跳过命令行阶段。GUI工具确实方便但生产环境排查问题的时候你能依赖的往往只有redis-cli。有一次我帮同事排查一个连接被拒绝的问题对方用GUI工具连不上换来换去试了半小时我让他打开命令行执行一下redis-cli ping十秒钟就确认了服务端根本没起来。命令行是你理解Redis的底层能力GUI只是锦上添花。第二配置改动前先看一眼INFO和CONFIG GET的结果。很多人直接改redis.conf然后重启但其实很多配置可以通过CONFIG SET动态修改不用重启也能生效比如maxmemory、appendonly。在确认在线修改的安全性之前不要无脑重启生产实例。第三也是最重要的一点——学习过程中每一个为什么都值得深挖到底。比如你配置了maxmemory-policy allkeys-lru你以为你懂了但如果你不去实际测一测内存满了之后写入会怎么走、哪些key被淘汰、淘汰策略在不同版本下的表现差异遇到线上内存打满的紧急情况时照样会手足无措。Redis的文档写得很清楚但文档没有写的是各种配置在真实压力下的表现——这些只能靠实操去体会。这篇日志还会继续更新后面我计划补充Redis 7的新特性比如Redis Function、多副本强一致读、大key和热key的治理实践、以及Cache与DB一致性方案在真实项目中的演进。如果你也有自己踩过的坑欢迎一起交流。
返回列表