分布式缓存系统——《高性能分布式缓存Redis》_高性能分布式缓存redis(1)

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Golang全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注go)
img

正文

  • 在服务器开启时一次性初始化到服务器本地缓存
  • 采用Guava Cache,Guava Cache用于存储频繁使用的少量数据,支持高并发访问
  • 也可以使用JDK的CurrentHashMap,需要自行实现

3、热门职位
数据特点:频繁变化,不必时时同步;但一定要有数据,不能为空
方案:

  • 数据从服务层读取(dubbo),然后放到本地缓存中(Guava),如果出现超时或读取为空,则返回原
  • 来本地缓存的数据。
    注意:不同的客户端看到的数据有可能不一样

4、数据回填
从Dubbo中读取数据时,先读取Redis集群的缓存,如果缓存命中则直接返回。如果缓存不命中则返回本地缓存,不能直接读取数据库。用异步的形式从数据库刷入到缓存中。
5、热点策略
对于热点数据我们采用本地缓存策略,而不采用服务熔断策略,因为首页数据可以不准确,但不能不响应。

高并发脏读

先更新数据库,在更新缓存

在这里插入图片描述

先删除缓存,在更新数据库

在这里插入图片描述

先更新数据库,再删除缓存(推荐)

在这里插入图片描述

Redis底层结构和缓存原理

本章学习目标:

  • 掌握Redis五种基本数据类型的用法和常见命令的使用
  • 了解bitmap、geo、stream的使用
  • 理解Redis底层数据结构(Hash、跳跃表、quicklist)
  • 了解RedisDB和RedisObject
  • 理解LRU算法
  • 理解Redis缓存淘汰策略
  • 能够较正确的应用Redis缓存淘汰策略

Redis数据类型和应用场景

Redis是一个Key-Value的存储系统,使用ANSI C语言编写。key的类型是字符串。value的数据类型有:常用的:string字符串类型、list列表类型、set集合类型、sortedset(zset)有序集合类型、hash类型。不常见的:bitmap位图类型、geo地理位置类型。Redis5.0新增一种:stream类型
注意:Redis中命令是忽略大小写,(set SET),key是不忽略大小写的 (NAME name)

Redis中key的设计
  1. 用:分割
  2. 把表名转换为key前缀, 比如: user:
  3. 第二段放置主键值
  4. 第三段放置列名

比如:用户表user, 转换为redis的key-value存储
username 的 key: user:9:username
{userid:9,username:zhangf}
email的key user:9:email
表示明确:看key知道意思
不易被覆盖

数据类型及命令手册

参考:https://blog.csdn.net/qq_36581961/article/details/113248387

Redis客户端访问

Java程序访问Redis

采用jedis API进行访问即可
关闭RedisServer端的防火墙

systemctl stop firewalld(默认)
systemctl disable firewalld.service(设置开启不启动)

新建maven项目后导入Jedis包

redis.clients jedis 2.9.0

测试类

@Test
public void testConn(){
//与Redis建立连接 IP+port
Jedis redis = new Jedis(“192.168.127.128”, 6379);
//在Redis中写字符串 key value
redis.set(“jedis:name:1”,“jd-zhangfei”);
//获得Redis中字符串的值
System.out.println(redis.get(“jedis:name:1”));
//在Redis中写list
redis.lpush(“jedis:list:1”,“1”,“2”,“3”,“4”,“5”);
//获得list的长度
System.out.println(redis.llen(“jedis:list:1”));
}

Spring访问Redis

新建项目,添加依赖

org.springframework spring-beans 5.2.5.RELEASE org.springframework spring-core 5.2.5.RELEASE org.springframework spring-context 5.2.5.RELEASE org.springframework spring-test 5.2.5.RELEASE junit junit 4.12 test org.springframework.data spring-data-redis 1.0.3.RELEASE

添加Spring配置文件

<?xml version="1.0" encoding="UTF-8"?>





classpath:redis.properties


添加properties文件

redis.pool.maxActive=100
redis.pool.maxIdle=50
redis.pool.maxWait=1000
redis.pool.testOnBorrow=true
redis.timeout=50000
redis.server=192.168.72.128
redis.port=6379

编写测试用例

import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
import java.io.Serializable;
@ContextConfiguration({ “classpath:redis.xml” })
public class RedisTest extends AbstractJUnit4SpringContextTests {
@Autowired
private RedisTemplate<Serializable, Serializable> rt;
@Test
public void testConn() {
rt.opsForValue().set(“name”,“zhangfei”);
System.out.println(rt.opsForValue().get(“name”));
}
}

SpringBoot访问Redis

新建项目,导入依赖

org.springframework.boot spring-boot-starter-data-redis

添加配置文件application.yml

spring:
redis:
host: 192.168.72.128
port: 6379
jedis:
pool:
min-idle: 0
max-idle: 8
max-active: 80
max-wait: 30000
timeout: 3000

添加配置类RedisConfig

package com.lagou.sbr.cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory factory;
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
}

** 添加RedisController**

package com.lagou.sbr.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping(value = “/redis”)
public class RedisController {
@Autowired
RedisTemplate redisTemplate;
@GetMapping(“/put”)
public String put(@RequestParam(required = true) String key,
@RequestParam(required = true) String value) {
//设置过期时间为20秒
redisTemplate.opsForValue().set(key,value,20, TimeUnit.SECONDS);
return “Success”;
}
@GetMapping(“/get”)
public String get(@RequestParam(required = true) String key){
return (String) redisTemplate.opsForValue().get(key);
}
}

修改Application并运行

package com.lagou.sbr;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class SpringbootRedisApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootRedisApplication.class, args);
}
}

底层数据结构

参考(未更新完整):https://blog.csdn.net/qq_36581961/category_10691600.html

缓存过期和淘汰策略

Redis性能高:
官方数据
读:110000次/s
写:81000次/s
长期使用,key会不断增加,Redis作为缓存使用,物理内存也会满
内存与硬盘交换(swap) 虚拟内存 ,频繁IO 性能急剧下降

maxmemory

不设置的场景
Redis的key是固定的,不会增加
Redis作为DB使用,保证数据的完整性,不能淘汰 , 可以做集群,横向扩展
缓存淘汰策略:禁止驱逐 (默认)
设置的场景
Redis是作为缓存使用,不断增加Key
maxmemory : 默认为0 不限制
问题:达到物理内存后性能急剧下架,甚至崩溃;内存与硬盘交换(swap) 虚拟内存 ,频繁IO 性能急剧下降
设置多少?与业务有关
1个Redis实例,保证系统运行 1 G ,剩下的就都可以设置Redis;物理内存的3/4
slaver : 留出一定的内存
在redis.conf中

maxmemory 1024mb

命令: 获得maxmemory数

CONFIG GET maxmemory

设置maxmemory后,当趋近maxmemory时,通过缓存淘汰策略,从内存中删除对象
不设置maxmemory 无最大内存限制 maxmemory-policy noeviction (禁止驱逐) 不淘汰
设置maxmemory maxmemory-policy 要配置

expire数据结构

在Redis中可以使用expire命令设置一个键的存活时间(ttl: time to live),过了这段时间,该键就会自动被删除
expire的使用
expire命令的使用方法如下:expire key ttl(单位秒)

127.0.0.1:6379> expire name 2 #2秒失效
(integer) 1
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> set name zhangfei
OK
127.0.0.1:6379> ttl name #永久有效
(integer) -1
127.0.0.1:6379> expire name 30 #30秒失效
(integer) 1
127.0.0.1:6379> ttl name #还有24秒失效
(integer) 24
127.0.0.1:6379> ttl name #失效
(integer) -2

expire原理

typedef struct redisDb {
dict *dict; – key Value
dict *expires; – key ttl
dict *blocking_keys;
dict *ready_keys;
dict *watched_keys;
int id;
} redisDb;

上面的代码是Redis 中关于数据库的结构体定义,这个结构体定义中除了 id 以外都是指向字典的指针,其中我们只看 dict 和 expires

dict 用来维护一个 Redis 数据库中包含的所有 Key-Value 键值对,expires则用于维护一个 Redis 数据库中设置了失效时间的键(即key与失效时间的映射)

当我们使用 expire命令设置一个key的失效时间时,Redis 首先到 dict 这个字典表中查找要设置的key是否存在,如果存在就将这个key和失效时间添加到 expires 这个字典表。当我们使用 setex命令向系统插入数据时,Redis 首先将 Key 和 Value 添加到 dict 这个字典表中,然后将 Key 和失效时间添加到 expires 这个字典表中。

简单地总结来说就是,设置了失效时间的key和具体的失效时间全部都维护在 expires 这个字典表中。

删除策略

Redis的数据删除有定时删除惰性删除主动删除三种方式。Redis目前采用惰性删除+主动删除的方式。

定时删除

在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。
创建一个定时器,当key设置有过期时间 且过期时间到达 由定时器任务立即对键的删除

  • 优点:节约内存 到时删除 快速释放不必要的内存
  • 缺点:CPU压力很大 无论CPU此时的负载多高 均占用CPU 会影响redis服务器相应时间和指令吞吐量

总结:以时间换空间

惰性删除

数据到达过期时间 不做处理 等下次访问该数据时

  • 如果过期 删除
  • 如果未过期 返回数据
  • 优点:节约CPU性能 发现必须删除才删除
  • 缺点:内存压力大 出现长期占用内存数据

总结:用存储空间换处理器性能 以空间换时间
调用expireIfNeeded函数,该函数的意义是:读取数据之前先检查一下它有没有失效,如果失效了就删除它。

int expireIfNeeded(redisDb *db, robj *key) {
//获取主键的失效时间 get当前时间-创建时间>ttl
long long when = getExpire(db,key);
//假如失效时间为负数,说明该主键未设置失效时间(失效时间默认为-1),直接返回0
if (when < 0) return 0;
//假如Redis服务器正在从RDB文件中加载数据,暂时不进行失效主键的删除,直接返回0
if (server.loading) return 0;
//如果以上条件都不满足,就将主键的失效时间与当前时间进行对比,如果发现指定的主键
//还未失效就直接返回0
if (mstime() <= when) return 0;
//如果发现主键确实已经失效了,那么首先更新关于失效主键的统计个数,然后将该主键失
//效的信息进行广播,最后将该主键从数据库中删除
server.stat_expiredkeys++;
propagateExpire(db,key);
return dbDelete(db,key);
}

定期删除(主动删除)

在这里插入图片描述
周期性轮询redis库中的时效性数据 采用随机抽取的策略 利用过期数据占比的方式控制删除频度

  • 特点:CPU性能占用设置有峰值 检测频度可以自定义设置
  • 特点:内存占用率不是很大 ,长期占用内存的冷数据会被持续清理
逐出算法

在这里插入图片描述
数据逐出的相关配置

  • 最大可使用配置

占用物理内存的比例 默认值为0 表示不限制 生产环境按照需求设定 通常设置在50%以上

maxmemory

  • 每次选取待删除的个数

选取数据时并不会全库扫描 导致严重的性能消耗 降低读写性能 因此采用随机获取数据的方式作为待检测删除数据

maxmemory-samples

  • 删除策略

达到最大内存之后 对被挑出来的数据进行删除的策略

maxmemory-policy

在这里插入图片描述

通信协议及事件处理机制

本章学习目标:

  • 知道Redis的请求响应模式
  • 理解请求数据格式(RESP)
  • 描述命令处理流程
  • 知道Redis的响应格式
  • 掌握4种IO多路复用模式(epoll)
  • 理解aeEventLoop

通信协议

Redis是单进程单线程的。应用系统和Redis通过Redis协议(RESP)进行交互。

请求响应模式

Redis协议位于TCP层之上,即客户端和Redis实例保持双工的连接。
在这里插入图片描述

串行的请求响应模式(ping-pong)

串行化是最简单模式,客户端与服务器端建立长连接
连接通过心跳机制检测(ping-pong)ack应答
客户端发送请求,服务端响应,客户端收到响应后,再发起第二个请求,服务器端再响应。
在这里插入图片描述

telnet和redis-cli 发出的命令 都属于该种模式
特点:

  • 有问有答
  • 耗时在网络传输命令
  • 性能较低
双工的请求响应模式(pipeline)

批量请求,批量响应。请求响应交叉进行,不会混淆(TCP双工)
在这里插入图片描述

  • pipeline的作用是将一批命令进行打包,然后发送给服务器,服务器执行完按顺序打包返回。
  • 通过pipeline,一次pipeline(n条命令)=一次网络时间 + n次命令时间
原子化的批量请求响应模式(事务)

Redis可以利用事务机制批量执行命令。后面会详细讲解。

发布订阅模式(pub/sub)

发布订阅模式是:一个客户端触发,多个客户端被动接收,通过服务器中转。后面会详细讲解。

脚本化的批量执行(lua)

客户端向服务器端提交一个lua脚本,服务器端执行该脚本。后面会详细讲解。

请求数据格式

Redis客户端与服务器交互采用序列化协议(RESP)。请求以字符串数组的形式来表示要执行命令的参数。Redis使用命令特有(command-specific)数据类型作为回复。
Redis通信协议的主要特点有:

  • 客户端和服务器通过 TCP 连接来进行数据交互, 服务器默认的端口号为 6379 。
  • 客户端和服务器发送的命令或数据一律以 \r\n (CRLF)结尾。

在这个协议中, 所有发送至 Redis 服务器的参数都是二进制安全(binary safe)的。简单,高效,易读。

内联格式

可以使用telnet给Redis发送命令,首字符为Redis命令名的字符,格式为 str1 str2 str3…

[root@localhost bin]# telnet 127.0.0.1 6379
Trying 127.0.0.1…
Connected to 127.0.0.1.
Escape character is ‘^]’.
ping
+PONG
exists name
:1

规范格式(redis-cli)

1、间隔符号,在Linux下是\r\n,在Windows下是\n
2、简单字符串 Simple Strings, 以 "+“加号 开头
3、错误 Errors, 以”-"减号 开头
4、整数型 Integer, 以 “:” 冒号开头
5、大字符串类型 Bulk Strings, 以 "$"美元符号开头,长度限制512M
6、数组类型 Arrays,以 "*"星号开头

用SET命令来举例说明RESP协议的格式。

redis> SET mykey Hello
“OK”

实际发送的请求数据:

*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$5\r\nHello\r\n
*3
$3
SET
$5
mykey
$5
Hello

实际收到的响应数据:

+OK\r\n

命令处理流程

整个流程包括:服务器启动监听、接收命令请求并解析、执行命令请求、返回命令回复等。
在这里插入图片描述

Server启动时监听socket
  • 启动调用 initServer方法
  • 创建eventLoop(事件机制)
  • 注册时间事件处理器
  • 注册文件事件(socket)处理器
  • 监听 socket 建立连接
建立Client
  • redis-cli建立socket
  • redis-server为每个连接(socket)创建一个 Client 对象
  • 创建文件事件监听socket
  • 指定事件处理函数
读取socket数据到输入缓冲区

从client中读取客户端的查询缓冲区内容。

解析获取命令
  • 将输入缓冲区中的数据解析成对应的命令
  • 判断是单条命令还是多条命令并调用相应的解析器解析
执行命令

解析成功后调用processCommand 方法执行命令,如下图:
在这里插入图片描述
大致分三个部分:

  • 调用 lookupCommand 方法获得对应的 redisCommand
  • 检测当前 Redis 是否可以执行该命令
  • 调用 call 方法真正执行命令
协议响应格式
  • 状态回复
    对于状态,回复的第一个字节是“+” OK
  • 错误回复
    对于错误,回复的第一个字节是“ - ”
  1. -ERR unknown command ‘foobar’
  2. -WRONGTYPE Operation against a key holding the wrong kind of value
  • 整数回复
    对于整数,回复的第一个字节是“:” ":6"
  • 批量回复

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
器解析

执行命令

解析成功后调用processCommand 方法执行命令,如下图:
在这里插入图片描述
大致分三个部分:

  • 调用 lookupCommand 方法获得对应的 redisCommand
  • 检测当前 Redis 是否可以执行该命令
  • 调用 call 方法真正执行命令
协议响应格式
  • 状态回复
    对于状态,回复的第一个字节是“+” OK
  • 错误回复
    对于错误,回复的第一个字节是“ - ”
  1. -ERR unknown command ‘foobar’
  2. -WRONGTYPE Operation against a key holding the wrong kind of value
  • 整数回复
    对于整数,回复的第一个字节是“:” ":6"
  • 批量回复

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-DVKnVhmH-1713467498240)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 18
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
  1.Memcached是什么?   Memcached是高性能的,分布式的内存对象缓存系统,用于在动态应用中减少数据库负载,提升访问速度。Memcached通过在内存里维护一个统一的巨大的hash表,它能够用来存储各种格式的数据,包括图像、视频、文件以及数据库检索的结果等。Memcached由Danga Interactive最初为了加速 LiveJournal网站访问速度而开发的,后 来被很多大型的网站采用。起初作者编写它可能是为了提高动态网页应用,为了减轻数据库检索的压力,来做的这个缓存系统。它的缓存是一种分布式的,也就是可 以允许不同主机上的多个用户同时访问这个缓存系统,这种方法不仅解决了共享内存只能是单机的弊端, 同时也解决了数据库检索的压力,最大的优点是提高了访问获取数据的速度!基于memcached作者对分布式cache的理解和解决方案。memcached完全可以用到其他地方 比如分布式数据库,分布式计算等领域。Memcached将数据库负载大幅度降低,更好的分配资源,更快速访问。   2.Memcached工作机制   通过在内存中开辟一块区域来维持一个大的hash表来加快页面访问速度,和数据库是独立的。但是目前主要用来缓存数据库的数据。允许多个server通过网络形成一个大的hash,用户不必关心数据存放在哪,只调用相关接口就可。存放在内存的数据通过LRU算法进行淘汰出内存。同时可以通过删除和设置失效时间来淘汰存放在内存的数据。   现在一些.NET开发人员开始放弃ASP.NET内置的缓存机制,转而使用Memcached——一种分布式的内存缓存系统。当运行在单独的Web服务器上,你可以很容易地清除一个已经确认被改变了的缓存。可惜,ASP.NET没有一个很好的方法来支持多服务器。每个服务器上的缓存都对其他缓存的改变一无所知。   ASP.NET允许通过基于文件系统和数据库表的触发器来作废一个缓存。然而,这也存在问题,比如数据库触发器需要使用昂贵的轮询,以及触发器本身冗长的编程。但是,我们还是有其他的选择的。   不像ASP.NET内置的缓存机制,Memcached是一个分布式缓存系统。任何Web服务器都能更新或删除一个缓存项,并且所有其他的服务器都能在下次访问这些缓存项的时候自动获取到更新的内容。这是通过把这些缓存项存储在一个或者多个缓存服务器上来实现的。每一个缓存项都根据它的关键字的哈希值来分配到一个服务器上。 注:相关组词来源于百度文库
Redis 是一种高性能的内存键值存储系统,常用作分布式缓存。它支持多种数据结构,如字符串、哈希、列表、集合和有序集合,并提供了丰富的操作命令和功能。 在分布式缓存中,Redis 可以通过搭建 Redis 集群来实现高可用和扩展性。Redis 集群使用哈希槽分片的方式将数据分散存储在多个节点上。每个节点负责管理一部分哈希槽,并与其他节点进行数据交互和同步。这样,当需要查询或写入数据时,客户端会根据键的哈希值将请求发送到相应的节点上,从而实现分布式的数据存储和访问。 使用 Redis 分布式缓存可以带来以下好处: 1. 高性能Redis 的数据存储在内存中,读写速度非常快,适用于对响应时间要求较高的场景。 2. 高可用性:通过搭建 Redis 集群,即使某个节点发生故障,系统仍然可以继续正常工作。 3. 扩展性:可以根据需求增加节点数量,实现横向扩展,提高系统的处理能力。 4. 数据持久化:Redis 支持将数据持久化到磁盘,以防止数据丢失。 当使用 Redis 分布式缓存时,需要注意以下事项: 1. 数据一致性:由于 Redis 集群会将数据分片存储在不同节点上,需要确保数据的一致性,可以使用一致性哈希算法来解决这个问题。 2. 故障处理:当某个节点发生故障时,需要及时进行故障转移,将故障节点的数据迁移到其他正常节点上。 3. 客户端的负载均衡:需要在客户端实现负载均衡的策略,将请求均匀地分发到不同的节点上,以提高系统的整体性能。 总之,Redis 分布式缓存提供了高性能、高可用性和可扩展性的解决方案,可以用于加速应用程序的数据访问并提高系统的吞吐量。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值