天天看點

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!

终身学习、乐于分享、共同成长!

前言

Seata是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata将为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本的GTS(Global Transaction Service 全局事务服务),不过该服务在2023年开始停止支持了。

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!

作为一个分布式事务框架,Seata可以支持多种数据源和多个服务的协同工作,从而实现分布式事务的管理。它的出现大大简化了开发人员的工作。下面,我们就来探究一下Seata的魅力,看看它究竟是如何让分布式事务变得不再困难的。

官网:https://seata.io/zh-cn/index.html(opens new window)

源码: https://github.com/seata/seata(opens new window)

官方Demo: https://github.com/seata/seata-samples(opens new window)

本文使用的seata版本:v1.6.1

Seata的核心

Seata的核心组件包括三个部分:

  1. Transaction Coordinator(TC):事务协调器,负责全局事务的协调和管理。
  2. Resource Manager(RM):资源管理器,负责管理分支事务所涉及的资源。
  3. Transaction Manager(TM):事务管理器,提供全局事务的创建、提交、回滚等操作。

其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。

Seata的工作流程如下:

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!
  1. TM请求TC开启一个全局事务。TC会生成一个XID作为该全局事务的编号。XID,会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。当一进入事务方法中就会生成XID ,global_table表存储的就是全局事务信息。
  2. RM请求TC将本地事务注册为全局事务的分支事务,通过全局事务的XID进行关联。当运行数据库操作方法,branch_table表存储事务参与者信息。
  3. TM请求TC告诉XID对应的全局事务是进行提交还是回滚。
  4. TC驱动RM们将XID对应的自己的本地事务进行提交还是回滚。

seata-server(TC)

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!
  • global_table:全局事务表,记录全局事务的基本信息,例如全局事务ID、事务状态等。每当有一个全局事务发起后,就会在该表记录全局事务ID。
  • branch_table:分支事务表,记录分支事务的基本信息,例如分支事务ID、所属全局事务ID、分支事务状态等。
  • lock_table:全局锁,记录分布式锁的信息,例如锁定的资源、锁的持有者等。

Seata快速开始

使用官方实验例子,模拟用户购物下单的业务操作。原理图如下:

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!

Seata具体实现步骤:

  1. TM端使用@GlobalTransaction进行全局事务的开启、提交、回滚。
  2. TM端开始远程调用业务服务。
  3. RM端(seata客户端)通过扩展DataSourceProxy,实现自动生成undo_log与TC上报。
  4. TM告知TC提交/回滚事务。
  5. TC通知RM各自提交/回滚事务操作,同时删除undo_log。
Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!

搭建seata-server(TC 协调者)

  • 下载seata-server

我用的seata是截止目前最新的版本1.6.1,下载地址:https://github.com/seata/seata/releases

下载后解压文件如下:

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!
  • 修改seata/conf/application.yml配置文件
Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!
#  Copyright 1999-2019 Seata.io Group.
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

server:
  port: 7091

spring:
  application:
    name: seata-server

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${user.home}/logs/seata
  extend:
    logstash-appender:
      destination: 127.0.0.1:4560
    kafka-appender:
      bootstrap-servers: 127.0.0.1:9092
      topic: logback_to_logstash

console:
  user:
    username: seata
    password: seata

seata:
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: nacos
    nacos: 
      server-addr: 127.0.0.1:8848  # nacos的ip端口
      group: DEFAULT_GROUP  # 对应的组,默认为DEFAULT_GROUP
      #namespace: 707f9b70-7c86-4278-bd74-83cef490a824 # 对应的命名空间,在nacos中配置
      username: nacos
      password: nacos
      data-id: seata-server.properties # nacos中存放seata的配置文件,后面会提该文件的使用方式,相当于seata服务启动的时候需要注册到nacos,并使用nacos中的配置文件

  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    nacos: 
      server-addr: 127.0.0.1:8848  # nacos的ip端口
      group: DEFAULT_GROUP  # 对应的组,默认为DEFAULT_GROUP
      #namespace: 707f9b70-7c86-4278-bd74-83cef490a824 # 对应的命名空间,在nacos中配置
      username: nacos
      password: nacos

  store:
    # support: file 、 db 、 redis
    mode: file
#  server:
#    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
           

seata.config.type 与seata.registry.type 都要修改为nacos。

修改config与registry中nacos的配置,其中namespace与group须提前在nacos中进行配置。

  • 在Nacos配置中心新增dataid为seata-server.properties的properties配置。

内容如下:

#For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html
#Transport configuration, for client and server
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableTmClientBatchSendRequest=false
transport.enableRmClientBatchSendRequest=true
transport.enableTcServerBatchSendResponse=false
transport.rpcRmRequestTimeout=30000
transport.rpcTmRequestTimeout=30000
transport.rpcTcRequestTimeout=30000
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
transport.serialization=seata
transport.compressor=none

#Transaction routing rules configuration, only for the client
# 此处的mygroup名字可以自定义,只修改这个值即可
service.vgroupMapping.mygroup=default
#If you use a registry, you can ignore it
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false

#Transaction rule configuration, only for the client
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=true
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.sagaJsonParser=fastjson
client.rm.tccActionInterceptorOrder=-2147482648
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
#For TCC transaction mode
tcc.fence.logTableName=tcc_fence_log
tcc.fence.cleanPeriod=1h

#Log rule configuration, for client and server
log.exceptionRate=100

#Transaction storage configuration, only for the server. The file, db, and redis configuration values are optional.
# 默认为file,一定要改为db,我们自己的服务启动会连接不到seata
store.mode=db
store.lock.mode=db
store.session.mode=db
#Used for password encryption

#These configurations are required if the `store mode` is `db`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can remove the configuration block.
# 修改mysql的配置
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
# 指定seata的数据库,下面会提
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000


#Transaction rule configuration, only for the server
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.distributedLockExpireTime=10000
server.xaerNotaRetryTimeout=60000
server.session.branchAsyncQueueSize=5000
server.session.enableBranchAsyncRemove=false
server.enableParallelRequestHandle=false

#Metrics configuration, only for the server
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
           

【注意】

  1. 修改service.vgroupMapping.mygroup=default该值,其中mygroup可以自定义,后面我们自己的服务启动时,配置文件中需要指定该group。
  2. 修改store.mode store.lock.mode store.session.mode这三个值为db,才能让seata连接到下面的数据库中。
  3. 修改store.db配置项下的配置,连接到自己的数据库。
  4. 该文件参数的具体作用参考官网:seata配置文件详解(官网 (opens new window))
  5. 该配置源文件存放在seata目录下:seata/script/config-center/config.txt
  • 在mysql数据库中新建seata所用到的数据库及数据表

数据库的名称与上述seata配置文件中的store.db.url保持一致。

添加seata的配置表,sql文件存放在:seata/script/server/db/mysql.sql中

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_status` (`status`),
    KEY `idx_branch_id` (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

CREATE TABLE IF NOT EXISTS `distributed_lock`
(
    `lock_key`       CHAR(20) NOT NULL,
    `lock_value`     VARCHAR(20) NOT NULL,
    `expire`         BIGINT,
    primary key (`lock_key`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
           
  • 启动seata-server
sh seata-server.sh -p 8091 -h 127.0.0.1           

参数解释:

-h: 将地址暴露给注册中心,其他服务可以访问seata服务

-p:要侦听的端口

启动成功后,即可访问http://127.0.0.1:7091/#/login (opens new window)该地址进入seata的webui,用户名与密码默认为seata,用户名和密码可在application.yml配置项:console.user中修改。

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!

并且查看nacos中的服务注册中心是否可以看到seata-server

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!

至此,seata-server就搭建成功了。

搭建seata-client(TM、RM)

再来回顾一下前面的业务场景,如图:

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!

因此这里需要搭建三个客户端应用:tm-server、order-server、storage-server,源码:

工程源码:https://gitee.com/it-codelab/springcloudalibaba-study/tree/main/seata-demo

新建数据库表,只有order-server、storage-server这两个应用需要用到数据库,新建两个对应的数据库:

  • storage数据库对应的库表
create table storage
(
    id         bigint(11) auto_increment
        primary key,
    product_id bigint(11) null comment '产品id',
    total      int        null comment '总库存',
    used       int        null comment '已用库存'
)
    charset = utf8;

create table undo_log
(
    id            bigint auto_increment
        primary key,
    branch_id     bigint       not null,
    xid           varchar(100) not null,
    context       varchar(128) not null,
    rollback_info longblob     not null,
    log_status    int          not null,
    log_created   datetime     not null,
    log_modified  datetime     not null,
    ext           varchar(100) null,
    constraint ux_undo_log
        unique (xid, branch_id)
)
    charset = utf8;
           
  • order数据库对应的库表
create table t_order
(
    id         bigint(11) auto_increment
        primary key,
    user_id    bigint(11)  null comment '用户id',
    product_id bigint(11)  null comment '产品id',
    count      int         null comment '数量',
    money      decimal(11) null comment '金额',
    status     int(1)      null comment '订单状态:0:创建中;1:已完成'
)
    charset = utf8;

create table undo_log
(
    id            bigint auto_increment
        primary key,
    branch_id     bigint       not null,
    xid           varchar(100) not null,
    context       varchar(128) not null,
    rollback_info longblob     not null,
    log_status    int          not null,
    log_created   datetime     not null,
    log_modified  datetime     not null,
    ext           varchar(100) null,
    constraint ux_undo_log
        unique (xid, branch_id)
)
    charset = utf8;
           

每个RM服务对应的数据库都需要添加undo_log表。

主要配置以及核心代码:

  • 添加seata依赖,三个应用都一样
<!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.6.1</version>
        </dependency>
           
  • 在bootstrap.yml文件加入seata的相关配置
seata:
  enabled: true
  #application-id: ${spring.application.name}
  # 事务群组(可以每个应用独立取名,也可以使用相同的名字),要与服务端nacos-config.txt中service.vgroup_mapping的后缀对应
  tx-service-group: my_test_tx_group # 事务分组名称,要和服务端对应
  service:
    vgroup-mapping:
      my_test_tx_group: default # key是事务分组名称 value要和服务端的机房名称保持一致
  config:
    type: nacos
    # 需要和server在同一个注册中心下
    nacos:
      #namespace: cab17056-9954-4e45-9223-eb33692f60f7
      serverAddr: 127.0.0.1:8848
      # 需要server端(registry和config)、nacos配置client端(registry和config)保持一致
      group: SEATA_GROUP
      username: "nacos"
      password: "nacos"
      namespace: 707f9b70-7c86-4278-bd74-83cef490a824
  registry:
    type: nacos
    nacos:
      # 需要和server端保持一致,即server在nacos中的名称,默认为seata-server
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: DEFAULT_GROUP
      #namespace: cab17056-9954-4e45-9223-eb33692f60f7
      username: "nacos"
      password: "nacos"
           

这里有一个深坑,启动应用后会报如下错误:

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!

seata can not get cluster name in registry config ‘service.vgroupMapping.XXX‘

解决办法就是需要再Nacos配置中心增加service.vgroupMapping.XXX配置,XXX是可以自定义的,value默认为default。

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!

注:核心业务代码可查看项目源码:https://gitee.com/it-codelab/springcloudalibaba-study/tree/main/seata-demo

AT模式原理

AT模式的核心优势是对业务无侵入,是一种改进后的两阶段提交。

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:提交异步化,非常快速地完成。回滚通过一阶段的回滚日志进行反向补偿。

其设计思路如图:

  • 第一阶段

业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。核心在于对业务sql进行解析,转换成undolog,并同时入库,这是怎么做的呢?先抛出一个概念DataSourceProxy代理数据源,通过名字大家大概也能基本猜到是什么个操作,后面做具体分析

参考官方文档: https://seata.io/zh-cn/docs/dev/mode/at-mode.html

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!
  • 第二阶段

第一种情况:分布式事务操作成功,则TC通知RM异步删除undolog。

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!

第二种情况:分布式事务操作失败,TM向TC发送回滚请求 ➔ TC收到回滚请求后,向RM发送回滚请求 ➔ RM收到协调器TC发来的回滚请求 ➔ 通过XID和Branch ID找到相应的回滚日志记录 ➔ 通过回滚记录生成反向的更新SQL并执行,以完成分支的回滚。

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!

完整执行流程:

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!

读写隔离机制

写隔离

  • 一阶段本地事务提交前,需要确保先拿到 全局锁 。
  • 拿不到 全局锁 ,不能提交本地事务。
  • 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

以一个示例来说明:

两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!

tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!

如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。

此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。

因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。

读隔离

在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。

如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

Seata,讓分布式事務不再是難題!實戰分享帶你領略Seata的魅力!

SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。

出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。

总结

  • alibaba seata框架总体使用下来是比较简单的,AT模式对业务代码零侵入的优势特点使得seata在业界得到了广泛的应用。
  • 框架使用起来越简单,那它内部的实现原理就越复杂、越抽象,本文也只是对seata的使用入了门,更原理性的内容后续持续深挖钻研。

繼續閱讀