优化系统性能:深入探讨Web层缓存与Redis应用的挑战与对策

news2024/12/26 3:50:43

Web层缓存对于提高应用性能至关重要,它通过减少重复的数据处理和数据库查询来加快响应时间。例如,如果一个用户请求的数据已经缓存,服务器可以直接从缓存中返回结果,避免了每次请求都进行复杂的计算或数据库查询。这不仅提高了应用的响应速度,还减轻了后端系统的负担。

Redis是一个流行的内存数据结构存储系统,常用于实现高效的缓存层。它支持各种数据结构,如字符串、哈希、列表、集合等,能够迅速存取数据。通过将常用的数据缓存到Redis中,应用可以大幅度降低数据库负担,同时提升用户体验。

缓存问题详解

在本章中,我们将不深入探讨Redis的基本缓存机制,而是专注于如何防范Redis失效可能带来的不必要损失。我们将详细讨论缓存穿透、缓存击穿和缓存雪崩等问题的产生原因及其解决策略。让我们开始深入了解这些内容。

缓存穿透

缓存穿透指的是查询一个根本不存在的数据时,缓存层和存储层都未能命中。这种情况通常出于容错考虑,如果存储层未能找到数据,系统通常不会将其写入缓存层。结果就是每次请求不存在的数据时,系统都需要直接访问存储层进行查询,从而失去了缓存保护后端存储的本质意义。这不仅增加了存储层的负担,也降低了系统的整体性能。

造成缓存穿透的基本原因主要有两个:

1、自身业务代码或数据问题:这类问题通常源于业务逻辑的缺陷或数据不一致。例如,如果业务代码未能正确处理某些数据查询,或数据源本身存在缺陷(如数据丢失、数据错误等),可能导致请求的查询始终无法在缓存或存储层找到对应的数据。这种情况下,缓存层无法有效地存储和返回查询结果,从而导致每次请求都需要直接访问存储层。

2、恶意攻击或爬虫行为:恶意攻击者或自动化爬虫可能会发起大量的请求,尝试查询大量不存在的数据。由于这些请求不断打击缓存和存储层,造成大量的空命中(即查询结果始终为空),不仅会消耗大量系统资源,还可能导致缓存层和存储层的压力显著增加,从而影响系统的整体性能和稳定性。

解决方案——缓存空对象

解决缓存穿透的有效方案之一是缓存空对象。这种方法涉及在缓存层中存储查询结果为“空”的标记或对象,以表明特定数据不存在。通过这种方式,当后续请求查询相同的数据时,系统可以直接从缓存层获取“空对象”,而不必重新访问存储层。这不仅减少了对存储层的频繁访问,还提高了系统的整体性能和响应速度,从而有效缓解缓存穿透问题。

String get(String key) {
    // 从缓存中获取数据
    String cacheValue = cache.get(key);

    // 缓存命中
    if (cacheValue != null) {
        return cacheValue;
    }

    // 缓存未命中,从存储中获取数据
    String storageValue = storage.get(key);

    // 如果存储中数据为空,则设置缓存并设定过期时间
    if (storageValue == null) {
        cache.set(key, "");  // 存储空对象标记
        cache.expire(key, 60 * 5);  // 设置过期时间(300秒)
    } else {
        // 存储中数据存在,则缓存该数据
        cache.set(key, storageValue);
    }

    return storageValue;
}

解决方案——布隆过滤器

对于恶意攻击中通过请求大量不存在的数据造成的缓存穿透问题,可以使用布隆过滤器来进行初步过滤。布隆过滤器是一种空间效率极高的概率型数据结构,它能有效地判断一个元素是否可能存在于集合中。具体而言,当布隆过滤器表示某个值可能存在时,实际情况可能是该值存在,也可能是布隆过滤器的误判;但当布隆过滤器表示某个值不存在时,则可以肯定该值确实不存在。

image

布隆过滤器是一种高效的概率型数据结构,由一个大型位数组和多个独立的无偏哈希函数组成。无偏哈希函数的特点是能够将输入元素的哈希值均匀地分布到位数组中,减少哈希冲突。添加一个键(key)到布隆过滤器时,首先使用这些哈希函数对键进行哈希运算,每个哈希函数生成一个整数索引值。然后,这些索引值经过对位数组长度的取模运算,确定在位数组中的具体位置。接着,将这些位置的值设置为1,标记该键的存在。

当查询布隆过滤器中某个键(key)是否存在时,操作过程与添加键时类似。首先,使用多个哈希函数对键进行哈希运算,得到多个位置索引。然后,检查这些索引对应的位数组位置。如果所有相关位置的值都是1,那么可以推测该键可能存在;否则,如果有任意一个位置的值为0,则可以确定该键一定不存在。值得注意的是,即使所有相关位置的值均为1,这也仅仅意味着该键“可能”存在,而不能绝对确认,因为这些位置可能已经被其他键置为1。通过调整位数组的大小和哈希函数的数量,可以优化布隆过滤器的性能,达到较好的准确性与效率平衡。

这种方法特别适用于数据命中率不高、数据集相对固定、对实时性要求不高的应用场景,尤其是在数据集较大时,布隆过滤器可以显著减少缓存空间的占用。尽管布隆过滤器的实现可能会增加代码维护的复杂度,但其带来的内存效率和查询速度的优势通常值得投入。

布隆过滤器在这类场景中的有效性得益于其能处理大规模数据集而只占用较少的内存空间。为了实现布隆过滤器,可以使用Redisson,这是一个支持分布式布隆过滤器的Java客户端。要在项目中引入Redisson,可以添加以下依赖项:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.16.2</version> <!-- 请根据需要选择合适的版本 -->
</dependency>

示例伪代码:

package com.redisson;

import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedissonBloomFilter {

    public static void main(String[] args) {
        // 配置Redisson客户端,连接到Redis服务器
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");

        // 创建Redisson客户端
        RedissonClient redisson = Redisson.create(config);

        // 获取布隆过滤器实例,名称为 "nameList"
        RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");

        // 初始化布隆过滤器,预计元素数量为100,000,000,误差率为3%
        bloomFilter.tryInit(100_000_000L, 0.03);

        // 将元素 "zhuge" 插入到布隆过滤器中
        bloomFilter.add("xiaoyu");

        // 查询布隆过滤器,检查元素是否存在
        System.out.println("Contains 'huahua': " + bloomFilter.contains("huahua")); // 应为 false
        System.out.println("Contains 'lin': " + bloomFilter.contains("lin")); // 应为 false
        System.out.println("Contains 'xiaoyu': " + bloomFilter.contains("xiaoyu")); // 应为 true

        // 关闭Redisson客户端
        redisson.shutdown();
    }
}

使用布隆过滤器时,首先需要将所有预期的数据元素提前插入布隆过滤器中,以便它能够通过其位数组结构和哈希函数有效地检测元素的存在性。在进行数据插入时,也必须实时更新布隆过滤器,以保证其数据的准确性。

以下是布隆过滤器缓存过滤的伪代码示例,展示了如何在初始化和数据添加过程中操作布隆过滤器:

// 初始化布隆过滤器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");

// 设置布隆过滤器的期望元素数量和误差率
bloomFilter.tryInit(100_000_000L, 0.03);

// 将所有数据插入布隆过滤器
void init(List<String> keys) {
    for (String key : keys) {
        bloomFilter.add(key);  
    }
}

// 从缓存中获取数据
String get(String key) {
    // 检查布隆过滤器中是否存在 key
    if (!bloomFilter.contains(key)) {
        return ""; // 如果布隆过滤器中不存在,返回空字符串
    }

    // 从缓存中获取数据
    String cacheValue = cache.get(key);

    // 如果缓存值为空,则从存储中获取
    if (StringUtils.isBlank(cacheValue)) {
        String storageValue = storage.get(key);
        if (storageValue != null) {
            cache.set(key, storageValue); // 存储非空数据到缓存
        } else {
            cache.expire(key, 300); // 设置过期时间为300秒
        }
        return storageValue;
    } else {
        // 缓存值非空,直接返回
        return cacheValue;
    }
}

注意:布隆过滤器不能删除数据,如果要删除得重新初始化数据。

缓存失效(击穿)

由于在同一时间大量缓存失效可能会导致大量请求同时穿透缓存,直接访问数据库,这种情况可能会导致数据库瞬间承受过大的压力,甚至可能引发数据库崩溃。

解决方案——随机过期时间

为了缓解这一问题,我们可以采取一种策略:在批量增加缓存时,将这一批数据的缓存过期时间设置为一个时间段内的不同时间。具体来说,可以对每个缓存项设置不同的过期时间,这样可以避免所有缓存项在同一时刻失效,从而减少瞬时请求对数据库的冲击。

以下是具体的示例伪代码:

String get(String key) {
    // 从缓存中获取数据
    String cacheValue = cache.get(key);

    // 如果缓存为空
    if (StringUtils.isBlank(cacheValue)) {
        // 从存储中获取数据
        String storageValue = storage.get(key);
        
        // 如果存储中的数据存在
        if (storageValue != null) {
            cache.set(key, storageValue);
            // 设置一个过期时间(300到600秒之间的随机值)
            int expireTime = 300 + new Random().nextInt(301); // Random range: 300 to 600
            cache.expire(key, expireTime);
        } else {
            // 存储中没有数据时,设置缓存的默认过期时间(300秒)
            cache.expire(key, 300);
        }
        return storageValue;
    } else {
        // 返回缓存中的数据
        return cacheValue;
    }
}

缓存雪崩

缓存雪崩是指在缓存层出现故障或负载过高的情况下,导致大量请求直接涌向后端存储层,从而引发存储层的过载或宕机现象。通常,缓存层的作用是有效地承载和分担请求流量,保护后端存储层免受高并发请求的压力。

然而,当缓存层由于某些原因无法继续提供服务时,比如遇到超大并发的冲击或者缓存设计不当(例如,访问一个极大的缓存项 bigkey 导致缓存性能急剧下降),大量的请求将会转发到存储层。此时,存储层的请求量会急剧增加,可能会导致存储层也发生过载或宕机,从而引发系统级的故障。这种现象被称为“缓存雪崩”。

解决方案

为了有效预防和解决缓存雪崩问题,可以从以下三个方面着手:

1、保证缓存层服务的高可用性

  • 确保缓存层的高可用性是避免缓存雪崩的关键措施。可以使用如 Redis Sentinel 或 Redis Cluster 等工具来实现缓存的高可用性。Redis Sentinel 提供自动故障转移和监控功能,可以在主节点出现问题时自动将从节点提升为新的主节点,从而保持服务的连续性。Redis Cluster 通过数据分片和节点间的复制,进一步提高了系统的可用性和扩展性。这样,即使部分节点发生故障,系统仍能正常运行并继续处理请求。

2、依赖隔离组件进行限流、熔断和降级

  • 利用限流和熔断机制来保护后端服务免受突发请求的冲击,可以有效缓解缓存雪崩带来的压力。例如,使用 Sentinel 或 Hystrix 等限流和熔断组件来实施流量控制和服务降级。针对不同类型的数据,可以采取不同的处理策略:非核心数据:例如电商平台中的商品属性或用户信息。如果缓存中的这些数据丢失,应用可以直接返回预定义的默认降级信息、空值或错误提示,而不是直接查询后端存储。这种方式可以减少对后端存储的压力,同时为用户提供一些基本的反馈。核心数据:例如电商平台中的商品库存。对于这些关键数据,仍然可以尝试从缓存中查询,如果缓存缺失,则通过数据库读取。这样即使缓存不可用,核心数据的读取仍可得到保证,避免了因缓存雪崩导致的系统功能丧失。

3、提前演练和预案制定

  • 在项目上线之前,进行充分的演练和测试,模拟缓存层宕机后的应用和后端负载情况,识别潜在问题并制定相应的预案。这包括模拟缓存失效、后端服务过载等情况,观察系统表现,并根据测试结果调整系统配置和策略。通过这些演练,可以发现系统的弱点,并制定相应的应急措施,以应对实际生产环境中的突发情况。这不仅可以提升系统的鲁棒性,还可以确保在缓存雪崩发生时,系统能够迅速恢复正常运行。

通过综合运用这些措施,可以显著降低缓存雪崩带来的风险,提升系统的稳定性和性能。

总结

Web层缓存显著提高了应用性能,通过减少重复的数据处理和数据库查询来加快响应时间。Redis作为高效的内存数据结构存储系统,在实现缓存层中发挥了重要作用,它支持各种数据结构,能够迅速存取数据,从而减少数据库负担,提升用户体验。

然而,缓存机制也面临挑战,如缓存穿透、缓存击穿和缓存雪崩等问题。缓存穿透通过缓存空对象和布隆过滤器来解决,前者避免了每次查询都访问数据库,后者有效减少了恶意请求的影响。缓存击穿则通过设置随机过期时间来缓解,这样可以避免大量请求同时涌向数据库。对于缓存雪崩,保证缓存层的高可用性、采用限流和熔断机制,以及制定充分的预案是关键。

有效的缓存管理不仅提升了系统性能,还增强了系统的稳定性。了解并解决这些缓存问题,能确保系统在高并发环境下保持高效、稳定的运行。精心设计和实施缓存策略是优化应用性能的基础,持续关注和调整这些策略可以帮助系统应对各种挑战,保持良好的用户体验。

文章转载自:努力的小雨

原文链接:https://www.cnblogs.com/guoxiaoyu/p/18350801

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

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

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

相关文章

【iOS】iOS中简单的网络请求

目录 前言认识API和RULAPIURL两者的联系 简单的网络请求1. 创建URL对象2. 创建URLRequest对象3. 设置请求头&#xff08;如果需要&#xff09;4. 发送请求异步请求同步请求 5. 处理请求结果6.启动数据任务完整代码示例及运行结果&#xff1a; 关于同步请求和异步请求同步请求异…

vue3 cascader省市区三级联动如何指定字段,如何根据id查到对应的名字

如果我们接口数据字段名不是value和code。要加个props :props"{ value:code,label:regionName}"根据id查name需要一个ref和一个change事件<el-cascader :options"areaData" ref"addressCodeRef" change"handleChange" :props"…

MySQL(五)——表设计(约束、范式、表关系)

文章目录 表设计约束非空约束&#xff08;NOT NULL&#xff09;唯一约束&#xff08;UNIQUE&#xff09;主键约束&#xff08;PRIMARY KEY&#xff09;外键约束&#xff08;FOREIGN KEY&#xff09;默认值约束&#xff08;DEFAULT&#xff09;检查约束&#xff08;CHECK&#x…

考研数学最迟什么时候要结束强化?10月才做真题是不是晚了?

已经快9月了&#xff0c;很多同学的考研数学的强化也接近尾声&#xff0c;这个时候要注意两个事情&#xff1a; 1、如果你还有很多没学完&#xff0c;不要强行收尾&#xff0c;稳扎稳打的把强化给结束了。 2、强化结束之前&#xff0c;要清理完所有在强化阶段产生的错题&…

Kafka日志及常见问题

目录 1.Topic下的消息是如何存储的 1.1log文件追加记录所有消息 1.2index和timeindex加速读取日志信息 2.文件清理机制 2.1如何判断哪些日志文件过期了 2.2日志清理策略 3.Kafka的文件高效读写机制 3.1Kafka的文件结构 3.2顺序写磁盘 3.3零拷贝 3.3.1传统IO 3.3.2m…

应用层与传输层

1.应用层 很多时候这一层的协议是程序员自定义的应用层协议&#xff08;相当于一种约定&#xff0c;约定数据如何进行传输&#xff09;。 eg&#xff1a; 实现登录的场景&#xff1a; 此时前端就需要与后端约定请求&#xff08;假设约定使用ajax请求&#xff09;中的一些参…

接口自动化测试面试题目详解

1、get和post区别是什么&#xff1f; 答&#xff1a;POST和GET都是向服务器提交数据&#xff0c;并且都会从服务器获取数据。 区别&#xff1a; &#xff08;1&#xff09;传送方式&#xff1a;get通过地址栏传输&#xff0c;post通过报文传输 &#xff08;2&#xff09;传…

c语言 自定义类型--枚举 、联合 #枚举类型的定义 #枚举的优点 #枚举的使用 #联合类型的定义 #联合的特点 #联合大小的计算

文章目录 前言 一、枚举 (一)、枚举类型的定义 (二)、枚举的优点 (三)、枚举的使用 二、联合 (一)、联合类型的定义 (二)、联合的特点 (三)、联合大小的计算 总结 前言 路漫漫其修远兮&#xff0c;吾将上下而求索。 枚举、联合跟结构体很像&#xff0c;想要细致地了…

基于SpringBoot+Vue+uniapp的“村游网”系统的微信小程序开发的详细设计和实现(源码+lw+部署文档+讲解等)

文章目录 前言详细视频演示具体实现截图技术栈后端框架SpringBoot前端框架Vue持久层框架MyBaitsPlus 系统测试系统测试目的系统功能测试系统测试结论 为什么选择我代码参考数据库参考源码获取源码获取 前言 &#x1f31e;博主介绍 &#xff1a;✌全网粉丝15W,CSDN特邀作者、21…

基于状态机实现WIFI模组物联网

1.0 状态机框架原理 如果成功的话就连接热点&#xff0c;如果失败就返回AT通信检查&#xff0c;如果AT通信检查还是失败就放回硬件复位这个状态&#xff0c;如果热点链接成功&#xff0c;就连接MQTT指令&#xff0c;如果失败就返回AT通信检查&#xff0c;如果成功就连接云平台通…

跟着B站前端面试总结回顾前端基础知识(一)

组件划分标准 组件划分_哔哩哔哩_bilibili 在前端Vue开发中&#xff0c;组件的划分是构建高效、可维护应用的关键步骤。Vue组件的划分标准通常基于多个方面的考虑&#xff0c;包括但不限于功能独立性、复用性、可维护性和可扩展性。以下是一些Vue组件划分的标准&#xff1a; …

破解历史合同“旧题” 答好集体经济“新篇”

——汕头市龙湖区&#xff1a;全面推进乡村振兴战略 实现农村集体经济新飞跃 农村集体资产资源是乡村赖以生存的家底&#xff0c;也是村集体经济发展壮大、更好推动乡村振兴战略加力提速的承载。自2023年10月开始&#xff0c;在汕头市龙湖区的广袤乡村上&#xff0c;一场关于村…

图解搜索算法(BFS、DFS、Dijstra算法、KSP算法、A*算法)

文章目录 深度优先搜索算法广度优先搜索算法Dijkstra算法KSP算法A*算法 由于在工作中用到了BFS算法、DFS算法、Dijkstra算法、KSP算法&#xff0c;因此将上述算法的工作原理记录一下&#xff0c;同时用图解的方式解释相应的算法。A*算法由于本文在工作中&#xff0c;还没用过&a…

深度学习语义分割篇——LR-ASPP原理详解+源码实战

&#x1f34a;作者简介&#xff1a;秃头小苏&#xff0c;致力于用最通俗的语言描述问题 &#x1f34a;专栏推荐&#xff1a;深度学习网络原理与实战 &#x1f34a;近期目标&#xff1a;写好专栏的每一篇文章 &#x1f34a;支持小苏&#xff1a;点赞&#x1f44d;&#x1f3fc;、…

简单使用富有创造力的DALL·E 3 图像生成器——OpenAI Images Generations API

OpenAI Images Generations API 申请及使用 DALL-E 3 是 OpenAI 开发的两个版本的图像生成模型&#xff0c;它们能够根据文本描述生成高质量的图像。 本文档主要介绍 OpenAI Images Generations API 操作的使用流程&#xff0c;利用它我们可以轻松使用官方 OpenAI DALL-E 的图…

类和构造函数之间的继承

类之间构造函数的继承是面向对象编程中的一个重要概念&#xff0c;它允许一个类&#xff08;子类&#xff09;继承另一个类&#xff08;父类&#xff09;的属性和方法。通过这种方式&#xff0c;子类可以复用父类的代码&#xff0c;从而避免重复&#xff0c;提高代码的可维护性…

Swagger的增强knife4j

效果图 导入依赖 <!--不是导入swagger原因就是&#xff1a;knife4j对swagger增强--><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>3.0.2</version><…

考研数学| 张宇线代9讲换李永乐线代讲义,强化来得及吗?

张宇线代9讲&#xff1a; 张宇老师的线代九讲和张宇老师的线代课程搭配使用&#xff0c;相对于更适合线代底子足够强的同学。整体书的题目难度较大&#xff0c;所以开始刷了之后发现不适合自己可以选择换一本习题册&#xff0c;当然如果担心时间不够还是想尽力刷下去的话可以去…

8款每天都能用到的网站和APP,值得收藏

5个网站3个APP&#xff0c;都是自己每天常用的一些工具&#xff0c;强大实用&#xff0c;能给自己省不少事&#xff0c;分享给大家~ 1、奶牛快传 https://cowtransfer.com/ 一个让你轻松上传和分享大文件的网站。跟那些需要下载app、操作复杂的服务不同&#xff0c;奶牛快传…

随机森林与线性回归

集成学习方法之随机森林 集成学习&#xff08;Ensemble Learning&#xff09;是一种通过组合多个分类器来提高预测性能的方法。主要分为三种类型:Bagging、Boosting和Stacking。以下是集成学习的基本步骤和概念&#xff1a; 1数据采样&#xff1a;从训练集中有放回地随机抽取…