Redis作为缓存应用场景分析

news2024/11/27 2:14:59

为什么使用缓存

Redis是一个内存型数据库,也就是说,所有的数据都会存在与内存中,基于Redis的高性能特性,我们将Redis用在缓存场景非常广泛。使用起来方便,响应也是远超关系型数据库。

应用场景

Redis的应用场景非常广泛。虽然Redis是一个key-value的内存数据库,但在实际场景中,Redis经常被作为缓存来使用,如面对数据高并发的读写、海量数据的读写等。
举个例子,A网站首页一天有100万人访问,其中有一个“积分商城”的板块,要直接从数据库查询,那么一天就要多消耗100万次数据库请求。如果将这些数据储存到Redis(内存)中,要用的时候,直接从内存调取,不仅可以大大节省系统直接读取磁盘来获得数据的IO开销,提高服务器的资源利用率,还能极大地提升速度。
比如很多大型电商网站、视频网站和游戏应用等,存在大规模数据访问,对数据查询效率要求高。Redis服务可实现页面缓存、应用缓存、状态缓存、事件并行处理,能够有效减少数据库磁盘IO,提高数据查询效率,减轻管理维护工作量,降低数据库存储成本。对传统磁盘数据库是一个重要的补充,成为了互联网应用,尤其是支持高并发访问的互联网应用必不可少的基础服务之一。
在这里插入图片描述

具体而言,分布式缓存Redis可用于以下场景:

1、页面缓存

Redis可将Web页面的内容片段,包括HTML,CSS和图片等静态数据,缓存到Redis实例,提高网站的访问性能。
比如在电商类应用中,热销商品展示、秒杀推荐等数据面临高并发读的压力,分布式缓存Redis的高并发及灵活扩展,可轻松支持此类应用。

2、状态缓存

Redis可将Session会话状态及应用横向扩展时的状态数据等缓存到DCS实例,实现状态数据共享。在应对游戏应用中爆发式增长的玩家数据存储和读写请求时,使用分布式缓存Redis可通过将热点数据放入缓存,加快用户端访问速度,提升用户体验。

3、应用对象缓存

Redis可作为服务层的二级缓存对外提供服务,减轻数据库的负载压力,加速应用访问。

4、事件缓存

Redis可提供针对事件流的连续查询(continuous query)处理技术,满足实时性需求。

使用缓存的收益和成本

如图左侧为客户端直接调用存储层的架构,右侧为比较典型的缓存层+存储层架构,下面分析一下缓存加入后带来的收益和成本。
在这里插入图片描述

收益:
l 加速读写:因为缓存通常都是全内存的,而存储层通常读写性能不够强悍(例如MySQL),通过缓存的使用可以有效地加速读写,优化用户体验。
l 降低后端负载:帮助后端减少访问量和复杂计算(例如很复杂的SQL语句),在很大程度降低了后端的负载。
成本:
l 数据不一致性:缓存层和存储层的数据存在着一定时间窗口的不一致性,时间窗口跟更新策略有关。
l 代码维护成本:加入缓存后,需要同时处理缓存层和存储层的逻辑,增大了开发者维护代码的成本。
l 运维成本:以Redis Cluster为例,加入后无形中增加了运维成本。

缓存不一致

一致性

1、强一致性
如果你的项目对缓存的要求是强一致性的,那么请不要使用缓存。这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大。
2、弱一致性
这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态
3**、最终一致性**
最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型。一般情况下,高可用只确保最终一致性,不确保强一致性。
强一致性,读请求和写请求会串行化,串到一个内存队列里去,这样会大大增加系统的处理效率,吞吐量也会大大降低。

业务场景

在绝大多数的系统中数据库往往是用户并发访问最薄弱的地方,并且在高并发下的读多写少的情况下,我们往往会借助一些中间键,来解决数据访问过大时造成的数据库宕机情况,例如我们可以使用Redis来作为缓存,让请求先访问到Redis,而不是直接访问数据库。而在这种业务场景下,可能会出现缓存和数据库数据不一致性的问题。

在这里插入图片描述

问题产生的原因

一般来说读取缓存步骤是不会有什么问题的,但是一旦涉及到数据更新,也就是数据库和缓存都操作,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。
在数据更新时,我们需要做以下两步:

  • 操作MySQL
  • 操作缓存

但是无论是先执行步骤1还是先执行步骤2,都有可能出现数据不一致的情况,主要是因为读写是并发的,我们无法保证他们的先后顺序。
相关策略
先做一个说明,从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案(如果要求强一致性的话,我认为没有必要添加缓存了,直接走数据库)。这种前提下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。因此,接下来讨论的思路不依赖于给缓存设置过期时间这个方案。
给出了三种更新策略:

  • 先更新数据库,再更新缓存
  • 先删除缓存,再更新数据库
  • 先更新数据库,在删除缓存

先更新数据库值,再更新缓存值

最不可能选择的策略,原因是此种策略可能会在线程安全的角度和业务场景角度生成脏数据和性能问题。
原因一:线程安全的角度
同时有请求A和请求B进行更新操作,那么就会出现

  1. 请求A更新数据库
  2. 请求B更新数据库
  3. 请求B更新缓存
  4. 请求A更新缓存

这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B比A更早更新了缓存。这就导致了脏数据,因此不考虑。
在这里插入图片描述

业务场景角度
(1)如果是写数据库场景比较多,而读数据场景比较少的业务需求,那么采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能,缓存此类数据,没有很大的意义。
(2)如果是写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合。

后面两种策略都是对缓存进行删除,这里先做一个解释。
例子:数据库在1小时内更新1000次那么缓存也更新1000次,但是这个缓存可能在1小时内只被读了1次,那么就没有必要更新1000次了。反过来,如果是删除的话,那么也只是做了1次删除操作,当缓存真正被读取的时候才去更新。

删除缓存值,再更新数据库值

  1. 请求A进行更新操作,首先删除缓存
  2. 请求B查询发现缓存不存在
  3. 请求B去数据库查询得到旧值
  4. 请求B将旧值写入缓存
  5. 请求A将新值写入数据库

在这里插入图片描述

上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。我们可以采用延迟双删策略,来解决这个问题。
相对应的步骤:

  1. 先淘汰缓存
  2. 再写数据库
  3. 休眠t秒,再次淘汰缓存

这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

// 伪代码
public void updateDb(String key,Object data) {
    redis.delKey(key);
    db.updateData(data);    
    Thread.sleep(t);
    redis.delKey(key);
}

如果系统中MySQL使用了读写分离模式,那么有可能会出现在主从同步没有完成时,读请求就去读取数据了,这时候就会读取到旧值,这里我们可以延长睡眠时间,让主从同步完成后在进行一次删除(如果不考虑主从的情况下,采用双删不用加延时时间也是可以保证一直性的)。

先更新数据库值,在删除缓存值

假设有两个请求,请求A进行更新操作,请求B进行查询操作。
那么会出现如下情形:

  1. 请求A进行更新操作,首先更新数据库
  2. 请求B进行查询操作,击中缓存,得到旧值
  3. 请求A进行删除缓存操作

在这里插入图片描述

在这种情况下如果其他线程并发读缓存的请求不多,那么,就不会有很多请求读取到旧值。而且,请求 A 一般也会很快删除缓存值,这样一来,其他线程再次读取时,就会发生缓存缺失,进而从数据库中读取最新值。所以,这种情况对业务的影响较小。
无论是策略2还是策略3都有可能会出现这种情况:删除缓存失败,这时我们可以采用重试机制来保证数据的一致性。

方案的详细设计

在相关策略的调用中,虽然提出了一些简单解决方案,但是没有考虑到列如 缓存删除失败,数据库更新失败等情况,因此需要增加重试策略,但是还是可能会出现比较不一致的问题,此处详细介绍几种方案。
在这里插入图片描述

流程如下:

  1. 更新数据库数据;
  2. 缓存因为种种问题删除失败
  3. 将需要删除的key发送至消息队列
  4. 自己消费消息,获得需要删除的key
  5. 继续重试删除操作,直到成功

如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了。否则的话,我们还需要再次进行重试。如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。

// 伪代码
public void updateDb(String key,Object data){
    db.updateData(data);
    if (!redis.delKey(key)) {
        mq.send(key);
        new Thread(() -> asyncDel()).start();
    }    
}
// 异步重试
private void asyncDel() {
    int count = 0;
    String key = mq.get();
    while(!redis.delKey(key)) {
        count++;
        if (count > 5) {
            throw new DelFailException();        
        }
    }
    mq.remove(key);
}

这种虽然可以解决,但是会对业务代码造成侵入,而且还需要去维护消息队列,如果可以容忍的话,我觉得是可选的方案之一。
注意 需要使用有序的消息队列,保证消息的有序性。重试删除
在这里插入图片描述

订阅binlog

在这里插入图片描述

业务代码只会操作数据库,不操作缓存。同时启动一个订阅binlog的程序去监听删除操作,然后投递到消息队列中。再启动一个消费者,根据消息去删除缓存。
canal是用来模拟MySQL slave,来订阅MySQL master 的binlog。

异步重试

总结

对于读多写少的数据,请使用缓存。
为了保持数据库和缓存的一致性,会导致系统吞吐量的下降。
为了保持数据库和缓存的一致性,会导致业务代码逻辑复杂。
缓存做不到绝对一致性,但可以做到最终一致性。
对于需要保证缓存数据库数据一致的情况,请尽量考虑对一致性到底有多高要求,选定合适的方案,避免过度设计。

缓存问题

缓存穿透

问题描述

缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层,如下图所示

在这里插入图片描述

整个过程分为如下3步:

  1. 缓存层不命中。
  2. 存储层不命中,不将空结果写回缓存。
  3. 返回空结果。

缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。
缓存穿透问题可能会使后端存储负载加大,由于很多后端存储不具备高并发性,甚至可能造成后端存储宕掉。通常可以在程序中分别统计总调用数、缓存层命中数、存储层命中数,如果发现大量存储层空命中,可能就是出现了缓存穿透问题。

解决方案

造成缓存穿透的基本原因有两个。第一,自身业务代码或者数据出现问题,第二,一些恶意攻击、爬虫等造成大量空命中。下面我们来看一下如何解决缓存穿透问题。

缓存空对象

如图所示,当第2步存储层不命中后,仍然将空对象保留到缓存层中,之后再访问这个数据将会从缓存中获取,这样就保护了后端数据源。
在这里插入图片描述

缓存空对象会有两个问题:第一,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。

布隆过滤器拦截

布隆过滤器:实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。可以告诉你某样东西一定不存在或者可能存在。
在这里插入图片描述

如图所示,在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截。例如:一个推荐系统有4亿个用户id,每个小时算法工程师会根据每个用户之前历史行为计算出推荐数据放到存储层中,但是最新的用户由于没有历史行为,就会发生缓存穿透的行为,为此可以将所有推荐数据的用户做成布隆过滤器。如果布隆过滤器认为该用户id不存在,那么就不会访问存储层,在一定程度保护了存储层。

两种方案比对

在这里插入图片描述

缓存雪崩

如图描述了什么是缓存雪崩:由于缓存层承载着大量请求,有效地保护了存储层,但是如果缓存层由于某些原因不能提供服务,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会级联宕机的情况。
在这里插入图片描述

预防和解决缓存雪崩问题,可以从以下三个方面进行着手。
(1) 保证缓存层服务高可用性。如果缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如前面介绍过的Redis Sentinel和Redis Cluster都实现了高可用。
(2) 依赖隔离组件为后端限流并降级。无论是缓存层还是存储层都会有出错的概率,可以将它们视同为资源。作为并发量较大的系统,假如有一个资源不可用,可能会造成线程全部阻塞在这个资源上,造成整个系统不可用。降级机制在高并发系统中是非常普遍的。实际项目中,我们需要对重要的资源(例如Redis、MySQL、HBase、外部接口)都进行隔离,让每种资源都单独运行在自己的线程池中,即使个别资源出现了问题,对其他服务没有影响。但是线程池如何管理,比如如何关闭资源池、开启资源池、资源池阀值管理,这些做起来还是相当复杂的。这里推荐使用Java依赖隔离工具Hystrix,他是解决依赖隔离的利器。
(3) 提前演练。在项目上线前,演练缓存层宕掉后,应用以及后端的负载情况以及可能出现的问题,在此基础上做一些预案设定。

缓存击穿(热点数据集中失效)

问题描述

当一个key是热点key,并发量很大,而且重建缓存不能在短时间完成,在缓存失效的一瞬间,就会有大量的线程来重建缓存,造成后端负载加大,甚至让应用崩溃,这就叫缓存击穿。如下图:在这里插入图片描述

解决方案

互斥锁

此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可,整个过程如图所示。
在这里插入图片描述

永远不过期

“永远不过期”包含两层意思:
l 从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期。
l 从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
整个过程如图所示:
在这里插入图片描述

此方法有效杜绝了热点key产生的问题,但唯一不足的就是重构缓存期间,会出现数据不一致的情况,这取决于应用方是否容忍这种不一致。

两种方案对比

在这里插入图片描述

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

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

相关文章

绝了,超越YOLOv7、v8,YOLOv6 v3.0正式发布

超越YOLOv7、v8! YOLOv6 v3.0正式发布!!! YOLOv6 全新版本v3.0正式发布!引入新的网络架构和训练方案,其中YOLOv6-S以484 FPS的速度达到45.0% AP,超过YOLOv5-S、YOLOv8-S,其代码刚刚开源 由于前段时间Ultralytics公司透露出V8的发…

马哥架构第2周课程作业

haproxy、nginx、lvs负责均衡相关的话题一. nginx和haproxy的异同点二. 实现haproxy四层地址透传,并且做基于cookie的会话保持2.1 haproxy四层地址透传2.2 基于cookie的会话保持2.2.1 配置选项2.2.2 配置示例2.2.3 验证 Cookie 信息三. 实现自定义错误页面和https的…

【学习笔记】【Pytorch】12.损失函数与反向传播

【学习笔记】【Pytorch】12.损失函数与反向传播一、损失函数的介绍1.L1Loss类的使用代码实现2.MSELoss类的使用3.损失函数在模型中的实现二、反向传播一、损失函数的介绍 参考: 损失函数(loss function) pytorch loss-functions 文档 作用&…

【JavaEE】基于TCP的客户端服务器程序

✨哈喽,进来的小伙伴们,你们好耶!✨ 🛰️🛰️系列专栏:【JavaEE】 ✈️✈️本篇内容:基于TCP的客户端服务器程序。 🚀🚀代码存放仓库gitee:JavaEE初阶代码存放! ⛵⛵作者…

【JavaEE初阶】第二节.进程篇

文章目录 前言 一、操作系统 二、进程 2.1 进程的概念 2.2 进程的管理​​​​​​​​​​​​​​ 2.3 PCB 2.3.1 PCB里面的一些属性 2.3.2 进程的调度 2.3.3 进程的虚拟地址空间 2.3.4 进程间通信 总结 前言 本节内容我们继续对JavaEE的有关内容进行学习,…

汽车智能化,集度做加法

CES2023刚刚落下帷幕,这场名为“国际消费电子展”的业界盛会,近几年重心正明显转向智能汽车及其周边产业链。在2022年的CES上,集度与英伟达宣布合作,也透露了智能汽车研发的相关计划。而在本届CES之前、2022年末的广州车展上&…

一个关于image访问图片跨域的问题

一、背景 项目中遇到一个问题,同一个图片在 dom 节点中使用了 img 标签来加载,同时由于项目使用了 ThreeJS 3D 渲染引擎,在加载纹理时使用了 TextureLoader 来加载了同一张图片,而由于图片是在阿里云服务器上的,所以最…

SourceTree 拉取、重置提交、回滚、变基与合并

SourceTree的重置当前分支到此次提交 使用场景:“我想把已提交未推送的修改撤销” 使用模式说明软合并软合并是指将此次提交回滚到指定提交位置,但这个过程中会将修改过的文件暂存到暂存区。混合合并混合合并是指将此次提交回滚到指定的位置&#xff0c…

本来挺喜欢刷《剑指offer》的.......(第十一天)

跟着博主一起刷题 这里使用的是题库: https://leetcode.cn/problem-list/xb9nqhhg/?page1 目录剑指 Offer 66. 构建乘积数组剑指 Offer 68 - I. 二叉搜索树的最近公共祖先剑指 Offer 68 - II. 二叉树的最近公共祖先剑指 Offer 66. 构建乘积数组 剑指 Offer 66. 构建…

使用react-bmapgl绘制区域并判断是否重叠

需求如下: 在react项目中使用百度地图实现区域(电子围栏)的绘制绘制的区域类型为:1、多边形 2、圆形可绘制多个区域区域不能有重叠可重新编辑区域 代码如下: index.tsx import { useCallback, useEffect, useState } from rea…

Python入门实践(二)——变量的使用

文章目录变量1、变量的命名和使用1.1、避免命名错误2、字符串2.1、修改字符串大小写2.2、合并(拼接)字符串2.3、使用制表符或换行符来添加空白2.4、删除空白3、数字3.1、整数3.2、浮点数3.3、使用str()避免类型错误4、注释变量是对一种数据结构的命名&am…

2023年基建工程(设计规划施工)经验分享,超多干货

为了彻底打通从工程外业勘探调查、数据资料整理,到内业详细设计之间的一系列障碍,结合工程外业调查的特点,基于安卓(Android)操作系统,精心打磨推出了“外业精灵”移动端应用软件。 该系统把工程外业探勘、…

MPP数据库简介及架构分析

目录什么是MPP?特性并行处理超大规模数据仓库真正适合什么典型的分析工作量数据集中化线性可伸缩性MPP架构技术特性数据库架构分析Shared EverythingShared DiskShare MemoryShared NothingShared Nothing数据库架构优势什么是MPP? MPP (Massively Paral…

分享88个C源码,总有一款适合您

C源码 分享88个C源码,总有一款适合您 下面是文件的名字,我放了一些图片,文章里不是所有的图主要是放不下...,大家下载后可以看到。 源码下载链接:https://pan.baidu.com/s/1TT87gt66kn5BtLqgRUTlUQ?pwdwje5 提取码…

Java图形化界面---JOptionPane

目录 一、JOptionPane的介绍 二、JOptionalPane的使用 (1)消息对话框 (2) 确认对话框 (3)输入对话框 (4)选项对话框 一、JOptionPane的介绍 通过JOptionPane可以非常方便地创建…

SpringCloud复习之Sleuth+Zipkin链路追踪实战

文章目录写作背景为什么要有链路监控SpringCloud SleuthZipkin能做什么上手实战启动一个Zipkin Server微服务集成SleuthZipkin写作背景 前面复习了SpringCloud Netflix的几个核心组件,包括Eureka、Ribbon、Feign、Hystrix、Zuul,并进行了Demo级别的实战…

高精度减法【c++】超详细讲解

前言 大家学过高精度加法之后,可能已经知道高精度减法的实现方法了吧 如果你还没有学过高精度加法的话,请点击这里(很详细的)—>高精度加法【C实现】详解 最大的问题 最大的问题莫过于负数问题了。其他方法和加法一样。 负…

4.二级缓存解析

文章目录1. 二级缓存配置2. 二级缓存结构3. 二级缓存命中条件4. 缓存空间的理解5. 二级缓存执行流程二级缓存也称作是应用级缓存,与一级缓存不同的,是它的作用范围是整个应用,而且可以跨线程使用。所以二级缓存有更高的命中率,适合…

从南丁格尔图到医学发展史

可视化中,前端用于表现不同类目的数据在总和中的占比的场景,往往会采用饼图。 针对数据大小相近,南丁格尔图的呈现会更加美观。 南丁格尔图,又称玫瑰图,是由弗罗伦斯南丁格尔发明。 弗洛伦斯南丁格尔 开创了护理事业…

二、django中的路由系统

django中的路由系统 django中路由的作用和路由器类似,当一个用户请求Django站点的一个页面时,是路由系统通过对url的路径部分进行匹配,一旦匹配成功就导入并执行对应的视图来返回响应。 django如何处理请求 当一个请求来到时,d…