OceanBase——双十一海量交易背后的复杂技术

前言:

其实我早就想整理这篇博客了,计划着在十一月初就搞定的,可是双十一活动太多,前几天一直在淘宝和天猫看要买的东西。这篇博客也是断断续续的整理、记录着,到今天才算是把它整理出来了。这篇博文主要介绍了 OceanBase 的系统框架,以及框架内各个成员的功能。主要是学习它的框架,以及分析问题、解决问题的方法。以下内容需要掌握基本的分布式系统相关知识,最好是阅读过分布式文件系统或者分布式数据库等相关书籍。以下内容源自于《大规模分布式存储系统》。这本书对我的帮助很大,首先它的作者是阿里巴巴的高级技术专家。其次是这本书的内容吸引了我。双十一是一个巨大的挑战,在这样海量交易的背后隐藏着的复杂技术,难道你不感兴趣吗?如果对分布式相关知识比较感兴趣的话,我建议可以先看看其基本概念,这类书籍有很多,如果是想系统的学习这方面的知识,我推荐《分布式系统概念与设计》。在了解相关知识后,也可以看看 Google 比较有名的论文,例如 GFS,BigTable 和 MapReduce 等,可以在我的专栏分类——分布式/大数据中找到这些论文的中文翻译。本文主要介绍的是分布式存储系统,那么就让我们先来了解下分布式存储系统的分类。

 

分布式存储系统的数据可以分为以下三类:

(1) 结构化数据一般存储在关系数据库中,可以用二维关系表结构来表示。结构化数据的模式和内容是分开的,数据模式需要预先定义。 国内开源的分布式数据库——阿里巴巴的 OceanBase

(2) 非结构化数据:包括所有格式的办公文档、文本、图片、音频和视频信息等。这类数据以对象的形式组织,对象之间没有关联,这样的数据一般称为 Blob(Binary Large Object) 数据。分布式文件系统用于存储 Blob 数据,典型的系统有 Fackbook Haystack、GFS 以及 HDFS。

(3) 半结构化数据:介于非结构化数据和结构化数据之间,HTML 文档就属于半结构化数据。它的模式结构和内容混在一起,没有明显的区分,也不需要预先定义数据的模式结构。典型的系统是 Google Bigtable

 

 

背景简介

OceanBase 是阿里集团研发的可扩展的关系数据库,实现了数千亿条记录、数百TB数据上的跨行跨表事务,截止到 2012年8月,支持了收藏夹、直通车报表、天猫评价等 OLTP 和 OLAP 在线业务,线上数据量已经超过了一千亿条。

  • OLTP (Online Transaction Processing): 联机事务处理,针对小数据进行增删改。如银行转账。
  • OLAP (Online Analytic Processing) :联机分析处理,针对大数据的分布式处理,通常为 select。

从模块划分角度看,OceanBase 可以划分为四个模块:主控服务器更新服务器基线服务器合并服务器OceanBase 系统内部 按照时间线 将数据划分为 基线数据 增量数据基线数据是只读的所有的修改更新到增量数据中系统内部通过合并操作定期将增量数据融合到基线数据中。

淘宝是一个迅速发展的网站。它的数据规模及其访问量对关系数据库提出了很大挑战:数百亿条的记录、数十 TB 的数据、数万 TPS、数十万 QPS 让传统的关系数据库不堪重负,单纯的硬件升级已经无法使问题得到解决,分库分表也并不总是奏效的。下面有一个实际例子。注意:以上数据量是 2012 年左右的规模。

淘宝收藏夹是淘宝线上应用之一,淘宝用户在其中保存自己感兴趣的商品,以便下次快速访问、对比和购买等,用户可以展示和编辑自己的收藏。淘宝收藏夹数据库包含了收藏 info 表 (一条一条的收藏信息)和收藏 item 表 (被收藏的宝贝和店铺)等:

  • 收藏 info 表保存收藏信息条目,数百亿条。
  • 收藏 item 表保存收藏的商品和店铺的详细信息,数十亿条。
  • 热门商品可能被多达数十万买家收藏。
  • 每个用户可以收藏千个商品。
  • 商品的价格、收藏人气等信息随时变化。

如果用户选择按商品价格排序后展示,那么数据库需要从收藏 item 表中读取收藏的商品的价格等最新信息,然后进行排序处理。如果用户的收藏条目比较多,那么查询对应的 item 的时间会较长:假设如果每条 item 查询时间是 5ms,则 4000 条的查询时间可能达到 20s,如果真的是这样,用户体验会很差。

若把收藏夹的商品的详细信息实时冗余到收藏 info 表,则上述查询收藏 item 表的操作就不再需要了。但是,由于许多热门商品可能有几千到几十万人收藏,那些热门商品的价格等信息的变动可能导致收藏 info 表的大量修改,并压垮数据库。为此,阿里巴巴需要研发适合互联网规模的分布式数据库,这个数据库不仅要能解决收藏夹面临的挑战,还要能做到可扩展、低成本、易用,并能够应用到更多的业务场景。

 

 

设计思路

OceanBase 的目标是支持数百TB的数据量以及数十万 TPS、数百万 QPS 的访问量,无论是数据量还是访问量,即使采用非常昂贵的小型机甚至是大型机,单台关系数据库系统都无法承受。

一种常见的做法是根据业务特点对数据库进行水平拆分,通常的做法是根据某个业务字段 (通常取用户编号,user_id) 哈希后取模,根据取模的结果将数据分布到不同的数据库服务器上,客户端请求通过数据库中间层路由到不同的分区。这种方式目前还存在一定的弊端:

(1) 数据和负载增加后添加机器的操作比较复杂,往往需要人工介入;

(2) 有些范围查询需要访问几乎所有的分区,例如,按照 user_id 分区,查询收藏了一个商品的所有用户可能需要访问所有的分区;

(3) 目前广泛使用的关系数据库存储引擎都是针对机械硬盘的特点设计的,不能够完全发挥新硬件(SSD)的能力。

关于(2),需要详细的说明一下。若使用 user_id 哈希后取模,根据取模的结果将数据分布到不同的数据库服务器上。这样用户在查看收藏夹里的商品时,可以请求相应数据库服务器,从而获取到数据。但是,一旦有个商品的价格发生了变动,那么收藏了该商品的用户的数据也要发生改变。试想一个商品价格的改动,需要修改所有收藏该商品的用户的收藏夹信息,这个操作可能是非常简单的,但是考虑到淘宝商品种类之多,用户量之大,该问题就变的异常费时且困难。而且到了双十一这样的特殊时刻,这样的操作复杂程度也会随之上升。

另外一种做法是参考分布式表格系统的做法,例如 Google Bigtable 系统,将大表划分为几万、几十万甚至几百万个子表,子表间按照主键有序,如果某台服务器发生故障,它上面服务的数据能够在很短的时间内自动迁移到集群中的其他服务器。这种方式解决了可扩展性的问题,少量突发的服务器故障或者增加服务器对使用者基本是透明的,能够轻松应对促销或者热点事件等突发流量增长。另外,由于子表是按照主键有序分布的,很好地解决了范围查询的问题。

分布式表格系统虽然解决了可扩展性问题,但往往无法支持事务,例如 Bigtable 只支持单行事务,针对同一个 user_id 下的多条记录的操作都无法保证原子性。 OceanBase 希望能够支持跨行跨表事务,这样使用起来会比较方便。

最直接的做法是在 Bigtable 开源实现(如 HBase 或者 Hypertable)的基础上引入两阶段提交协议支持分布式事务,这种思路在 Google 的 Percolator 系统中得到了体现。然而,Percolator 系统中事务的平均响应时间达到 2~5 秒,只能应用在类似网页建库这样的半线上业务中。另外,Bigtable 的开源实现也不够成熟,单台服务器能够支持的数据量有限,单个请求的最大响应时间很难得到保证,机器故障等异常处理机制也有很多比较严重的问题。总体上看,这种做法的工作量和难度超出了项目的承受能力。

通过分析,开发人员发现,虽然在线业务的数据量十分庞大,例如几十亿条、上百亿条甚至更多记录,但最近一段时间(例如一天)的修改量往往并不多,通常不超过几千万条到几亿条,因此,OceanBase 决定采用单台更新服务器来记录最近一段时间的修改增量,而以前的数据保持不变,以前的数据称为基线数据。基线数据以类似分布式文件系统的方式存储于多台基线数据服务器中,每次查询都需要把基线数据和增量数据融合后返回给客户端。这样,写事务都集中在单台更新服务器上,避免了复杂的分布式事务,高效地实现了跨行跨表事务;另外,更新服务器上的修改增量能够定期分发到多台基线数据服务器中,避免成为瓶颈,实现了良好的扩展性。

 

 

整体架构图

OceanBase 由以下几个部分组成:

(1) 客户端:用户使用 OceanBase 的方式和 MySQL 数据库完全相同,支持 JDBC、C 客户端访问,等等。基于 MySQL 数据库开发的应用程序、工具能够直接迁移到 OceanBase

(2) RootServer:管理集群中的所有服务器,子表数据分布以及副本管理。RootServer 一般为一主一备,主备之间数据强同步。

(3) UpdateServer:存储 OceanBase 系统的增量更新数据。UpdateServer 一般为一主一备,主备之间可以配置不同的同步模式。部署时,UpdateServer 进程和 RootServer 进程往往共用物理服务器

(4) ChunkServer:存储 OceanBase 系统的基线数据。基线数据一般存储两份或者三份,可配置。

(5) MergeServer:接收并解析用户的 SQL 请求,经过词法分析、语法分析、查询优化等一系列操作后转发给相应的 ChunkServer 或者 UpdateServer。如果请求的数据分布在多台 ChunkServer 上,MergeServer 还需要对多台 ChunkServer 返回的结果进行合并。客户端和 MergeServer 之间采用原生的 MySQL 通信协议,MySQL 客户端可以直接访问 MergeServer。

OceanBase 支持部署多个机房,每个机房部署一个包含 RootServer、MergeServer、ChunkServer 以及 UpdateServer 的完整 OceanBase 集群,每个集群由各自的 RootServer 负责数据划分、负载均衡、集群服务器管理等操作,集群之间数据同步通过主集群的主 UpdateServer 往备集群同步增量更新操作日志实现。客户端配置了多个集群的 RootServer 地址列表,使用者可以设置每个集群的流量分配比例,客户端根据这个比例将读写操作发往不同的集群。

 

 

客户端

OceanBase 客户端与 MergeServer 通信,目前主要支持以下几种客户端:

MySQL 客户端:MergeServer 兼容 MySQL 协议,MySQL 客户端及相关工具(如 Java 数据库访问方式 JDBC)只需要将服务器的地址设置为任意一台 MergeServer 的地址,就可以直接使用。

Java 客户端:OceanBase 内部部署了多台 MergeServer,Java 客户端提供对 MySQL 标准 JDBC Driver 的封装,并提供流量分配、负载均衡、MergeServer 异常处理等功能。简单来讲,Java 客户端首先按照一定的策略定位到某台 MergeServer,接着调用MySQL JDBC Driver 往这台 MergeServer 发送读写请求。Java 客户端实现符合 JDBC 标准,能够支持 Spring、iBatis 等 Java 编程框架。

C 客户端:OceanBase C 客户端的功能和Java客户端类似。它首先按照一定的策略定位到某台 MergeServer,接着调用 MySQL 标准 C 客户端往这台 MergeServer 发送读写请求。C 客户端的接口和 MySQL 标准 C 客户端接口完全相同,因此,能够通过 LD_PRELOAD 的方式将应用程序依赖的 MySQL 标准 C 客户端替换为 OceanBase C 客户端,而无需修改应用程序的代码。

OceanBase 集群有多台 MergeServer,这些 MergeServer 的服务器地址存储在 OceanBase 服务器端的系统表(与 Oracle 的系统表类似,存储 OceanBase 系统的元数据)内。OceanBase Java/C 客户端首先请求服务器端获取 MergeServer 地址列表,接着按照一定的策略将读写请求发送给某台 MergeServer,并负责对出现故障的 MergeServer 进行容错处理。

Java/C 客户端访问 OceanBase 的流程大致如下:

(1) 请求 RootServer 获取集群中 MergeServer 的地址列表。

(2) 按照一定的策略选择某台 MergeServer 发送读写请求。客户端与 MergeServer 之间的通信协议兼容原生的 MySQL 协议,因此,只需要调用 MySQL JDBC Driver 或者 MySQL C 客户端这样的标准库即可。客户端支持的策略主要有两种:随机以及一致性哈希。一致性哈希的目的是将相同的 SQL 请求发送到同一台 MergeServer,方便 MergeServer 对查询结果进行缓存。

(3) 如果请求 MergeServer 失败,则从 MergeServer 列表中重新选择一台 MergeServer 重试;如果请求某台 MergeServer 失败超过一定的次数,将这台 MergeServer 加入黑名单并从 MergeServer 列表中删除。另外,客户端会定期请求 RootServer 更新 MergeServer 地址列表。

 

 

RootServer

RootServer 的功能主要包括:集群管理、数据分布以及副本管理。

RootServer 管理集群中的所有 MergeServer、ChunkServer 以及 UpdateServer。每个集群内部同一时刻只允许一个 UpdateServer 提供写服务,这个 UpdateServer 称为主 UpdateServer。这种方式通过牺牲一定的可用性获取了强一致性。RootServer 通过租约机制选择唯一的主 UpdateServer,当原先的主 UpdateServer 发生故障后,RootServer 能够在原先的租约失效后选择一台新的 UpdateServer 作为主 UpdateServer。另外,RootServer 与 MergeServer & ChunkServer 之间保持心跳,从而能够感知到在线和已经下线的 MergeServer & ChunkServer 机器列表。

OceanBase 内部使用主键对表格中的数据进行排序和存储,主键由若干列组成并且具有唯一性。在 OceanBase 内部,基线数据按照主键排序并且划分为数据量大致相等的数据范围,称为子表。每个子表的默认大小是 256 MB(可配置)。OceanBase 的数据分布方式与 Bigtable 一样采用顺序分布,不同的是,OceanBase 没有采用根表(RootTable)+元数据表(MetaTable)两级索引结构,而是采用根表一级索引结构。

如图所示,主键在 [1,100] 之间的表格被划分为四个子表。RootServer 中的根表记录了每个子表所在的 ChunkServer 位置信息,每个子表包含多个副本(一般为三个副本,可配置),分布在多台 ChunkServer 中。当其中某台 ChunkServer 发生故障时, RootServer 能够检测到,并且触发对这台 ChunkServer 上的子表增加副本的操作;另外,RootServer 也会定期执行负载均衡,选择某些子表从负载较高的机器迁移到负载较低的机器上。

RootServer 采用一主一备的结构,主备之间数据强同步,并且通过 Linux HA(http://www.linux-ha.org) 软件实现高可用性。主备 RootServer 之间共享 VIP,当主 RootServer 发生故障后,VIP 能够自动漂移到备 RootServer 所在的机器,备 RootServer 检测到以后切换为主 RootServer 提供服务。

 

 

MergeServer

MergeServer 的功能主要包括:协议解析、SQL 解析、请求转发、结果合并、多表操作等。

OceanBase 客户端与 MergeServer 之间的协议为 MySQL 协议。MergeServer 首先解析 MySQL 协议,从中提取出用户发送的 SQL 语句,接着进行词法分析和语法分析,生成 SQL 语句的逻辑查询计划和物理查询计划,最后根据物理查询计划调用 OceanBase 内部的各种操作符。

MergeServer 缓存了子表分布信息,根据请求涉及的子表将请求转发给该子表所在的 ChunkServer。如果是写操作,还会转发给 UpdateServer。某些请求需要跨多个子表,此时 MergeServer 会将请求拆分后发送给多台 ChunkServer,并合并这些 ChunkServer 返回的结果。如果请求涉及多个表格,MergeServer 需要首先从 ChunkServer 获取每个表格的数据,接着再执行多表关联或者嵌套查询等操作。

MergeServer 支持并发请求多台 ChunkServer,即将多个请求发给多台 ChunkServer,再一次性等待所有请求的应答。另外,在 SQL 执行过程中,如果某个子表所在的 ChunkServer 出现故障,MergeServer 会将请求转发给该子表的其他副本所在的 ChunkServer。这样,ChunkServer 故障是不会影响用户查询的。MergeServer 本身是没有状态的,因此,MergeServer 宕机不会对使用者产生影响,客户端对自动将发生故障的 MergeServer 屏蔽掉。

 

 

ChunkServer

ChunkServer 的功能包括:存储多个子表,根据读取服务,执行定期合并以及数据分发。

OceanBase 将大表划分为大小约为 256MB 的子表,每个子表由一个或者多个 SSTable 组成(一般为一个),每个 SSTable 有多个块(Block,大小为4KB~64KB之间,可配置)组成,数据在 SSTable 中按照主键有序存储。查找某一行数据时,需要首先定位这一行所属的子表,接着在相应的 SSTable 中执行二分查找。SSTable 支持两种缓存模式,块缓存以及行缓存。块缓存以块为单位缓存最近读取的数据,行缓存以行为单位缓存最近读取的数据。

MergeServer 将每个子表的读取请求发送到子表所在的 ChunkServer,ChunkServer 首先读取 SSTable 中包含的基线数据,接着请求 UpdateServer 获取相应的增量更新数据,并将基线数据与增量更新融合后得到最终结果。

由于每次读取都需要从 UpdateServer 中获取最新的增量更新,为了保证读取性能,需要限制 UpdateServer 中增量更新的数据量,最好能够全部存放在内存中。OceanBase 内部会定期触发合并或者数据分发操作,在这个过程中,ChunkServer 将从 UpdateServer 获取一段时间之前的更新操作。通常情况下,OceanBase 集群会在每天的服务低峰期(凌晨1:00开始)执行一次合并操作。

 

 

UpdateServer

UpdateServer 是集群中唯一能够接受写入的模块,每个集群中只有一个主 UpdateServer。UpdateServer 中的更新操作首先写入到内存表,当内存表的数据量超过一定值时,可以生成快照文件并转储到 SSD 中。快照文件的组织方式与 ChunkServer 中的 SSTable 类似,因此,这些快照文件也称为 SSTable。另外,由于数据行的某些列被更新,某些列没被更新,SSTable 中存储的数据行是稀疏的,称为稀疏型 SSTable。

为了保证可靠性,主 UpdateServer 更新内存表之前需要首先写操作日志,并同步到备 UpdateServer。当主 UpdateServer 发生故障时,RootServer 上维护的租约将失效,此时,RootServer 将从备 UpdateServer 列表中选择一台最新的备 UpdateServer 切换为主 UpdateServer 继续提供写服务。UpdateServer 宕机重启后需要首先加载转储的快照文件( SSTable 文件),接着回放快照点之后的操作日志。

 

 

定期合并&数据分发

定期合并和数据分发都是将 UpdateServer 中的增量更新分发到 ChunkServer 中的手段,二者的整体流程比较类似:

(1) UpdateServer 冻结当前的活跃内存表,生成冻结内存表,并开启新的活跃内存表,后续的更新操作都写入新的活跃内存表。

(2) UpdateServer 通知 RootServer 数据版本发生了变化,之后 RootServer 通过心跳消息通知 ChunkServer。

(3) 每台 ChunkServer 启动定期合并或者数据分发操作,从 UpdateServer 获取每个子表对应的增量更新数据。

定期合并与数据分发两者之间的不同点在于,数据分发过程中 ChunkServer 只是将 UpdateServer 中冻结内存表中的增量更新数据缓存到本地,而定期合并过程中 ChunkServer 需要将本地 SSTable 中的基线数据与冻结内存表的增量更新数据执行一次多路归并,融合后生成新的基线数据并存到新的 SSTable 中。定期合并对系统服务能力影响很大,往往安排在每天服务低峰期执行,而数据分发可以不受限制。

如图,活跃的内存表冻结后生成冻结内存表,后续的写操作进入新的活跃内存表。定期合并过程中 ChunkServer 需要读取 UpdateServer 中冻结内存表的数据、融合后生成新的子表,即:新子表 = 旧子表 + 冻结内存表

虽然定期合并过程中各个 ChunkServer 的各个子表合并时间和完成时间可能都不相同,但并不影响读取服务。如果子表没有合并完成,那么使用旧子表,并且读取 UpdateServer 中的冻结内存表以及新的活跃内存表;否则,使用新子表,只读取新的活跃内存表,即:查询结果 = 旧子表 + 冻结内存表 + 新的活跃内存表 = 新子表 + 新的活跃内存表。

 

 

一致性选择

Eric Brewer 教授的 CAP 理论指出,在满足分区可容忍性的前提下,一致性和可用性不可兼得。

虽然目前大量的互联网项目选择了弱一致性,但这是底层存储系统,比如 MySQL 数据库,在大数据量和高并发需求压力之下的无奈选择。弱一致性给应用带来了很多麻烦,比如数据不一致时需要人工订正数据。如果存储系统既能够满足大数据量和高并发的需求,又能够提供强一致性,且硬件成本相差不大,用户毫不犹豫地选择它。强一致性将大大简化数据库的管理,应用程序也会因此而简化。因此,OceanBase 选择支持强一致性和跨行跨表事务。

OceanBase UpdateServer 为主备高可用架构,修改操作流程如下:

(1) 将修改操作的操作日志(redo日志)发送到备机;

(2) 将修改操作的操作日志写入主机硬盘;

(3) 将操作日志应用到主机的内存表中;

(4) 返回客户端写入成功。

OceanBase 要求将操作日志同步到主备的情况下才能够返回客户端写入成功,即使主机出现故障,备机自动切换为主机,也能够保证新的主机拥有以前所有的修改操作,严格保证数据不丢失。另外,为了提高可用性,OceanBase 还增加了一种机制,如果主机往备机同步操作日志失败,比如备机故障或者主备之间网络故障,主机可以将备机从同步列表中剔除,本地更新成功后就返回客户端写入成功。主机将备机剔除前需要通知 RootServer,后续如果主机故障,RootServer 能够避免将不同步的备机切换为主机。

OceanBase 的高可用机制保证主机、备机以及主备之间网络三者之中的任何一个出现故障都不会对用户产生影响,然而,如果三者之中的两个同时出现故障,系统可用性将受到影响,但仍然保证数据不丢失。如果应用对可用性要求特别高,可以增加备机数量,从而容忍多台机器同时出现故障的情况。

OceanBase 主备同步也允许配置为异步模式,支持最终一致性。这种模式一般用来支持异地容灾。例如,用户请求通过杭州主站的机房提供服务,主站的 UpdateServer 内部有一个同步线程不停地将用户更新操作发送到青岛机房。如果杭州机房整体出现不可恢复的故障,比如地震,还能够通过青岛机房恢复数据并继续提供服务。另外,OceanBase 所有写事务最终都落到了 UpdateServer,而 UpdateServer 逻辑上是一个单点,支持跨行跨表事务,实现上借鉴了传统关系数据库的做法。

 

 

数据结构

OceanBase 数据分为基线数据和增量数据两个部分,基线数据分布在多台 ChunkServer 上,增量数据全部存放在一台 UpdateServer 上。如图,系统中有 5 个子表,每个子表有 3 个副本,所有的子表分布到 4 台 ChunkServer 上。RootServer 中维护了每个子表所在的 ChunkServer 的位置信息,UpdateServer 存储了这 5 个子表的增量更新。

不考虑数据复制,基线数据的数据结构如下:

  • 每个表格按照主键组成一颗分布式 B+ 树,主键由若干列组成;
  • 每个叶子节点包含表格一个前开后闭的主键范围 (rk1,rk2] 内的数据;
  • 每个叶子节点称为一个子表 (tablet),包含一个或者多个 SSTable;
  • 每个 SSTable 内部按主键范围有序划分为多个块并内建块索引;
  • 每个块的大小通常在 4~64KB 之间并内建块内的行索引;
  • 数据压缩以块为单位,压缩算法由用户指定并可随时变更;
  • 叶子节点可能合并或者分裂;
  • 所有叶子节点基本上是均匀的,随机分布在多台 ChunkServer 机器上;
  • 通常情况下每个叶子节点有 2~3 个副本;
  • 叶子节点是负载平衡和任务调度的基本单元;
  • 支持布隆过滤器的过滤。

 

增量数据的数据结构如下:

  • 增量数据按照时间从旧到新划分为多个版本;
  • 最新版本的数据为一颗内存中的 B+ 树,称为活跃 MemTable;
  • 用户的修改操作写入活跃 MemTable,到达一定大小后,原有的活跃 MemTable 将被冻结,并开启新的活跃 MemTable 接受修改操作;
  • 冻结的 MemTable 将以 SSTable 的形式转储到 SSD 中持久化;
  • 每个 SSTable 内部按主键范围有序划分为多个块并内建块索引,每个块的大小通常为 4~8KB 并内建块内行索引,一般不压缩;
  • UpdateServer 支持主备,增量数据通常为 2 个副本,每个副本支持 RAID1 存储。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Tyler_Zx

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

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

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

打赏作者

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

抵扣说明:

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

余额充值