RocketMQ源码解析(上)

news2024/11/25 18:36:05

一、ACL权限控制

应用场景:

​RocketMQ提供了针对队列、用户等不同维度的非常全面的权限管理机制。通常来说,RocketMQ作为一个内部服务,是不需要进行权限控制的,但是,如果要通过RocketMQ进行跨部门甚至跨公司的合作,权限控制的重要性就显现出来了。

应用场景:

​RocketMQ提供了针对队列、用户等不同维度的非常全面的权限管理机制。通常来说,RocketMQ作为一个内部服务,是不需要进行权限控制的,但是,如果要通过RocketMQ进行跨部门甚至跨公司的合作,权限控制的重要性就显现出来了。

权限控制体系:

​ 1、RocketMQ针对每个Topic,就有完整的权限控制。比如,在控制平台中,就可以很方便的给每个Topic配置权限。

perm字段表示Topic的权限。有三个可选项。 2:禁写禁订阅,4:可订阅,不能写,6:可写可订阅

2、在Broker端还提供了更详细的权限控制机制。主要是在broker.conf中打开acl的标志:aclEnable=true。然后就可以用他提供的plain_acl.yml来进行权限配置了。并且这个配置文件是热加载的,也就是说要修改配置时,只要修改配置文件就可以了,不用重启Broker服务。文件的配置方式,也非常简单,一目了然。

#全局白名单,不受ACL控制
#通常需要将主从架构中的所有节点加进来
globalWhiteRemoteAddresses:
- 10.10.103.*
- 192.168.0.*

accounts:
#第一个账户
- accessKey: RocketMQ
  secretKey: 12345678
  whiteRemoteAddress:
  admin: false 
  defaultTopicPerm: DENY #默认Topic访问策略是拒绝
  defaultGroupPerm: SUB #默认Group访问策略是只允许订阅
  topicPerms:
  - topicA=DENY #topicA拒绝
  - topicB=PUB|SUB #topicB允许发布和订阅消息
  - topicC=SUB #topicC只允许订阅
  groupPerms:
  # the group should convert to retry topic
  - groupA=DENY
  - groupB=PUB|SUB
  - groupC=SUB
#第二个账户,只要是来自192.168.1.*的IP,就可以访问所有资源
- accessKey: rocketmq2
  secretKey: 12345678
  whiteRemoteAddress: 192.168.1.*
  # if it is admin, it could access all resources
  admin: true

接下来,在客户端就可以通过accessKey和secretKey提交身份信息了。客户端在使用时,需要先引入一个Maven依赖包。

<dependency>
 <groupId>org.apache.rocketmq</groupId>
 <artifactId>rocketmq-acl</artifactId>
 <version>4.9.1</version>
</dependency>

 然后在声明客户端时,传入一个RPCHook。

//声明时传入RPCHook
 DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName", getAclRPCHook());


    private static final String ACL_ACCESS_KEY = "RocketMQ";
    private static final String ACL_SECRET_KEY = "1234567";
    static RPCHook getAclRPCHook() {
        return new AclClientRPCHook(new SessionCredentials(ACL_ACCESS_KEY,ACL_SECRET_KEY));
    }

二、springboot整合RocketMQ

1、快速实战

快速创建RocketMQ的客户端。创建Maven工程,引入关键依赖:

<dependencies>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.2.2</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.rocketmq</groupId>
                    <artifactId>rocketmq-client</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-client</artifactId>
            <version>4.9.5</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.5.9</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.5.9</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
    </dependencies>

使用SpringBoot集成时,要非常注意版本!!!

启动类

@SpringBootApplication
public class RocketMQSBApplication {

    public static void main(String[] args) {
        SpringApplication.run(RocketMQSBApplication.class,args);
    }
}

 配置文件:

rocketmq.name-server=192.168.65.112:9876
rocketmq.producer.group=springBootGroup

#如果这里不配,那就需要在消费者的注解中配。
#rocketmq.consumer.topic=
rocketmq.consumer.group=testGroup
server.port=9000

接下来就可以声明生产者,直接使用RocketMQTemplate进行消息发送。 

package com.roy.rocketmq.basic;

import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Component
public class SpringProducer {

    @Resource
    private RocketMQTemplate rocketMQTemplate;

    public void sendMessage(String topic,String msg){
        this.rocketMQTemplate.convertAndSend(topic,msg);
    }
}

 另外,这个rocketMQTemplate不光可以发消息,还可以主动拉消息。

消费者的声明也很简单。所有属性通过@RocketMQMessageListener注解声明。

@Component
@RocketMQMessageListener(consumerGroup = "MyConsumerGroup", topic = "TestTopic",consumeMode= ConsumeMode.CONCURRENTLY,messageModel= MessageModel.BROADCASTING)
public class SpringConsumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println("Received message : "+ message);
    }
}

这里唯一需要注意下的,就是消息了。SpringBoot框架中对消息的封装与原生API的消息封装是不一样的。 

2、如何处理各种消息类型

1、各种基础的消息发送机制参见单元测试类:com.roy.rocketmq.SpringRocketTest

​ 2、一个RocketMQTemplate实例只能包含一个生产者,也就只能往一个Topic下发送消息。如果需要往另外一个Topic下发送消息,就需要通过@ExtRocketMQTemplateConfiguration()注解另外声明一个子类实例。

​ 3、对于事务消息机制,最关键的事务监听器需要通过@RocketMQTransactionListener注解注入到Spring容器当中。在这个注解当中可以通过rocketMQTemplateBeanName属性,指向具体的RocketMQTemplate子类。

3、实现原理

Push模式

Push模式对于@RocketMQMessageListener注解的处理方式,入口在rocketmq-spring-boot-2.2.2.jar中的org.apache.rocketmq.spring.autoconfigure.ListenerContainerConfiguration类中。

怎么找到的?评经验猜以及碰运气。

​ 这个ListenerContainerConfiguration类继承了Spring当中的SmartInitializingSingleton接口,当Spring容器当中所有非懒加载的实例加载完成后,就会触发他的afterSingletonsInstantiated方法进行初始化。在这个方法中会去扫描所有带有注解@RocketMQMessageListener注解的类,将他注册到内部一个Container容器当中。

public void afterSingletonsInstantiated() {
        Map<String, Object> beans = this.applicationContext.getBeansWithAnnotation(RocketMQMessageListener.class)
            .entrySet().stream().filter(entry -> !ScopedProxyUtils.isScopedTarget(entry.getKey()))
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        beans.forEach(this::registerContainer);
    }

这里这个Container可以认为是客户端实例的一个容器,通过这个容器来封装RocketMQ的原生API。

​ registerContainer的方法挺长的,我这里截取出跟今天的主题相关的几行重要的源码:

private void registerContainer(String beanName, Object bean) {
       .....
 //获取Bean上面的注解
        RocketMQMessageListener annotation = clazz.getAnnotation(RocketMQMessageListener.class);

     ...
    //检查注解的配置情况
        validate(annotation);

        String containerBeanName = String.format("%s_%s", DefaultRocketMQListenerContainer.class.getName(),
            counter.incrementAndGet());
        GenericApplicationContext genericApplicationContext = (GenericApplicationContext) applicationContext;
  //将扫描到的注解转化成为Container,并注册到上下文中。
        genericApplicationContext.registerBean(containerBeanName, DefaultRocketMQListenerContainer.class,
            () -> createRocketMQListenerContainer(containerBeanName, bean, annotation));
        DefaultRocketMQListenerContainer container = genericApplicationContext.getBean(containerBeanName,
            DefaultRocketMQListenerContainer.class);
     //启动容器,这里就相当于是启动了消费者
        if (!container.isRunning()) {
            try {
                container.start();
            } catch (Exception e) {
                log.error("Started container failed. {}", container, e);
                throw new RuntimeException(e);
            }
        }

        log.info("Register the listener to container, listenerBeanName:{}, containerBeanName:{}", beanName, containerBeanName);
    }

这其中最关注的,当然是创建容器的createRocketMQListenerContainer方法中。而在这个方法中,你基本看不到RocketMQ的原生API,都是在创建并维护一个DefaultRocketMQListenerContainer对象。而这个DefaultRocketMQListenerContainer类,就是我们今天关注的重点。

​ DefaultRocketMQListenerContainer类实现了InitializingBean接口,自然要先关注他的afterPropertiesSet方法。这是Spring提供的对象初始化的扩展机制。

public void afterPropertiesSet() throws Exception {
        initRocketMQPushConsumer();

        this.messageType = getMessageType();
        this.methodParameter = getMethodParameter();
        log.debug("RocketMQ messageType: {}", messageType);
    }

 这个方法就是用来初始化RocketMQ消费者的。在这个方法里就会创建一个RocketMQ原生的DefaultMQPushConsumer消费者。同样,方法很长,抽取出比较关注的重点源码。

private void initRocketMQPushConsumer() throws MQClientException {
       .....
        //检查并创建consumer对象。
        if (Objects.nonNull(rpcHook)) {
            consumer = new DefaultMQPushConsumer(consumerGroup, rpcHook, new AllocateMessageQueueAveragely(),
                enableMsgTrace, this.applicationContext.getEnvironment().
                resolveRequiredPlaceholders(this.rocketMQMessageListener.customizedTraceTopic()));
            consumer.setVipChannelEnabled(false);
        } else {
            log.debug("Access-key or secret-key not configure in " + this + ".");
            consumer = new DefaultMQPushConsumer(consumerGroup, enableMsgTrace,
                this.applicationContext.getEnvironment().
                    resolveRequiredPlaceholders(this.rocketMQMessageListener.customizedTraceTopic()));
        }
        // 定制instanceName,有没有很熟悉!!!
        consumer.setInstanceName(RocketMQUtil.getInstanceName(nameServer));
  .....
        //设定广播消费还是集群消费。
        switch (messageModel) {
            case BROADCASTING:
                consumer.setMessageModel(org.apache.rocketmq.common.protocol.heartbeat.MessageModel.BROADCASTING);
                break;
            case CLUSTERING:
                consumer.setMessageModel(org.apache.rocketmq.common.protocol.heartbeat.MessageModel.CLUSTERING);
                break;
            default:
                throw new IllegalArgumentException("Property 'messageModel' was wrong.");
        }
     //维护消费者的其他属性。   
     ...
           //指定Consumer的消费监听 --》在消费监听中就会去调用onMessage方法。
           switch (consumeMode) {
            case ORDERLY:
                consumer.setMessageListener(new DefaultMessageListenerOrderly());
                break;
            case CONCURRENTLY:
                consumer.setMessageListener(new DefaultMessageListenerConcurrently());
                break;
            default:
                throw new IllegalArgumentException("Property 'consumeMode' was wrong.");
        }
    }

这整个就是在维护RocketMQ的原生消费者对象。其中的使用方式,其实有很多地方是很值得借鉴的,尤其是消费监听的处理。

Pull模式

Pull模式的实现其实是通过在RocketMQTemplate实例中注入一个DefaultLitePullConsumer实例来实现的。只要注入并启动了这个DefaultLitePullConsumer示例后,后续就可以通过template实例的receive方法,来调用DefaultLitePullConsumer的poll方法,主动去Pull获取消息了。

​ 初始化DefaultLitePullConsumer的代码依然是在rocketmq-spring-boot-2.2.2.jar包中。不过处理类是org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration。这个配置类会配置在jar包中的spring.factories文件中,通过SpringBoot的自动装载机制加载进来。

@Bean(CONSUMER_BEAN_NAME)
    @ConditionalOnMissingBean(DefaultLitePullConsumer.class)
    @ConditionalOnProperty(prefix = "rocketmq", value = {"name-server", "consumer.group", "consumer.topic"}) //解析的springboot配置属性。
    public DefaultLitePullConsumer defaultLitePullConsumer(RocketMQProperties rocketMQProperties)
            throws MQClientException {
        RocketMQProperties.Consumer consumerConfig = rocketMQProperties.getConsumer();
        String nameServer = rocketMQProperties.getNameServer();
        String groupName = consumerConfig.getGroup();
        String topicName = consumerConfig.getTopic();
        Assert.hasText(nameServer, "[rocketmq.name-server] must not be null");
        Assert.hasText(groupName, "[rocketmq.consumer.group] must not be null");
        Assert.hasText(topicName, "[rocketmq.consumer.topic] must not be null");
  
        ...
        //创建消费者   
        DefaultLitePullConsumer litePullConsumer = RocketMQUtil.createDefaultLitePullConsumer(nameServer, accessChannel,
                groupName, topicName, messageModel, selectorType, selectorExpression, ak, sk, pullBatchSize, useTLS);
        litePullConsumer.setEnableMsgTrace(consumerConfig.isEnableMsgTrace());
        litePullConsumer.setCustomizedTraceTopic(consumerConfig.getCustomizedTraceTopic());
        litePullConsumer.setNamespace(consumerConfig.getNamespace());
        return litePullConsumer;
    }

RocketMQUtil.createDefaultLitePullConsumer方法中,就是在维护一个DefaultLitePullConsumer实例。这个实例就是RocketMQ的原生API当中提供的拉模式客户端。

实际开发中,拉模式用得比较少。但是,其实RocketMQ针对拉模式也做了非常多的优化。原本提供了一个DefaultMQPullConsumer类,进行拉模式消息消费,DefaultLitePullConsumer在此基础上做了很多优化。有兴趣可以自己研究一下。 

三、RocketMQ最佳实践 

1、合理分配Topic、Tag

一个应用尽可能用一个Topic,而消息子类型则可以用tags来标识。tags可以由应用自由设置,只有生产者在发送消息设置了tags,消费方在订阅消息时才可以利用tags通过broker做消息过滤:message.setTags("TagA")。

2、使用Key加快消息索引

每个消息在业务层面的唯一标识码要设置到keys字段,方便将来定位消息丢失问题。服务器会为每个消息创建索引(哈希索引),应用可以通过topic、key来查询这条消息内容,以及消息被谁消费。由于是哈希索引,请务必保证key尽可能唯一,这样可以避免潜在的哈希冲突。

3、关注错误消息重试

我们已经知道RocketMQ的消费者端,如果处理消息失败了,Broker是会将消息重新进行投送的。而在重试时,RocketMQ实际上会为每个消费者组创建一个对应的重试队列。重试的消息会进入一个 “%RETRY%”+ConsumeGroup  的队列中。

如果重试次数超过设置的次数会进入死信队列,到时候需要手动处理,所以要检查消费者端执行失败的代码

4、手动处理死信队列

死信队列需要人工进行干预,而死信队列的默认权限是不可读并且不可写,权限perm被设置成了2:禁读(这个权限有三种 2:禁读,4:禁写,6:可读可写),需要手动将死信队列的权限配置成6,才能被消费(可以通过mqadmin指定或者web控制台)。死信队列和重试队列都只与消费者组有关,和topic和消费者终端无关

5、消费者端进行幂等控制

在MQ系统中,对于消息幂等有三种实现语义:

  • at most once 最多一次:每条消息最多只会被消费一次
  • at least once 至少一次:每条消息至少会被消费一次
  • exactly once 刚刚好一次:每条消息都只会确定的消费一次

这三种语义都有他适用的业务场景。

其中,at most once是最好保证的。RocketMQ中可以直接用异步发送、sendOneWay等方式就可以保证。

为保证消息消费只有一次,生产者发送消息时最好设置一个全局标识性id,消费者端根据标识判断消费一次就行

四、RocketMQ基本源码分析

1、nameserver启动流程

  • kvConfigManager:key、value的配置读取
  • routeInfoManager:组件路由,重定位到broker上
  • NettyRemotingServer:接收远端请求的服务器,接收broker请求注册和客户端发来的请求

2、broker启动流程

1、关注重点

​ Broker是整个RocketMQ的业务核心。所有消息存储、转发这些重要的业务都是Broker进行处理。

​ 这里重点梳理Broker有哪些内部服务。这些内部服务将是整理Broker核心业务流程的起点。

2、源码重点

Broker启动的入口在BrokerStartup这个类,可以从他的main方法开始调试。

启动过程关键点:重点也是围绕一个BrokerController对象,先创建,然后再启动。

首先: 在BrokerStartup.createBrokerController方法中可以看到Broker的几个核心配置:

  • BrokerConfig : Broker服务配置
  • MessageStoreConfig : 消息存储配置。 这两个配置参数都可以在broker.conf文件中进行配置
  • NettyServerConfig :Netty服务端占用了10911端口。同样也可以在配置文件中覆盖。
  • NettyClientConfig : Broker既要作为Netty服务端,向客户端提供核心业务能力,又要作为Netty客户端,向NameServer注册心跳。

这些配置是我们了解如何优化 RocketMQ 使用的关键。

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

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

相关文章

公司如何监控员工自己的电脑(监控单位员工电脑的几个好用的方法)

在现代的商业环境中&#xff0c;公司需要在保护敏感数据和确保员工生产力之间找到平衡。为此&#xff0c;许多公司选择监控员工的电脑使用情况。本文将详细介绍如何专业且有效地监控公司员工电脑。 一、为何需要监控员工电脑 公司可能会出于各种原因去监控员工的电脑使用&…

CSS中的定位

position 的属性与含义 CSS 中的 position 属性用于控制元素在页面中的定位方式&#xff0c;有四个主要的取值&#xff0c;每个取值都会影响元素的布局方式&#xff0c;它们是&#xff1a; static&#xff08;默认值&#xff09;&#xff1a; 这是所有元素的初始定位方式。在静…

字符函数和字符串函数(C语言进阶)

字符函数和字符串函数 一.求字符串长度1.strlen 二.长度不受限制的字符串函数介绍1.strcpy2.strcat3.strcmp 前言 C语言中对字符和字符串的处理很是频繁&#xff0c;但是C语言本身是没有字符串类型的&#xff0c;字符串通常放在常量字符串中或者字符数组中。 字符串常量适用于那…

[刷题记录]牛客面试笔刷TOP101(二)

(一)传送门: [刷题记录]牛客面试笔刷TOP101(一)_HY_PIGIE的博客-CSDN博客 目录 1.合并二叉树 2.二叉树的镜像 3.判断是否为二叉搜索树 4.判断是不是完全二叉树 1.合并二叉树 合并二叉树_牛客题霸_牛客网 (nowcoder.com) 思路: 在后序遍历的基础上进行,两颗二叉树可…

【网络协议】Http-中

搜索引擎&#xff1a;搜索引擎是指根据一定的策略、运用特定的计算机程序从互联网上采集信息&#xff0c;在对信息进行组织和处理后&#xff0c;为用户提供检索服务&#xff0c;将检索的相关信息展示给用户的系统。搜索引擎是工作于互联网上的一门检索技术&#xff0c;它旨在提…

WPF 类库 使用handycontrol 配置

在学习wpf发现了一个非常好用的UI库 handycontrol 但是很多地方讲的都是WPF应用程序怎么用&#xff0c;很少有讲类库那么引用的问题&#xff0c;所以在这里自己总结一下&#xff0c;希望能帮助到大家&#xff1a; 1.添加 handycontrol 的引用&#xff1b;安装&#xff0c;我已…

Webpack打包图片

一、在js文件中引入图片 二、在package.config.js中配置加载器 module.exports {mode: "production", // 设置打包的模式&#xff1a;production生产模式 development开发模式module: {rules: [// 配置img加载器{test: /\.(jpg|png|gif)$/i,type:"asset/resou…

计算机竞赛 深度学习+python+opencv实现动物识别 - 图像识别

文章目录 0 前言1 课题背景2 实现效果3 卷积神经网络3.1卷积层3.2 池化层3.3 激活函数&#xff1a;3.4 全连接层3.5 使用tensorflow中keras模块实现卷积神经网络 4 inception_v3网络5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; *…

基于TensorFlow+CNN+协同过滤算法的智能电影推荐系统——深度学习算法应用(含微信小程序、ipynb工程源码)+MovieLens数据集(六)

目录 前言总体设计系统整体结构图系统流程图 运行环境模块实现1. 模型训练1&#xff09;数据集分析2&#xff09;数据预处理3&#xff09;模型创建4&#xff09;模型训练5&#xff09;获取特征矩阵 2. 后端Django3. 前端微信小程序1&#xff09;小程序全局配置文件2&#xff09…

【广州华锐互动】煤矿坍塌VR事故警示教育突破了哪些限制?

煤矿坍塌事故是煤矿行业的一种常见事故&#xff0c;对于矿工的生命安全和生产设备都存在着严重威胁。传统的安全培训方式往往难以真实地呈现事故场景&#xff0c;难以达到理想的安全教育效果。而虚拟现实&#xff08;VR&#xff09;技术的出现&#xff0c;为煤矿安全教育带来了…

测试人职场生存必须避开的5个陷阱

在互联网职场的工作发展道路上&#xff0c;软件测试人员其实在公司中也面临着各种各样的职场陷阱&#xff0c;有些可能是因为项目业务不熟练造成的&#xff0c;有些可能是自身技术能力不足导致的...等等。软件测试入门相对来说比较容易些&#xff0c;但是想要在测试行业长久发展…

力扣刷题-数组-数组理论基础

数组是存放在连续内存空间上的相同类型数据的集合。 需要两点注意的是 数组下标都是从0开始的。数组内存空间的地址是连续的 正是因为数组的在内存空间的地址是连续的**&#xff0c;所以我们在删除或者增添元素的时候&#xff0c;就难免要移动其他元素的地址。** 注意&…

S型加减速行车位置控制(支持点动和停靠位置搜索)

S型加减速位置控制详细算法和应用场景介绍&#xff0c;请查看下面文章博客。本篇文章不再赘述&#xff0c;这里主要介绍点动动和位置点搜索功能。 S速度曲线轨迹规划(普通变频位置闭环控制算法详细介绍SCL代码)_s曲线轨迹规划_RXXW_Dor的博客-CSDN博客位置控制用PD控制器&…

Rsync学习笔记2

Rsync&#xff1a; 增量操作&#xff1a; 1&#xff09; server01服务文件变动。 [rootserver03 tp5shop]# rsync -av /usr/local/nginx/html/tp5shop root192.168.17.109:/usr/local/nginx/html/ sending incremental file listsent 88,134 bytes received 496 bytes 177,…

KMP,ACM集训

目录 831. KMP字符串 输入格式 输出格式 数据范围 输入样例&#xff1a; 输出样例&#xff1a; 解析&#xff1a;KMP模板 D - Cyclic Nacklace 解析&#xff1a;KMP-next数组应用循环字符串判断 F - Power Strings 解析&#xff1a;KMP-next数组应用循环字符串判断 H - …

Haproxy负载均衡群集

HAproxy搭建Web群集一、Web集群调度器1、常见的Web集群调度器2、常用集群调度器的优缺点&#xff08;LVS ,Nginx,Haproxy)2.1 Nginx2.2 LVS2.3 Haproxy 3、LVS、Nginx、HAproxy的区别 二、Haproxy1、简介2、Haproxy应用分析3、HAProxy的主要特性4、Haproxy调度算法&#xff08;…

智慧云图书馆: 能支撑智慧图书馆服务体系的图书馆管理与服务平台

一、开源项目简介 柏拉图 PLATO 智慧云图书馆&#xff1a; 能支撑智慧图书馆服务体系的图书馆管理与服务平台。 二、开源协议 未使用主流开源协议 三、界面展示 四、功能概述 平台优势 总分馆架构&#xff1a;不再是信息的孤岛&#xff0c;而是共享信息的平台。友好的界…

openGauss学习笔记-76 openGauss 数据库管理-内存优化表MOT管理-内存表特性-MOT简介

文章目录 openGauss学习笔记-76 openGauss 数据库管理-内存优化表MOT管理-内存表特性-MOT简介76 MOT简介 openGauss学习笔记-76 openGauss 数据库管理-内存优化表MOT管理-内存表特性-MOT简介 本节介绍了openGauss内存优化表&#xff08;Memory-Optimized Table&#xff0c;MOT…

spring-boot---validation,参数校验,分组,嵌套,各种类型

写在前面&#xff1a; 参数校验基本上是controller必做的事情&#xff0c;毕竟前端传过来的一切都不可信。 但是每次if(StrUtil.isNotNull())啥的有时候多还难写。validation可以简化这一操作。 文章目录 项目构建问题展示validation使用快速入门注释 Valid与Validated区别使…

【面试必刷TOP101】判断一个链表是否为回文结构 链表的奇偶重排

目录 题目&#xff1a;判断一个链表是否为回文结构_牛客题霸_牛客网 (nowcoder.com) 题目的接口&#xff1a; 解题思路&#xff1a; 代码&#xff1a; 过啦&#xff01;&#xff01;&#xff01; 题目&#xff1a;链表的奇偶重排_牛客题霸_牛客网 (nowcoder.com) 题目的…