在我们日常用的缓存系统里,很多人会遇到“明明设置了过期时间,为啥还占内存?”或者“CPU突然飙高,查了半天发现是过期键处理的锅”的问题,这其实都和过期键的淘汰逻辑有关——过期键删得好不好,直接牵连着服务器的CPU和内存表现,而这背后的核心是惰性删除和定时删除的平衡问题。

一、过期键淘汰对CPU与内存的实际影响

1.1 内存怎么被过期键“偷偷占”的

缓存系统的核心是“临时存数据,减少重复计算/查询”,比如电商的用户购物车、社交平台的临时会话,都会给每个键设置过期时间(比如24小时),确保不会一直占用内存。但如果这些过期键不主动删除,就会一直躺在内存里“占位置”。举个实际的例子:某社区论坛为了加快用户发帖速度,把用户的草稿内容存在缓存里,每个草稿设置1小时过期。上线初期只有1万用户,大概占10MB内存;后来用户量涨到100万,哪怕大部分用户3天没打开帖子,缓存里的100万条过期草稿没被处理,直接多占了1GB内存——要是服务器总内存只有2GB,这1GB的浪费直接导致系统崩溃。

1.2 CPU为什么会因为过期键“累垮”

内存不够还好排查,CPU被过期键拖垮更隐蔽。我们可以把CPU比作公司前台,每次用户请求就像来取东西的客人。如果用的是“惰性删除”策略(后面会讲),每次取东西(查缓存)都要先翻一遍手里的纸条(键),看是不是过期了;如果有100万张过期纸条,每来一个客人都要过一遍,前台的效率会直接打对折。比如某外卖平台的缓存层,每次用户查订单都要扫10万条过期键,原本CPU使用率只有30%,高峰时直接跳到90%,导致订单查询超时率飙升到15%,这就是过期键给CPU带来的额外开销。

二、惰性删除 vs 定时删除:两种策略的权衡

这是过期键处理的两个核心思路,我们用生活化的例子来理解:惰性删除是“垃圾来了不主动扔,等有人扔垃圾(取数据)路过时顺手把过期垃圾扔掉”;定时删除是“专门雇人每天定点扫一遍,把所有过期垃圾全清走”。

2.1 惰性删除:等访问时才处理

惰性删除的逻辑很简单:只在用户要取某个键的时候,才检查这个键是不是过期,过期就当场删掉,不占用其他时间。我们用Python模拟这个逻辑,代码里加了详细注释,方便理解:

# 技术栈:Python 3.9
import time

class LazyDeletionCache:
    def __init__(self):
        # 用字典存缓存,键是自定义的key,值是(实际数据,过期时间戳)
        self.cache = {}
    
    def set_key(self, key, data, expire_seconds):
        # 设置键的过期时间:当前时间 + 要保留的秒数
        expire_timestamp = time.time() + expire_seconds
        self.cache[key] = (data, expire_timestamp)
    
    def get_key(self, key):
        # 惰性删除核心:取键时先查是否过期
        if key not in self.cache:
            return None  # 键不存在
        value, expire_ts = self.cache[key]
        # 判断当前时间是否超过过期时间
        if time.time() > expire_ts:
            del self.cache[key]  # 过期就直接删掉,释放内存
            return None
        return value  # 没过期就返回数据

# 示例使用
if __name__ == "__main__":
    cache = LazyDeletionCache()
    # 设置用户1的购物车,2秒后过期
    cache.set_key("user1_cart", "牛奶,面包,鸡蛋", 2)
    print("第一次取购物车:", cache.get_key("user1_cart"))  # 输出:牛奶,面包,鸡蛋
    time.sleep(3)  # 等待3秒,超过过期时间
    print("第二次取购物车:", cache.get_key("user1_cart"))  # 输出:None,惰性删除生效

2.2 定时删除:定期主动扫过期键

定时删除的逻辑是:系统单独开一个后台线程,每隔固定时间扫一遍所有键,把过期的键批量删掉,不需要每次访问都处理。同样用Python模拟:

# 技术栈:Python 3.9
import time
import threading

class TimedDeletionCache:
    def __init__(self, scan_interval=1):
        self.cache = {}
        self.scan_interval = scan_interval  # 扫过期键的间隔,单位是秒
        # 启动后台守护线程,专门做过期键清理,主线程结束时自动终止
        threading.Thread(target=self._clean_expired, daemon=True).start()
    
    def set_key(self, key, data, expire_seconds):
        expire_ts = time.time() + expire_seconds
        self.cache[key] = (data, expire_ts)
    
    def _clean_expired(self):
        # 后台线程的清理逻辑,循环执行
        while True:
            now = time.time()
            # 找出所有过期的键
            expired_keys = [key for key, (_, ts) in self.cache.items() if ts < now]
            # 批量删除过期键
            for key in expired_keys:
                del self.cache[key]
            # 等待下一次扫描
            time.sleep(self.scan_interval)
    
    def get_key(self, key):
        # 直接查缓存,不用处理过期,因为后台已经删了
        return self.cache.get(key, (None,))[0]

# 示例使用
if __name__ == "__main__":
    cache = TimedDeletionCache(scan_interval=1)  # 每秒扫一次过期键
    cache.set_key("user2_cart", "苹果,香蕉", 2)
    print("第一次取购物车:", cache.get_key("user2_cart"))  # 输出:苹果,香蕉
    time.sleep(2)  # 等待2秒,刚好扫过一次
    print("第二次取购物车:", cache.get_key("user2_cart"))  # 输出:None

2.3 两种策略的优缺点对比

惰性删除的优点是CPU开销极小,因为只有访问到的键才会处理,不会额外占用CPU;但缺点是内存会冗余过期键——那些很少被访问的键会一直占着内存,比如用户半年没登录的账号缓存,一直躺在内存里占位置,导致内存利用率低。定时删除的优点是内存利用率高,几乎不会有过期键占内存;但缺点是CPU开销和扫描频率挂钩,如果扫的间隔太短(比如0.1秒扫一次),后台线程会一直占用CPU;如果间隔太长(比如10分钟扫一次),过期键会攒太多,内存被占满,同样会出问题。

三、实际应用中的权衡方案

3.1 常见的应用场景

现在成熟的缓存系统(比如Redis)用的是混合策略,就是把惰性删除和定时删除结合起来,既减少CPU开销,又控制内存占用。比如Redis的做法是:惰性删除+每秒扫100个随机过期键,这样不会因为全量扫描占满CPU,也能及时清理大部分过期键。举两个典型场景:

  1. 核心业务缓存:比如电商的商品详情,访问量极高,适合用“短周期扫描+惰性删除”,比如每秒扫100个键,同时每次取数据时再检查一次,平衡CPU和内存;
  2. 非核心缓存:比如用户的历史浏览记录,很少被访问,适合用“长周期扫描”,比如10分钟扫一次,避免CPU被占用过多,同时内存不会被浪费太多。

3.2 选策略的注意事项

要根据业务的“核心需求”来选,不能一概而论:

  1. 如果业务对响应速度要求极高(比如支付场景),不要用太长的扫描间隔,避免刚过期的键还在缓存里,返回给用户错误数据;
  2. 如果服务器内存资源紧张,优先用定时删除的短扫描间隔,及时释放过期键;
  3. 如果服务器CPU资源紧张,优先用惰性删除,或者减少定时扫描的频率,接受少量过期键暂时占内存;
  4. 绝对不能只依赖一种策略——只做惰性删除会内存爆炸,只做定时删除会CPU过载,混合策略才是最优解。

3.3 总结

过期键的处理本质是“资源分配的权衡”:CPU和内存都是有限的,要在这两个资源之间找平衡点。惰性删除和定时删除没有绝对的好坏,关键是贴合业务场景:访问多的键用惰性删除,访问少的键用定时扫描;对延迟敏感的场景缩短扫描间隔,对性能敏感的场景延长扫描间隔。只有把这两种策略结合起来,才能既不浪费内存,也不拖垮CPU,让缓存系统稳定运行。