clickhouse简介

背景

架构分类

OLAP名为联机分析,又可以称为多维分析,是由关系型数据库之父埃德加·科德(Edgar Frank Codd)于1993年提出的概念。顾名思义,它指的是通过多种不同的维度审视数据,进行深层次分析。为了实现上述这些操作,将常见的OLAP架构大致分成两类。

第一类架构称为ROLAP(Relational OLAP,关系型OLAP)。顾名思义,它直接使用关系模型构建,数据模型常使用星型模型或者雪花模型,多维分析的操作,可以直接转换成SQL查询,但是这种架构对数据的实时处理能力要求很高,在大数据量下执行的sql查询会异常缓慢。

第二类架构称为MOLAP(Multidimensional OLAP,多维型OLAP)。它的出现是为了缓解ROLAP性能问题。MOLAP使用多维数组的形式保存数据,其核心思想是借助预先聚合结果,使用空间换取时间的形式最终提升查询性能。也就是说,用更多的存储空间换得查询时间的减少。但是这种架构也并不完美。维度预处理可能会导致数据的膨胀。由于使用了预处理的形式,数据立方体会有一定的滞后性,不能实时进行数据分析。而且,立方体只保留了聚合后的结果数据,导致无法查询明细数据。

技术演进

OLAP领域技术发展至今方兴未艾,分析型数据库百花齐放,众观OLAP的发展历程,可以分为两个阶段:

第一个可以称为传统关系型数据库阶段。在这个阶段中,OLAP主要基于以Oracle、MySQL为代表的一众关系型数据实现。在ROLAP架构下,直接使用这些数据库作为存储与计算的载体;在MOLAP架构下,则借助物化视图的形式实现数据立方体。在这个时期,不论是ROLAP还是MOLAP,在数据体量大、维度数目多的情况下都存在严重的性能问题,甚至存在根本查询不出结果的情况。

第二个可以称为大数据技术阶段。由于大数据处理技术的普及,人们开始使用大数据技术重构ROLAP和MOLAP。以ROLAP架构为例,传统关系型数据库就被Hive和SparkSQL这类新兴技术所取代。虽然,以Spark为代表的分布式计算系统,相比Oracle这类传统数据库而言,在面向海量数据的处理性能方面已经优秀很多,但是直接把它们作为面向终端用户的在线查询系统还是太慢了。我们的用户普遍缺乏耐心,如果一个查询响应需要几十秒甚至数分钟才能返回,那么这套方案就完全行不通。再看MOLAP架构,MOLAP背后也转为依托MapReduce或Spark这类新兴技术,将其作为立方体的计算引擎,加速立方体的构建过程。其预聚合结果的存储载体也转向HBase这类高性能分布式数据库。大数据技术阶段,主流MOLAP架构已经能够在亿万级数据的体量下,实现毫秒级的查询响应时间。尽管如此,MOLAP架构依然存在维度爆炸、数据同步实时性不高的问题。

不难发现,虽然OLAP在经历了大数据技术的洗礼之后,其各方面性能已经有了脱胎换骨式的改观,但不论是ROLAP还是MOLAP,仍然存在各自的痛点。

clickhouse

ClickHouse背后的研发团队是来自俄罗斯的Yandex公司,Yandex占据了本国47%以上的搜索市场,是现今世界上最大的俄语搜索引擎,而一般搜索引擎公司为了更好地帮助自身及用户分析网络流量,都会推出自家的在线流量分析产品,例如Google的Google Analytics、百度的百度统计。Yandex也不例外,Yandex.Metrica就是这样一款用于在线流量分析的产品。伴随着Yandex.Metrica业务的发展,其底层架构经历过数次调整,才逐渐形成了现在的clickhouse,clickhouse其具有ROLAP、在线实时查询、完整的DBMS、列式存储、不需要任何数据预处理、支持批量更新、拥有非常完善的SQL支持和函数、支持高可用、不依赖Hadoop复杂生态、开箱即用等许多特点。它解决了ROLAP、MOLAP各自的痛点,实现了大数据量下实时查询快,同步实时性高,数据量占用小的目标,满足了一款高性能、高可用OLAP数据库的一切诉求。

优势劣势

优势

  • 按列存储,非常适合聚合计算
  • 数据压缩空间大,网络io传输快,存储空间占用小
  • 写入速度快,单次写入可到50m-200m每秒
  • 充分调用cpu资源,查询速度极快
  • 提供了标准协议的SQL查询接口,上手快

劣势

  • 不支持事务
  • 不太支持高并发,官方建议100qps,简单短查询的的情况下能达到几百qps
  • 不擅长根据主键按行粒度进行查询(虽然支持)

特性

列式存储与数据压缩

对于一款高性能数据库来说是必不可少的特性。一个非常流行的观点认为,如果你想让查询变得更快,最简单且有效的方法是减少数据扫描范围和数据传输时的大小。

  • 按列存储与按行存储相比,前者可以有效减少查询时所需扫描的数据量,而分析统计数据本身就是只要少量的列,因此列式存储数据库也十分适合做统计分析。
  • 相应的,列式存储由于每列的数据格式都一致,因此重复性会更高,压缩率也更高,数据默认使用LZ4算法压缩,在Yandex.Metrica的生产环境中,数据总体的压缩比可以达到8:1(未压缩前17PB,压缩后2PB)

向量化执行引擎

向量化执行,可以简单地看作一项消除程序中循环的优化。为了实现向量化执行,需要利用CPU的SIMD指令。SIMD的全称是Single Instruction Multiple Data,即用单条指令操作多条数据。现代计算机系统概念中,它是通过数据并行以提高性能的一种实现方式(其他的还有指令级并行和线程级并行),它的原理是在CPU寄存器层面实现数据的并行操作,而cpu从寄存器访问数据的速度极快,所以利用CPU向量化执行的特性,可以显著提升程序性能。

关系模型与SQL查询

相比HBase和Redis这类NoSQL数据库,ClickHouse使用关系模型描述数据并提供了传统数据库的概念(数据库、表、视图和函数等)。与此同时,ClickHouse完全使用SQL作为查询语言(支持GROUP BY、ORDER BY、JOIN、IN等大部分标准SQL),这使得它平易近人,容易理解和学习。同时由于ClickHouse使用了关系模型,所以将构建在传统关系型数据库或数据仓库之上的系统迁移到ClickHouse的成本会变得更低。

多样化的表引擎

与MySQL类似,ClickHouse也将存储部分进行了抽象,把存储引擎作为一层独立的接口。ClickHouse共拥有合并树、内存、文件、接口和其他6大类20多种表引擎。其中每一种表引擎都有着各自的特点,用户可以根据实际业务场景的要求,选择合适的表引擎使用。

多线程与分布式

如果说向量化执行是通过数据级并行的方式提升了性能,那么多线程处理就是通过线程级并行的方式实现了性能的提升。相比基于底层硬件实现的向量化执行SIMD,线程级并行通常由更高层次的软件层面控制。现代计算机系统早已普及了多处理器架构,所以现今市面上的服务器都具备良好的多核心多线程处理能力。由于SIMD不适合用于带有较多分支判断的场景,ClickHouse也大量使用了多线程技术以实现提速,以此和向量化执行形成互补。

多主架构

ClickHouse则采用Multi-Master多主架构,集群中的每个节点角色对等,客户端访问任意一个节点都能得到相同的效果。这种多主的架构有许多优势,例如对等的角色使系统架构变得更加简单,不用再区分主控节点、数据节点和计算节点,集群中的所有节点功能相同。所以它天然规避了单点故障的问题,非常适合用于多数据中心、异地多活的场景。

在线查询

与其他数据库相比,ClickHouse拥有明显的优势。例如,Vertica这类商用软件价格高昂;SparkSQL与Hive这类系统无法保障90%的查询在1秒内返回,在大数据量下的复杂查询可能会需要分钟级的响应时间;而Elasticsearch这类搜索引擎在处理亿级数据聚合查询时则显得捉襟见肘。而ClickHouse即便是在复杂查询的场景下,它也能够做到极快响应,且无须对数据进行任何预处理加工。

数据分片与分布式查询

ClickHouse支持分片,而分片则依赖集群。每个集群由1到多个分片组成,而每个分片则对应了ClickHouse的1个服务节点。分片的数量上限取决于节点数量(1个分片只能对应1个服务节点)。

ClickHouse并不像其他分布式系统那样,拥有高度自动化的分片功能。ClickHouse提供了本地表(Local Table)与分布式表(Distributed Table)的概念。一张本地表等同于一份数据的分片。而分布式表本身不存储任何数据,它是本地表的访问代理,其作用类似分库中间件。借助分布式表,能够代理访问多个数据分片,从而实现分布式查询。

这种设计类似数据库的分库和分表,十分灵活,当数据量小时,使用单个节点的本地表(单个数据分片)即可满足业务需求;当数据量大时,再通过新增数据分片的方式分流数据,并通过分布式表实现分布式查询。

性能表现

读取性能

根据官方的数据,在1亿数据集体量的情况下,ClickHouse的平均响应速度(第1、2、3次平均)是Vertica的4.16倍、InfiniDB的24.38倍、Hive的208.43倍、MySQL的635.99倍。

写入性能

建议在至少 1000 行的数据包中插入数据,或者每秒不超过一个请求。从制表符分隔的转储插入 MergeTree 表时,插入速度可以为 50 到 200 MB/s。如果插入的行大小约为 1 KB,则速度将为每秒 50,000 到 200,000 行。如果行数很小,则每秒行数的性能会更高。为了提高性能,您可以并行进行多个 INSERT 查询,这些查询可以线性扩展。

https://raw.githubusercontent.com/lydon-GH/cloudimg/main/img/image-20220705163512055.png

吞吐量

吞吐量可以以每秒行数或每秒兆字节数来衡量。如果数据放在页面缓存中,一个不太复杂的查询在现代硬件上以大约 2-10 GB/s 的速度在单个服务器上处理未压缩数据(对于最简单的情况,速度可能达到 30 GB/秒)。如果数据没有放在页面缓存中,速度取决于磁盘子系统和数据压缩率。例如,如果磁盘子系统允许以 400 MB/s 的速度读取数据,并且数据压缩率为 3,则速度预计在 1.2 GB/s 左右。要获得以每秒行数为单位的速度,请将速度(以每秒字节数为单位)除以查询中使用的列的总大小。例如,如果提取 10 字节的列,则速度预计在每秒 100-2 亿行左右。

并发情况

在相同的条件下,ClickHouse 可以在单台服务器上每秒处理数百个查询(最好情况下可达数千个)。由于这种情况对于分析型 DBMS 来说并不典型,因此我们建议预计每秒最多 100 个查询。

其他性能测试参考:

ClickHouse 集群方案测评

集群

副本Replication & 分片Sharding

ClickHouse依靠ReplicatedMergeTree引擎族与ZooKeeper实现了复制表机制, 成为其高可用的基础.

ClickHouse像ElasticSearch一样具有数据分片(shard)的概念, 这也是分布式存储的特点之一, 即通过并行读写提高效率. ClickHouse依靠Distributed引擎实现了分布式表机制, 在所有分片(本地表)上建立视图进行分布式查询.

副本

Replicated Table & ReplicatedMergeTree Engines

不同于HDFS的副本机制(基于集群实现), Clickhouse的副本机制是基于表实现的. 用户在创建每张表的时候, 可以决定该表是否高可用.

创建本地表Local_table

CREATE TABLE IF NOT EXISTS {local_table} ({columns}) 
ENGINE = ReplicatedMergeTree('/clickhouse/tables/#_tenant_id_#/#__appname__#/#_at_date_#/{shard}/hits', '{replica}')
partition by toString(_at_date_) sample by intHash64(toInt64(toDateTime(_at_timestamp_)))
order by (_at_date_, _at_timestamp_, intHash64(toInt64(toDateTime(_at_timestamp_))))

支持复制表的引擎都是ReplicatedMergeTree引擎族, 具体可以查看官网:

ReplicatedMergeTree引擎族接收两个参数:

ZK中该表相关数据的存储路径, ClickHouse官方建议规范化, 例如: /clickhouse/tables/{shard}/[database_name]/[table_name].副本名称, 一般用{replica}即可.

ReplicatedMergeTree引擎族非常依赖于zookeeper, 它在zookeeper中存储了大量的数据,如表结构信息、元数据、操作日志、副本状态、数据块校验值、数据part merge过程中的选主信息.

同时, zookeeper又在复制表之下扮演了三种角色:元数据存储、日志框架、分布式协调服务

可以说当使用了ReplicatedMergeTree时, zookeeper压力特别重, 一定要保证zookeeper集群的高可用和资源。

数据同步的流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5KNz1nEy-1657353463896)(https://raw.githubusercontent.com/lydon-GH/cloudimg/main/img/image-20220705163914561.png)]

  1. 写入到一个节点
  2. 通过interserver HTTP port端口同步到其他实例上
  3. 更新zookeeper集群记录的信息

分片

Distributed Table & Distributed Engine

ClickHouse分布式表的本质并不是一张表, 而是一些本地物理表(分片)的分布式视图,本身并不存储数据. 分布式表建表的引擎为Distributed.

创建Distrbuted_table的示例:

CREATE TABLE IF NOT EXISTS {distributed_table} as {local_table} 
ENGINE = Distributed({cluster}, '{local_database}', '{local_table}', rand())

Distributed引擎需要以下几个参数:

Distributed引擎需要以下几个参数:

  • 集群标识符
  • 本地表所在的数据库名称
  • 本地表名称
  • 分片键(sharding key) - 可选
    该键与config.xml中配置的分片权重(weight)一同决定写入分布式表时的路由, 即数据最终落到哪个物理表上. 它可以是表中一列的原始数据(如site_id), 也可以是函数调用的结果, 如上面的SQL语句采用了随机值rand(). 注意该键要尽量保证数据均匀分布, 另外一个常用的操作是采用区分度较高的列的哈希值, 如intHash64(user_id).

4.1. 数据查询的流程

image-20220705164020656

  1. 各个实例之间会交换自己持有的分片的表数据
  2. 汇总到同一个实例上返回给用户

扩展

支持kafka,mysql,pg,s3,dbc

mysql

  • 支持mysql数据同步至clickhouse

  • 支持clickhoulse 查询,插入路数据到mysql数据库中

对象性存储

Amazon S3 或 Amazon Simple Storage Service 是 Amazon Web Services (AWS) 提供的一项服务,它通过 Web 服务接口提供对象存储。用户可以将基于 S3 的数据插入 ClickHouse 并使用 S3 作为导出目的地,从而允许与“数据湖”架构进行交互。此外,s3 可以提供“冷”存储层并协助分离存储和计算。

与s3对接

使用表函数远程操作

支持使用clickhouse作为查询入库查询s3数据,但直接从 s3 读取数据不太可能在更大的数据集上执行。

SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/nyc-taxi/trips_*.gz', 'TabSeparatedWithNames') LIMIT 10;

支持从 s3 存储桶中读取数据并使用远程函数插入到 clickhouse metritree

INSERT INTO trips SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/nyc-taxi/trips_*.gz', 'TabSeparatedWithNames') LIMIT 1000000;

支持从clickhouse流式传输到s3存储桶

INSERT INTO FUNCTION s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/csv/trips.csv.lz4', 's3_key', 's3_secret', 'CSV') SELECT * FROM trips LIMIT 10000;
表引擎

虽然这些s3函数允许对存储在 S3 中的数据执行临时查询,但对于更复杂的查询,它们在语法上是冗长的。用户可能自然希望将 S3 存储桶视为表,在现有查询中使用它。为了解决这个问题,ClickHouse 提供了 S3 表引擎。

支持查询s3数据
CREATE TABLE trips_raw
(
   `trip_id`               UInt32,
   `vendor_id`             Enum8('1' = 1, '2' = 2, '3' = 3, '4' = 4, 'CMT' = 5, 'VTS' = 6, 'DDS' = 7, 'B02512' = 10, 'B02598' = 11, 'B02617' = 12, 'B02682' = 13, 'B02764' = 14, '' = 15),
   `pickup_date`           Date,
   `pickup_datetime`       DateTime,
   `dropoff_date`          Date,
   `dropoff_datetime`      DateTime,
   `store_and_fwd_flag`    UInt8,
   `rate_code_id`          UInt8,
   `pickup_longitude`      Float64,
   `pickup_latitude`       Float64,
   `dropoff_longitude`     Float64,
   `dropoff_latitude`      Float64,
   `passenger_count`       UInt8,
   `trip_distance`         Float64,
   `fare_amount`           Float32,
   `extra`                 Float32,
   `mta_tax`               Float32,
   `tip_amount`            Float32,
   `tolls_amount`          Float32,
   `ehail_fee`             Float32,
   `improvement_surcharge` Float32,
   `total_amount`          Float32,
   `payment_type_`         Enum8('UNK' = 0, 'CSH' = 1, 'CRE' = 2, 'NOC' = 3, 'DIS' = 4),
   `trip_type`             UInt8,
   `pickup`                FixedString(25),
   `dropoff`               FixedString(25),
   `cab_type`              Enum8('yellow' = 1, 'green' = 2, 'uber' = 3),
   `pickup_nyct2010_gid`   Int8,
   `pickup_ctlabel`        Float32,
   `pickup_borocode`       Int8,
   `pickup_ct2010`         String,
   `pickup_boroct2010`     FixedString(7),
   `pickup_cdeligibil`     String,
   `pickup_ntacode`        FixedString(4),
   `pickup_ntaname`        String,
   `pickup_puma`           UInt16,
   `dropoff_nyct2010_gid`  UInt8,
   `dropoff_ctlabel`       Float32,
   `dropoff_borocode`      UInt8,
   `dropoff_ct2010`        String,
   `dropoff_boroct2010`    FixedString(7),
   `dropoff_cdeligibil`    String,
   `dropoff_ntacode`       FixedString(4),
   `dropoff_ntaname`       String,
   `dropoff_puma`          UInt16
) ENGINE = S3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/nyc-taxi/trips_{0..9}.tsv.gz', 'TabSeparatedWithNames', 'gzip');
SELECT payment_type, max(tip_amount) as max_tip FROM trips_raw GROUP BY payment_type;
支持插入

创建插入的clickhouse表

CREATE TABLE trips_dest
(
   `trip_id`               UInt32,
   `pickup_date`           Date,
   `pickup_datetime`       DateTime,
   `dropoff_datetime`      DateTime,
   `tip_amount`            Float32,
   `total_amount`          Float32
) ENGINE = S3('<bucket path>/trips.bin', 'Native');
INSERT INTO trips_dest SELECT trip_id, pickup_date, pickup_datetime, dropoff_datetime, tip_amount, total_amount FROM trips LIMIT 10;

需要注意

  • 不支持 ALTER 查询
  • 不支持 SAMPLE 操作
  • 没有索引的概念,即主要或跳过。

s3作为存储桶

ClickHouse 存储卷允许从 MergeTree 表引擎中抽象出物理磁盘。任何单个卷都可以由一组有序的磁盘组成。虽然原则上允许多个块设备潜在地用于数据存储,但这种抽象还允许其他存储类型,包括 S3。ClickHouse 数据部分可以根据存储策略在卷和填充率之间移动,从而创建存储层的概念。

存储层解锁了冷热架构,其中最新数据(通常也是查询最多的数据)仅需要高性能存储(例如 NVMe SSD)上的少量空间。随着数据的老化,查询时间的 SLA 会增加,查询频率也会增加。这种肥尾数据可以存储在速度较慢、性能较低的存储设备(如 HDD)或对象存储设备(如 S3)上。

配置配置文件
<clickhouse>
    <storage_configuration>
        ...
        <disks>
            <s3>
                <type>s3</type>
                <endpoint>https://sample-bucket.s3.us-east-2.amazonaws.com/tables/</endpoint>
                <access_key_id>your_access_key_id</access_key_id>
                <secret_access_key>your_secret_access_key</secret_access_key>
                <region></region>
                <metadata_path>/var/lib/clickhouse/disks/s3/</metadata_path>
                <cache_enabled>true</cache_enabled>
            <data_cache_enabled>true</data_cache_enabled>   
                <cache_path>/var/lib/clickhouse/disks/s3/cache/</cache_path>
            </s3>
        </disks>
        <policies>
            <s3_main>
                <volumes>
                    <main>
                        <disk>s3</disk>
                    </main>
                </volumes>
            </s3_main>
        </policies>
        ...
    </storage_configuration>
</clickhouse>
创建mergeTree表
CREATE TABLE trips_s3
(
   `trip_id` UInt32,
   `pickup_date` Date,
   `pickup_datetime` DateTime,
   `dropoff_datetime` DateTime,
   `pickup_longitude` Float64,
   `pickup_latitude` Float64,
   `dropoff_longitude` Float64,
   `dropoff_latitude` Float64,
   `passenger_count` UInt8,
   `trip_distance` Float64,
   `tip_amount` Float32,
   `total_amount` Float32,
   `payment_type` Enum8('UNK' = 0, 'CSH' = 1, 'CRE' = 2, 'NOC' = 3, 'DIS' = 4)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(pickup_date)
ORDER BY pickup_datetime
SETTINGS index_granularity = 8192, storage_policy='s3_main'
导入数据
INSERT INTO trips_s3 SELECT trip_id, pickup_date, pickup_datetime, dropoff_datetime, pickup_longitude, pickup_latitude, dropoff_longitude, dropoff_latitude, passenger_count, trip_distance, tip_amount, total_amount, payment_type FROM s3('https://ch-nyc-taxi.s3.eu-west-3.amazonaws.com/tsv/trips_{0..9}.tsv.gz', 'TabSeparatedWithNames') LIMIT 1000000;
查询统计数据
SELECT passenger_count, avg(tip_amount) as avg_tip, avg(total_amount) as avg_amount FROM trips_s3 GROUP BY passenger_count;

在对象存储这一块,AWS 的 S3、华为云的 OBS、腾讯云的 COS 以及 阿里云的 OSS,ClickHouse 现在已经全面适配。

数据统计查询场景clickhouse实现

统计总数等聚合场景

使用count、sum等常见聚合函数来实现功能

普通分组

使用group by函数分组统计

按时间间隔分组

使用分时统计函数 toStartOfInterval(t, INTERVAL 5 minute)

image-20220705172708681

toStartOfInterval(t, INTERVAL 3 hour)

image-20220705172815844

toStartOfInterval(t, INTERVAL 1 day)

image-20220705172900577

数据聚合

在es等不擅长聚合的存储库中,需要预先通过手动编写聚合逻辑并写入库中,而在clickhouse可通过将AggregatingMergeTree引擎作为物化视图的表引擎,架在明细表上对外提供查询,明细表作为写入接口来完成数据聚合的实现,如下图所示:

image-20220705175637280

  1. 在新增数据时,面向的对象是底表MergeTree,在底表新增的数据会自动同步到物化视图,并按照AggregatingMergeTree引擎的规则处理。
  2. 在查询数据时,面向的对象是物化视图AggregatingMergeTree

实例说明:

1、首先,建立明细数据表,也就是俗称的底表:


CREATE TABLE agg_table_basic(
    id String,
    city String,
    code String,
    value UInt32
)ENGINE = MergeTree()
PARTITION BY city
ORDER BY (id,city)

2、接着,新建一张物化视图:


CREATE MATERIALIZED VIEW agg_view
ENGINE = AggregatingMergeTree() 
PARTITION BY city
ORDER BY (id,city)
AS SELECT
    id,
    city,
    uniqState(code) AS code,
    sumState(value) AS value
FROM agg_table_basic
GROUP BY id, city

上例中列字段id和city是聚合条件,等同于下面的语义:


GROUP BY id,city

而code和value是聚合字段,其语义等同于:


UNIQ(code), SUM(value)

3、然后新增数据

INSERT INTO TABLE agg_table_basic 
VALUES('A000','wuhan','code1',100),('A000','wuhan','code2',200),('A000','zhuhai', 'code1',200)

4、在查询数据

ELECT id, sumMerge(value), uniqMerge(code) FROM agg_view GROUP BY id, city
┌─id──┬─sumMerge(value)──┬──uniqMerge(code)─┐
│ A000  │             200    │               1     │
│ A000  │             300    │               2     │
└─────┴────────────┴────────────┘

别人使用clickhouse的实践示例

Clickhouse 实践之路

支撑700亿数据量的ClickHouse高可用架构实践
腾讯云Clickhouse 实践

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值