分布式处理方案

一、引言

在分布式环境下,传统的一些技术会失败,比如传统的synchronized或者lock锁,以及创建数据库的事务,无法保证ACID,还有定时任务也可能会出现重复执行的问题。

二、分布式锁介绍


由于传统的锁是基于Tomcat服务器内部的,搭建了集群之后,导致锁失效,使用分布式锁来处理。

分布式锁介绍
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N0MLP9yI-1626955561741)(Pictures/1588483261673.png)]

三、分布式锁解决方案【重点


单体架构中的锁
public class ThreadDemo {

    public static void main(String[] args) {

        User user1 = new User("张三");
        User user2 = new User("李四");

        MyThread myThread1 = new MyThread(user1);
        MyThread myThread2 = new MyThread(user2);

        for(int i =0;i<10;i++){
            new Thread(myThread1).start();
            new Thread(myThread2).start();
        }
    }
}


class User {

    public String name;

    public User(String name){
        this.name = name;
    }

    public void add() throws InterruptedException {
        synchronized (User.class){ // this和User.class的区别?
            Thread.sleep(1000);
            System.out.println("name:"+name+",threadNmae:"+Thread.currentThread().getName()+"---> add");
            Thread.sleep(1000);
        }
    }
}

class MyThread implements  Runnable{

    private User user;

    public MyThread(User user){
        this.user = user;
    }

    @Override
    public void run() {
        try {
            user.add();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
双重锁机制
@Override
public List<User> getUserList() {
    List<User> userList = null;
    userList= (List<User>) redisTemplate.opsForValue().get("userList");
    if(userList == null){
        synchronized (this){
            userList= (List<User>) redisTemplate.opsForValue().get("userList");
            if(userList == null){
                System.out.println("查询数据库");
                userList = userDao.select(null);
                // 进行缓存重建
                redisTemplate.opsForValue().set("userList",userList);
                redisTemplate.expire("userList",5, TimeUnit.SECONDS); // 5s后失效
            }else{
                return userList;
            }
        }
    }
    return userList;
}
3.1 搭建环境

创建SpringBoot

编写抢购的业务

@RestController
public class SecondKillController {

    //1. 准备商品的库存
    public static Map<String,Integer> itemStock = new HashMap<>();

    //2. 准备商品的订单
    public static Map<String,Integer> itemOrder = new HashMap<>();

    static{
        itemStock.put("牙刷",10000);
        itemOrder.put("牙刷",0);
    }

    @GetMapping("/kill")
    public String kill(String item) throws InterruptedException {
        //1. 减库存
        Integer stock = itemStock.get(item);
        if(stock <= 0){
            return "商品库存数不足!!!";
        }
        
        Thread.sleep(100);
        itemStock.put(item,stock - 1);


        //2. 创建订单
        Thread.sleep(100);
        itemOrder.put(item,itemOrder.get(item) + 1);

        //3. 返回信息
        return "抢购成功!!!" + item + ": 剩余库存数为 - " + itemStock.get(item) + ",订单数为 - " + itemOrder.get(item);
    }

}

下载ab压力测试

ab -n 请求数 -c 并发数 访问的路径

测试

测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LEHKrMPs-1626955561749)(Pictures/1588486284377.png)]
3.2 Zookeeper实现分布式锁原理
Zookeeper实现分布式锁原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xZAp11rf-1626955561753)(Pictures/1588498580850.png)]
3.3 Zookeeper实现分布式锁

导入依赖

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.6.0</version>
    <exclusions>
        <exclusion>
            <artifactId>slf4j-api</artifactId>
            <groupId>org.slf4j</groupId>
        </exclusion>
        <exclusion>
            <artifactId>slf4j-log4j12</artifactId>
            <groupId>org.slf4j</groupId>
        </exclusion>
        <exclusion>
            <artifactId>log4j</artifactId>
            <groupId>log4j</groupId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>2.7.1</version>
</dependency>

编写配置类

@Configuration
public class ZkConfig {


    @Bean
    public CuratorFramework cf(){

        RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,2);

        CuratorFramework curatorFramework = CuratorFrameworkFactory.builder()
                .connectString("192.168.199.109:2181,192.168.199.109:2182,192.168.199.109:2183")
                .retryPolicy(retryPolicy)
                .build();

        curatorFramework.start();

        return curatorFramework;
    }

}

在业务代码中添加分布式锁

 @RequestMapping(value = "/seckill")
    public String seckill(String gname) throws Exception {

        InterProcessMutex interProcessMutex = new InterProcessMutex(cf, "/seckill_lock");

        // 获取锁
       // interProcessMutex.acquire(); // 没有获取到锁就一直等待
        if(interProcessMutex.acquire(1, TimeUnit.SECONDS)){ // 等待1s就放弃获取锁资源
            // 1.先减库存
            Integer stock = goodsMap.get(gname);
            if (stock <= 0) {
                return "库存不足。。。。";
            }
            goodsMap.put(gname, stock - 1);
            Thread.sleep(100);


            // 2.加订单数量
            Integer count = orderMap.get(gname);
            orderMap.put(gname, count + 1);
            Thread.sleep(100);

            // 释放锁
            interProcessMutex.release();
            
            return "抢购成功【" + gname + "】,库存:" + goodsMap.get(gname) + ",订单:" + orderMap.get(gname);
        }else{
           return "请稍后再试。。。";
        }

    }
3.4 Redis实现分布式锁原理
Redis实现分布式锁原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CI8MlOPi-1626955561757)(Pictures/1588503607681.png)]
3.5 Redis实现分布式锁

导入依赖,添加配置文件

# redis依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

# 配置文件
spring:
  redis:
    host: 192.168.199.109
    port: 6379

编写工具类

@Component
public class RedisLockUtil {

    @Autowired
    private StringRedisTemplate redisTemplate;


    public boolean lock(String key,String value,int second){
        return redisTemplate.opsForValue().setIfAbsent(key,value,second, TimeUnit.SECONDS);
    }


    public void unlock(String key){
        redisTemplate.delete(key);
    }
}

修改业务逻辑代码

@GetMapping("/redis/kill")
public String redisKill(String item) throws Exception {
    //...加锁
    if(lock.lock(item,System.currentTimeMillis() + "",1)){
        // 业务代码。。。
        // 释放锁
        lock.unlock(item);
    }
}

测试

lua脚本实现锁释放

分布式锁最终要保证加锁和设置过期时间是原子性。

查询锁和释放锁要保持原子性

    	// 查询缓存
        String s = redisTemplate.opsForValue().get("user-cache");

        if (s == null) {

            String lockValue = System.currentTimeMillis() + "";

            // 获取分布式锁
            Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", lockValue, 10, TimeUnit.SECONDS);
            if (lock) {
                try {
                    // 查询数据库,只要查询数据库出现异常就马上释放锁
                    s = "123";
                    redisTemplate.opsForValue().set("user-cache", s);
                } finally {
                    // 释放锁,这里查询和解锁不是一个原子性的操作,所以要使用lun脚本来释放
//                String lock1 = redisTemplate.opsForValue().get("lock");
                    
//                if (lockValue.equals(lock1)) {
//                    redisTemplate.delete("lock");
//                }

                    // 使用lua脚本来释放锁,因为它是原子性的
                    String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                            "then\n" +
                            "    return redis.call(\"del\",KEYS[1])\n" +
                            "else\n" +
                            "    return 0\n" +
                            "end";

                    // 注意这里的泛型只能是Long类型或其他的类型,在org.springframework.data.redis.connectio.ReturnType中有规定
                    DefaultRedisScript<Long> script1 = new DefaultRedisScript<>(script, Long.class);
                    redisTemplate.execute(script1,
                            Collections.singletonList("lock"), lockValue);
                }

            } else {
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                s = redisTemplate.opsForValue().get("user-cache");
            }
        }

四、分布式任务介绍


分布式任务介绍
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eG51NGiK-1626955561759)(Pictures/1588509745570.png)]

五、分布式任务解决方案【重点


5.1 Elastic-Job介绍

官网:http://elasticjob.io/index_zh.html

由当当网基于Quartz + Zookeeper的二次开放产品

  • 基于Zookeeper分布式锁,保证只有一个服务去执行定时任务。

  • 基于Zookeeper实现了注册中心,自动帮助我们去调度指定的服务执行定时任务。

  • 基于Zookeeper实现了注册中心,基于心跳的方式,自动去检测服务的健康情况。

5.2 Elastic-Job实现分布式任务

创建SpringBoot工程

导入依赖

<dependency>
    <groupId>com.dangdang</groupId>
    <artifactId>elastic-job-lite-spring</artifactId>
    <version>2.1.5</version>
</dependency>

配置Zookeeper信息

// 注册中心
@Bean
public CoordinatorRegistryCenter center(){
    CoordinatorRegistryCenter regCenter = new ZookeeperRegistryCenter(
            new ZookeeperConfiguration("192.168.199.109:2181,192.168.199.109:2182,192.168.199.109:2183", "elastic-job-demo"));
    regCenter.init();
    return regCenter;
}

创建指定的定时任务

@Component
public class MyElasticJob implements SimpleJob {

    @Override
    public void execute(ShardingContext context) {
        switch (context.getShardingItem()) {
            case 0:
                System.out.println("执行0任务!!");
                break;
            case 1:
                System.out.println("执行1任务!!");
                break;
            case 2:
                System.out.println("执行2任务!!");
                break;
            // case n: ...
        }
    }
}

配置执行的周期,并且开始调度任务

// 执行任务调度信息
@Bean
public SpringJobScheduler scheduler(MyElasticJob job,CoordinatorRegistryCenter center){
    // 定义作业核心配置
    JobCoreConfiguration simpleCoreConfig = JobCoreConfiguration.
            newBuilder("demoSimpleJob", "0/10 * * * * ?", 3)
            .shardingItemParameters("0=A,1=B,2=C").build();
    // 定义SIMPLE类型配置
    SimpleJobConfiguration simpleJobConfig = new SimpleJobConfiguration(simpleCoreConfig, MyElasticJob.class.getCanonicalName());
    // 定义Lite作业根配置
    LiteJobConfiguration simpleJobRootConfig = LiteJobConfiguration.newBuilder(simpleJobConfig).build();
    // 定义SpringJobScheduler
    SpringJobScheduler scheduler = new SpringJobScheduler(job,center,simpleJobRootConfig);
    scheduler.init();
    return scheduler;
}

测试

六、分布式事务介绍


6.1分布式事务介绍
分布式事务介绍
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dGAqSx3J-1626955561761)(images/1598769327830.png)]
6.2 Base理论

CAP理论,C:一致性,A:可用性,P:分区容错性。分布式环境下,三者取其二。

  • Eureka:AP,保证了可用性,舍弃了一致性。
  • Zookeeper,Redis:CP(强一致性),每一个节点必须能够找到Master才能对外提供服务,舍弃了可用性。

最终一致性和强一致性

强一致性:不管在任何时间段查询任何节点数据都必须一直,比如ZK。

最终一致性:在一段时间内查询节点数据不一致,但是经过一段时间查询每个节点,数据必须一致,比如数据库的主从复制。

Base理论

Base是Basically Available(基本可用),Soft State (软状态 ),Eventual Consitancy (最终一致性)三个短语缩写,Base理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当服务出现故障允许部分不可用,但要担保核心功能可用。允许数据在一段时间内不一致,但最终要达到一致,而满足Base理论事务,称之为柔性事务

Base理论特点,BA:基本可用,S:软状态,E:最终一致性。

  • 基于CAP理论演化而来的,通过牺牲强一致性来获得可用性。
  • 核心思想:允许部分功能不可用但要担保核心功能可用。允许数据在一段时间内不一致,但最终要达到一致。
  • 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如,电商网站交易付款出现问题了,商品依然可以正常浏览。
  • 软状态:由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫软状态),这个状态不影响系统可用性,如订单的"支付中"、“数据同步中”等状态,待数据最终一致后状态改为“成功”状态。
  • 最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。如订单的"支付中"状态,最终会变为“支付成功”或者"支付失败",使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。

七、分布式事务解决方案【重点


7.1 2PC两段提交

2是指两个阶段,P指的是准备阶段,C指的是提交阶段。

  • 准备阶段,参与者需要开启事务,执行SQL,保证数据库中已经存在相应的数据。参与者会向TransactionManager发送OK。

  • 提交阶段当TransactionManager收到了所有的参与者的通知之后,向所有的参与者发送Commit请求。

问题1:执行的性能是很低的。一般是传统事务的10倍以上。

问题2:TransactionManager是没有超时时间的。

问题3:TransactionManager存在单点故障的问题

2PC两段提交
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pQ0XzgQc-1626955561762)(Pictures/1588588652092.png)]
7.2 3PC三段提交

三段提交在二段提交的基础上,引入了超时时间机制,并且在二段提交的基础上,又多了一个步骤,在提交事务之前,再询问一下,数据库的日志信息,是否已经完善。

3PC三段提交
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BlGc8qNu-1626955561763)(images/1598771583471.png)]

什么是undo?----》记录修改前的数据,用于回滚事务

undo日志用于存放数据被修改前的值,假设修改 tba 表中 id=2的行数据,把Name=’B’ 修改为Name = ‘B2’ ,那么undo日志就会用来存放Name=’B’的记录,如果这个修改出现异常,可以使用undo日志来实现回滚操作,保证事务的一致性 。

什么是redo?—》记录修改后的数据,用于提交事务后写入数据文件,DB宕机后方便数据恢复。

redo是当数据库对数据做修改的时候,需要把数据页从磁盘读到buffer pool中,然后在buffer pool中进行修改,那么这个时候buffer pool中的数据页就与磁盘上的数据页内容不一致,如果这个时候发生非正常的DB服务重启,数据并没有同步到磁盘文件中,也就是会发生数据丢失,如果这个时候,能够在有一个文件,当buffer pool 中的data page变更结束后,把相应修改记录记录到这个文件,那么当DB服务发生宕机的情况,恢复DB的时候,也可以根据这个文件的记录内容,重新应用到磁盘文件,数据保持一致。

什么是Buffer Pool?

应用系统分层架构,为了加速数据访问,会把最常访问的数据,放在缓存(cache)里,避免每次都去访问数据库。
操作系统,会有缓冲池(buffer pool)机制,避免每次访问磁盘,以加速数据的访问。
MySQL作为一个存储系统,同样具有缓冲池(buffer pool)机制,以避免每次查询数据都进行磁盘IO。

缓冲池(buffer pool)是一种常见的降低磁盘访问的机制;
缓冲池通常以页(page)为单位缓存数据;

7.3 TCC机制

TCC(Try,Confirm,Cancel),和你的业务代码切合在一起。

  • Try:尝试去预执行具体业务代码。 下单订ing。。。

  • try成功了:Confirm:再次执行Confirm的代码。

  • try失败了:Cancel:再次执行Cancel的代码。

TCC成功情况
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HA3o6nZV-1626955561764)(images/20191125203340859.png)]
TCC失败情况
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zfsvvGD2-1626955561765)(images/20191125203433174.png)]

TCC分为三个阶段 :

​ Try阶段是做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confirm一起才能真正构成一个完整的业务逻辑。

​ Confirm阶段是做确认提交,Try阶段所有分支事务执行成功后开始执行Confirm。通常情况下,采用TCC则认为Confirm阶段是不会出错的。即 :只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需引入重试机制或人工处理。

​ Cancel阶段是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理。

事务模式原理

Try Confirm Cancle 如何实现A转账给B的呢?

idnamebalacne
1A200
2B100
//尝试方法
function try(){
    //记录日志
    todo save A 转出了 100 元 
    todo save B 转入了 100//执行转账
    update amount set balacne = balacne-100 where id = 1
    update amount set balacne = balacne+100 where id = 2
}
//确认方法
function confirm(){
    //清理日志
    clean save A 转出了 100 元 
    clean save B 转出了 100}

//取消方法
function cancle(){
    //加载日志
    load log A
    load log B

     //退钱
    update amount set balacne = balacne+100 where id = 1
    update amount set balacne = balacne-100 where id = 2    
}
7.4 MQ分布式事务

RabbitMQ在发送消息时,confirm机制,可以保证消息发送到MQ服务中,消费者有手动ack机制,保证消费到MQ中的消息。

MQ分布式事务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GKzytEw1-1626955561767)(Pictures/1588597788593.png)]
7.5TX- LCN实现分布式事务

基于三段提交和TCC实现的

TX-LCN分布式事务框架,LCN并不生产事务,LCN只是本地事务的协调工,LCN是一个高性能的分布式事务框架,兼容dubbo、springcloud框架,支持RPC框架拓展,支持各种ORM框架、NoSQL、负载均衡、事务补偿.

特性一览
1、一致性,通过TxManager协调控制与事务补偿机制确保数据一致性
2、易用性,仅需要在业务方法上添加@TxTransaction注解即可
3、高可用,项目模块不仅可高可用部署,事务协调器也可集群化部署
4、扩展性,支持各种RPC框架扩展,支持通讯协议与事务模式扩展

文档地址:https://www.codingapi.com/docs/txlcn-setting-manager/

lcn流程图实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-60oLoytW-1626955561768)(images/20190927094325130.png)]
  1. 服务发起者 在事务协调者内创建事务组,并将本事务加入事务组
  2. 事务参与者加入事务组,直到有结束标记出现
  3. 事务协调者向所有的事务参与者发送询问,是否能够提交!全部提交则事务组提交!有一个回滚标记则事务组回滚!
  4. 事务组执行操作之后,释放所有锁资源!

创建一个协调者工程,创建两个服务

协调者:添加依赖

<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-tm</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

协调者:编写配置文件

server:
  port: 8888
spring:
  datasource:
    hikari: # springBoot自动的一个连接池 ,因为tx-lcn他读取的就是hikari属性下面的
      username: root
      password: root
      driver-class-name: com.mysql.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/xsh
#    password: root
#    username: root
#    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/xsh
  jpa:
    hibernate:
      use-new-id-generator-mappings: false
  redis:
    host: 127.0.0.1
tx-lcn:
  manager:
    manager-address: localhost:8070

协调者:添加注解

@EnableTransactionManagerServer

协调者:准备表

# 创建表
CREATE TABLE `t_tx_exception`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `group_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `unit_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `mod_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `transaction_state` tinyint(4) NULL DEFAULT NULL,
  `registrar` tinyint(4) NULL DEFAULT NULL,
  `remark` varchar(4096) NULL DEFAULT  NULL,
  `ex_state` tinyint(4) NULL DEFAULT NULL COMMENT '0 未解决 1已解决',
  `create_time` datetime(0) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

服务: 添加依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-tc</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>

<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-txmsg-netty</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

服务: 编写配置文件

server:
  port: 8081

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql:///lcn?serverTimezone=UTC
    username: root
    password: root
  jpa:
    hibernate:
      use-new-id-generator-mappings: false
tx-lcn:
  client:
    manager-address: localhost:8070

服务: 启动类添加注解

@EnableDistributedTransaction

服务:Service层添加注解

@Override
@Transactional
@LcnTransaction
public void createOrder() {
//        1. 减库存
    restTemplate.getForObject("http://localhost:8082/item",String.class);
    int i = 1/0;
//        2. 创建订单
    orderMapper.save();
}

测试,异常后,事务回滚

测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NrZ5i8lc-1626955561769)(Pictures/1588606169980.png)]
TX-LCN常见的问题

异常的抛出一定要写到业务层才可以,否则分布式事务是没办法回滚异常的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值