# 防止缓存穿透、雪崩与击穿

URL: https://caijiao.org/redis/09-use-cases/02-anti-patterns
Source: docs/redis/09-use-cases/02-anti-patterns.md
Description: 在使用 Redis 作为缓存系统时，缓存穿透、雪崩和击穿是常见的问题，本文介绍了如何通过合理的设计和策略来避免这些问题。

在使用 Redis 作为缓存时，缓存穿透、缓存雪崩和缓存击穿是三种常见的缓存问题。如果不加以防范，可能会对系统的性能和稳定性产生严重影响。本文将详细介绍这三种问题及其解决方案。

## 1. 缓存穿透

### 1.1 什么是缓存穿透？

缓存穿透是指请求的数据既不在缓存中，也不在数据库中，导致每次请求都会查询数据库。由于每次都查询数据库，无法从缓存中获益，严重时会给后端数据库带来巨大压力，甚至造成数据库崩溃。

### 1.2 缓存穿透的解决方法

- **使用布隆过滤器**：
  布隆过滤器是一种空间效率极高的数据结构，可以用来判断某个元素是否存在。在缓存穿透的场景中，可以利用布隆过滤器在请求查询数据库之前，先判断请求的数据是否可能存在。如果数据不存在，可以直接返回空，避免无意义的数据库查询。

  - 布隆过滤器通过使用多个哈希函数将数据映射到一个位数组中，从而达到快速判断数据是否存在的目的。
  
  - 如果布隆过滤器判断该数据不存在，可以直接返回错误或者默认值，减少数据库的访问。

  - 需要注意的是，布隆过滤器有一定的误判率，可能会错误地判断数据存在，但其误判的代价是较小的。

- **缓存空值**：
  对于不存在的数据，可以将其“空值”缓存起来。这样，即使是不存在的数据，在短时间内再次请求时，缓存会直接返回空，避免每次都查询数据库。可以为这些“空数据”设置较短的过期时间，防止缓存中堆积大量无效数据。

  - 设置一个较短的过期时间（如几秒），避免缓存中存储长时间的空数据。

### 1.3 示例

假设你有一个用户信息缓存，当查询某个用户信息时，首先检查缓存中是否存在该用户的数据。如果缓存中没有该用户数据，可以查询数据库。如果查询结果为空，可以将“空数据”缓存一定时间。

```python
# 判断用户信息是否存在
if not redis.exists(user_id):
    # 用户信息不存在，检查布隆过滤器
    if not bloom_filter.exists(user_id):
        return None
    # 从数据库查询
    user_data = db.get_user(user_id)
    if user_data:
        redis.set(user_id, user_data)
    else:
        redis.set(user_id, None, ex=10)  # 缓存空数据，过期时间较短
````

## 2. 缓存雪崩

### 2.1 什么是缓存雪崩？

缓存雪崩是指缓存中大量的缓存数据在同一时刻过期，导致大量请求同时访问数据库，给数据库带来极大压力，可能造成数据库崩溃。

### 2.2 缓存雪崩的解决方法

* **设置随机过期时间**：
  避免缓存中大量数据同时过期，可以通过为每个缓存数据设置随机的过期时间（如在一定范围内随机选择过期时间），使得不同缓存项过期时间不一致，从而避免在同一时刻大量缓存数据同时过期。

  * 例如，假设某个缓存数据的过期时间设置为 10 分钟，可以将过期时间在 9 到 11 分钟之间随机化。

  * 这样即使缓存的部分数据失效，其他数据仍然有效，数据库压力会分散。

* **使用互斥锁**：
  对于某些缓存数据，可以通过设置锁来避免多次并发请求访问数据库。只有第一个请求可以查询数据库，其他请求需要等待直到数据库更新完成。

  * 在更新缓存时，使用分布式锁确保只有一个请求能够查询数据库并更新缓存。

  * 这样可以有效减少同一时刻对数据库的压力。

### 2.3 示例

假设你要缓存用户信息的查询结果，为避免缓存雪崩，可以设置过期时间时加上随机值：

```python
import random

# 随机过期时间（9 到 11 分钟）
expire_time = 9 * 60 + random.randint(0, 120)

# 设置缓存
redis.set(user_id, user_data, ex=expire_time)
```

## 3. 缓存击穿

### 3.1 什么是缓存击穿？

缓存击穿是指某个热点数据的缓存失效，导致大量请求同时访问数据库，给数据库带来巨大的压力。这种情况通常发生在热点数据的缓存过期时，由于缓存同时失效，导致数据库遭遇流量突增。

### 3.2 缓存击穿的解决方法

* **使用互斥锁**：
  对于热点数据，可以采用互斥锁机制，确保只有一个请求去查询数据库并更新缓存。当一个请求正在从数据库加载数据并更新缓存时，其他请求会等待，避免多个请求同时访问数据库。

* **使用双重检查锁定**：
  在获取缓存时先检查缓存是否存在，如果缓存存在则直接返回，如果缓存不存在则尝试获取锁，只有第一个获取锁的请求去查询数据库并更新缓存，其他请求等待缓存更新完成后再返回数据。

* **延长缓存过期时间**：
  对于热点数据，避免频繁过期可以设置较长的缓存时间，减少缓存失效的频率，从而减少缓存击穿的风险。

### 3.3 示例

假设你有一个商品的缓存，商品数据是热点数据。如果缓存过期，可以通过互斥锁机制避免多个请求同时查询数据库：

```python
import threading

lock = threading.Lock()

def get_product(product_id):
    if redis.exists(product_id):
        return redis.get(product_id)
    
    # 缓存失效，获取锁
    with lock:
        if not redis.exists(product_id):
            # 查询数据库并更新缓存
            product_data = db.get_product(product_id)
            redis.set(product_id, product_data, ex=3600)  # 缓存1小时
        return redis.get(product_id)
```

## 4. 总结

* **缓存穿透**：通过布隆过滤器或缓存空值来避免无意义的数据库查询。
* **缓存雪崩**：通过随机过期时间和使用互斥锁来避免大量缓存同时过期。
* **缓存击穿**：通过使用互斥锁、双重检查锁定以及延长缓存过期时间来防止热点数据的缓存失效引发的数据库压力。

通过合理的缓存策略和设计，可以有效避免缓存穿透、雪崩和击穿问题，确保系统在高并发场景下的稳定性和性能。
