【Redis】缓存击穿、缓存穿透、缓存雪崩原理以及多种解决方案

一、前言

在 Spring Cloud 微服务集群项目中,客户端的请求首先会经过 Nginx,Nginx 会将请求反向代理到 Gateway 网关层,接着才会将请求发送到具体的服务 service。

在 service 中如果要查询数据,则会到缓存中查询,如果缓存未命中,再到数据库中查询数据。

作为缓存的 Redis 扛住了系统中大量的请求,极大的减小了数据库的压力。

但是当流量很大、高并发时,倘若 Redis 没能扛住,便会导致缓存穿透、缓存击穿、缓存雪崩

二、缓存击穿

当一个被大量并发访问且缓存重建过程比较复杂的键(key)突然失效时,大量的请求会在瞬间直接打到数据库,给数据库带来巨大的压力。

比如,某个电商网站上有一款非常热销的商品,很多人都在不断刷新页面查看商品详情。这个商品详情被缓存了起来,访问速度很快。但是,如果缓存突然失效了,所有用户的请求都会直接发送到数据库服务器,短时间内数据库会承受巨大的访问压力,可能导致数据库响应变慢或甚至宕机。

解决方案:解决缓存击穿问题的关键在于防止在缓存失效时大量请求直接打到数据库。

通过使用乐观锁、分布式锁等策略,可以有效地减轻数据库的压力,提高系统的稳定性和响应速度。

乐观锁

使用乐观锁机制来防止多个客户端同时重建缓存。

  1. 当缓存中找不到数据时,尝试更新缓存。

  2. 如果更新失败(例如因为数据版本冲突),则重试。

示例代码(伪代码):

while (true) {data = redis.get(key);if (data != null) {break;}try {data = db.getData(key);redis.set(key, data);break;} catch (OptimisticLockingFailureException e) {// 更新失败,重试}
}

 

互斥锁(Mutex Lock)

使用分布式锁(如 Redis 的 SETNX 或 Redlock 算法)来确保同一时间内只有一个请求可以重建缓存。

  1. 当缓存中找不到数据时,尝试获取分布式锁。

  2. 如果获取锁成功,再查询数据库并将数据写回缓存。

  3. 如果获取锁失败,等待一段时间后重试。

示例代码(伪代码):

if (redis.setnx(lockKey, lockValue)) {// 获取锁成功try {// 查询数据库data = db.getData(key);// 将数据写回缓存redis.set(key, data);} finally {// 释放锁redis.del(lockKey);}
} else {// 获取锁失败,等待并重试Thread.sleep(100);// 递归调用自己getDataFromCacheOrDb(key);
}

优点:

  1. 没有额外的内存消耗

  2. 保证一致性

  3. 实现简单

缺点:

  1. 线程需要等待,性能受影响

  2. 可能有死锁风险

这里还存在一个问题:分布式锁 key 的时间应该设置多长?

如果设置得太短,可能会导致线程还没有执行完业务逻辑锁就失效了;

如果设置得太长,则可能导致锁占用时间过长,影响其他请求的处理效率。

为了解决这个问题,可以使用 Redisson 这样的分布式协调工具,它提供了看门狗机制(Watchdog Mechanism)来动态延长锁的超时时间。

Redisson 的看门狗机制可以自动续期锁的过期时间,确保锁持有者在执行业务逻辑期间锁不会过期。

当一个线程获得了锁之后,Redisson 会在后台启动一个定时任务(看门狗),每隔一段时间自动延长锁的过期时间。

这样,只要锁持有者还在执行业务逻辑,锁就不会过期。

下面是一个使用 Redisson 实现分布式锁的例子,包括如何利用看门狗机制:

  • 引入依赖

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.16.4</version>
</dependency>
  • 配置 Redisson 客户端

创建一个 Redisson 客户端实例,连接到 Redis 服务器

public class DistributedLockExample {private static final String LOCK_NAME = "myDistributedLock";public static void main(String[] args) {Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");RedissonClient redisson = Redisson.create(config);RLock lock = redisson.getLock(LOCK_NAME);try {// 尝试获取锁,如果没有获取到,则等待lock.lock();// 执行业务逻辑System.out.println("Executing business logic...");Thread.sleep(5000); // 模拟耗时操作} catch (InterruptedException e) {Thread.currentThread().interrupt();System.err.println("Interrupted while waiting for lock.");} finally {// 释放锁if (lock.isHeldByCurrentThread()) {lock.unlock();System.out.println("Lock released.");}redisson.shutdown();}}
}
  • 看门狗机制的细节

Redisson 的看门狗机制是自动启用的。

调用 lock.lock() 方法获取锁时,Redisson 会自动启动一个后台任务(看门狗),每隔一段时间(默认为锁的超时时间的一半)自动续期锁的过期时间。

这样,只要持有锁的线程还在执行业务逻辑,锁就不会过期。

如果希望自定义看门狗续期的时间间隔,可以通过 lock.tryLock(long waitTime, long leaseTime, TimeUnit unit) 方法来指定:

try {lock.tryLock(10, 30, TimeUnit.SECONDS); // 尝试获取锁,等待 10 秒,锁的过期时间为 30 秒// 执行业务逻辑System.out.println("Executing business logic...");Thread.sleep(5000); // 模拟耗时操作
} catch (InterruptedException e) {Thread.currentThread().interrupt();System.err.println("Interrupted while waiting for lock.");
}

优势:

  1. 自动续期:看门狗机制自动续期锁的过期时间,确保锁持有者在执行业务逻辑期间锁不会过期。

  2. 简化代码:使用 Redisson 可以简化分布式锁的实现代码,避免手动管理锁的续期逻辑。

  3. 容错性:如果持有锁的线程因异常而提前结束,Redisson 会自动释放锁,避免死锁。

通过使用 Redisson 的看门狗机制,可以有效地解决分布式锁的超时问题,确保锁在执行业务逻辑期间不会过期,同时也提高了系统的健壮性和可用性。

为什么不建议使用布隆过滤器解决缓存击穿问题?

  1. 误报问题:布隆过滤器的误报特性意味着它可能会错误地认为一个不在缓存中的数据项存在于缓存中。如果这个数据项实际上并不存在,那么误报会导致不必要的缓存查找,反而增加了系统负担。

  2. 缓存更新问题:布隆过滤器不能用于存储具体的值,只能用于标记存在性。因此,它不能替代缓存来存储实际的数据。

  3. 缓存重建问题:缓存击穿问题的核心在于缓存重建时如何避免大量并发请求直接打到数据库。布隆过滤器无法解决这个问题,因为它并不涉及缓存重建的逻辑。

 

三、缓存穿透 

缓存穿透是指当你要访问的数据既不在缓存中,也不在数据库中时,导致请求先去访问缓存,发现缓存中没有数据,于是再去访问数据库,结果发现数据库中也没有这个数据。这样一来,应用无法从数据库中读取数据并写入缓存,导致后续请求还是会直接打到数据库,缓存就失去了作用。

想象一下,你正在使用一个购物网站,用户输入了一个根本不存在的商品编号(比如一个随机生成的编号)。第一次请求时,缓存中没有这个商品的信息,于是请求会去数据库查找。但是数据库中也没有这个商品的信息。这时,应用无法把数据缓存起来,因为根本就没有数据可以缓存。接下来,如果其他用户也输入同样的编号,他们的请求还是会直接打到数据库,因为缓存中还是没有这条数据。

存在一种情况,恶意用户伪造了大量不存在的 id 发起请求,这将会直接导致 DB 宕机

解决方案:可以通过缓存空值、布隆过滤器、前端请求校验

缓存空值

当查询结果为空时,也将这个空结果缓存起来,并设置一个较短的过期时间。

优点:

  • 简单易行,可以立即减少数据库的压力。

  • 适用于偶尔出现的空查询。

缺点:

  • 如果大量请求查询的都是不存在的数据,缓存中会积累大量的空值,占用缓存空间。

  • 如果空值的过期时间设置不合理,可能会导致频繁的缓存失效。

// 查询缓存
String value = redisTemplate.opsForValue().get(key);if (value == null) {// 查询数据库value = databaseService.getValue(key);if (value == null) {// 缓存空值redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);} else {// 正常缓存数据redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);}
} else {// 从缓存中获取数据
}

 

布隆过滤器

使用布隆过滤器来判断某个数据项是否可能存在。如果布隆过滤器表明数据项不存在,则直接返回;否则再进行正常的缓存和数据库查询。

优点

  • 几乎不会占用太多内存,可以高效地过滤掉不存在的数据项。

  • 减少了数据库的压力。

缺点

  • 存在一定的误报率(false positive),即有可能把不存在的数据误认为存在。

  • 需要维护布隆过滤器,有一定的复杂度。

布隆过滤器可能会产生误判(即把不在集合中的元素误认为在集合中),但它不会漏掉任何一个实际在集合中的元素。

布隆过滤器存在一定的误报率,因此设计时需要考虑到这一点,并合理选择布隆过滤器的容量和哈希函数数量,以降低误报的概率。

如果数据频繁增删改,是不太适合用布隆过滤器的。

因为一个数据变更之后,布隆过滤器无法删除 key,因此,只能重新创建一个布隆过滤器,再加载一遍所有的数据,创建出新的 bitmap。

可以使用带删除功能的布谷鸟布隆过滤器,来满足动态变化的需求。

// 初始化布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), expectedInsertions, fpp);// 检查数据项是否存在
if (!bloomFilter.mightContain(key)) {// 数据项不存在,直接返回return null;
}// 查询缓存
String value = redisTemplate.opsForValue().get(key);if (value == null) {// 查询数据库value = databaseService.getValue(key);if (value == null) {// 缓存空值redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);} else {// 正常缓存数据redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);}
} else {// 从缓存中获取数据
}

前端请求校验

在前端对请求进行检查确实可以帮助减轻缓存穿透问题,但这通常不是唯一的解决方案,而是与其他后端策略相结合的一种补充措施。

  • 在前端对用户输入进行验证,确保请求的数据项是合法的。

  • 在前端维护一个黑名单或白名单,确保只有合法的数据项才能发起请求。

  • 在前端通过UI提示用户输入无效的数据项,并阻止提交请求。

  • 分析用户的行为,检测异常的请求模式。

前端的检查和验证可以作为一种预防措施来减少缓存穿透的风险,但它并不是万能的。为了全面解决缓存穿透问题,还需要结合后端的技术手段。前端和后端的结合使用可以更有效地应对缓存穿透问题,提高系统的整体性能和稳定性。

四、缓存雪崩 

缓存雪崩是指很多个缓存数据项同时过期,导致大量请求在同一时间涌向数据库。

不像缓存击穿那样,只是一个单独的数据项过期,但即使是一个数据项过期,也会给数据库带来很大的压力。因此,可以把缓存雪崩看作是由很多个缓存击穿组合而成的现象。

在实际应用中,缓存雪崩是一个更常见的问题。

它不像缓存击穿那样极端,一个数据项过期就能引发成千上万的并发请求,直接把数据库打垮。

相反,缓存雪崩可能是因为每一个数据项只带来几十到几百个并发请求,但当大量数据项同时过期时,这些并发请求叠加在一起,累积到成千上万个,从而把数据库压垮。

想象一下,你在一个大型电商网站上购物,有很多商品的信息都被缓存起来了。如果这些商品的信息都在同一时间过期了,那么所有的用户在查询这些商品信息时,都会直接向数据库发起请求。虽然每个商品的请求量不算太大,但加在一起就变得非常庞大,足以让数据库不堪重负。

相比之下,缓存击穿更像是一个单一的热点商品突然过期了,所有的用户都在同一时间查询这个商品的信息,短时间内数据库会受到极大的压力。

解决方案:

设置不同的过期时间

为了避免大量缓存条目同时过期,可以为每个缓存条目设置一个随机的过期时间,使得缓存条目不会在同一时间失效。

示例代码:

import java.util.concurrent.TimeUnit;public void cacheData(String key, Object value, int baseExpirationTime) {// 为每个缓存条目设置一个随机的过期时间int expirationTime = baseExpirationTime + new Random().nextInt(60); // 例如,基础过期时间 ± 30 秒redisTemplate.opsForValue().set(key, value, expirationTime, TimeUnit.SECONDS);
}

 

降级策略

当缓存失效时,如果数据库压力过大,可以采用降级策略,以减轻数据库压力。

  • 返回固定值或默认值

public String fetchDataWithFallback(String key) {String value = redisTemplate.opsForValue().get(key);if (value == null) {// 数据库压力过大时返回默认值return "Default Value";}return value;
}
  • 返回错误信息

public ResponseEntity<String> fetchDataWithFallback(String key) {String value = redisTemplate.opsForValue().get(key);if (value == null) {// 数据库压力过大时返回错误信息return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body("Too many requests, please try again later.");}return ResponseEntity.ok(value);
}
  • 直接拒绝请求

public ResponseEntity<String> fetchDataWithDirectRejection(String key) {String value = redisTemplate.opsForValue().get(key);if (value == null) {// 数据库压力过大时直接拒绝请求return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("Request rejected due to high load.");}return ResponseEntity.ok(value);
}
  • 重定向到降级页面

public ResponseEntity<String> fetchDataWithRedirect(String key) {String value = redisTemplate.opsForValue().get(key);if (value == null) {// 数据库压力过大时重定向到降级页面return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).header("Location", "/high-load").build();}return ResponseEntity.ok(value);
}
  • 延迟响应

public String fetchDataWithDelay(String key) {String value = redisTemplate.opsForValue().get(key);if (value == null) {try {// 延迟响应Thread.sleep(5000); // 延迟 5 秒} catch (InterruptedException e) {Thread.currentThread().interrupt();System.err.println("Interrupted while sleeping.");}// 数据库压力过大时返回默认值return "Default Value";}return value;
}

在实际应用中,通常会结合多种降级策略来处理高负载情况。例如,可以先尝试返回默认值或错误信息,如果仍然无法缓解压力,则进一步采取直接拒绝请求或延迟响应的措施。  

public ResponseEntity<String> fetchDataWithCombinedFallback(String key) {String value = redisTemplate.opsForValue().get(key);if (value == null) {// 数据库压力过大时尝试返回默认值return ResponseEntity.ok("Default Value");}// 检测系统负载,如果负载过高,则进一步降级if (isSystemUnderHighLoad()) {return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("Request rejected due to high load.");}return ResponseEntity.ok(value);
}private boolean isSystemUnderHighLoad() {// 检测系统负载的方法return false; // 根据实际情况实现
}

不过,要考虑一种情况,如果你的业务对时点性要求高,必须每天的指定时间,去更新我们的数据,比如游戏排行每日零点更新。

在某一个固定的时间,由于业务要求,必须使得数据刷新,并且不允许出现旧数据,让缓存全部失效。

像这样的业务应该怎么办?

既然 redis 无法分散过期时间,那就在业务层下功夫。

时间一到,redis 数据全部失效,大量并发前来查询,在 service 服务层查询时,设置一个短暂的随机延迟时间。

这样,少量的请求先查询,就会读数据库,然后存入 redis;其他请求,由于随机时间稍稍慢了点,就可以去 redis 读出数据。

在业务层设置一个短暂的随机延迟时间,可以有效平滑请求。

import java.util.concurrent.ThreadLocalRandom;public String fetchDataWithRandomDelay(String key) {String value = redisTemplate.opsForValue().get(key);if (value == null) {// 设置一个短暂的随机延迟时间try {Thread.sleep(ThreadLocalRandom.current().nextInt(1000)); // 随机延迟 0 到 1000 毫秒} catch (InterruptedException e) {Thread.currentThread().interrupt();System.err.println("Interrupted while sleeping.");}value = databaseService.getValue(key);if (value != null) {redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);}}return value;
}

参考文章:

不用背八股文!一文搞懂redis缓存击穿、穿透、雪崩!

【面试】redis缓存穿透、缓存击穿、缓存雪崩区别和解决方案 

 

一  叶  知  秋,奥  妙  玄  心 

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

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

相关文章

2024年最强图纸加密软件大揭秘!图纸加密软件推荐

在数字化时代&#xff0c;信息安全成为企业发展的重要保障&#xff0c;尤其是对于设计图纸等敏感数据的保护&#xff0c;选择一款可靠的图纸加密软件尤为重要。本文将为您推荐2024年十大图纸加密软件&#xff0c;帮助企业在日常工作中更好地保护知识产权和商业机密。 2024年最强…

宽带和带宽分不清楚

如何理解带宽 我们平时经常听到的带宽其实是宽带&#xff0c;举个栗子&#xff1a;我家用的是xx运营商提供的&#xff0c;号称1000M宽带&#xff0c;这其实指是的网络数据传输的速率是&#xff1a;1000Mbs&#xff08;即125MBps&#xff09;。 那么既然有宽带&#xff0c;就有…

OSS上传文件

注册阿里云账号 开通oss服务 创建accesskey和secret 进入oss选项&#xff0c;根据sdk开发代码

网站开发:XTML+CSS - 网页文档结构

1. 前言 HTML&#xff08;HyperText Markup Language&#xff0c;超文本标记语言&#xff09;是构建网页和 web 应用程序的标准标记语言。它定义了网页的结构和内容&#xff0c;允许开发者创建有组织、语义化的文档。 HTML 使用一系列的元素&#xff08;elements&#xff09;和…

如何轻松开启美股交易之旅?

你是否正在考虑进入美股市场&#xff0c;却不知道从哪里开始&#xff1f;“如何投资美股”的具体步骤和技巧&#xff0c;你是否已经掌握&#xff1f; 掌握美股交易时间与规则 1. 美股交易时间&#xff1a;灵活安排交易计划的基础 如何投资美股&#xff1f;首先&#xff0c;你…

简单的java调动远程服务器shell脚本

简单的java调动远程服务器shell脚本 1.需求 我们想要在我们的xxl-job中调用一个定时任务&#xff0c;固定时间频率去调用另一个服务器的shell脚本&#xff0c;进行数据批量的处理&#xff0c;整体需求逻辑非常简单&#xff0c;此处记录一下java调用shell脚本部分&#xff0c;…

Redis应用(2)——Redis的项目应用(一)

/** * 雪花id的工具类 */ Slf4j public class SnowFlakeUtil { private static long workerId 0; private static long datacenterId 1; private static Snowflake snowflake IdUtil.getSnowflake(workerId,datacenterId); PostConstruct // 自动调用&#xff0c;在构造方法…

Python 与 Excel 图表自动化:让数据“会说话”

在数据驱动的时代&#xff0c;数据分析师、财务专家、销售经理们都离不开Excel——这个简单而强大的工具。而Excel图表是展现数据故事的不二之选。然而&#xff0c;手动创建图表不仅耗时&#xff0c;还容易出错。如何让这繁琐的工作变得简单&#xff1f;答案就是&#xff1a;Py…

网络安全服务基础Windows--第8节-DHCP部署与安全

DHCP协议理解 定义&#xff1a;DHCP&#xff1a;Dynamic Host Configuration Protocol&#xff0c;动态主机配置协议&#xff0c;是⼀个应⽤在局域⽹中的⽹络协议&#xff0c;它使⽤UDP协议⼯作。 67&#xff08;DHCP服务器&#xff09;和68&#xff08;DHCP客户端&#xff0…

如何在 Cursor IDE 中使用驭码CodeRider 进行 AI 编程?

驭码CodeRider 是极狐GitLab 公司自研发布的 AIGC 产品&#xff0c;可以用来进行 AI 编程和 DevOps 流程处理。本文分享如何在 Cursor 中使用驭码CodeRider。 Cursor 是近期比较火爆的一款 AI 代码编辑器&#xff0c;通过将 AI 能力引入软件研发来提升软件研发效率。而驭码Cod…

三级_网络技术_53_应用题

一、 请根据下图所示网络结构回答下列问题。 1.设备1应选用__________网络设备。 2.若对整个网络实施保护&#xff0c;防火墙应加在图中位置1~3的__________位置上。 3.如果采用了入侵检测设备对进出网络的流量进行检测&#xff0c;并且探测器是在交换机1上通过端口镜像方式…

CISAW认证涉及10个技术方向,到底哪个更适合您?

对于渴望在信息安全领域提升自己技能的你&#xff0c;CISAW&#xff08;信息安全保障人员&#xff09;认证无疑是一个理想的选择。 这项认证不仅适用于网络信息安全岗位的专业人员&#xff0c;还为那些寻求深化专业知识、提高职业竞争力的人士提供了宝贵的学习机会。 CISAW认…

【Linux】消息队列信号量

目录 消息队列 原理 接口 指令 信号量 概念 对于信号量理论的理解 信号量的操作 信号量的指令 消息队列 原理 消息队列提供了一个从一个进程向另外一个进程发送一个数据块的方法&#xff0c;每个数据块都有一个类型。对消息队列的的管理也是先描述&#xff0c;再组织…

开源网安引领AIGC+开发安全,智能防护铸就软件安全新高度

近日&#xff0c;国内网络安全领域知名媒体数说安全正式发布了《2024年中国网络安全市场100强》和《2024年中国网络安全十大创新方向》。开源网安凭借在市场表现力、资源支持力以及产品在AI方向的创新力上的优秀表现成功入选百强榜单&#xff0c;并被评为“AIGC开发安全”典型厂…

go发邮件的功能如何使用?新手必备的指南?

Go发邮件怎么实现发信功能&#xff1f;使用go发邮件的注意事项&#xff1f; 在这个数字化时代&#xff0c;电子邮件已成为我们日常沟通的重要工具。对于新手来说&#xff0c;掌握如何使用go发邮件功能是至关重要的。AokSend将详细介绍go发邮件的使用方法&#xff0c;帮助你快速…

分布式服务调用RPC框架复习

目录 1、Dubbo概念与架构 1.1 Dubbo简介 1.2 注册与发现流程图 1.3 Dubbo架构图 2、Dubbo调用流程 3、关于Dubbo 服务治理 3.1 Dubbo通信协议 3.2 序列化方式 3.3 负载均衡算法 4、Dubbo与Spring Cloud关系 4.1 相似之处 4.2 差异之处 5、Dubbo 与 gRPC 关系 6、…

【C/C++】web服务器项目开发总结【请求 | 响应 | CGI】

博客主页&#xff1a;花果山~程序猿-CSDN博客 文章分栏&#xff1a;Linux_花果山~程序猿的博客-CSDN博客 关注我一起学习&#xff0c;一起进步&#xff0c;一起探索编程的无限可能吧&#xff01;让我们一起努力&#xff0c;一起成长&#xff01; 目录 一&#xff0c;背景 二&…

使用 Milvus 和 Streamlit 搭建多模态产品推荐系统

我们可以使用 Milvus 搭建多模态 RAG 应用&#xff0c;用于产品推荐系统。用户只需简单上传一张图片并输入文字描述&#xff0c;Google 的 MagicLens 多模态 Embedding 模型就会将图像和文本编码成一个多模态向量。然后&#xff0c;使用这个向量从 Milvus 向量数据库中找到最相…

C++设计模式——Template Method模板方法模式

一&#xff0c;模板方法模式的定义 模板方法模式是一种行为型设计模式&#xff0c;它先定义了一个算法的大致框架&#xff0c;然后将算法的具体实现步骤分解到多个子类中。 模板方法模式为算法设计了一个抽象的模板&#xff0c;算法的具体代码细节由子类来实现&#xff0c;从…

【Linux】进程状态(RSDT 阻塞 僵尸 孤儿)

目录 进程状态 进程状态的查看 R和S 运行状态 T/t 暂停状态 kill命令 D &#xff08;disk sleep&#xff09;状态、 Z 状态&#xff08;僵尸状态&#xff09; 孤儿状态 运行状态 阻塞状态 进程状态 一个进程通常有三种状态 ◉ 就绪状态&#xff08;Ready&#xff0…