redis 极简分布式锁实现

写在前面


  • 工作中遇到,整理 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)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

个人多域名SSL证书推荐

SSL数字证书和通配符SSL证书、多域名通配符SSL证书一样&#xff0c;可以同时保护多个域名站点&#xff0c;但是它们之间还是存在一些区别。其中&#xff0c;最明显的区别就是它们的保护域名网站的类型和适用场景。今天就随SSL盾小编来了解多域名SSL证书。 1.多域名SSL证书可以…

Netty源码三:NioEventLoop创建与run方法

1.入口 会调用到父类SingleThreadEventLoop的构造方法 2.SingleThreadEventLoop 继续调用父类SingleThreadEventExecutor的构造方法 3.SingleThreadEventExecutor 到这里完整的总结一下&#xff1a; 将线程执行器保存到每一个SingleThreadEventExcutor里面去创建了MpscQu…

汽车标定技术(十七)--Bypass的前世今生

目录 1.Bypass的诞生 2.Bypass的发扬光大 2.1 基于XCP的Bypassing 2.2 基于Debug的Bypass 2.3 小结 3.Bypass的实际应用 1.Bypass的诞生 下图我相信只要用过INCA的朋友都非常熟悉。 这是远古时期(2000年左右&#xff1f;我猜)ETAS针对发动机控制参数标定设计的一种并行数据…

【鸿蒙开发】第十二章 Stage模型应用组件-信息传递载体Want

1 概述 上一章节我们学习了UIAbility组件【鸿蒙开发】第十一章 Stage模型应用组件-UIAbility&#xff0c;其中组件间的交互传递信息的媒介就是Want&#xff0c;本章节我们来更加深入学习Want的相关知识。 Want是一种对象&#xff0c;用于在应用组件之间传递信息。 2 类型 显…

【CSS】常见

一. 溢出隐藏 1.1 单行文本溢出 .content{max-width:200px; /* 定义容器最大宽度 */overflow:hidden; /* 隐藏溢出的内容 */text-overflow:ellipsis; /* 溢出部分...表示 */white-space: nowrap; /* 确保文本在一行内显示 */ }问题&#xff1a;display:flex 和 ellipsis 冲…

Centos7安装原生Nginx并配置反向代理

一、背景 当我的应用程序需要集群化部署之时&#xff0c;必然需要一个反向代理&#xff0c;当然Nginx的大名&#xff0c;这里不做更多的介绍了&#xff0c;这里介绍一下Nginx常用的四大阵营 1 Ngnix 原生版本 nginx news 2 Nginx Plus 商用版&#xff08;收费的&#xff09…

【JAVA】Long类型返回到前端,精度丢失

一. 问题阐述 20位long类型的数字&#xff0c;从后端接口返回到前端后【四舍五入】 MYSQL端 &#xff08;1&#xff09;bigint (20) &#xff08;2&#xff09;具体某一条数据 JAVA端 &#xff08;1&#xff09;实体类 &#xff08;2&#xff09;服务类 &#xff08;3&…

传统企业要实现数字化转型,需要从哪些方面入手?

数字化转型是一个综合过程&#xff0c;涉及利用数字技术从根本上改变企业运营方式并为客户提供价值。希望踏上数字化转型之旅的传统企业应考虑几个关键方面&#xff0c;以确保成功、平稳过渡。以下是一些需要开始的基本方面&#xff1a; 1.领导承诺&#xff1a; 自上而下的支…

idea Statistic使用

问题描述&#xff1a;本地idea版本为2018.3.5&#xff0c;安装Statistic插件后没有出现Statistic图标 原因如下&#xff1a;插件版本太新了&#xff0c;需要历史版本 解决办法&#xff1a; IDEA安装代码统计插件Statistic后左下角图标出不来(亲测)_idea statistic不展示-CSD…

20240130在ubuntu20.04.6下卸载NVIDIA显卡的驱动

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

老网工秒懂的行业“黑话”,你对齐颗粒度了吗?

你们好&#xff0c;我的网工朋友。 年关将至&#xff0c;多少网工朋友放假了&#xff1f;学技术的心是不是都飘走了。 快过年了&#xff0c;准备和大家聊点有趣、轻松的话题。 前两天部门团建&#xff0c;大家一起去看了年会不能停&#xff0c;挺有意思。 互联网黑话那是一…

解析Kubernets pod DNS域名

k8s dns理解 这个博主讲的很详细 我的这篇文章主要是演示测试 k8s的dns nslookup怎么解析到k8spod域名 创建一个busybox的pod&#xff0c;测试一下pod内是否可以解析 1、流程验证 cat >dns-Deployment.yaml<<EOF apiVersion: apps/v1 kind: Deployment metadata:nam…

PLC找出数据队列里的最大数和最小数所在序号(完整SCL代码)

对于一些需要根据累计运行时间智能启泵和停泵的应用场景,可能会用到此算法,在学习本算法之前,我们需要了解如何在一组数据队列里找出最大数和最小数(这里不涉及排序,只要找到最大数和最小数)。 最大数和最小数搜索FC 请参考下面文章链接: https://rxxw-control.blog.csd…

java+springboot企业员工工作日志审批管理系统ssm+vue

企业OA管理系统具有管理员角色&#xff0c;用户角色&#xff0c;这两个操作权限。 ①管理员 管理员在企业OA管理系统里面查看并管理人事信息&#xff0c;工作审批信息&#xff0c;部门信息&#xff0c;通知公告信息以及内部邮件信息。 管理员功能结构图如下&#xff1a; ide工具…

服务器部署geoserver

linux 进入服务器&#xff0c;创建geoserver文件夹并且解压压缩包 cd /opt mkdir geoserver unzip geoserver-2.19.x-2023-09-22-bin.zip编辑start.ini文件&#xff0c;将port更改为自己的端口 进入bin目录&#xff0c;执行命令包 cd /opt/geoserver/bin ./startup.sh 浏览器…

GoLang和GoLand的安装和配置

1. GoLang 1.1 特点介绍 Go 语言保证了既能达到静态编译语言的安全和性能&#xff0c;又达到了动态语言开发维护的高效率&#xff0c;使用一个表达式来形容 Go 语言&#xff1a;Go C Python , 说明 Go 语言既有 C 静态语言程序的运行速度&#xff0c;又能达到 Python 动态语…

SpringBoot集成MongoDB(3)|(MongoTemplate的List操作)

SpringBoot集成MongoDB&#xff08;3&#xff09;|&#xff08;MongoTemplate的List操作&#xff09; 文章目录 SpringBoot集成MongoDB&#xff08;3&#xff09;|&#xff08;MongoTemplate的List操作&#xff09;[TOC] 前言一、场景说明一、向数组字段添加元素二、从数组中删…

Kube-Promethus配置Nacos监控

Kube-Promethus配置Nacos监控 前置&#xff1a;Kube-Promethus安装监控k8s集群 一.判断Nacos开启监控配置 首先通过集群内部任一节点访问Nacos的这个地址<NacosIP>:端口号/nacos/actuator/prometheus&#xff0c;查看是否能够获取监控数据。 如果没有数据则修改Nacos集群…

【数据结构 02】队列

一、原理 队列通常是链表结构&#xff0c;只允许在一端进行数据插入&#xff0c;在另一端进行数据删除。 队列的特性是链式存储&#xff08;随机增删&#xff09;和先进先出&#xff08;FIFO&#xff1a;First In First Out&#xff09;。 队列的缺陷&#xff1a; 不支持随机…

[Python] 什么是集成算法,什么是随机森林?随机森林分类器(RandomForestClassifier)及其使用案例

什么是集成算法&#xff1f; 集成算法是一种机器学习方法&#xff0c;它将多个基本的学习算法&#xff08;也称为弱学习器&#xff09;组合在一起&#xff0c;形成一个更强大的预测模型。集成算法通过对基本模型的预测进行加权平均或多数投票等方式&#xff0c;来产生最终的预…