【微服务】SpringCloud微服务注册源码解析

news2024/11/13 23:24:08

目录

一、前言

1、简述

2、SpringCloudCommons 项目

二、客户端服务注册

1、流程图

2、入口

2.1、客户端注册引入依赖

3、EurekaServiceRegistry服务注册机

3.1、EurekaServiceRegistry注册逻辑

4、ApplicationInfoManager

4.1、setInstanceStatus(InstanceStatus status)逻辑

5、DiscoveryClient核心组件

5.1、状态变更者

6、InstanceInfoReplicator组件

2)启动复制任务

6.1、onDemandUpdate()按需更新逻辑

6.2、run()任务逻辑注册微服务

7、DiscoveryClient的注册逻辑

8、发送HTTP请求注册中心完成微服务注册

三、服务端服务注册

1、InstanceRegistry

2、缓存到注册中心本地


一、前言

1、简述

    本篇文章要探讨的问题如下:服务注册发现的抽象(这个提供了方向)?Eureka微服务注册的入口?Future模式的使用、调度线程池使用、周期性任务、令牌桶限流、加锁、取消了如何恢复?当然,提出了这些问题,都是下面涉及到的。

2、SpringCloudCommons 项目

    SpringCloudCommons 提供了两个库的特性: SpringCloudContext 和 SpringCloudCommons。Spring Cloud Context 为 Spring Cloud 应用程序的 ApplicationContext 提供实用工具和特殊服务(引导上下文、加密、刷新范围和环境端点)。SpringCloudCommons 是在不同的 SpringCloud 实现中使用的一组抽象公共类(Nacos、Spring Cloud Netflix、Spring Cloud Consul都是基于它实现微服务注册和发现的)。

二、客户端服务注册

1、流程图(自行补充)

2、入口

2.1、客户端注册引入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

如果想了解SpringCloud项目搭建请看SpringCloud微服务第1章之项目搭建

是不是很熟悉,同样的配方,跟Nacos客户端注册核心相关类前缀不一样,还有SpringBoot自动装配相关的META-INF下的spring.factories文件。关于集成这方面前面的文章都详细分析了,这里就不会过多赘述,感兴趣的读者可以回头巩固一下,这里我们就直奔主题了。

3、EurekaServiceRegistry服务注册机

RegistrationServiceRegistryspring-cloud-commons项目下的,Registration用来维护需要注册的服务信息,ServiceRegistry抽象了微服务注册、发现等,EurekaServiceRegistry是spring-cloud-netflix-eureka-client下的服务注册机。

3.1、EurekaServiceRegistry注册逻辑

EurekaServiceRegistry的初始化是SpringBoot的自动装配完成的,触发注册逻辑也是这个时候开始的。

	@Override
	public void register(EurekaRegistration reg) {
		maybeInitializeClient(reg);

		if (log.isInfoEnabled()) {
			log.info("Registering application "
					+ reg.getApplicationInfoManager().getInfo().getAppName()
					+ " with eureka with status "
					+ reg.getInstanceConfig().getInitialStatus());
		}

		// 设置实例状态,开始注册
		reg.getApplicationInfoManager()
				.setInstanceStatus(reg.getInstanceConfig().getInitialStatus());
		// 注册健康检查
		reg.getHealthCheckHandler().ifAvailable(healthCheckHandler -> reg
				.getEurekaClient().registerHealthCheck(healthCheckHandler));
	}

initialStatus为向远程 Eureka 服务器注册的初始状态,默认InstanceStatus.UP,在EurekaInstanceConfigBean中维护。setInstanceStatus()设置实例的状态,里面会触发注册逻辑。点击跟进到下面的处理逻辑:

4、ApplicationInfoManager

下面的一些组件都是在原生的Eureka项目。

4.1、setInstanceStatus(InstanceStatus status)逻辑

    public synchronized void setInstanceStatus(InstanceStatus status) {
        // UP
        InstanceStatus next = instanceStatusMapper.map(status);
        if (next == null) {
            return;
        }

        // STARTING
        InstanceStatus prev = instanceInfo.setStatus(next);
        if (prev != null) {
            // 自动装配流程已经初始化
            for (StatusChangeListener listener : listeners.values()) {
                try {
                    // 通知状态变更
                    listener.notify(new StatusChangeEvent(prev, next));
                } catch (Exception e) {
                    logger.warn("failed to notify listener: {}", listener.getId(), e);
                }
            }
        }
    }

启动注册中心再启动一个微服务,

会发现StatusChangeEvent的两个状态值,说明执行了这里的逻辑;而这个日志是下面的方法打印的,即这里调用了下面的方法,这就意味着:next为UP,prev为STARTING。调用监听器的通知方法,发布状态变更事件。

1)setStatus(InstanceStatus status)

    public synchronized InstanceStatus setStatus(InstanceStatus status) {
        if (this.status != status) {
            InstanceStatus prev = this.status;
            this.status = status;
            setIsDirty();
            return prev;
        }
        return null;
    }

该方法在InstanceInfo维护,方法逻辑:如果设置了与当前状态不同的状态,则返回 prev 状态,否则返回 null。

2)InstanceStatus服务实例状态枚举类

    public enum InstanceStatus {
        UP, // Ready to receive traffic准备接收通讯
        DOWN, // Do not send traffic- healthcheck callback failed不要发送交通健康检查回调失败
        STARTING, // Just about starting- initializations to be done - do not send traffic
        // 刚刚开始-初始化要做-不要发送流量
        OUT_OF_SERVICE, // Intentionally shutdown for traffic故意关闭交通
        UNKNOWN;

        public static InstanceStatus toEnum(String s) {
            if (s != null) {
                try {
                    return InstanceStatus.valueOf(s.toUpperCase());
                } catch (IllegalArgumentException e) {
                    // ignore and fall through to unknown
                    logger.debug("illegal argument supplied to InstanceStatus.valueOf: {}, defaulting to {}", s, UNKNOWN);
                }
            }
            return UNKNOWN;
        }
    }

该类同样是在InstanceInfo维护:

  • UP, // 运行中状态,准备接收通讯
  • DOWN, // 下线状态,不要发送交通健康检查回调失败
  • STARTING, // 初始化状态,刚刚开始-初始化不要发送流量
  • OUT_OF_SERVICE, // 故意关闭交通
  • UNKNOWN; // 未知状态

5、DiscoveryClient核心组件

5.1、状态变更者

它是在DiscoveryClient初始化时初始化的。

            // 状态变更监听者
            statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
                @Override
                public String getId() {
                    return "statusChangeListener";
                }

                @Override
                public void notify(StatusChangeEvent statusChangeEvent) {
                    // Saw local status change event StatusChangeEvent [timestamp=1668595102513, current=UP, previous=STARTING]
                    logger.info("Saw local status change event {}", statusChangeEvent);
                    instanceInfoReplicator.onDemandUpdate();
                }
            };
            // 初始化状态变更监听者
            if (clientConfig.shouldOnDemandUpdateStatusChange()) {
                applicationInfoManager.registerStatusChangeListener(statusChangeListener);
            }

这个是Java匿名内部类的,ApplicationInfoManager维护了监听器的缓存注册等,故在4.1中调用监听器的方法来到这里的处理逻辑。这里打印日志,如果你开始了日志打印可以在控制台看到。我们点击继续跟踪到下面逻辑:

6、InstanceInfoReplicator组件

InstanceInfoReplicator实现了Runnable接口,在DiscoveryClient初始化时初始化,职责:更新本地instanceInfo并将其复制到远程服务器的任务:

  • 配置为单个更新线程,以保证对远程服务器的顺序更新
  • 更新任务可以通过onDemandUpdate()按需调度
  • 任务处理的速率受到BurstSize(默认2)的限制
  • 新的更新任务总是在早期更新任务之后自动安排。但是,如果启动了 按需任务,计划的自动更新任务将被丢弃(并且将在 新的按需 更新之后安排新的任务)。

1)初始化

            // InstanceInfo replicator实例信息复制任务
            instanceInfoReplicator = new InstanceInfoReplicator(
                    this,
                    instanceInfo,
                    clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                    2); // burstSize

            // 状态变更监听者
            .....省略.......


            // 定时刷新服务实例信息和检查应用状态的变化,在服务实例信息发生改变的情况下向server重新发起注册
            instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());

构建实例信息复制任务,定时刷新服务实例信息和检查应用状态的变化,在服务实例信息发生改变的情况下向server重新发起注册。

2)启动复制任务

    public void start(int initialDelayMs) {
        // 上面默认false,可见CAS成功
        if (started.compareAndSet(false, true)) {
            instanceInfo.setIsDirty();  // for initial register
            // 调度执行任务(自己的run()逻辑)
            Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
            scheduledPeriodicRef.set(next);
        }
    }

新的更新任务总是在早期更新任务之后自动安排。但是,如果启动了 按需任务,计划的自动更新任务将被丢弃(并且将在 新的按需 更新之后安排新的任务)。如下面:

6.1、onDemandUpdate()按需更新逻辑

    public boolean onDemandUpdate() {
        // 令牌桶算法进行限流
        if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) {
            // 执行器未关闭
            if (!scheduler.isShutdown()) {
                // 提交任务
                scheduler.submit(new Runnable() {
                    @Override
                    public void run() {
                        logger.debug("Executing on-demand update of local InstanceInfo");
    
                        Future latestPeriodic = scheduledPeriodicRef.get();
                        // isDone()如果此任务完成,则返回 true。观察控制台初始化时可见是false
                        if (latestPeriodic != null && !latestPeriodic.isDone()) {
                            // 取消最新的预定更新,将在onDemandUpdate()结束时重新调度
                            // 由于scheduler只有一个核心线程,而start(int initialDelayMs)中使用了,故这里取消
                            logger.debug("Canceling the latest scheduled update, it will be rescheduled at the end of on demand update");
                            // 取消但是不中断
                            latestPeriodic.cancel(false);
                        }
                        // 执行自己的run逻辑
                        InstanceInfoReplicator.this.run();
                    }
                });
                return true;
            } else {
                logger.warn("Ignoring onDemand update due to stopped scheduler");
                return false;
            }
        } else {
            logger.warn("Ignoring onDemand update due to rate limiter");
            return false;
        }
    }

主要逻辑:

  1. RateLimiter基于令牌桶算法进行限流(令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务)。
  2. 执行器未关闭,提交任务
  3. 取消最新的预定更新,将在onDemandUpdate()结束时重新调度。由于scheduler只有一个核心线程,而start(int initialDelayMs)中使用了,故这里取消
  4. 执行自己的run逻辑,下面分析

6.2、run()任务逻辑注册微服务

    public void run() {
        try {
            // 刷新本地服务实例信息
            discoveryClient.refreshInstanceInfo();
            // start中初始化时设置了isInstanceInfoDirty为true,所以dirtyTimestamp不为空
            Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
            if (dirtyTimestamp != null) {
                // 注册
                discoveryClient.register();
                instanceInfo.unsetIsDirty(dirtyTimestamp);
            }
        } catch (Throwable t) {
            // 实例信息复制器出了点问题
            logger.warn("There was a problem with the instance info replicator", t);
        } finally {
            // 恢复复制,默认实例信息复制器随需应变允许的每分钟更新率为4
            Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
            scheduledPeriodicRef.set(next);
        }
    }

主要逻辑:

  1. 刷新本地服务实例信息
  2. 请求注册中心注册
  3. 恢复复制,默认实例信息复制器随需应变允许的每分钟更新率为4。如果onDemandUpdate()又被执行,还是可能被丢弃的。

7、DiscoveryClient的注册逻辑

    boolean register() throws Throwable {
        logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
        EurekaHttpResponse<Void> httpResponse;
        try {
            // 请求注册
            httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
        } catch (Exception e) {
            logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
            throw e;
        }
        if (logger.isInfoEnabled()) {
            logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
        }
        // 返回注册是否成功
        return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
    }

兜个圈又回来了!!!调用与注册中心通信类的注册方法,返回是否注册成功。

8、发送HTTP请求注册中心完成微服务注册

    @Override
    public EurekaHttpResponse<Void> register(InstanceInfo info) {
        // 拼接URL
        String urlPath = "apps/" + info.getAppName();
        ClientResponse response = null;
        try {
            // 获取resourceBuilder
            Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
            addExtraHeaders(resourceBuilder);
            // 发送post请求注册中心
            response = resourceBuilder
                    .header("Accept-Encoding", "gzip")
                    .type(MediaType.APPLICATION_JSON_TYPE)
                    .accept(MediaType.APPLICATION_JSON)
                    .post(ClientResponse.class, info);
            return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
        } finally {
            if (logger.isDebugEnabled()) {
                logger.debug("Jersey HTTP POST {}{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
                        response == null ? "N/A" : response.getStatus());
            }
            if (response != null) {
                response.close();
            }
        }
    }

主要逻辑:

  1. 拼接请求注册中心URL
  2. 获取resourceBuilder,这个是sun公司提供的
  3. 发送post请求注册中心,返回响应结果

三、服务端服务注册

1、InstanceRegistry

	@Override
	public void register(InstanceInfo info, int leaseDuration, boolean isReplication) {
		handleRegistration(info, leaseDuration, isReplication);
		super.register(info, leaseDuration, isReplication);
	}

	@Override
	public void register(final InstanceInfo info, final boolean isReplication) {
		handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication);
		super.register(info, isReplication);
	}

这里的逻辑不多,两个注册方法,通过super委托PeerAwareInstanceRegistryImpl父类AbstractInstanceRegistry处理。

AbstractInstanceRegistry抽象服务实例注册机,我们下面看看它的注册逻辑:

2、缓存到注册中心本地

    public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
        // 加锁
        read.lock();
        try {
            // 尝试从本地获取
            Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
            REGISTER.increment(isReplication);
            // 本地没有则缓存起来
            if (gMap == null) {
                final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
                gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
                if (gMap == null) {
                    gMap = gNewMap;
                }
            }
            Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
            // Retain the last dirty timestamp without overwriting it, if there is already a lease
            // 如果已经有租约,则保留最后一个脏时间戳而不覆盖它
            if (existingLease != null && (existingLease.getHolder() != null)) {
                Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
                Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
                logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);

                // this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted
                // InstanceInfo instead of the server local copy.
                if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
                    logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +
                            " than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
                    logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
                    registrant = existingLease.getHolder();
                }
            } else {
                // The lease does not exist and hence it is a new registration
                // 租约不存在,因此是一个新的注册
                synchronized (lock) {
                    if (this.expectedNumberOfClientsSendingRenews > 0) {
                        // Since the client wants to register it, increase the number of clients sending renews
                        // 因为客户端想要注册它,所以增加发送更新的客户端的数量
                        this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
                        updateRenewsPerMinThreshold();
                    }
                }
                logger.debug("No previous lease information found; it is new registration");
            }
            Lease<InstanceInfo> lease = new Lease<>(registrant, leaseDuration);
            if (existingLease != null) {
                lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
            }
            gMap.put(registrant.getId(), lease);
            // 修改数据保存到最近队列 用于客户端增量更新
            recentRegisteredQueue.add(new Pair<Long, String>(
                    System.currentTimeMillis(),
                    registrant.getAppName() + "(" + registrant.getId() + ")"));
            // This is where the initial state transfer of overridden status happens
            if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
                logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
                                + "overrides", registrant.getOverriddenStatus(), registrant.getId());
                if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
                    logger.info("Not found overridden id {} and hence adding it", registrant.getId());
                    overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
                }
            }
            InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
            if (overriddenStatusFromMap != null) {
                logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
                registrant.setOverriddenStatus(overriddenStatusFromMap);
            }

            // Set the status based on the overridden status rules
            InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
            registrant.setStatusWithoutDirty(overriddenInstanceStatus);

            // If the lease is registered with UP status, set lease service up timestamp
            if (InstanceStatus.UP.equals(registrant.getStatus())) {
                lease.serviceUp();
            }
            registrant.setActionType(ActionType.ADDED);
            // 修改数据保存到最近队列 用于客户端增量更新
            recentlyChangedQueue.add(new RecentlyChangedItem(lease));
            registrant.setLastUpdatedTimestamp();
            invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
            logger.info("Registered instance {}/{} with status {} (replication={})",
                    registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
        } finally {
            // 释放锁
            read.unlock();
        }
    }

主要逻辑:

  1. 加锁,尝试从本地获取看下有没有注册了
  2. 本地registry(ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>)没有则重新缓存起来
  3. 如果已经有租约,则保留最后一个脏时间戳而不覆盖它;否则租约不存在,因此是一个新的注册,因为客户端想要注册它,所以增加发送更新的客户端的数量。
  4. 修改数据保存到最近队列 用于客户端增量更新

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

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

相关文章

【区块链技术与应用】(七)

资料来源 https://pkg.go.dev/github.com/hyperledger/fabric-sdk-go#section-readme https://github.com/hyperledger/fabric-sdk-go https://wiki.hyperledger.org/display/fabric https://github.com/hyperledger/fabric-samples 书接上回&#xff0c;补充getway链码分析 上…

maven assembly打包生成Java应用启动脚本bat和sh

1. maven插件介绍 springboot应用通过maven插件appassembler-maven-plugi生成启动脚本bat和sh。根据官网介绍&#xff0c;这个插件主要用于生成启动 java应用程序的脚本&#xff0c;能将项目依赖jar能够打包目录中&#xff0c;并且它们加入启动脚本类路径中。 主要命令 appas…

springboot瑞吉外卖

创建数据库,项目初始化静态资源不在static目录下&#xff0c;如何映射结果类登录过滤器拦截路径全局异常处理器分页查询消息转换器修改禁用分页编辑公共字段自动填充使用ThreadLocal新增用户绑定的数据不可删除上传下载前端传递的数据&#xff0c;不在同一张表时&#xff0c;DT…

java设计模式之策略模式

一&#xff1a;策略模式 1.什么是策略模式? 模板方法模式是一种行为设计模式&#xff0c; 它在超类中定义了一个算法的框架&#xff0c; 允许子类在不修改结构的情况下重写算法的特定步骤。 策略模式的基本介绍 1.策略模式&#xff08;Strategy Pattern&#xff09;中&#x…

【MQ基本概念 MQ的工作原理】

一、MQ基本概念 1、MQ概述 MQ全称Message Queue&#xff08;消息队列&#xff09;&#xff0c;是在消息的传输过程中保存 消息的容器。多用于分布式系统之间进 行通信。 小结 MQ&#xff0c;消息 队列&#xff0c;存储消息的中间件 分布式系统通信两种方式&#xff1a;直接远程…

2022年数维杯D题 极端天气问题思路指导

D题损失评估与应对策略的研究三重拉尼娜事件下的极端气候灾害 很明显D题是一个数据收集➕数据处理类型题目&#xff0c;这与美赛中的E题题型相似。该题所涉及的极端天气与2021年小美赛极端天气题目高度相似。因此&#xff0c;我们首先对大家整理了去年小美赛极端天气的相关论文…

Kubeadm搭建kubernetes集群

Kubeadm搭建kubernetes集群 环境说明 | 角色 | ip | 操作系统 |组件 | | – | – | – | | master | 192.168.226.10 |centos8 | docker&#xff0c;kubectl&#xff0c;kubeadm&#xff0c;kubelet | | node1 | 192.168.226.20 |centos8 |docker&#xff0c;kubectl&#xff…

登陆拦截案例

登陆拦截案例 登陆拦截器小案例&#xff0c;判断登陆的用户名及密码是否正确&#xff1b;&#xff1a; 1.创建一个maven项目&#xff0c;导入相关的坐标&#xff1a; <dependencies><dependency><groupId>org.springframework</groupId><artifact…

【Hack The Box】windows练习-- Blackfield

HTB 学习笔记 【Hack The Box】windows练习-- Blackfield &#x1f525;系列专栏&#xff1a;Hack The Box &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f4c6;首发时间&#xff1a;&#x1f334;2022年11月17日&#x1f334; &…

[Games 101] Lecture 10 Geometry 1 (Introduction)

Geometry 1 (Introduction) Ways to Represent Geometry 隐式 (Implicit) 几何 只告诉点满足某种约束或关系&#xff0c;并不给出实际的点&#xff0c;也就是说&#xff0c;定义 f(x,y,z)0f(x,y,z) 0 f(x,y,z)0 例如&#xff0c;定义三维空间中的点&#xff0c;满足&#…

上海亚商投顾:信创概念掀涨停潮

上海亚商投顾前言&#xff1a;无惧大盘大跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 市场情绪指数早盘低开低走&#xff0c;沪指一度跌超1%&#xff0c;失守3100点关口&#xff0c;创业板指盘中跌逾2%&#xff…

软件工程毕业设计 SSM计算机毕设项目合集【源码+论文】

文章目录前言 题目1 : 基于SSM的网上租车系统 <br /> 题目2 : 基于SSM的药品管理系统 <br /> 题目3 : 基于SSM的药源药品商城保健品购物网站 <br /> 题目4 : 基于SSM的疫情防控物业管理系统 <br /> 题目5 : 基于SSM的音乐网站 <br />前言 &…

如何通过更好的文档管理减轻小型企业的压力

如何通过更好的文档管理减轻小型企业的压力 企业如何处理企业文档在很大程度上体现了企业以目前的形式茁壮成长的能力以及在当今的市场中成长为成熟的、有竞争力的实体的能力。 具体来说&#xff0c;在小型企业中&#xff0c;许多员工都需要承担多种职责&#xff0c;每一分钟…

代码随想录58——单调栈:739每日温度、 496下一个更大元素I

文章目录1.739每日温度1.1.题目1.2.解答1.2.1.单调栈使用情况1.2.2.本题解答2. 496下一个更大元素I2.1.题目2.2.解答1.739每日温度 参考&#xff1a;代码随想录&#xff0c;739每日温度&#xff1b;力扣题目链接 1.1.题目 1.2.解答 1.2.1.单调栈使用情况 首先想到的当然是暴…

通过STM32Cube配置完成基于I2C协议的AHT20温湿度传感器的数据采集

文章目录前言一、I2C协议1、应用2、组成3、软件I2C和硬件I2C3.1软件I2C3.2硬件I2C二、通过硬件I2C协议采集AHT20的数据1、配置项目2、配置代码三、效果四、总结五、参考资料前言 硬件&#xff1a;stm32f103c8t6 核心板软件&#xff1a;STM32CubeMX 6.4.0软件&#xff1a;keil5…

java常见集合框架的区别

1.说说有哪些常见集合&#xff1f; 集合相关类和接口都在java.util中&#xff0c;主要分为3种&#xff1a;List&#xff08;列表&#xff09;、Map&#xff08;映射&#xff09;、Set(集)。 Java集合主要关系 其中Collection是集合List、Set的父接口&#xff0c;它主要有两个子…

澳大利亚博士后招聘|国立大学—太阳能电池方向

【国外博士后招聘-知识人网】澳大利亚国立大学博士后—太阳能电池方向 澳大利亚国立大学&#xff08;The Australian National University&#xff09;&#xff0c;简称ANU&#xff0c;始建于1946年&#xff0c;坐落于澳大利亚首都堪培拉&#xff0c;是公立研究型综合类大学&am…

Ernie-SimCSE对比学习在内容反作弊上应用

作者 | ANTI 导读 AI技术在不同行业和业务被广泛的应用&#xff0c;本文介绍了反作弊团队在与spammer对抗愈演愈烈的趋势下&#xff0c;不断探索前沿技术&#xff0c;将百度NLP预训练模型结合对比学习用于解决spam内容中知道提问群发推广作弊的技术方案。 本次分享&#xff0c;…

从单车智能到车路协同,均胜电子正在加快智能驾驶商业化进程

进入2022年&#xff0c;自动驾驶迈入了商业化的关键期&#xff0c;但市场分层也开始越来越明显。 一方面&#xff0c;L2级及以上智能辅助驾驶的搭载量在不断攀升&#xff0c;未来将成为量产车的标准配置。根据《高工智能汽车研究院》数据显示&#xff0c;今年1-9月前装标配搭载…

基于python的停车场管理系统的设计与实现/智能停车管理系统

摘要 车位信息是停车场供应用户必不可少的一个部分。在停车场发展的整个过程中&#xff0c;车位信息担负着最重要的角色。为满足如今日益复杂的管理需求&#xff0c;各类系统管理程序也在不断改进。本课题所设计的停车场管理系统&#xff0c;使用Django框架&#xff0c;Python语…