【2023】redis-stream配合spring的data-redis详细使用(包括广播和组接收)

目录

  • 一、简介
    • 1、介绍
    • 2、对比
  • 二、整合spring的data-redis实现
    • 1、使用依赖
    • 2、配置类
      • 2.1、配置`RedisTemplate bean`
      • 2.2、异常类
    • 3、实体类
      • 3.1、User
      • 3.2、Book
    • 4、发送消息
      • 4.1、RedisStreamUtil工具类
      • 4.2、通过延时队列线程池模拟发送消息
      • 4.3、通过http主动发送消息
    • 5、🌟消息接收
      • 5.1、不绑定消费组---可以实现广播📢效果
        • 方式1:主动读取
          • 测试日志
        • 🍉方式2:通过监听器监听是否有新消息
      • 5.2、指定消费组----实现一个组内只有一个成员可以接收到
        • 5.2.1、配置类
        • 5.2.2、监听器
          • 通过延时队列发送到消息---测试结果:
          • 通过http发送User到子类Book对象数据测试结果
  • 三、完整代码
  • 四、引用

背景:
使用该方式实现,主要是因为项目中有个地方刚好需要异步来实现,而项目又没有配置专业的消息中间件,并且使用的也不是太频繁,就觉得没必要专门安装一个MQ服务了,直接通过现有的redis的stream来实现异步消息接收直接具体的业务逻辑。

一、简介

1、介绍

Redis Stream(Redis Streams)是Redis 5.0版本引入的一种数据结构,用于处理时间序列数据、消息队列和日志流。它提供了高吞吐量、持久性、有序、可扩展的消息传递解决方案。Redis Stream 结构是对传统发布/订阅模式的增强,使你能够更灵活地处理数据流,并提供了以下主要特性:

  1. 多生产者和多消费者:多个生产者可以同时向 Stream 中写入消息,而多个消费者可以独立订阅并消费消息。每个消费者可以有不同的消费速率。

  2. 消费组:Redis Stream引入了消费者组的概念,多个消费者可以加入同一个消费者组并共同消费消息,这确保了消息在消费时不会被多次处理。

  3. 消费者阻塞:消费者可以使用 XREADGROUP 命令以阻塞方式获取消息,只有当有新消息到达时才会被推送给消费者。

  4. 消费者自动确认:Redis Stream 支持自动确认消息,消费者可以告诉 Redis 何时确认已经成功处理了一条消息。

  5. 多 Stream 支持:你可以创建多个 Stream 来存储不同种类的数据,并分别处理它们。

  6. 有序性:消息在 Stream 中按照消息的时间戳有序存储,因此你可以按照消息的顺序读取数据。

  7. 持久性存储:Redis Stream 使用内存数据结构,但也支持将数据异步保存到磁盘,以确保数据不会丢失。

2、对比

对比redis的其他几种实现方式来说功能更加全面,支持可持久化和通过ack确认的方式基本实现了消息丢失的问题,当然对比专业的消息队列中间件来说还是有些不足的。
需要看详细对比可以看 🔗redis队列对比 这篇文章
在这里插入图片描述

二、整合spring的data-redis实现

1、使用依赖

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>2.11.1</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies>

2、配置类

2.1、配置RedisTemplate bean

重点是下面这一句,不能用json的序列化类,否则会序列化失败
redisTemplate.setHashValueSerializer(RedisSerializer.string());

	@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(connectionFactory);redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new StringRedisSerializer());redisTemplate.setHashKeySerializer(new StringRedisSerializer());// 这个地方不可使用 json 序列化,如果使用的是ObjectRecord传输对象时,可能会有问题,会出现一个 java.lang.IllegalArgumentException: Value must not be null! 错误redisTemplate.setHashValueSerializer(RedisSerializer.string());return redisTemplate;}

2.2、异常类

@Slf4j
public class CustomErrorHandler implements ErrorHandler {@Overridepublic void handleError(Throwable throwable) {log.error("发生了异常",throwable);}
}

3、实体类

该地方使用了两个实体类,主要是用于测试,如果不是指定的同一个类型时,指定的是父类的类型,是否可以正常反序列化接收消息

3.1、User

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Book extends User{private String title;private String author;
}

3.2、Book

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User implements Serializable {private String name;private Integer age;
}

4、发送消息

发送消息主要是通过redisTemplate.opsForStream().add(record);进行发送到redis中(接收时会分两种方式接收,看后续!)

4.1、RedisStreamUtil工具类

用于实现消息发送、初始化组、key绑定组、清除消费了的消息等方法

  • 在第一次发送消息时需要先绑定接收的组和可key,否则在接收时会报不存在该组的异常
  • 发送消息后,需要把该条消费了的消息清除掉,否则会一直保持在stream中
@Component
@Slf4j
public class RedisStreamUtil {public static final String STREAM_KEY_001 = "stream-001";@Resourceprivate RedisTemplate<String,Object> redisTemplate;/*** 添加记录到流中* @param streamKey* @param t* @param <T>*/public <T> RecordId add(String streamKey,T t){ObjectRecord<String, T> record = StreamRecords.newRecord().in(streamKey)  //key.ofObject(t) //消息数据.withId(RecordId.autoGenerate());
//        发送消息RecordId recordId = redisTemplate.opsForStream().add(record);log.info("添加成功,返回的record-id[{}]",recordId);return recordId;}/*** 用来创建绑定流和组*/public void addGroup(String key, String groupName){redisTemplate.opsForStream().createGroup(key,groupName);}/*** 用来判断key是否存在*/public boolean hasKey(String key){if(key==null){return false;}else{return redisTemplate.hasKey(key);}}/*** 用来删除掉消费了的消息*/public void delField(String key,RecordId recordIds){redisTemplate.opsForStream().delete(key,recordIds);}/*** 用来初始化 实现绑定key和消费组*/public void initStream(String key, String group){//判断key是否存在,如果不存在则创建boolean hasKey = hasKey(key);if(!hasKey){Map<String,Object> map = new HashMap<>();map.put("key","value");RecordId recordId = add(key, map);addGroup(key,group);   //第一次初始化时需要把Stream和group绑定,delField(key,recordId);  //清除掉该条无用数据log.info("stream:{}-group:{} initialize success",key,group);}}public String getStreamKey001(){return STREAM_KEY_001;}
}

4.2、通过延时队列线程池模拟发送消息

  • 该方法里通过模拟延时5秒后,每隔3秒发送一条数据,发送10条后关闭线程池
/*** 在spring初始化时执行,定时发送消息到stream中,用于模拟发送消息*/
//@Component
public class StreamMessageRunner implements ApplicationRunner {@Resourceprivate RedisStreamUtil redisStreamUtil;@Overridepublic void run(ApplicationArguments args) throws Exception {ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);AtomicInteger integer = new AtomicInteger(0);//使用延时队列线程池模拟发送数据消息pool.scheduleAtFixedRate(()->{User zhangsan = new User("zhangsan"+integer.get(), 1 + integer.get());RecordId recordId = redisStreamUtil.add(redisStreamUtil.getStreamKey001(), zhangsan);integer.getAndIncrement();
//需要把消费了的消息清除掉,否则会一直保持在stream中,会被重复消费redisStreamUtil.delField(redisStreamUtil.getStreamKey001(),recordId);if (integer.get()>10){System.out.println("---------退出发送消息--------");pool.shutdown();}},5,3, TimeUnit.SECONDS);}
}

4.3、通过http主动发送消息

  • 通过分别发送父类和子类比对查看不同效果
@RestController
@RequestMapping("/index")
public class index {@Resourceprivate RedisStreamUtil redisStreamUtil;/*** 父类*/@GetMapping("/login")public String login(User user){RecordId recordId = redisStreamUtil.add(redisStreamUtil.getStreamKey001(), user);redisStreamUtil.delField(redisStreamUtil.getStreamKey001(),recordId);return "成功!";}/*** 子类*/@GetMapping("/login2")public String login(Book book){RecordId recordId = redisStreamUtil.add(redisStreamUtil.getStreamKey001(), book);redisStreamUtil.delField(redisStreamUtil.getStreamKey001(),recordId);return "成功!";}
}

5、🌟消息接收

5.1、不绑定消费组—可以实现广播📢效果

节点消费者不绑定消费组,直接和stream进行绑定,即可实现广播的效果,每次有消息发送到该指定节点的stream,都可以接收到。

如下图:有消息发送到redis Stream 里面绑定的A0到B2全部可以接收到这条消息
在这里插入图片描述

方式1:主动读取

通过redisTemplate.opsForStream().read()方法主动去stream中读取消息

/*** 独立消费者---可以读取到该key的全部消息*/
@Component
@Slf4j
public class XreadNonBlockConsumer01 implements InitializingBean, DisposableBean {private ThreadPoolExecutor threadPoolExecutor;@Resourceprivate RedisTemplate<String,Object> redisTemplate;private volatile boolean stop = false;/*** 初始化bean时执行,以轮询的方式主动去stream的指定key里读取消息* @throws Exception*/@Overridepublic void afterPropertiesSet() throws Exception {// 初始化线程池threadPoolExecutor = new ThreadPoolExecutor(3, 5, 0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(), r -> {Thread thread = new Thread(r);thread.setDaemon(true);thread.setName("xread-nonblock-01");return thread;});StreamReadOptions options = StreamReadOptions.empty()
//                如果没有数据,则阻塞1s,阻塞时间需要小于·spring.redis.timeout·.block(Duration.ofMillis(1000))
//                一直阻塞直到获取数据,可能会报超时异常
//                .block(Duration.ofMillis(0))
//                一次获取10条数据.count(10);StringBuilder readBuilder = new StringBuilder("0-0");threadPoolExecutor.execute(()->{while (!stop){//主动到redis的stream中去读取,options设置了每读取一次阻塞一秒List<ObjectRecord<String, User>> objectRecords = redisTemplate.opsForStream().read(User.class, options,StreamOffset.create(RedisStreamUtil.STREAM_KEY_001, ReadOffset.from(readBuilder.toString())));if (CollectionUtils.isEmpty(objectRecords)){log.warn("没有读取到数据");continue;}objectRecords.stream().forEach(objectRecord->{log.info("获取到的数据信息 id:[{}] book:[{}]", objectRecord.getId(), objectRecord.getValue());readBuilder.setLength(0);readBuilder.append(objectRecord.getId());});}});}/*** 在销毁bean时把线程池关闭* @throws Exception*/@Overridepublic void destroy() throws Exception {stop = true;threadPoolExecutor.shutdown();threadPoolExecutor.awaitTermination(3,TimeUnit.SECONDS);}}
测试日志

在这里插入图片描述

🍉方式2:通过监听器监听是否有新消息

具体代码和分组的是一样的,只不过不指定组而已,就合并在下面写了
主要通过StreamMessageListenerContainer这个监听器类实现。

主要通过下面这一句:

container.receive(StreamOffset.fromStart(RedisStreamUtil.STREAM_KEY_001), new MonitorStreamListener("独立消费", null, null));

5.2、指定消费组----实现一个组内只有一个成员可以接收到

进行分组之后,一个组内,只会有一个成员可以读到消息,具体如下图,当然在使用时也可以绑定多个组,每个组接收不听的消息。下面方式就相当于mq了,交换机,队列和路由键的关系
在这里插入图片描述

5.2.1、配置类

下面代码具体流程时先创建一个线程池;然后在配置消息监听容器,最后在把用于接收消息的监听器放入到监听容器中去,最后把这个侦听容器注入到bean去

@Configuration
public class RedisStreamConfiguration {@Resourceprivate RedisStreamUtil redisStreamUtil;@Resourceprivate RedisConnectionFactory redisConnectionFactory;@Bean(initMethod = "start",destroyMethod = "stop")public StreamMessageListenerContainer<String, ObjectRecord<String,User>> streamMessageListenerContainer(){AtomicInteger index = new AtomicInteger(1);
//        获取本机线程数int processors = Runtime.getRuntime().availableProcessors();ThreadPoolExecutor executor = new ThreadPoolExecutor(processors, processors, 0,TimeUnit.SECONDS,new ArrayBlockingQueue<>(50), (r) -> {Thread thread = new Thread(r);thread.setName("async-stream-consumer-" + index.getAndIncrement());thread.setDaemon(true);return thread;}, new ThreadPoolExecutor.CallerRunsPolicy());//        消息监听容器,不能在外部实现。创建后,StreamMessageListenerContainer可以订阅Redis流并使用传入的消息StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String,ObjectRecord<String,User>> options =StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()// 一次最多获取多少条消息.batchSize(10)// 运行 Stream 的 poll task.executor(executor)// Stream 中没有消息时,阻塞多长时间,需要比 `spring.redis.timeout` 的时间小.pollTimeout(Duration.ofSeconds(1))// ObjectRecord 时,将 对象的 filed 和 value 转换成一个 Map 比如:将Book对象转换成map
//                        .objectMapper(new ObjectHashMapper())// 获取消息的过程或获取到消息给具体的消息者处理的过程中,发生了异常的处理.errorHandler(new CustomErrorHandler())// 将发送到Stream中的Record转换成ObjectRecord,转换成具体的类型是这个地方指定的类型.targetType(User.class).build();StreamMessageListenerContainer<String, ObjectRecord<String, User>> container = StreamMessageListenerContainer.create(redisConnectionFactory, options);//        初始化-绑定key和消费组redisStreamUtil.initStream(RedisStreamUtil.STREAM_KEY_001,"group-a");//        不绑定消费组,独立消费container.receive(StreamOffset.fromStart(RedisStreamUtil.STREAM_KEY_001), new MonitorStreamListener("独立消费", null, null));// 消费组A,不自动ack// 从消费组中没有分配给消费者的消息开始消费
//        container.receive(Consumer.from("group-a","consumer-a"),
//                StreamOffset.create(RedisStreamUtil.STREAM_KEY_001, ReadOffset.lastConsumed()),new MonitorStreamListener("消费者组A","group-a", "consumer-a"));//        自动ackcontainer.receiveAutoAck(Consumer.from("group-a","consumer-b"),StreamOffset.create(RedisStreamUtil.STREAM_KEY_001, ReadOffset.lastConsumed()),new MonitorStreamListener("消费者组B","group-a", "consumer-b"));return container;}
}

重要代码解析:

  1. .targetType(User.class) :在配置监听容器时,用于指定类型,不指定时默认是string类型,如果你传入的不是string机需要指定;如果配置的是父类的,也可以接收子类的消息,进行转换。但如果是配置的Object类型,接收时就会为路径,不能正常得到传入的对象(不知道为什么,有研究懂的可以解答一下)
  2. redisStreamUtil.initStream(RedisStreamUtil.STREAM_KEY_001,"group-a"):在第一次生成时,需要把消费组绑定该stream的key,否则会报错,具体内部执行逻辑可以看initStream()方法(或者自己手动通过命令到redis去绑定:xgroup create stream-001 group-a $)stream-001(key) group-a(消费组)
  3. container.receive(StreamOffset.fromStart(RedisStreamUtil.STREAM_KEY_001), new MonitorStreamListener("独立消费", null, null)) :该句是不绑定消费组,也就是广播的方式监听该key中的所有消息(和上面的区别是,该方式是被动的监听消息
  4. 🌟container.receiveAutoAck(Consumer.from("group-a","consumer-b") ,StreamOffset.create(RedisStreamUtil.STREAM_KEY_001, ReadOffset.lastConsumed()),new MonitorStreamListener("消费者组B","group-a", "consumer-b"))就是通过该句代码实现分组监听消息的,绑定了消费组和消费者的名字,以及监听器类。然后使用的自动ack的方式回复stream确认接收到了消息(或者通过手动ack的方式返回stream接收到了消息,否则会重复发送),
5.2.2、监听器

用于接收消息然后实现具体业务代码

@Slf4j
public class MonitorStreamListener <T> implements StreamListener<String, ObjectRecord<String,T>> {/*** 消费者类型:独立消费、消费组消费*/private String consumerType;/*** 消费组*/private String group;/*** 消费组中的某个消费者*/private String consumerName;public MonitorStreamListener(String consumerType, String group, String consumerName) {this.consumerType = consumerType;this.group = group;this.consumerName = consumerName;}@Overridepublic void onMessage(ObjectRecord<String, T> message) {log.info("接受到来自redis的消息");String stream = message.getStream();RecordId id = message.getId();User value = (User) message.getValue();value.getName();//        执行具体的接收到消息的业务逻辑if (StringUtils.isEmpty(group)) {log.info("[{}]: 接收到一个消息 stream:[{}],id:[{}],value:[{}]", consumerType, stream, id, value);} else {log.info("[{}] group:[{}] consumerName:[{}] 接收到一个消息 stream:[{}],id:[{}],value:[{}]", consumerType,group, consumerName, stream, id, value);}// 当是消费组消费时,如果不是自动ack,则需要在这个地方手动ack
//        redisTemplate.opsForStream()
//                 .acknowledge("key","group","recordId");}
}
通过延时队列发送到消息—测试结果:

在这里插入图片描述

通过http发送User到子类Book对象数据测试结果

在这里插入图片描述
结果:也是可以正常接受到的
在这里插入图片描述

三、完整代码

🪟完整代码仓库地址

四、引用

https://juejin.cn/post/7029302992364896270#heading-0
https://juejin.cn/post/6844904125822435341?searchId=202310141054532F9807A1000F6680C0DF#heading-1

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

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

相关文章

UWB承启定位基站

UWB承启定位基站 随着我们使用UWB做超高精度的定位项目越来越多&#xff0c;我们发现之前的定位基站完全站在二维或三维的角度去设计还是存在对应的缺陷&#xff0c;这个时候需要在很短的距离内安装多一个基站&#xff0c;对于用户来说&#xff0c;会觉得设备变多了&#xff0…

多目标鳟海鞘算法(Multi-objective Salp Swarm Algorithm,MSSA)求解微电网优化MATLAB

一、微网系统运行优化模型 微电网优化模型介绍&#xff1a; 微电网多目标优化调度模型简介_IT猿手的博客-CSDN博客 参考文献&#xff1a; [1]李兴莘,张靖,何宇,等.基于改进粒子群算法的微电网多目标优化调度[J].电力科学与工程, 2021, 37(3):7 二、多目标鳟海鞘算法MSSA 多…

ABB机器人关于重定位移动讲解

关于机器人如何重定位移动&#xff0c;首先来看一下示教器上的重定位移动是在哪。 从图中所示的坐标位置和操纵杆方向得知&#xff0c;重定位的本质是绕X、Y、Z轴的旋转。那么实现跟摇杆一样的操作&#xff0c;就可以通过改变当前位置的欧拉角来实现&#xff0c;参考Rapid指令…

《Unity Shader 入门精要》笔记07

透明效果 为什么渲染顺序很重要Unity Shader的渲染顺序透明度测试透明度混合开启深度写入的半透明效果ShaderLab 的混合命令混合等式和参数混合操作常见的混合类型 双面渲染的透明效果透明度测试的双面渲染透明度混合的双面渲染 Unity中通常使用两种方法来实现透明效果&#xf…

openGauss学习笔记-99 openGauss 数据库管理-管理数据库安全-客户端接入认证之配置文件参考

文章目录 openGauss学习笔记-99 openGauss 数据库管理-管理数据库安全-客户端接入认证之配置文件参考99.1 参数说明99.2 认证方式 openGauss学习笔记-99 openGauss 数据库管理-管理数据库安全-客户端接入认证之配置文件参考 99.1 参数说明 表 1 参数说明 参数名称描述取值范…

swift ui 布局 ——Stack(HStack、VStack、ZStack)

一、HStack 水平布局 将其子视图排列在水平线上 import Foundation import SwiftUI struct MyView: View {var body: some View {HStack{Text("text")Image("yuyin").resizable().frame(width: 102,height: 80)}} } 默认子视图是水平中心对齐的,可添加al…

python+django高校教室资源预约管理系统lqg8u

技术栈 后端&#xff1a;pythondjango 前端&#xff1a;vueCSSJavaScriptjQueryelementui 开发语言&#xff1a;Python 框架&#xff1a;django/flask Python版本&#xff1a;python3.7.7 数据库&#xff1a;mysql 数据库工具&#xff1a;Navicat 开发软件&#xff1a;PyChar…

Go语言入门心法(五): 函数

一: go语言函数认知 Go语言入门心法(一): 基础语法 Go语言入门心法(二): 结构体 Go语言入门心法(三): 接口 忙着去耍帅,后期补上..........

集合的进阶

不可变集合 创建不可变的集合 在创建了之后集合的长度内容都不可以变化 静态集合的创建在list &#xff0c;set &#xff0c;map接口当中都可以获取不可变集合 方法名称说明static list of(E …elements)创建一个具有指定元素集合list集合对象staticlist of(E…elements)创…

掌握Python爬虫实现网站关键词扩展提升曝光率

目录 一、关键词优化的重要性 二、关键词优化的基本方法 1、选择与网站内容相关的关键词 2、控制关键词的密度和分布 3、关键词的层次布局 三、Python爬虫实现网站关键词扩展 1、确定目标网站 2、分析目标网站的HTML结构 3、编写Python爬虫代码 4、分析爬取到的关键词…

BSPHP 未授权访问 信息泄露

漏洞描述 BSPHP 存在未授权访问 泄露用户 IP 和 账户名信息 漏洞复现 访问url&#xff1a; 构造payload访问&#xff1a; /admin/index.php?madmin&clog&atable_json&jsonget&soso_ok1&tuser_login_log&page1&limit10&bsphptime16004073…

25栈和队列-理解栈和队列

目录 LeetCode之路——232. 用栈实现队列 分析&#xff1a; LeetCode之路——225. 用队列实现栈 分析&#xff1a; 栈&#xff08;Stack&#xff09;和队列&#xff08;Queue&#xff09;是两种基本的数据结构&#xff0c;它们在计算机科学中用于不同的目的。以下是它们的定…

【windows下docker安装rocketMQ】

namesrv和broker安装就不说了&#xff0c;见如下博客 https://blog.csdn.net/Wonderful1025/article/details/107244434/ 安装rocketMQ-console docker run -d -e "JAVA_OPTS-Drocketmq.config.namesrvAddr192.168.65.2:9876 -Drocketmq.config.isVIPChannelfalse"…

12.SpringBoot之RestTemplate的使用

SpringBoot之RestTemplate的使用 初识RestTemplate RestTemplate是Spring框架提供用于调用Rest接口的一个应用&#xff0c;它简化了与http服务通信方式。RestTemplate统一Restfull调用的标准&#xff0c;封装HTTP链接&#xff0c;只要需提供URL及返回值类型即可完成调用。相比…

Spark中的Driver、Executor、Stage、TaskSet、DAGScheduler等介绍

工作流程&#xff1a; Driver 创建 SparkSession 并将应用程序转化为执行计划&#xff0c;将作业划分为多个 Stage&#xff0c;并创建相应的 TaskSet。Driver 将 TaskSet 发送给 TaskScheduler 进行调度和执行。TaskScheduler 根据资源情况将任务分发给可用的 Executor 进程执…

DAE转换GLB格式

1、DAE模型介绍 DAEA&#xff08;Deep Attentive and Ensemble Autoencoder&#xff09;模型是一种用于无监督学习的深度学习模型&#xff0c;由华为公司提出。DAEA模型结合了自编码器和深度注意力机制&#xff0c;能够对高维数据进行降维和特征提取&#xff0c;并且在处理大规…

博图数值按照特定格式(“T000000”)转换成字符串

一、前言 1.string to dint物流输送线往往需要通过扫码器读取托盘条码&#xff0c;一维码或者二维码​。 读取的数据需要解析才能正常使用。两种方式读取的数据直接是字符串&#xff0c;但当设备与上位机通信时&#xff0c; 字符串数据量太大&#xff0c;故可以通过算法转换成…

Ceph分布式存储的简单介绍与Ceph集群的部署搭建

文章目录 1. 存储的概述1.1 单机存储设备1.1.1 DAS&#xff08;直接附加存储&#xff09;1.1.2 NAS&#xff08;网络附加存储&#xff09;1.1.3 SAN&#xff08;存储区域网络&#xff09; 1.2 单机存储的缺陷1.3 分布式存储&#xff08;软件定义的存储 SDS&#xff09;1.4 分布…

unity ugui text 超链接和下划线,支持部分富文本格式

unity版本&#xff1a;2021.3.6f1 局限性&#xff1a; 1.测试发现不能使用 size 富文本标签, 2.同一文本不能设置不同颜色的超链接文本 其它&#xff1a;代码中注释掉使用innerTextColor的地方&#xff0c;可以使用富文本设置超链接颜色&#xff0c; 但是下划线是文本本身颜色 …

windows部署django服务器

windows部署django服务器 1、安装IIS1.1 控制面板-----程序----程序和功能----启用或关闭windows功能1.2安装IIS服务器&#xff0c;完成后&#xff0c;重新进入&#xff0c;把CGI安装进系统 2、安装python与虚拟环境2.1 安装python2.2 安装virtualenv虚拟环境2.3 创建一个虚拟环…