【2023】Redis实现消息队列的方式汇总以及代码实现

news2024/12/23 15:38:34

Redis实现消息队列的方式汇总以及代码实现

  • 前言
  • 开始前准备
      • 1、添加依赖
      • 2、添加配置的Bean
  • 具体实现
    • 一、从最简单的开始:List 队列
        • 代码实现
    • 二、发布订阅模式:Pub/Sub
      • 1、使用RedisMessageListenerContainer实现订阅
      • 2、还可以使用redisTemplate实现订阅
    • 三、、 趋于成熟的队列:Stream
      • 具体java代码实现:
  • 总结

前言

经常听到很多人讨论,关于「把 Redis 当作队列来用是否合适」的问题。

有些人表示赞成,他们认为 Redis 很轻量,用作队列很方便。也些人则反对,认为 Redis 会「丢」数据,最好还是用「专业」的队列中间件更稳妥。

这篇文章就聊一聊把 Redis 当作队列,究竟是否合适这个问题。我们会从简单到复杂,一步步带你梳理其中的细节,把常用的实现方式展现一遍。

开始前准备

1、添加依赖

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
                <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
                <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
                <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.4.0</version>
        </dependency>

2、添加配置的Bean

避免不方便用软件查看存储的数据

    /**
     * redisTemplate 序列化使用的jdkSerializeable, 存储二进制字节码, 所以自定义序列化类
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 使用Jackson2JsonRedisSerialize 替换默认序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 设置value的序列化规则和 key的序列化规则
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //jackson2JsonRedisSerializer就是JSON序列号规则,
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

具体实现

一、从最简单的开始:List 队列

首先,我们先从最简单的场景开始讲起。

如果你的业务需求足够简单,想把 Redis 当作队列来使用,肯定最先想到的就是使用 List 这个数据类型。

因为 List 底层的实现就是一个「链表」,在头部和尾部操作元素,时间复杂度都是 O(1),这意味着它非常符合消息队列的模型。

如果把 List 当作队列,你可以这么来用。

代码实现

生产者端读取:

@RestController
@RequestMapping("/redis01")
public class RedisTest1 {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;


	//LPUSH 发布消息
    @GetMapping("/set")
    public void set(String code){
        redisTemplate.opsForList().leftPush("code",code);
    }
    // RPOP 拉取消息
    @GetMapping("/get1")
    public String get1(String key){
        Object code =  redisTemplate.opsForList().rightPop(key);
        if (code!=null){
            return code.toString();
        }
        return "redis中没数据!";
    }

实现模型:
这个模型也非常简单容易理解。 在这里插入图片描述

但这里有个小问题,当队列中已经没有消息了,消费者在执行 RPOP 时,会返回 NULL。
在这里插入图片描述
一般在编写消费者时,会采用一个死循环,这个实现方式就是不断去队列中拉取数据。

    @GetMapping("/get2")
    public String get2(String key) throws InterruptedException {
        while (true){
            Object code = redisTemplate.opsForList().rightPop(key);
            System.out.println(code);
 //            读取到消息,退出,没读到继续循环
            if (code!=null){
                return code.toString();
            }
        }
    }

如果此时队列为空,那消费者依旧会频繁拉取消息,这会造成「CPU 空转」,不仅浪费 CPU 资源,还会对 Redis 造成压力。

怎么解决这个问题呢?

也很简单,当队列为空时,我们可以「休眠」一会,再去尝试拉取消息。代码可以修改成这样:

    @GetMapping("/get2")
    public String get2(String key) throws InterruptedException {
        while (true){
            Object code = redisTemplate.opsForList().rightPop(key);
            System.out.println(code);
 //            读取到消息,退出,没读到继续循环
            if (code!=null){
                return code.toString();
            }
            Thread.sleep(2000);
        }
    }

这就解决了 CPU 空转问题。

这个问题虽然解决了,但又带来另外一个问题:当消费者在休眠等待时,有新消息来了,那消费者处理新消息就会存在「延迟」。

假设设置的休眠时间是 2s,那新消息最多存在 2s 的延迟。

要想缩短这个延迟,只能减小休眠的时间。但休眠时间越小,又有可能引发 CPU 空转问题。

鱼和熊掌不可兼得。

那如何做,既能及时处理新消息,还能避免 CPU 空转呢?

Redis 是否存在这样一种机制:如果队列为空,消费者在拉取消息时就「阻塞等待」,一旦有新消息过来,就通知我的消费者立即处理新消息呢?

幸运的是,Redis 确实提供了「阻塞式」拉取消息的命令:BRPOP / BLPOP,这里的 B 指的是阻塞(Block)。
在这里插入图片描述
在java中也已经封装好了,调用pop方法时,直接设置一个过期时间就行

    @GetMapping("/get3")
    public String get3(String key) throws InterruptedException {
        Object code = redisTemplate.opsForList().rightPop(key,0, TimeUnit.SECONDS);
        if (code==null){
            return "数据读取超时!";
        }
        return code.toString();
    }

使用 BRPOP 这种阻塞式方式拉取消息时,还支持传入一个「超时时间」,如果设置为 0,则表示不设置超时,直到有新消息才返回,否则会在指定的超时时间后返回 NULL。

这个方案不错,既兼顾了效率,还避免了 CPU 空转问题,一举两得。

注意:如果设置的超时时间太长,这个连接太久没有活跃过,可能会被 Redis Server 判定为无效连接,之后 Redis Server
会强制把这个客户端踢下线。所以,采用这种方案,客户端要有重连机制。

解决了消息处理不及时的问题,你可以再思考一下,这种队列模型,有什么缺点?

我们一起来分析一下:

  1. 不支持重复消费:消费者拉取消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费,即不支持多个消费者消费同一批数据
  2. 消息丢失:消费者拉取到消息后,如果发生异常宕机,那这条消息就丢失了
    第一个问题是功能上的,使用 List 做消息队列,它仅仅支持最简单的,一组生产者对应一组消费者,不能满足多组生产者和消费者的业务场景。

第二个问题就比较棘手了,因为从 List 中 POP 一条消息出来后,这条消息就会立即从链表中删除了。也就是说,无论消费者是否处理成功,这条消息都没办法再次消费了。

这也意味着,如果消费者在处理消息时异常宕机,那这条消息就相当于丢失了。

针对这 2 个问题怎么解决呢?我们一个个来看。

二、发布订阅模式:Pub/Sub

从名字就能看出来,这个模块是 Redis 专门是针对「发布/订阅」这种队列模型设计的。

它正好可以解决前面提到的第一个问题:重复消费。

即多组生产者、消费者的场景,我们来看它是如何做的。

Redis 提供了 PUBLISH / SUBSCRIBE 命令,来完成发布、订阅的操作。
在这里插入图片描述
依赖继续用前面的就行

1、使用RedisMessageListenerContainer实现订阅

  • 通过实现MessageListener接口来处理接收到的消息。这允许您在Spring应用程序中以更高级的方式处理消息,例如使用依赖注入和其他Spring功能。它还支持基于注解的消息监听器,使消息处理更加简洁和灵活。
  • 该方式是Spring Data Redis库提供的方法,用于在Spring应用程序中使用Redis的发布订阅功能。它需要创建一个MessageListenerContainer对象,并通过调用addMessageListener方法来添加消息监听器。
  • 添加监控器,用于监听通道
    为了便于更加直观的对比测试,我添加了两个
/**
 * @author zhengfuping
 * @version 1.0
 * @description: TODO  配置监控器
 * @date 2023/7/28 17:10
 */
@Component
public class RedisMessaeListener1 implements MessageListener {
    @Override
    public void onMessage(Message message, byte[] pattern) {

        String channel = new String(message.getChannel());
        String body = new String(message.getBody());
        System.out.println("监听器1号:消息: " + body + " 通道QQ: " + channel);
    }
}

/*########################*/

/**
 * @author zhengfuping
 * @version 1.0
 * @description: TODO 配置监控器1
 * @date 2023/8/2 11:24
 */
@Component
public class RedisMessaeListener2 implements MessageListener {
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(message.getChannel());
        String body = new String(message.getBody());
        System.out.println("监听器2号:消息: " + body + " 通道QQ: " + channel);
    }
}

  • 配置订阅可以单和多个
    用于绑定主题(通道)和监听器,该发送是
/**
 * @author zhengfuping
 * @version 1.0
 * @description: TODO 使用RedisMessageListenerContainer直接注入到bean进行监听
 * @date 2023/7/28 15:43
 */
@Configuration
public class RedisPubSubExample {

    @Autowired
    private  RedisMessaeListener1 redisMessaeListener1;
    @Autowired
    private  RedisMessaeListener2 redisMessaeListener2;

    /**
     * 订阅三个频道
     * @author zhengfuping
     * @date 2023/8/2 11:19
     * @param redisConnectionFactory redis线程工厂
     * @return RedisMessageListenerContainer
     */
//    @Bean
    public RedisMessageListenerContainer subscribeToChannel(RedisConnectionFactory redisConnectionFactory){
        RedisMessageListenerContainer listenerContainer = new RedisMessageListenerContainer();

        listenerContainer.setConnectionFactory(redisConnectionFactory);
        List<Topic> list = new ArrayList<>();
        list.add(new PatternTopic("TEST01"));
        list.add(new PatternTopic("TEST02"));
        list.add(new PatternTopic("TEST03"));
        /*
         *  redisMessaeListener  消息监听器
         *  list  订阅的主题(可以单个和多个)
         */
        listenerContainer.addMessageListener(redisMessaeListener1,list);
        listenerContainer.addMessageListener(redisMessaeListener2,new PatternTopic("TEST01"));
        return listenerContainer;
    }
}

  • 向指定频道发送消息
    /**
     *  PUBLISH 发送消息到指定频道
     * @author zhengfuping
     * @date 2023/8/2 11:14
     * @param channel 通道
     * @param name  数据
     * @param age
     * @return Object
     */
    @GetMapping("/pub")
    public Object pub(String channel,String name,Integer age) {
        User user = new User(name, age);
        redisTemplate.convertAndSend(channel,user);
        return user;
    }

在这里插入图片描述

2、还可以使用redisTemplate实现订阅

  • 该方式是Redis客户端的方法,用于在独立的Redis客户端中直接使用发布订阅功能。它需要创建一个Redis连接对象,并通过调用subscribe方法来订阅一个或多个频道。

  • 如果您只是在独立的Redis客户端中使用发布订阅功能,并且不需要使用Spring的其他功能,则可以选择connection.subscribe

 /**
 * 自行添加订阅
 */
    @GetMapping("/sub")
    public void sub(String channel) {
        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();

        /*
         *  MessageListener:监听器,直接使用内部类实现绑定监听可以把数据传递出去
         *  channel 订阅频道
         */
        connection.subscribe((message, pattern) -> {
            String channel1 = new String(message.getChannel());
            String body = new String(message.getBody());
            System.out.println("subscribe方式监听:消息: " + body + " 通道QQ: " + channel1);
        }, channel.getBytes());

//            connection.close();
    }

在这里插入图片描述

发送消息

    @GetMapping("/pub")
    public Object pub(String channel,String name,Integer age) {
        User user = new User(name, age);
        redisTemplate.convertAndSend(channel,user);
        return user;
    }

在这里插入图片描述

最终监听到的结果
在这里插入图片描述

三、、 趋于成熟的队列:Stream

我们来看 Stream 是如何解决上面这些问题的。

我们依旧从简单到复杂,依次来看 Stream 在做消息队列时,是如何处理的?

首先,Stream 通过 XADD 和 XREAD 完成最简单的生产、消费模型:

  • 生产者发布 2 条消息:
// *表示让Redis自动生成消息ID
127.0.0.1:6379> XADD queue * name zhangsan
"1618469123380-0"
127.0.0.1:6379> XADD queue * name lisi
"1618469127777-0"
  • 消费者拉取消息:
// 从开头读取5条消息,0-0表示从开头读取
127.0.0.1:6379> XREAD COUNT 5 STREAMS queue 0-0
1) 1) "queue"
   2) 1) 1) "1618469123380-0"
         2) 1) "name"
            2) "zhangsan"
      2) 1) "1618469127777-0"
         2) 1) "name"
            2) "lisi"

流程图
在这里插入图片描述

具体java代码实现:

  • 先配置监听消息类
@Slf4j
@Component
public class ListenerMessage implements StreamListener<String, MapRecord<String, String, String>> {

    @Override
    public void onMessage(MapRecord<String, String, String> entries) {
        log.info("接受到来自redis的消息");
        System.out.println("message id "+entries.getId());
        System.out.println("stream "+entries.getStream());
        System.out.println("body "+entries.getValue());
    }
}
  • 添加工具类,实现初始化
@Component
@Slf4j
public class RedisStreamUtil {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;


    /**
     * @author zhengfuping 添加数据
     * @param streamKey
     * @param map
     * @return RecordId
     */
    public RecordId addStream(String streamKey,Map<String, Object> map){
        RecordId recordId = redisTemplate.opsForStream().add(streamKey, map);
        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,String recordIds){
        redisTemplate.opsForStream().delete(key,recordIds);
    }

    /**
     * 用来初始化 实现绑定
     */
    public void initStream(String key, String group){
        //判断key是否存在,如果不存在则创建
        boolean hasKey = hasKey(key);
        if(!hasKey){
            Map<String,Object> map = new HashMap<>();
            map.put("field","value");
            RecordId recordId = addStream(key, map);
            addGroup(key,group);   //把Stream和gropu绑定
            delField(key,recordId.getValue());
            log.info("stream:{}-group:{} initialize success",key,group);
        }
    }
}
  • 添加配置类,配置Stream
/**
 * @author zhengfuping
 * @version 1.0
 * @description: TODO  添加配置类,配置Stream
 */
@Configuration
@Slf4j
public class RedisStreamConfig {

    @Autowired
    private RedisStreamUtil redisStream;
    @Autowired
    private ListenerMessage listenerMessage;


    @Bean
    public Subscription subscription(RedisConnectionFactory factory){
//        代码中的var是使用了Lombok的可变局部变量。主要是为了方便
//        StreamMessageListenerContainer: 消息侦听容器,不能在外部实现。创建后,StreamMessageListenerContainer可以订阅Redis流并使用传入的消息
        var options = StreamMessageListenerContainer
                .StreamMessageListenerContainerOptions
                .builder()
                .pollTimeout(Duration.ofSeconds(1))
                .build();
        redisStream.initStream("mystream","mygroup");	//调用初始化
        var listenerContainer = StreamMessageListenerContainer.create(factory,options);

        /*
         *  注意这里接受到消息后会被自动的确认,如果不想自动确认请使用其他的创建订阅方式
         * 消费组 consumer group ,它不能为null (Consumer类型)
         * stream offset ,stream的偏移量(StreamOffset 类型)
         * listener 不能为null (StreamListener<K,V> 类型)
         */
        var subscription = listenerContainer.receiveAutoAck(Consumer.from("mygroup","huhailong"),
                StreamOffset.create("mystream", ReadOffset.lastConsumed()),listenerMessage);

        listenerContainer.start();
        return subscription;
    }
}

  • 调用测试
/**
 * @author zhengfuping
 * @version 1.0
 * @description: TODO
 * @date 2023/8/2 16:06
 */
@RestController
@RequestMapping("/redisStream")
public class RedisStreamTest {

    @Autowired
    private RedisStreamUtil redisStream;

    @GetMapping("add")
    public void add(String key,String data){
        Map<String, Object> map = new HashMap<>();
        map.put(key,data);
//        添加数据到mystream流中
        RecordId recordId = redisStream.addStream("mystream", map);
//        删除流中消费了的指定key的数据
        redisStream.delField("mystream",recordId.getValue());
    }
}

Stream的好处在于可以写入到 RDB 和 AOF 做持久化。
Stream是新增加的数据类型,它与其它数据类型一样,每个写操作,也都会写入到 RDB 和 AOF 中。
我们只需要配置好持久化策略,这样的话,就算 Redis 宕机重启,Stream 中的数据也可以从 RDB 或 AOF 中恢复回来。

总结

好了,总结一下。这篇文章我们从「Redis 能否用作队列」这个角度出发,介绍了 List、Pub/Sub、Stream 在做队列的使用方式,以及它们各自的优劣。

之后又把 Redis 和专业的消息队列中间件做对比,发现 Redis 的不足之处。

最后,我们得出 Redis 做队列的合适场景。

在这里插入图片描述

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

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

相关文章

win10 64位 vs2017 qt5.12.6 pcl1.9.1 vtk8.1.1配置安装步骤

由于我电脑中有 QT5.12.6 VS2017&#xff0c;就不介绍怎么安装了&#xff0c;只介绍cmake&#xff0c;pcl及vtk的配置步骤 为了便于后续QT的调用&#xff0c;以下所有安装路径中均不能出现中文及空格等 PCL自带VTK是不完整的&#xff0c;所以需要下载VTK源码进行重新编译使其…

激光切割机在镂空技术中的运用场景具体包括哪些部分

本文将为您呈现一些激光镂空工艺的实际应用情况。激光切割机应用在镂空工艺上的一些地方。 首先&#xff0c;纸艺激光镂空的应用&#xff1a; 纸是中国古代四大发明之一&#xff0c;激光则是20世纪以来人类的一项重大发明。当传统文化与现代科技相互碰撞时&#xff0c;使得纸雕…

分享低成本非隔离PWM控制AC-DC开关芯片 YB5011

简介&#xff1a; YB5011系列是一款高性能低成本PWM控制功率开关&#xff0c;适用于离线式小功率降 压型应用场合&#xff0c;外围电路简单、器件个数少。同时产品内置高耐压MOSFET可提高 系统浪涌耐受能力,集成有完备的带自恢复功能的保护功能&#xff1a;VDD欠压保护、逐周期…

使用傲梅 VMware 备份软件保障数据保护

VMware数据保护一直是热门话题&#xff0c;因为VMware是虚拟化的驱动力。96% 的用户至少经历过数据丢失的主要原因之一&#xff1a;人为错误、硬盘驱动器故障、断电、火灾和自然灾害。 有效的 VMware 备份解决方案可以保护您的虚拟环境&#xff0c;并能够在需要时快速保护和恢…

Kafka-Broker工作流程

kafka集群在启动时&#xff0c;会将每个broker节点注册到zookeeper中&#xff0c;每个broker节点都有一个controller&#xff0c;哪个controller先在zookeeper中注册&#xff0c;哪个controller就负责监听brokers节点变化&#xff0c;当有分区的leader挂掉时&#xff0c;contro…

在商业广告领域中,LDE透明屏有哪些应用表现?

LDE透明屏是一种新型的显示技术&#xff0c;它能够在显示内容的同时保持屏幕的透明度&#xff0c;使得用户可以透过屏幕看到背后的物体。LDE透明屏的出现&#xff0c;为我们的生活带来了许多新的可能性。 首先&#xff0c;LDE透明屏可以应用于商业广告领域。 传统的广告牌需要…

浅谈实际工程中智能照明系统的节能设计

安科瑞 华楠 摘要&#xff1a;本文介绍了智能照明系统在实际工程中的应用&#xff0c;简单介绍了智能照明控制设计系统&#xff0c;阐述当前智能照明对建筑节能的重要意义&#xff0c;合理地分析了智能照明系统的发展前景。 关键词&#xff1a;智能照明系统控制&#xff0c;…

​17款画流程图的工具全面分析,功能一览!

流程图又称框图&#xff0c;是以特定的图形符号加上说明&#xff0c;表示算法的图。流程图相对于纯文字的表达而言在视觉上更清晰&#xff0c;能帮助我们进行更加有效的沟通和分析。流程图制作软件是一种提供创建图表功能的应用程序&#xff0c;解决了手动绘制流程图比较耗费时…

Roboflow制作yolov8数据集

进入官网网页 Sign in to Roboflow 先注册&#xff0c;因为是外网&#xff0c;注册前可以选择》》fanqiangruanjian 链接&#xff1a;https://pan.baidu.com/s/1YhLxSynvtcY1_FAbhc9q0g 提取码&#xff1a;f3es Roboflow标注平台使用----小白都能看懂_李大帅哥哈哈的博客-…

spring.config.location 手动指定配置文件文件

–spring.config.locationD:\javaproject\bangsun\ds-admin\ds-oper-mgr\src\main\resources\application.yml

海康视频插件VideoWebPlugin在vue中的实现

一,将js文件放在public文件下 二,在index中全局引入 三.在视频页面写方法,创建实例,初始化,我写的是1*4屏的 <template><!--视频窗口展示--><div idplayWnd classNameplayWnd refplayWnd styleleft: 0; bottom: 0;height: 902px;width: 60vw></div>&…

五分钟理解NIO与BIO

java NIO与BIO的区别&#xff1f; BIO -- Blocking IO 即阻塞式 IO。NIO -- Non-Blocking IO, 即非阻塞式 IO 或异步 IO。 BIO 基于字节流和字符流进行操作&#xff0c;数据的读取写入必须阻塞在一个线程内等待其完成。 NIO 主要有三大核心部分&#xff1a; Channel (通道)…

行业追踪,2023-08-02

自动复盘 2023-08-02 凡所有相&#xff0c;皆是虚妄。若见诸相非相&#xff0c;即见如来。 k 线图是最好的老师&#xff0c;每天持续发布板块的rps排名&#xff0c;追踪板块&#xff0c;板块来开仓&#xff0c;板块去清仓&#xff0c;丢弃自以为是的想法&#xff0c;板块去留让…

在线帮助中心 HelpLook 的致命弱点!!!

您可以使用Helplook搭建一个高效的企业知识库。利用这个知识库&#xff0c;您的团队成员将能够更好地管理、共享和获取内部知识&#xff0c;提高工作效率和协作能力。但是也得看清楚其中的有些功能需求是否满足&#xff01;&#xff01;&#xff01; HelpLook的功能欠缺还有很多…

【SQL开发实战技巧】系列(一):关于SQL不得不说的那些事

系列文章目录 【SQL开发实战技巧】系列&#xff08;一&#xff09;:关于SQL不得不说的那些事 【SQL开发实战技巧】系列&#xff08;二&#xff09;&#xff1a;简单单表查询 【SQL开发实战技巧】系列&#xff08;三&#xff09;&#xff1a;SQL排序的那些事 【SQL开发实战技巧…

这些能帮你跨越音乐边界的音频转换器推荐给你

嘿&#xff0c;朋友,你是否曾经遇到过这样的情况&#xff1a;收集了许多喜爱的音乐&#xff0c;但发现其中一些仅仅支持wma格式&#xff0c;而你的设备却只能播放mp3&#xff1f;别担心&#xff0c;因为在这个数字化时代&#xff0c;有一个神奇的工具可以帮助你解决这个问题——…

老板说把跳针改过去,什么是主板跳针

最近骑车拍了很多视频&#xff0c;把电脑磁盘堆满了&#xff0c;想着买一条固态SSD卡扩展一下。 一咬牙一跺脚&#xff0c;直接安排&#xff0c;毫不犹豫。顺带加装了无限网卡和蓝牙5.2。 收到后立马安装。安装完发现识别不到新磁盘 确认安装没问题。然后就去问固态硬盘的客服 …

Live Market做世界C端用户数据的耕耘,数据和流量的价值呈现

在数字化时代&#xff0c;数据成为了推动经济增长和商业发展的重要资源&#xff0c;而流量则是数据价值的体现和传递媒介。随着全球互联网的普及和移动设备的智能化&#xff0c;C端用户数据的收集和分析变得尤为重要。在这个领域&#xff0c;有一家专注于世界C端用户数据耕耘的…

对象的深拷贝和浅拷贝

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。 数据类型 数据分为基本数据类型(String, Number, Boolean, Null, Undefined&#xff0c;Symbol)和对象数据类型。 基本数据类型的特点&#xff1a;直接存储在栈(stack)中的数据引用数据类型的特点&#xff1a;存…

微信小程序选项卡切换(滑动切换,点击切换)

效果如下&#xff1a;可点击切换&#xff0c;滑动切换 代码如下 这个可以在项目用 index.wxml <view classtopTabSwiper><view classtab {{currentData 0 ? "tabBorer" : ""}} data-current "0" bindtapcheckCurrent>选项一&…