Skip to content

056. 缓存失效与 Redis 分布式锁 ​

学习目标 ​

这一节讨论缓存失效后的并发问题,并用 Redis 实现分布式锁。

学完后,你应该能理解:

  • 为什么缓存命中后不会查数据库;
  • 什么是缓存击穿;
  • 为什么缓存集中失效会压垮数据库;
  • 后端为什么不能只靠普通变量排队;
  • Redis 的 NX 能解决什么;
  • 释放锁为什么不能直接 delete;
  • Lua 脚本为什么能保证原子操作。

缓存命中的效果 ​

第一次查询某个用户信息时,如果 Redis 没有缓存,就会查 MySQL。

查到之后写入 Redis。

第二次再查询同一个用户,如果缓存还没过期,就直接从 Redis 返回。

这时不会再执行 SQL。

这就是缓存提高性能的直接体现。

缓存失效的问题 ​

缓存总会失效。

常见原因是:

  • key 过期;
  • key 被删除;
  • 服务刚启动还没预热;
  • 缓存里本来就没有这个数据。

缓存失效后,请求会打到数据库。

如果同一时间大量请求访问同一个热点数据,数据库压力会突然变大。

缓存击穿 ​

缓存击穿指的是某个热点 key 失效后,大量并发请求同时去查数据库。

例如秒杀活动开始时,很多用户同时请求同一个商品信息。

如果缓存刚好没了,所有请求都穿过缓存,直接打到数据库。

这会造成很大的瞬时压力。

缓存集中失效 ​

如果很多 key 在同一时间过期,也会形成压力峰值。

例如早上大量用户登录后,缓存都设置为两小时过期。

到了中午,这批缓存一起失效。

数据库请求就会像波峰一样突然升高。

常见缓解方式是给过期时间加随机偏移,让缓存分散过期。

热点数据永不过期 ​

一种处理方式是让热点数据不过期,或者启动服务时主动预热。

例如服务启动后立刻把关键配置写入 Redis。

这种做法适合部分场景。

但它也带来数据更新和内存占用问题。

所以项目里更常见的是结合过期时间和锁机制。

为什么需要分布式锁 ​

前端刷新 token 时,可以用一个 Promise 队列让后续请求等待。

后端不一样。

后端可能有多个线程、多个进程,甚至多台服务器。

普通变量只能约束当前进程,无法约束其他服务实例。

所以要借助外部系统实现互斥。

这里使用 Redis 做分布式锁。

锁的目标 ​

多个请求同时要加载同一个资源时,只允许一个请求去查数据库。

其他请求等待。

第一个请求查完数据库并写入缓存后,其他请求再从缓存读取。

这样数据库只承受一次查询,而不是一批并发查询。

Redis NX ​

Redis 设置 key 时可以使用 NX。

它的含义是:只有 key 不存在时才设置成功。

用它可以表达“抢锁”:

txt
set lock:user:zhangsan taskA nx ex 10

如果设置成功,说明当前任务拿到了锁。

如果设置失败,说明锁已经被别的任务占用。

锁要有过期时间 ​

锁必须设置过期时间。

否则拿到锁的任务如果崩溃,锁会一直留在 Redis 里。

后续所有请求都会一直等待。

过期时间是兜底机制。

它不能太短,否则任务还没结束,锁就提前过期。

它也不能太长,否则异常后等待时间过久。

等待重试 ​

没有拿到锁的请求不应该立刻放弃。

它可以循环等待一小段时间,再重试获取锁。

等待间隔不能为 0。

如果不等待,就会空转 CPU。

例如每次等待 0.1s,再尝试获取锁。

具体间隔要根据业务接口耗时调整。

释放锁的风险 ​

释放锁不能简单地直接删除 key。

原因是任务可能执行太久。

例如:

  1. A 拿到锁,锁 10 秒过期;
  2. A 执行 11 秒;
  3. 10 秒时锁自动过期;
  4. B 拿到同一个锁;
  5. A 执行完后直接删除锁;
  6. B 的锁被 A 删掉。

这会破坏互斥关系。

锁标识 ​

锁的 value 要存当前任务自己的标识。

释放锁时必须先判断:

  • Redis 里的锁标识是不是我自己的;
  • 如果是,才释放;
  • 如果不是,不处理。

这样 A 就不会删除 B 的锁。

为什么需要 Lua ​

判断锁标识和删除锁必须是一个原子操作。

不能先 get,再在代码里判断,最后 delete。

因为这三个步骤之间有网络传输和时间间隔。

中间可能插入其他操作,导致判断结果过期。

Redis 支持执行 Lua 脚本。

Lua 脚本在 Redis 内部执行,可以把读取、判断、删除放在同一个原子操作里。

Lua 释放锁逻辑 ​

释放锁的逻辑可以理解为:

txt
如果 lock key 当前值等于我的 lock token:
  删除 lock key
否则:
  什么都不做

这个逻辑用 Lua 在 Redis 里一次执行完成。

这样就不会因为网络间隔误删别人的锁。

这一节的重点 ​

这一节的核心是缓存失效后的并发控制。

你需要记住:

  • 缓存失效时可能出现大量并发查库;
  • 热点 key 失效会形成缓存击穿;
  • 过期时间集中会形成请求波峰;
  • 多实例后端要用分布式锁;
  • 获取锁可以用 Redis NX;
  • 释放锁必须校验锁标识,并用 Lua 保证原子性。

AI Agent 课程学习文档。