Kafka

分布式消息中间件 - Kafka

1.认识kafka

Kafka 是一个优秀的分布式消息中间件,许多系统中都会使用到 Kafka 来做消息通信,主要用来处理大量数据状态下的消息队列。

2. 消息队列

分布式消息是一种通信机制,和 RPC、HTTP、RMI 等不一样,消息中间件采用分布式中间代理的方式进行通信。上游业务系统发消息先储存在消息中间件,然后由消息中间件将消息分发到对应的业务模块应用

2.1 为什么使用消息队列

  • 解耦合
  • 冗余
  • 扩展性
  • 灵活性,峰值处理能力
  • 可恢复性
  • 顺序保证
  • 缓冲异步通信

解耦合: 在以往的分布式系统中,业务系统之间通过rpc等方式调用这种方式加大了服务之前的耦合性,使用消息队列可以数据先储存在消息中间件内部,消费着即使挂掉了等恢复后也可以继续消费数据。
冗余: 内部数据有多份(备份数据),即使有某台机器出现故障,也可在其他节点恢复数据
扩展性: 可以根据业务系统的负载情况增加分区,增强处理能力
峰值处理能力: 高流量的时候,使用消息队列作为中间件可以将流量的高峰保存在消息队列中,从而防止了系统的高请求,减轻服务器的请求处理压力。
可恢复性: 当集群某台机器挂掉了,其他服务器内的副本仍可提供服务,当服务器恢复的时候又可以继续提供服务
顺序保证: 同一分区内数据顺序可以保证的
缓冲异步通信: 异步处理不需要让流程走完就返回结果,可以将消息发送到消息队列中,然后返回结果,剩下让其他业务处理接口从消息队列中拉取消费处理即可

2.2 消费模式

2.2.1 点对点模式(一对一)

消费者主动拉取数据,消费后数据清除。Queue支持多个消费者,但对于一条消息而言,只有一个消费者可以消费,即一条消息只能被一个消费者消费
在这里插入图片描述

2.2.2 发布/订阅模式(一对多)

数据生产之后推送给所有订阅者。即利用Topic存储消息,消息生产者将消息发布到Topic中,同时有多个消费者订阅此topic,消费者可以从中消费消息,注意发布到Topic中的消息会被多个消费者消费,消费者消费数据之后,数据不会被清除,Kafka会默认保留一段时间,然后再删除。
在这里插入图片描述

3. Kafka架构图

在这里插入图片描述

  • Producer: 消息生产者,向Kafka中发布消息的角色
  • Consumer: 消息消费者,即从Kafka中拉取消息消费的客户端。
  • Topic: 主题,可以理解为一个队列,生产者和消费者都是面向一个Topic
  • Consumer Group: 消费者组,一个topic可以有多个消费者组访问,kafka是以消费者组为单位去消费数据的,一个消费者组内可以有多个消费者,每个消费者消费Broker中当前Topic的不同分区中的消息。某一个分区中的消息只能够一个消费者组中的一个消费者所消费
  • Broker: 一台kafka服务器就是一个Broker,一个Broker中可以有多个topic
  • Partition: 分区,为了实现扩展性,一个非常大的Topic可以分布到多个Broker上,一个Topic可以分为多个Partition,每个Partition是一个有序的队列(分区有序,不能保证全局有序)
  • Replica: 副本Replication,为保证集群中某个节点发生故障,节点上的Partition数据不丢失,Kafka可以正常的工作,Kafka提供了副本机制,一个Topic的每个分区有若干个副本,一个Leader和多个Follower
  • Leader: 每个分区多个副本的主角色,生产者发送数据的对象,以及消费者消费数据的对象都是Leader。
  • Follower: 每个分区多个副本的从角色,实时的从Leader中同步数据,保持和Leader数据的同步,Leader发生故障的时候,某个Follower会成为新的Leader。
  • Offset: 偏移量,记录消费者当前消费的位置

4. Kafka安装部署

之前介绍过安装部署的步骤,请移步:kafka集群部署

5.常用命令

5.1 创建主题

./bin/kafka-topics.sh --create --bootstrap-server linux1:9092 --replication-factor 2 --partitions 3 --topic epac

topic:主题名字
partitions :分区数量
replication-factor:副本的个数

5.2 查看主题信息

./kafka-topics.sh --describe --topic epac --bootstrap-server linux1:9092

查看结果:

在这里插入图片描述

说明: 刚才创建主题的时候我们指定了三个分区两个副本。所以此处的含义是 Partition指的是分区号 0,1,2
分别对应三个分区,Leader表示 当前分区的 leader副本在哪台机器上,Leader:2 代表 0分区的leader副本在机器2上
Replicas指的是两个副本在哪台机器上,既然leader在2上 那么follower就是机器1上(这里的机器指的是broker.id),Isr指的是正在同步消息的副本。

5.2 生产数据(使用控制台)

./kafka-console-producer.sh --broker-list linux1:9092 --topic epac

输入这行指令之后可以在控制台上写自己要发送的内容
在这里插入图片描述

5.3 消费数据

./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic frist0401

5.4 删除topic

./kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic rich

5.5 修改分区数

./kafka-topics.sh --bootstrap-server localhost:9092 --alter --topic first --partitions 3

5.6 手动触发再平衡

./kafka-leader-election.sh --bootstrap-server linux1:9092 --topic rich --partition=0 --election-type preferred

其中,–partition:指定需要重新分配leader的partition编号

5.7 查看分段日志文件

../../bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.log --print-data-log

6.kafka生产过程分析

6.0 发送原理

main线程中创建一个Producer对象,调用send方法,选拦截器 -> 序列化器 -> 分区器- > 队列管理器RecordAccumulator(每个分区创建一个DQuence队列) -> sender线程 -> NetworkClient(最多缓存五个请求) -> selector -> kafka集群副本应答(ack=0,1,-1) -> 成功就清理队列数据,失败重试

在这里插入图片描述

6.1 写入方式

producer采用推(push)模式将消息发送到broker,每条消息都被追加(append)到分区(patition)中,属于顺序写磁盘(顺序写磁盘比随机写磁盘效率要搞,保证kafka的高吞吐量)

kafka的高效读写:

  • kafka本身是分布式集群,采用分区技术,并行度高
  • 读数据采用稀疏索引,可以快速定位要消费的数据
  • 顺写磁盘(kafka的producer生产数据,要写入log文件中,写的过程是一直追加到文件的末端)
  • 页缓存+零拷贝

6.2 分区(Partition)

消息发送时都会被发送到一个topic,其本质就是一个目录,而topic是由一些Partition Logs(分区日志)组成,其结构如下如所示:

在这里插入图片描述
kafka分区文件夹命名规则:Topic+“-”+分区号

在这里插入图片描述
.log 文件存放数据,.index文件为索引(可以快速定位数据位置,文件中存的偏移量和字节对应的位置)

每个Partition中的消息都是有序的,生产的消息被不断的追加到.log文件中,每一个消息都被赋予了一个唯一的offset值,Producer生产的数据会被不断的追加到该log文件的末端,且每条数据都有自己的offset,consumer组中的每个consumer,都会实时记录自己消费到了哪个offset,以便出错恢复的时候,可以从上次的位置继续消费

  • 分区原因
  1. 方便在集群中扩展,每个Partition可以通过调整以适应他所在的机器,而一个topic又可以有多个partition组成,因此整个集群就可以适应任意大小的数据
  2. 可以提高并发,因为可以以Partition为单位读写
  • 分区策略
  1. 指定了partition,可直接使用
  2. 未指定partition但是指定key,会通过过keyvalue进行hash出一个partition(根据key的值与topic的分区个数取余得到分区号)
  3. partitionkey都未指定,使用round-robin 轮询算法。第一次调用生成一个随机数,之后的每次调用递增,将这个值与可用的partition总数取余得到分区号。可以保证负载均衡

猜想: 如果想要把订单表中的数据放入同一个分区呢?
答: 将订单表名作为key即可(key一致那么hash出来的分区号也一定是同一个,在同一个分区内数据一致的)

6.3 副本(Reolication)

同一个分区 可以有多个副本,对应 service.properties 中的offsets.topic.replication.factor=1,没有副本的话一旦broker宕机,分区中的所有数据都不可以消费,生产者也无法往此分区生产数据,所有副本中要选出一个leader来,生产者和消费者只与leader交互。其他副本从leader中同步数据

注意: kafka默认副本1个,生产环境一般配置2个。副本配置过多也会增大磁盘存储压力,增加网络数据传输,降低效率
kafka分区中所有的副本统称AR。AR = ISR + OSR
ISR: 表示和Leader 保持同步的Follower集合。如果Follower长时间未向Leader发送通信请求或者同步数据,则该Follower将被踢出ISR啊。该时间由replica.lag.time.max.ms 参数设定,默认30s。Leader发生故障之后,就会从ISR中选举新的Leader。
OSR: 表示Follower与Leader副本同步时,延时过多的副本

6.4 写入流程

  1. producer 先从 集群拿到对应的topic的partition信息和partition的leader的相关信息
  2. producer 发送消息给leader
  3. leader将消息存入本地log
  4. followers 从 leader pull 消息,写入本地 log 后向 leader 发送 ACK
  5. leader 收到所有 ISR 中的 replica 的 ACK 后,增加 HW(high watermark,最后 commit 的 offset) 并向 producer 发送 ACK

在这里插入图片描述

kafka 生成数据的应答机制(ACK)
为保证producer发送的数据能够可靠的发送到指定的topic中,topic的每个partition收到producer发送的数据后,都需要向producer发送ackacknowledgement,如果producer收到ack就会进行下一轮的发送,否则重新发送数据。

在这里插入图片描述

取值0: 生产者发送完数据,不关系数据是否到达kafka,然后直接发送下一条数据,这样效率非常高,但是丢失数据的可能非常大
取值1: 生产者发送数据,需要等待leader的应答,如果应答完成,才能发送下一条数据,不关心follower 是否接收成功,这种场合性能会慢一些,但是数据比较安全,但是在leader保存数据成功后,突然down掉,follower 没来得及同步数据,那么数据就会丢失

在这里插入图片描述

取值为-1: 生产者发送数据,需要等待所有的副本(leader+follower)的应答,这种方式数据最安全但是效率最低。

在这里插入图片描述

说明: producer返ack,0无落盘直接返,1只leader落盘然后返,-1全部落盘然后返

6.5 数据一致性

  • LEO(Log End Offset):每个副本最后的一个offset
  • HW(High Watermark):高水位,指代消费者能消费的最大的offset,ISR队列中最小的LEO

以下图为例:
在这里插入图片描述

现在向leader发送12,13,14,15 四条数据:

  1. 首先leader节点增大自己的LEO的值为15,此时HW的值仍为11
  2. 然后follower带着自身的LEO值 轮询去leader中取数据,leader会根据follower带来的LEO的值更新本地保存的followerLEO的值,并且区其中最小的LEO的值来更新自身的HW的值,之后返回follower LEO值之后的数据以及自身的HW
  3. follower节点 接收到数据后更新自身的LEOleader几点返回的HW
  4. 然后 follower 节点再次向 leader 节点拉取数据,携带自身的 LEO ,leader 节点收到后同样取 follower 节点中最小的 LEO 值来更新自身的 HW 值,返回给 follower 节点。
  5. 以此往复从而保证数据的一致性

在这里插入图片描述

说明: 这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复

6.6 副本数据同步策略

半数follower同步完成即发送ack

说明: 优点为:低延迟。缺点:选举新的leader的时候,容忍n台节点的故障,需要n+1个副本。因为需要半数同意,所以故障的时候,能够选举的前提是剩下的副本超过半数,容错率为1/2

全部的follower同步完成发送ack

优点:容错率高,选举新的leader的时候,容忍n台节点的故障只需要n+1的副本即可,因为只需要剩下的一个人同意即可发送ack了。缺点:延迟高,因为需要全部的副本都同步完成才可

kafka选择的是第二种,因为在容错率上面更加有优势,同时对于分区的数据而言,每个分区都有大量的数据,第一种方案会造成大量数据的冗余。虽然第二种网络延迟较高,但是网络延迟对于Kafka的影响较小。

猜想:
采用了第二种方案进行同步ack之后,如果leader收到数据,所有的follower开始同步数据,但有一个follower因为某种故障,迟迟不能够与leader进行同步,那么leader就要一直等待下去,直到它同步完成,才可以发送ack,此时需要如何解决这个问题呢?
解决:

leader中维护了一个动态的ISR(in-sync replica set),即与leader保持同步的follower集合即(leader:0,isr:0,1,2)。当ISR中的follower完成数据的同步之后,给leader发送ack,如果follower长时间没有向leader同步数据,则该follower将从ISR中被踢出,该之间阈值由replica.lag.time.max.ms参数设定,默认30s。当leader发生故障之后,会从ISR中选举出新的leader。

6.7 储存方式

物理上把topic分成一个或者多个partition 对应server.properties中的 num.partition=3配置,每个partition物理上对应一个文件夹,该文件夹存储partition的所有新消息和索引文件
在这里插入图片描述
在这里插入图片描述
kafka存储方式为稀疏索引的方式,log文件每写入4kb数据就会往index文件写入一条索引。index文件中保存的值为相对offset,这样能确保offset的值所占空间不会过大,因此能将offset的值控制在固定大小

在这里插入图片描述

6.8 储存策略

无论消息是否被消费,kafka都会保存所有的消息,两种策略可以删除数据

  • 基于时间:log.retention=168
  • 基于大小:log.retention.bytes=1073741824

kafka读取特定消息的事件复杂度为O(1),即与文件大小无关,所以删除过期文件与提高kafka性能无关

6.9 如何提高kafka的吞吐量

  1. bath.size:增大批次大小,默认16K可以改为32k
  2. linger.ms: 等待时间,修改为5-100ms
  3. compression.type:压缩,默认none,修改为snappy
  4. RecordAccumulator:缓冲区大小,修改为64m

6.10 数据重复

幂等性: producer 不论向borker发送多少条重复数据,broker都只会实例化一条,保证了不重复。精确一次(Exactly Once) = 幂等性+至少一次(ack=-1 + 分区副本数>=2 +ISR最小副本数量>=2)

数据重复的判定标准: 具有,PID,Partition,SeqNumber 相同的主键消息提交时,broker只会持久化一条。其中PID是kafka每次重启都会分配一个新的;Partition标识分区号;Sequence Number表示单调递增,所以幂等性只能保证在单分区单会话内数据不会重复。

在这里插入图片描述
开启幂等性配置: enable.idempotence 默认为 true,false关闭

生产者事务: 开启事务,必须先开启幂等性,_transaction_state-分区-Leader(储存事务信息的特殊主题),默认有50个分区,每个分区负责一部分事务。事务划分是根据 transaction.idhashcode%50 计算出Leader副本所在的broker节点,即为这个transaction.id 对应的 Transaction Coordinator (事务协调器)节点

在这里插入图片描述

使用幂等性生产者,ack必须设置为all/-1

6.11 数据乱序

  1. 在kafka1.x版本之前保证数据的但分区有序,条件如下:
    max.in.flight.request.per.connection=1(不考虑是否开启幂等性)
    在kafka1.x之后版本保证但分区有序。条件如下:
  • 未开启幂等性: max.in.flight.request.per.connection=1
  • 开启幂等性:max.in.flight.request.per.connection 设置小于等于5

原因说明:因为kafka1.x以后,启用幂等后,kafka服务会缓存producer发来的最近5个request 的元数据 故无论如何都可以保证最近5个request的数据都属有序的

6.12 ZK工作流程

每台kafka的borker节点启动成功后会往zk中的/brokers/ids中注册,然后开始选择controller节点监听/brokers/ids节点的变化,选举leader (选举规则:在isr中存活为前提,按照ar中排在前面的优先,例如ar[1,0,2],isr[1,2,0],就会按照1,0,2轮询) 然后将选举结果写入zk的/brokers/topics/rich/partitions/0/state 节点下,接下来其他controller在 /brokers/topics/rich/partitions/0/state 拉取信息,follower主动跟leader pull数据 存入本地log 最后向生产者发送应答

在这里插入图片描述

6.13 Leader选举

在isr中存活为前提,按照ar中排在前面的优先,例如ar[1,0,2],isr[1,2,0],就会按照1,0,2轮询,首次选举 1为Leader ,假如1节点故障 就会被踢出isr中,2会被选举为新的Leader,执行Leader的职责。1节点恢复以后leader节点不会发生变化,1会重新加入到 isr中

6.14 Leader 和 Follower 故障

Follower 故障:

  • 首先follower发生故障会被踢出isr
  • 这个期间leader和follower继续接收数据
  • 等待follower恢复后,follower会读取本地磁盘记录的HW,并将log文件高于HW的部门截掉,从HW开始向leader进行同步
  • 等该follower的LEO大于等于该分区的HW的时候,就可以重新加入isr队列

Leader故障:

  • 首先leader故障会被踢出isr,选出一个新的leader
  • 为保证多个副本之间的数据一致性,其余的follower会先将各自的log文件高于HW的部分截掉,然后从新的leader同步数据

注意: 只能保证数据一致性,不能保证数据不丢失

6.15 手动指定分区位置

在这里插入图片描述

在这里插入图片描述

6.16 LeaderPartition 负载均衡

正常情况下,kafka本身会自动把leader partition 均匀分散在各个机器上,来保证 每台机器的读写吞吐量都是均匀的。但是如果某些broker当即,会导致leader partition 过于几种在其他少部分几台broker上,这会导致少数几台的broker的读写请求压力过高,其他宕机的broker重启之后都是 follower partition,读写请求很低,造成集群负载不均衡。

  • auto.leader.rebalance.enable =true 自动平衡leader partition
  • leader.imbalance.per.broker.percentage 默认10% 。每个broker允许的不平衡leader的比率。如果每个broker超过这个值,控制器会触发leader的平衡
  • leader.imbalance.check.interval.seconds 默认300s 检查leader负载是否平衡的时间间隔

计算leader不平衡比率规则: 以下图集群为例
在这里插入图片描述

针对broker0节点,分区2的AR优先副本是0节点,但是0节点却不是leader节点,所以不平衡数+1 AR总副本数为4,所以 不平衡率为1/4 > 10% 会触发再平衡,broker的不平衡术则为0 ,不需要再平衡

注意: 触发再平衡消耗大量性能,生产环境 不建议开启再平衡配置。原则上不建议频繁的触发再平衡

6.17 文件储存机制

7 数据的消费

消费者是以 consumer group 消费者组的方式工作的,由一个或者多个消费者组工作共同消费一个topic。每个分区在同一时间只能由group中的一个消费者读取,但是多个group可以同时消费一个分区,由三个消费者组成的消费者组消费有四个分区的topic时候 其中一个消费者消费两个分区,两外两个消费者各消费一个分区。某个消费者消费某个分区,也可以叫做某个消费者是某个分区的拥有者。
在这种情况下,可以通过水平扩展消费者的方式同时读取大量的消息,如果一个消费者失败了,其他的group成员会触发自动调整负载机制,重新分配消费者的分区

一个消费者可以消费多个分区,多个消费者不能消费同一个分区

7.1 消费方式

consumer通过pull(拉)的方式从broker中读取数据
push(推)的模式很难适应消费速率不同的消费者,因为消息发送率是由broker决定的,它的目标是尽可能以最快的速度传递消息,但是这样容易造成consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull方式则可以让consumer根据自己的消费处理能力以适当的速度消费消息

7.2 消费者组

Consumer Group (CG):消费者组,由多个counsumer组成。形成一个组的条件是有同样的groupid

  • 消费者组内的每个消费者负责消费不同的分区,一个分区只能由一个组内的消费者消费
  • 消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者

一个消费者可以消费多个分区,但是一个分区只能被一个消费者消费

7.3 消费者组的初始化

coordinator:辅助实现消费者组的初始化和分区分配
coordinator节点选择 = groupid 的hashcode% 50 (_consumer_offsets的分区数量)。

例如:groupid的hashcode的值为1,1%50 =1 ,那么_consumer_offsets主题的1号分区在那个broker上,就选择这个节点的coordinator 作为整个消费者组的老大。消费者组下的所有的消费者提交offset 的时候就往这个分区提交offset。

在这里插入图片描述

7.4 消费者详细消费流程

在这里插入图片描述

7.5 消费者分区分配策略

一个消费者组有多个消费者组成,一个topic有多个partition组成,那么到底哪个消费者消费哪个分区?
kafka有四种主流分区分配策略:Range、RoundRobin、Sticky、CooperativeSticky。可以通过参数 partition.assignment.strategy,修改分区分配策略。默认策略是Range + CooperativeSticky。kafka可以同时使用多个分区分配策略

7.5.1 Range

Range是对每一个topic而言的。

  1. 首先对同一个topic里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序
  2. 通过partitions数/consumer线程总数数来决定每个消费者应该消费几个分区。如果除不尽,那么前面几个消费者将会多消费1个分区。

假如: 现在有7个分区,3个消费者,排序后的分区 将会是0,1,2,3,4,5,6;消费者排完序后是C0,C1,C2. 那么:7/3 = 2
…1,那么消费者C0便会多消费一个分区。如果是八个分区,8/3 = 2…2 那么C0和C1 分别多消费一个分区。
确点: 如果是针对一个topic而言,C0消费者多消费1个分区影响不大,但如果有N个topic,那么针对每个topic,消费者C0都将多消费1个分区,topic越多,C0消费的分区回避其他消费者明显增多N个分区。这种话情况称之为:数据倾斜

7.5.2 RoundRobin

RoundRobin 针对所有的消费者而言
RoundRobin轮询分区策略,是把所有的partition和所有的consumer都列出来,然后按照hashcode进行排序,最后通过轮询算法来分配partition给到各个消费者。这里有两种情况:

  • 同一个消费组内所有的消费者的订阅信息都是相同的,那么RoundRobinAssignor策略的分区分配会是均匀的
  • 同一个消费组内的消费者所订阅的信息是不相同的,那么在执行分区分配的时候就不是完全的轮询分配,有可能会导致分区分配的不均匀
7.5.3 Sticky

粘性分区: 可以理解为分配的结果 带有“粘性的”。即再执行一次新的分配之前,考虑上一次分配的结果,尽量减少调整分配的变动,可以节省大量的开销

粘性分区是kafka从0.11.x版本开始引入的分匹配策略,首先会尽量均衡的放置分区到消费者上面,再出现同一消费者出现问题的时候,会尽量保持原有的分配的分区不变化

注意: 如果有消费者断开连接,默认45s后会触发再平衡

7.6 offset 位置

0.9版本以前维护在zookeeper中,0.9版本以后维护在系统主题中 _consumer_offsets,其中采用key,value方式存储,key是group.id+topic+分区号,value就是当前的offset值。kafka每隔一段时间就会对这个topic进行compat。也就是每个 _consumer_offsets只保留最新数据

7.7 自动 offset

enable-auto-commit: false 默认值为 true 。开启自动提交offset值。

auto-commit-interval: 1S 自动提交的时间间隔。默认是5s

7.8 手动 offset

手动提交offset的方式有两种,都会将本次一批数据的最高偏移量提交:

  • 同步提交: 阻塞当前线程,一直到提交成功,并且会自动失败重试(如果是不可控因素也会导致提交失败)必须等待提交完成,才可以消费下一条数据
  • 异步提交: 没有失败重试机制,有可能提交失败

7.9 指定 offset

auto-offset-reset: earliest/latest/nonc:该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理

earliest 在偏移量无效的情况下,消费者将从起始位置读取分区的记录

latest(默认值)在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录)

nonc 如果未找到消费者先前的偏移量,则抛出异常

7.10 消费者事务

  • 重复消费: 自动提交offset引起
    在这里插入图片描述

  • 漏消费: 设置手动提交offset,数据还在内存中未落盘,此时刚好线程被kill,那么offset已经提交,但是数据未处理,导致这部分内存的数据丢失
    在这里插入图片描述

消费者事务: 如果想完成consumer的精准消费,那么需要kafka消费端将消费过程和提交offset过程做原子绑定。此时我们需要将kafka的offset保存到支持事务的自定义介质(比如mysql、redis)。

7.11数据积压

  • 增加topic的分区数,同时增加消费者数量(两者缺一不可)。分区数 = 消费者数量
  • 提高每批次拉取的数据量
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值