从零开始学习Spark - SparkRDD、SparkDStream与HBase交互

一. SparkRDD 与 HBase的交互

1.1 依赖配置以及注意事项

1.1.1 特别注意

建议参考 2.3 添加数据 - put的使用里面的处理方法

1.1.2 POM 文件
<properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <encoding>UTF-8</encoding>
        <hadoop.version>2.7.5</hadoop.version>
        <spark.version>2.3.4</spark.version>
        <scala.version>2.11.12</scala.version>
        <junit.version>4.12</junit.version>
        <netty.version>4.1.42.Final</netty.version>
    </properties>

    <dependencies>
        <!-- spark 核心依赖包 -->
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-core_2.11</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hbase.connectors.spark</groupId>
            <artifactId>hbase-spark</artifactId>
            <version>1.0.0</version>
         </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming_2.11</artifactId>
            <version>${spark.version}</version>
            <!--
                java.lang.NoClassDefFoundError: org/apache/spark/streaming/dstream/DStream
            -->
           <!-- <scope>provided</scope>-->
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>${netty.version}</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!-- 编译Scala 的插件 -->
            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>3.4.6</version>
            </plugin>
            <!-- 编译Java 的插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
1.1.2 注意事项

在下载依赖的时候很有可能出现下载不下来的问题, 会卡到一个地方,一直下载
在这里插入图片描述
解决办法 : 尝试删掉这个文件, 或许可以

1.2 获取数据 - Get 的使用

特别注意 : 千万不要忘了导 import org.apache.hadoop.hbase.spark.HBaseRDDFunctions._

package com.wangt.hbase.spark

import org.apache.hadoop.hbase.client.{Get, Result}
import org.apache.hadoop.hbase.spark.HBaseContext
import org.apache.hadoop.hbase.spark.HBaseRDDFunctions._
import org.apache.hadoop.hbase.util.Bytes
import org.apache.hadoop.hbase.{Cell, CellUtil, HBaseConfiguration, TableName}
import org.apache.spark.{SparkConf, SparkContext}

/**
 * 使用 RDD 作为数据源, 将RDD中的数据写入到HBase
 * 特别注意 : 一定要导入 HBase 的隐式方法org.apache.hadoop.hbase.spark.HBaseRDDFunctions._
 *
 * @author 王天赐
 * @create 2019-11-29 19:35
 */
object HBaseBulkGetExampleByRDD extends App{

	// 1.创建SparkConf 以及 SparkContext, 设置本地运行模式
	val conf = new SparkConf()
		.setMaster("local[*]")
		.setAppName("HBase")
	val sc = new SparkContext(conf)
	// 设置日志输出等级为 WARN
	sc.setLogLevel("WARN")

	try {
		// 2. 创建HBaseConfiguration对象设置连接参数
		val hbaseConf = HBaseConfiguration.create()
		// 设置连接参数
		hbaseConf.set("hbase.zookeeper.quorum", "222.22.91.81")
		hbaseConf.set("hbase.zookeeper.property.clientPort", "2181")

		// 3.创建HBaseContext
		val hc = new HBaseContext(sc, hbaseConf)

		// 4. 将需要获取的数据的 Rowkey 字段等信息封装到 RDD中
		val rowKeyAndQualifier = sc.parallelize(Array(
			Array(Bytes.toBytes("B1001"), Bytes.toBytes("name")),
			Array(Bytes.toBytes("B1002"), Bytes.toBytes("name")),
			Array(Bytes.toBytes("B1003"), Bytes.toBytes("name"))
		))

		// 5. 获取指定RowKey 以及指定字段的信息
		val result = rowKeyAndQualifier.hbaseBulkGet(hc, TableName.valueOf("Student"), 2,
			(info) => {
				val rowkey = info(0)
				// 字段名
				val qualify = info(1)
				val get = new Get(rowkey)
				get
			}
		)
		// 6. 遍历结果
		result.foreach(data => {
			// 注意 Data是 Tuple 类型
			val result: Result = data._2
			// 获取 Cell数组对象
			val cells: Array[Cell] = result.rawCells()
			// 遍历
			for (cell <- cells) {
				// 获取对应的值
				val rowKey = Bytes.toString(CellUtil.cloneRow(cell))
				val qualifier = Bytes.toString(CellUtil.cloneQualifier(cell))
				val value = Bytes.toString(CellUtil.cloneValue(cell))
				// 打印输出结果
				println("[ " + rowKey + " , " + qualifier + " , " + value + " ]")
			}
		})

	} finally {
		sc.stop()
	}

}

1.3 添加数据 - put 的使用

package com.wangt.hbase.spark

import org.apache.spark.{SparkConf, SparkContext}
import org.apache.hadoop.hbase.client.{Get, Put, Result}
import org.apache.hadoop.hbase.spark.HBaseContext
import org.apache.hadoop.hbase.spark.HBaseRDDFunctions._
import org.apache.hadoop.hbase.util.Bytes
import org.apache.hadoop.hbase.HBaseConfiguration
import org.apache.hadoop.hbase.TableName
import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
/**
 * @author 王天赐
 * @create 2019-11-29 9:28
 */
object HBaseBulkPutExample extends App {

	val tableName = "Student"

	val sparkConf = new SparkConf()
		.setAppName("HBaseBulkGetExample " + tableName)
    	.setMaster("local[*]")
	val sc = new SparkContext(sparkConf)

	try {

		//[(Array[Byte])]
		val rdd = sc.parallelize(Array(
			Array(Bytes.toBytes("B1001"),Bytes.toBytes("name"),Bytes.toBytes("张飞")),
			Array(Bytes.toBytes("B1002"),Bytes.toBytes("name"),Bytes.toBytes("李白")),
			Array(Bytes.toBytes("B1003"),Bytes.toBytes("name"),Bytes.toBytes("韩信"))))

		val conf = HBaseConfiguration.create()
		conf.set("hbase.zookeeper.quorum", "222.22.91.81")
		conf.set("hbase.zookeeper.property.clientPort", "2181")

		val hbaseContext = new HBaseContext(sc, conf)

		val getRdd = rdd.hbaseBulkPut(hbaseContext, TableName.valueOf("Student"),
			record => {
				val put = new Put(record(0))
				put.addColumn(Bytes.toBytes("info"), record(1), record(2));
				put
			}
		)

	} finally {
		sc.stop()
	}
}

1.4 删除数据 - delete 的使用

package com.wangt.hbase.spark

import org.apache.hadoop.hbase.client.Delete
import org.apache.hadoop.hbase.{HBaseConfiguration, TableName}
import org.apache.hadoop.hbase.spark.HBaseContext
import org.apache.hadoop.hbase.util.Bytes
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.hadoop.hbase.spark.HBaseRDDFunctions._

/**
 * @author 王天赐
 * @create 2019-11-29 22:01
 */
object HBaseBulkDeleteExample extends App{

	// 1.创建SparkConf 以及 SparkContext, 设置本地运行模式
	val conf = new SparkConf()
		.setMaster("local[*]")
		.setAppName("HBase")
	val sc = new SparkContext(conf)
	// 设置日志输出等级为 WARN
	sc.setLogLevel("WARN")

	try {
		// 2. 创建HBaseConfiguration对象设置连接参数
		val hbaseConf = HBaseConfiguration.create()
		// 设置连接参数
		hbaseConf.set("hbase.zookeeper.quorum", "222.22.91.81")
		hbaseConf.set("hbase.zookeeper.property.clientPort", "2181")

		// 3.创建HBaseContext
		val hc = new HBaseContext(sc, hbaseConf)

		// 4. 将需要删除的数据的 Rowkey 字段等信息封装到 RDD中
		val deletedRowkeyAndQualifier = sc.parallelize(Array(
			Array(Bytes.toBytes("B1001"), Bytes.toBytes("name"))
		))

		// 5. 删除数据
		deletedRowkeyAndQualifier.hbaseBulkDelete(hc, TableName.valueOf("Student"),
			(record) => {

				val rowkey = record(0)
				val qualifier = record(1)
				val delete = new Delete(rowkey)
				delete.addColumn(Bytes.toBytes("info"), qualifier)
				// 最后需要返回一个 Delete 对象
				delete
			},
			2 // 批处理的大小
		)
	} finally {
		sc.stop()
	}
}

1.5 自定义操作 - hbaseMapPartitions 的使用

  1. hbaseMapPartitions 相当于是针对每个RDD的分区的数据进行操作

  2. 强烈建议 : hbaseMapPartitions 只作为封装 Get 对象或者 Put 对象不要直接在里面 put数据

  3. Get 数据可以, 但是不要直接在 hbaseMapPartitions 方法里面就直接把数据提交删除,具体参考下面的代码

1.5.1 HBaseMapPartitionPut 操作
package com.wangt.hbase.spark

import java.util

import org.apache.hadoop.hbase.client.Put
import org.apache.hadoop.hbase.{HBaseConfiguration, TableName}
import org.apache.hadoop.hbase.spark.HBaseContext
import org.apache.hadoop.hbase.spark.HBaseRDDFunctions._
import org.apache.hadoop.hbase.util.Bytes
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.JavaConverters._

/**
 * @author 王天赐
 * @create 2019-11-29 22:10
 */
object HBaseMapPartitionPutExample extends App {

	// 1.创建SparkConf 以及 SparkContext, 设置本地运行模式
	val conf = new SparkConf()
		.setMaster("local[*]")
		.setAppName("HBase")
	val sc = new SparkContext(conf)
	// 设置日志输出等级为 WARN
	sc.setLogLevel("WARN")

	try {
		// 2. 创建HBaseConfiguration对象设置连接参数
		val hbaseConf = HBaseConfiguration.create()
		// 设置连接参数
		hbaseConf.set("hbase.zookeeper.quorum", "222.22.91.81")
		hbaseConf.set("hbase.zookeeper.property.clientPort", "2181")

		// 3.创建HBaseContext
		val hc = new HBaseContext(sc, hbaseConf)

		// 4. 将 Rowkey 字段等信息封装到 RDD中
		val rdd = sc.parallelize(Array(
			Array(Bytes.toBytes("B1004"), Bytes.toBytes("name"),Bytes.toBytes("貂蝉"))
		))

		// 5.使用 HBaseMapPartition
		val putsRdd = rdd.hbaseMapPartitions[Put](hc, (rddData, connection) => {

			val puts = rddData.map(r => {
				// 取出对应的数据
				val rowkey = r(0)
				val qualifier = r(1)
				val value = r(2)

				// 注意 : 这个时候 我们有 HBase的Connection对象. 有 Table 对象, 我们可以做关于HBase的任何事
				// 包括但不限于 创建表 或者删除 只需要获取一个Admin对象即可 等等, 下面只是举一个例子
				val put = new Put(rowkey)
				// 将数据添加进去
				put.addColumn(Bytes.toBytes("info"), qualifier, value)
				put
			})
			// 最后再把 puts 返回即可, 它会自动把数据添加进RDD中
			puts
		})
		// 强烈建议 : ! 在 hbaseMapPartitions 方法中将RDD的数据封装成 put类型
		// 然后 在 hbaseBulkPut 去添加, 直接在 hbaseMapPartitions 添加, 虽然有 Connection对象, 但是真的不好用,
		// 参考 我在 HBaseMapPartitionGetExample 类里面写的, 可以直接分开
		// hbaseMapPartitions 封装get, hbaseBulkGet 获取数据, 然后 RDD 遍历 
		putsRdd.hbaseBulkPut(hc, TableName.valueOf("Student"), (put) => (put))

	}finally {
		sc.stop()
	}
}

1.5.2 HBaseMapPartitionGet 操作

注意 : HBaseMapPartition 和上面的方法的区别是, 就像map和 mapParatition的区别

它会针对每个分区统一执行一次map方法, 而不是针对每一条数据执行一次 推荐使用

package com.wangt.hbase.spark

import java.util

import org.apache.hadoop.hbase.{Cell, CellUtil, HBaseConfiguration, TableName}
import org.apache.hadoop.hbase.client.{Get, Put, Result}
import org.apache.hadoop.hbase.spark.HBaseContext
import org.apache.hadoop.hbase.spark.HBaseRDDFunctions._
import org.apache.hadoop.hbase.util.Bytes
import org.apache.spark.{SparkConf, SparkContext}

/**
 * @author 王天赐
 * @create 2019-11-30 10:14
 */
object HBaseMapPartitionGetExample extends App{

	// 1.创建SparkConf 以及 SparkContext, 设置本地运行模式
	val conf = new SparkConf()
		.setMaster("local[*]")
		.setAppName("HBase")
	val sc = new SparkContext(conf)
	// 设置日志输出等级为 WARN
	sc.setLogLevel("WARN")

	try {
		// 2. 创建HBaseConfiguration对象设置连接参数
		val hbaseConf = HBaseConfiguration.create()
		// 设置连接参数
		hbaseConf.set("hbase.zookeeper.quorum", "222.22.91.81")
		hbaseConf.set("hbase.zookeeper.property.clientPort", "2181")

		// 3.创建HBaseContext
		val hc = new HBaseContext(sc, hbaseConf)

		// 4. 将 Rowkey 字段等信息封装到 RDD中
		val rdd = sc.parallelize(Array(
			Array(Bytes.toBytes("B1002"), Bytes.toBytes("name")),
			Array(Bytes.toBytes("B1003"), Bytes.toBytes("name"))
		))

		// 5.使用 HBaseMapPartition
		val results = rdd.hbaseMapPartitions[String](hc, (rddData, connection) => {

			// (1). 获取Table对象
			val table = connection.getTable(TableName.valueOf("Student"))
			// (2). 注意 rddData 是 iterator 类型
			// 可以使用下面的方式获取数据, 但是不推荐
			/*
			while(rddData.hasNext){
				val info = rddData.next()
			}
			*/
			// 官方的例子. 注意 : 这个map 不是RDD 算子, 而是scala自带的函数
			// 最后返回的是 一个 iterator 类型 比如下面的例子是 返回的 iterator[Put]
			val infos = rddData.map(r => {
				/**
				 * 获取指定 RowKey 和指定字段的值
				 */
				// 取出对应的数据
				val rowKey = r(0)
				val qualifier = r(1)
				// 创建 Get 对象, 并添加相应的对象以及rowkey信息
				val get = new Get(rowKey)
				get.addColumn(Bytes.toBytes("info"), qualifier)
				// 获取Result对象
				val result : Result = table.get(get)
				// 获取Cells ,类型是我加上为了 知道这个数据是什么类型的, 可以选择不加
				val cells : util.Iterator[Cell] = result.listCells().iterator()
				// 遍历 Cells
				val sb = new StringBuilder()
				while (cells.hasNext){
					val cell = cells.next()
					// 获取cell中的数据
					val rowKey = Bytes.toString(CellUtil.cloneRow(cell))
					val qualifier = Bytes.toString(CellUtil.cloneQualifier(cell))
					val value = Bytes.toString(CellUtil.cloneValue(cell))
					sb.append("[ " + rowKey + " , " + qualifier + " , " + value + " ]" )
				}
				// 将得到信息返回
				sb.toString()
			}
			)
			// 最后再把 infos 返回即可 ,它会把info中的信息封装到 RDD中
			infos
		})

		// 6.遍历结果
		results.foreach(println(_))

	}finally {
		sc.stop()
	}
}


二. SparkDStream 与 HBase的交互

2.1 依赖配置以及注意事项

Get 时如果你传入的rowKey是空的话, 后面获取Result的时候会报空指针, 解决方法参考我的代码

2.2 获取数据 - Get的使用

注意事项 : 我用的是netcat作为数据源, 需要先开netcat 再启动程序

参考 :

  1. 开启netcat => nc -lk cm5 8989 (端口和主机改成你自己的)

  2. 输入数据 :

B1001 name

B1002 name

注意 : 第一个是rowKey , 第二个是 字段 (建议输入你HBase中有的RowKey和字段)

package com.wangt.hbase.sparkstreaming

import org.apache.hadoop.hbase.{CellUtil, HBaseConfiguration, TableName}
import org.apache.hadoop.hbase.client.Get
import org.apache.hadoop.hbase.spark.HBaseContext
import org.apache.hadoop.hbase.spark.HBaseDStreamFunctions._
import org.apache.hadoop.hbase.util.Bytes
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}

/**
 * @author 王天赐
 * @create 2019-11-30 20:36
 */
object HBaseBulkGetDStreamExample extends App {

	// 1.创建SparkConf 以及 SparkContext, 设置本地运行模式
	val sc = new SparkContext("local[*]", "HBase")
	val ssc = new StreamingContext(sc, Seconds(5))
	// 设置日志输出等级为 WARN
	sc.setLogLevel("WARN")

	try {
		// 3.创建StreamingContext 设置将5s内的数据一块处理

		// 2.获取 HBase 配置对象以及HBaseContext
		val hBaseConf = HBaseConfiguration.create()
		hBaseConf.set("hbase.zookeeper.quorum", "222.22.91.81")
		hBaseConf.set("hbase.zookeeper.property.clientPort", "2181")
		val hBaseContext = new HBaseContext(sc, hBaseConf)

		// 4.获取指定端口的数据
		val dStream = ssc.socketTextStream("cm5", 8989)
		dStream.print()

		// 将数据封装到Get对象, 然后将数据转换为 DStream
		val getsDStream = dStream.hbaseMapPartitions[Get](hBaseContext, (record, connection) => {
			val gets: Iterator[Get] = record.map(r => {
				// 读取的单条数据 : B1001 name
				val arr: Array[String] = r.split(" ")
				// 默认值
				var rowKey: Array[Byte] = Bytes.toBytes("-")
				var qualifier: Array[Byte] = Bytes.toBytes("0")
				// 这里其实应该过滤下, 过滤掉不符合的数据, 但是这里只是作为Demo, 假设数据格式是规范的...
				if (arr.length == 2) {
					rowKey = Bytes.toBytes(arr(0))
					qualifier = Bytes.toBytes(arr(1))
				}
				// 将数据封装成 get 对象
				val get = new Get(rowKey)
				get.addColumn(Bytes.toBytes("info"), qualifier)
				// 注意需要将get对象返回
				get
			})
			// 最后将gets 返回
			gets
		})
		// 根据 Get 获取对应的Result 以及结果
		val data = getsDStream.hbaseBulkGet(hBaseContext, TableName.valueOf("Student"),
			2, (get) => (get), result => {
				// 特别注意 : 这个判断的必须加上, 否则, 一旦你输入的rowkey是错误的, 获取不到数据
				// 会立马报错 ,程序就会停止 最好使用 rawCells().size , 其他的属性我都试过., 没有用, 比如 getExist的...
				if (result.rawCells().size > 0 ) {
					// 获取 Cells
					// 下面这种方法也是可以的
					//val cells = result.rawCells()
					val cells = result.listCells().iterator()
					var sb: StringBuilder = null
					while (cells.hasNext) {
						var cell = cells.next()
						// 获取指定的数据
						val rowKey = Bytes.toString(CellUtil.cloneRow(cell))
						val qualifier = Bytes.toString(CellUtil.cloneQualifier(cell))
						val value = Bytes.toString(CellUtil.cloneValue(cell))
						// 使用StringBuilder拼接数据
						sb = new StringBuilder()
						sb.append("[ " + rowKey + " , " + qualifier + " , " + value + " ]")
					}
					sb.toString()
				}
			})
		// 打印结果
		data.print()

		ssc.start()
		ssc.awaitTermination()
		ssc.stop()
	}

}

2.3 添加数据 - Put的使用

  1. 注意事项 : put中我增加了过滤数据的步骤, 建议在使用时都增加过滤数据, 否则如果是不规则的数据容易报错

  2. 推荐 :

(1) 尽量先对数据进行过滤, 拿到你想要格式的数据

(2) 使用 hbaseMapPartitions 对数据进行封装成 Get / Put 对象

(3) 使用 hbaseBulkGet / hbaseBulkPut 对数据进行处理

package com.wangt.hbase.sparkstreaming

import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hbase.{HBaseConfiguration, TableName}
import org.apache.hadoop.hbase.client.Put
import org.apache.hadoop.hbase.spark.HBaseContext
import org.apache.hadoop.hbase.spark.HBaseDStreamFunctions._
import org.apache.hadoop.hbase.util.Bytes
import org.apache.spark.SparkContext
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}

/**
 * @author 王天赐
 * @create 2019-12-01 9:15
 */
object HBaseBulkPutDStreamExample extends App {

	// 1.创建SparkContext 以及StreamingContext 设置本地运行模式以及数据间隔时间
	// (1) 补充: 如果创建了SparkContext, 那么在创建StreamingContext的时候直接将 sc 作为参数传入即可
	val sc = new SparkContext("local[*]", "HBase-Spark")
	sc.setLogLevel("WARN")
	val ssc = new StreamingContext(sc, Seconds(2))

	try {
		// 2.创建HBaseConfiguration对象以及HBaseContext对象
		val hBaseConf: Configuration = HBaseConfiguration.create()
		hBaseConf.set("hbase.zookeeper.quorum", "222.22.91.81:2181")
		val hBaseContext = new HBaseContext(sc, hBaseConf)

		// 3.获取DStream流
		val dStream: ReceiverInputDStream[String] = ssc.socketTextStream("cm5", 8989)

		// 4.对不符合数据规则的数据进行过滤
		// 规则 : 要求 => B1001 name 张飞 (数据切分后长度为3)
		val filterDStream: DStream[String] = dStream.filter(line => {
			val data = line.split(" ")
			if (data.length == 3) {
				true
			} else {
				false
			}
		})

		// 5.将数据封装成 Put对象
		val putsDStream: DStream[Put] = filterDStream.hbaseMapPartitions(hBaseContext, (record, connection) => {
			val puts = record.map(r => {
				//(1) 切分数据
				val data = r.split(" ")
				//(2) 获取对应的数据
				val rowKey = Bytes.toBytes(data(0))
				val qualifier = Bytes.toBytes(data(1))
				val value = Bytes.toBytes(data(2))
				//(3) 封装数据
				val put = new Put(rowKey)
				put.addColumn(Bytes.toBytes("info"), qualifier, value)
				put
			})
			// 返回Puts
			puts
		})
		// 5.将put 对象中的数据写入到 HBase
		putsDStream.hbaseBulkPut(hBaseContext, TableName.valueOf("Student"), (put) => (put))

		// 6.开启StreamingContext
		ssc.start()
		ssc.awaitTermination()
		ssc.stop()
	}finally {
		sc.stop()
	}
}

= filterDStream.hbaseMapPartitions(hBaseContext, (record, connection) => {
val puts = record.map(r => {
//(1) 切分数据
val data = r.split(" ")
//(2) 获取对应的数据
val rowKey = Bytes.toBytes(data(0))
val qualifier = Bytes.toBytes(data(1))
val value = Bytes.toBytes(data(2))
//(3) 封装数据
val put = new Put(rowKey)
put.addColumn(Bytes.toBytes(“info”), qualifier, value)
put
})
// 返回Puts
puts
})
// 5.将put 对象中的数据写入到 HBase
putsDStream.hbaseBulkPut(hBaseContext, TableName.valueOf(“Student”), (put) => (put))

	// 6.开启StreamingContext
	ssc.start()
	ssc.awaitTermination()
	ssc.stop()
}finally {
	sc.stop()
}

}




  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

兀坐晴窗独饮茶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值