随笔录--零拷贝

Redis 6.0 之后为何引入了多线程?6.0 之前为什么不使用多线程?

官方解析

在 Redis 6.0 之前,Redis 是单线程的,这是因为 Redis 的主要瓶颈是在 CPU 上。但是随着硬件的发展,现代服务器的 CPU 核心数已经达到了几十个,这就导致 Redis 单线程模型无法充分利用多核处理器的性能。因此,Redis 6.0 引入了多线程,以提高 Redis 在多核处理器上的性能。

Redis 6.0 之前为什么不使用多线程,主要有以下几个原因:

  1. Redis 单线程模型相对简单,容易维护和调试,代码逻辑也比较清晰。

  2. Redis 的主要瓶颈在于 CPU,而不是 I/O,因此采用多线程模型并不能显著提高性能

  3. Redis 是一个内存型数据库,它的性能主要受到 CPU 和内存带宽的限制。采用多线程模型会增加线程之间的竞争和锁等开销,反而可能降低 Redis 的性能。

但是随着硬件的发展,多核处理器已经成为了现代服务器的标配,因此 Redis 引入多线程的举措可以更好地发挥硬件的性能,提高 Redis 的吞吐量和响应速度。

redis 的多线程
  1. redis 在 6.0 支持的多线程是指网络 IO 的多线程,而 redis 的指令执行还是单线程的

  2. redis 的性能取决于网络 IO,内存和 CPU

  3. 在 redis6.0 之前的处理网络 IO 是单线程的,服务端对客户端的请求,socket 的连接,指令数据的接收,解析都由一个线程来执行,效率较低

  4. redis6.0 之后支持网络 IO 的多线程,通过多线程并行的执行来提高网络 IO 的处理效率

  5. redis 的服务端对用户传过来的指令的执行还是单线程的

  6. redis 的网络 IO 多线程默认是关闭的,需要在 redis.conf 中进行配置

  7. redis 对指令的执行不改为多线程,是因为指令的执行是在内存中,没有性能的瓶颈,且 redis 对数据结构进行了很多的优化。若 redis 指令的执行支持多线程,则需要对数据的操作加锁的同步,这会导致性能的下降,代价较大

  8. redis6.0 之前不使用多线程的原因:

    • redis 的性能瓶颈为内存和网络,官方说明 redis 几乎不存在 CPU 的性能瓶颈问题

    • redis 使用单线程,其可维护性提高

    • redis 若使用多线程,会增加需要对数据加锁,这增加了系统的复杂度,且会造成性能下降

关键词:网络IO多线程,指令执行单线程,内存,网络,CPU,默认关闭,可维护性,加锁同步

Redis 6.0 之后,引入多线程主要是为了解决 Redis 在处理大规模数据时,单线程的性能瓶颈问题,以提高 Redis 的处理能力和效率。

在 Redis 6.0 之前,Redis 只采用单线程的方式进行处理,这是因为 Redis 的核心是一个基于内存的键值对存储系统,它的瓶颈主要在于 CPU 的计算能力,而不是 I/O 操作的速度

单线程模式下,Redis 可以使用简单的事件驱动模型,来实现高效的网络通信和事件处理,避免了线程切换和上下文切换带来的开销,同时也避免了多线程之间的锁竞争问题。因此,在 6.0 之前,Redis 一直采用单线程模式运行。

但是随着 Redis 的应用场景不断扩大和升级,Redis 也面临着越来越大规模、越来越高并发的挑战,单线程模式已经不能满足这些需求了。因此,在 Redis 6.0 中引入了多线程技术,以利用多核 CPU 的计算能力,提高 Redis 的性能和处理能力。

Redis 6.0 引入多线程后,采用了多种技术手段来实现多线程操作的安全和稳定性,如使用锁和原子操作来保证数据一致性和线程安全。

需要注意的是:

  • Redis 6.0 中的多线程并不是完全替代了单线程模型,而是在其基础上引入了多线程支持,通过将一些负载耗时的操作(如I/O操作)交给后台线程处理,从而提高 Redis 的性能。同时,在多线程模式下,Redis 仍然保留了所有的单线程模式特性,如 ACID 事务等。

  • Redis 6.0 中多线程的使用是可选的,并且可以通过配置文件进行启用或禁用,以便在不同的应用场景下选择最适合的运行模式。

Redis 6. 0 引入多线程的原因有两个:

  1. 充分利用 CPU 多核,6.0 之前主线程只能利用一个核,多线程可以利用服务器 CPU 资源。

  2. 多线程任务可以分摊 Redis 同步 IO 读写负荷

Redis 6.0 默认情况下是关闭多线程的,需要在 redis.conf 配置文件中进行配置:

io-threads-do-reads yes

开启多线程后需要设置线程数,否则不会生效。线程数应该小于机器核数。官方建议 4 核的机器建议设置为 2 或 3 个线程,8 核的建议设置为 6 个线程 .

Redis 在处理客户端的请求时是单线程的,其中执行命令阶段,由于 Redis 是单线程来处理命令的,所有每一条到达服务端的命令不会立刻执行,所有的命令都会进入一个 Socket 队列中,当 socket 可读则交给单线程事件分发器逐个被执行. Redis6.0 引入多线程主要是为了解决 Redis 在处理客户端请求时网络 I/O 消耗过大的问题。多线程主要用来处理网络数据的读写和协议解析,而执行命令仍然是单线程.

Redis 的多线程网络模型并不是标准的 Multi-Reactors/Master-Workers 模型,I/O 线程任务仅仅是通过 socket 读取客户端请求命令并解析,但没有真正去执行命令. Redis 的多线程方案中,删除操作是可以被其他线程异步处理的,这些删除操作可以通过多线程的方式异步处理.

总的来说,Redis 引入多线程的目的是为了提高 Redis 的性能充分利用 CPU 多核分摊 Redis 同步 IO 读写负荷解决 Redis 在处理客户端请求时网络I/O消耗过大的问题。

之前为什么不使用多线程的原因:

  1. Redis 6.0 之前不使用多线程的原因是因为 Redis 通过 AE 事件模型以及 IO 多路复用等技术,处理性能非常高,因此没有必要使用多线程。单线程机制使得 Redis 内部实现的复杂度大大降低,Hash 的惰性 RehashLpush 等等“线程不安全”的命令都可以无锁进行。

  2. Redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,对于小数据包,Redis 服务器可以处理 80,000到 100,000 QPS,这也是 Redis 处理的极限了,对于 80% 的公司来说,单线程的 Redis 已经足够使用了。因此,在处理性能方面,Redis 6.0 之前的单线程机制已经足够。

  3. 但是随着互联网业务系统所要处理的线上流量越来越大,Redis 的单线程模式会导致系统消耗很多 CPU 时间在网络 I/O 上从而降低吞吐量。因此,Redis 6.0 引入了多线程来提高性能

  4. Redis 6.0 支持多线程有两个原因:一是可以充分利用服务器 CPU 资源,目前主线程只能利用一个核二是多线程任务可以分摊 Redis 同步 IO 读写负荷。通过将主线程 IO 读写任务拆分出来给一组独立的线程处理,使得多个 socket 读写可以并行化,但是 Redis 命令还是主线程串行执行

  5. Redis 的多线程网络模型实际上并不是一个标准的 Multi-Reactors/Master-Workers 模型I/O 线程任务仅仅是通过 socket 读取客户端请求命令并解析,却没有真正去执行命令。因此,Redis 6.0 的多线程模型相对于其他多线程模型更加轻量级。

  6. Redis 6.0 默认没有开启多线程,但可以在 conf 文件中进行配置。关于线程数的设置,官方建议 4 核的机器设置为 2 或 3 个线程,8 核的机器建议设置为 6 个线程,线程数一定要小于机器核数。

题目二

HTTP 协议中 GET 和 POST 有什么区别?分别适用于什么场景?

官方解析

HTTP 协议中 GET 和 POST 是两种常用的请求方法,它们的区别如下:

  1. 参数传递方式不同

    • GET 请求参数是在 URL 中以键值对的形式传递的,例如:http://www.example.com/?key1=value1&key2=value2。

    • 而 POST 请求参数是在请求体中以键值对的形式传递的。

  2. 参数传递大小不同 GET 请求参数有大小限制,因为 URL 长度有限制,不同的浏览器和服务器对 URL 长度的限制不同,一般为 2048 个字符。而 POST 请求参数没有大小限制,因为它们是以请求体的形式传递的。

  3. 安全性不同

    • GET 请求的参数是明文传输的,因为参数在 URL 中,如果涉及敏感信息(如密码),容易被窃取或暴露在浏览器历史记录、代理服务器日志等地方。

    • 而 POST 请求的参数在请求体中传输,相对安全一些,但是也需要注意参数加密和防止 CSRF 攻击等问题。

  4. GET 和 POST 适用的场景不同:

    • GET 请求适用于获取数据,如浏览网页、搜索等。因为 GET 请求参数以明文形式传输,容易被拦截和篡改,所以不适用于提交敏感信息的操作。

    • POST 请求适用于提交数据,如登录、注册、发布内容等。因为 POST 请求参数在请求体中传输,相对安全一些,可以提交敏感信息,但是需要注意参数加密和防止 CSRF 攻击等问题。

既然评论区都有这么多答案了,那还缺我一个吗?

  • 关于二者的区别 和 使用场景 ( 详见评论区 )

    • 参数位置

    • 安全性

    • 幂等

    • 长度

    • 缓存

    • 使用场景

拓展区别:

- GET 产生一个 TCP 数据包 ,浏览器会把 header 和 data 一并发送出去,服务器响应 200
- POST 产生两个 TCP 数据包 , 浏览器先发送 header,服务器响应 100(continue),然后再发送 data,服务器响应 200

在网络良好的情况下,差别不大。当然也不是所有的 POST 都发两次包,FireFox 在处理 POST 的时候,会一次性直接发送 header 和 data。(从这里也可以看出来,GET 和 POST 之所以产生区别,是因为浏览器/服务器)

补充说明( 把 get 和 post 结合 Http ):

  • 关于 get 和 post 甚至是 delete,put 等方法

    • 1、他们都是 Http 发送请求的方法

    • 2、Http 是基于 TCP 的

    • 所以,其实 GET,POST 都是 TCP 连接

    • 既然如此,从 TCP 协议来理解,GET 和 POST 没有本质区别,

  • 所以其实

    • get也可以带RequestBody

    • post也可以有?+参数

  • 之所以出现各种差异

    • 是因为Http 的规定和服务器/浏览器的限制,让 get 不能让 url 超过一定的长度限制,让 get 的 RequestBody 不被服务器处理,让 Post 的 url 的路径参数(?+参数)不被处理

题目三

什么是零拷贝?说一说你对零拷贝的理解?

官方解析

零拷贝(Zero-Copy)是一种高效的数据传输技术,它可以将数据从内核空间直接传输到应用程序的内存空间中,避免了不必要的数据拷贝,从而提高了数据传输的效率和性能。

在传统的数据传输方式中,当应用程序需要从磁盘、网络等外部设备中读取数据时,操作系统需要先将数据从外部设备拷贝到内核空间的缓冲区,然后再将数据从内核空间拷贝到应用程序的内存空间中,这个过程中需要进行两次数据拷贝,浪费了大量的 CPU 时间和内存带宽。

而使用零拷贝技术,数据可以直接从外部设备复制到应用程序的内存空间中,避免了中间的内核空间缓冲区,减少了不必要的数据拷贝,提高了数据传输的效率和性能

在网络编程中,零拷贝技术可以用于大文件的传输、网络文件系统的读写、数据库查询等场景中,提高数据传输的效率和响应速度。同时,零拷贝技术也可以减少系统内存的开销,提高系统的稳定性和可靠性。

零拷贝(Zero-Copy)是一种高效的数据传输技术,它可以将数据从内核空间直接传输到应用程序的内存空间中,避免了不必要的数据拷贝,从而提高了数据传输的效率和性能。

传统IO:

图片

零拷贝:

图片

  1. java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA 将数据读入内核缓冲区,不会使用 cpu

  2. 只会将一些 offset 和 length 信息拷入 socket 缓冲区,几乎无消耗

  3. 使用 DMA 将 内核缓冲区的数据写入网卡,不会使用 cpu

整个过程仅只发生了一次用户态与内核态的切换,数据拷贝了 2 次。所谓的【零拷贝】,并不是真正无拷贝,而是在不会拷贝重复数据到 jvm 内存中,零拷贝的优点有

  • 更少的用户态与内核态的切换

  • 不利用 cpu 计算,减少 cpu 缓存伪共享

  • 零拷贝适合小文件传输

零拷贝零拷贝(Zero-copy)是一种技术,它可以让计算机在处理数据时,不必在内存之间来回复制数据,从而降低内存复制的开销,提高计算机的性能和效率。具体来说,零拷贝是指在数据传输过程中,数据可以直接从发送端内存区域被传输到接收端内存区域,而无需在传输过程中进行任何复制操作。从而可以减少上下文切换以及CPU的拷贝时间。它是一种 I/O 操作优化技术

传统的 IO 执行流程

传统 IO 流过程包括 read 和 write 过程

  • read:把数据从磁盘读到内核缓冲区,再拷贝到用户缓冲区

  • write:先把数据写到 socket 缓冲区,再写入到网卡设备

图片

  • 用户应用进程调用 read 函数,向操作系统发起 IO 调用, 上下文从用户态转为内核态(切换1)

  • DMA 控制器把数据从磁盘中,读取到内核缓冲区。

  • CPU 把内核缓冲区数据,拷贝到用户应用缓冲区, 上下文从内核态转为用户态(切换2),read 函数返回

  • 用户应用进程通过 write 函数,发起 IO 调用,上下文从用户态转为内核态(切换3)

  • CPU 将用户缓冲区中的数据,拷贝到 socket 缓冲区

  • DMA 控制器把数据从 socket 缓冲区,拷贝到网卡设备, 上下文从内核态切换回用户态(切换 4),write 函数返回

从流程图可以看出,传统 IO 的读写流程,包括了 4 次上下文切换(4 次用户态和内核态的切换)4 次数据拷贝(两次 CPU 拷贝以及两次的 DMA 拷贝)

零拷贝实现的几种方式这里的零拷贝其实是根据内核状态划分的,在这里没有经过 CPU 的拷贝,数据在用户态的状态下,经历了零次拷贝,所以才叫做零拷贝,但不是说不拷贝。

  • mmap + write

  • sendfile

  • 带有 DMA 收集拷贝功能的 sendfile

sendfile+DMA scatter/gather 实现的零拷贝sendfile 表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作,因此可以使用它来实现零拷贝。

图片

  • 用户进程发起 sendfile 系统调用, 上下文(切换1)从用户态转向内核态

  • DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。

  • CPU 把内核缓冲区中的 文件描述符信息包括内核缓冲区的内存地址和偏移量)发送到 socket 缓冲区

  • DMA 控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡

  • 上下文(切换2)从内核态切换回用户态,sendfile 调用返回。

可以发现,sendfile+DMA scatter/gather 实现的零拷贝,I/O 发生了 2 次用户空间与内核空间的上下文切换,以及 2 次数据拷贝。其中 2 次数据拷贝都是包 DMA 拷贝这就是真正的零拷贝(Zero-copy) 技术,全程都没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。

零拷贝(Zero-copy)是一种高效的数据传输机制,在追求低延迟的传输场景中十分常用。

它是一种 I/O 操作优化技术,指计算机执行 IO 操作时,CPU 不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及 CPU 的拷贝时间。

传统的数据传输方法需要将数据从一个缓冲区拷贝到另一个缓冲区,而零拷贝技术可以直接在内核空间中完成数据传输,避免了数据复制的过程,从而提高了数据传输的效率。

在传统的数据传输方法中,数据需要经过多次复制才能到达目标缓冲区,这个过程包含了用户空间和内核空间之间的多次上下文切换,这些上下文切换会导致系统的性能下降。而零拷贝技术可以避免这些上下文切换,从而提高了系统的性能。例如,当应用程序需要将大量的数据写入到磁盘或者网络中时,使用零拷贝技术可以减少数据复制的过程,提高数据传输的效率

零拷贝技术的实现主要有以下几种方式:

  • 文件描述符传递:通过文件描述符的传递,将数据从一个进程传递到另一个进程,避免了数据复制的过程。

  • sendfile()函数:在 Linux 系统中,sendfile()函数可以将一个文件描述符指向的文件内容直接传输到一个 socket 文件描述符中,避免了数据复制的过程。

  • mmap()函数:mmap()函数可以将一个文件映射到内存中,避免了数据复制的过程。

总之,零拷贝是一种高效的数据传输机制,在追求低延迟的传输场景中应用广泛。使用零拷贝技术可以避免数据复制的过程,减少上下文切换,提高系统的性能。

前端

题目一

怎么使用 CSS3 来实现动画?你实现过哪些动画?

官方解析

CSS3 提供了丰富的动画效果,通过使用 CSS3 动画可以实现许多视觉效果,比如旋转、平移、缩放、淡入淡出等。

CSS3 动画的实现需要使用 @keyframes 规则和 animation 属性,具体的实现步骤如下:

定义关键帧 使用 @keyframes 规则定义动画关键帧。例如,定义一个旋转动画:

@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

这个关键帧定义了从 0 度到 360 度的旋转动画。

应用动画 使用 animation 属性将动画应用到元素上。例如,将上面定义的 spin 动画应用到一个元素:

div {
  animation: spin 2s linear infinite;
}

这个样式定义了一个 2 秒线性旋转动画,并且无限循环。

CSS3 动画的优点在于它不需要使用 JavaScript,可以通过 CSS 代码实现各种复杂的动画效果,并且动画效果比使用 JavaScript 实现的动画效果更加平滑。

在实际开发中,我们可以使用 CSS3 动画来实现各种动态效果,比如页面切换、元素滚动、菜单展开等。具体实现要根据具体的需求来选择使用哪种动画效果,一些常见的动画效果包括:

  • 淡入淡出

  • 旋转

  • 缩放

  • 平移

  • 拉伸

  • 闪烁 我们可以使用 CSS3 动画来实现这些效果,提高用户体验和交互性。

css3 动画的实现的方案,大概有以下方案:

  • js 的 animation() 方法实现动画

  • @keyframes + animation:这是一个实现动画的组合,必须一起使用。

    i. @keyframes——创建动画

    (1)在 @keyframes 中用 from 和 to 创建动画

    (2)在 @keyframes 中用 “百分比” 创建动画

    (3)将 @keyframes 嵌套进要添加动画的元素的样式里

    ii. animation 执行动画

  • transition:表示过渡。transition 可以单独使用

  • transform:表示变形。使用 transform 实现动画时有两种选择:

  • transform + transition:一次性动画。transform 定义行为,transition 驱动,但一次仅能驱动一次。

  • transform + @keyframes + animation:支持循环动画。在 @keyframes 里使用 transform 定义行为,animation 驱动,可充分调整动画的实现,包括:指定动画任意的执行次数,指定动画的结束与开始的状态等等。

.iconfont-loadding {
  animation: start_loadding 800ms linear 100ms infinite normal none running;
}

@keyframes start_loadding {
  from { transform: rotate(0deg);}
  to { transform: rotate(360deg);}
}

transition 和 animation 实现动画的区别:transition:需要触发一个事件才执行动画。animation:自动执行动画,可循环执行。

题目二

如何使用 JS 判断某个字符串长度(要求支持 Emoji 表情)?

官方解析

在 JavaScript 中,可以使用 String 类型的 length 属性来获取字符串的长度。但是,由于 Emoji 表情在字符串中占用了两个字符的位置,因此直接使用 length 属性得到的结果并不准确。

为了正确地获取字符串的长度,可以使用如下的方法:

/**
 * 计算字符串的长度(支持 Emoji 表情)
 * @param {string} str - 要计算长度的字符串
 * @returns {number} - 字符串的长度
 */
function getLength(str) {
  let length = 0;
  for (let i = 0; i < str.length; i++) {
    const code = str.charCodeAt(i);
    if (code >= 0xd800 && code <= 0xdbff) {
      i++;
    }
    length++;
  }
  return length;
}

举个例子,对于字符串 "Hello,🌍!",使用该函数计算出的长度为 8,而使用 length 属性计算出的长度则为 9。

正则表达式替换

利用正则表达式将 emoji 替换成单字符的符号,然后再获取len就是正确的 length了。

replace不会影响原字符串的内容

const str = "H😋";
const len = str.replace(/\uD83C[\uDF00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/g,"-").length;
console.log(len)

输出

2
ES6特性

ES6大幅增强了对字符串的处理能力。借用数组,可以快速判断带有emoji字符的字符串长度。

const str = "A😋";
const arr = Array.from(str);
console.log(str.length,arr.length)

输出:

3 2
局限性

多码点的emoji比如👨‍👩‍👧,会显示长度为5,这是因为unicode的约定导致的,详情可看阮一峰的这篇博文 http://www.ruanyifeng.com/blog/2017/04/emoji.html

题目三

Vue Router 路由有哪些模式?各模式有什么区别?

官方解析

Vue Router 路由有三种模式:

  1. hash 模式:使用 URL 中的 hash(即 # 后面的内容)来作为路由路径。这种模式下,页面不会重新加载,只会更新 hash 值,并触发路由变化,从而渲染对应的组件。

  2. history 模式:使用 HTML5 中新增的 History API 来管理浏览历史记录,从而实现页面的前进和后退。在这种模式下,URL 中不会带有 # 号,而是使用真实的 URL 路径来作为路由路径。

  3. abstract 模式:在不需要基于浏览器的 API 时,可以使用这种模式。在这种模式下,路由器并不会监听 URL 变化,而是通过调用 router.replace 或 router.push 来进行导航。区别:

  4. hash 模式可以兼容较老的浏览器,但 URL 中会带有 # 号。

  5. history 模式无需带有 # 号,更加美观,但需要后端支持,否则刷新页面会导致 404 错误。

  6. abstract 模式主要用于一些特定场景,例如在使用 Node.js 时,可以使用 abstract 模式来构建路由。一般来说,如果需要支持较老的浏览器,或者不需要后端支持,可以使用 hash 模式;否则建议使用 history 模式。

Vue Router 路由有三种模式:hash 模式、history 模式和 abstract 模式。

区别hash模式history模式abstract模式
URL 格式URL 中带有 #,如:http://example.com/#/homeURL 中没有 #,而是直接使用 path,如:http://example.com/home没有真实的 URL,只使用虚拟路径,如:/home
浏览器兼容性兼容性好,支持所有浏览器需要 HTML5 支持,不支持 IE9 及以下版本无浏览器兼容问题
服务端配置不需要进行额外配置,可以直接在前端中使用需要服务器端进行配置,防止 404不需要服务端配置
SEO不利于 SEO,因为搜索引擎不会爬取 URL 中的 # 后面的内容对 SEO 更加友好,因为路径更加规范不利于 SEO
使用场景不需要考虑浏览器兼容性和服务端配置,适用于简单应用场景需要考虑 SEO 和更规范的 URL 地址,适用于较复杂应用场景非浏览器环境下的应用程序或者只需要使用编程式导航的情况

一般情况下,我们建议使用 history 模式,因为它对 SEO 更加友好、URL 更加规范,并且随着 HTML5 技术的普及,浏览器兼容性也不再是问题。但是在特定场景下,如需要支持 IE9 及以下浏览器,或者不方便进行服务端配置时,可以选择使用 hash 模式。而 abstract 模式则适用于一些特殊的场景,如非浏览器环境下的应用程序或者只需要使用编程式导航的情况。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值