redis(五)运维与原理(内存)

一、内存消耗分析

内存消耗可以分为父进程自身消耗和子进程消耗。

一、父进程消耗

1、内存使用统计

要查看redis自身使用内存相关指标,可用执行info memory命令查看:

127.0.0.1:6379> info memory
# Memory
used_memory:998552
used_memory_human:975.15K
used_memory_rss:5201920
used_memory_rss_human:4.96M
used_memory_peak:1058440
used_memory_peak_human:1.01M
used_memory_peak_perc:94.34%
used_memory_overhead:953640
used_memory_startup:809856
used_memory_dataset:44912
used_memory_dataset_perc:23.80%
allocator_allocated:1044080
allocator_active:1314816
allocator_resident:3723264
total_system_memory:3823226880
total_system_memory_human:3.56G
used_memory_lua:37888
used_memory_lua_human:37.00K
used_memory_scripts:0
used_memory_scripts_human:0B
number_of_cached_scripts:0
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
allocator_frag_ratio:1.26
allocator_frag_bytes:270736
allocator_rss_ratio:2.83
allocator_rss_bytes:2408448
rss_overhead_ratio:1.40
rss_overhead_bytes:1478656
mem_fragmentation_ratio:5.43
mem_fragmentation_bytes:4244392
mem_not_counted_for_evict:0
mem_replication_backlog:0
mem_clients_slaves:0
mem_clients_normal:143520
mem_aof_buffer:0
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0
lazyfreed_objects:0

部分属性说明

 其中需要重点的关注几个指标:used_memory_rss(redis进程占用的内存)和used_memory(内部数据总的内存大小)以及它们的比值mem_fragmentation_ratio。

        当mem_fragmentation_ratio>1,则表示redis有部分内存没有用于数据存储,而是被内存碎片消耗,两者相差越大,说明碎片率越严重。

        当mem_fragmentation_ratio<1时,这种情况一般出现在操作系统把Redis内存交换(Swap)到硬盘导致,出现这种情况时要格外关注,由于硬盘速度远远慢于内存,Redis性能会变得很差,甚至僵死。

 2、内存消耗划分

redis进程内消主要包括:自身内存+对象内存+缓冲内存+内存碎片。(used_memory:内部数据存储内存;内存碎片:used_memory_rss - used_memory)

 其中redis空进程的自身内存很小,通常used_memory_rss在3M左右,userd_memory在800k,所以相对于其他三个内存自身内存可以忽略不记。

1)、对象内存

对象内存是redis占用内存最大的一块,存储着用户所有的数据。redis的所有数据都采用key-value数据类型,每次创建键值对时,至少会创建两个类型对象——key对象和value对象。

其中key对象都是字符串,开发中长会忽略键的长度对内存的消耗,可以尽量少用过长的键。

value对象复杂一些,有五种基本类型,其他数据类型都是基于这五种类型上实现的,例如bitmqp和HyperLogLog都是使用字符串实现的,GEO使用有序集合实现的。类型不同,占用的内存就不同。

2)、缓冲内存

缓冲内存主要包括:客户端缓冲、复制积压缓冲区、AOF缓冲区(aof缓冲区和aof重写缓冲区)。

客户端缓冲区:所有接入redis连接的输入输出缓冲区。输入缓冲区无法控制,最大空间时1G,如果超时将断开连接。输出缓冲区由参数client-output-buffer-limit控制,具体如下:

(1)普通客户端:除了复制和订阅的客户端之外的所有连接,Redis的默认配置是:client-output-buffer-limit normal000,Redis并没有对普通客户端的输出缓冲区做限制,一般普通客户端的内存消耗可以忽略不计,但是当有大量慢连接客户端接入时这部分内存消耗就不能忽略了,可以设置maxclients做限制。特别是当使用大量数据输出的命令且数据无法及时推送给客户端时,如monitor命令,容易造成Redis服务器内存突然飙升。

(2)从客户端:主节点会为每个从节点单独建立一条连接用于命令复制,默认配置是:client-output-buffer-limit slave256mb64mb60。当主从节点之间网络延迟较高或主节点挂载大量从节点时这部分内存消耗将占用很大一部分,建议主节点挂载的从节点不要多于2个,主从节点不要部署在较差的网络环境下,如异地跨机房环境,防止复制客户端连接缓慢造成溢出。

(3)订阅客户端:当使用发布订阅功能时,连接客户端使用单独的输出缓冲区,默认配置为:client-output-buffer-limit pubsub32mb8mb60,当订阅服务的消息生产快于消费速度时,输出缓冲区会产生积压造成输出缓冲区空间溢出。

输入输出缓冲区容易在大流量场景中失控,这个需要重点监控,具体看之前文章。

复制积压缓冲区:Redis在2.8版本之后提供了一个可重用的固定大小缓冲区用于实现部分复制功能,根据repl-backlog-size参数控制,默认1MB。对于复制积压缓冲区整个主节点只有一个,所有的从节点共享此缓冲区,因此可以设置较大的缓冲区空间,如100MB,这部分内存投入是有价值的,可以有效避免全量复制。

AOF缓冲区(aof缓冲区和aof重写缓冲区):这部分空间用于在Redis重写期间保存最近的写入命令(具体看之前的持久化文章)。AOF缓冲区空间消耗用户无法控制,消耗的内存取决于AOF重写时间和写入命令量,这部分空间占用通常很小。

(这里提出一个疑问:aof缓冲区和aof重写缓冲区的区别???)

3)、内存碎片

redis默认的内存分配器采用的是jemalloc,还有其他的分配器例如glibc、tcmalloc等。

内存分配器为了更好的管理和重复利用内存,分配内存的策略一般采用固定范围的内存快进行分配。

例如jemalloc在64位系统中将内存空间划
分为:小、大、巨大三个范围。每个范围内又划分为多个小的内存块单位,如下所示:
·小:[8byte],[16byte,32byte,48byte,...,128byte],[192byte,256byte,...,512byte],[768byte,1024byte,...,3840byte]
·大:[4KB,8KB,12KB,...,4072KB]
·巨大:[4MB,8MB,12MB,...]
比如当保存5KB对象时jemalloc可能会采用8KB的块存储,而剩下的3KB空间变为了内存碎片不能再分配给其他对象存储。内存碎片问题虽然是所有内存服务的通病,但是jemalloc针对碎片化问题专门做了优化,一般不会存在过度碎片化的问题,正常的碎片率(mem_fragmentation_ratio)在1.03左右。但是当存储的数据长短差异较大时,以下场景容易出现高内存碎片问题:
(1)频繁做更新操作,例如频繁对已存在的键执行append、setrange等更新操作。
(2)大量过期键删除,键对象过期删除后,释放的空间无法得到充分利用,导致碎片率上升。

redis每个键值的大小都不一致,当有内存释放后有可能太小最终无法被马上使用。

上图中假定A、B、C三块内存都为4KB,A被释放后会有总共有3*4=12KB的内存可用。但是由于A、B、C不连续,如果这个时候有一个10KB的数据需要分配内存,以上内存是无法使用的,这就是内存的浪费。

大量的内存碎片会导致内存被浪费,这就需要对内存的碎片进行清理。

出现高内存碎片问题时常见的解决方式如下:
(1)数据对齐:在条件允许的情况下尽量做数据对齐,比如数据尽量采用数字类型或者固定长度字符串等,但是这要视具体的业务而定,有些场景无法做到。
(2)安全重启:重启节点可以做到内存碎片重新整理,因此可以利用高可用架构,如Sentinel或Cluster,将碎片率过高的主节点转换为从节点,进行安全重启。

(3)redis4.0之后,可以使用动态命令或者配置文件配置的方式自动处理内存碎片化

4.0版本后redis提供了自动内存碎片清理机制。核心思想就是对内存进行搬家合并,让空闲的内存合并到一起,形成大块可以使用的连续空间。继续上面的例子。

可以看到通过碎片整理的内存能够存储10KB的数据。

动态开启碎片自动清理

CONFIG SET activedefrag yes

具体看:https://www.cnblogs.com/Brake/p/14359330.html

二、子进程内存消耗

子进程内存消耗指的是AOF、RDB重写/持久化时redis创建子进程的内存消耗。

AOF进行重写的时候,虽然理论上需要两倍内存,但因为使用了copy-on-write技术,所有大大的减少了需要的内存。

在linux2.6.38后内核提供了THP机制(Transparent Huge Pages),该配置默认开启,该机制开启后内存页的4kB变成2MB(THP能减少内存分配的次数,同时可以加快子进程的fork速度)。这样如果父进程有大量写命令(这些修改的数据比较分散,此时需要很多个2 MB),则会造成大量无用内存副本,这样就会造成内存消耗。所以一般建议关闭该机制。

有时候开启redis可能报一些警告就是开启了THP机制:

redis启动日志的warning:

WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo madvise > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled (set to 'madvise' or 'never').

关闭THP机制:

echo never >  /sys/kernel/mm/transparent_hugepage/enabled

之后再加载则不会出现该警告了。

子进程内存消耗总结如下:
1、Redis产生的子进程并不需要消耗1倍的父进程内存,实际消耗根据期间写入命令量决定,但是依然要预留出一些内存防止溢出。
2、需要设置sysctl vm.overcommit_memory=1允许内核可以分配所有的物理内存,防止Redis进程执行fork时因系统剩余内存不足而失败。
3、排查当前系统是否支持并开启THP,如果开启建议关闭,防止copy-on-write期间内存过度消耗。

二、管理内存的原理与方法

主要通过控制内存上限和回收策略实现内存管理

一、设置内存上限

redis使用maxmemory参数限制最大可用内存。(防止所用内存超过服务器物理内存、超出maxmemory后使用LRU等删除策略释放空间)

注意这个maxmemory分配的内存是redis内部数据占用的内存(对应最大的used_memory,不包括内存碎片的内存)

所以当数据较多时,实际消耗的内存会比maxmemory要大(要注意这部分内存溢出)。

在给redis分配内存时,不仅要给redis实例分配内存,还要给fork子进程和linux系统本身分配内存。

例如我现在有一个24G的机器,现在我们就可以给linux分配4G、给fork分配4G,然后剩下16G则可以分配四个4G的redis实例。

二、动态调整内存上限

设置maxmemory可以用命令config  set  maxmemory进行动态修改。例如修改为6G:config set maxmemory 6GB

maxmemory的默认配置是无限使用服务器内存,但为了防止极端情况下导致系统内存耗尽,建议还是要设置maxmemory。

三、内存回收策略

内存回收主要体现在删除到期对象、内存达到maxmemory上限时触发内存溢出控制策略。

1、删除过期键对象(使用惰性删除+定时任务删除)

惰性删除:读值时查看是否过期,过期则删除。(存在大量没用过且过期了的键)

定时任务删除:每10秒有个定时任务,该定时任务采用自适应算法,采取部分键根据键的过期比例去使用快慢两种速率回收键。

该定时任务具体流程:

1)、定时任务在每个数据库随机检查20个键,发现有过期的键就直接删除。

2)、统计如果超过25%的键过期,则循环执行1)步骤直到比例没超过25%。

3)、先以慢模式统计回收键的时间,如果超过25毫秒,则触发快模式进行回收键任务(快模式下超时时间为1毫秒且2秒内只能运行1次)

(这个第3步我是看不懂????)

2、内存溢出控制策略

当所用内存达到maxmemory上限则会触发溢出控制策略。配置具体策略是maxmemory-policy参数控制,redis支持6种策略。

1)、noeviction:默认的策略,拒绝写入新数据,不删任何数据,此时只响应读操作。(会返回错误信息(error)OOM command not allowed when used memory)

2)、volatile-lru:根据LRU算法删除超时的键,如果没有可删除的键则回退到noeviction策略

3)、allkeys-lru:根据LRU算法删除键(不管键有没有设置过期时间)

4)、allkeys-random:随机删除键

5)、volatile-random:随机删除过期键

6)、volatile-ttl:删除最近将要过期的键,没有则回退到noeviction策略

可以通过config set maxmemory-policy {policy}动态配置。可以通过执行info stats命令查看evicted_keys指标找出当前Redis服务器已剔除的键数量。

三、内存优化技巧

一、redisObject对象

redis的所有值对象(string、hash、list、set、zset等)在内部都被定义为Object结构体,内部结构如下:

1、type字段:表示对象使用的数据类型(值对象对应五种基本类型),键都是string类型。

2、encoding字段:表示redis的内部编码。(同一个对象采用不同编码实现内存的占用情况存在明显差异)

3、lru字段:记录对象最后一次被访问的时间。当配置了maxmemory-policy=volatile-lru或者allkeys-lru时会辅助LRU算法进行键的删除。

(可以使用scan+object idletime命令批量查询哪些键长时间未被访问,找出长时间不访问的键进行清理,可降低内存占用)

 4、refcount字段:记录对象被引用的次数。用于通过引用次数回收内存。当值为0则可以安全回收。使用object refcount{key}
获取当前对象引用。当对象为整数且范围在[0-9999]时,Redis可以使用共享对象的方式来节省内存。

5、*ptr字段:如果数据是整数则直接存数据,否则则表示指向数据的指针。(如果非整数则会指向相应的数据,例如sds之类的)

(Redis在3.0之后对值对象是字符串且长度<=39字节的数据,内部编码为embstr类型,字符串sds和redisObject一起分配,从而只要
一次内存操作即可。即如果字符串的编码类型是emstr,则会一次性给redisObject和sds分配连续的空间,如果是非整数非emstr编码,则会各自对redisObject和sds分配内存(此时是两次,增加了消耗))高并发写入场景中,在条件允许的情况下,建议字符串长度控制在39字
节以内,减少创建redisObject内存分配次数,从而提高性能。

二、缩减键值对象

降低内存最直接的方式就是缩减键和值的长度。

值可以通过压缩算法压缩相应的json或者xml再存进redis种。例如使用Gzip压缩后的json可以降低60%的空间。

(可以使用比较高效的google的Snappy压缩工具)

三、共享对象池

共享对象池是指Redis内部维护[0-9999]的整数对象池。除了整数值对象,其他类型如list、hash、set、zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。

当设置maxmemory并启用LRU相关淘汰策略如:volatile-lru,allkeys-lru时,Redis禁止使用共享对象池。

为什么开启maxmemory和LRU淘汰策略后对象池无效?

LRU算法需要获取对象最后被访问时间,以便淘汰最长未访问数据,每个对象最后访问时间存储在redisObject对象的lru字段。对象共享意味着多个引用共享同一个redisObject,这时lru字段也会被共享,导致无法获取每个对象的最后访问时间。如果没有设置maxmemory,直到内存被用尽Redis也不会触发内存回收,所以共享对象池可以正常工作。综上所述,共享对象池与maxmemory+LRU策略冲突,使用时需要注意。对于ziplist编码的值对象,即使内部数据为整数也无法使用共享对象池,因为ziplist使用压缩且内存连续的结构,对象共享判断成本过高,ziplist编码细节后面内容详细说明。

为什么只有整数对象池?
首先整数对象池复用的几率最大,其次对象共享的一个关键操作就是判断相等性,Redis之所以只有整数对象池,是因为整数比较算法时间复杂度为O(1),只保留一万个整数为了防止对象池浪费。如果是字符串判断相等性,时间复杂度变为O(n),特别是长字符串更消耗性能(浮点数在Redis内部使用字符串存储)。对于更复杂的数据结构如hash、list等,相等性判断需要O(n 2 )。对于单线程的Redis来说,这样的开销显然不合理,因此Redis只保留整数共享对象池。

四、字符串优化

所有的键都是字符串类型,值对象数据虽然说是支持五种类型,但实际上除了整数外都是用字符串存储。

redis没有使用C语言的字符串类型,而是自己实现了字符串结构——内部简单动态字符串(SDS),具体结构如下:

字符串底层用char[] 字节数组存储(char只占一个字节),支持安全的二进制数据存储,内部采用预分配机制(小于1M每次扩容为1倍,其他每次+1M),惰性删除机制。

1、预分配机制

SDS存在预分配机制,日常中需要小心预分配带来的内存浪费。

 从测试数据上看,同样的数据追加后内存消耗非常严重。

在阶段1中,我们正常插入新的字符串(其中free表示预分配的空闲空间)

 

 其中总的占用空间=实际占用空间+1字节。最后的一个字节保存‘\0’标示结尾,(这里忽略len和ferr字段消耗的8字节)

阶段2中,我们往每个对象追加60个字节的数据。

追加后,因为没满1M,此时扩容1倍空间,则多出了120字节的空闲空间。

阶段3, 我们再插入和追加后相同的数据

此时的SDS分配如下:

此时发现free为0,则不存在空闲的空间(此时使用了阶段2的空闲空间,所以此时的free空间为0)。 此时相对于阶段2节省了每个对象预分配的空间,同时降低了碎片率。

虽然预分配防止重复分配内存,但也是因为这样会造成一定的内存浪费。空间预分配的规则如下:

1)第一次创建len属性等于数据实际大小,free等于0,不做预分配。
2)修改后如果已有free空间不够且数据小于1M,每次预分配一倍容量。如原有len=60byte,free=0,再追加60byte,预分配120byte,总占用空间:60byte+60byte+120byte+1byte。
3)修改后如果已有free空间不够且数据大于1MB,每次预分配1MB数据。如原有len=30MB,free=0,当再追加100byte,预分配1MB,总占用空间:1MB+100byte+1MB+1byte。

尽量减少字符串频繁修改操作如append、setrange,改为直接使用set修改字符串,降低预分配带来的内存浪费和内存碎片化。

2、字符串重构

有时一些json数据可以使用hash结构进行存储,例如

{
"vid": "413368768",
"title": " 搜狐屌丝男士 ",
"videoAlbumPic":"http://photocdn.sohu.com/60160518/vrsa_ver8400079_ae433_pic26.jpg",
"pid": "6494271",
"type": "1024",
"playlist": "6494271",
"playTime": "468"
}

 从上面的测试可以看出,第一次默认配置下使用hash类型,内存消耗不但没有降低反而比字符串存储多出2倍,而调整hash-max-ziplist-value=66之后内存降低为535.60M。因为json的videoAlbumPic属性长度是65,而hash-max-ziplist-value默认值是64,此时次超过了则会使用hashtable编码,而采用hashtable编码后会消耗大量内存,此时我们只要调整一下hash-max-ziplist-value为66,这样就只会使用ziplist编码,从而降低内存消耗。(后面会讲ziplist编码优化细节)

五、编码优化

1、了解编码

redis对外提供的几种数据类型,而再redis内部对这些类型存在不同编码的概念,编码的不太直接影响数据的内存占用和读写效率。

redis针对每种数据类型至少采用两种编码方式来实现。具体如下:

 通过不同编码实现效率和空间的平衡。比如当我们的存储只有10个元素的列表,当使用双向链表数据结构时,必然需要维
护大量的内部字段如每个元素需要:前置指针,后置指针,数据指针等,造成空间浪费,如果采用连续内存结构的压缩列表(ziplist),将会节省大量内存,而由于数据长度较小,存取操作时间复杂度即使为O(n2)性能也可满足需求。

2、控制编码类型

编码类型转换在写入数据的时候已经自动完成了,这个转换过程是不可逆的,转换规则只能从小内存编码向大内存编码转换。

下面是hash、list、set、zset内部编码的配置:

 

 掌握编码转换机制,我们就可以通过编码来优化内存。可以使用conig set命令来设置编码相关参数。

3、ziplist编码(压缩列表)

ziplist编码的主要目的是为了节约内存,因此数据都是采用线性连续的内存结构。hash、list、zset中都有ziplist编码的实现。

下面是一个ziplist的结构:

 其中一个ziplist中可以有多个entry元素,每个元素保存具体的数据。(整数或者字节数组)。其中各个字段的含义:

1、zlbytes:记录整个压缩列表所占字节长度(int32类型,长度4字节)

2、zltail:记录距离尾节点的偏移量,方便尾节点弹出(int32类型,长度4字节)

3、zllen:记录节点数量。(int16类型,长度2字节)

4、entry:记录节点数据。(长度由数据决定)

5、zlend:记录列表结尾(一字节)

每个entry元素中的参数说明:
1、pre_entry_bytes_length:记录前一个节点所占的空间(快速定位上一个节点,从而实现反向迭代)

2、encoding:表示当前节点编码和长度(前两位尾编码类型(字符串或整数,最后所有数据的存储类型都是字符串或者整数),其余的表示数据长度)

3、contents:保存节点的值。(可以针对这个数据的长度来修改编码长度从而优化内存占用)

通过上面的说明我们可以来总结下压缩列表的数据结构:
1)、内部表现为数据紧凑排列的一块连续内存数组。(可以看成java中的arraylist)
2)、可以模拟双向链表结构,以O(1)时间复杂度入队和出队。
3)、新增删除操作涉及内存重新分配或释放,加大了操作的复杂性。(?????)
4)、读写操作涉及复杂的指针移动,最坏时间复杂度为O(n2)。(????)
5)、适合存储小对象和长度有限的数据。

下面是对于hash、list、zset中使用ziplist的速度测试:

 可以看出,ziplist编码可以大幅度的降低内存占用。但相对的是运行耗时会增长。(耗时list<hash<zset)

ziplist压缩编码的性能表现跟值长度和元素个数密切相关,正因为如此Redis提供了{type}-max-ziplist-value和{type}-max-ziplist-entries相关参数来做控制ziplist编码转换。最后再次强调使用ziplist压缩编码的原则:追求空间和时间的平衡。

针对性能要求较高的场景使用ziplist,建议长度不要超过1000,每个元素大小控制在512字节以内。命令平均耗时使用info Commandstats命令获取,包含每个命令调用次数、总耗时、平均耗时,单位为微秒。

4、inset编码

inset是set的一种编码。内部表现为有序、不重复的整数集。

当集合只包含整数且长度不超过set-max-intset-entries配置时被启用。inset内部结构:

 1、encoding:整数类型(分为int-16、int-32、int-64)

2、length:表示集合元素个数

3、contents:整数数组(从小到大顺序排序)

因为整数类型存在三种情况,所以如果有的整数较大时,需要将整个整数数组的类型都变大(例如16都全部升级为32,此时会有内存浪费),升级会重新申请内存,然后把原数据拷贝到新数组。

所以一般尽量所有元素都在某个范围内,防止升级导致的消耗。

intset的内存占用和耗时都比hashtable的低。

六、控制键的数量

利用redis的数据结构降低外层键的数量,页可以节省大量内存。

例如现在有100万个键,此时可以根据手动哈希分组拆为1000个hash,每个hash存1000个。

 同样的数据使用ziplist编码的hash比string节约内存,节约内存量随着value空间减少越来越明显。

hash-ziplist类型比string类型写入耗时,但随着value空间的减少,耗时逐渐降低。

对于小对象的场景,使用hash存储比string存储的占用内存更低(注意是小对象,小对象时hash的编码才会使用ziplist编码)。对比结论:

1、hash节省内存的原因是使用了ziplist编码,如果使用了hashtable内存反而会增加。

2、ziplist的长度需要控制在1000以内,否则存取操作时间复杂度在O(n)到O(n2)之间,长列表会导致CPU消耗严重,得不偿失。(??????)

3、ziplist适合存储小对象,对于大对象不但内存优化效果不明显还会增加命令操作耗时。
4、需要预估键的规模,从而确定每个hash结构需要存储的元素数量。
5、根据hash长度和元素大小,调整hash-max-ziplist-entries和hash-max-ziplist-value参数,确保hash类型使用ziplist编码。

使用hash结构控制键的规模虽然可以大幅降低内存,但同样会带来问题,需要提前做好规避处理。如下所示:
1、客户端需要预估键的规模并设计hash分组规则,加重客户端开发成本。
2、hash重构后所有的键无法再使用超时(expire)和LRU淘汰机制自动删除,需要手动维护删除。
3、对于大对象,如1KB以上的对象,使用hash-ziplist结构控制键数量反而得不偿失。
不过瑕不掩瑜,对于大量小对象的存储场景,非常适合使用ziplist编码的hash类型控制键的规模来降低内存。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值