首页 > 数据库 >Redis缓存三兄弟

Redis缓存三兄弟

时间:2024-04-06 12:01:28浏览次数:22  
标签:缓存 return String 过期 Redis 兄弟 json key

Redis缓存的问题都是因为缓存过期,导致大量请求打到数据库,给数据库添加了压力。

以下是典型的三个缓存问题。

缓存穿透

概念

缓存穿透: 频繁请求缓存和数据库中没有的数据,导致数据库的压力过大

解决方案

  • 规则校验:增加对key的规则校验,防止恶意请求
  • 设默认值:数据库中没有数据时,给该key一个默认值,并设置合理的过期时间,防止较长时间的数据不一致
  • 布隆过滤器:布隆过滤器中存在该key就通过,不存在就不访问Redis和数据库

基于设空值代码实现

public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> doFallback, Long time, TimeUnit timeUnit){
    String key = keyPrefix + id;
    String json = stringRedisTemplate.opsForValue().get(key);
    // 如果缓存中有数据且不为空,直接返回
    if (StrUtil.isNotBlank(json)){
        return JSONUtil.toBean(json, type);
    }
    // 如果缓存中的数据为空,返回空值
    if ("".equals(json)){
        return null;
    }

    // 没有缓存,查询数据库
    R result = doFallback.apply(id);

    // 如果从数据库中查询不到数据,设空值
    if (result == null){
        stringRedisTemplate.opsForValue().set(key, "", time, timeUnit);
        return null;
    }
    // 数据库中有数据
    this.set(key, result, time, timeUnit);

    return result;

}

缓存雪崩

概念

大量key同时过期或者Redis宕机,同时,大量请求打过来,导致数据库压力突然增大

解决方案

  • 如果是宕机导致的雪崩,配置Redis集群,提高可用性
  • 设置均匀分布的过期时间,防止同时过期,比如在5分钟的基础上,加上0-180秒的随机值
  • 使用限流策略
  • 服务降级,当Redis不可用时,进行服务降级,或者停用部分服务

缓存击穿

概念

大量请求同时访问一个过期的热key,导致数据库压力突然增大

解决方案

  • 使用互斥锁:大量请求进来,只有一个请求能够拿到锁访问数据库,减少不必要的访问
  • 设置永不过期:给热key不设置过期时间,或者开一个异步线程定时去更新热key的过期时间。

使用互斥锁解决

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    // 设置逻辑过期
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit timeUnit){

        // 设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));
        // 写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }
    
    public boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
        return BooleanUtil.isTrue(flag);
    }
    
    public void unlock(String key){
        stringRedisTemplate.delete(key);
    }
基于JVM的synchronized实现缓存击穿
    // 基于synchronized互斥锁解决缓存击穿
    public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> doFallback, Long time, TimeUnit timeUnit) {
        // redis键
        String key = keyPrefix + id;
        // 从redis中查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 判断是否有缓存 防止缓存穿透
        if (StrUtil.isBlank(json)){
            // 不存在
            return null;
        }
        
        // 存在
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        R result = JSONUtil.toBean(data, type);
        // 判断是否过期
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())){
            // 未过期,直接返回
            return result;
        }
        
        // 缓存过期,需要缓存重建
        String lockKey = "lock:" + id;
        // 获取互斥锁
        // 减小锁的粒度,锁的是每个键
        synchronized (lockKey.intern()){
            // 由于可能在这之前有人拿到锁并修改了,防止重复修改
            // 双重检查锁
            // 判断是否命中
            // 1.从redis中查询缓存
            String json2 = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isBlank(json2)){
                // 不存在
                return null;
            }
            // 存在
            RedisData redisData2 = JSONUtil.toBean(json, RedisData.class);
            JSONObject data2 = (JSONObject) redisData2.getData();
            R result2 = JSONUtil.toBean(data2, type);
            // 判断是否过期
            LocalDateTime expireTime2 = redisData2.getExpireTime();
            if (expireTime2.isAfter(LocalDateTime.now())){
                // 未过期,直接返回
                return result;
            }
            // 过期了,去数据库中查询新数据
            R apply = doFallback.apply(id);
            // 存入redis
            this.setWithLogicalExpire(key, apply, time, timeUnit)
        }
        // 返回旧值
        return result;
    }

这种方式可以解决穿透问题,不过效率较低,会阻塞等待获取锁,降低了吞吐量。

基于Redis的SetNX实现

同时,我们可以根据redis的setnx的原子性来优化。

    // 基于setnx互斥锁解决缓存击穿
    public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> doFallback, Long time, TimeUnit timeUnit) {
        // redis键
        String key = keyPrefix + id;
        // 从redis中查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 判断是否有缓存 防止缓存穿透
        if (StrUtil.isBlank(json)){
            // 不存在
            return null;
        }

        // 存在
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        R result = JSONUtil.toBean(data, type);
        // 判断是否过期
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())){
            // 未过期,直接返回
            return result;
        }

        // 缓存过期,需要缓存重建
        String lockKey = "lock:" + id;
        // 尝试获取锁
        boolean isLock = tryLock(lockKey);
        if (isLock){
            // 由于可能在这之前有人拿到锁并修改了,防止重复修改
            // 双重检查锁
            // 判断是否命中
            // 1.从redis中查询缓存
            String json2 = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isBlank(json2)){
                // 不存在
                return null;
            }
            // 存在
            RedisData redisData2 = JSONUtil.toBean(json, RedisData.class);
            JSONObject data2 = (JSONObject) redisData2.getData();
            R result2 = JSONUtil.toBean(data2, type);
            // 判断是否过期
            LocalDateTime expireTime2 = redisData2.getExpireTime();
            if (expireTime2.isAfter(LocalDateTime.now())){
                // 未过期,直接返回
                return result2;
            }
            // 重建缓存
            try {
                // 查询数据库
                R r1 = doFallback.apply(id);
                // 写入redis
                this.setWithLogicalExpire(key, r1, time, timeUnit);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                // 释放锁
                unlock(lockKey);
            }
        }
        // 返回旧值
        return result;
    }

如果获取锁失败,就直接返回上次的旧值,这种方式的效率较高,提高了系统的吞吐量,但是存在数据不一致的问题。

我们还可以对上述过程进行优化,将查询数据库和释放锁的过程交给一个新线程去做。

            // 重建缓存
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                // 重建缓存
                try {
                    // 查询数据库
                    R r1 = doFallback.apply(id);
                    // 写入redis
                    this.setWithLogicalExpire(key, r1, time, timeUnit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });

基于Redis的SetNX实现虽然效率较高,但是存在一个极端问题,在释放锁的过程中,有可能会误删他人的锁,因为释放锁的过程不是原子性的。

在线程1释放锁的过程中,恰好线程1cpu时间片用完,同时这个时候线程2拿到了锁,然后线程2cpu时间片用完,但是任务还没执行完,同时线程1得到新的时间片开始执行释放锁,这个时候就会把线程2的锁误删。

标签:缓存,return,String,过期,Redis,兄弟,json,key
From: https://blog.csdn.net/z2698751935/article/details/137371839

相关文章

  • Redis从入门到精通(七)Redis实战(四)库存超卖、一人一单与Redis分布式锁
    ↑↑↑请在文章开头处下载测试项目源代码↑↑↑文章目录前言4.3优惠券秒杀4.3.4库存超卖问题及其解决4.3.4.1问题分析4.3.4.2问题解决4.3.5一人一单需求4.3.5.1需求分析4.3.5.2代码实现4.3.5.3并发问题4.3.5.4悲观锁解决并发问题4.3.5.5集群环境下的并发问题......
  • 详解 Redis 在 Ubuntu 系统上的安装
    在Ubuntu20.04安装Redis1.先切换到root用户在Ubuntu20.04中,可以通过以下步骤切换到root用户:输入以下命令,以root用户身份登录:sudosu-按回车键,并输入当前用户的密码(即具有sudo权限的用户的密码)如果密码正确,将会切换到root用户,并且提示符会变为以r......
  • Redis中惰性策略的启发和流量包应用设计
    引言    在技术领域,许多中间件之所以获得巨大成功,部分原因在于它们所采用的思想之先进。这些思想解决了一个个世纪难题,接下来我将讲述一个我学习到的思想,并将其应用至工作中的案例。        惰性策略在日常编码中随处可见,但究竟什么是惰性策略呢?简而言之,惰性......
  • Redis各个方面入门详解
    目录一、Redis介绍二、分布式缓存常见的技术选型方案三、Redis和Memcached的区别和共同点四、缓存数据的处理流程五、Redis作为缓存的好处六、Redis常见数据结构以及使用场景七、Redis单线程模型八、Redis给缓存数据设置过期时间九、Redis判断数据过期的原理十......
  • 【爬虫】项目篇-爬取豆瓣电影周榜Top10,保存至Redis
    写法一:编写两个爬虫程序文件:爬虫1将豆瓣一周口碑榜的电影url添加到redis中名为movie_url的列表中(注意避免多次运行导致重复的问题);爬虫2从movie_url中读出网址,爬取每一部电影的导演、主演、类型、制片国家/地区、语言、上映日期、片长,并将它们保存到redis的hash表(自行命名)中。d......
  • Redis数据库——群集(主从、哨兵)
    目录前言 一、主从复制1.基本原理2.作用3.流程4.搭建主动复制4.1环境准备4.2修改主服务器配置4.3从服务器配置(Slave1和Slave2)4.4查看主从复制信息4.5验证主从复制二、哨兵模式——Sentinel1.定义2.原理3.作用4.组成5.工作机制6.搭建哨兵模式6.1环境准备6.......
  • Redis缓存穿透和缓存雪崩
    一、缓存穿透1什么是缓存穿透        缓存穿透说简单点就是大量请求的key根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的key发起大量请求,导致大量请求落到数据库。2处理流程如下图所示,用户......
  • redis6.2.6配置文件说明
     导游Redis版本配置文件说明###UNIT(单位)###(了解)###INCLUDES(包含)###(了解)###MODULES(模块)###(了解)###NETWORK(网络)###(需记)###TLS/SSL(安全套接字)###(了解)###GENERAL(通用)###(精通)###SNAPSHOTTING(快照)###(需记)###REPLICATION(主从)###(必会)###KEYSTRACKING(键跟踪)###(了......
  • 【Redis系列】Redis安装与使用
    ......
  • LRU缓存(超详细注释)
    /***表示双向链表中的节点。*/classNode{constructor(key=0,value=0){this.key=key;//缓存条目的唯一标识符。this.value=value;//与键关联的值。this.prev=null;//引用列表中的上一个节点。this.next......