Oralce的LRU算法

 

>Buffer cache作用<

   Buffer cache是Oracle建立的数据文件的缓冲区。Oracle中所有Select、DML、DDL等命令,凡是需要所有对数据文件进行读、写操作的,基本上都是对Buffercache进行读、写操作。如果需要读或写的块没有包括在Buffer cache,Oracle会先把它读进Buffer cache中,再进行读、写。

   通常,如果要读的块已经在Buffer cache中了,Oracle将不必再访问磁盘数据文件,这个读操作被称为逻辑读。也就是没有真正读写磁盘的意思。

   如果要读的块不在Buffer cache,那么Oracle必须到磁盘上将其调入Buffer cache中,这个操作被称为物理读。再将块物理读进Buffer cache后,Oracle再从Buffer cache中逻辑读取需要的数据。

 

 

>Buffer cache大小的设置<

   在10g中,非常简单,我们不必设置Buffer cache的大小,只需设置一个sga_target参数,SGA目标值,10g自然会根据你的数据库的情况,自己调整共享池、Buffercache池等等所有SGA中内存组件的大小。这一点我们在共享池部分也说过。db_cache_size参数是设置Buffer cache大小的,和共享池一样,在10g中,它也只是个下限值。

   Buffer cache是磁盘数据文件的缓存,数据文件的基本读写单位是Oracle数据块,最常的块大小是4KB、8KB、16KB等等,在我们的实例库,数据库块大小是8KB。也就是说Oracle一次读写将从数据文件中读写8KB字节。Buffer cache既然是数据文件的直接缓存,Buffer cache的内存块大小当然也是8KB了。

   Oracle从9i之后开始支持多重块大小,你在创建数据库时设置的块大小被称为标准块大小,除了标准块大小外,我们还可以使用多个非标准的块大小。就像我们的实例库,标准块大小是8KB,我们还可以使用2KB、4KB、16KB和32KB的非标准块大小。

   那么,数据库有必要使用多种块大小吗?绝对有必要。对于全表扫描操作,越大的块效率越高。因为全表扫描要访问表所有的行,所有块。块越大,相同大小的表所占用的块就越少。而每读一个块,Oracle都算作一次物理读,表块越少,访问全表所需的物理读就越少。因此,全表扫描操作适合大块。但对于索引扫描,索引扫描是一种随机的形式访问表中的行。随机访问下小块效率更好。因为你每访问一个块,Oracle都要将块读进Buffer cahce,这样做的目的是希望你再次访问块中其他行时,不必再从磁盘读取(从磁盘中读是物理读),而直接从内存中读取(从内存中读是逻辑读)。随机访问很可能访问完块中一行后,就不再访问此块了。因此是随机本身就是无规律的访问。因为小块占用的内存少,随机访问下,将小块读入内存比将大块读入内存,能更节省内存空间,提高内存的使用效率。因此,随机访问下,小块的效率比大块要好一些。还有,大块的争用可能性更高,而小块争用的可能性低。因为大块中行比较多,而小块中行少。这就好像一个团队有100个人,另一个团队有10个人,当然人越多越容易发生矛盾。总之,大块有大块的好低,小块有小块的好处,如果数据库只能使用一种块大小,这显然是不太合理的。

   我们已经在体系结构部分课程的“存储结构”章节中讲过了如何配置使用非标准块大小,这里就不再重试了。其要点有如下两点:

Ÿ  只有配置了相应非标准块大小的Buffer cache池后,才能使用非标准块大小

Ÿ  非标准块大小是在表空间层指定的。一旦在表空间层确定了表空间所使用的块大小,以后被创建在此表空间中的所有表、索引等等段对象,都将按表空间块大小为准。

有一定需要补充的是,用来配置非标准块大小Buffer cache的初始化参数db_16k_cache_size、db_2k_cache_size、db_32k_cache_size、db_4k_cache_size、db_8k_cache_size,它们并不在Oracle自动内存管理的范筹内,Oracle并不会自动的调整他们的大小。你将这些参数设置为多大,只要SGA内还有空闲内存,Oracle将会立即分配相应大小的内存空间。

 

 

>LRU<

   任何缓存的大小都是有限制的,并且总不如被缓存的数据多。就像Buffer cache用来缓存数据文件,数据文件的大小远远超过Buffer cache。因此,缓存总有被占满的时候。当缓存中已经没有空闲内存块时,如果新的数据要求进入缓存,就只有从缓存中原来的数据中选出一个牺牲者,用新进入缓存的数据覆盖这个牺牲者。这一点我们在共享池中曾提及过,这个牺牲者的选择,是很重要的。缓存是为了数据可以重用,因此,通常应该挑选缓存中最没有可能被重用的块当作牺牲者。牺牲者的选择,从CPU的L1、L2缓存,到共享池、Buffer cache池,绝大多数的缓存池都是采用著名的LRU算法,不过在Oracle中,Oracle采用了经过改进的LRU算法。具体的算法它没有公布,不过LRU算法总的宗旨就是――“最近最少”,其意义是将最后被访问的时间距现在最远的内存块作为牺牲者。比如说,现在有三个内存块,分别是A、B、C,A被访问过10次,最后一次访问是在10:20,B被访问过15次,最后一次访问是10:18,C也被访问10次,最后一次被访问是在10:22。当需要选择牺牲者时,B访问次数最多,牺牲者肯定不是它。A、C访问次数一样,但A在10:20被访问,而C在10:22被访问,A最后被访问的更早些,牺牲者就是A。注意,这就是LRU的宗旨,“将最后访问时间距现在最远的块作为牺牲者”。

   为了实现LRU的功能,Oracle在Buffer cache中创建了一个LRU链表,Oracle将Buffercache中所有内存块,按照访问次数、访问时间排序串在链表中。链表的两头我们分别叫做热端与冷端, 如下图

   当你第一次访问某个块时,如果这个块不在Buffer cache中,Oracle要选将它读进Buffer cache。在Buffer cache中选择牺牲者时,Oracle将从冷端头开始选择,在上图的例子中,内存块U将是牺牲者。

如上图,新块将会被读入U,覆盖U原来的内容。这里,我们假设新块是V。但是块V不会被放在冷端头,因为冷端头的块,会很快被当作牺牲者权覆盖的。这不符合“将最后访问时间距现在最远的块作为牺牲者”的宗旨。块V是最后时间距当前时刻最近的,它不应该作为下一个牺牲者。Oracle是如何实验LRU的,我们继续看。

Oracle将LRU链从中间分为两半,一半记录热端块、一半记录冷端块。如上图,而刚刚被访问的块V,如下图:

   如过再有新的块进入Buffer cache,比如块X被读入Buffercache,它将覆盖T,并且会被移至块V的前面,如下图:

   大家可以想像一下,如果按照这面的方式继续下去,最右边冷端头处的块,一定是最后一次访问时间距现在最远的块。那么,访问次数多的块是不会被选做牺牲者的,这一点Oracle是如何实现的?这很简单,Oracle一般以2次为准,块被访问2次以上了,它就有机会进入热端。

   Oracle为内存中的每个块都添加了一个记录访问次数的标志位,假设图中每个块的访问次数如下:

   如果现在又有新块要被读入Buffer cache,Oracle开始从冷端头寻找牺牲者,冷端头第一个块S,它的访问次数是2,那么,它不能被覆盖,只要访问次数大于等于2的块,Oracle会认为它可能会被经常访问到,Oracle要把它移到热端,它会选择R做为本次的牺牲者:

   块S会被从冷端移到热端,并且它的访问次数会被清零。此时,块R就是牺牲者了,因为它的访问次数不到两次。

   新块Y覆盖了块R,并被移到了冷端块开始处,它的访问次数是1。如果块Y再被访问了一次,它的访问次数变为了2:

   虽然Y的访问次数达到了两次,但它不会马上被移到热端,它仍然留在原来的位置,随着不断有新块加入,被插入到它的前面,它会不断的被向后推移。

   如上图,又加入了很多的新块,Y又被推到了冷端头,当再有新块进入Buffer cache时,Y不会是牺牲者,它会被移到热端头S的前面,Y后面的Z,它的访问次数没有达到2,它将会是牺牲者。

   好了,这就是Oracle中Buffer cache管理LRU的原理。按照这种方式运作,Oracle可以把常用的块尽量长的保持在Buffer cache中。而且,每有新块进入Buffer cache,Oracle都会从冷端头处,从右向左搜索牺牲块。因为越靠近冷端,块的访问次数有可能越少、最后的访问时间离现在最远。

 

LRU算法中,是由_db_percent_hot_default隐含参数决定冷端热端的分界线的。

 

>脏块与脏LRU链<

   Oracle中修改块的规则是只对Buffer cache中的块进行修改,并不直接修改磁盘中的块。如果要修改的块不在Buffercache中,Oracle会先将它读入Buffer cache,再在Buffer cache中进行修改。当Buffer cache中的块被修改后,Oracle会把它标记为“脏”块。脏块含有脏数据,脏数据就是用户修改过的数据。Oracle会定期的将脏块写到磁盘中。有一个专门的后台进程就是专门负责写脏块到磁盘的,它就是DBWn。我们也把DBWn写脏块到磁盘这个过程叫做刷新脏块,刷新过后,脏块就不脏了,又变成了干净块。其实,有一个块A,如果Buffer cache中此块的数据和磁盘上块中数据不一致,那么,这个块就是脏块。否则,就是干净块。当修改完成后,因为Oracle只修改Buffer cache,因此,块中数据和磁盘肯定不一致,这时块就是脏块。当块被刷新后,块被写到磁盘,那么,磁盘中块数据和Buffer cache中块的数据又是一致的,此时,块就又变成了干净块。

脏块在被写回磁盘前,也就是在它还是脏块时,它是不能被覆盖的,因为,脏块含有用户修改过的数据,而这些数据还没被写到磁盘,如果此时覆盖了脏块,用户的修改结果将会丢失。

   设当前LRU链如上图所示,其中V、L、O、P、Q是脏块。当新的块要进入Buffercache时,Oracle从冷端头开始选择牺牲块,Q、P和O都不能做作牺牲块,因为它们是脏块,N是这一次的牺牲者,新进入的块将会覆盖N,然后将N插入到Y之前。然后呢,下一次有块进入Buffer cache时,Oracle从冷端头开始搜索,它还要检查一边Q、P和O,发现它们都不能覆盖,再将M定为牺牲者。等等,每一次都要检查一边O、P、Q,这太浪费时间了,Oracle不会这么傻,Oracle有准备了一个脏LRU链,专门保存脏块。当块变脏时,块不会马上被移到脏LRU中,只有当Oracle从冷端头开始,寻找牺牲者时,才会将发现的脏块移动到脏LRU链中。这样做的目的我们刚才已经快要讲到了,就是下次再寻找牺牲者时,可以不用再检查这些脏块。好,让我们继续看图,接着上图,有新块Z要进入Buffer cache:

   如上图,O、P、Q将被移到脏LRU链中。

   冷端头变成了N,N的访问次数小于2,它就是本次的牺牲者了。这样当下一次再需要从冷端头开始寻找牺牲者时,就不用再检查O、P、Q这三个脏块了。当脏LRU链的长度,也就是脏LRU链中的脏块达到一定数目时,DBWn会开始刷新脏块。

   通过上面所讲述的LRU链与脏LRU链的原理,我们可以发现Oracle把很多工作,都留到了在LRU的冷端搜索牺牲者时。当块的访问次数增加的超过2时,块在LUR链的位置不变;当块变脏时,块的LRU链位置也不变。只有当从LRU的冷端搜索牺牲者时,才会将发现的脏块移到脏LRU链,将访问次数超过2的,插入到热端,这就是Oracle改进了的LRU算法。Oracle这样做的目的,是为了让我们平时的查询、修改所需完成的操作尽量的少。对于用户的查询、修改操作,LRU算法几乎没有任何的影响,额外所做的工作只是改变了几个标志位而已,查询时增加访问次数标志位,修改块时设置脏块标志位。LRU算法大部分的工作,都是在寻找牺牲者时完成的。因此,有时寻找牺牲者这个过程有可能会出现等待,等待事件是free buffer waits。

Ÿ  访问次数大于2的块太多,或者脏块太多,反正这些块都是不能覆盖的,Oracle不得不移动它们到它们该去的位置。当碰到的这样的块超过LRU中总块数的40%时,也就是说搜索了一小半LRU链,还是没有发现可以覆盖的牺牲者,Oracle就不在找了,它会唤醒DBWn刷新脏块。在DBWn刷新期间的等待,就会被记入到free buffer waits事件中。另外,在资料视图中有一个资料free buffer inspected,它记录了Oracle在所有次的寻找牺牲者的过程中,共计碰到了多少个不可覆盖的块。

Ÿ  在寻找牺牲者过程中发现脏块,Oracle将其移动到脏LRU链,但是脏LRU链中脏块数目达到限制,DBWn被唤醒开始刷新脏块,Oracle必须等待刷新脏块完毕,才能再继续寻找牺牲者,这其间的等待事件,也会被记入free buffer inspected。

总之,free buffer waits事件发生的主要原因就是在LRU中寻找牺牲者的时间过长。如果这个等待事件频繁出现,说明Buffer cache中脏块太多了,这通常是DBWn写刷新速度慢造成的。我们应该将DBWn更频繁的被唤醒去刷新脏块,好让它们变干净、可以被选为牺牲者。我们不应该让脏块从脏LRU链中被刷新,因为这时通常会出现free buffer inspected。脏LRU链并不是为了将脏块集中到一起,让DBWn去刷新的,我们上面的图例中已经讲过,将脏块移动到脏LRU链中,是为了减少下一次寻找牺牲者时,所需搜寻的块。Oracle中另有一个链表,专门用来记录脏块,好让DBWn定期刷新,这个链表是检查点队列。

 

>检查点队列与增量检查点<

   检查点的主要目的是以对数据库的日常操作影响最小的方式刷新脏块。脏块不断的产生,如何将脏块刷新到磁盘中去呢?在8i之前,Oracle定期的锁住所有的修改操作,刷新Buffer cache中的所有脏块,这种刷新脏块的方式被称为完全检查点,这极大的影响了效率,从9i之后只有当关闭数据库时才会发生完全检查点。

   从8i开始,Oracle增加了增量检查点的概念,增量检查点的主要宗旨就是定期的刷新一部分脏块。将脏块一次刷新完是不合理的,因为脏块不断产生,没有穷尽。像完全检查点那样停止用户所有的修改操作,将脏块刷新完再继续,这绝对会极大的影响性能。所有增量检查点的一次刷新部分块是脏块问题的最好解决办法。那么,每次刷新时,都刷新那些块呢?根据统计研究,根据块变脏的顺序,每次刷新那些最早脏的块,这种方式最为合理。为了实现这一点,Oracle在Buffer cache中又建立了一个链表,就是检查点队列。每个块在它变脏时,会被链接到检查点队列的末尾。就好像排队一样,9:00来的人站在第一位,9:05来的人排第二位,以后每来一个人都站在队伍的末尾,这个队伍就是按来到的时间顺序排列的一个队列。检查点队列就是这样,块在变脏时会被链到末尾。因此检查点队列是按块变脏的时间顺序,将块排成了一个队列。

   如上图,检查点队列中的每一节点,都指向一个脏块。检查点队列每个节点中的信息其实非常少,就是记录对应块在Buffer cache中的地址,脏块对应的重做记录在日志文件中的位置,另外还有前一个节点、后一个节点的地址。检查点队列还有LRU、脏LRU,这些都是双向链表。双向链表就是在节点中记录前、后两个节点的地址。

   检查点队列头部的块是最早变脏的,因此,Oracle会定期唤醒DBWn从检查点队列头开始,沿着检查点队列的顺序,刷新脏块。在刷新脏块的同时,仍可以不断的有新的脏块被链接到检查点队列的尾部。这个定期唤醒DBWn刷新脏块的操作,Oracle就称为增量检查点。

   如上图,1、2、3号节点所指向的脏块已经被刷新为干净块。同时,又有两个块变脏,它们被链接到了检查点队列的末尾,它们是9号、10号节点。

   检查点队列的头,又被称为检查点位置,Checkpoint postion,这些名称我们不必从字面上去理解。总之,检查点位置就是检查点队列头。检查点队列头节点(也就是检查点位置)的信息,Oracle会频繁的将它记录到控制文件中,而且会很频繁的记录。一般是每隔三秒,有一个专门的进程CKPT,会将检查点位置记录进控制文件。

   如上图,当前的检查点位置是检查点队列的1号节点。又一个三秒到了,CKPT进程启动,将新的检查点位置记入控制文件:

   新的检查点位置是4号节点,它对应当前变脏时间最早的脏块。1、2、3号节点已经从检查点队列中摘除了。因为它们对应的脏块已经不脏了。一般来说,控制文件中的检查点位置之后的块都是脏块。但是有时也例外,因检查点位置每三秒才会更新一次,就像上图,1、2、3号节点对应的脏块已经被刷新过了,但是由于三秒间隔没到,检查点位置还是指向1号节点。只有当三秒到后,检查点位置才会被更新到4号节点上。

   关于检查点队列、检查点位置我们先说到这里,在全面的介绍什么是增量检查点之前,我们先说一下检查点队列的一个重要作用。

让我们先来总结一下用户修改块时,Oracle内部都发生了什么:

1.如果块不在Buffer cache,将块读入Buffer cache

2.先生成重做记录,并记入日志缓存,在用户提交时写到日志文件中

3.在Buffer cache中修改块

4.在Buffer cache中设置块的脏标志位,标志块变成脏块,同时在检查点队列末尾增加一个新节点,记录这个新脏块的信息,信息包括:脏块在Buffer cache中的位置,在步骤2时生成的与此脏块对应的重做记录位置。

5.用户提交后,将相应的重做记录从重做缓存写入日志文件。

   我现在将日志补充到上面的图中:

   就像上图,检查点队列的每个节点,都保存有脏块的地址和脏块对应的重做记录的编号。脏块在Buffer cache中的位置是随机的,用户不一定修改那个块。但重做记录是顺序生成的,就和检查点队列的排列顺序一样。因为,它们都是当块被修改而变脏时产生的。块A先被修改,块A的重做记录就排在前面,块B后被修改,块B对应的重做记录会被排在块A对应的重做记录的后面。和它们在检查点中的顺序是一样。每当数据库因异外而当机,比如异常死机、断电等等,Buffer cache中有许多脏块没来的及写到磁盘上。以图为例,比如说现在断电了,现在磁盘上还有7个脏块,它们里面有用户修改过的数据,Oracle已经将反馈信息“你的修改完成”发送给用户,用户也以为他们的修改完成了,将为一直保存到数据库中。但是,断然的断电,令这几个脏块中的数据丢失了,它们没来得及写到磁盘上。

Oracle如何解决这个问题呢?很简单,当数据库重新启动时,Oracle只需从控制文件中读出检查点位置,检查点位置中记录有重做记录编号,根据此编号,Oracle可以很快的定位到日志文件中的重做记录n,它读出重做记录n中的重做数据,将用户的修改操作重现到数据库。接着,Oracle读取重做记录n+1中的重做数据,重现用户修改,这个过程将沿着日志流的顺序,一直进行下去,直挡最后一条重做记录,在上图的例子中,最后一条重做记录是第n+6条。这个过程完成后,用户所有的修改又都被重现了,一点都不会丢失。只要你的日志文件是完整,日志流是完整的,就一点信息都不会丢失。

有人可能会有一个问题,重做记录在生成后,也是先被送进重做缓存,再由重做缓存写往日志文件。这样的机制下,一定会有某些重做记录在没来的及写到日志文件中时,数据库突然当机,而造成这些重做记录丢失。这样,这些重做记录所对应的脏块,将得不到恢复。用户还是会丢失一些数据。

这种情况的确会发生,但丢失的都是没用的信息。为什么这么说的。Oracle会在用户每次发出提交命令时,将事务所修改脏块对应的重做记录写进日志文件,只有当这个操作完成时,用户才会收到“提交完成”,这样的信息,对于一个完整的事务,当用户看到提交完成后,也就意味着所对应的重做记录一定被写到了日志文件中,即使发生异常死机,它也是绝对可以恢复。而当用户没有提交,或没来得及提交,数据库就崩溃了,那么事务就是不完整的,这个事务必须被回滚,它根本用不着恢复。对于这样不完整的事务,它对应的重做记录有可能丢失,但这无所谓了,因为不完整的事务根本不需要恢复。也就是说,只有用户的事务提交了,用户的修改一定不会丢失。不过这还有一个前提,就是日志文件千万不能损坏,DBA所要做的就是要保证日志文件不能损坏。DBA可以使用RAID1这样的磁盘镜像技术,或者多元备份日志文件,等等,这个我们在前面章节中已经讲过了的。

我们上面所讲到的这种恢复,是自动进行的,并且不需要DBA参与,它被称之为实例恢复。

 

>检查点设置<

   这里所说的检查点设置,主要指增量检查点变化的设置。注意增量检查点只是一个名词,不必按字面的意义去理解它。增量检查点发生时,Oracle会唤醒DBWn沿着检查点队列写脏块,这就是增量检查点。那么到底多长时间一次发生一次增量检查点呢?这个增量检查点的频率是非常重要的,它基本上控制着DBWn多长时间去刷新一次脏块。DBWn活动的太频繁,会影响数据库的整体性能,如果DBWn活动太不频繁,又会使脏块积压太多,这同样也会影响性能。而且,如果出现异常崩溃,需要实例恢复,脏块越多,实例恢复越慢。在9i之前DBA主要靠间隔时间等方式来设置增量检查点的频率,比如可以让Oracle每10分钟发生一次增量检查点。如果这个数字设置不合适,对数据库性能的影响是很大的。而且有可能造成实例恢复时间过长。在9i之后,特别是到了10g中,检查点已经相当的智能化了,很少会成为I/O问题的原凶。9i中设置fast_start_mttr_target参数为你所期望的实例恢复时间,系统将自动控制增量检查点的频率。比如,你希望实例恢复可以在5分钟内完成,你可以将此参数设置为300,也就是300秒。

如果此参数设置的值超出了硬件实际的限制,比如你将它设置为60,你期望无论在任何情况下,数据库都可以在1分钟内完成实例恢复,但根据数据库的脏块生成速度、存储设备的写性能,1分钟内根本无法完成实例恢复。这时候Oracle会自动设置合适的fast_start_mttr_target参数值,我们可以在参数文件中看到修正后的参数值,也可以在V$instance_recovery视图中的Target_mttr列中看到实际的值。

我们不能将这个值设置的太小,因为实例恢复毕竟只是偶然现象。如果为了让实例恢复尽快完成,而设置fast_start_mttr_target为很小的值,那么DBWn将活动的很频繁,这会造成性能问题的。 为了避免用户设置不合理的增量检查点频率,在10G中,如果将fast_start_mttr_target设置为0,Oracle将根据产生脏块的速度、存贮硬件的性能自动调节检查点的频率,尽量使检查点频率不成为I/O问题的原凶。

检查点的主要任务就是催促DBWn刷新脏块,如果DBWn刷新脏块时的等待事件太多,就说明脏块太多、存储设备的写速度太慢,或者就是增量检查点的频率太高了,或太低了。DBWn写脏块的等待事件是Db  file  parallel write。如果你的增量检查点频率很低,你发现了此事件,在排除了存储设备写性能的问题后,你应该将增量检查点频率设置的高一些。反之,如果你的增量检查点频率本身很高,出现了Db file parallel write事件,这说明检查点频率太高了。

除它之外,还有一个和DBWn、增量检查点有关的等待事件,它是Write complete waits事件,当前台进程要修改DBWn正要成批写的块中的若干个块时,就会有此等待事件,这个事件是前台进程再等待DBWn写完成。这个等待事太多,说明了存储设备写性能有问题,或者增量检查点太频率了。

   我们可以V$instance_recovery中看到有关检查点的很多信息:

   Estimated_mttr列如果太大,说明检查点不够频繁,同时也说明脏块产生的太多。

   同时在V$sysstat资料视图中,还有两个资料background checkpoints started、backgroundcheckpoints completed,前面的一个是后台进程检查点开始次数,后一个是后台进程检查点完成次数。后台进程检查点的意义,其实就是增量检查点。只有增量检查点是由后台进程触发的。如果你用Alter system checkpoing命令让系统完成完全检查点,这叫做前台检查点与增量检查点无关,是不会被记入这两个资料了。如果这两个值经常相差一些,比如检查点的开始次数比完成次数大的不至1,这说明有太多次检查点开始,但没有及时完成。这说明检查点太频繁或检查点完成的太慢。

   检查点的问题大多数情况下其实都是DBWn写I/O的问题, DBWn写脏块的等待事件是Db file parallel write,还有Write completewaits等待事件,是当前台进程要修改DBWn正要成批写的块中的若干个块时,就会有此等待事件,这个事件是前台进程再等待DBWn写完成。这个等待事太多,也说明了DBWn有问题。

  

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值