# Redis 实战

# 缓存更新策略

内存淘汰超时剔除主动更新
说明不用自己维护,利用 Redis 的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存给缓存添加 TTL 时间,到期后自动删除缓存,下次更新时更新缓存编写业务逻辑,在修改数据库时,更新缓存
一致性一般
维护成本

业务场景 :

  • 低一致性需求:使用内存淘汰机制,例如店铺类型的查询缓存
  • 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存

# 主动更新策略

  1. Cache Aside Pattern : 由缓存的调用者,在更新数据库的同时更新缓存
  2. Read/Write Through Pattern : 缓存与服务器整合为一个服务,由服务来维护一致性。调用者调用该服务,,无需关心缓存一致性的问题
  3. Write Behind Cacheing Pattern : 调用者只操作缓存,由其它线程异步的将缓存数据持久化到数据库,保证最终一致

** 最佳实践方案 : **

  1. 低一致性需求:使用 Redis 自带的内存淘汰机制
  2. 高一致性需求:主动更新,并以超时剔除作为兜底方案
    • 读操作 :
      • 缓存命中直接返回
      • 缓存未命中则查询数据库,并写入缓存,设定超时时间
    • 写操作 :
      • 先写数据库,然后再删除缓存
      • 要确保数据库与缓存操作的原子性

先删缓存,再操作数据库

image-20221216145643257

先操作数据库,再删除缓存

image-20221216145708759

# 缓存穿透

缓存穿透是指客户端请求的数据再缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库

常见的解决方案有两种 :

  • 缓存空对象
    • 优点:实现简单,维护方便
    • 缺点 :
      • 额外的内存消耗
      • 可能造成短期的不一致
  • 布隆过滤
    • 优点:内存占用较少,没有多余 key
    • 缺点 :
      • 实现复杂
      • 存在误判可能

image-20221216145208398

# 缓存雪崩

缓存雪崩是指在同一时段大量的缓存 key 同时失效或者 Redis 服务宕机,导致大量请求到达数据库,带来巨大压力

解决方案 :

  • 给不同的 key 的 TTL 添加随机值
  • 利用 Redis 集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

image-20221216152201680

# 缓存击穿

缓存击穿问题也叫热点 Key 问题,就是一个被高并发访问并且缓存重建业务较复杂的 key 突然失效了,无数的请求访问会在一瞬间给数据库带来巨大的冲击

常见的解决方案有两种 :

  • 互斥锁
  • 逻辑过期

image-20221216152953702

上图是缓存击穿问题实例

# 互斥锁

image-20221216154034094

# 逻辑删除

image-20221216154057053

解决方案优点缺点
互斥锁没有额外的内存消耗,保证一致性,实现简单线程需要等待,性能受影响,可能有死锁风险
逻辑过期线程无需等待,性能较好不保证一致性,有额外内存损耗,实现复杂

# 全局 ID 生成器

为了增加 ID 的安全性,我们可以不直接使用 Redis 自增的数值,而是拼接一些其他信息 :

image-20221222203123529

ID 的组成部分 :

  • 符号位 : 1bit, 永远为 0
  • 时间戳 : 31bit, 以秒为单位,可以使用 69 年
  • 序列号 : 32bit, 秒内的计数器,支持每秒产生2322^{32} 个不同 ID

# 超卖问题

超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁

  • 悲观锁

    认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行

    • 例如 Synchronized, Lock 都属于悲观锁
  • 乐观锁

    认为线程安全不一定会发生,因此不加锁,只是在更新数据时去判断有没有其他线程对数据进行了更改

    • 如果没有修改则认为是安全的,自己才会更新数据
    • 如果已经被其他线程修改说明发生了线程安全问题,此时可以重试或异常

# 乐观锁

  • 版本号法

    数据库维护一个 version 信息,每次更改数据之前判断 version 是否与获取到的 version 一致

  • CAS 法

    每次更改数据前判断数据是否与之前或独到的一致

# 分布式锁的实现

MySQLRedisZookeeper
互斥利用 mysql 本身的互斥锁机制利用 setnx 这样的互斥命令利用节点的唯一性和有序性实现互斥
高可用
高性能一般一般
安全性断开连接,自动释放锁利用锁超时时间,到期释放临时节点,断开连接自动释放

# 基于 Redis 的分布式锁

实现分布式锁需要实现的两个基本方法 :

  • 获取锁 :

    • 互斥:确保有一个线程获取锁

    • 非阻塞:尝试一次,成功返回 true, 失败返回 false

      1
      SET lock thread1 NX EX 10
  • 释放锁 :

    • 手动释放

    • 超时释放:获取锁时添加一个超时时间

      1
      DEL key

# Redis 消息队列

消息队列,字面意思就是存放消息的队列。最简单的消息队列包括 3 个角色

  • 消息队列:储存和管理消息,也被称为消息代理 (Message Broker)
  • 生产者:发送消息到消息队列
  • 消费者:从消息队列获取消息并处理消息

Redis 提供了三种不同的方式来实现消息队列 :

  • List 结构:基于 List 结构模型模拟消息队列
  • PubSub : 基本的点对点消息模型
  • Stream : 比较完善的消息队列模型

image-20230113151010132

# 基于 List 结构模型模拟消息队列

优点 :

  • 利用 Redis 存储,不受限于 JVM 内存上限
  • 基于 Redis 的持久化机制,数据安全性有保证
  • 可以满足消息有序性

缺点 :

  • 无法避免消息丢失
  • 只支持单消费者 (无法实现一个消息被多个消费者使用,因为一旦 pop 之后该队列元素就删除了)

# 基于 PubSub 的消息队列

PubSub (发布订阅) 是 Redis2.0 版本引入的消息传递模型。顾名思义,消费者可以订阅一个或多个 channel, 生产者向对应 channel 发送消息后,所有订阅者都能收到相关消息

  • subscribe channel [channel] : 订阅一个或多个评到
  • publish channel msg : 向一个频道发送消息
  • psubscribe pattern [pattern] : 订阅与 pattern 格式匹配的所有频道

优点 :

  • 采用发布订阅模型,支持多生产,多消费

缺点 :

  • 不支持数据持久化
  • 无法避免消息丢失
  • 消息堆积有上限,超出时数据丢失

# 基于 Stream 的消息队列

Stream 是 Redis5.0 引入的一种新数据模型,可以实现一个功能非常完善的消息队列

Stream 类型消息队列的 xread 命令特点 :

  • 消息可回溯
  • 一个消息可以被多个消费者读取
  • 可以阻塞读取
  • 有消息漏读的风险

# 消费者组

消费者组 (Consumer Group) : 将多个消费者划分到一个组中,监听同一个队列。具备以下特点 :

  1. 消息分流:队列中的消息会分流给组内的不同消费者,而不是重复消费,从而加快消息处理的速度
  2. 消费者会围护一个标识,记录最后一个被处理的消息,哪怕消费者宕机重启,还会从标识之后读取消息。确保每一个消息都会被消费
  3. 消费者获取消息后,消息处于 pending 状态,并存入一个 pending-list 中,当处理完成后需要通过 xack 来确认消息,标记消息为已处理,才会从 pending-list 中移除
1
XGROUP CREATE KEY groupName ID [MKSTREAM]
  • key : 队列名称
  • groupName : 消费者组名称
  • ID : 起始 ID 标识,$ 代表队列中最后一个消息,0 则代表队列中第一个消息
  • MKSTREAM : 队列不存在时自动创建按队列
1
XREADGROUP GROUP group consumer [COUNT count] [BLOCK millseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
  • group : 消费组名称
  • consumer : 消费者名称
  • count : 本次查询的最大数量
  • BLOCK milliseconds : 当没有消息时最长等待时间
  • NOACK : 无需手动 ACK, 获取到消息后自动确认
  • STREAMS key : 指定队列名称
  • ID : 获取消息的起始 ID :
    • “>” : 从下一个未消费的消息开始
    • 其他:根据指定 id 从 pending-list 中获取已消费但未确认的消息,例如 0, 是从 pending-list 中的第一个消息开始

stream 类型队列消息的 xreadgroup 命令特点 :

  • 消息可回溯
  • 可以多消费者争抢消息,加快消费速度
  • 可以阻塞读取
  • 没有消息漏读的风险
  • 有消息确认机制,保证消息至少被消费一次

# Redis 消息队列

dListPubSubStream
消息持久化支持不支持支持
阻塞读取支持支持支持
消息堆积处理受限于内存空间,可以利用多消费者加快处理受限于消费者缓冲区受限于队列长度,可以利用消费者组提高消费速度,减少堆积
消息确认机制不支持不支持支持
消息回溯不支持不支持支持