6.Redis 缓存使用问题及解决方案

引言

Redis 作为一种高效的缓存解决方案,广泛应用于各类项目中。然而,使用缓存时也会面临一些问题,特别是数据一致性、缓存穿透、击穿、雪崩等问题。 

1. 数据一致性

数据一致性是指在使用缓存时,缓存中的数据与数据库中的数据保持一致。数据不一致可能导致用户获取到过时的信息,影响用户体验。

1.1 数据操作方案

在进行数据增删改操作时,常见的方案有:

  1. 先更新缓存,再更新数据库

    • 优点:缓存命中率提高,用户可以快速获取到更新后的数据。
    • 缺点:如果更新缓存成功但更新数据库失败,缓存和数据库的数据将不一致,可能导致数据丢失。
  2. 先更新数据库,再更新缓存

    • 优点:确保数据库中的数据始终是最新的。
    • 缺点:如果数据库更新成功但更新缓存失败,用户可能会获取到旧的缓存数据。
  3. 先删除缓存,后更新数据库

    • 优点:避免了在缓存中读取到过期的数据。
    • 缺点:在高并发情况下,可能会导致查询到旧数据。
  4. 先更新数据库,后删除缓存

    • 优点:确保数据库始终是最新的,同时删除缓存后,下一次请求会重新从数据库中获取最新数据。
    • 缺点:在高并发时,可能会导致短时间内缓存不命中。

1.2 推荐方案

一般情况下,推荐使用 先更新数据库,后删除缓存 的方案。通过延时双删策略(先删除缓存,再写数据库,休眠一段时间后再次删除缓存),可以有效降低缓存不一致的风险。

示例代码

以下是实现延时双删策略的 Java 代码示例,代码中包含详细注释:

java

import redis.clients.jedis.Jedis;

public class CacheUpdateExample {
    private Jedis jedis; // Redis 客户端

    // 构造函数,初始化 Redis 客户端
    public CacheUpdateExample() {
        this.jedis = new Jedis("localhost", 6379); // 连接到本地 Redis 服务
    }

    // 更新数据的方法
    public void updateData(String key, String value) {
        // 1. 先删除缓存
        jedis.del(key);
        
        // 2. 更新数据库
        updateDatabase(key, value);
        
        // 3. 休眠一段时间,确保数据库更新完成
        try {
            Thread.sleep(1000); // 休眠1秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 4. 再次删除缓存(延时双删)
        jedis.del(key);
    }

    // 模拟数据库更新操作
    private void updateDatabase(String key, String value) {
        System.out.println("Updating database: " + key + " = " + value);
        // 这里可以添加实际的数据库更新逻辑
    }

    // 主方法,程序入口
    public static void main(String[] args) {
        CacheUpdateExample example = new CacheUpdateExample();
        example.updateData("user:1001", "John Doe"); // 更新用户数据
    }
}
生活场景

想象一下,一个在线购物网站,用户在购物车中添加商品。当用户结算时,系统需要更新商品的库存和订单信息。如果系统先更新缓存,再更新数据库,可能导致库存信息不准确,造成用户下单失败。采用先更新数据库,后删除缓存的策略,可以确保数据的一致性,避免用户体验受到影响。

2. 缓存穿透、击穿与雪崩

在使用 Redis 缓存时,还可能面临缓存穿透、击穿和雪崩的问题。这些问题会导致数据库压力增加,影响系统性能。

2.1 缓存穿透

缓存穿透是指查询一个根本不存在的数据,导致请求直接打到数据库,可能造成数据库负载过高。

解决方案
  1. 缓存空对象:当存储层不命中时,将空对象缓存一段时间,避免频繁查询数据库。

java

public String getData(String key) {
    // 从缓存中获取数据
    String value = jedis.get(key);
    if (value == null) {
        // 查询数据库
        value = queryDatabase(key);
        if (value == null) {
            // 如果数据库也没有,缓存空对象,避免频繁查询
            jedis.setex(key, 3600, ""); // 设置空对象的缓存,过期时间为1小时
        }
    }
    return value; // 返回结果
}

// 模拟数据库查询
private String queryDatabase(String key) {
    System.out.println("Querying database for key: " + key);
    return null; // 假设数据库中没有该数据
}
生活场景

在用户请求某个商品信息时,如果该商品不存在,系统可以将空对象缓存,以避免后续的无效请求直接查询数据库,减轻数据库压力。比如用户请求一个不存在的商品 ID,系统可以缓存该请求的空结果,避免后续相同请求再次访问数据库。

2.2 缓存击穿

缓存击穿是指某个热点数据在失效时,瞬间大量请求直接访问数据库,造成数据库压力过大。

解决方案
  1. 使用互斥锁:在缓存失效时,加锁并加载数据库的数据,避免多个请求同时查询数据库。

java

public String getHotData(String key) {
    // 从缓存中获取数据
    String value = jedis.get(key);
    if (value == null) {
        synchronized (this) { // 互斥锁,确保同一时间只有一个线程能查询数据库
            value = jedis.get(key); // 再次检查缓存
            if (value == null) {
                // 如果缓存仍然不存在,查询数据库
                value = queryDatabase(key);
                if (value != null) {
                    // 将查询到的数据存入缓存
                    jedis.set(key, value);
                }
            }
        }
    }
    return value; // 返回结果
}
生活场景

假设一个热门活动的页面在某个时间段内访问量激增,使用互斥锁可以确保在缓存失效的瞬间,只有一个请求会去查询数据库,避免数据库被瞬间打垮。例如,当某个活动的页面缓存失效时,多个用户同时请求该页面,只有第一个请求会查询数据库,其他请求会等待,直到第一个请求完成。

2.3 缓存雪崩

缓存雪崩指的是缓存失效后,大量请求瞬间打到数据库,导致数据库崩溃。

解决方案
  1. 保证缓存高可用性:使用 Redis Sentinel 或 Cluster 实现高可用。
  2. 随机过期时间:为缓存设置随机的过期时间,避免集中失效。

java

public void setData(String key, String value) {
    // 设置随机的过期时间,避免集中失效
    int randomExpireTime = (int) (Math.random() * 300) + 600; // 600s到900s之间随机过期时间
    jedis.setex(key, randomExpireTime, value); // 设置缓存
}
生活场景

在大型促销活动期间,很多商品的缓存同时过期,系统可以通过设置随机的过期时间,降低同一时间请求数据库的压力。例如,商品 A 的缓存设置为 600s,商品 B 的缓存设置为 720s,商品 C 的缓存设置为 900s,这样可以避免在同一时间大量请求打到数据库。

3. 热点 Key 和 Big Key

3.1 热点 Key

热点 Key 是指被频繁访问的 Key,可能导致 Redis 性能下降。

解决方案
  1. 使用二级缓存:将热点 Key 加载到本地缓存中,减少对 Redis 的访问。

java

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

public class HotKeyCache {
    private Cache<String, String> localCache; // 本地缓存
    private Jedis jedis; // Redis 客户端

    // 构造函数,初始化本地缓存和 Redis 客户端
    public HotKeyCache() {
        this.localCache = CacheBuilder.newBuilder().maximumSize(100).build(); // 设置本地缓存最大容量
        this.jedis = new Jedis("localhost", 6379); // 连接到本地 Redis 服务
    }

    // 从缓存中获取数据的方法
    public String getData(String key) {
        // 首先检查本地缓存
        String value = localCache.getIfPresent(key);
        if (value == null) {
            // 如果本地缓存没有,再从 Redis 获取数据
            value = jedis.get(key);
            if (value != null) {
                // 将获取到的数据存入本地缓存
                localCache.put(key, value);
            }
        }
        return value; // 返回结果
    }
}
生活场景

在一个新闻网站中,某篇热门文章会频繁被访问,可以将该文章缓存到本地,减少对 Redis 的请求,提升访问速度。通过使用 Guava Cache,热点文章可以直接从本地缓存中读取,避免对 Redis 的高频访问。

3.2 Big Key

Big Key 是指占用内存较大的 Key,可能导致 Redis 性能下降。

解决方案
  1. 拆分 Big Key:将一个大的 Key 拆分为多个小 Key,减小单个 Key 的内存占用。

java

public void setBigValue(String bigKey, List<String> values) {
    // 将大 Key 拆分为多个小 Key
    for (int i = 0; i < values.size(); i++) {
        jedis.set(bigKey + ":" + i, values.get(i)); // 拆分存储
    }
}

// 获取拆分的小 Key 的值
public List<String> getBigValue(String bigKey, int count) {
    List<String> values = new ArrayList<>();
    for (int i = 0; i < count; i++) {
        String value = jedis.get(bigKey + ":" + i); // 从 Redis 获取小 Key 的值
        if (value != null) {
            values.add(value); // 添加到结果列表
        }
    }
    return values; // 返回结果列表
}
生活场景

在存储用户的购物车时,如果购物车中商品较多,可以将购物车拆分成多个小 Key,方便管理和查询。例如,用户的购物车可以拆分为 user:1001:cart:0user:1001:cart:1 等小 Key 存储每个商品的信息,从而避免单个 Key 的内存占用过大。

4. 数据倾斜与 Redis 脑裂

4.1 数据倾斜

数据倾斜分为访问量倾斜和数据量倾斜,可能导致集群性能不均衡。

解决方案

使用负载均衡策略,合理分配请求,避免集中访问某个节点。例如,可以使用一致性哈希算法,将不同的请求分配到不同的 Redis 节点。

java

import java.util.SortedMap;
import java.util.TreeMap;

public class ConsistentHash {
    private SortedMap<Integer, String> circle = new TreeMap<>(); // 哈希环

    // 添加节点到哈希环
    public void addNode(String node) {
        int hash = node.hashCode(); // 计算节点的哈希值
        circle.put(hash, node); // 将节点添加到哈希环
    }

    // 获取对应于某个 key 的节点
    public String getNode(String key) {
        int hash = key.hashCode(); // 计算 key 的哈希值
        SortedMap<Integer, String> tailMap = circle.tailMap(hash); // 获取哈希环中大于或等于 hash 的部分
        Integer nodeHash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey(); // 找到下一个节点
        return circle.get(nodeHash); // 返回节点
    }
}
生活场景

在一个在线游戏中,用户的游戏数据需要频繁读写。如果所有用户的请求都集中在某个数据库节点上,可能会导致该节点负载过高。通过一致性哈希,可以将用户请求均匀分配到多个 Redis 节点,避免单点压力。

4.2 Redis 脑裂

脑裂是指在主从集群中出现多个主节点,导致数据不一致。

解决方案
  1. 配置 Sentinel:设置最小的从节点数和最大延迟,避免脑裂。

plaintext

min-replicas-to-write 2   # 至少需要 2 个从节点可用才能进行写操作
min-replicas-max-lag 10    # 从节点的最大延迟为 10 秒
  1. 使用奇数个主节点:构建 Redis 集群时,确保主节点数量为奇数,减少脑裂风险。
生活场景

在一个在线支付系统中,如果出现脑裂,可能导致用户的支付数据不一致,配置 Sentinel 可以有效防止这种情况发生。通过设置参数,确保只有在一定数量的从节点可用时,主节点才能接收写请求,从而避免数据丢失。

5. 多级缓存实例

在实际应用中,使用多级缓存可以有效提升系统性能。以携程金融为例,构建了自顶向下的多层次系统架构,使用 Redis 作为统一的缓存服务,确保数据的准确性、完整性和系统的可用性。

5.1 整体方案

  1. 异地多机房部署:提高可用性,减少跨地域访问延迟。
  2. 多种数据更新触发源:使用定时任务、MQ 和 binlog 变更等多种方式,确保数据及时更新。

5.2 数据准确性与完整性

  1. 并发控制:使用 Redis 实现分布式锁,确保缓存更新的顺序性。

java

public void updateDataWithLock(String key, String value) {
    String lockKey = "lock:" + key; // 锁的 Key
    try {
        // 尝试获取锁
        if (jedis.setnx(lockKey, "locked") == 1) { // 尝试获取锁
            jedis.expire(lockKey, 5); // 设置锁的过期时间为5秒

            // 更新数据库
            updateDatabase(key, value);

            // 更新缓存
            jedis.set(key, value);
        }
    } finally {
        // 释放锁
        jedis.del(lockKey); // 确保锁被释放
    }
}
  1. 全量数据刷新任务:定期全量刷新缓存,确保数据的完整性。

java

public void refreshCache() {
    List<String> allKeys = getAllKeysFromDatabase(); // 从数据库获取所有需要刷新的 Key
    for (String key : allKeys) {
        String value = queryDatabase(key); // 查询数据库
        jedis.set(key, value); // 更新缓存
    }
}

// 模拟从数据库获取所有 Key
private List<String> getAllKeysFromDatabase() {
    return Arrays.asList("user:1001", "user:1002", "user:1003"); // 返回示例 Key 列表
}
生活场景

在一个电商平台,商品信息需要频繁更新,为了确保数据的准确性,可以使用分布式锁控制更新过程,确保不会出现数据更新冲突。同时,定期全量刷新缓存,可以确保在长时间内数据的一致性。例如,每隔一段时间,系统会从数据库中获取所有商品的最新信息,并更新到 Redis 中。

 

  • 12
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值