Redis企业开发实战(二)——点评项目之商户缓存查询

news2025/2/7 9:17:11

目录

一、缓存介绍

 二、缓存更新策略

三、如何保证redis与数据库一致性

1.解决方案概述

2.双写策略

3.双删策略

3.1延迟双删的目的

4.数据重要程度划分 

四、缓存穿透 

(一)缓存穿透解决方案

(二)缓存穿透示意图 

五、缓存雪崩

(一)缓存雪崩解决方案

(二)缓存雪崩示意图 

六、缓存击穿(热点Key问题)

(一)缓存击穿解决方案

1.互斥锁

2.逻辑过期 

3.两种解决方案对比

4.CAP理论

4.1CAP理论概述

4.2CAP理论的核心观点

4.3实际应用中的考量

4.4结合其他技术手段优化

七、封装工具类


一、缓存介绍

        缓存就是数据交换的缓冲区(称作Cache),是临时存储数据的地方,一般读写性能较高。       为什么要使用缓存?一句话:速度快,好用。

        缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力。实际开发过程中,企业的数据量,少则几十万,多则几千万,这么大数据量,如果没有缓存来作为"避震器",系统是几乎撑不住的,所以企业会大量运用到缓存技术。但是缓存也会增加代码复杂度和运营的成本:

缓存的作用:

  • 降低后端负载
  • 提高读写效率,降低响应时间

缓存的成本:

  • 数据一致性成本
  • 代码维护成本
  • 运维成本 

 

 二、缓存更新策略

读操作:

  • 缓存命中则直接返回
  • 缓存未命中则查询数据库,并写入缓存,设定超时时间

写操作:

  • 先写数据库,再删除缓存
  • 要确保数据库与缓存操作的原子性

三、如何保证redis与数据库一致性

1.解决方案概述

2.双写策略

在更新数据库的同时也更新缓存,以保证两者的数据同步。

3.双删策略

在更新数据库前和之后对缓存进行两次删除操作

3.1延迟双删的目的
  1. 减少缓存穿透的时间窗口:第一次删除是为了立即让旧的缓存失效,防止后续请求获取到过期的数据。而第二次删除主要是为了确保在这段时间内,如果有任何尝试将旧数据重新写入缓存的操作,可以被清理掉,避免脏数据进入缓存

  2. 降低缓存击穿的风险:通过延迟一段时间再进行第二次删除,希望这段时间足够让第一个读取数据库并更新缓存的操作完成,从而减少其他并发请求同时查询数据库的可能性。

4.数据重要程度划分 

四、缓存穿透 

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

(一)缓存穿透解决方案

1.缓存空对象

        优点:实现简单,维护方便;缺点:额外的内存消耗;可能造成短期的不一致

2.布隆过滤器

        优点:内存占用较少,没有多余key;缺点:实现复杂;存在误判可能

3.检查非法请求,封禁其IP及其账号
4.增强id的复杂度,避免被猜测id规律
5.做好数据的基础格式校验
6.加强用户权限校验
7.做好热点参数的限流

(二)缓存穿透示意图 

五、缓存雪崩

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

(一)缓存雪崩解决方案

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

(二)缓存雪崩示意图 

六、缓存击穿(热点Key问题)

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

(一)缓存击穿解决方案

1.互斥锁

          因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,可以采用tryLock方法 + double check来解决这样的问题。

        假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。

 

2.逻辑过期 

        把过期时间设置在 redis的value中。注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。

        假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个新的线程,去进行以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。

        这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。 

3.两种解决方案对比

        互斥锁方案:由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响。

        逻辑过期方案:线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦。 

解决方案

优点

缺点

互斥锁
  • 没有额外的内存消耗
  • 保证一致性
  • 实现简单
  • 现成需要等待,性能受影响
  • 可能有死锁风险
逻辑过期
  • 现成无需等待,性能较好
  • 不保证一致性
  • 存储过期时间,有额外的内存消耗
  • 实现复杂

4.CAP理论

4.1CAP理论概述

        CAP理论是分布式系统设计中的一个基础概念,由计算机科学家Eric Brewer在2000年提出。该理论指出,在任何分布式系统中,无法同时满足以下三个属性:

        1.Consistency(一致性):每个读操作都能看到最近的一次写操作的结果。这意味着,在任意时刻,所有节点访问的数据都是最新的且一致的。换句话说,系统的状态看起来就像只有一个副本一样。

        2.Availability(可用性):每个请求都能接收到响应,而不论该响应是否成功。即系统总是可以正常处理客户端的请求,即使部分组件出现故障。这保证了服务的持续可用性。

        3.Partition Tolerance(分区容错性):即使网络发生分区故障(即部分节点之间无法通信),系统仍然能够继续运行并提供服务。由于网络分区几乎是不可避免的现实,因此大多数分布式系统都必须支持分区容错性。

4.2CAP理论的核心观点

        根据CAP理论,在网络可能发生分区的情况下,系统只能在一致性和可用性之间做出选择,也就是说,无法同时实现所有三个特性。具体来说:

  •         CP(一致性和分区容错性):在这种情况下,系统会选择保证数据的一致性,即使这意味着在某些情况下需要牺牲可用性。当网络分区发生时,系统可能会阻止某些操作,直到确保所有节点的数据是一致的。
  •         AP(可用性和分区容错性):这种系统会优先保证服务的可用性,即使在网络分区期间可能暂时牺牲数据的一致性。这意味着所有节点都可以接受请求,但不同节点上的数据可能不完全一致,直到网络恢复并且数据同步完成。

        值得注意的是,虽然理论上不可能同时达到这三个目标,但在实践中,很多系统会采用“最终一致性”的策略来平衡一致性和可用性,允许短时间内数据不一致,但通过异步复制等机制保证最终所有节点的数据达成一致。

4.3实际应用中的考量

        在实际的分布式系统设计中,开发者通常需要基于业务需求和应用场景来权衡这些属性:

  •         对于一些金融交易系统或银行应用,可能更倾向于选择CP系统,因为数据的一致性至关重要。
  •         对于社交网络或在线论坛这样的应用,可能会选择AP系统,因为高可用性和用户体验更为重要,而短时间内的数据不一致是可以接受的。
4.4结合其他技术手段优化

        此外,现代分布式系统还会结合使用其他技术手段来优化性能和可靠性,例如:

  1. Quorum机制:通过设置读写成功的条件来调整一致性和可用性的平衡。
  2. 版本控制和冲突解决机制 :允许存在短暂的数据不一致,并提供机制解决后续的数据冲突问题。
  3. 缓存、异步复制、读写分离等 :利用这些技术可以在一定程度上缓解一致性和可用性之间的矛盾。

七、封装工具类

@Component
@Slf4j
public class CacheClient {
    private final StringRedisTemplate stringRedisTemplate;

    // 线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    // 存放缓存数据
    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    // 设置逻辑过期
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    // 设置缓存穿透方法
    public <R, ID> R queryWithPassThrough(
            String keyPrefix,
            ID id,
            Class<R> type,
            Function<ID, R> dbFallback,
            Long time,
            TimeUnit unit) {
        // 设置缓存key
        String key = keyPrefix + id;
        // 去redis中查询
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)) {
            // 如果存在,则直接返回
            return JSONUtil.toBean(json, type);
        }
        // 如果缓存中没有查到,则判断是否为空值
        if (json != null) {
            // 不是空值,返回null
            return null;
        }

        // 缓存中不存在,查询数据库
        R r = dbFallback.apply(id);
        // 如果数据库中不存在,则存储空值,并设置有效期
        if (r == null) {
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 数据库中存在,则写入redis中
        this.set(key, r, time, unit);
        return r;
    }

    // 设置逻辑过期缓存击穿方法
    public <R, ID> R queryWithLogicalExpire(
            String keyPrefix,
            ID id,
            Class<R> type,
            Function<ID, R> dbFallback,
            Long time,
            TimeUnit unit) {
        // 设置缓存查询的key
        String key = keyPrefix + id;
        // 去redis中查询,如果未命中
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(json)) {
            // 找到之前存放的空值,直接返回null
            return null;
        }

        // 如果redis未命中,先将json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 判断缓存是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 缓存未过期,直接返回
            return r;
        }
        // 缓存已过期,需要缓存重建
        // 设置锁的key
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        // 获取锁
        boolean isLock = tryLock(lockKey);
        // 如果获取到锁,则直接返回
        if (isLock) {
            // 开启独立线程,重建缓存
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R r1 = dbFallback.apply(id);
                    // 写入redis
                    this.setWithLogicalExpire(key, r1, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unLock(lockKey);
                }
            });
        }
        // 没有获取到锁,返回旧数据
        return r;
    }

    // 设置互斥锁解决缓存击穿方法
    public <R, ID> R queryWithMutex(
            String keyPrefix,
            ID id,
            Class<R> type,
            Function<ID, R> dbFallback,
            Long time,
            TimeUnit unit) {
        // 设置缓存查询的key
        String key = keyPrefix + id;
        // 去redis中查询
        String json = stringRedisTemplate.opsForValue().get(key);
        // 判断缓存中是否存在
        // 如果存在,则直接返回
        if (StrUtil.isNotBlank(json)) {
            return JSONUtil.toBean(json, type);
        }
        // 如果缓存中不存在,则判断是否为空值
        if (json != null) {
            return null;
        }

        // 缓存中不存在,实现缓存重建
        // 设置锁的key
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        // 获取锁
        boolean isLock = tryLock(lockKey);
        R r = null;
        try {
            // 判断获取锁是否成功
            if (!isLock) {
                // 如果获取锁失败,则睡眠,重新再次尝试获取锁
                Thread.sleep(50);
                return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
            }
            // 如果获取锁成功,则查询数据库
            r = dbFallback.apply(id);
            // 判断数据库中的数据是否存在
            if (r == null) {
                // 将空值写入redis中
                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            this.set(key, r, time, unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 释放锁
            unLock(lockKey);
        }
        return r;
    }

    // 尝试获取锁
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    // 释放锁
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2294212.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RK3568中使用QT opencv(显示基础图像)

文章目录 一、查看对应的开发环境是否有opencv的库二、QT使用opencv一、查看对应的开发环境是否有opencv的库 在开发板中的/usr/lib目录下查看是否有opencv的库: 这里使用的是正点原子的ubuntu虚拟机,在他的虚拟机里面已经安装好了opencv的库。 二、QT使用opencv 在QT pr…

C++模板编程——完美转发与可变参函数模板

1 基础概念 首先介绍几个概念&#xff1a; 假设现在有A、B、C三个函数。 直接调用&#xff1a;在A函数中调用C就叫做直接调用&#xff0c;不拐弯抹角。转发&#xff1a;在A函数中调用B函数&#xff0c;在B函数调用C函数&#xff0c;这就叫做转发。这种情况下&#xff0c;B函数…

蓝桥杯单片机(十)PWM脉宽调制信号的发生与控制

模块训练&#xff1a; 一、PWM基本原理 1.占空比 2.脉宽周期与占空比 当PWM脉宽信号的频率确定时&#xff0c;脉宽周期也确定了&#xff0c;此时改变占空比即可。当利用PWM脉宽周期改变LED灯的亮度时&#xff0c;灯是低电平亮&#xff0c;所以将低电平占空比改成10%即可实现…

Redis --- 使用HyperLogLog实现UV(访客量)

UV 和 PV 是网站或应用数据分析中的常用指标&#xff0c;用于衡量用户活跃度和页面访问量。 UV (Unique Visitor 独立访客)&#xff1a; 指的是在一定时间内访问过网站或应用的独立用户数量。通常根据用户的 IP 地址、Cookies 或用户 ID 等来唯一标识一个用户。示例&#xff1…

postgresql-COALESCE函数、NULLIF函数、NVL函数使用

COALESCE函数 COALESCE函数是返回参数中的第一个非null的值&#xff0c;它要求参数中至少有一个是非null的; select coalesce(1,null,2),coalesce(null,2,1),coalesce(null,null,null); NULLIF(ex1,ex2)函数 如果ex1与ex2相等则返回Null&#xff0c;不相等返回第一个表达式的值…

【办公类-99-01】20250201学具PDF打印会缩小一圈——解决办法:换一个PDF阅读器

背景需求&#xff1a; 2024年1月13日&#xff0c;快要放寒假了&#xff0c;组长拿着我们班的打印好的一叠教案来调整。 “前面周计划下面的家园共育有调整&#xff0c;你自己看批注。” “还有你这个教案部分的模版有问题&#xff0c;太小&#xff08;窄&#xff09;了。考虑…

Macos安装APOC拓展库

文章目录 说明错误提示原因分析解决方法 说明 Macos安装APOC核心库 错误提示 There is no procedure with the name apoc.generate.ba registered for this database instance. Please ensure youve spelled the procedure name correctly and that the procedure is prope…

Google C++ Style / 谷歌C++开源风格

文章目录 前言1. 头文件1.1 自给自足的头文件1.2 #define 防护符1.3 导入你的依赖1.4 前向声明1.5 内联函数1.6 #include 的路径及顺序 2. 作用域2.1 命名空间2.2 内部链接2.3 非成员函数、静态成员函数和全局函数2.4 局部变量2.5 静态和全局变量2.6 thread_local 变量 3. 类3.…

HELLOCTF反序列化靶场全解

level 2 <?php/* --- HelloCTF - 反序列化靶场 关卡 2 : 类值的传递 --- HINT&#xff1a;尝试将flag传递出来~# -*- coding: utf-8 -*- # Author: 探姬 # Date: 2024-07-01 20:30 # Repo: github.com/ProbiusOfficial/PHPSerialize-labs # email: adminhello-ctf.com…

禅道社区版项目管理软件部署(记录篇)

系统要求&#xff08;这里推荐使用docker容器化方式&#xff09;安装前的准备Docker快速安装最后通过查看地址验证是否部署成功开始界面化安装配置 禅道&#xff08;ZenTao&#xff09;是一款国产开源的项目管理软件&#xff0c;专注于敏捷开发流程&#xff0c;支持 Scrum 和 K…

【Redis】主从模式,哨兵,集群

主从复制 单点问题&#xff1a; 在分布式系统中&#xff0c;如果某个服务器程序&#xff0c;只有一个节点&#xff08;也就是一个物理服务器&#xff09;来部署这个服务器程序的话&#xff0c;那么可能会出现以下问题&#xff1a; 1.可用性问题&#xff1a;如果这个机器挂了…

DockerFile详细学习

目录 1.DockerFile介绍 2.DockerFile常用指令 3.指令详细讲解 4.实例 构建Node-Exporter 构建Alertmanager 构建Mariadb 1.DockerFile介绍 什么是 Dockerfile&#xff1f; Dockerfile 是一个文本文件&#xff0c;包含了构建 Docker 镜像的所有指令。 Dockerfile 是一…

寒假2.5

题解 web:[网鼎杯 2020 朱雀组]phpweb 打开网址&#xff0c;一直在刷新&#xff0c;并有一段警告 翻译一下 查看源码 每隔五秒钟将会提交一次form1&#xff0c;index.php用post方式提交了两个参数func和p&#xff0c;func的值为date&#xff0c;p的值为Y-m-d h:i:s a 执行fu…

中国销冠,比亚迪1月销量超30万台,出口量飙升83%

近日&#xff0c;比亚迪公布的 1 月销量数据成为了汽车行业热议的焦点&#xff0c;比亚迪凭借 300538 辆的销量成绩&#xff0c;进一步巩固了其在新能源汽车市场的统治力&#xff0c;再次稳坐中国品牌汽车市场销冠宝座。 在 1 月的销售数据中&#xff0c;比亚迪王朝丨海洋系列无…

大数据sql查询速度慢有哪些原因

1.索引问题 可能缺少索引&#xff0c;也有可能是索引不生效 2.连接数配置&#xff1a;连接数过少/连接池比较小 连接数过 3.sql本身有问题&#xff0c;响应比较慢&#xff0c;比如多表 4.缓存池大小 可能是缓存问题&#xff08;命中率>99%&#xff09; 5.加了锁 6. redis&a…

2 fastAPI请求参数

1. 路径参数 (Path Parameters) 路径参数是 URL 路径的一部分&#xff0c;通常用于标识资源的唯一性。路径参数在 FastAPI 中通过在路由装饰器中使用大括号 {} 来定义。 获取路径参数的方式 from fastapi import FastAPIapp FastAPI()app.get("/items/{item_id}"…

为何实现大语言模型的高效推理以及充分释放 AI 芯片的计算能力对于企业级落地应用来说,被认为具备显著的研究价值与重要意义?

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ AI 芯片&#xff1a;为人工智能而生的 “大脑” AI 芯片&#xff0c;又称人工智能加速器或计算卡&#xff0c;是专为加速人工智能应用&#xff0c;特别是深度学习任务设计的专用集成电路&#xff08;A…

C++11详解(二) -- 引用折叠和完美转发

文章目录 2. 右值引用和移动语义2.6 类型分类&#xff08;实践中没什么用&#xff09;2.7 引用折叠2.8 完美转发2.9 引用折叠和完美转发的实例 2. 右值引用和移动语义 2.6 类型分类&#xff08;实践中没什么用&#xff09; C11以后&#xff0c;进一步对类型进行了划分&#x…

AI 编程工具—Cursor 进阶篇 文章改写生成整理爬取

AI 编程工具—Cursor 进阶篇 文章改写生成整理爬取 其实对做自媒体的人而言,整理素材其实是一件非常耗时的事情,今天我们来看一下如何使用Cursor来帮我们解决这些问题,首先我们要建一个单独的项目,因为这个项目不涉及任何代码操作,只是文字相关的事情,还有就是这个项目需…

Yageo国巨的RC系列0402封装1%电阻库来了

工作使用Cadence多年&#xff0c;很多时候麻烦的就是整理BOM&#xff0c;因为设计原理图的时候图省事&#xff0c;可能只修改value值和封装。 但是厂家&#xff0c;规格型号&#xff0c;物料描述等属性需要在最后的时候一行一行的修改&#xff0c;繁琐又容易出错&#xff0c;过…