rabbitMQ学习记录

tmq需要完成的任务是什么?

tmq是公司自主开发的一个消息队列,用于支持线上各个需要进行异步通信/流量削峰等场景的业务线,其本身是对rabbitMQ做了一次二次开发,封装了底层的消息队列的操作

为什么选择rabbitMQ做二次开发,直接使用不可以吗?

rabbitMQ是一款开源的高吞吐量的AMQP协议的实例,社区活跃,支持可持久化消息,消息发送机制灵活,支持多种消息路由模式,有消息确认机制提高了消息的送达率,重事务,基本能满足我们对消息队列的常规要求.而不直接使用rabbitMQ是因为想做一个基础服务平台对上层业务提供统一的服务入口,这样利于消息队列这种中间件的维护,同时我们基于rabbitMQ对外封装了两种通信方式,一种是基于应用层的HTTP协议,一种是基于MYSQL的通信方式,业务根据自身需求选择,同时还对数据格式使用了不同序列化方式,有我们内部自定义的MC_PACK协议(一种内部使用的二进制协议,用于说明body长度,magic校验,命令序号,校验和,版本号等)和普通的json格式串,通过这种封装对业务更加友好.

如何保证一个MQ系统的可靠性?

个人针对MQ的可靠性理解可以分为三个方面:
1.消息安全
这里的安全并不讨论信息安全,而是说消息的从producer披荆斩棘最终能完整的送到consumer完成消费,具体可以分为消息丢失问题/消息重复问题,关于这两个问题,在MQ系统中可以从三个方面着手
1.producer
从producer角度,它所能做的就是尽量将消息安全送达mq-server上,具体拿rabbitMQ来说,就是安全的送到指定的exchange上,一般的技术原理有事务机制/消息confirm机制,具体请自行搜索,或看本章相关问答^_^
2.mq-server
mq-server是整个消息的调度中心,负责解耦,送达消息,拥有异步,流量削峰等技能,,,扯远了,,, mq-server本身针对消息的安全性能做的更多,还是说rabiitMQ的例子:持久化/镜像队列/消息确认机制/事务机制等等
3.consumer
consumer可以通过消息确认机制尽量保证.另外,三者在使用上务必保证正常退出/异常crash都要做退出处理,个人经验,,,例如在服务中碰到的异常较高的是SIGSEGV信号,这个一般try catch机制无法捕获,我们可以用信号机制设置callback完成优雅退出,这个我觉得是MQ开发者应该保证的机制而不是rabbitMQ设计架构
通过上述操作,一般一个MQ系统才会变得比较靠谱,才不会被人喷~~~
2.服务可用性
服务可用性在这里指的是整个MQ系统的可用性,分两个角度:
1.producer/consumer:
首先这个无关乎MQ,而是一种集群方案
2.mq-server:
自己可以通过一些高可用方案实现,具体请看rabbitMQ集群方案
3.服务性能

一方面从单一节点上,合理的配置规则,例如使用confirm机制代替事务机制,消息系统不做日志重做

另一方面需要使用HA/分布式方案,具体的请看rabbitMQ集群方案
4.消息的顺序保证:



如何保证消息的顺序发送和消息的重复发送问题规避?

假设有两个消息M1,M2分别需要通过rabbitMQ发送给consumer,但是M1,M2是要求有先后消费顺序的,即M1先被消费,M2后被消费,我们的思路可能是这样子的:


简单的说就是严格保证producer->MQ-server->consumer的一对一,但是其中会有一些诟病的问题:
1.吞吐量下降.任务串行了
2.更多的异常处理.一旦consumer出问题了整个流程阻塞
上述设计流程是可以规避顺序消费问题,但是可能会出现重复消费的问题,如何解决呢,这个需要consumer来做保证,例如可以使用消息的唯一id和去重日志进行对比,这个可以解决重复消费的问题,但是因为重做日志的事情会影响性能同时在规避消息重复消费的时候针对consumer有两种方式:
1.auto_ack=false
2.consumer确保能捕获程序中的异常保证其除了不可抗力因素外都基本能保证正常退出

consumer如何正常退出?

consumer只要来得及对已发送的消息进行ACK就可以了,所以一般不使用kill -9 保证注册了可控制的信号,然后让它优雅退出,例如在tmq项目中,注册了usr1,然后在回调函数中进行了处理,主要是互斥置全局标识is_run,让类似
while(is_run)的东西正常退出然后return,或exit(0).我们这里做了正常的处理,保证把当前接到的消息都安全的处理完之后不再拉取消息,然后退出,让守护进程自动重启,从而达到优雅重启的目的

rabbitMQ消息确认机制

rabbitMQ通过两种方式实现消息的确认:
1.通过AMQP事务实现
有三个重要的方法:txSelect():将当前的channel设置成transaction, txCommit():提交事务, txRollback():回滚事务.事务保障producer->broker数据能送达,如果在txCommit之后发生异常说明消息已经到达了borker,需要进行回滚的同时可以进行重试操作.但是事务会降低rabbitMQ的性能
2.通过channel设置成confirm模式实现
producer将channel设置成confirm模式,所有在该channel上发布的message都会被指派一个唯一的ID,当消息发送到所有匹配的队列或开启了消息持久化后消息已经落盘之后borker会给producer发送一个ack,这种模式也支持批量处理,批量确认.
confirm模式优势是异步的,在rabbitMQ发送确认消息后producer可以通过回调接口进行确认消息的处理,即使是非ack的消息也可以在这个回调接口中处理
开启方法:通过confirmSelect()
一般producer有三种confirm方式:
1.普通的confirm:消息会被串行的处理
2.批量的confirm:消息批量发送,批量确认,还是同步的方式
3.异步的confirm:通过通知方式处理消息确认

性能测试:


在consumer端也需要进行消息的确认,所以rabbitMQ可以在队列中有一个noAck选项,可以选择ack,默认是选择,当选择ack模式后rabbitMQ会等待consumer显示发回ack信号后才从队列中删除消息,这种方式的优势就是能尽可能的保障消息不因为consumer异常导致消息丢失掉,但是有一个比较大的隐患就是可能会出现重复消费的问题,例如有两个consumer(为了高并发消费)A,B,A先消费了,然后A在发送ack之前挂掉了,那B就会重新消费,就会出现重复消费的问题,这里思考个问题:rabbitMQ是不是需要对它持有的消息做一个投递和未投递的分类呢?答案肯定是是的.
消息在rabbitMQ内部分为两种:
1.等待投递给consumer的消息
2.等待consumer发送ack的消息
如果consumer断开连接,rabbitMQ会安排该消息重新进入队列,等待投递给下一个consumer(也可能是原来的consumer,总之会重新消费)
需要注意的是,rabbitMQ并不会为ack设置超时时间,所以rabbitMQ判断消息是否需要重新投递给consumer重新消费的唯一依据就是与等待发送ack的consumer之间的连接是否断开,我个人认为这个是比较大的一个缺点,因为网络不稳定是不可忽略的一个因素
所以此处的问题规避请看上边关于消息重复发送规避的问答.
查看上述两种队列的命令是:rabbitmqctl list_queues name messages_ready messages_unacknowledged

rabbitMQ消息持久化问题

背景:在rabbitMQ退出/异常情况下数据不丢失,需要将queue,exchange和message都进行一个持久化操作,下边具体说说操作和原理:
exchange持久化操作:
因为已经持久化的消息不再需要经过exchange了,所以在rabbitMQ重启之后exchange对持久化的而消息没有影响,但是还是建议尽量对exchange进行持久化因为后续的producer操作需要知道exchange,否则无法发送到预期的queue上,具体操作:
channel.exchangeDeclare(exchangeName, “direct/topic/header/fanout”, true);即在声明的时候讲durable字段设置为true即可
exchange持久化原理:

queue持久化操作:
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("queue.persistent.name", true, false, false, null);
关键的是第二个参数设置为true,即durable=true
Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
Map<String, Object> arguments) throws IOException;
参数说明:

queue:queue的名称

exclusive:排他队列,如果一个队列被声明为排他队列,该队列仅对首次申明它的连接可见,并在连接断开时自动删除。这里需要注意三点:

1. 排他队列是基于连接可见的,同一连接的不同信道是可以同时访问同一连接创建的排他队列;

2.“首次”,如果一个连接已经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同;

3.即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除的,这种队列适用于一个客户端发送读取消息的应用场景。


autoDelete:自动删除,如果该队列没有任何订阅的消费者的话,该队列会被自动删除。这种队列适用于临时队列。
queue持久化原理:

message持久化操作:
void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException;
exchange表示exchange的名称
routingKey表示routingKey的名称
body代表发送的消息体
channel.basicPublish("exchange.persistent", "persistent", MessageProperties.PERSISTENT_TEXT_PLAIN, "persistent_test_message".getBytes());
message持久化原理:

针对消息的确认机制需要注意进一步保证消息不的安全(丢失/重复)?

消息持久化操作无论怎么牛逼操作都不能100%保证消息不丢失,还有重要环境就是在consumer上对消息的确认策略的设置,一般rabbitMQ默认会开启autoack=false,在consumer端也有autoack,但默认是true,我们需要关闭它,然后在消息发送到下游服务后在代码中显示ack回复rabbitMQ,但即使这样也不能一定保证消息不会丢失,例如已经发送了ack到rabbitMQ,但是rabbitMQ没来的及处理就挂了或者在持久化中没来的及fsync就挂了,前者可能造成重复消费(当然还有consumer的缘故也会导致重复消费,具体的看重复消费规避那个问答),后者可能造成消息丢失,我们这里讨论rabbitMQy应该做的事情,rabbitMQ为了保证消息的安全,有一种方案是可以做队列的镜像,还有,在producer引入事务机制或者confirm机制(推荐)确保消息已经发送到broker上了,

rabbitMQ消息发送机制?

在rabbitMQ的消息发送机制上,如果是使用pull模式的话(我们用的是这种),rabbitMQ大致发送是这样的:轮询发送到每个consumer,基本确保每个consumer的消息数是在一个数量级的

rabbitMQ怎么尽可能公平的调度?

问题:在rabbitMQ的轮询发送机制上,基本能确保消息的数量上在每个consumer是均衡的,但是并没有保证每个消息的耗时,这个也似乎没发保证,那么就出现了consumer负载不均衡的现象:
所以就需要一种公平调度的策略.
一种方案是在分配任务时可以优先考虑分配给空闲consumer,具体的操作配置:
channel.basic_qos(prefetch_count=1) # 同一时刻,不要发送超过一条消息给一个消费者

rabbitMQ集群方案?

rabbitMQ的HA集群方案中根据不同的HA policy达到分布式的目的,具体分析如下:
部署方式:rabbitMQ可通过三种方式部署分布式系统
cluster
不支持跨网段,用于同一个网段内的LAN
可以随意动态改动集群中的节点
节点之间必须运行相同版本的rabbitMQ和Erlang环境
federation
应用于WAN,允许单台节点上的交换机或队列接收发布到另一个节点上的交换机或队列,直到被consuer消费,一般在internet上作为中间服务器,作为订阅分发消息
shovel
节点的类型:
RAM node:内存存储队列,交换机,绑定,用户,权限,和vhost的元数据
Disk node:RAM内的数据搬到磁盘,便于恢复
一般在rabbitMQ中,如果是单个节点的话,强制要求是Disck node,如果是多于一个node的cluster模式,会强制要求至少一个Disk node,当节点加入或离开cluster时,需要通知至少一个Disk node,如果集群中唯一的一个Disk node挂掉了,整个集群就不能进行增/删/改操作了,但是可以运行,所以一般的解决方案是:
设置两个Disk node
在我们的项目tmq中使用的是cluster模式,节点有两个,都是Disk node
Erlang Cookie:
作用是node互相通信的密钥, 所以务必保证每个node上的Erlang Cookie相同, 具体一般保存在 /var/lib/rabbitmq/.erlang.cookie
我们tmq服务部署的Erlang Cookie在: ~/.erlang.cookie
不过诟病的rabbitMQ节点间识别有点恶心,通过hostname识别的,一旦系统环境不小心改动了rabbitMQ就无法重新建立集群关系了,会形成分区,一般在修复环境后是需要重启rabbitMQ然后重新配置
集群关系,,不过这块还没有太研究清楚,可能还有更好的方案,不过实践的效果是这个样子的,确实需要重启
镜像队列:
具体我们说说cluster的模式:
普通模式:
举个例子(rabbitmq01, rabbitmq02),对于一个消息实体,一般只有一份,rabbitmq01(或者rabbitmq02),两个节点相同的队列结构,当消息进入到rabbitmq01的queue后,consumer从rabbitmq02消费的时候,rabbitMQ会临时在rabbit01,rabbit02之间进行消息传输,这会有一些隐患,主要是性能上,在消息量大的时候这种中间传输会加重网络IO,所以一般的解决方案是针对同一个逻辑队列,和多个甚至每一个节点建立连接
镜像模式:
这种模式其实真的实现了HA方案,具体有不同的策略:
镜像队列的添加通过添加policy: rabbitmqctl set_policy [-p Vhost] Name Pattern Definition [Priority]
-p Vhost: 指定vhost下的queue
Name: policy的名称
pattern: queue的匹配模式(正则表达式)
Definition:镜像定义,具体如下:
ha-mode
ha-params
功能
all
-
在整个集群节点中复制,当一个新的节点加入后也会在这个节点上进行复制
exactly
count
在集群上复制count份,如果集群节点数少于count,会在所有节点上复制,如果大于count,集群中有一个节点crash掉了,新加入的也不会被复制消息
nodes
node-name
在node name指定的节点复制,如果该名称不是集群中的一个,不触发错误,如果该Node list节点都没有在线,这个queue会被声明在client现在连接的节点上
priority: policy的优先级
ha-sync-mode:镜像队列中的消息同步方式: automatic, manually
镜像队列的实现原理参考: my.oschina.net/hncscwc/blo…
具体的配置参考: blog.csdn.net/liaomin4161…

镜像队列问题梳理:

1.在镜像队列模式中,假如有rabbitMQ01/rabbitMQ02两个节点,那么如果consumer同时和这两个建立了连接,会出现重复消费吗?
queue和message虽然会存在所有镜像队列中,但客户端读取时不论物理面连接的主节点还是从节点,都是从主节点读取数据,然后主节点再将queue和message的状态同步给从节点,因此多个客户端连接不同的镜像队列不会产生同一message被多次接受的情况。
普通队列问题梳理:
2.在普通队列模式中,如何更高效的进行消息投递,例如消息在rabbitMQ01,我从rabbitMQ02进行pull,这种情况就需要一次消息传递,怎么优化?
比较妥的做法是和多个物理节点连接

镜像队列的特点:

1. 镜像队列是基于普通的集群模式的,所以你还是得先配置普通集群,然后才能设置镜像队列。
2. 镜像队列可以同步queue和message,当主queue挂掉,从queue中会有一个变为主queue来接替工作。
3. 镜像队列设置后,会分一个主节点和多个从节点,如果主节点宕机,从节点会有一个选为主节点,原先的主节点起来后会变为从节点。

关于队列的误区澄清:

1.master和slave是针对queue而言,node节点不存在master和slave一说,只是说queue创建在哪台node上则这台node则为这个queue的master.
2.queue有master 节点和slave节点。 但是要说明的是,在rabbitmq中master和slave是针对一个queue而言的,而不是一个node作为所有queue的master,其它node作为slave。 一个queue第一次创建的node为它的master节点,其它node为slave节点

关于队列的读取过程:

在镜像队列(Mirrored Queue)中,只有master的copy对外提供服务,而其他slave copy只提供备份服务,在master copy所在节点不可用时,选出一个slave copy作为新的master继续对外提供服务。
无论客户端的请求打到master还是slave最终数据都是从master节点获取。
当请求打到master节点时,master节点直接将消息返回给client,同时master节点会通过GM(Guaranteed Multicast)协议将queue的最新状态广播到slave节点。GM保证了广播消息的原子性,要么都更新要么都不更新
当请求打到slave节点时,slave节点需要将请求先重定向到master节点,master节点将将消息返回给client,同时master节点会通过GM协议将queue的最新状态广播到slave节点。
所以,如果master分布不均匀的话,所有节点的负载就不会均匀

关于队列的同步过程和队列的一些注意事项

: www.opsdev.cn/post/rabbit…

关于rabbitMQ的重要指标都有哪些,该怎么看?

直接参考这个:

什么场景下会考虑使用消息中间件?

对MQ的认识:一发一存一消费
消息中间件一般是比较高效可靠的消息传输机制实现数据通信的异步操作,服务解耦,流量削峰,数据同步等.

在MQ系统选型的时候怎么考虑?

功能维度:
优先级队列:
消息不再是先进先出的规则,而是根据一定的策略得出的优先级进行排队处理,那这种情况一般在broker中有较多的消息的时候有比较明显的效果,对于小量级的没有什么太大差别.
延迟队列:
分为两种:
1.基于消息的延迟:每当队列中有新消息进入,会重新根据延迟时间排序,所以带来的有性能影响
2.基于队列的延迟:可以设置不同级别的延迟,例如5s,10s,等等,每个队列中的消息延迟时间是相同的,所以可以通过避免排序,用一个定时器就可以达到效果且不损伤性能
死信队列:
当消息没有正确的被consumer消费的时候,MQ系统为了确保消息不会被无辜丢弃,为这种遗弃的消息专门维护一个叫做死信的队列,与此对应的还有"回退队列"(当consumer异常时频繁导致频繁的将一个消息在处理和回滚之间流动就需要)
重试队列:
消息消费失败会放在这里,且重试随着次数的增加延迟会会越来越大,但是重试次数有上线限制,超过限制还未成功就会进入死信队列.
与死信队列相同的地方:
都需要设置延迟界别
与延迟队列不同的地方:
重试队列动作由consumer触发,延迟队列作用一次重试队列作用范围会传递
消费模式:
push:broker主动推送给consumer
优势:实时性好
不足:会有消息风暴的风险把borker和consumer压垮
适用场景:消息体量小
pull:consumer主动去broker拉取消息
优势:不需特别进行流量控制
不足:实时性稍差
适用场景:体量大
广播消费:
消息一般有两种传递模式,P2P,pub/sub rabbitMQ可通过exchange的类型实现不同的消费模式
消息回溯:
为了提高对消息丢失的重发能力
消息堆积+持久化:
支持消息堆积能力可满足业务的流量削峰.也可认为是一种冗余存储.分类一般有两种:
内存式堆积
磁盘式堆积
rabbitMQ是内存式堆积,但如果触发幻夜的话内存消息也会落盘,或者队形队列直接持久化
消息追踪
消息过滤
多租户:
多个用户同时使用同一套消息系统保证数据的隔离性
rabbitMQ支持多租户,其中的vhost就是这个概念,本质上是一个MQ-server,有一套自己的独立空间,避免了多用户的队列交换机,配置等冲突问题
多协议:
producer需要明确如何构造消息,consumer需要明确如何解析消息,所以消息一定是有消息协议确定格式的否则无意义,在消息系统一般有自己的协议或支持多个协议,例如rabbitMQ支持AMQP, MQTT
跨语言:
一般跨语言特性能有效的基于其进行扩展做二次开发,所以支持多语言的客户端是使其被更多开发团队接收所必须的一个特性
流量控制:
进行流量控制的作用是想均衡发送和消费的能力,一般的方法有滑动窗口, 令牌桶,漏桶算法等,rabbitMQ的流量控制机制可以参考: www.pandan.xyz/2017/03/18/…
消息顺序
安全性
消息幂等
事务消息
性能
可靠性和可用性
运维管理
社区力度和周边工具
开源还是自研
团队技术栈
生态:
参考因素应该是:生态>性能>功能

rabbitMQ消息可靠性怎么保证的?

个人认为,可靠性是一个相对性的概念,在合理范围内系统所能保证的多少个9的可靠性.
内容转载自:
先简单了解下消息流程:


a.msg从producer出发到达exchange
b.exchange根据路由规则将消息转发对应的queue之上
c.存储msg到queue等待消费
d.consumer pull消息
分别看下四个阶段可能的问题以及解决方案:
a producer->exchange:
网络丢包,故障
解决方案:
事务机制//不推荐,性能不太好
相关的函数:
channel.txSelect:将当前的信道设置成事务模式
channel.txCommit:提交事务
channel.txRollback:回滚事务
如果rabbitMQ本身发生异常了,我们就调用回滚
消息confirm机制:(推荐)
1.consumer 设置channel为confirm模式,消息都会有全局ID,消息投递给符合绑定规则(exchange的类型和routing key)的queue后,rabbitMQ会给producer发送一个BasicAck.producer就知道该消息已经安全入队了,该确认机制也支持流水线:设置multiple参数就行在channel.basicAck
事务的缺陷是消息从producer发送出去是阻塞的等待MQ的响应的,而confirm机制是异步的,producer收到确认后会有个回调接口处理确认的消息.如果rabbitMQ内部出错会发送BasicNack给producer,producer就可以在自己的回调接口中处理这种情况了,一般是重试
producer在调用channel.confirmSelect后rabbitMQ会返回ConfirmSelect-ok命令,后续所有消息都对应一个ack/nack消息,该消息的确认不保证快慢
2.exchange设置mandatory参数,会将无法投递的消息返回给producer,但是这回增加producer的逻辑复杂度,所以一般更好的解决方法就是使用备份exchange,在声明exchange的时候添加alternate-exchange实现



3.queue设置持久化+消息持久化,不过刷盘有延迟时间,但是磁盘故障无法避免,还是会失败,所以使用镜像队列做备份,master挂掉了,可以实现自动切换,生产环境一般都会配置

4.consumer设置autoAck=false,让消息在消费失败的情况下重新进行投递,重发队列也好,但是为了避免死循环,就是一直消费不成功,可以直接使用死信队列,便于定位


关于集群搭建:

Erlang:
rabbitMQ:

启动RabbitMQ
rabbitmq-server -detached 或者start

RabbitMQ-Management安装
rabbitmq-plugins enable rabbitmq_management

新增账户
rabbitmqctl add_user mq 123456
rabbitmqctl set_user_tags mq administrator

镜像集群搭建:通过命令行:
Step1:集群节点(对等)通信---erlang Cookie
erlang分布式的每个节点上要保持相同的.erlang.cookie文件,文件路径:~/.erlang.cookie
Step2:将rabbit2加入到rabbit1(RAM节点,默认Disk节点)
PS:若希望修改节点类型,则(需要先Stop)
rabbitmqctl stop_app
rabbitmqctl change_cluster_node_type ram
rabbitmqctl start_app




rabbitMQ流量控制: segmentfault.com/a/119000001…



常用命令:
rabbitmq-server -detached 启动RabbitMQ节点
rabbitmqctl start_app 启动RabbitMQ应用,而不是节点
rabbitmqctl stop_app 停止
rabbitmqctl status 查看状态
rabbitmqctl add_user mq 123456
rabbitmqctl set_user_tags mq administrator 新增账户
rabbitmq-plugins enable rabbitmq_management 启用RabbitMQ_Management
rabbitmqctl cluster_status 集群状态
rabbitmqctl forget_cluster_node rabbit@rabbit3 节点摘除
rabbitmqctl reset application重置

用户管理:
新建用户:rabbitmqctl add_user xxxpwd

删除用户: rabbitmqctl delete_user xxx

改密码: rabbimqctlchange_password {username} {newpassword}

设置用户角色:rabbitmqctlset_user_tags {username} {tag ...}

Tag可以为 administrator,monitoring, management
其它常用命令:





///tmq压力测试分析///
条件:

服务名称
节点数
单节点资源
是否混部
配置属性
producer
4
mem:15GB
disk:200GB SSD
producer:consumer = 1:1

rabbitMQ
2
mem:15GB
disk:200GB SSD
配置内存限制:5.8GB
每个节点分别有一个producer和一个consumer
镜像队列模式
同步方案:ha-all
exchange/queue/msg 持久化开启
confirm开启
disk node模式
consumer
4
mem:15GB
disk:200GB SSD
producer:consumer = 1:1



测试结果:
QPS:2400



注意的问题:
rabbitMQ在更改hsotname时会影响两个方面:
1.外部服务无法正常访问
需要重启让rabbitMQ
解决方案参考: blog.csdn.net/Boycy/artic…
2.集群节点通信故障


关于集群的指南: www.rabbitmq.com/clustering.…


另:遇到问题/调研看
详细的rabbitMQ基本说明,集群,分布式,HA还是要看官方文档: www.rabbitmq.com/distributed…







转载于:https://juejin.im/post/5bf6502bf265da616a474c10

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值