Skip to content

基础

事务

Redis 支持事务。单个命令具有原子性;多个命令可以通过 MULTIEXEC 组成事务,但 Redis 事务不提供传统关系型数据库事务的回滚能力。

命令

  • SETNX key value:只在 key 不存在时将值设为 value

分布式锁

  • Redisson

优化:

  • 分段锁:Hash 运算 分段锁,或者商品id锁

存在问题:

  • 主从架构可能会存在主节点数据未同步从节点导致锁重复
    • Redlock
    • ZooKeeper:CP 一致性,数据同步时会短时间不可用

缓存淘汰

当 Redis 使用的内存达到 maxmemory 后,会按照 maxmemory-policy 决定拒绝写入还是淘汰部分 key。Redis 使用的是近似 LRU/LFU 算法,不是传统的“双向链表 + 哈希表”精确实现,也不提供 FIFO 或 LRU-K 淘汰策略。

策略候选范围行为
noeviction不淘汰 key,可能增加内存的写命令返回错误
allkeys-lru所有 key淘汰最近最少使用的 key
allkeys-lfu所有 key淘汰访问频率最低的 key
allkeys-random所有 key随机淘汰 key
volatile-lru设置了过期时间的 key淘汰最近最少使用的 key
volatile-lfu设置了过期时间的 key淘汰访问频率最低的 key
volatile-random设置了过期时间的 key随机淘汰 key
volatile-ttl设置了过期时间的 key优先淘汰剩余生存时间较短的 key

缓存场景通常从 allkeys-lruallkeys-lfu 中选择;不能容忍数据被自动删除时使用 noeviction。具体选择应依据访问分布和数据可恢复性。

过期删除

Redis 通过两种机制配合删除过期 key:

  1. 惰性过期(Passive expiration):客户端访问 key 时检查其是否过期;如果已过期,先删除再按 key 不存在处理。
  2. 主动过期(Active expiration):Redis 周期性地从设置了过期时间的 key 中抽样检查并删除过期项,避免长期不被访问的过期 key 一直占用内存。

Redis 不会为每个 key 创建一个到期定时器,因为大量定时器会带来显著的 CPU 和内存开销。过期删除与内存淘汰也不是一回事:前者处理已经到期的 key,后者在达到 maxmemory 时根据策略释放空间。

缓存一致

Cache Aside 的核心思想就是,当缓存的数据有更新值了,它采用的不是更新缓存数据,而是删除缓存数据

保证最终一致性

Cache Aside 常用的写入顺序是:先更新数据库,事务提交成功后再删除缓存。如果删除失败,需要通过重试、消息队列或 Binlog/CDC 补偿。下面的延迟双删主要用于解释“先删缓存、再更新数据库”时的并发补偿方案。

延迟双删

先删除缓存、再更新数据库时,可能出现以下并发问题:

  1. 写线程删除缓存。
  2. 读线程发现缓存不存在,从数据库读到旧值。
  3. 写线程更新数据库为新值。
  4. 读线程把之前读到的旧值写回缓存。

最终数据库是新值,缓存却长期保留旧值。延迟双删会在数据库更新后等待一小段时间,再删除一次缓存,把并发读线程可能回填的旧值清理掉。

text
删除缓存 → 更新数据库 → 延迟一段时间 → 再次删除缓存
java
public void updateProduct(Product product) {
    String key = "product:" + product.getId();

    // 第一次删除,避免后续请求继续读取旧缓存
    redisTemplate.delete(key);

    productRepository.update(product);

    // 实际项目应交给延迟队列或异步任务,不要阻塞业务线程
    delayQueue.send(() -> redisTemplate.delete(key), 500);
}

延迟时间不能随意写死,应大于一次并发读请求完成“查询数据库并回填缓存”的最长耗时,同时结合接口延迟监控动态评估。第二次删除还必须具备重试能力,否则 Redis 超时或消息丢失后仍可能留下脏数据。

优点局限
实现简单,能缩短脏缓存存在时间只能保证最终一致性,不能保证强一致性
适合读多写少、允许短暂不一致的场景依赖合理的延迟时间和可靠的第二次删除
不需要同时更新数据库和缓存写操作回滚、进程宕机、删除失败都需要额外处理

生产环境通常使用消息队列、延迟队列或定时补偿执行第二次删除,并记录失败任务。对一致性要求更高时,可采用“更新数据库后删除缓存 + 删除失败重试”,或监听 Binlog/CDC 异步失效缓存。不要把延迟双删当成强一致性方案。

其他缓存更新模式

  1. Read/Write Through

  1. Write Behind

缓存击穿

查询某个数据的值,该值在缓存中无,在数据库中有

  1. 一般缓存都会设置个数据过期时间,所以缓存击穿的情况比较常见
  2. 缓存击穿带来的问题:如果对该值查询突然特别大,由于缓存中无该数据,所以请求就会打到后端数据库上
  3. 处理
    1. 从MySQL角度出发:减少击穿后的直接流量

  • 从Redis角度出发:
    • 设置热点数据永不过期
    • 热点数据后台启动一个异步线程,重新把数据回填缓存层

缓存穿透

查询一个数据库也不存在的数据,即缓存查不到,数据库也查不到所以透了

  1. 拦截非法查询请求
  2. 缓存空对象
  3. 布隆过滤器
    1. 设计思路:设计k个hash映射函数,这k个映射函数各不相同,把在数据库中的有效数据通过这个映射函数得到数组指定位置,并置为1
    2. 查询思路:输入查询数据,通过这一系列hash函数,判断对应位置是否全为1。如果是,则很大概率就是我们的有效请求了,如果有一位为0,则该查询一定是无效查询
    3. 提前查询数据库做一个布隆表

缓存雪崩

一大批被缓存的数据,同时失效,此时对于这一批的数据请求就全打到数据库上,导致数据库宕机

和缓存击穿类似

  1. 从MySQL角度出发

  1. Redis
    1. 设置热点数据永不过期
    2. 分析失效时间,尽量让失效时间点分散
    3. 缓存预热,即在上线前,根据当天的情况分析,将热数据直接加载到缓存系统