ARTICLE DETAIL

资讯详情

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

Redis过期时间机制全解:从EXPIRE命令到内存淘汰实战

Redis过期时间机制全解:从EXPIRE命令到内存淘汰实战 1. 为什么你的Redis需要过期时间从一次缓存事故说起你有没有遇到过这种情况线上某个Redis key突然占了好几个G的内存明明业务上早就该丢掉的临时数据却一直赖在内存里不走。如果你还没给key设置过期时间那恭喜你迟早会踩这个坑。Redis设置过期时间简单说就是给每个key加一个生命周期倒计时时间一到Redis自动帮你把它清掉。这个机制解决的是什么问题最直接的是防止内存被无限占满其次是保证数据保鲜——比如验证码5分钟有效、登录态7天过期、缓存30分钟刷新这些场景全靠TTLTime To Live存活时间来控制。我见过太多新手把有效期写死在代码里比如存登录态的时候在value里塞一个expire_time字段每次读取再判断。不是不行但和Redis原生的过期机制比起来既绕又不安全。你这边的业务逻辑判断一旦出bugRedis内存直接爆掉用了原生的EXPIRE命令Redis自己盯着时间到点就删省心得多。这篇文章写给谁看主要是刚接触Redis、或者已经在用Redis但只会get/set的开发者。不管你是写后端接口、搞数据缓存还是维护微服务基础设施只要你的Redis里有不该永久存活的数据下面这些东西你都得吃透。我会把这套机制从命令怎么敲、原理怎么走、到生产环境怎么搭配使用一次性讲明白。2. 设置过期时间的四种核心姿势命令详解与取舍Redis设置过期时间这件事其实不止一种做法。不同场景下用不同的命令效率和安全程度差别很大。我先把最常用的几招列出来再逐个说清楚。2.1 最常用的三兄弟EXPIRE / PEXPIRE / EXPIREAT这三条命令是给已存在的key设置过期时间这个需求的标准答案。EXPIRE key seconds以秒为单位设置过期时间。这是90%场景下的首选。PEXPIRE key milliseconds以毫秒为单位。对时间精度要求极高时才用比如抢购活动中的防重复提交。EXPIREAT key timestamp设置一个绝对时间戳Unix秒级到了这个时间点就过期。适合处理定时的、一次性预热任务。用EXPIRE有个特别容易踩的坑命令执行成功只代表设置成功不代表这个key一定会在你预期的时间被删除。因为Redis对过期key的清理不是实时的后面我会专门讲删除策略。另外注意EXPIRE对不存在的key执行会返回0对没有设置过期时间的key执行同样返回1。所以你不能用EXPIRE的返回值来判断key是否存在要判断key是否存在得用EXISTS。2.2 一步到位的SETEX与SET NX EX组合如果你是设置一个新key同时给它一个过期时间这种需求就不用分两步走了。两步走的坏处是如果第一条SET成功第二条EXPIRE执行前程序崩了这个key就成了永不超时的僵尸key。安全做法是# 方式一SETEX一步设置值和过期时间 SETEX verify_code_182xxxx 300 123456 # 方式二SET NX EX同时解决不存在才设置和过期时间两个诉求 SET login_token_2024 abc123 NX EX 86400第二种方式的妙处在于NX参数保证只有key不存在时才写入EX参数直接指定过期时间。这在分布式场景下特别关键。比如实现Redis分布式锁很多人习惯用SETNX后跟EXPIRE两条命令后面我会讲到这个组合在并发下会有多致命的问题。2.3 特殊类型Hash、Set、ZSet里的字段级过期这里要泼一盆冷水Redis的过期时间是key级别的不是元素级别的。也就是说你可以给整个Hash设置过期时间但没办法给Hash里的某个field单独设置过期时间。遇到过这个需求的人都知道很痛苦一个Hash里存了用户的基本信息你只想让其中的验证错误次数5分钟后清零结果整个Hash都得跟着过一遍生死轮回。实际项目中处理这个问题的思路一般有三种拆key把需要独立过期的字段单独拎出来存成一个String类型各自设置TTL。最常见、也最干净。依赖业务逻辑在value里记录时间字段读取时自己判断。适合字段多且不希望拆Redis key的场景。用Redis 7.0的新特性从Redis 7.0开始支持了field级别的TTL设置通过HEXPIRE、HPEXPIRE等命令操作Hash字段。但我个人不推荐刚上手就依赖这个因为这种写法对客户端和运维版本都有要求老环境跑不起来而且很多业务团队根本没有升级到7.0的动力。2.4 怎么选一张表给你答案需求场景推荐命令不推荐原因新key设置值并附带过期时间SETEX / SET NX EX分步SETEXPIRE存在崩断风险已存在的key重新设置过期时间EXPIRE简单直接精度要求到毫秒级PEXPIRESECONDS精度不足指定某个绝对时刻过期EXPIREAT相对时间算不准判断key是否已经过期TTL查看剩余时间无独立是否过期命令生产环境分布式锁SET NX EXEXPIRE单独执行有原子性问题这套选型逻辑的核心就一句话能一条命令完成的绝不用两条能在服务端保证原子性的绝不依赖客户端的先后顺序。理解了这条你就不容易在Redis的操作上翻车。3. 过期时间之外Redis的删除策略与内存淘汰机制很多人以为设置了EXPIRERedis就会像闹钟一样在到期那一刻立刻把key删掉。真实情况完全不是这样。Redis对过期key的清理走的是定期删除 惰性删除的组合拳而且还有一套内存淘汰兜底。这块不搞清楚你会遇到key明明过期了还占着内存、过期key的CPU占用异常高这些诡异现象。3.1 定期删除与惰性删除为什么不直接实时删想象一下这个场景你的Redis里有10万个key都设置了不同的过期时间如果Redis每时每刻都在检查这些key是否到期CPU会被浪费在大量无意义的扫描上。所以Redis的惰性删除策略是当某个key被访问时才检查它是否已过期如果过期则立即删除。这就像你的衣柜不是每件衣服到了一年就自动飞走而是你打开衣柜准备穿它的时候发现它破了才扔掉。但惰性删除有个隐患如果一个过期key从此再也没有被访问过它就会永久占着内存。为了解决这个问题Redis又加了一个定期删除机制后台每隔一段时间默认100ms随机抽取一部分设置了过期时间的key检查并清理掉已过期的。这就是为什么设置了过期时间但不是精确到秒删除的原因——有可能你设置5秒过期实际它第6秒、第7秒才被清理掉。3.2 内存淘汰策略当过期key来不及删除时如果Redis的内存被写满了而定期删除和惰性删除还没来得及把过期key清空Redis就会被逼到内存淘汰这一步。这个机制和过期时间配合使用能达到多一层保险的效果。Redis的maxmemory-policy参数决定了内存满了之后的行为。常用的有volatile-lru从设置了过期时间的key中淘汰最近最少使用的。volatile-ttl从设置了过期时间的key中淘汰剩余时间最短的。allkeys-lru从所有key中淘汰最近最少使用的不管你有没有设置过期时间。noeviction内存满了直接拒绝写入不淘汰任何key。你会注意到volatile开头的策略只针对设置了过期时间的key这正好呼应了本文主题如果你某个业务的key没有设置过期时间它就会被排除在淘汰范围之外。换句话说设置过期时间不只是给自己的数据一个生命周期也是给Redis在内存紧张时一个可以率先牺牲的对象。3.3 持久化对过期key的影响RDB与AOF的坑这个坑很多老手都踩过Redis开启了持久化结果重启之后发现过期数据还在。解释一下RDB持久化时Redis会把未过期的key写入快照加载RDB文件时同样也会检查过期时间按理说过期key不会复活。但AOF持久化不一样它记录的是操作日志如果Redis在写入AOF时key还没有过期这条记录会被保留等到Redis重启重放AOF日志时如果超过了key的过期时间Redis会通过惰性删除再次清理。这个清理是有延迟的所以重启后你可能会短暂看到本应过期的key还在。应对方法很简单重要的缓存服务重启后做一次主动的过期检查或者直接清空一部分可以容忍冷启动的缓存别完全依赖持久化机制帮你保证过期了就一定删干净。4. 实战从单条命令到完整业务方案讲清楚了原理和命令接下来进入真正值钱的环节——这些机制在业务里应该怎么组合使用。我会分三个场景来展开分布式锁、缓存治理、统计类业务。4.1 分布式锁中的过期时间设计三步走Redis实现分布式锁最经典的写法是SET NX EX一步到位。这个写法的完整流程是SET lock_order_pay_12345 unique_value NX EX 30这条命令的含义只有当lock_order_pay_12345这个key不存在时才能设置成功并赋予30秒的过期时间。设置成功即代表获取锁成功。这里我要重点强调一下为什么不能用SETNX EXPIRE两步走因为并发环境下SETNX执行成功后程序在执行EXPIRE之前如果发生崩溃这个锁就永远不会释放其他线程会永久卡死。这就是非原子性操作在生产环境中的致命问题。而SET NX EX一条命令完成从Redis服务端层面保证了原子性这也是我反复强调的能一条命令完成绝不用两条。接下来是释放锁的场景。很多人直接DEL这是错误的。正确做法是用Lua脚本保证原子性if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这个脚本做的事是先比对value是否是本线程设置的为了避免误删别人的锁确认了才删除。如果你直接DEL可能出现一种情况线程A的锁过期了线程B拿到新锁此时线程A执行DEL把线程B的锁给误删了。加了value比对误删问题就解决了。4.2 缓存治理中的TTL设计怎么设一个刚刚好的时间缓存过期时间设置多长是个经典的收益与风险权衡问题。设得太短缓存经常失效请求频繁穿透到数据库Redis的意义被弱化。设得太久数据长期不更新用户看到的是脏数据且Redis内存压力大。我踩过最典型的一个坑之前给一个商品详情接口做缓存TTL设了1小时。结果运营改了价格用户刷新一小时还是看到旧价格投诉直接炸了。后来改成主动失效 短TTL兜底的双保险价格变更时业务代码主动DELETE对应缓存如果业务代码漏了30分钟的TTL也能保证数据自动刷新。这种设计思路是缓存治理里特别推荐的——不能只靠TTL兜底但也不能没有TTL兜底。TTL是最后一道防线而不是唯一的治理手段。给几个参考值数据类型建议TTL说明验证码5分钟太短用户来不及输入太长不安全用户登录态7天配合续期机制长时间活跃用户自动延长商品详情30分钟配合主动失效短TTL兜底搜索热词列表1小时时效适中允许一定概率的延迟分布式锁30秒配合看门狗续期防止业务未完成锁先过期4.3 批量设置与动态续期生产环境中的进阶操作生产环境里经常会遇到需要给一批key统一设置过期时间的需求。比如业务上线了一批优惠券一次性往Redis里塞了几万个key每个都要15天过期。逐个循环执行EXPIRE不是不行但如果这个循环是写在应用代码里的每次操作都要经过网络往返性能会差很多。更优雅的做法是用管道Pipeline或者Lua脚本把多条命令打包发给Redis减少RTTRound-Trip Time网络往返时间。代码级别用Spring Data Redis的话可以这样操作redisTemplate.executePipelined((RedisCallbackObject) connection - { for (String key : keys) { connection.expire(key.getBytes(), 15 * 24 * 3600); } return null; });还有一种动态续期的场景典型的就是看门狗模式。分布式锁加了一个30秒的TTL但业务执行时间可能超过30秒这时候你需要在锁快过期时自动续期。实现思路也不复杂拿到锁之后启动一个定时任务每隔10秒检查一次锁是否还在如果还在就把过期时间重置为30秒业务执行完释放锁的同时取消这个定时任务。Redis的Java客户端Redisson里内置了这种续期逻辑如果你自己实现别忘记用一个守护线程来跑续期避免主线程结束后定时任务还赖着不放。5. 常见问题与排查技巧实录5.1 为什么我的key一直没被删除问这个问题的同学往往是把设置了TTL等同于到点立刻消失。前面讲过Redis的过期清理有惰性删除和定期删除两道机制但这两道机制都不能保证实时性。如果你恰好设置了TTL之后这个key再也没被访问过Redis的定期删除又因为随机抽样没抽到它它就会一直躺在那直到占用内存到达阈值触发淘汰。还有一个隐蔽的坑Redis的过期时间只对当前数据库生效。如果你用SELECT切换了DB给DB1的key设置了TTL然后在DB0里查询你会觉得key凭空消失了实际上只是库没对上。5.2 命令超时与阻塞排查很多人在给大量key设置过期时间时遇到Redis command timed out这个报错。这个热词搜索量居高不下说明踩坑的人不少。这类报错最常见的原因是批量操作的数量太大比如一次循环里执行几十万个EXPIRE导致Redis单线程处理不过来命令积压客户端等待超时。排查思路可以按这个顺序来先用SLOWLOG GET查看Redis的慢查询日志确认是否有耗时的批量操作。检查客户端的连接池配置看最大等待时间是不是设得太短。如果确认是批量操作导致改用Pipeline或Lua脚本合并命令。检查网络延迟用redis-cli --latency测一下本机到Redis服务的网络状况。5.3 可视化客户端里怎么设置过期时间很多人习惯用可视化工具管理Redis问得最多的就是界面里怎么给key设置过期时间。不同工具操作位置不一样Redis Desktop Manager右键点击key选择Set TTL输入秒数。Another Redis Desktop Manager选中key后在右侧信息面板里找到TTL字段直接改数值。RedisInsight选中key后在Additional details区域可以看到TTL并支持直接修改。这里补充一个细节在可视化工具里TTL显示为-1表示永不过期显示为-2表示key不存在。你如果在界面里看到-2千万别以为这个key的过期时间是-2那是key本身已经没了。5.4 常见问题速查表问题现象原因解决方案设置了TTL但key一直存在惰性删除定期删除并非实时访问该key触发惰性删除或等待定期删除扫到TTL显示为-1key从未设置过期时间执行EXPIRE补上过期时间TTL显示为-2key不存在或已过期被删用EXISTS确认key状态重启后过期key复活AOF持久化重放导致删除延迟重启后主动清理或等待惰性删除SETNX后程序崩溃锁没释放非原子性操作改用SET NX EX一步设置批量设置TTL导致命令超时命令过多阻塞Redis单线程使用Pipeline/Lua脚本批量执行内存满了但旧key没删maxmemory-policy未合理配置设置volatile-lru等淘汰策略5.5 我的雨伞经验设置过期时间背后真正重要的事我个人在实际操作中最深的感受是设置过期时间这件事表面上是敲一条命令实际上是培养一套所有临时数据都要有生命周期的思维方式。你每敲入一个key都该下意识地问自己一句这个key能活多久它需要活这么久吗它过期了影响什么养成这个习惯之后你的Redis内存曲线会稳定很多线上内存满的报警也能减少一大半。最后再分享一个小技巧用redis-cli --stat观察Redis的瞬时状态配合INFO keyspace看每个库的过期key数量和平均TTL比只看DBSIZE有用得多。你需要在监控面板里同时盯住已过期的key数量和内存使用率这两条曲线才能对Redis的健康状况有真实的感知避免一脸懵地面对突然的内存爆满。
返回列表