ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Springcloud基础知识(15)- Spring Cloud Alibaba Seata (一) | Seata 简介、事务模式、Seata Server

2022-07-22 09:03:29  阅读:151  来源: 互联网

标签:事务 15 Seata Springcloud Nacos Server id seata



随着业务的不断发展,单体架构已经无法满足我们的需求,分布式微服务架构逐渐成为大型互联网平台的首选,但所有使用分布式微服务架构的应用都必须面临一个十分棘手的问题,那就是 “分布式事务” 问题。

在分布式微服务架构中,几乎所有业务操作都需要多个服务协作才能完成。对于其中的某个服务而言,它的数据一致性可以交由其自身数据库事务来保证,但从整个分布式微服务架构来看,其全局数据的一致性却是无法保证的。

例如,用户在某电商系统下单购买了一件商品后,电商系统会执行下 4 步:

    (1) 调用订单服务创建订单数据
    (2) 调用库存服务扣减库存
    (3) 调用账户服务扣减账户金额
    (4) 最后调用订单服务修改订单状态

为了保证数据的正确性和一致性,我们必须保证所有这些操作要么全部成功,要么全部失败,否则就可能出现类似于商品库存已扣减,但用户账户资金尚未扣减的情况。各服务自身的事务特性显然是无法实现这一目标的,此时,我们可以通过分布式事务框架来解决这个问题。

Seata 就是这样一个分布式事务处理框架,它是由阿里巴巴和蚂蚁金服共同开源的分布式事务解决方案,能够在微服务架构下提供高性能且简单易用的分布式事务服务。


1. Seata 简介

    阿里巴巴作为国内最早一批进行应用分布式(微服务化)改造的企业,很早就遇到微服务架构下的分布式事务问题。

    阿里巴巴对于分布式事务问题先后发布了以下解决方案:

        (1) 2014 年,阿里中间件团队发布 TXC(Taobao Transaction Constructor),为集团内应用提供分布式事务服务。
        (2) 2016 年,TXC 在经过产品化改造后,以 GTS(Global Transaction Service) 的身份登陆阿里云,成为当时业界唯一一款云上分布式事务产品。在阿云里的公有云、专有云解决方案中,开始服务于众多外部客户。
        (3) 2019 年起,基于 TXC 和 GTS 的技术积累,阿里中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback, FESCAR),和社区一起建设这个分布式事务解决方案。
        (4) 2019 年 fescar 被重命名为了seata(simple extensiable autonomous transaction architecture)。
        (5) TXC、GTS、Fescar 以及 seata 一脉相承,为解决微服务架构下的分布式事务问题交出了一份与众不同的答卷。

    分布式事务主要涉及以下概念:

        (1) 事务:由一组操作构成的可靠、独立的工作单元,事务具备 ACID 的特性,即原子性、一致性、隔离性和持久性。
        (2) 本地事务:本地事务由本地资源管理器(通常指数据库管理系统 DBMS,例如 MySQL、Oracle 等)管理,严格地支持 ACID 特性,高效可靠。本地事务不具备分布式事务的处理能力,隔离的最小单位受限于资源管理器,即本地事务只能对自己数据库的操作进行控制,对于其他数据库的操作则无能为力。
        (3) 全局事务:全局事务指的是一次性操作多个资源管理器完成的事务,由一组分支事务组成。
        (4) 分支事务:在分布式事务中,就是一个个受全局事务管辖和协调的本地事务。

        我们可以将分布式事务理解成一个包含了若干个分支事务的全局事务。全局事务的职责是协调其管辖的各个分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个满足 ACID 特性的本地事务。

    Seata 对分布式事务的协调和控制,主要是通过 XID 和 3 个核心组件实现的。

        (1) XID:是全局事务的唯一标识,它可以在服务的调用链路中传递,绑定到服务的事务上下文中。
        (2) TC(Transaction Coordinator)组件:事务协调器,它是事务的协调者(这里指的是 Seata 服务器),主要负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。
        (3) TM(Transaction Manager)组件:事务管理器,它是事务的发起者,负责定义全局事务的范围,并根据 TC 维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议。
        (4) RM(Resource Manager)组件:资源管理器,它是资源的管理者(这里可以将其理解为各服务使用的数据库)。它负责管理分支事务上的资源,向 TC 注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚。

    Seata 的整体工作流程如下:

        (1) TM 向 TC 申请开启一个全局事务,全局事务创建成功后,TC 会针对这个全局事务生成一个全局唯一的 XID;
        (2) XID 通过服务的调用链传递到其他服务;
        (3) RM 向 TC 注册一个分支事务,并将其纳入 XID 对应全局事务的管辖;
        (4) TM 根据 TC 收集的各个分支事务的执行结果,向 TC 发起全局事务提交或回滚决议;
        (5) TC 调度 XID 下管辖的所有分支事务完成提交或回滚操作。

    Seata: https://seata.io/zh-cn/index.html
    Seata GibHub: https://github.com/seata/seata


2. Seata 事务模式

    Seata 提供了 AT、TCC、SAGA 和 XA 四种事务模式,可以快速有效地对分布式事务进行控制。

    在这四种事务模式中使用最多,最方便的就是 AT 模式。与其他事务模式相比,AT 模式可以应对大多数的业务场景,且基本可以做到无业务入侵,开发人员能够有更多的精力关注于业务逻辑开发。

    1) AT 模式的前提

        任何应用想要使用 Seata 的 AT 模式对分布式事务进行控制,必须满足以下 2 个前提:

            (1) 必须使用支持本地 ACID 事务特性的关系型数据库,例如 MySQL、Oracle 等;
            (2) 应用程序必须是使用 JDBC 对数据库进行访问的 Java 应用。

        此外,我们还需要针对业务中涉及的各个数据库表,分别创建一个 UNDO_LOG(回滚日志)表。不同数据库在创建 UNDO_LOG 表时会略有不同,以 MySQL 为例,其 UNDO_LOG 表的创表语句如下:

 1             CREATE TABLE `undo_log` (
 2                 `id` bigint(20) NOT NULL AUTO_INCREMENT,
 3                 `branch_id` bigint(20) NOT NULL,
 4                 `xid` varchar(100) NOT NULL,
 5                 `context` varchar(128) NOT NULL,
 6                 `rollback_info` longblob NOT NULL,
 7                 `log_status` int(11) NOT NULL,
 8                 `log_created` datetime NOT NULL,
 9                 `log_modified` datetime NOT NULL,
10                 PRIMARY KEY (`id`),
11                 UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
12             ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;


    2) AT 模式的工作机制

        Seata 的 AT 模式工作时大致可以分为以两个阶段,下面我们就结合一个实例来对 AT 模式的工作机制进行介绍。

        假设数据库存在一个 user 表,表结构如下。

列名 类型 主键
id bigint(20) Y
name varchar(255)   
url varchar(255)   


        在某次分支事务中,需要在 user 表中执行以下操作。

            UPDATE user SET url = 'www.test2.com' WHERE name = 'test';

        AT 模式阶段一:

            (1) 获取 SQL 的基本信息:Seata 拦截并解析业务 SQL,得到 SQL 的操作类型(UPDATE)、表名(user)、判断条件(WHERE name = 'test')等相关信息。

            (2) 查询前镜像:根据得到的业务 SQL 信息,生成 “前镜像查询语句”。

                    SELECT id,name,url FROM user WHERE name='test';

                执行 “前镜像查询语句”,得到即将执行操作的数据,并将其保存为 “前镜像数据(beforeImage)”。

                    id    name    url
                    1    test    www.test.com

            (3) 执行业务 SQL(UPDATE user SET url = 'www.test2.com' WHERE name = 'test';),将这条记录的 url 修改为 www.test2.com。

            (4) 查询后镜像:根据 “前镜像数据”的主键(id : 1),生成 “后镜像查询语句”。

                    SELECT id,name,url FROM user WHERE id= 1;

                执行 “后镜像查询语句”,得到执行业务操作后的数据,并将其保存为 “后镜像数据(afterImage)”。

                    id    name    url
                    1    test    www.test2.com

            (5) 插入回滚日志:将前后镜像数据和业务 SQL 的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中,示例回滚日志如下。

 1                 {
 2                 "@class": "io.seata.rm.datasource.undo.BranchUndoLog",
 3                 "xid": "172.26.54.1:8091:5962967415319516023",
 4                 "branchId": 5962967415319516027,
 5                 "sqlUndoLogs": [
 6                     "java.util.ArrayList",
 7                     [
 8                     {
 9                         "@class": "io.seata.rm.datasource.undo.SQLUndoLog",
10                         "sqlType": "UPDATE",
11                         "tableName": "user",
12                         "beforeImage": {
13                         "@class": "io.seata.rm.datasource.sql.struct.TableRecords",
14                         "tableName": "user",
15                         "rows": [
16                             "java.util.ArrayList",
17                             [
18                             {
19                                 "@class": "io.seata.rm.datasource.sql.struct.Row",
20                                 "fields": [
21                                 "java.util.ArrayList",
22                                 [
23                                     {
24                                     "@class": "io.seata.rm.datasource.sql.struct.Field",
25                                     "name": "id",
26                                     "keyType": "PRIMARY_KEY",
27                                     "type": -5,
28                                     "value": [
29                                         "java.lang.Long",
30                                         1
31                                     ]
32                                     },
33                                     {
34                                     "@class": "io.seata.rm.datasource.sql.struct.Field",
35                                     "name": "url",
36                                     "keyType": "NULL",
37                                     "type": 12,
38                                     "value": "www.test.com"
39                                     }
40                                 ]
41                                 ]
42                             }
43                             ]
44                         ]
45                         },
46                         "afterImage": {
47                         "@class": "io.seata.rm.datasource.sql.struct.TableRecords",
48                         "tableName": "user",
49                         "rows": [
50                             "java.util.ArrayList",
51                             [
52                             {
53                                 "@class": "io.seata.rm.datasource.sql.struct.Row",
54                                 "fields": [
55                                 "java.util.ArrayList",
56                                 [
57                                     {
58                                     "@class": "io.seata.rm.datasource.sql.struct.Field",
59                                     "name": "id",
60                                     "keyType": "PRIMARY_KEY",
61                                     "type": -5,
62                                     "value": [
63                                         "java.lang.Long",
64                                         1
65                                     ]
66                                     },
67                                     {
68                                     "@class": "io.seata.rm.datasource.sql.struct.Field",
69                                     "name": "url",
70                                     "keyType": "NULL",
71                                     "type": 12,
72                                     "value": "www.test2.com"
73                                     }
74                                 ]
75                                 ]
76                             }
77                             ]
78                         ]
79                         }
80                     }
81                     ]
82                 ]
83                 }


            (6) 注册分支事务,生成行锁:在这次业务操作的本地事务提交前,RM 会向 TC 注册分支事务,并针对主键 id 为 1 的记录生成行锁。

            (7) 本地事务提交:将业务数据的更新和前面生成的 UNDO_LOG 一并提交。

            (8) 上报执行结果:将本地事务提交的结果上报给 TC。

        AT 模式阶段二 (提交):

            当所有的 RM 都将自己分支事务的提交结果上报给 TC 后,TM 根据 TC 收集的各个分支事务的执行结果,来决定向 TC 发起全局事务的提交或回滚。

            若所有分支事务都执行成功,TM 向 TC 发起全局事务的提交,并批量删除各个 RM 保存的 UNDO_LOG 记录和行锁;否则全局事务回滚。

        AT 模式阶段二 (回滚):

            若全局事务中的任何一个分支事务失败,则 TM 向 TC 发起全局事务的回滚,并开启一个本地事务,执行如下操作。

            (1) 查找 UNDO_LOG 记录:通过 XID 和分支事务 ID(Branch ID) 查找所有的 UNDO_LOG 记录。

            (2) 数据校验:将 UNDO_LOG 中的后镜像数据(afterImage)与当前数据进行比较,如果有不同,则说明数据被当前全局事务之外的动作所修改,需要人工对这些数据进行处理。

            (3) 生成回滚语句:根据 UNDO_LOG 中的前镜像(beforeImage)和业务 SQL 的相关信息生成回滚语句:

                UPDATE user SET url = 'www.test.com' WHERE id = 1;

            (4) 还原数据:执行回滚语句,并将前镜像数据、后镜像数据以及行锁删除。

            (5) 提交事务:提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。


3. Seata Server

    本文使用 “Springcloud基础知识(11)- Spring Cloud Alibaba Nacos (一) | Nacos 简介、服务注册中心” 里的 Nacos 2.1.0 作为 Seata 的配置中心和注册中心。以 Seata Server 1.4.2 为例,演示 Windows 下安装和配置 Seata Server,步骤如下。


    1) 下载

        浏览器访问 Seata Server 下载页面(https://github.com/seata/seata/releases/tag/v1.4.2),并在页面最下方点击链接 seata-server-1.4.2.zip。

        下载完成后,解压 seata-server-1.4.2.zip,目录结构如下:

            seata
              |- bin
              |- conf
              |- lib
              |- logs

            各目录说明如下:

                bin:用于存放 Seata Server 可执行命令。
                conf:用于存放 Seata Server 的配置文件。
                lib:用于存放 Seata Server 依赖的各种 Jar 包。
                logs:用于存放 Seata Server 的日志。   


    2) 整合 Nacos 配置中心

        什么是配置中心? 配置中心可以说是一个 "大货仓",内部放置着各种配置文件,你可以通过自己所需进行获取配置加载到对应的客户端。比如 Seata Client 端 (TM, RM)、Seata Server (TC) 会去读取全局事务开关,事务会话存储模式等信息。

        Seata 支持多种配置中心:

            (1) nacos
            (2) consul
            (3) apollo
            (4) etcd
            (5) zookeeper
            (6) file (读本地文件,包含 conf、properties、yml 等配置文件)

        在 Seata Server 安装目录下的 config/registry.conf 中,将配置方式(config.type)修改为 Nacos,并对 Nacos 配置中心的相关信息进行配置,配置如下。

 1             config {
 2                 type = "nacos"
 3 
 4                 nacos {
 5                     serverAddr = "127.0.0.1:8848"
 6                     namespace = ""
 7                     group = "SEATA_GROUP"
 8                     username = "nacos"
 9                     password = "nacos"
10                     dataId = "seataServer.properties"
11                 }
12             }

 

    3) 整合 Nacos 注册中心

        什么是注册中心? 注册中心可以说是微服务架构中的 ”通讯录“,它记录了服务和服务地址的映射关系。在分布式架构中,服务会注册到这里,当服务需要调用其它服务时,就到这里找到服务的地址,进行调用。比如 Seata Client 端 (TM, RM),发现 Seata Server (TC) 集群的地址,彼此通信。

        Seata 支持多种注册中心:

            (1) eureka
            (2) consul
            (3) nacos
            (4) etcd
            (5) zookeeper
            (6) sofa
            (7) redis
            (8) file (直连)

        在 Seata Server 安装目录下的 config/registry.conf 中,将注册方式(registry.type)修改为 Nacos,并对 Nacos 注册中心的相关信息进行配置,配置如下。

 1             registry {
 2                 type = "nacos"
 3 
 4                 nacos {
 5                     application = "seata-server"
 6                     serverAddr = "127.0.0.1:8848"
 7                     group = "SEATA_GROUP"
 8                     namespace = ""
 9                     cluster = "default"
10                     username = "nacos"
11                     password = "nacos"
12                 }
13             }

 

    4) Seata 事务分组

        事务分组是 Seata 提供的一种 TC(Seata Server) 服务查找机制。

        Seata 通过事务分组获取 TC 服务,流程如下:

            (1) 在应用中配置事务分组。
            (2) 应用通过配置中心去查找配置:service.vgroupMapping.{事务分组},该配置的值就是 TC 集群的名称。
            (3) 获得集群名称后,应用通过一定的前后缀 + 集群名称去构造服务名。
            (4) 得到服务名后,去注册中心去拉取服务列表,获得后端真实的 TC 服务列表。

        这里以上文 Nacos 注册中心为例,介绍 Seata 事务的使用。在 Seata Server 的 config/registry.conf 中,修改 registry 配置如下。

 1             registry {
 2                 type = "nacos"
 3 
 4                 nacos {
 5                     application = "seata-server"
 6                     serverAddr = "127.0.0.1:8848"
 7                     group = "SEATA_GROUP"
 8                     namespace = ""
 9                     cluster = "www.test.com"
10                     username = "nacos"
11                     password = "nacos"
12                 }
13             }


            注:把 cluster 从 "default" 修改成了 "www.test.com"。


    5) 在 Nacos 配置中心创建配置

        (1) 通过页面创建

            从 Seata Server 1.4.2 版本开始,已支持从一个 Nacos dataId 中获取所有配置信息,只需在 Nacos 配置中心添加一个配置项。

            浏览器访问 http://localhost:8848/nacos/, 输入登录名和密码(默认 nacos/nacos),点击提交按钮,跳转到 Nacos Server 控制台页面。

            在 Nacos Server 控制台的 “配置管理” 下的 “配置列表” 中,点击 “+” 按钮,新建如下配置。

                Data ID: seataServer.properties
                Group:   SEATA_GROUP
                配置格式: Properties
                配置内容:

                注:配置内容参考 https://github.com/seata/seata/tree/develop/script/config-center/config.txt,根据项目需要添加到 “配置内容” 栏。

        (2) 通过脚本上传

            可以下载 https://github.com/seata/seata/tree/develop/script/config-center 目录的文件到本地,该目录包含了 nacos 脚本和 config.txt。

            本文直接 https://github.com/seata/seata/archive/refs/tags/v1.4.2.zip,解压后把目录中的整个 script 目录复制到本地 Seata Server 的安装目录。

            Nacos 脚本 Shell 命令:

                $ cd ${SEATAPATH}/script/config-center/nacos/
                $ sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t '' -u username -w password

                参数说明:

                    -h: Nacos 主机,默认 localhost。
                    -p: Nacos 端口,默认 8848。
                    -g: Nacos 配置组,默认 'SEATA_GROUP'。
                    -t: 租户信息,等同于 Nacos 命名空间 ID,默认 '' (public 空间)。
                    -u: Nacos 用户名,默认 ''。
                    -w: Nacos 密码,默认 ''。

            以上命令会自动读取 config-center/config.txt 文件里的配置,每一行配置在 nacos 配置里产生一个 Nacos dataId。

            注:这个方式适合在 Seata Server 版本 < 1.4.2 的版本中使用。


    6) 配置 db 存储模式

        Seata Server 共有以下 3 种存储模式(store.mode):

模式 描述
file 文件存储模式,默认存储模式;该模式为单机模式,全局事务的会话信息在内存中读写,并持久化本地文件 root.data,性能较高。
db 数据库存储模式;该模式为高可用模式,全局事务会话信息通过数据库共享,性能较低。    建数据库表
redis 缓存模式;Seata Server 1.3 及以上版本支持该模式,性能较高,但存在事务信息丢失风险,    配置 redis 持久化配置


        在 db 模式下,需要针对全局事务的会话信息创建以下 3 张数据库表。

            (1) 全局事务表,对应的表为:global_table
            (2) 分支事务表,对应的表为:branch_table
            (3) 全局锁表,对应的表为:lock_table

        在 MariaDB (MySQL) 中,创建一个名为 seata 的数据库实例,并在该数据库内执行以下 SQL。

 1             CREATE TABLE IF NOT EXISTS `global_table` (
 2                 `xid`                       VARCHAR(128) NOT NULL,
 3                 `transaction_id`            BIGINT,
 4                 `status`                    TINYINT NOT NULL,
 5                 `application_id`            VARCHAR(32),
 6                 `transaction_service_group` VARCHAR(32),
 7                 `transaction_name`          VARCHAR(128),
 8                 `timeout`                   INT,
 9                 `begin_time`                BIGINT,
10                 `application_data`          VARCHAR(2000),
11                 `gmt_create`                DATETIME,
12                 `gmt_modified`              DATETIME,
13                 PRIMARY KEY (`xid`),
14                 KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
15                 KEY `idx_transaction_id` (`transaction_id`)
16             ) ENGINE = InnoDB DEFAULT CHARSET = utf8;
17 
18             CREATE TABLE IF NOT EXISTS `branch_table` (
19                 `branch_id`         BIGINT NOT NULL,
20                 `xid`               VARCHAR(128) NOT NULL,
21                 `transaction_id`    BIGINT,
22                 `resource_group_id` VARCHAR(32),
23                 `resource_id`       VARCHAR(256),
24                 `branch_type`       VARCHAR(8),
25                 `status`            TINYINT,
26                 `client_id`         VARCHAR(64),
27                 `application_data`  VARCHAR(2000),
28                 `gmt_create`        DATETIME(6),
29                 `gmt_modified`      DATETIME(6),
30                 PRIMARY KEY (`branch_id`),
31                 KEY `idx_xid` (`xid`)
32             ) ENGINE = InnoDB DEFAULT CHARSET = utf8;
33 
34             CREATE TABLE IF NOT EXISTS `lock_table` (
35                 `row_key`        VARCHAR(128) NOT NULL,
36                 `xid`            VARCHAR(96),
37                 `transaction_id` BIGINT,
38                 `branch_id`      BIGINT NOT NULL,
39                 `resource_id`    VARCHAR(256),
40                 `table_name`     VARCHAR(32),
41                 `pk`             VARCHAR(36),
42                 `gmt_create`     DATETIME,
43                 `gmt_modified`   DATETIME,
44                 PRIMARY KEY (`row_key`),
45                 KEY `idx_branch_id` (`branch_id`)
46             ) ENGINE = InnoDB DEFAULT CHARSET = utf8;


        在 Nacos Server 上创建或修改 Data ID 为 seataServer.properties 的配置,配置内容如下。

 1             Data ID: seataServer.properties
 2             Group:   SEATA_GROUP
 3             配置格式: Properties
 4             配置内容:
 5 
 6             # 将 Seata Server 的存储模式修改为 db
 7             store.mode=db
 8             # 数据库驱动
 9             store.db.driverClassName=org.mariadb.jdbc.Driver
10             # 数据库 url
11             store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useSSL=false&characterEncoding=UTF-8&useUnicode=true&serverTimezone=UTC
12             # 数据库的用户名
13             store.db.user=nacos
14             # 数据库的密码
15             store.db.password=nacos
16             # 自定义事务分组
17             service.vgroupMapping.service-order-group=default
18             service.vgroupMapping.service-storage-group=default
19             service.vgroupMapping.service-account-group=default           

 

    7) 启动 Seata Server

        双击 bin 目录下的启动脚本 seata-server.bat ,启动 Seata Server。

 1             ...
 2 
 3             SLF4J: A number (18) of logging calls during the initialization phase have been intercepted and are
 4             SLF4J: now being replayed. These are subject to the filtering rules of the underlying logging system.
 5             SLF4J: See also http://www.slf4j.org/codes.html#replay
 6             22:36:41.922  INFO --- [                     main] io.seata.config.FileConfiguration        : The file name of the operation is registry
 7             22:36:41.926  INFO --- [                     main] io.seata.config.FileConfiguration        : The configuration file used is C:\Applications\Java\alibaba-cloud\seata-server-1.4.2\conf\registry.conf
 8             22:36:41.985  INFO --- [                     main] io.seata.config.FileConfiguration        : The file name of the operation is file.conf
 9             22:36:41.985  INFO --- [                     main] io.seata.config.FileConfiguration        : The configuration file used is C:\Applications\Java\alibaba-cloud\seata-server-1.4.2\conf\file.conf
10             22:36:42.608  INFO --- [                     main] i.s.core.rpc.netty.NettyServerBootstrap  : Server started, listen port: 8091

 



标签:事务,15,Seata,Springcloud,Nacos,Server,id,seata
来源: https://www.cnblogs.com/tkuang/p/16504462.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有