返回顶部
首页 > 资讯 > 后端开发 > JAVA >RabbitMQ延迟队列
  • 193
分享到

RabbitMQ延迟队列

rabbitmq分布式java-rabbitmq后端java 2023-09-01 14:09:45 193人浏览 独家记忆
摘要

目录 一、概念 二、使用场景 三、RabbitMQ 中的 TTL (一)队列设置 TTL (二)消息设置 TTL (三)两者的区别 四、整合SpringBoot实现延迟队列 (一)创建项目 (二)添加依赖 (三)修改配置文件 (四)添加Sw

目录

一、概念

二、使用场景

三、RabbitMQ 中的 TTL

(一)队列设置 TTL

(二)消息设置 TTL

(三)两者的区别

四、整合SpringBoot实现延迟队列

(一)创建项目

(二)添加依赖

(三)修改配置文件

(四)添加Swagger配置类

五、队列TTL

(一)代码架构图

(二)配置文件类

(三)消息生产者

(四)消息消费者 

六、延迟队列优化

(一)代码架构图

(二)配置文件类

(三)消息生产者

七、Rabbitmq 插件实现延迟队列

(一)代码架构图

(二)配置文件类

(三)消息生产者

(四)消息消费者

八、总结


一、概念

        延时队列, 队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。

二、使用场景

  1. 订单在十分钟之内未支付则自动取消
  2. 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
  3. 用户注册成功后,如果三天内没有登陆则进行短信提醒。
  4. 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
  5. 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如: 发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎使用 定时任务 ,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗? 如果数据量比较少,确实可以这样做 ,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求,如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。 但对于数据量比较大,并且时效性较强的场景,如:“订单十分钟内未支付则关闭“,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。

 

 

三、RabbitMQ 中的 TTL

TTL RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有 消息的最大存活时间, 单位是毫秒。换句话说,如果一条消息设置了 TTL 属性或者进入了设置 TTL 属性的队列,那么这条消息如果在 TTL 设置的时间内没有被消费,则会成为 " 死信 " 。如果同时配置了队列的 TTL 和消息的TTL,那么较小的那个值将会被使用,有两种方式设置 TTL

(一)队列设置 TTL

第一种是在创建队列的时候设置队列的“x-message-ttl”属性
Map arguments = new HashMap<>();// 声明队列的TTLarguments.put("x-message-ttl", 10000);return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();

(二)消息设置 TTL

另一种方式便是针对每条消息设置 TTL

rabbitTemplate.convertAndSend("X", "XC", message, msg -> {    msg.getMessageProperties().setExpiration(ttl);    return msg;});

(三)两者的区别

如果设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃 ( 如果配置了死信队列被丢到死信队列中) ,而第二种方式,消息即使过期,也不一定会被马上丢弃,因为 消息是否过期是在即将投递到消费者 之前判定的 如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间 ;另外,还需要注意的一点是,如果不设置 TTL ,表示消息永远不会过期, 如果将 TTL 设置为 0,则表示除非此时可以直接投递该消息到消费者,否则该消息将会被丢弃。 前一小节我们介绍了死信队列,刚刚又介绍了 TTL ,至此利用 RabbitMQ 实现延时队列的两大要素已经集齐,接下来只需要将它们进行融合,再加入一点点调味料,延时队列就可以新鲜出炉了。想想看,延时队列,不就是想要消息延迟多久被处理吗,TTL 则刚好能让消息在延迟多久之后成为死信,另一方面,成为死信的消息都会被投递到死信队列里,这样只需要消费者一直消费死信队列里的消息就完事了,因为里面的消息都是希望被立即处理的消息。

四、整合SpringBoot实现延迟队列

(一)创建项目

 

         

(二)添加依赖

                    org.springframework.boot            spring-boot-starter                                    org.springframework.boot            spring-boot-starter-amqp                            org.springframework.boot            spring-boot-starter-WEB                            org.springframework.boot            spring-boot-starter-test            test                            com.alibaba            fastJSON            1.2.47                            org.projectlombok            lombok                                    io.springfox            springfox-swagger2            2.9.2                            io.springfox            springfox-swagger-ui            2.9.2                                    org.springframework.amqp            spring-rabbit-test            test        

(三)修改配置文件

spring.rabbitmq.host=192.168.23.100spring.rabbitmq.port=5672spring.rabbitmq.username=guestspring.rabbitmq.passWord=guest

(四)添加Swagger配置类

@Configuration@EnableSwagger2public class SwaggerConfig {    @Bean    public Docket webapiConfig() {        return new Docket(DocumentationType.SWAGGER_2)                .groupName("webapi")                .apiInfo(webApiInfo())                .select()                .build();    }    public ApiInfo webApiInfo() {        return new ApiInfoBuilder()                .title("rabbitmq 接口文档")                .description("本文档描述了 rabbitmq 微服务接口定义")                .version("1.0")                .contact(new Contact("enjoy6288", "Http://atguigu.com",                        "1551388580@qq.com"))                .build();    }}

五、队列TTL

(一)代码架构

        创建两个队列 QA QB ,两者队列 TTL 分别设置为 10S 40S ,然后在创建一个交换机 X 和死信交换机 Y ,它们的类型都是 direct ,创建一个死信队列 QD ,它们的绑定关系如下:

 

 (二)配置文件类

@Configurationpublic class TtlQueueConfig {    public static final String X_EXCHANGE = "X";    public static final String QUEUE_A = "QA";    public static final String QUEUE_B = "QB";    public static final String Y_DEAD_LETTER_EXCHANGE = "Y";    public static final String DEAD_LETTER_QUEUE = "QD";    // 声明xExchange    @Bean("xExchange")    public DirectExchange xExchange() {        return new DirectExchange(X_EXCHANGE);    }    // 声明yExchange    @Bean("yExchange")    public DirectExchange yExchange() {        return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);    }    // 声明队列A    @Bean("queueA")    public Queue queueA() {        Map arguments = new HashMap<>();        // 当前队列的死信交换机        arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);        // 当前队列的死信路由key        arguments.put("x-dead-letter-routing-key", "YD");        // 声明队列的TTL        arguments.put("x-message-ttl", 10000);        return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();    }    // 声明队列A绑定交换机X    @Bean    public Binding queueABindingX(@Qualifier("queueA") Queue queueA,      @Qualifier("xExchange")DirectExchange xExchange) {        return BindingBuilder.bind(queueA).to(xExchange).with("XA");    }    // 声明队列B    @Bean("queueB")    public Queue queueB() {        Map arguments = new HashMap<>();        arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);        // 当前队列的死信路由key        arguments.put("x-dead-letter-routing-key", "YD");        // 声明队列的TTL        arguments.put("x-message-ttl", 40000);        return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();    }    // 声明队列B绑定交换机X    @Bean    public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,      @Qualifier("xExchange")DirectExchange xExchange) {        return BindingBuilder.bind(queueB).to(xExchange).with("XB");    }    // 声明死信队列    @Bean("queueD")    public Queue queueD() {        return new Queue(DEAD_LETTER_QUEUE);    }    @Bean    // 声明死信队列 QD 绑定关系    public Binding queuedBindingY(@Qualifier("queueD")Queue queueD,      @Qualifier("yExchange")DirectExchange exchange) {        return BindingBuilder.bind(queueD).to(exchange).with("YD");    }}

(三)消息生产者

    @GetMapping("/sendMsg/{message}")    public void sendMsg(@PathVariable String message) {        log.info("当前时间是{},发送一条信息给两个 TTL 队列:{}", new Date().toString(), message);        rabbitTemplate.convertAndSend("X", "XA", "消息来自ttl为10s的队列" + message);        rabbitTemplate.convertAndSend("X", "XB", "消息来自ttl为40s的队列" + message);    }

(四)消息消费者 

@Component@Slf4jpublic class DeadLetterQueueConsumer {    @RabbitListener(queues = "QD")    public void receiveD(Message message, Channel channel) throws IOException {        String msg = new String(message.getBody());        log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);    }}

发起一个请求 http://localhost:8080/ttl/sendMsg/嘻嘻嘻 

 

 

第一条消息在 10S 后变成了死信消息,然后被消费者消费掉,第二条消息在 40S 之后变成了死信消息,然后被消费掉,这样一个延时队列就打造完成了。 不过,如果这样使用的话,岂不是 每增加一个新的时间需求,就要新增一个队列 ,这里只有 10S 40S 两个时间选项,如果需要一个小时后处理,那么就需要增加 TTL 为一个小时的队列,如果是预定会议室然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?

六、延迟队列优化

(一)代码架构图

在这里新增了一个队列 QC,绑定关系如下,该队列不设置 TTL 时间,而是由生产者设置过期时间

 

(二)配置文件类

@Configurationpublic class TtlQueueConfig {    public static final String X_EXCHANGE = "X";    public static final String QUEUE_A = "QA";    public static final String QUEUE_B = "QB";    public static final String Y_DEAD_LETTER_EXCHANGE = "Y";    public static final String DEAD_LETTER_QUEUE = "QD";    public static final String QUEUE_C = "QC";    // 声明xExchange    @Bean("xExchange")    public DirectExchange xExchange() {        return new DirectExchange(X_EXCHANGE);    }    // 声明yExchange    @Bean("yExchange")    public DirectExchange yExchange() {        return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);    }    // 声明队列A    @Bean("queueA")    public Queue queueA() {        Map arguments = new HashMap<>();        // 当前队列的死信交换机        arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);        // 当前队列的死信路由key        arguments.put("x-dead-letter-routing-key", "YD");        // 声明队列的TTL        arguments.put("x-message-ttl", 10000);        return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();    }    // 声明队列A绑定交换机X    @Bean    public Binding queueABindingX(@Qualifier("queueA") Queue queueA,      @Qualifier("xExchange")DirectExchange xExchange) {        return BindingBuilder.bind(queueA).to(xExchange).with("XA");    }    // 声明队列B    @Bean("queueB")    public Queue queueB() {        Map arguments = new HashMap<>();        arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);        // 当前队列的死信路由key        arguments.put("x-dead-letter-routing-key", "YD");        // 声明队列的TTL        arguments.put("x-message-ttl", 40000);        return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();    }    // 声明队列B绑定交换机X    @Bean    public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,      @Qualifier("xExchange")DirectExchange xExchange) {        return BindingBuilder.bind(queueB).to(xExchange).with("XB");    }    // 声明队列C    @Bean("queueC")    public Queue queueC() {        Map arguments = new HashMap<>();        arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);        // 当前队列的死信路由key        arguments.put("x-dead-letter-routing-key", "YD");        return QueueBuilder.durable(QUEUE_C).withArguments(arguments).build();    }    // 声明队列C绑定交换机X    @Bean    public Binding queueCBindingX(@Qualifier("queueC") Queue queueC,      @Qualifier("xExchange")DirectExchange xExchange) {        return BindingBuilder.bind(queueC).to(xExchange).with("XC");    }    // 声明死信队列    @Bean("queueD")    public Queue queueD() {        return new Queue(DEAD_LETTER_QUEUE);    }    @Bean    // 声明死信队列 QD 绑定关系    public Binding queuedBindingY(@Qualifier("queueD")Queue queueD,      @Qualifier("yExchange")DirectExchange exchange) {        return BindingBuilder.bind(queueD).to(exchange).with("YD");    }}

(三)消息生产者

    @GetMapping("/sendExpirationMsg/{message}/{ttl}")    public void sendMsg(@PathVariable String message, @PathVariable String ttl) {        log.info("当前时间是{},发送一条过期信息给两个 TTL 队列:{}", new Date().toString(), message);        rabbitTemplate.convertAndSend("X", "XC", message, msg -> {            msg.getMessageProperties().setExpiration(ttl);            return msg;        });    }

发起请求 http://localhost:8080/ttl/sendExpirationMsg/ 你好 1/20000 http://localhost:8080/ttl/sendExpirationMsg/ 你好 2/2000

看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用在消息属性上设置 TTL 的方式,消息可能并不会按时“死亡“,因为 RabbitMQ 只会检查第一个消息是否过期 ,如果过期则丢到死信队列, 如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行

 

七、Rabbitmq 插件实现延迟队列

关于插件的安装可以查看这篇文章Docker安装RabbitMq延迟队列插件

 

(一)代码架构图

在这里新增了一个队列 delayed.queue,一个自定义交换机 delayed.exchange,绑定关系如下:  

 

(二)配置文件类

在我们自定义的交换机中,这是一种新的交换类型,该类型消息支持延迟投递机制 消息传递后并 不会立即投递到目标队列中,而是存储在 mnesia( 一个分布式数据系统 ) 表中,当达到投递时间时,才投递到目标队列中。

(三)消息生产者

@Configurationpublic class DelayedQueueConfig {    public static final String DELAYED_QUEUE_NAME = "delayed.queue";    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";    public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";    // 声明队列    @Bean    public Queue delayedQueue() {        return new Queue(DELAYED_QUEUE_NAME);    }    // 声明自定义交换机    @Bean    public CustomExchange delayedExchange() {        Map args = new HashMap<>();        args.put("x-delayed-type", "direct");        return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, args);    }    // 声明队列和延迟交换机的绑定    @Bean    public Binding bindingDelayedQueue(@Qualifier("delayedQueue")Queue delayedQueue,           @Qualifier("delayedExchange")CustomExchange exchange) {        return BindingBuilder.bind(delayedQueue).to(exchange).with(DELAYED_ROUTING_KEY).noargs();    }}

(四)消息消费者

@Component@Slf4jpublic class DelayedQueueConsumer {    @RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)    public void receiveDelayedQueue(String message) {        log.info("当前时间:{}, 接收到消息: {}", new Date().toString(), message);    }}
发起请求: http://localhost:8080/ttl/sendDelayMsg/come on baby1/20000 http://localhost:8080/ttl/sendDelayMsg/come on baby2/2000

第二个消息被先消费掉了,符合预期

八、总结

延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用 RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。 当然,延时队列还有很多其它选择,比如利用 Java DelayQueue,利用 Redis zset,利用 Quartz或者利用 kafka 的时间轮,这些方式各有特点 , 看需要适用的场景。

来源地址:https://blog.csdn.net/m0_62946761/article/details/129245805

--结束END--

本文标题: RabbitMQ延迟队列

本文链接: https://lsjlt.com/news/388601.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • RabbitMQ延迟队列
    目录 一、概念 二、使用场景 三、RabbitMQ 中的 TTL (一)队列设置 TTL (二)消息设置 TTL (三)两者的区别 四、整合SpringBoot实现延迟队列 (一)创建项目 (二)添加依赖 (三)修改配置文件 (四)添加Sw...
    99+
    2023-09-01
    rabbitmq 分布式 java-rabbitmq 后端 java
  • SpringBoot整合RabbitMQ处理死信队列和延迟队列
    目录简介实例代码路由配置控制器发送器接收器application.yml实例测试简介 说明 本文用示例介绍SpringBoot整合RabbitMQ时如何处理死信队列/延迟队列。 Ra...
    99+
    2024-04-02
  • RabbitMQ中死信队列和延迟队列如何使用
    这篇文章主要讲解了“RabbitMQ中死信队列和延迟队列如何使用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“RabbitMQ中死信队列和延迟队列如何使用”吧!死信队列简介DLX,全称为De...
    99+
    2023-06-30
  • SpringBoot怎么整合RabbitMQ处理死信队列和延迟队列
    今天小编给大家分享一下SpringBoot怎么整合RabbitMQ处理死信队列和延迟队列的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了...
    99+
    2023-06-30
  • 手把手带你掌握SpringBoot RabbitMQ延迟队列
    目录1. 简介2. 安装插件3. 实现延迟队列3.1 引入所需依赖3.2 application.yaml3.3 RabbitConfig3.4 Producer3.5 Consum...
    99+
    2024-04-02
  • RabbitMQ消息队列实现延迟任务示例
    目录一、序言1、实现原理2、组件选型二、方案设计(一)服务器(二)生产者(三)消费者三、SpringBoot实现(一)生产者(二)消费者(三)通用工具包一、序言 延迟任务应用广泛,延...
    99+
    2024-04-02
  • RabbitMQ消息队列怎么实现延迟任务
    这篇文章主要介绍“RabbitMQ消息队列怎么实现延迟任务”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“RabbitMQ消息队列怎么实现延迟任务”文章能帮助大家解决问题。一、序言延迟任务应用广泛,延...
    99+
    2023-06-29
  • 详解RabbitMQ中死信队列和延迟队列的使用详解
    目录简介死信队列简介示例延迟队列简介使用场景简介 本文介绍RabbitMQ的死信队列和延迟队列。 本内容也是Java后端面试中常见的问题。 死信队列 简介 DLX,全称为Dead-L...
    99+
    2024-04-02
  • RabbitMQ实现延迟队列的两种方式详解
    目录1. 用插件1.1 安装插件1.2 消息收发2. DLX 实现延迟队列2.1 延迟队列实现思路2.2 案例3. 小结定时任务各种各样,常见的定时任务例如日志备份,我们可能在每天凌...
    99+
    2024-04-02
  • RabbitMQ死信机制实现延迟队列的实战
    目录延迟队列应用场景TimeToLive(TTL) DeadLetterExchanges(DLX)延迟队列 延迟队列存储的对象肯定是对应的延时消息,所谓”延时消息”是指当...
    99+
    2024-04-02
  • Redis延迟队列和分布式延迟队列的简答实现
            最近,又重新学习了下Redis,Redis不仅能快还能慢,简直利器,今天就为大家介绍一下Redi...
    99+
    2024-04-02
  • .Net实现延迟队列
    目录介绍使用场景方案Redis过期事件配置控制台订阅WebApi中订阅RabbitMq延迟队列生产消息消费消息其他方案介绍 具有队列的特性,再给它附加一个延迟消费队列消息的功能,也就...
    99+
    2024-04-02
  • ThinkPHP6 think-queue 消息队列(延迟队列)
    安装 composer require topthink/think-queue 配置 配置文件位于 config/queue.php [ 'default'=>'sync' //...
    99+
    2023-09-04
    php 数据库 mysql
  • SpringBoot整合RabbitMQ实现延迟队列的示例详解
    目录如何保证消息不丢失什么是消息投递可靠性ttl死信队列什么是死信队列消息有哪几种情况成为死信延迟队列springboot整合rabbitmq实现订单超时自动关闭如何保证消息不丢失 ...
    99+
    2023-05-16
    SpringBoot RabbitMQ实现延迟队列 SpringBoot RabbitMQ延迟队列 SpringBoot RabbitMQ队列 SpringBoot RabbitMQ
  • 详解RabbitMQ延迟队列的基本使用和优化
    目录1.延迟队列基本介绍2.延迟队列使用场景3.Spring Boot集成RabbitMQ3.1创建项目,引入依赖3.2application.properties配置文件3.3 队...
    99+
    2023-05-19
    RabbitMQ 延迟队列 RabbitMQ 队列
  • Spring Boot与RabbitMQ结合实现延迟队列的示例
    背景何为延迟队列?顾名思义,延迟队列就是进入该队列的消息会被延迟消费的队列。而一般的队列,消息一旦入队了之后就会被消费者马上消费。场景一:在订单系统中,一个用户下单之后通常有30分钟的时间进行支付,如果30分钟之内没有支付成功,那么这个订单...
    99+
    2023-05-30
    spring boot rabbitmq
  • 怎么在Redis中实现延迟队列和分布式延迟队列
    这篇文章给大家介绍怎么在Redis中实现延迟队列和分布式延迟队列,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。1. 实现一个简单的延迟队列。  我们知道目前JAVA可以有DelayedQueue,我们首先开一个Dela...
    99+
    2023-06-15
  • springcloud中RabbitMQ死信队列与延迟交换机实现方法
    目录0.引言1. 死信队列1.2 什么是死信?1.3 什么是死信队列?1.4 创建死信交换机、死信队列1.5 实现死信消息1.5.1 基于消费者进行reject或nack实现死信消息...
    99+
    2024-04-02
  • RabbitMQ实现延迟队列的两种方式分别是什么
    这期内容当中小编将会给大家带来有关RabbitMQ实现延迟队列的两种方式分别是什么,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。定时任务各种各样,常见的定时任务例如日志备份,我们可能在每天凌晨 3 点去备...
    99+
    2023-06-22
  • 如何实现Redis延迟队列
    这期内容当中小编将会给大家带来有关如何实现Redis延迟队列,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。延迟队列,顾名思义它是一种带有延迟功能的消息队列。那么,是在什么...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作