redis 极简分布式锁实现

news2024/12/26 22:59:13

写在前面


  • 工作中遇到,整理 reids 做简单分布式锁的思考
  • 博文适合刚接触 redis 的小伙伴
  • 理解不足小伙伴帮忙指正

对每个人而言,真正的职责只有一个:找到自我。然后在心中坚守其一生,全心全意,永不停息。所有其它的路都是不完整的,是人的逃避方式,是对大众理想的懦弱回归,是随波逐流,是对内心的恐惧 ——赫尔曼·黑塞《德米安》


假设现在有这样一个需求,需要做排队预约住宿的功能,当前宿舍住满了,有新的同学需要来入住,可以进行排队预约,排队编号通过累加的方式生成

我们设计这样一张数据表

CREATE TABLE `ams_student_queue_check_in_sync` (
	`queue_check_in_id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '学生队列ID',
	`student_name` VARCHAR(50) NOT NULL COMMENT '学生姓名' COLLATE 'utf8mb4_general_ci',
	`student_uid` VARCHAR(50) NULL DEFAULT NULL COMMENT '学生uid' COLLATE 'utf8mb4_general_ci',
	`student_card` VARCHAR(30) NULL DEFAULT NULL COMMENT '学生身份证号' COLLATE 'utf8mb4_general_ci',
	`student_contact_number` VARCHAR(20) NOT NULL COMMENT '学生联系电话' COLLATE 'utf8mb4_general_ci',
	`student_email` VARCHAR(50) NULL DEFAULT NULL COMMENT '学生电子邮件地址' COLLATE 'utf8mb4_general_ci',
	`student_gender` TINYINT(4) NOT NULL DEFAULT '0' COMMENT '学生性别',
	`student_emergency_contact_name` VARCHAR(100) NULL DEFAULT NULL COMMENT '第二联系人姓名' COLLATE 'utf8mb4_general_ci',
	`student_emergency_contact_number` VARCHAR(20) NULL DEFAULT NULL COMMENT '第二联系人电话' COLLATE 'utf8mb4_general_ci',
	`student_status` TINYINT(4) NULL DEFAULT '1' COMMENT '学生排队状态(1.待入住,2.以入住 3.以取消)',
	`arrival_dates` DATETIME NULL DEFAULT NULL COMMENT '预计入住时间',
	`departure_dates` DATETIME NULL DEFAULT NULL COMMENT '预计离开日期',
	`queue_position` INT(11) NULL DEFAULT NULL COMMENT '学生在排队中的位置',
	`check_in_remark` TEXT NULL DEFAULT NULL COMMENT '备注' COLLATE 'utf8mb4_general_ci',
	`extended1` VARCHAR(50) NULL DEFAULT NULL COMMENT '扩展字段1' COLLATE 'utf8mb4_general_ci',
	`extended2` VARCHAR(50) NULL DEFAULT NULL COMMENT '扩展字段2' COLLATE 'utf8mb4_general_ci',
	`extended3` VARCHAR(50) NULL DEFAULT NULL COMMENT '扩展字段3' COLLATE 'utf8mb4_general_ci',
	`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
	`updated_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
	PRIMARY KEY (`queue_check_in_id`) USING BTREE,
	INDEX `student_uid` (`student_uid`) USING BTREE
)
COMMENT='入住排队表'
COLLATE='utf8mb4_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=1363
;

queue_position 为每一位同学的排队编号,需要根据当前的学生编号最大来累加

下面为实现的基础代码

    @ApiOperation("入住排队接口")
    @PostMapping("/checkInQueue")
    @Transactional
    public   AjaxResult checkInQueue( @RequestHeader("UID") String uid, @RequestBody AmsStudentQueueCheckIn amsStudentQueueCheckIn){

        if (Objects.isNull(uid)){
            return AjaxResult.error("Uid 为空");
        }
        if (Objects.isNull(amsStudentQueueCheckIn.getStudentEmergencyContactNumber())){
            return AjaxResult.error("电话号为空");
        }
        StringBuilder stringBuilder = new StringBuilder();
        String studentContactNumber = amsStudentQueueCheckIn.getStudentContactNumber();
        List<AmsStudentQueueCheckIn> amsStudentQueueCheckIns1 = amsStudentQueueCheckInService.selectAmsStudentQueueCheckInList(new AmsStudentQueueCheckIn().setStudentContactNumber(studentContactNumber));

        Integer count = amsStudentQueueCheckInService.selectAmsStudentQueueCheckInListCount(amsStudentQueueCheckIn.getStudentGender());

        if (Objects.nonNull(amsStudentQueueCheckIns1) && amsStudentQueueCheckIns1.size() !=0 ){
            stringBuilder.append("已经排队预约啦,请耐心等待 ^_^")
                    .append(", 预约编号为 " ).append(amsStudentQueueCheckIns1.get(0).getQueuePosition())
                    .append(", 前面还有 ").append(count - 1).append( " 人");
            return  AjaxResult.success(stringBuilder.toString(),ImmutableMap.of("queuePosition",amsStudentQueueCheckIns1.get(0).getQueuePosition(),"beforePeopleBumber",count -1 ));
        }

        AmsStudentQueueCheckIn amsStudentQueueCheckIns = amsStudentQueueCheckInService.selectAmsStudentQueueCheckInListMax(amsStudentQueueCheckIn.getStudentGender());
        Long queuePosition = 0L;
        if (Objects.nonNull(amsStudentQueueCheckIns)){
            queuePosition = amsStudentQueueCheckIns.getQueuePosition()
        }
        amsStudentQueueCheckIn.setStudentStatus(1).setQueuePosition(queuePosition + 1L).setStudentUid(uid);
        amsStudentQueueCheckIn.setStudentStatus(1).setQueuePosition(amsStudentQueueCheckIns.getQueuePosition() + 1L).setStudentUid(uid);
        int i = amsStudentQueueCheckInService.insertAmsStudentQueueCheckIn(amsStudentQueueCheckIn);
        if (i != 1){
            return AjaxResult.error("排队预约失败!");
        }
        stringBuilder.append("排队预约成功")
                .append(", 预约编号为 " ).append(amsStudentQueueCheckIn.getQueuePosition())
                .append(", 前面还有 ").append(count).append( " 人");
        return  AjaxResult.success(stringBuilder.toString(),ImmutableMap.of("queuePosition",amsStudentQueueCheckIn.getQueuePosition(),"beforePeopleBumber",count));
    }

逻辑比较简单,拿到数据,获取编号最大值累加,数据落表,但是上面的代码存在一个问题,因为是 Springboot 项目,使用 tomcat 部署,Spring Boot 嵌入的 Tomcat 默认启用 Http11NioProtocol,可以切换日志级别为 Debug 可看到

Http11NioProtocol 表示多线程非阻塞模式的HTTP协议的通信(web 服务端网络IO处理模型包括:单(多)线程阻塞(非阻塞)IO模型)。

# 日志级别 Debug
# 日志配置
logging:
  level:
    root: debug
11:42:51.810 [restartedMain] INFO  o.a.c.h.Http11NioProtocol - [log,173] - Initializing ProtocolHandler ["http-nio-8080"]
11:42:51.811 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [Connector[HTTP/1.1-8080]] to [INITIALIZED]
11:42:51.811 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [StandardService[Tomcat]] to [INITIALIZED]
11:42:51.811 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [StandardServer[-1]] to [INITIALIZED]
11:42:51.811 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [StandardServer[-1]] to [STARTING_PREP]
11:42:51.811 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [StandardServer[-1]] to [STARTING]
11:42:51.812 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [org.apache.catalina.deploy.NamingResourcesImpl@1dc49001] to [STARTING_PREP]
11:42:51.812 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [org.apache.catalina.deploy.NamingResourcesImpl@1dc49001] to [STARTING]
11:42:51.812 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [org.apache.catalina.deploy.NamingResourcesImpl@1dc49001] to [STARTED]
11:42:51.812 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [StandardService[Tomcat]] to [STARTING_PREP]
11:42:51.812 [restartedMain] INFO  o.a.c.c.StandardService - [log,173] - Starting service [Tomcat]
11:42:51.812 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [StandardService[Tomcat]] to [STARTING]
11:42:51.813 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [StandardEngine[Tomcat]] to [STARTING_PREP]
11:42:51.813 [restartedMain] INFO  o.a.c.c.StandardEngine - [log,173] - Starting Servlet engine: [Apache Tomcat/9.0.75]

可以看到 spring-boot-starter-web 嵌入的 9.0.75 版本的 tomcat ,Tomcat 从 8.5 版本开始移除了 BIO,默认启用 NIO

下图为从套接字连接接收、处理请求、响应客户端的整个过程

《Tomcat内核设计剖析》

所以当多个排队请求并发调用接口时,不同的线程会分别进入方法,这个时候有可能会从数据库获取相同的排队编号进行累加,同时生成相同新编号,所以这里需要考虑方法线程安全,

最简单的方式是使用同步方法,保证只有一个线程获取锁,但是这不是最优的方式,这里不做考虑

 public  synchronized  AjaxResult checkInQueue( @RequestHeader("UID") String uid, @RequestBody AmsStudentQueueCheckIn amsStudentQueueCheckIn){
 ....................

使用同步方法的方式解决了上面的问题,但是如果当前项目是在 k8s 集群上面部署,以分布式的方式,就需要考虑多个 Pod 的数据同步问题。

假设两个排队请求被负载到两个不同的 Pod,这个时候同时查询数据会获取相同的最大编号,生成相同的编号,考虑使用分布式锁。下面为对方法的改进,这里如果使用分布式锁的方式,那么上面的同步方法即可以去掉了,因为获取锁的方法是原子操作。

分布式锁实现很简单,就是进来一个线程先占位,当别的线城进来操作时,发现已经有人占位了,就会放弃或者稍后再试。这里的占位状态是全局的,相对整个集群而言,代码如下

        String token = UUID.randomUUID().toString();
        // 添加分布式锁
        if (redisCache.tryAcquireLock("checkInQueue", token, 2, 10)){
            AmsStudentQueueCheckIn amsStudentQueueCheckIns = amsStudentQueueCheckInService.selectAmsStudentQueueCheckInListMax(amsStudentQueueCheckIn.getStudentGender());
            Long queuePosition = 0L;
            if (Objects.nonNull(amsStudentQueueCheckIns)){
                queuePosition = amsStudentQueueCheckIns.getQueuePosition();

            }
            amsStudentQueueCheckIn.setStudentStatus(1).setQueuePosition(queuePosition + 1L).setStudentUid(uid);
            int i = amsStudentQueueCheckInService.insertAmsStudentQueueCheckIn(amsStudentQueueCheckIn);
            // 释放分布式锁
            redisCache.unlock("checkInQueue", token);
            if (i != 1){
                return AjaxResult.error("排队预约失败!请重新填写");
            }

        }else {
            return AjaxResult.error("系统繁忙,请稍后提交!");
        }

tryAcquireLocktryLock 以及 unlock 的方法实现

public class RedisCache
{
    private static final Logger log = LoggerFactory.getLogger(RedisCache.class);
    private static final String REDIS_UNLOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    @Autowired
    public RedisTemplate redisTemplate;
    /**
     * 获取分布式锁
     *
     * @param key
     * @param token
     * @param expireInSeconds 锁超时时间
     * @return
     */
    public boolean tryLock(String key, String token, long expireInSeconds) {
        Boolean res = redisTemplate.opsForValue().setIfAbsent(key, token, expireInSeconds, TimeUnit.SECONDS);
        log.info("获取分布式锁:"+ key + ":" + token);
        return Objects.equals(res, true);
    }
    /**
     * 分布式锁 unlock,使用lua脚本保证事务
     *
     * @param key
     * @param token lock时的token值,只有token一致才能解锁
     * @return
     */
    public void unlock(String key, String token) {
        try {
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(REDIS_UNLOCK_SCRIPT, Long.class);
            Long res = (Long) redisTemplate.execute(redisScript, Collections.singletonList(key), token);
            log.info("释放分布式锁:"+ key + ":" + token);
            if (!Objects.equals(res, 1L)) {
                log.warn("redis unlock wrong:key=[{}],token=[{}],res=[{}]", key, token, res);
            }
        } catch (Exception e) {
            log.error("redis unlock error:key=[{}],token=[{}]", key, token, e);
        }
    }
    /**
     * @param key
     * @param token
     * @param lockTimeout  锁的超时时间
     * @param acquireTimeout  获取锁的截止时间
     * @return
     */
    public boolean tryAcquireLock(String key, String token, long lockTimeout, long acquireTimeout) {
        try {
            long end = System.currentTimeMillis() + acquireTimeout;
            while (System.currentTimeMillis() < end) {
                Boolean res = redisTemplate.opsForValue().setIfAbsent(key, token, lockTimeout, TimeUnit.MILLISECONDS);

                if (Boolean.TRUE.equals(res)) {
                    log.info("获取分布式锁:"+ key + ":" + token);
                    return true;
                }
                try {
                    Thread.sleep(100);
                } catch (Exception e) {
                    log.error("thread sleep error", e);
                    Thread.currentThread().interrupt();
                }
            }
        } catch (Exception e) {
            log.error("try acquire lock error, ", e);
        }
        return false;
    }
}    

tryAcquireLocktryLock 都用于获取分布式锁,unlock 用于释放分布式锁,逻辑简单,这里不做说明,关注以下几点:

  • tryAcquireLocktryLock 的区别在于,前者在没有获取到锁之后会在限定的时间进行重复尝试获取,后者只尝试获取一次。
  • 防止业务代码在执行的时候抛出异常,每一个锁添加了一个超时时间,超时之后,锁会被自动释放,考虑获取锁和设置过期时间之间如果服务器突然挂掉了,这个时候锁被占用,无法及时得到释放,也会造成死锁所以,所以要保证这个操作是原子的,所以使用 Redis 提供的原子操作 setIfAbsent(检查指定的键是否存在,如果不存在则设置键值对)
  • 如果当前线程执行业务较耗时,超时时间会自动释放锁,其他线程会获取锁,当前线程执行完释放锁或释放到其他线程的锁,会出现混乱,所以需要锁相对线程唯一,自己的锁只能自己释放,使用 key+token 的机制
  • 使用 key+token 的机制,每次释放锁都要判断 value, 一致才释放,但是这样的话,要去查看锁的 value,比较 value 的值是否正确,释放锁, 多个操作不保证原子性,所以unlock 需要引入 lua脚本,Lua 脚本可以在 Redis 服务端原子的执行多个 Redis 命令

上面的实现是最简单的 redis 实现分布式锁,如果要进一步增强分布式锁的可靠性和性能,可以考虑使用更复杂的方案,如 RedLock 算法(redis 集群)、基于 Redis 的 Pub/Sub 机制等。这些方案可以提供更强的分布式锁功能,并解决一些特殊情况下的竞态条件和故障恢复问题。

博文部分内容参考

© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知,这是一个开源项目,如果你认可它,不要吝啬星星哦 😃


https://liruilong.blog.csdn.net/article/details/107076223

http://www.gxitsky.com/2022/02/12/SpringBoot-60-tomcat-nio/


© 2018-2024 liruilonger@gmail.com, All rights reserved. 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)

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

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

相关文章

二百二十一、HiveSQL报错:return code 2 from org.apache.hadoop.hive.ql.exec.mr.MapRedTask

一、目的 在运行HiveSQL时&#xff0c;执行报错 tatement: FAILED: Execution Error, return code 2 from org.apache.hadoop.hive.ql.exec.mr.MapRedTask 二、在yarn上查看任务报错 The required MAP capability is more than the supported max container capability in t…

springboot3+vue支付宝在线支付案例-解决跨域请求的问题

springboot3vue支付宝在线支付案例-解决跨域请求的问题&#xff01;为了使用外网地址&#xff0c;跨域请求业务接口。我们需要设置一个类。配置一下。 我们采用的方案是。借助于 WebMvcConfigurer package com.example.demo.config;import org.springframework.context.annot…

MySQL前百分之N问题--percent_rank()函数

PERCENT_RANK()函数 PERCENT_RANK()函数用于将每行按照(rank - 1) / (rows - 1)进行计算,用以求MySQL中前百分之N问题。其中&#xff0c;rank为RANK()函数产生的序号&#xff0c;rows为当前窗口的记录总行数 PERCENT_RANK()函数返回介于 0 和 1 之间的小数值 selectstudent_…

Git安装,Git镜像,Git已安装但无法使用解决经验

git下载地址&#xff1a; Git - 下载 (git-scm.com) <-git官方资源 Git for Windows (github.com) <-github资源 CNPM Binaries Mirror (npmmirror.com) <-阿里镜像&#xff08;推荐&#xff0c;镜…

Android studio打包apk比较大

1.遇到的问题 在集成linphone打包时发现有118m&#xff0c;为什么如此之大额。用studio打开后发现都是c不同的pu架构。 2.解决办法 增加ndk配置&#xff0c;不选配置那么多的cpu结构&#xff0c;根据自己需要调整。 defaultConfig { applicationId "com.matt.linphoneca…

线性代数---------学习总结

线性代数之行列式 行列式的几条重要的性质 1.某两行某两列交换位置之后&#xff0c;值变号 2.行列式转置&#xff0c;值不变 3.范德蒙德行列式&#xff0c;用不同行的公比做一系列的累乘运算 4.把某一行的行列式加到另一行上&#xff0c;利用他们之间的倍数关系&#xff0…

(十)springboot实战——springboot3下的webflux项目mysql数据库事务处理

前言 WebFlux 是 Spring Framework 5.0 中引入的一种新型反应式编程模型&#xff0c;支持非阻塞 I/O&#xff0c;适用于高并发、高吞吐量的应用程序。在 WebFlux 应用程序中使用事务需要注意以下几点。使用 Reactive R2DBC&#xff1a;WebFlux 支持使用 Reactive R2DBC 访问关…

20240130在ubuntu20.04.6下给GTX1080安装最新的驱动和CUDA

20240130在ubuntu20.04.6下给GTX1080安装最新的驱动和CUDA 2024/1/30 12:27 缘起&#xff0c;为了在ubuntu20.4.6下使用whisper&#xff0c;以前用的是GTX1080M&#xff0c;装了535的驱动。 现在在PDD拼多多上了入手了一张二手的GTX1080&#xff0c;需要将安装最新的545的驱动程…

JavaScript-for循环的执行顺序

1.目标 掌握for执行顺序 2.实现思路 使用for循环输出0-到5 3.代码实现 Document 4.总结 for执行顺序 1.执行 var i 0 变量初始化 条件判断 是否成立 成立 执行循环体 不成立 退出for循环

GC8838取代DRV8838直流电机驱动芯片,可应用在摄像机,玩具等产品上

GC8838 一款 12V 直流电机驱动芯片&#xff0c;为摄像机、消费类产品、玩具和其他低压或者电池供电的运动控制类应用提供了集成的电机驱动解决方案。芯片一般用了驱动一个直流电机或者使用两颗来驱动步进电机。 可以工作在 0~12V 的电源电压上&#xff0c;能提供高达 1.5A 持续…

「优选算法刷题」:只出现一次的数字Ⅱ

一、题目 给你一个整数数组 nums &#xff0c;除某个元素仅出现 一次 外&#xff0c;其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。 你必须设计并实现线性时间复杂度的算法且使用常数级空间来解决此问题。 示例 1&#xff1a; 输入&#xff1a;nums …

蓝桥杯AT24C02问题记录

问题1&#xff1a;从这个图片上可以看出这两个在IIC的.c文件里延时时间不一样&#xff0c;第一张图使用了15个_nop_(); 12M晶振机器周期是 1/12M*121uS&#xff1b;nop()要延时1个指令周期。延时时间不对会对时序产生影响&#xff0c;时序不对&#xff0c;则AT24C02有没被使用…

百度输入法往选字框里强塞广告

关注卢松松&#xff0c;会经常给你分享一些我的经验和观点。 国内几乎100%的输入法都有广告&#xff0c;只是你们没发现而已&#xff01;&#xff01;&#xff01; 百度输入法居然在输入法键盘上推送广告&#xff0c;近日&#xff0c;博主阑夕 表示&#xff0c;V2EX论坛上有…

什么是DDOS流量攻击,DDoS防护安全方案

随着互联网的发展普及&#xff0c;云计算成新趋势&#xff0c;人们对生活方式逐渐发生改变的同时&#xff0c;随之而来的网络安全威胁也日益严重&#xff01; 目前在网络安全方面&#xff0c;网络攻击是最主要的威胁之一&#xff0c;其中DDoS攻击是目前最为常见的网络攻击手段…

Kafka 记录

推荐资源 官网http://kafka.apache.org/Githubhttps://github.com/apache/kafka书籍《深入理解Kafka 核心设计与实践原理》 Kafka 架构 Kafka使用ZooKeeper作为其分布式协调框架&#xff0c;其动态扩容是通过ZooKeeper来实现的。Kafka使用Zookeeper保存broker的元数据和消费者信…

Ubuntu22.04 网络图标突然消失

本来好好的&#xff0c;突然就发现没有网络了&#xff0c;图标也不见了。 特别是Ubuntu虚拟机&#xff0c;容易出现此问题。 修复办法 1. sudo service network-manager stop2. sudo rm /var/lib/NetworkManager/NetworkManager.state3. sudo service network-manager start到…

快速理解MoE模型

最近由于一些开源MoE模型的出现&#xff0c;带火了开源社区&#xff0c;为何&#xff1f;因为它开源了最有名气的GPT4的模型结构&#xff08;OPEN AI&#xff09;&#xff0c;GPT4为何那么强大呢&#xff1f;看看MoE模型的你就知道了。 MoE模型结构&#xff1a; 图中&#xff0…

基于单片机的自动浇花系统设计

摘要&#xff1a;快节奏的生活导致人们忙于工作而无暇顾及家中植物的及时浇水&#xff0c;影响了植物的生长发育&#xff0c; 也降低了其种植成功率。针对上述问题&#xff0c;该文设计了一种自动浇花系统&#xff0c;该系统能在无人环境下 根据土壤湿度情况自动启动&#xff0…

第16章_网络编程(网络通信要素,TCP与UDP协议,网络编程API,TCP网络编程,UDP网络编程,URL编程)

文章目录 第16章_网络编程本章专题与脉络1. 网络编程概述1.1 软件架构1.2 网络基础 2. 网络通信要素2.1 如何实现网络中的主机互相通信2.2 通信要素一&#xff1a;IP地址和域名2.2.1 IP地址2.2.2 域名 2.3 通信要素二&#xff1a;端口号2.4 通信要素三&#xff1a;网络通信协议…

Centos 7.9 安装 Veracrypt-1.26.7

1 下载 veracrypt-1.26.7-CentOS-7-x86_64.rpm VeraCrypt - Free Open source disk encryption with strong security for the Paranoid 2 切换到下载目录&#xff0c;打开终端&#xff0c;切换到管理员用户 运行 yum install veracrypt-1.26.7-CentOS-7-x86_64.rpm 3 安装完…