RocketMQ 消息重试机制

news2024/10/6 10:31:43

文章目录

  • 消息发送重试
    • 重试触发条件
    • 重试流程
    • 重试间隔
    • 重试常见问题
    • 消息流控机制
      • 流控触发条件
  • 生产者控制消息发送重试次数
    • gRPC 客户端
    • remoting 客户端
  • 消费重试
    • 重试触发条件
    • PushConsumer 消费重试策略
      • PushConsumer 重试间隔时间
      • 修改 PushConsumer 最大重试次数
        • gRPC 协议端口
        • Remoting 协议端口
    • SimpleConsumer 消费重试策略
      • SimpleConsumer 消费重试时间间隔
      • 修改 SimpleConsumer 最大重试次数
  • 消息重试注意问题
    • 消息重试导致的重复消息和重复消费的问题(消息幂等性)
  • gRPC 协议消费者重试示例
    • 添加消费者分组,并设置重试次数
    • 生产者
    • 消费者
    • 死信队列
    • 消费死信队列
  • 如何查看死信消息

RocketMQ 消息重试分为发送重试(生产者)和消费重试(消费者)

消息发送重试

RocketMQ 客户端连接服务端发起消息发送请求时,可能会因为网络故障、服务异常等原因导致调用失败。为保证消息的可靠性, RocketMQ 在客户端SDK中内置请求重试逻辑,尝试通过重试发送达到最终调用成功的效果。同步发送和异步发送模式均支持消息发送重试。

重试触发条件

  • 客户端消息发送请求调用失败或请求超时
  • 网络异常造成连接失败或请求超时。
  • 服务端节点处于重启或下线等状态造成连接失败。
  • 服务端运行慢造成请求超时。
  • 服务端返回失败错误码
    • 系统逻辑错误:因运行逻辑不正确造成的错误。
    • 系统流控错误:因容量超限造成的流控错误。

对于事务消息,网络超时或异常等场景不会进行重试。

重试流程

生产者在初始化时设置消息发送最大重试次数,当出现上述触发条件的场景时,生产者客户端会按照设置的重试次数一直重试发送消息,直到消息发送成功或达到最大重试次数重试结束,并在最后一次重试失败后返回调用错误响应。

  • 同步发送:调用线程会一直阻塞,直到某次重试成功或最终重试失败,抛出错误码和异常。
  • 异步发送:调用线程不会阻塞,但调用结果会通过异常事件或者成功事件返回。

重试间隔

  • 除服务端返回系统流控错误场景,其他触发条件触发重试后,均会立即进行重试,无等待间隔。

  • 若由于服务端返回流控错误触发重试,系统会按照指数退避策略进行延迟重试。指数退避算法通过以下参数控制重试行为:

    • INITIAL_BACKOFF: 第一次失败重试前后需等待多久,默认值:1秒。

    • MULTIPLIER :指数退避因子,即退避倍率,默认值:1.6。

    • JITTER :随机抖动因子,默认值:0.2。

    • MAX_BACKOFF :等待间隔时间上限,默认值:120秒

    • MIN_CONNECT_TIMEOUT :最短重试间隔,默认值:20秒。

计算算法如下:

ConnectWithBackoff()
current_backoff = INITIAL_BACKOFF
current_deadline = now() + INITIAL_BACKOFF
while (TryConnect(Max(current_deadline, now() + MIN_CONNECT_TIMEOUT))!= SUCCESS){
	SleepUntil(current_deadline)
	current_backoff = Min(current_backoff * MULTIPLIER, MAX_BACKOFF)
	current_deadline = now() + current_backoff + UniformRandom(-JITTER * current_backoff, JITTER * current_backoff)
}

重试常见问题

消息肯定不能无限重试,所以生产者可以控制最大重试次数,如果最终重试还是失败,将会抛出异常。重试还可能造成消息重复(这需要根据业务逻辑自己编码处理 —— 幂等性)

消息流控机制

消息流控指的是系统容量或水位过高, RocketMQ 服务端会通过快速失败返回流控错误来避免底层资源承受过高压力。

流控触发条件

  • 存储压力大:大量的消息需要同时存储时,MQ 存储压力瞬间飙升,会触发消息流控。
  • 服务端请求任务排队溢出:若消费者消费能力不足,导致队列中有大量堆积消息,当堆积消息超过一定数量后会触发消息流控,减少下游消费系统压力。

当前系统触发流控是,客户端一般会收到错误和异常信息如下:

  • reply-code:530
  • reply-text:TOO_MANY_REQUESTS

生产者控制消息发送重试次数

gRPC 客户端

// 构建生产者
Producer producer = provider.newProducerBuilder()
         // Topics 列表:生产者和主题是多对多的关系,同一个生产者可以向多个主题发送消息
         .setTopics("MY_FIFO_TOPIC")
         .setClientConfiguration(configuration)
         // 设置消息发送重试次数(默认:3 次)
         .setMaxAttempts(3)
         // 构建生产者,此方法会抛出 ClientException 异常
         .build();

remoting 客户端

// 设置同步发送重试次数(默认:2)
producer.setRetryTimesWhenSendFailed(2);
// 设置一般发送重试次数(默认:2)
producer.setRetryTimesWhenSendAsyncFailed(2);

消费重试

消费重试指的是,消费者在消费某条消息失败后,RocketMQ 服务端会根据重试策略重新消费该消息,超过一次定数后若还未消费成功,则该消息将不再继续重试,直接被发送到死信队列中。

重试触发条件

  • 消费失败:包括消费者返回失败状态或抛出异常
  • 消息处理超时

不同的消费者类型,重试触发条件是一样的,但 PushConsumer 和 SimpleConsumer 重试策略稍有不同。

PushConsumer 消费重试策略

PushConsumer 消费的消息,涉及到的状态如下(DLQ:dead letter queue 死信队列):

在这里插入图片描述

  • Ready:就绪状态的消息才能被消费者获取。(参考 SimpleConsumer 的消费不可见时间和重试等待时间)
  • Inflight:处理中,消费者获取消息,执行消息但尚未执行结束返回消费结果。
  • Wait Retry:等待重试。PushConsumer独有的状态。当消费失败或超时,但重试次数未达上限时的状态。此状态经过重试时间间隔后消息将重新进入 Ready 状态,等待消费。多次重试之间,可通过重试间隔进行延长,防止无效高频的失败。
  • Acked:成功消费状态。达到此状态,说明消息被成功消费。
  • DLQ:死信状态。消费重试到达上限时,消息不会再此重试,会被投递到死信队列。我们可以通过消费死信队列的消息进行业务修复。

PushConsumer 重试间隔时间

  • 非顺序消息
第几次重试间隔时间
110s
230s
31m
42m
53m
64m
75m
86m
97m
108m
119m
1210m
1320m
1430m
151h
162h
>16 大于16次,后续间隔都为2h2h
  • 顺序消息重试间隔为固定时间,默认为:3000ms(因为前面消息在重试,后面的消息在排队,无法消费)

修改 PushConsumer 最大重试次数

gRPC 协议端口

gRPC 协议端口的重试次数设置,以及顺序消费的设置都是设置在消费者分组创建时的元数据控制,也就是说我们在编写代码都时候不需要在代码中设置,请参考前篇《RocketMQ 消费者分类与分组》创建或修改消费者分组来设置。

PushConsumerBuilder 在 build 方法中实例化 PushConsumer 的实现的时候,会读取 MQ 服务端对应消费者分组的设置,也就是说 gRPC 协议的客户端,不允许在客户端代码中修改相关消费者分组的设置。

Remoting 协议端口
// 默认即为 16 次
consumer.setMaxReconsumeTimes(16);

SimpleConsumer 消费重试策略

在这里插入图片描述

SimpleConsumer 没有 wait retry 状态。消费失败后根据 InvisibleDuration (消费不可见时间)来计算时间间隔

SimpleConsumer 消费重试时间间隔

消息重试间隔 = InvisibleDuration (不可见时间)- 消息实际处理时长

例如,消息不可见时间为30 ms,实际消息处理用了10 ms就返回失败响应,则距下次消息重试还需要20 ms,此时的消息重试间隔即为20 ms;若直到30 ms消息还未处理完成且未返回结果,则消息超时,立即重试,此时重试间隔即为0 ms。

为了避免 InvisibleDuration 时间小于消息实际处理时长,在消息消费过程中,我们可以动态的调整InvisibleDuration 的时长,来避免此类情况出现。

// 修改 InvisibleDuration
simpleConsumer.changeInvisibleDuration(); 
simpleConsumer.changeInvisibleDurationAsync() 

修改 SimpleConsumer 最大重试次数

SimpleConsumer 重试次数的修改与PushConsumer 相同

消息重试注意问题

消息重试适用业务处理失败且当前消费为小概率事件的场景,是为了解决偶发情况,消费失败。如果消费重试情况经常出现,请考虑修改相应业务逻辑或修改相关代码。

顺序消息频发重试,可能导致顺序消息堆积。

消息重试导致的重复消息和重复消费的问题(消息幂等性)

  • 重复消息:生产者重试可能导致消息重复
  • 重复消费:消费者重试可能导致重复消费

RocketMQ 无法避免消息重复(Exactly-Once),所以如果业务对消费重复非常敏感,务必要在业务层面进行去重处理。可以借助关系数据库进行去重。首先需要确定消息的唯一键,可以是msgId,也可以是消息内容中的唯一标识字段,例如订单Id等。在消费之前判断唯一键是否在关系数据库中存在。如果不存在则插入,并消费,否则跳过。(实际过程要考虑原子性问题,判断是否存在可以尝试插入,如果报主键冲突,则插入失败,直接跳过)

注:msgId一定是全局唯一标识符,但是实际使用中,可能会存在相同的消息有两个不同msgId的情况(消费者主动重发、因客户端重投机制导致的重复等),这种情况就需要使业务字段进行重复消费。

gRPC 协议消费者重试示例

添加消费者分组,并设置重试次数

$> ./mqadmin updateSubGroup -n 127.0.0.1:9876 -g MY_RETRY_GROUP -r 3 -c DefaultCluster

我们此处为了验证方便,设置重试次数为 3 次。

生产者

import com.yyoo.mq.rocket.MyMQProperties;
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.message.Message;
import org.apache.rocketmq.client.apis.producer.Producer;
import org.apache.rocketmq.client.apis.producer.SendReceipt;

public class RetryProducerDemo {

    public static void main(String[] args) throws ClientException {

        // 用于提供:生产者、消费者、消息对应的构建类 Builder
        ClientServiceProvider provider = ClientServiceProvider.loadService();

        // 构建配置类(包含端点位置、认证以及连接超时等的配置)
        ClientConfiguration configuration = ClientConfiguration.newBuilder()
                // endpoints 即为 proxy 的地址,多个用分号隔开。如:xxx:8081;xxx:8081
                .setEndpoints(MyMQProperties.ENDPOINTS)
                .build();

        // 构建生产者
        Producer producer = provider.newProducerBuilder()
                // Topics 列表:生产者和主题是多对多的关系,同一个生产者可以向多个主题发送消息
                .setTopics("MY_NORMAL_TOPIC")
                .setClientConfiguration(configuration)
                // 构建生产者,此方法会抛出 ClientException 异常
                .build();

        // 构建消息类
        Message message = provider.newMessageBuilder()
                // 设置消息发送到的主题
                .setTopic("MY_NORMAL_TOPIC")
                // 设置消息索引键,可根据关键字精确查找某条消息。其一般为业务上的唯一值。如:订单id
                .setKeys("order_id_1001")
                // 设置消息Tag,用于消费端根据指定Tag过滤消息。其一般用作区分不同的业务,最好给它定义好命名规范
                .setTag("RETRY_TEST")
                // 消息体,单条消息的传输负载不宜过大。所以此处的字节大小最好有个限制
                .setBody("{\"success\":true,\"order_id\":\"1001\",\"msg\":\"消费重试测试!\"}".getBytes())
                .build();

        // 发送消息(此处最好进行异常处理,对消息的状态进行一个记录)
        try {
            SendReceipt sendReceipt = producer.send(message);
            System.out.println("Send message successfully, messageId=" + sendReceipt.getMessageId());
        } catch (ClientException e) {
            System.out.println("Failed to send message");
        }

    }

}

生产者代码和普通的消息发送代码一致。

消费者

import com.yyoo.mq.rocket.MyMQProperties;
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.FilterExpression;
import org.apache.rocketmq.client.apis.consumer.FilterExpressionType;
import org.apache.rocketmq.client.apis.consumer.PushConsumer;

import java.nio.ByteBuffer;
import java.util.Collections;

public class RetryConsumerDemo {

    public static void main(String[] args) throws ClientException {

        // 用于提供:生产者、消费者、消息对应的构建类 Builder
        ClientServiceProvider provider = ClientServiceProvider.loadService();

        // 构建配置类(包含端点位置、认证以及连接超时等的配置)
        ClientConfiguration configuration = ClientConfiguration.newBuilder()
                // endpoints 即为 proxy 的地址,多个用分号隔开。如:xxx:8081;xxx:8081
                .setEndpoints(MyMQProperties.ENDPOINTS)
                .build();


        // 设置过滤条件(这里为使用 tag 进行过滤)
        String tag = "RETRY_TEST";
        FilterExpression filterExpression = new FilterExpression(tag, FilterExpressionType.TAG);

        // 构建消费者
        PushConsumer pushConsumer = provider.newPushConsumerBuilder()
                .setClientConfiguration(configuration)
                // 设置消费者分组
                .setConsumerGroup("MY_RETRY_GROUP")
                // 设置主题与消费者之间的订阅关系
                .setSubscriptionExpressions(Collections.singletonMap("MY_NORMAL_TOPIC", filterExpression))
                .setMessageListener(messageView -> {
                    System.out.println("============开始消费!");
                    System.out.println(messageView);
                    ByteBuffer rs = messageView.getBody();
                    byte[] rsByte = new byte[rs.limit()];
                    rs.get(rsByte);
                    // 一直失败,测试消费重试机制
                    if(true){
                        return ConsumeResult.FAILURE;
                    }

                    System.out.println("Message body:" + new String(rsByte));
                    // 处理消息并返回消费结果。
                    System.out.println("Consume message successfully, messageId=" + messageView.getMessageId());
                    return ConsumeResult.SUCCESS;
                }).build();

        System.out.println(pushConsumer);


        // 如果不需要再使用 PushConsumer,可关闭该实例。
        // pushConsumer.close();

    }

}

达到最大重试次数后,消息将被发送到死信队列。

死信队列

死信队列对应的 TOPIC 名称为:%DLQ% + 消费者分组名称。我们的示例对应的死信 TOPIC 名称为:%DLQ%MY_RETRY_GROUP。我们可以通过消费死信队列的消息进行业务恢复或者进行死信消息的持久化存储。

死信消息在 MQ 中的最长存储时间为 3 天,即便我们消费了死信队列的消息,死信消息依然会存储在MQ,到期后才删除。

消费死信队列

import com.yyoo.mq.rocket.MyMQProperties;
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.FilterExpression;
import org.apache.rocketmq.client.apis.consumer.FilterExpressionType;
import org.apache.rocketmq.client.apis.consumer.PushConsumer;

import java.nio.ByteBuffer;
import java.util.Collections;

public class DLQConsumerDemo {

    public static void main(String[] args) throws ClientException {

        // 用于提供:生产者、消费者、消息对应的构建类 Builder
        ClientServiceProvider provider = ClientServiceProvider.loadService();

        // 构建配置类(包含端点位置、认证以及连接超时等的配置)
        ClientConfiguration configuration = ClientConfiguration.newBuilder()
                // endpoints 即为 proxy 的地址,多个用分号隔开。如:xxx:8081;xxx:8081
                .setEndpoints(MyMQProperties.ENDPOINTS)
                .build();


        // 设置过滤条件(这里为使用 tag 进行过滤)
        String tag = "RETRY_TEST";
        FilterExpression filterExpression = new FilterExpression(tag, FilterExpressionType.TAG);

        // 构建消费者
        PushConsumer pushConsumer = provider.newPushConsumerBuilder()
                .setClientConfiguration(configuration)
                // 设置消费者分组
                .setConsumerGroup("DLQ_CONSUMER_GROUP")
                // 设置主题与消费者之间的订阅关系
                .setSubscriptionExpressions(Collections.singletonMap("%DLQ%MY_RETRY_GROUP", filterExpression))
                .setMessageListener(messageView -> {
                    System.out.println("============开始消费!");
                    System.out.println(messageView);
                    ByteBuffer rs = messageView.getBody();
                    byte[] rsByte = new byte[rs.limit()];
                    rs.get(rsByte);
                    // 测试消费重试机制
                    if(true){
                        return ConsumeResult.FAILURE;
                    }

                    System.out.println("死信消息消费:" + new String(rsByte));
                    // 处理消息并返回消费结果。
                    System.out.println("死信消息消费 Consume message successfully, messageId=" + messageView.getMessageId());
                    return ConsumeResult.SUCCESS;
                }).build();

        System.out.println(pushConsumer);


        // 如果不需要再使用 PushConsumer,可关闭该实例。
        // pushConsumer.close();

    }

}

死信队列消费与普通消息消费是一样的,它同样有重试逻辑,遵循同样的规律。即死信消息消费时也有重试机制以及超过最大重试次数成为死信消息。

如何查看死信消息

  • 通过 Admin Tool 命令在服务端查看
  • 通过 RocketMQ Dashboard 后台查看
  • 通过 Promethus 查看 MQ 的监控指标(由 RocketMQ Promethus Exporter 实现)
  • 通过 Admin Tool 源码接口 MQAdminExt 自行编码实现(其实以上 3 种方式都是通过 MQAdminExt 接口来实现的)

不仅仅是查看死信消息,Dashboard 还提供了后台管理界面,可以新增主题、消费者分组等各种 Admin Tool 支持的操作,但 Dashboard 版本几乎没有更新,比如修改消费者分组为顺序消费等功能没有,而且很多功能可能会报错,但其是我们自定义 MQ 管理功能的重要参考。

Admin Tool 命令涉及到源码主要在:
在这里插入图片描述

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

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

相关文章

华为数字能源,开启超充新纪元

编辑:阿冒 设计:沐由 在过去很长的一段时间里,国内某著名品牌火锅是从来不担心获客的。顶峰时期,该品牌每年服务超过1.6亿人次的顾客,翻台率达到了5次/天,几乎创下了餐饮界的最高翻台率。 翻台率是餐饮企业…

调用CFCA金信反欺诈服务相关接口,很详细

调用CFCA金信反欺诈服务相关接口,很详细 一、准备二、调用接口1、查询接口文档2、查看代码示例3、测试调用接口 三、工具类1、CFCA金信反欺诈服务接口码枚举类2、CFCA金信反欺诈服务的公共参数配置3、加密解密工具类4、请求参数dto5、调用接口工具类(关键…

【N年测试总结】证券行业的测试特点

每个行业由于其业务形式,产品形态,行业要求等等的不同,都有其不同于其他行业的测试特点,对测试人员的重点能力要求也不同。 一、证券行业业务系统简介 证券行业的业务系统这里按照C端系统和B端业务系统两大类进行介绍。 C端系统…

tensorrt C++推理

char* trtModelStream{ nullptr }; //char* trtModelStreamnullptr; 开辟空指针后 要和new配合使用,比如89行 trtModelStream new char[size]size_t size{ 0 };//与int固定四个字节不同有所不同,size_t的取值range是目标平台下最大可能的数组尺寸,一些平台下size_…

通讯网关软件012——利用CommGate X2OPC实现MS SQL数据写入OPC Server

本文推荐利用CommGate X2OPC实现从MS SQL服务器获取数据并写入OPC Server。CommGate X2OPC是宁波科安网信开发的网关软件,软件可以登录到网信智汇(http://wangxinzhihui.com)下载。 【案例】如下图所示,实现从MS SQL数据库获取数据并写入OPC Server。 【…

(Vue2)智慧商城项目

新增两个目录api、utils api接口模块:发送ajax请求的接口模块 utils工具模块:自己封装的一些工具方法模块 第三方组件库vant-ui PC端:element-ui(element-plus) ant-design-vue 移动端:vant-ui Mint UI…

Vue3最佳实践 第五章 Vue 组件应用 3( Slots )

5.4 Slots 我们已经了解到组件能够接收任意类型的 JavaScript 值作为 props,但组件要如何接收模板内容呢?在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。Slots 可用于将Html内容从父组…

怎么样深入学习一门技术(Python)

进入官网 Python官网文档 https://docs.python.org/zh-cn/ 边敲代码边理解 多看教学视频 狠狠的花时间

Android 使用kotlin+注解+反射+泛型实现MVP架构

一,MVP模式的定义 ①Model:用于存储数据。它负责处理领域逻辑以及与数据库或网络层的通信。 ②View:UI层,提供数据可视化界面,并跟踪用户的操作,以便通知presenter。 ③Presenter:从Model层获…

Securing TEEs With Verifiable Execution Contracts【TDSC`23】

目录 摘要引言贡献 背景Intel SGX侧信道攻击Intel处理器的硬件扩展 概述威胁模型SGX已存的安全威胁侧信道泄露操作系统相关的威胁现有防御的限制 可验证的执行合同作为防御 摘要 最近的研究表明,可信执行环境,如Intel Software Guard Extensions&#x…

Nginx 背锅解析漏洞

Nginx 背锅解析漏洞 文章目录 Nginx 背锅解析漏洞1 在线漏洞解读:2 环境搭建3 影响版本:4 漏洞复现4.1 访问页面4.2 上传文件 4.3 上传失败4.4 使用bp进行分析包4.5 对返回图片位置进行访问4.6 执行php代码技巧-图片后缀加./php4.7 分析原因 --》cgi.fix_pathinfo--…

工艺防错指导、可视化工具管理——SunTorque智能扭矩系统

智能扭矩系统-智能拧紧系统-智能扭矩控制-SunTorque 拧紧的定义——运用拧紧工具及螺栓,使被联接体紧密贴合,并能承受一定的载荷,且被连接体间具备足够的夹紧力,以确保被联接零件的可靠联接和正常工作。 从定义中前六个字“运用…

解读:ISO 14644-21:2023《洁净室及相关受控环境:悬浮粒子采样》发布指导粒子采样!

药品洁净实验室环境监测结果是否满足微生物检测需求,直接决定检测结果的有效性准确性,进行药品微生物检测,必须对实验环境进行日常和定期监测,其内容包括非生物活性的空气悬浮粒子数及有生物活性的微生物监测。 悬浮粒子监测是保证…

python百钱百鸡

编写程序,解决“百钱百鸡”问题。 一只公鸡值五钱,一只母鸡值三钱,三只小鸡值一钱。 源代码: for a in range(1, 101): for b in range(1, 101): for c in range(1, 101): if (a * 5 b * 3 c / 3 100)…

CSRF攻击

防御策略 过滤判断换referer头,添加tocken令牌验证,白名单 CSRF攻击和XSS比较 相同点:都是欺骗用户 不同点: XSS有攻击特征,所有输入点都要考虑代码,单引号过滤 CSRF没有攻击特征,利用的点…

城市智慧公厕:引领科技创新的新时代

城市智慧公厕已经成为当下社会治理模式的升级范式,催生了无限的科技创新。如智慧公厕源头厂家广州中期科技有限公司,所推出的智慧公厕整体解决方案,除基本的厕位监测与引导、环境监测与调节、安全防范与管理、保洁考勤管理、多媒体交互、综合…

数字化转型的五个等级及思考

数字化转型是当前企业和社会关注的热点话题。然而,对于数字化转型的五个等级及其思考,并没有一个清晰的概述。以下是我对数字化转型的五个等级及其思考的简要探讨。 第一等级:基础设施升级 在数字化转型的初始阶段,企业需要对其基…

长期用眼不再怕!NineData SQL 窗口支持深色模式

您有没有尝试过被明亮的显示器闪瞎眼的经历? 在夜间或低光环境下,明亮的界面会导致许多用眼健康问题,例如长时间使用导致的眼睛疲劳、干涩和不适感,同时夜间还可能会抑制褪黑素分泌,给您的睡眠质量带来影响。 这些问…

uni-app:实现picker下拉列表

效果 代码 <template><view class"container"><picker name"info" change"bindPickerChange9" :value"index9" :range"selectDatas9"><view class"right"><view class"right_l…