ARTICLE DETAIL

资讯详情

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

Redis实战指南:从安装配置到数据类型、缓存三大坑与分布式锁

Redis实战指南:从安装配置到数据类型、缓存三大坑与分布式锁 Redis 这个东西很多人的学习曲线是这样的第一天下载安装第二天把所有数据类型敲一遍第三天就断了——因为实在想不明白这些命令到底用到哪里。我以前也是这样后来真正在项目里扛过一堆线上问题才逐渐把 Redis 的用法想通。今天这篇实战篇就从 redis 下载安装开始把五种基础数据类型和几个新数据类型讲透再带上持久化、淘汰策略、缓存三兄弟、分布式锁和限流这些高频场景最后送你一套排查手册。无论你是刚入门还是已经写过几行 redis-cli读完都能直接照着落地。1. 为什么 Redis 值得系统过一遍1.1 快是它的表象单线程才是它的性格Redis 给人第一印象就是快。本地跑个 SET/GET一秒钟几十万次读写毫无压力。很多人以为快是因为它在内存里这当然没错但更关键的是它的执行模型Redis 的命令处理核心是单线程的。单线程意味着不需要在多个线程之间去竞争共享数据没有锁的消耗也不用频繁切换上下文配合 Linux 的 epoll 多路复用机制单线程就能把所有网络请求管理得很好。不过要提醒一点Redis 6.0 引入了多线程 IO但那是把网络读写这类 IO 操作放到别的线程去做命令本身的执行依然是单线程。也就是说你的命令一定是串行执行的这反而带来了一个隐藏福利——单个命令天然是原子的后续讲分布式锁、限流都依赖这个特性。因为单线程最怕的就是你的某个命令特别慢把后面的所有请求都堵住。比如执行一个 KEYS *一个 O(N) 的遍历线上几百万个 key能卡到整个 Redis 几十秒没反应。这个后面排查部分我会详细说这里先记住别在生产环境敲 KEYS *。1.2 它能干什么不能干什么我把 Redis 的常见用途分成三类。第一类是缓存这也是使用面最广的。把热点数据、读多写少的数据放到 Redis 里能大幅降低数据库的压力。第二类是内存态结构数据比如排行榜需要全局排序、计数器阅读量、库存、去重集合已读用户等这些用 Redis 的数据类型表达起来特别顺手。第三类是轻量级的协调能力比如分布式锁、限流、小型延时队列很多场景不用再引入额外的中间件。但它不是万能的。Redis 本质是内存数据库数据量受内存限制成本高它的持久化能力相对数据库来说偏弱不能当唯一数据源它也不适合做复杂查询——没有表结构、没有 SQL、没有多条件联合查询别在 Redis 里做类似按时间范围和状态同时过滤的需求。一句话Redis 解决的是高速存取 特定结构运算的问题而不是强一致 复杂查询的问题。2. 先把环境跑起来下载、安装与基础配置2.1 redis 下载与安装三个平台怎么选先说 redis 下载。最正规的渠道是 Redis 官网目前主流版本是 6.x 和 7.x7.x 已经相当稳定新项目直接用 7.x 就行。装之前可以先看下系统下面是我用过的几种方式。Linux最推荐用包管理器最省事。Ubuntu/Debian 上大概是 apt install redis-serverCentOS 上用 yum install redis。装完先用 redis-server --version 看一眼版本如果包管理器里的版本太老可以去官网下载源码编译步骤无非是 make make install前提是装了 gcc。我们自己线上环境有几年都是源码编译好处是版本可控坏处是要自己处理依赖。macOSbrew install redis 一条命令装好后 brew services start redis 就能开机自启。临时用的话直接 redis-server 前台跑起来开另一个终端 redis-cli 就行。Windows这是个尴尬话题。Redis 官方不提供 Windows 版本以前微软维护过一个分支版本已经停止维护现在 Windows 上要跑 Redis要么用 WSL 在 Linux 子系统里装要么用 Docker 跑容器镜像。我测试环境里就是 docker run -d -p 6379:6379 --name redis-test redis:7。注意 Windows 上那些第三方编译版不要乱下很多版本陈旧不说安全上也不放心。2.2 配置文件里必须改掉的默认设置Redis 默认配置只适合本机玩上生产前这几个参数必须改。配置文件一般在 /etc/redis/redis.conf如果你用源码编译启动可能会在安装目录的 redis.conf 里。bind默认 127.0.0.1只监听本机。线上要让别的机器访问把 bind 0.0.0.0 或具体内网 IP 写进去。但这就带来安全风险必须配 requirepass。requirepass设置访问密码。很多人觉得内网环境无所谓其实 Redis 历史上出现过不少被入侵的事件一个重要原因就是裸奔加这个配置问题。设置 requirepass 123456 就够用了生产环境建议用一个强度足够的密码。protected-mode默认是 yes它会在你没设密码且没改 bind 的情况下拒绝远程访问。千万别为了省事改成 no除非你有完整的网络隔离方案。maxmemory不设默认是无限用内存打满系统会 OOM直接威胁到操作系统。上线前必须设定比如 maxmemory 2gb再配合下面的淘汰策略。daemonize默认 no前台运行。无所谓如果用 systemd 管理服务这个参数反而不重要。注意如果你用 Docker 跑 Redis配置文件可以用 -v 挂载进去或者直接通过启动命令加参数覆盖比如 docker run --memory 本身也是一种兜底。2.3 启动、连接与第一组命令启动方式取决于安装方式。Linux 下 systemctl start redis-server 是常见姿势源码编译的话直接 redis-server --daemonize yes 后台启动。启动后用一个命令验证redis-cli -a yourpassword ping返回 PONG 就说明服务活着。再跑一组简单命令感受一下工作流redis-cli SET user:1001:name 张三 GET user:1001:name EXPIRE user:1001:name 3600 TTL user:1001:name DBSIZE这套命令是 Redis 最基础的。SET 没有 key 就创建有 key 就覆盖EXPIRE 给 key 设置过期时间TTL 返回剩余秒数-1 表示永久不过期-2 表示 key 不存在。建议大家一开始就养成带过期时间的习惯避免缓存数据越长越陈旧甚至把 Redis 当成永久存储用。3. 数据类型不是背出来的是场景练出来的3.1 String不只是缓存值String 是 Redis 最基础的数据类型JSON 字符串、数字、二进制数据都能存。最大上限 512MB日常根本用不到。除了存缓存它另外两个高频用法一个是计数器一个是临时状态。计数器INCR 命令对 key 做原子加一应用场景多得吓人。文章阅读量、点赞数、库存扣减都可以直接用 INCR/DECR 来做避免了读-改-写的并发问题。库存场景尤其典型每次扣减用 DECR如果返回值小于 0 就说明超卖再去回滚这一个命令顶掉一堆 if 判断。缺点是这个数字只存在于内存需要定期同步到数据库防止进程崩溃丢失。临时状态比如限流的固定窗口用 INCR EXPIRE分布式锁的加锁用 SET NX EX幂等控制用 SETNX 记录一个 token 并判断是否成功。这都是 String 的天花板级应用后面专门展开。3.2 Hash 与 List对象归属和顺序型数据Hash 我愿称之为对象映射一个 key 下面可以挂多个 field-value。存用户信息就比 String 存整段 JSON 灵活想改一个字段只 HSET 一个 field不用整个读出来重新序列化。典型例子是购物车——用户维度一个 hash商品ID 是 field数量是 value增删改查都是 O(1)。List 是双向链表可以理解为消息队列的最简单实现。LPUSH 往左塞RPOP 从右弹FIFO 就出来了。进阶用法是 BRPOP设置阻塞时间没有数据就等着相当于自带阻塞消费的队列。不过这种简易队列没有 ACK 机制消费者处理失败消息就丢了只适合对丢消息容忍度高的场景比如日志采集的缓冲。消息可靠性要求高的还是得上专业的 MQ或者用后面的 Stream。3.3 Set 与 ZSet集合运算和排序武器Set 的看家本领是去重和集合运算。SADD 加成员SCARD 取数量SISMEMBER 判断是否在集合里。举几个场景一个用户抽奖每个 user_pool:activity:1024 集合保证不会重复中奖标签系统用 SINTER 取两个集合的交集就能得到既喜欢技术又喜欢篮球的用户。ZSet 是 Redis 里最聪明的成员每个 value 带一个 score靠 score 排序。最经典场景就是排行榜ZINCRBY leaderboard:2024 5 user_1001给用户加分再 ZREVRANGE 取前 N 名。另一个很好用的场景是延时队列score 存执行时间戳业务轮询用 ZRANGEBYSCORE 取出到期的任务处理后 ZREM 删除。它几乎就是一个小型任务队列。我用表格总结一下五种基础类型各自适合什么方便对号入座类型底层结构典型命令适合场景String动态字符串SET/GET/INCR缓存、计数器、限流、分布式锁Hash哈希表HSET/HGET/HINCRBY对象属性、购物车、字段级修改List双向链表LPUSH/RPOP/BRPOP消息队列、最新列表、日志缓冲Set哈希集合SADD/SINTER/SISMEMBER去重、集合运算、抽奖用户池ZSet跳表哈希表ZADD/ZRANGEBYSCORE/ZREVRANK排行榜、延时队列、带权重排序3.4 Bitmap、HyperLogLog、GEO 与 Stream四个新面孔这些是因为场景而生的类型。Bitmap 本质是 String 上做位运算用来做签到最合适一个用户一年 365 天就 365 个 bit一天一个 bit统计连续签到用 BITCOUNT、BITFIELD 就能算比存 365 个日期字符串省太多内存。HyperLogLog 是 UV 神器PFADD 把所有用户 ID 扔进去PFCOUNT 直接给近似计数标准误差 0.81%但内存固定几 KB在千万级 UV 场景里成本几乎为零代价是不支持精确统计和不能删除元素。GEO 就是地理位置存储附近的人、门店距离排序这种需求直接 GEOADD 坐标GEO 系列命令就能搞定不用再用地理算法自己算。Stream 是后来加入的消息队列形态支持消费者组、消息确认、回溯比 List 队列可靠得多小规模消息场景可以替代 Kafka 的一部分工作。4. 持久化、过期和淘汰策略数据安全的三道闸4.1 RDB 和 AOF怎么选才是成年人做法Redis 是内存数据库进程一死内存清空所以持久化是个躲不开的问题。两种方式一种是 RDB快照把某个时间点的全量数据压缩落地成一个 .rdb 文件相当于拍照另一种是 AOF追加文件每收到一个写命令就往文件里追加一条相当于录像。RDB 恢复快、文件紧凑但两次快照之间数据会丢AOF 丢数据更少甚至可以根据配置刷盘频率牺牲性能来接近零丢失但文件体积越来越大恢复也比较慢。所以线上最标准的思路是 AOF RDB 都开RDB 做定期全量备份再配合 AOF 记录增量同时打开 Redis 的混合持久化配置——AOF 重写时把已有的历史数据生成 RDB 格式放在文件头部后面再追加增量命令。这样重启时加载快丢失范围又小两全其美。具体配置一般这样save 900 1 save 300 10 save 60 10000 appendonly yes appendfsync everysec aof-use-rdb-preamble yes注意fork 子进程做 RDB 或 AOF 重写时如果内存特别大fork 本身可能带来毫秒到秒级的停顿。这就是为什么大内存实例要谨慎、别让 Redis 装上几十个 G 的数据还不考虑容量规划。4.2 过期删除与内存淘汰别等打满了才去配过期 key 的删除不是到期就立刻消失Redis 用的是惰性删除加定期删除的组合拳。惰性删除是访问到这个 key 时才判断是否过期定期删除是每隔一段时间抽一批 key 检查。这样设计是为了避免用一个全局定时器去扫全量 key那在几百万 key 下性能会爆炸。副作用就是过期 key 可能占着内存没有立即回收这是正常现象不用慌。如果数据量太大、新写入把内存逼到 maxmemory 红线就轮到淘汰策略出手。默认 noeviction内存满了直接拒绝写入这在线上非常容易导致各种报错。常见的是 allkeys-lru按 LRU 近似算法淘汰最久没用过的 key如果不希望缓存被打穿重要数据还有 volatile-lru 这种只淘汰设置了过期时间的数据。Redis 4.0 之后还提供了 LFU 策略更适合访问频率不同的业务。一般线上缓存场景我习惯用 allkeys-lru因为它不关心你有没有设置过期只关心谁最久没被碰。但如果你有明确的不能丢的数据和可以丢的数据之分就设置不同 key 的过期时间再用 volatile-lru 保底这样能进一步保护那些没有设置 TTL 的关键数据。策略淘汰范围适用场景注意点noeviction不淘汰绝不能丢写的场景内存满直接报错体验差allkeys-lru全体 key纯缓存可容忍任意 key 丢失实现简单最常用volatile-lru设置了 TTL 的 key缓存持久数据混合无 TTL 数据不会误删allkeys-lfu / volatile-lfu全体 / 过期的 key访问频率差异大的业务内存换频次适合热点集中5. 缓存三大坑穿透、击穿、雪崩5.1 缓存穿透查了一个不存在的东西穿透说的是请求查一个缓存和数据库里都不存在的数据。比如拿一个不存在的商品 ID 反复请求缓存里没有数据库里也没有每次都打库流量大的时候数据库直接崩。处理思路有三个方向。第一是接口层做参数校验非法 ID 直接拦截第二是缓存空值查询数据库返回 null 就把一个空值或标志位写进缓存设置比较短的 TTL比如 60 秒这样后续同样的请求命中的就是空缓存第三是布隆过滤器把所有合法 ID 存到过滤器里查之前先问过滤器过滤器说没有就直接返回极大减小无意义查询。布隆过滤器本身也是一段位图有误判率但非常低适合数据集合相对稳定的场景。5.2 缓存击穿热点 Key 突然过期击穿针对的是一个热点 key 过期瞬间大量请求同时打到数据库。比如某个爆款商品缓存过期的一瞬间一千个请求同时来读数据库瞬间被打穿。解决办法最常用的有两个互斥锁和逻辑过期。互斥锁的做法是重建缓存的代码里先尝试加锁可以用 SETNX只有一个线程能抢到锁去查库重建缓存其余线程短暂等待后从缓存里拿到数据。逻辑过期更巧妙——缓存里不设置 TTL放一个过期时间戳字段读的时候发现逻辑上过期了但不能直接更新而是尝试获取互斥锁抢到的人去后台异步重建其他人先返回旧数据。这样能保证在高并发下谁也不崩代价是短暂的数据不一致。这两个方案我都用过。互斥锁实现简单但会有一小段时间其他线程阻塞等待逻辑过期更适合追求吞吐的读多写少场景。如果你的项目用了 Redisson那直接用它的 Lock 就行不用自己造轮子。5.3 缓存雪崩大面积同时失效雪崩比击穿更恐怖是一大批 key 在同一时间集体过期比如很多数据的缓存都设置了相同的午夜过期时间或者 Redis 整个集群挂了数据库压力瞬间爆炸。应对三板斧第一过期时间加随机值让失效时间在区间内错开比如 TTL 是基础时间 60 分钟加随机 0 到 5 分钟这是最便宜也最常用的手法第二缓存预热高峰前提前把热点数据写进缓存降低冷启动压力第三做多级缓存或者限流降级真正扛不住的时候直接用网关这类组件把流量挡在数据库之外。云上的 Redis 高可用集群也能让整个集群挂掉的概率大幅降低但架构上仍然要假设集群会挂。问题触发场景核心思路常用手段穿透大量查询不存在的数据不让非法查询到达数据库参数校验、缓存空值、布隆过滤器击穿单个热点 key 过期瞬间只允许一个线程重建缓存互斥锁、逻辑过期异步重建雪崩大量 key 同时过期或集群故障打散失效时间、降低数据库压力随机 TTL、缓存预热、多级缓存、限流降级6. 走出单机分布式锁与限流实战6.1 基于 SETNX 的分布式锁这个场景几乎每个后端都会遇到。多个服务进程要互斥地执行某个动作比如扣库存、发放优惠券、分布式任务调度不能靠 synchronized因为进程不是一个。Redis 的分布式锁思路其实很朴素让某个 key 同一时刻只能被一个客户端占住。最简单的加锁命令是 SET key value NX EX seconds。NX 表示只有 key 不存在时才写入EX 设置过期时间避免客户端崩了锁永远不解。value 最好是每个请求唯一的随机值比如 UUID这样释放锁的时候用 Lua 脚本判断一下 value 是不是自己的是才删除if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end没有这个判断会出大问题客户端 A 的锁超时被自动释放客户端 B 拿到锁结果 A 处理完一顿 DEL把 B 的锁删了互斥直接失效。用随机 value 判断之后再删就避免了这种误删。锁超时本身是另一个坑。假设业务执行超过了锁的过期时间锁自动释放别人就能进来。解决办法是续约Redisson 的看门狗机制就会自动帮你续期每隔一段时间检查锁是否在在就延长过期时间。但我要泼盆冷水单机 Redis 的分布式锁在最极端情况下主节点挂了数据还没同步到从节点依然可能失去互斥性所以 RedLock 那个复杂的多节点方案也有人质疑。如果你做的是金融级别的强一致场景建议考虑专门的一致性协调组件如果只是业务幂等、防并发重复执行的场景Redis 锁完全够用搞清楚原理别过度设计。6.2 Redis 限流固定窗口、滑动窗口与令牌桶限流做起来比想象中简单。最基础的固定窗口每个窗口一个 keyINCR 计数首次访问时 EXPIRE 设置窗口长度计数超过阈值就拒绝。它的缺陷是两个窗口临界点可能钻空子——前一个窗口最后 1 秒和下一个窗口最开始 1 秒都能各打 100 次实际 2 秒内打了 200 次超过限流值。滑动窗口更严谨用 ZSetscore 存时间戳每个请求 ZADD 一个 member可以用 UUID然后 ZREMRANGEBYSCORE 把窗口起点之前的数据清掉ZCARD 取当前窗口数量。这样无论请求落在哪个时间点统计的都是真正的滑动窗口。代价是 ZSet 会存储很多请求记录内存消耗比固定窗口大。令牌桶则适合控制突发 均匀的流量每秒钟往桶里放一定数量的 token请求来了取 token没有就排队或拒绝。Redis 实现令牌桶要用到 Lua 脚本判断上次放 token 的时间和当前时间差算出新增 token 数量再判断能不能取。这套逻辑写在 Lua 里能保证原子性是我推荐的实现方式。7. 日常故障排查速查手册7.1 连接被人打满先看这几处现象是应用报 too many connections 或者 Cannot connect。每个 Redis 有默认 maxclients默认 10000但真实瓶颈经常是文件描述符上限。这时候先看 INFO stats查 connected_clients 有多少再看是不是应用连接池没配置好比如每台机器开了几千个长连接却没复用。解决方案包括调大 maxclients、调大系统 fs.file-max 和 ulimit -n、代码里该关的连接及时归还连接池、排查客户端进程是否有大量闲置连接。有一次我就碰见某个服务的 Redis 客户端默认的 idle 超时设成了 0导致连接永远不回收几分钟把连接池耗尽改成 60 秒后立竿见影。7.2 大 Key 和热 Key 的排查三板斧大 key 指单个 key 的数据量特别大比如一个 List 存了百万条消息一个 Hash 存了千万个字段。危害有很多操作它本身耗时网络传输把带宽打满甚至触发阻塞。排查办法是 redis-cli 的 --bigkeys 扫描它会遍历所有 key 并按类型统计最大的几个更进一步用 INFO memory 看内存分布或者用 MEMORY USAGE key 查看单 key 占用。热 key 是某个 key 被疯狂读写比如双十一的爆款库存。单实例每秒处理能力再高也扛不住几十万 QPS 压同一个 key。解决热 key 的手段常见有三种给 key 加随机后缀把读写分散到 N 个副本 key或者做本地缓存把热点数据放到 JVM 或本地进程里注意一致性再或者用读写分离把流量分摊到多个副本。热 key 的定位可以用 redis-cli 的 --hotkeys 参数前提是开启了相应配置且版本支持实际操作中我觉得用 bigkeys 扫描再结合业务日志判断更直接。redis-cli --bigkeys redis-cli --hotkeys redis-cli MEMORY USAGE user:10017.3 慢查询与内存碎片的处理慢查询是 Redis 线上最重要的健康指标。Redis 会把执行时间超过阈值的命令记录到慢查询日志配置项是 slowlog-log-slower-than微秒和 slowlog-max-len。排查时先看redis-cli SLOWLOG GET 20 SLOWLOG LEN一旦发现大量慢命令重点看是不是我在前面说的 KEYS *、SMEMBERS 大集合、ZRANGEBYSCORE 大范围这几种 O(N) 操作或者是不是有大 key。优化手段无非是拆分 key、用 HSCAN/SCAN 分批遍历代替全量命令、缩短序列化大小几类。内存碎片则是另一类隐蔽问题。频繁的写删会让内存碎片率上升比如 mem_fragmentation_ratio 大于 1.5说明申请的内存远远大于实际数据白白浪费。这时候先看 INFO memory碎片率高通常推荐重启节点要配合集群方案或使用 MEMORY PURGE 手动整理。但重启前一定要确保数据持久化已经做好不然分分钟学会数据全丢。这里还要提醒碎片率高不一定全是问题如果 ratio 长期小于 1反而说明系统内存分配可能有问题需要具体看。现象可能原因排查命令解决手段Cannot connect / too many connectionsmaxclients 打满、连接未回收INFO stats调大限制、修复连接池 idle 配置单命令长时间阻塞大 key、KEYS * 全量扫描SLOWLOG GET、--bigkeys拆 key、SCAN 分批遍历内存占用远大于数据量碎片率过高INFO memory、MEMORY USAGEMEMORY PURGE、重启节点缓存命中率骤降热点 key 过期、大量 key 集中失效INFO stats 的 hit_rate随机 TTL、预热、逻辑过期7.4 我自己踩坑之后的几条习惯讲几个我反复用到的实操习惯。第一新项目上线前就把 maxmemory 和淘汰策略配好不然内存打满了才发现线上报错一片第二所有 key 命名尽量统一带业务模块前缀和冒号比如 order:pay:2024:1001后期用 SCAN 扫描、定位归属都方便第三写命令的时候想一想这命令是不是 O(N)不是特别需要就别用全量操作第四监控永远要大于人肉排查提前把慢查询日志、info 指标输出到监控告警比出了事再去看 redis-cli 强得多。8. 最后分享一个小技巧这个技巧我在好几个项目里都用上了给 Redis 缓存的 key 加版本号。比如用户配置缓存key 叫 config:user:1001当配置变更时不要直接更新缓存内容而是把 key 改成 config:user:1001:v2自然通过 MISS 去重新加载。这样做的好处是旧数据在客户端继续持有到过期不会因为更新缓存过程中的并发写问题把数据搞脏同时你还能自然获得缓存版本管理能力发布新版本时只需要把 v1 的 key 批量失效。这个思路本质上是把缓存和源数据的一致性问题转化为版本替换问题实测下来比直接 set 省心很多。Redis 的学习说到底是场景驱动的像后缀加版本、用 Lua 保证原子性、给 key 加随机 TTL 这种事没有踩过坑很难意识到它的价值。希望这篇实战篇能帮你少走一段弯路把 Redis 真正变成一把趁手的工具。
返回列表