《DDIA-数据密集型应用系统设计》

第一部分

可靠性、可扩展性、可维护性

什么算是"数据密集型" (data-intensive )应用?
对于一个应用系统,如果“数据”是其成败决定性因素,包括数据的规模 、 数据的复杂度或者数据产生与变化的速率等,我们就可以称为“数据密集型应用系统” ;

知其然,知其所以然

可靠性 (Reliability)
当出现意外情况如硬件、软件故障、人为失误等,系统应可以继续正常运转:虽然性能可能有所降低,但确保功能正确。

可扩展性 (Scalability)
随着规模的增长 ,例如数据量 、流量或复杂性,系统应以合理的方式来匹配这种增长。

可维护性 (Maintainability)
随着时间的推移,许多新的人员参与到系统开发和运维, 以维护现有功能或适配新场景等,系统都应高效运转。

硬件故障

当我们考虑系统故障时,对于硬件故障总是很容易想到 : 硬盘崩溃,内存故障,电网停电,甚至有人误拔掉了网线。任何与大型数据中心合作过的人都可以告诉你,当有很多机器时,这类事情迟早会发生。
有研究证明硬盘的平均无故障时间( MTTF )约为 10 ~ 50年。因此,在一个包括10,000个磁盘的存储集群中,我们应该预期平均每天有一个磁盘发生故障。我们的第 一个反应通常是为硬件添加冗余来减少系统故障率。 例如对磁盘配置RAID ,服务器配备双电源,甚至热插拔CPU, 数据中心添加备用电源、发电机等。当 一个组件发生故障,备用组件可以快速接管,之后再更换失效的组件。这种方法可能并不能完全防止硬件故障所引发的失效,但还是被普遍采用,且在实际中也确实可以让系统不间断运行长达数年。

由于软件错误,导致当输入特定值时应用服务器总是崩溃。 例如2012年6月30日发生闰秒,由于Linux 内核中的一个bug ,导致了很多应用程序在该时刻发生挂起。

描述负载

我们以Twitter为例,使用其2012年 l l 月 发布的数据。 Twitter的两个典型业务操作是:
1、发推:用户可以快速推送新消息到所有的关注者,平均请求大约 4.6k/s,峰值约 12k/s。
2、主页时间线浏览:平均请求 300k/s 来查看关注对象的最新推。
Twitter扩展性的挑战重点在于巨大的扇出结构:每个用户会关注很多人,也有许多粉丝。
此时大概有两种处理方案:
1、将发送的新推插入到全局的推集合中,当用户查看时间线时,首先查找所有的关注对象,列出这些人的所有推 ,最后以时间为序来排序合并。
2、对每个用户的时间线维护一个缓存,当用户推送新推时,查询其关注者,将新推插入到每个关注者的时间线缓存中。因为已经预先将结果取出,之后访问时间线性能非常快。
Twitter在其第一个版本使用了方案1 ,但发现主页时间线的读负载压力与日俱增,系统优化颇费周折,因此转而采用第二种方法并加以改造 。普通人第二种,粉丝数多的第一种。

批处理系统如Hadoop 中 ,我们通常关心吞吐量,即每秒可处理的记录条数。
在线系统通常更看重服务的响应时间,即客户端从发送请求到接收响应之间的间隔 。

吞吐与延迟

响应时间和延迟容易说淆使用,但它们并不完全一样。
通常响应时间是客户端看到的 :除了处理请求时间外,还包括来回网络延迟和各种排队延迟 。
延迟则是请求花费在处理上的时间。

采用较高的响应时间百分位数很重要,因为它们直接影响用户的总体服务体验。亚马逊采用 99.9百分位数来定义其内部服务的响应时间标准,或许它仅影响 1000个请求中的 1个。但是考虑到请求最慢的客户往往是购买了更多的商品,因此数据量更大。换言之, 他们是最有价值的客户。让这些客户始终保持愉悦的购物体验显然非常重要 : 亚马逊还注意到,响应时间每增加100ms ,销售额就会下降了约 1 %,其他研究则表明,1s的延迟增加等价于客户满意度下降 16%。

可维护性

软件的大部分成本并不在最初的开发阶段,而是在于整个生命周期内持续的投入,这包括维护与缺陷修复,监控系统来保持正常运行、故障排查、适配新平台、搭配新场景、技术缺陷的完善以及增加新功能等。

可以从软件设计时开始考虑,尽可能较少维护期间的麻烦预测未来可能的问题,并在问题发生之前即使解决(例如容量规划)。制定流程来规范操作行为,并保持生产环境稳定 。
提供良好的文档和易于理解的操作模式,诸如“如果我做了X ,会发生Y”。

第二章 数据模型与查询语言

大多数应用程序是通过一层一层叠加数据模型来构建的 。
1.程序开发人员,观测现实世界,把人员、货物、行为等,通过对象或数据结构,以及操作这些数据结构的API来建模 。
2.当需要存储这些数据结构时,采用关系型数据库中的表、图模型、JSON或XML文档来表示。
3.数据库工程师接着决定用何种内存、磁盘或网络的字节格式来表示上述JSON/XML/关系/图形数据 。数据表示需要支持多种方式的查询、搜索、操作和处理数据。
4.在更下一层,硬件工程师则需要考虑用电流、光脉冲、磁场等来表示字节。

基本思想相同 : 每层都通过提供一个简洁的数据模型来隐藏下层的复杂性

对象-关系不匹配
现在大多数应用开发都采用面向对象的编程语言 ,由于兼容性问题,普遍对SQL数据模型存在抱怨: 如果数据存储在关系表中 , 那么应用层代码中的对象与表、行和列的数据库模型之间需要一个笨拙的转换层。
面向对象编程语言中的数据结构是对象,每个对象可以包含属性和方法。关系型数据库的数据结构是表,表由行(记录)和列(字段)组成。
数据类型映射问题:面向对象语言和关系型数据库使用不同的数据类型,比如Java的String类型和MySQL的VARCHAR类型。这就需要你在存储或者检索数据时进行类型转换,以确保数据能够正确地被处理。

Hibernate、MyBatis这些对象-关系映射( ORM )框架一定程度上可以简化这样的问题处理,但是他们并不能完全隐藏两个模型之间的差异。

关系模型所做的则是定义了所有数据的格式:关系(表)只是元组(行)的集合 ,仅此而已。没有复杂的嵌套结构, 也没有复杂的访问路径。可以读取表中的任何一行或者所有行,支持任意条件查询。

第三章

一个最基本的数据库只需做两件事情:向它插入数据时,它就保存数据查询时,它应该返回那些数据
比如:一个纯文本文件,其中每行包含一个key-value对 ,用逗号分隔。每次调用 db set 追加新内容到文件末尾,这样如果多次更新某个键,旧版本的值不会被覆盖。查询时去看文件中最后一次出现的键来找到最新的值。

索引

问题1
如果日志文件保存了大量的记录,那么 db_get 函数的性能会非常差。每次想查找一个键, db_get必须从头到尾扫描整个数据库文件来查找键的出现位置,查找的开销是 O(n),如果数据库的记录条数加倍,则查找需要两倍的时间。
解决1:为了高效地查找数据库中特定键的值 , 需要新的数据结构:索引它们背后的基本想法都是保留一些额外的元数据,这些元数据作为路标,帮助定位想要的数据

哈希索引

一个最简单的索引实现就是哈希K-V,保存内存中的 hashmap ,把每个键一一映射到数据文件中特定的字节偏移量 ,这样就可以找到每个值的位置。每当在文件中追加新的key-value对时,还要更新hashmap来反映刚刚写入数据的偏移量 (包括插入新的键和更新已有的键)。当查找某个值时,使用 hashmap来找到文件中的偏移量,即存储位置,然后读取其内容 。只需一次磁盘寻址,就可以将value从磁盘加载到内存。如果那部分数据文件已经在文件系统的缓存中,则读取根本不需要任何的磁盘I/O 。
问题:只追加到一个文件,那么如何避免最终用尽磁盘空间 ?
解决:一个好的解决方案是将日志分解成一定大小的段,当文件达到一定大小时就写入到新的段文件中。然后可以在这些段上执行压缩,并在执行压缩的同时将多个段合并在一起。现在每个段现在都有自己的内存哈希表 ,将键映射到文件的偏移量。 为了找到键的值,首先检查最新的段的 hashmap ;如果键不存在,检查第二最新的段,以此类推。由于合并过程可以维持较少的段数量 ,因此查找通常不需要检查很多 hashmap 。
问题:为什么不设计成新值直接覆盖旧值?
解决:追加和分段合并主要是
顺序写
,它通常比随机写入快得多。

哈希索引局限:1哈希表必须全部放入内存2范围查询效率不高

B-tree

B-tree将数据库分解成固定大小的块或页,传统上大小为4 KB ,页是内部读写的最小单元。这种设计更接近底层硬件,因为磁盘也是以固定大小的块排列。
用这些页面引用来构造一个树状页面 ,指定一页为 B-t ree的根:每当查找索引中的一个键时,总是从这里开始。该页面包含若干个键和对子页的引用。每个孩子都负责一个连续范围内的键,相邻引用之间的键可以指示这些范围之间的边界。
B-tree底层的基本写操作是使用新数据覆盖磁盘上的旧页。它假设覆盖不会改变页的磁盘存储位置, 也就是说,当页被覆盖时,对该页的所有引用保持不变。可以认为磁盘上的页覆盖写对应确定的硬件操作。在磁性硬盘驱动器上,这意味着将磁头首先移动到正确的位置,然后旋转盘面,最后用新的数据覆盖相应的扇区。对于SSD ,由于SSD必须一次擦除并重写非常大的存储芯片块,情况会更为复杂。

非易失性存储(NVM)在数据库中的作用主要体现在以下几个方面:
提高数据安全性:NVM具有非易失性,即在断电后数据不会丢失。这使得数据库在遇到意外断电或其他故障时,能够更好地保护数据的完整性和一致性。
提升读写性能:NVM的读写性能接近DRAM,特别是读取速度远快于写入速度。这意味着在数据库操作中,数据的读取可以更加迅速,从而提高整体的数据库性能。
减少数据寻道时间:NVM没有数据寻道时间,类似于SSD。这使得数据库在进行数据访问时,可以更快速地定位到所需的数据,减少等待时间

OLTP & OLAP

在这里插入图片描述
数据库管理员通常不愿意让业务分析人员在OLTP数据库上直接运行临时分析查询,这些查询通常代价很高,要扫描大量数据集,这可能会损害并发执行事务的性能。
相比之下,数据仓库则是单独的数据库,分析人员可以在不影响OLTP操作的情况下尽情地使用。数据仓库包含公司所有各种OLTP系统的只读副本。

事实表:销售的产品ID、产品名、购买的客户ID、客户名、产品的分类、购买的日期…
维度表:产品表、客户表、产品分类表…

在大多数OLTP数据库中,存储以面向行的方式布局:来自表的一行的所有值彼此相邻存储。可以在表上使用索引,告诉存储引擎在哪里查找特定日期或特定产品的所有销售。但是,面向行的存储引擎仍然需要将所有行从磁盘加载到内存中、解析它们, 并过滤出不符合所需条件的行。这可能需要很长时间 。
面向列存储的想法很简单:不要将一行中的所有值存储在一起,而是将每列中的所有值存储在一起。如果每个列存储在一个单独的文件中,查询只需要读取和解析在该查询中使用的那些列,这可以节省大量的工作。但请注意,单独排序每列是没有意义的,如果这样的话就无法知道列中的某一项属于哪一行。因为知道某列中 的第k项和 另一列的第k项一定属于同一行,基于这种约定我们可以重建一行。

概括来讲,存储引擎分为两大类:针对事务处理(OLTP)优化的架构,以及针对分析型(OLAP)的优化架构。

  • OLTP系统通常面向用户,这意味着它们可能收到大量的请求。为了处理负载,应用程序通常在每个查询中只涉及少量的记录。应用程序基于某种键来请求记录,而存储引擎使用索引来查找所请求键的数据。磁盘寻道时间往往是瓶颈
  • 由于不是直接面对最终用户 ,数据仓库和类似的分析型系统相对并不太广为人知,它们主要由业务分析师使用。处理的查询请求数目远低于OLTP系统,但每个查询通常要求非常苛刻,需要在短时间 内扫描数百万条记录。 磁盘带宽(不是寻道时间)通常是瓶颈,而面向列的存储对于这种工作负载成为日益流行的解决方案。

第三章 编码

程序通常使用( 至少)两种不同的数据表示形式 :
1.在内存中,数据保存在对象、结构体、列表、数组、哈希表和树等结构中。这些数据结构针对CPU的高效访问和操作进行了优化(指针)。
2.将数据写入文件或通过网络发送时,必须将其编码为某种自包含的字节序列(例如JSON文档)。
因此,在这两种表示之间需要进行类型的转化。从内存中的表示到字节序列的转化称为编码(序列化等),相反的过程称为解码(反序列化)。

第二部分、分布式数据系统

扩展能力
当负载增加需要更强的处理能力时,最简单的办法就是购买更强大的机器(有时称为垂直扩展)。由一个操作系统管理更多的CPU ,内存和磁盘,通过高速内部总线使每个CPU都可以访问所有的存储器或磁盘。在这样一个共享内存架构中,所有这些组件的集合可看作一台大机器。共享内存架构的问题在于,成本增长过快甚至超过了线性 :即如果把一台机器内的CPU数量增加一倍,内存扩容一倍,磁盘容量加大一倍,则最终总成本增加不止一倍。并且由于性能瓶颈因素,这样一台机器尽管拥有了两倍的硬件指标但却不一定能处理两倍的负载。

第五章 数据复制

如何确保所有副本之间 的数据是一致的?

单主从复制

只有主节点才可以接受写请求,主节点把新数据写入本地存储后,然后将数据更改作为复制的日志发送给所有从节点。读数据时 ,可以在主节点或者从节点上执行查询。
又分为同步与异步

同步复制的优点, 一旦向用户确认,从节点可以明确保证完成了与主节点的更新同步,数据已经处于最新版本 。万一主节点发生故障,总是可以在从节点继续访问最新数据。缺点,如果同步的从节点无法完成确认(例如由于从节点发生崩溃,或者网络故障,或任何其他原因),写入就不能视为成功。 主节点会阻塞其后所有的写操作,直到同步副本确认完成 。任何一个同步节点的中断都会导致整个系统更新停滞不前 。

把所有从节点都配置为同步复制有些不切实际,因为任何一个同步节点的中断都会导致整个系统更新停滞不前 。

异步复制: 如果主节点发生失败且不可恢复 ,则所有尚未复制到从节点的写请求都会丢失。这意味着即使向客户端确认了 写操作, 却无法保证数据的持久化。

从节点全同步/全异步都不可取 ,一般是一个同步剩余异步。这样可以保证至少有两个节点(即主节点和一个同步从节点)拥有最新的数据副本。这种配置有时也称为半同步。

问题:新增了节点怎么保证新节点和主节点数据一致?

  • 简单地将数据文件从一个节点复制到另二个节点通常是不够的。主要是因为客户端仍在不断向数据库写入新数据,数据始终处于不断变化之中,因此常规的文件拷贝方式将会导致不同节点上呈现出不同时间点的数据。
  • 锁定数据库(不可写)来使磁盘上的文件保持一致,但这会违反高可用的设计目标。
  • 在某个时间点对主节点的数据副本产生一个快照,将此快照拷贝到新的从节点 。从节点先更新快照,再请求这期间主节点发生的数据更改日志。(也就是MySQL里的binlog)。

节点失效

从节点失效 : 追赶式恢复
从节点的本地磁盘上都保存了副本收到的数据变更日志。如果从节点发生崩溃,然后顺利重启,根据副本的复制日志,从节点可以知道在发生故障之前所处理的最后一笔事务,然后连接到主节点,并请求自那笔事务之后中断期间内所有的数据变更。在收到这些数据变更日志之后,将其应用到本地来追赶主节点。之后就和正常情况一样持续接收来自主节点数据流的变化。
主节点失效:主从切换

日志实现

  • 基于语句的实现INSERT 、 UPDATE或DELETE语句,类似于AOF。
    问题:非确定性的语句,如NOW() 获取当前时间,或RAND()获取一个随机数等,可能会在不同的副本上产生不同的值。(如果语句存在不确定性操作,MySQL会切换到基于行的复制)
    如果语句中使用了自增列,则所有副本必须按照完全相同的顺序执行,否则可能会带来不同的结果。
  • 基于行的逻辑日志某行数据发生更新或删除操作时,不是记录整条SQL语句,而是记录哪些行发生了变化(比如哪些行被更新或删除),然后将这些变化信息发送给其他节点,接收节点根据这些信息来执行相应的更新或删除操作。(由于逻辑日志与存储引擎逻辑解锢,因此可以更容易地保持向后兼容,从而使主从节点能够运行不同版本的软件甚至是不同的存储引擎。)

复制滞后问题

写读一致性
主从节点,主节点进行改,主从进行查,如果改了之后,查询刚好是从节点,并且修改的数据还没同步过来,就发生了不一致(比如个人资料修改)。
解决:用户首页信息只能由自己编辑,那就总是从主节点读取用户自己的首页配置文件,而在从节点读取其他用户的配置文件 。
或者是:跟踪最近更新的时间 ,如果更新后xx分钟之内,就在主节点读取;并监控从节点的复制滞后程度 ,避免从那些滞后时间超过xx分钟的从节点读取 。
前缀一致读
你好然后才是吃饭了没,它们之间存在因果关系,由于复制滞后,变成了吃饭了没,你好。

多主节点复制

第六章、数据分区

每一条数据(或者每条记录,每行或每个文档)只属于某个特定分区。采用数据分区的主要目的是提高可扩展性。
分区通常与复制结合使用,即每个分区在多个节点都存有副本。这意味着某条记录属于特定的分区 ,而同样的内容会保存在不同的节点上以提高系统的容错性。一个节点上可能存储了多个分区。一个节点可能即是某些分区的主副本,同时又是其他分区的从副本。

分区的主要目标是将数据和查询负载均匀分布在所有节点上 。 如果节点平均分担负载 ,那么理论上 10个节点应该能够处理 10倍的数据量和 10倍于单个节点的读写吞吐量

如果分区不均匀,则会出现某些分区节点比其他分区承担更多 的数据量或查询负载,称之为倾斜。倾斜会导致分区效率严重下降,在极端情况下,所有的负载可能会集中在一个分区节点上,这就意味着 10个节点9个空闲,系统的瓶颈在最繁忙的那个节点上。

基于关键字区间分区
一种分区方式是为每个分区分配一段连续的关键字或者关键宇区间范围(以最小值和最大值来指示)
然而,基于关键字的区间分区的缺点是某些访问模式会导致热点。如果关键字是时间戳,则分区对应于一个时间范围,例如每天一个分区。然而,当测量数据从传感器写入数据库时,所有的写入操作都集中在同一个分区(即当天的分区),这会导致该分区在写入时负载过高,而其他分区始终处于空闲状态。

基于关键字晗希值分区
对于上述数据倾斜与热点问题,许多分布式系统采用了基于关键字哈希函数的方式来分区。
一个好的哈希函数可以处理数据倾斜并使其均匀分布 。 哈希分区。将哈希函数作用于每个关键字,每个分区负责一定范围 的哈希值。这种方法打破了原关键字的顺序关系,它的区间查询效率比较低,但可以更均匀地分配负载。

第七章 事务

即事务中的所有读写是一个执行的整体,整个事务要么成功(提交)、要么失败(中止或回滚)。如果失败,应用程序可以安全地重试,目的是简化应用层的编程模型。
一致性
CAP理论中, 一致性用来表示线性化。
ACID 中, 一致性指数据库处于应用程序所期待的预期状态。

原子性,隔离性和持久性是数据库自身的属性,而ACID 中的一致性更多是应用层的属性。

隔离性
隔离性意味着并发执行的多个事务相互隔离,它们不能互相交叉。
持久性
数据库系统本质上是提供一个安全可靠的地方来存储数据而不用担心数据丢失等。持久性就是这样的承诺,它保证一且事务提交成功,即使存在硬件故障或数据库崩溃 ,事务所写入的任何数据也不会消失。

第八章

可以说,在分布式系统中,怀疑,悲观和偏执狂才能生存。
系统的可靠性应该取决于最不可靠的组件。
网络是不可靠的,(a )请求丢失; ( b )远程节点关闭; (c )响应丢失
处理这个问题通常采用超时机制:在等待一段时间之后,如果仍然没有收到回复则选择放弃,并且认为响应不会到达。

第九章 一致性与共识

强一致性

可线性化(强一致性):其基本的想法是让一个系统看起来好像只有一个数据副本,且所有的操作都是原子的。

可串行化
可串行化是事务的隔离属性,其中每个事务可以读写多个对象(行 , 记录等)。它用来确保事务执行的结果与串行执行(即每次执行一个事务)的结论完全相同,即使串行执行的顺序可能与 事务 实际执行顺序不同 。
可线性化
可线性化是读写寄存器(单个对象)的最新佳保证 ,它并不要求将操作组合到事务中 。

因果一致性

因果关系对所发生的事件施加了某种排序,些因果关系的依赖链条定义了系统中的因果顺序,即某件事应该发生另一件事情之前。

可线性化一定意味着因果关系

原子提交

对于在单个数据库节点上执行的事务,原子性通常由存储引擎来负责。当客户端请求数据库节点提交事务时,数据库首先使事务的写入持久化,然后把提交记录追加写入到磁盘的日志文件中 。 如果在崩溃之前提交记录已成功写入磁盘,就认为事务己安全提交; 否则 ,回滚该事务的所有写入。这就是在单设备上实现原子提交的核心思路。

事务提交不可撤销,不能事后再改变。因为一旦数据提交,就被其他事务可见,继而其他客户端会基于此做出相应的决策。这个原则构成了读已提交隔离级别的基础

但是,如果一个事务涉及多个节点,向所有节点简单地发送一个提交请求,然后各个节点独立执行事务提交,这样做很容易发生部分节点提交成功,而其他一些节点发生失败,从而违反了原子性保证。
必须要:如果有部分节点提交了事务,则所有节点也必须跟着提交事务。

两阶段提交

两阶段提交( 2PC )是一种在多节点之间实现事务原子提交的算法,用来确保所有节点要么全部提交,要么全部中止。
2PC引入了单节点事务所没有的一个新组件:协调器。
阶段 l :协调者发送一个准备请求到所有节点,询问他们是否可以提交。如果所有参与者回答是,那么协调者在阶段2会发出提交请求,提交开始实际执行。如果有任何参与者回复否,则协调者在阶段2中向所有节点发送放弃请求。

开弓没有回头箭,当参与者投票是时,它做出了肯定提交的承诺,然后协调者做出了提交(或放弃)的决定,这都是不可撤销的。正是这两个承诺确保了 2PC的原子性。(如果协调者崩溃,2PC能够顺利完成的唯一方法是等待协调者恢复。这就是为什么 协调者必须在向参与者发送提交或中止请求之前要将决定写入磁盘的事务日志)

如果在决定到达之前,出现协调者崩溃或网络故障, 则参与者只能等待。此时参与者处在一种不确定的状态。

三阶段提交

因为两阶段提交可能在等待协调者恢复时卡住。理论上,可以使其改进为非阻塞式从而避免这种情况。但是,实践中要想做到这一点并不容易。
三阶段提交算法假定有一个完美的故障检测器,即有一个非常可靠的机制可以判断出节点是否已经崩溃。在无限延迟的网络环境中,超时机制并不是可靠的故障检测器 ,因为即使节点正常,请求也可能由于网络问题而最终超时 。正是由于这样的原因,尽管大家已经意识到上述协调者潜在的问题,但还在普遍使用 2PC。

第十章 批处理系统

在线系统
服务等待客户请求或指令的到达。当收到请求或指令时,服务试图尽可能快地处理它,并发回一个晌应。响应时间通常是服务性能的主要衡量指标,而可用性同样非常重要(如果客户端无法访问服务,用户可能会收到一个报错消息)。
批处理系统(或离线系统 )
批处理系统接收大量的输入数据,运行一个作业来处理数据,并产生输出数据 。作业往往需要执行一段时间(从几分钟到几天),所以用户通常不会等待作业完成。相反,批量作业通常会定期运行(例如,每天一次)。 批处理作业的主要性能衡量标准通常是吞吐量 (处理一定大小的输入数据集所需的时间)。
流处理系统(或称近实时系统)
流处理介于在线与离线/批处理之间(所以有时称为近实时或近线处理)。与批处理系统类似,流处理系统处理输入并产生输出(而不是响应请求)。但是,流式作业在事件发生后不久即可对事件进行处理 , 而批处理作业则使用固定的一组输入数据进行操作。这种差异使得流处理系统比批处理系统具有更低的延迟。

流处理是在批处理的基础上进行的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值