技术研究:Redis 实现消息队列

news2024/9/20 20:34:47

综述

我们先看看消息队列的消息存取到底有哪些需求吧:

需求1:消息保序:由于消费者是异步处理消息,但是消费者需要按照生产者发送消息的顺序来处理消息,避免后发送的消息被先处理了。

需求2:重复消息处理:有时候可能因为网络堵塞出现消息重传的情况,消费者需要保证幂等性。换句话说,对于同一条消息,消费者收到一次的处理结果和收到多次的处理结果是一致的。

需求3:消息可靠性保证:有时候消费者在处理消息时可能出现因故障或宕机导致消息没有处理完成的情况,因此消息队列需要提供消息可靠性的保证。换句话说,当消费者重启之后,可以重新读取消息并再次进行处理。

消息队列的目的是在分布式的系统间进行通信,通信方式是通过网络传输的方式,通过网络传输就有消息丢失、重复发送消息、不同消息到达顺序混乱的可能。

所以消息队列应用的三大需求是:1、消息保序;2、重复消息处理;3、消息可靠性保证。

对应处理方案是:1、消息数据有序存取;2、消息数据具有全局唯一编号;3、消息数据在未消费完宕机恢复时继续消费,消费完成后被删除。

Redis如何实现MQ的需求?

总体来说,Redis提供了两种解决方案:一是基于List的消息队列解决方案,另一种则是基于Stream的消息队列解决方案。

基于List的MQ解决方案

首先,对于需求1-消息保序,List是按照先进先出的顺序对数据进行存取的,因此使用List作为消息队列保存消息可以满足需求1。具体实现就是LPUSH+RPOP/BRPOP。

潜在风险点:即使没有新消息写入List,消费者也需要不停地调用RPOP命令,这就会导致消费者程序的CPU一直消耗在执行RPOP命令上,带来不必要的性能损失。因此,Redis提供了BRPOP命令,提供阻塞式读取,即在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。

其次,对于需求2-重复消息处理,List本身是不会为每个消息生成ID号的,所以,消息的全局唯一ID号就需要生产者程序在发送消息前自动生成,生成之后,在LPUSH时需要把这个全局唯一ID包含进去。

例如:将一条全局 ID 为 101030001、库存量为 5 的消息插入Redis消息队列

LPUSH mq "101030001:stock:5"
(integer) 1

最后,对于需求3-消息可靠性保证,List本身在读取一条消息后就不会再留存这条消息了,所以为了留存消息,List提供了BRPOPLPUSH命令,即让消费者程序从一个List中读取消息,同时再把这个消息插入到另一个List(可以理解为备份List)留存。这样一来,如果消费者程序读了消息但没能正常处理,等它重启后,就可以从备份 List 中重新读取消息并进行处理了。

综上所述,基于List可以满足对MQ的三大需求,但是,使用List还是存在一个问题:生产者消息发送很快,但是消费者处理消息的速度很慢,这就可能会导致List中的消息越积越多,给Redis的内存带来很大的压力。在Redis 5.0开始,提供了Stream数据类型,它支持多个消费者程序组成一个消费组,一起分担消息处理压力。

基于Stream的MQ解决方案

Streams 是 Redis 专门为消息队列设计的数据类型,它提供了丰富的消息队列操作命令。

(1)XADD

插入消息,保证有序,可以自动生成全局唯一ID

(2)XREAD

读取消息,可以按ID读取数据

(3)XREADGROUP

按消费组形式读取消息

(4)XPENDING + XACK

XPENDING用来查询每个消费组内所有消费者已读取但尚未确认的消息

XACK用来向消息队列确认消息处理已完成

下面一一讲解各个核心命令操作:

XADD

XADD 命令可以往消息队列中插入新消息,消息的格式是键 - 值对形式。对于插入的每一条消息,Stream 可以自动为其生成一个全局唯一的 ID

例如:往一个名为mqstream的队列中插入一条消息,key=repo, value=5,中间的*表示让Redis为插入的数据自动生成一个全局唯一的ID号,这里是1599203861727-0,它的格式是当前服务器时间(精确到毫秒)+序号。

XADD mqstream * repo 5
"1599203861727-0"

XREAD

XREAD 在读取消息时,可以指定一个消息 ID,并从这个消息 ID 的下一条消息开始进行读取。

例如:从 ID 号为 1599203861727-0 的消息开始,读取后续的所有消息(示例中一共 3 条),下面命令中的block配置项代表XREAD的阻塞时间,即当消息队列中没有消息时,XREAD就会阻塞指定的毫秒数。

XREAD BLOCK 100 STREAMS  mqstream 1599203861727-0
1) 1) "mqstream"
   2) 1) 1) "1599274912765-0"
         2) 1) "repo"
            2) "3"
      2) 1) "1599274925823-0"
         2) 1) "repo"
            2) "2"
      3) 1) "1599274927910-0"
         2) 1) "repo"
            2) "1"

此外,也可以直接在XREAD后跟"$"符号,代表读取最新的消息。下面命令中的 XREAD 执行后,消息队列 mqstream 中一直没有消息,所以,XREAD 在 1 秒后返回了空值(nil)。

XREAD block 1000 streams mqstream $
(nil)
(1.00s)

XREADGROUP

Stream 使用 XGROUP 创建消费组,创建消费组之后,Stream 可以使用 XREADGROUP 命令让消费组内的消费者读取消息。

例如:使用下面的XGROUP命令创建一个消费组group1,这个消费组的消息队列是mqstream。

XGROUP create mqstream group1 0
OK

然后,再用下面的XREADGROUP命令来让消费组group1中的消费者consumer1从mqstream消息队列中读取所有消息。其中,命令最后的参数“>”,表示从第一条尚未被消费的消息开始读取。

XREADGROUP group group1 consumer1 streams mqstream >
1) 1) "mqstream"
   2) 1) 1) "1599203861727-0"
         2) 1) "repo"
            2) "5"
      2) 1) "1599274912765-0"
         2) 1) "repo"
            2) "3"
      3) 1) "1599274925823-0"
         2) 1) "repo"
            2) "2"
      4) 1) "1599274927910-0"
         2) 1) "repo"
            2) "1"

注意事项:当消息队列(上面例子是mqstream)中的消息被消息组中的其中一个消费者读取消费处理之后,就不能再被消费组中的其他消费者读取了,这时其他消费者再执行相同的XREADGROUP命令时,读到的就是空值。

XREADGROUP group group1 consumer2  streams mqstream 0
1) 1) "mqstream"
   2) (empty list or set)

此外,使用消费组的目的是让组内的多个消费者共同分担读取消息,所以,我们通常会让每个消费者读取部分消息,从而实现消息读取负载在多个消费者间是均衡分布的,比如我们可以使用count N命令来让各个消费者各自读取N条消息。

XREADGROUP group group2 consumer1 count 1 streams mqstream >
1) 1) "mqstream"
   2) 1) 1) "1599203861727-0"
         2) 1) "repo"
            2) "5"

XREADGROUP group group2 consumer2 count 1 streams mqstream >
1) 1) "mqstream"
   2) 1) 1) "1599274912765-0"
         2) 1) "repo"
            2) "3"

XREADGROUP group group2 consumer3 count 1 streams mqstream >
1) 1) "mqstream"
   2) 1) 1) "1599274925823-0"
         2) 1) "repo"
            2) "2"

使用扩展:如果一个生产者发送给消息队列的消息,需要被多个消费者进行读取和处理(例如,一个消息是一条从业务系统采集的数据,既要被消费者 1 读取进行实时计算,也要被消费者 2 读取并留存到分布式文件系统 HDFS 中,以便后续进行历史查询)。这时我们可以基于Stream类型创建多个消费者组,实现同时消费生产者的数据。每个消费者组内可以再挂多个消费者分担读取消息进行消费,消费完成后,各自向Redis发送XACK,标记自己的消费组已经消费到了哪个位置,而且消费组之间互不影响。另外,Redis 基于字典和链表数据结构,实现了发布和订阅功能,这个功能可以实现一个消息被多个消费者消费使用,也可以满足上面问题中的场景需求。

XPENDING + XACK

Stream 会自动使用内部队列(也称为 PENDING List)留存消费组里每个消费者读取的消息,直到消费者使用 XACK 命令通知 Stream “消息已经处理完成”,这时 Stream 就会抢这个消息移除掉。

如果消费者没有成功处理消息,它就不会给 Stream 发送 XACK 命令,消息仍然会留存。此时,消费者可以在重启后,用 XPENDING 命令查看已读取、但尚未确认处理完成的消息。

例如:查看消费组group2中各个消费者已读取但尚未确认的消息数量,其中 XPENDING 返回结果的第二、三行分别表示 group2 中所有消费者读取的消息最小 ID 和最大 ID。

XPENDING mqstream group2
1) (integer) 3
2) "1599203861727-0"
3) "1599274925823-0"
4) 1) 1) "consumer1"
      2) "1"
   2) 1) "consumer2"
      2) "1"
   3) 1) "consumer3"
      2) "1"

当某个消费者例如group消费了某个消息之后,就可以用XACK命令通知Stream从PENDING LIST中移除这条消息。

XACK mqstream group2 1599274912765-0
(integer) 1
 // 下面的命令可以查看某个消费者具体读取了哪些数据
XPENDING mqstream group2 - + 10 consumer2
(empty list or set)

List 与 Stream 方案的对比

一图胜千言:

实际使用

// 生产者
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Map;

import static com.nageoffer.shortlink.project.common.constant.RedisKeyConstant.SHORT_LINK_STATS_STREAM_TOPIC_KEY;

/**
 * 短链接监控状态保存消息队列生产者
 */
@Component
@RequiredArgsConstructor
public class ShortLinkStatsSaveProducer {

    private final StringRedisTemplate stringRedisTemplate;

    /**
     * 发送延迟消费短链接统计
     */
    public void send(Map<String, String> producerMap) {
        stringRedisTemplate.opsForStream().add(SHORT_LINK_STATS_STREAM_TOPIC_KEY, producerMap);
    }
}


// 消费者
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.Week;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.nageoffer.shortlink.project.common.convention.exception.ServiceException;
import com.nageoffer.shortlink.project.dao.entity.LinkAccessLogsDO;
import com.nageoffer.shortlink.project.dao.entity.LinkAccessStatsDO;
import com.nageoffer.shortlink.project.dao.entity.LinkBrowserStatsDO;
import com.nageoffer.shortlink.project.dao.entity.LinkDeviceStatsDO;
import com.nageoffer.shortlink.project.dao.entity.LinkLocaleStatsDO;
import com.nageoffer.shortlink.project.dao.entity.LinkNetworkStatsDO;
import com.nageoffer.shortlink.project.dao.entity.LinkOsStatsDO;
import com.nageoffer.shortlink.project.dao.entity.LinkStatsTodayDO;
import com.nageoffer.shortlink.project.dao.entity.ShortLinkGotoDO;
import com.nageoffer.shortlink.project.dao.mapper.LinkAccessLogsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkAccessStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkBrowserStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkDeviceStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkLocaleStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkNetworkStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkOsStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkStatsTodayMapper;
import com.nageoffer.shortlink.project.dao.mapper.ShortLinkGotoMapper;
import com.nageoffer.shortlink.project.dao.mapper.ShortLinkMapper;
import com.nageoffer.shortlink.project.dto.biz.ShortLinkStatsRecordDTO;
import com.nageoffer.shortlink.project.mq.idempotent.MessageQueueIdempotentHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.stream.StreamListener;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import static com.nageoffer.shortlink.project.common.constant.RedisKeyConstant.LOCK_GID_UPDATE_KEY;
import static com.nageoffer.shortlink.project.common.constant.ShortLinkConstant.AMAP_REMOTE_URL;

/**
 * 短链接监控状态保存消息队列消费者
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class ShortLinkStatsSaveConsumer implements StreamListener<String, MapRecord<String, String, String>> {

    private final ShortLinkMapper shortLinkMapper;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final RedissonClient redissonClient;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final LinkStatsTodayMapper linkStatsTodayMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final MessageQueueIdempotentHandler messageQueueIdempotentHandler;

    @Value("${short-link.stats.locale.amap-key}")
    private String statsLocaleAmapKey;

    @Override
    public void onMessage(MapRecord<String, String, String> message) {
        String stream = message.getStream();
        RecordId id = message.getId();
        if (!messageQueueIdempotentHandler.isMessageProcessed(id.toString())) {
            // 判断当前的这个消息流程是否执行完成
            if (messageQueueIdempotentHandler.isAccomplish(id.toString())) {
                return;
            }
            throw new ServiceException("消息未完成流程,需要消息队列重试");
        }
        try {
            Map<String, String> producerMap = message.getValue();
            ShortLinkStatsRecordDTO statsRecord = JSON.parseObject(producerMap.get("statsRecord"), ShortLinkStatsRecordDTO.class);
            actualSaveShortLinkStats(statsRecord);
            stringRedisTemplate.opsForStream().delete(Objects.requireNonNull(stream), id.getValue());
        } catch (Throwable ex) {
            // 某某某情况宕机了
            messageQueueIdempotentHandler.delMessageProcessed(id.toString());
            log.error("记录短链接监控消费异常", ex);
            throw ex;
        }
        messageQueueIdempotentHandler.setAccomplish(id.toString());
    }

    public void actualSaveShortLinkStats(ShortLinkStatsRecordDTO statsRecord) {
        String fullShortUrl = statsRecord.getFullShortUrl();
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, fullShortUrl));
        RLock rLock = readWriteLock.readLock();
        rLock.lock();
        try {
            LambdaQueryWrapper<ShortLinkGotoDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(queryWrapper);
            String gid = shortLinkGotoDO.getGid();
            int hour = DateUtil.hour(new Date(), true);
            Week week = DateUtil.dayOfWeekEnum(new Date());
            int weekValue = week.getIso8601Value();
            LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                    .pv(1)
                    .uv(statsRecord.getUvFirstFlag() ? 1 : 0)
                    .uip(statsRecord.getUipFirstFlag() ? 1 : 0)
                    .hour(hour)
                    .weekday(weekValue)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkAccessStatsMapper.shortLinkStats(linkAccessStatsDO);
            Map<String, Object> localeParamMap = new HashMap<>();
            localeParamMap.put("key", statsLocaleAmapKey);
            localeParamMap.put("ip", statsRecord.getRemoteAddr());
            String localeResultStr = HttpUtil.get(AMAP_REMOTE_URL, localeParamMap);
            JSONObject localeResultObj = JSON.parseObject(localeResultStr);
            String infoCode = localeResultObj.getString("infocode");
            String actualProvince = "未知";
            String actualCity = "未知";
            if (StrUtil.isNotBlank(infoCode) && StrUtil.equals(infoCode, "10000")) {
                String province = localeResultObj.getString("province");
                boolean unknownFlag = StrUtil.equals(province, "[]");
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .province(actualProvince = unknownFlag ? actualProvince : province)
                        .city(actualCity = unknownFlag ? actualCity : localeResultObj.getString("city"))
                        .adcode(unknownFlag ? "未知" : localeResultObj.getString("adcode"))
                        .cnt(1)
                        .fullShortUrl(fullShortUrl)
                        .country("中国")
                        .date(new Date())
                        .build();
                linkLocaleStatsMapper.shortLinkLocaleState(linkLocaleStatsDO);
            }
            LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                    .os(statsRecord.getOs())
                    .cnt(1)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkOsStatsMapper.shortLinkOsState(linkOsStatsDO);
            LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                    .browser(statsRecord.getBrowser())
                    .cnt(1)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkBrowserStatsMapper.shortLinkBrowserState(linkBrowserStatsDO);
            LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                    .device(statsRecord.getDevice())
                    .cnt(1)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkDeviceStatsMapper.shortLinkDeviceState(linkDeviceStatsDO);
            LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                    .network(statsRecord.getNetwork())
                    .cnt(1)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStatsDO);
            LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                    .user(statsRecord.getUv())
                    .ip(statsRecord.getRemoteAddr())
                    .browser(statsRecord.getBrowser())
                    .os(statsRecord.getOs())
                    .network(statsRecord.getNetwork())
                    .device(statsRecord.getDevice())
                    .locale(StrUtil.join("-", "中国", actualProvince, actualCity))
                    .fullShortUrl(fullShortUrl)
                    .build();
            linkAccessLogsMapper.insert(linkAccessLogsDO);
            shortLinkMapper.incrementStats(gid, fullShortUrl, 1, statsRecord.getUvFirstFlag() ? 1 : 0, statsRecord.getUipFirstFlag() ? 1 : 0);
            LinkStatsTodayDO linkStatsTodayDO = LinkStatsTodayDO.builder()
                    .todayPv(1)
                    .todayUv(statsRecord.getUvFirstFlag() ? 1 : 0)
                    .todayUip(statsRecord.getUipFirstFlag() ? 1 : 0)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkStatsTodayMapper.shortLinkTodayState(linkStatsTodayDO);
        } catch (Throwable ex) {
            log.error("短链接访问量统计异常", ex);
        } finally {
            rLock.unlock();
        }
    }
}

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

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

相关文章

【区块链+金融服务】河北股权交易所综合金融服务平台 | FISCO BCOS应用案例

区域性股权市场是我国资本市场的重要组成部分&#xff0c;是多层次资本市场体系的基石。河北股权交易所&#xff08;简称&#xff1a;河交所&#xff09; 作为河北省唯一一家区域性股权市场运营机构&#xff0c;打造河北股权交易所综合金融服务平台&#xff0c;将区块链技术与区…

信号与系统MATLAB实验:连续信号的采样与恢复

一、实验内容 &#xff08;1&#xff09;修改示例中的门信号宽度、采样周期等参数&#xff0c;重新运行程序&#xff0c;观察得到的采样信号时域和频域特性&#xff0c;以及重构信号与误差信号的变化。 示例1&#xff1a;选取门信号f(t) g2(t)为被采样信号。利用MATLAB实现对…

搭建超好用的个人网盘

目录 前言filebrowser下载地址介绍优点缺点部署效果 cloudreve官网介绍优点缺点部署效果 总结 前言 目前我使用过的文件存储管理软件&#xff0c;包括fastdfs、minio、filebrowser、cloudreve&#xff0c;这4款软件&#xff0c;我自己亲自搭建且都使用过&#xff0c;fastdfs很…

Datawhale X 魔搭 AI夏令营第四期 AIGC方向 task02笔记

AI工具使用 1. baseline 代码2. 使用通义千问理解代码2.1 工作流程2.2 逐行释意 3. 使用通义千问生成 Prompt3.1 生成的 Prompt3.1 根据 Prompt 生成的图片 1. baseline 代码 !pip install simple-aesthetics-predictor!pip install -v -e data-juicer!pip uninstall pytorch-…

docker的基本管理和应用

1、概念 docker是一个开源的应用容器引擎&#xff0c;基于go语言开发的。 docker是运行在linux的容器化工具&#xff0c;可以理解为轻量级的虚拟机。 可以在任何主机上轻松创建的一个轻量级、可移植的、自给自足的容器 2、设计的理念 鲸鱼——宿主机 集装箱——独立运行的…

07:【stm32】中断一:NVIC的配置

中断 1、中断的简介1.1、什么是中断1.2、为什么需要中断 2、中断的优先级2.1、中断优先级的表示方法 3、NVIC3.1、什么的NVIC3.2、NVIC的内部结构3.3、中断向量表3.4、程序实现①开启中断源②配置NVIC③中断响应函数 1、中断的简介 1.1、什么是中断 正在进行的事务被突发事件打…

1688商品详情API返回值中的供应商信息

在使用1688&#xff08;阿里巴巴中国站&#xff09;的商品详情API时&#xff0c;API的返回值中通常会包含丰富的产品信息&#xff0c;包括供应商&#xff08;卖家&#xff09;的信息。不过&#xff0c;具体的返回值内容可能会根据API的版本、调用参数以及API的更新情况有所不同…

什么是国际网络组网?

国际网络组网是指通过互联网技术将全球各地的不同网络相互连接&#xff0c;以实现信息交换与资源共享的过程。在这一过程中&#xff0c;涉及到数据传输、协议转换、跨网络通信等多个技术领域&#xff0c;旨在实现全球信息的无缝互联互通。国际网络组网的主要目标是扩大网络覆盖…

半导体RFID识别系统134.2K低频读写器|读写头JY-V610之SECS协议通信说明

什么是SECS协议&#xff1f; SECS&#xff08;Semiconductor Equipment Communication Standard&#xff09;协议是半导体设备通讯标准&#xff0c;用来统一各个生产设备之间以及生产设备和控制设备之间的通讯&#xff0c;由SEMI&#xff08;Semiconductor Equipment and Mate…

手搓滑动窗口

前言&#xff1a;好久没写滑动窗口&#xff0c;导致一些边界问题处理不好&#xff0c;back和top的初始值都搞不好 #include<bits/stdc.h> using namespace std;const int N (int)1e65; int n,m; int a[N],b[N]; int back,top;int main(){cin >> n >> m;for(…

【前端设计方案】H5 图片懒加载 SDK

实现思路 定义<img srcloading.png data-srcxxx.png/>页面滚动&#xff0c;图片露出时&#xff0c;将 data-src 赋值给 src 注意事项&#xff1a;滚动要节流 技术要点 获取图片的位置 elem.getBoundingClientRect() 图片 top < window.innerHeight 时&#xff0c;图片…

【学习笔记】爱立信SPO 1400 CRAFT软件基础知识9——Bridge(网桥)显示参数

一、前期准备 条件1.确认已正确使用爱立信SPO 1400 CRAFT软件通过网络登录设备&#xff08;以下简称NE&#xff09; 具体登录教程参考&#xff1a;使用爱立信SPO 1400 CRAFT软件通过网络登录设备的详细过程 二、学习内容&#xff1a; 提示&#xff1a;学习爱立信SPO 1400 CRA…

阿里员工:33岁,房贷还剩223万,每月还1.5W,失业中

中年失业 中年失业&#xff0c;真的很难。 虽然人到中年&#xff0c;一般多少都会有些储蓄&#xff0c;但也意味着会有更多的支出。 最近&#xff0c;一位阿里员工&#xff08;这会可能是前阿里员工了&#xff09;在社区分享到自己的经历。 贴主 33 岁&#xff0c;作为已结婚有…

搬瓦工日本软银线路VPS测评

搬瓦工日本VPS支持softbank/软银&#xff0c;Japan: Osaka (Softbank) &#xff0c;网络在2.5Gbps-10Gbps之间&#xff0c;底层为KVM虚拟、纯SSD阵列、支持在多机房之间切换。搬瓦工软银来国内的网络情况怎么样&#xff1f;测评数据大致如下&#xff1a; CPU具体型号不知道&…

深入了解指针(6)

文章目录 1.函数指针数组2.转移表3.回调函数 1.函数指针数组 存放函数指针的数组 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> int add(int x, int y) {return x y; } int sub(int x, int y) {return x - y; }int main() {int (*p1)(int x, int y) add;int (…

基于python的百度迁徙迁入、迁出数据分析(八)

副标题&#xff1a;从百度迁徙数据看——重大公共卫生事件的影响 先来回顾一下&#xff0c;迁徙规模指数定义&#xff1a;反映迁入或迁出人口规模&#xff0c;城市间可横向对比。 2019年—2022年的部分春运数据已经不可查&#xff0c;用的环哥的数据&#xff0c;可参考环哥的…

LVS原理及相关配置

1. 描述以及工作原理 1. 什么是 LVS linux virtural server 的简称&#xff0c;也就是 linxu 虚拟机服务器&#xff0c;这是一个 由章文嵩博士发起的开源项目&#xff0c;官网是 http://www.linuxvirtualserver.org,现在 lvs 已经是 linux 内核标 准的一部分&#xff0c;使用…

用python的manim库实现表格格式操作【table 下】

1.Table 是 Manim 中用于创建一个包含文本或其他 数学符号的表格的类 Table 是 Manim 中用于创建一个包含文本或其他 数学符号的表格的类它能够帮助你在场景中清晰地展示数据或信息。 参数解释 table: 一个二维数组或列表&#xff0c;表示表格中的内容。每个子列表代表表格的…

zigbee笔记:十一、设备网络类型展示与按键实验

一、设备网络类型展示 利用开发板上的三个LED灯来显示设备当前的网络类型&#xff08;协调器、路由器、终端&#xff09;。 1、添加LED灯的初始化代码 1&#xff09;开发板LED原理图 2&#xff09;在模板工程的...Projects\zstack\Samples\smartHomeApp\Source目录下新建两个文…

【TabBar嵌套Navigation案例-发现页面-按钮上的图片旋转 Objective-C语言】

一、接下来,我们来做这个,点击以后,让它出一个蓝色的View 1.就是我们示例程序的这种效果, 一点击,让这个按钮旋转,然后呢,再让它出来一个蓝色的View, 首先,我们要去监听它的点击事件,这是第一,我点击以后,我要做一些什么样的操作,要有点击事件, 所以呢,我要把…