• Seata 解决分布式事务理论与实践




    在这里插入图片描述

    1.分布式事务问题

    1.1.本地事务

    本地事务,也就是传统的单机事务。在传统数据库事务中,必须要满足四个原则:A C I D

    1.2.分布式事务

    分布式事务,就是指不是在单个服务或单个数据库架构下,产生的事务,例如:

    - 跨数据源的分布式事务
    - 跨服务的分布式事务
    - 综合情况
    
    • 1
    • 2
    • 3

    在数据库水平拆分、服务垂直拆分之后,一个业务操作通常要跨多个数据库、服务才能完成。例如电商行业中比较常见的下单付款案例,包括下面几个行为:

    - 创建新订单
    - 扣减商品库存
    - 从用户账户余额扣除金额
    
    • 1
    • 2
    • 3

    完成上面的操作需要访问三个不同的微服务和三个不同的数据库。

    在这里插入图片描述

    订单的创建、库存的扣减、账户扣款在每一个服务和数据库内是一个本地事务,可以保证ACID原则。

    但是当我们把三件事情看做一个"业务",要满足保证“业务”的原子性,要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是分布式系统下的事务了。

    此时ACID难以满足,这是分布式事务要解决的问题

    1.3.演示分布式事务问题

    我们通过一个案例来演示分布式事务的问题:

    微服务结构如下:

    在这里插入图片描述

    其中:

    seata-demo:父工程,负责管理项目依赖

    • account-service:账户服务,负责管理用户的资金账户。提供扣减余额的接口
    • storage-service:库存服务,负责管理商品库存。提供扣减库存的接口
    • order-service:订单服务,负责管理订单。创建订单时,需要调用 account-service和storage-service

    3)启动nacos、所有微服务

    4)测试下单功能,发出Post请求:

    请求如下:

    http://localhost:8082/order?userId=user202103032042012&commodityCode=100202003032041&count=20&money=200
    
    • 1

    如图:

    在这里插入图片描述

    测试发现,当库存不足时,如果余额已经扣减,并不会回滚,出现了分布式事务问题。

    2.理论基础

    解决分布式事务问题,需要一些分布式系统的基础知识作为理论指导。

    2.1.CAP定理

    Consistency(一致性): 用户访问分布式系统中的任意节点,得到的数据必须一致
    Availability(可用性): 用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。
    Partition(分区): 因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。
    tolerance(容错): 在集群出现分区时,整个系统也要持续对外提供服务
    ================结论:
    CP : 强一致性,弱可用性(牺牲部分机器的可用性,保证数据一致性)
    AP : 强可用性,弱一致性(牺牲一致性,保证可用性)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    CAP详情跳转


    2.2.BASE理论

    BASE 理论是对 CAP 的一种解决思路,包含三个思想:

    • Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
    • Soft State(软状态) 在一定时间内,允许出现中间状态,比如临时的不一致状态。
    • Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。

    2.3.解决分布式事务的思路

    分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论,有两种解决思路:

    • AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。

    • CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。

    但不管是哪一种模式,都需要在子系统事务之间互相通讯,协调事务状态,也就是需要一个事务协调者(TC)

    在这里插入图片描述

    这里的子系统事务,称为分支事务;有关联的各个分支事务在一起称为全局事务


    3.初识Seata

    Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。

    官网中的文档、播客中提供了大量的使用说明、源码分析。

    在这里插入图片描述

    3.1.Seata的架构

    Seata事务管理中有三个重要的角色:

    • TC (Transaction Coordinator) - 事务协调者维护全局和分支事务的状态,协调全局事务提交或回滚。

    • TM (Transaction Manager) - 事务管理器定义全局事务的范围、开始全局事务、提交或回滚全局事务。

    • RM (Resource Manager) - 资源管理器管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

    整体的架构如图:

    在这里插入图片描述

    Seata基于上述架构提供了四种不同的分布式事务解决方案:

    • XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
    • TCC模式:最终一致的分阶段事务模式,有业务侵入
    • AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
    • SAGA模式:长事务模式,有业务侵入

    无论哪种方案,都离不开TC,也就是事务的协调者。

    3.2.部署TC服务

    部署Seata 详情

    3.3.微服务集成Seata

    我们以order-service为例来演示。

    3.3.1.引入依赖

    首先,在order-service中引入依赖:

    
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-seataartifactId>
        <exclusions>
             
            <exclusion>
                <artifactId>seata-spring-boot-starterartifactId>
                <groupId>io.seatagroupId>
            exclusion>
        exclusions>
    dependency>
    <dependency>
        <groupId>io.seatagroupId>
        <artifactId>seata-spring-boot-starterartifactId>
        
        <version>${seata.version}version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    3.3.2.配置TC地址

    在order-service中的application.yml中,配置TC服务信息,通过注册中心nacos,结合服务名称获取TC地址:

    seata:
      registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
        type: nacos # 注册中心类型 nacos
        nacos:
          server-addr: 127.0.0.1:8848 # nacos地址
          namespace: "" # namespace,默认为空
          group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
          application: seata-tc-server # seata服务名称
          username: nacos
          password: nacos
      tx-service-group: seata-demo # 事务组名称
      service:
        vgroup-mapping: # 事务组与cluster的映射关系
          seata-demo: SH
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    微服务如何根据这些配置寻找TC的地址呢?

    我们知道注册到Nacos中的微服务,确定一个具体实例需要四个信息:

    • namespace:命名空间
    • group:分组
    • application:服务名
    • cluster:集群名

    以上四个信息,在刚才的yaml文件中都能找到:

    在这里插入图片描述

    namespace为空,就是默认的public

    结合起来,TC服务的信息就是:public@DEFAULT_GROUP@seata-tc-server@SH,这样就能确定TC服务集群了。然后就可以去Nacos拉取对应的实例信息了。

    3.3.3.其它服务

    其它两个微服务也都参考order-service的步骤来做,完全一样。

    4.动手实践

    下面我们就一起学习下Seata中的四种不同的事务模式。

    4.1.XA模式

    XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。

    4.1.1.两阶段提交

    XA是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交。

    正常情况:

    在这里插入图片描述

    异常情况:

    在这里插入图片描述

    一阶段:

    • 事务协调者通知每个事物参与者执行本地事务
    • 本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁

    二阶段:

    • 事务协调者基于一阶段的报告来判断下一步操作
      • 如果一阶段都成功,则通知所有事务参与者,提交事务
      • 如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务

    4.1.2.Seata的XA模型

    Seata对原始的XA模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图:

    在这里插入图片描述

    RM一阶段的工作:

    ​ ① 注册分支事务到TC

    ​ ② 执行分支业务sql但不提交

    ​ ③ 报告执行状态到TC

    TC二阶段的工作:

    • TC检测各分支事务执行状态

      a.如果都成功,通知所有RM提交事务

      b.如果有失败,通知所有RM回滚事务

    RM二阶段的工作:

    • 接收TC指令,提交或回滚事务

    4.1.3.优缺点

    XA模式的优点是什么?

    • 事务的强一致性,满足ACID原则。
    • 常用数据库都支持,实现简单,并且没有代码侵入

    XA模式的缺点是什么?

    • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
    • 依赖关系型数据库实现事务

    4.1.4.实现XA模式

    Seata 的 starter 已经完成了 XA 模式的自动装配,实现非常简单,步骤如下:

    1)修改application.yml文件(每个参与事务的微服务),开启XA模式:

    seata:
      data-source-proxy-mode: XA
    
    • 1
    • 2

    在这里插入图片描述

    2)给发起全局事务的入口方法添加@GlobalTransactional注解:

    本例中是OrderServiceImpl中的create方法.

    @Override
    @GlobalTranaction
    public Long create(Order order){
        // TODO
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3)重启服务并测试

    重启order-service,再次测试,发现无论怎样,三个微服务都能成功回滚。

    4.2.AT模式

    AT模式同样是分阶段提交的事务模型,不过却弥补了XA模型中资源锁定周期过长的缺陷。(效率比XA高 但是一致性比XA若)

    4.2.1.Seata的AT模型

    基本流程图:

    在这里插入图片描述

    阶段一RM的工作:

    • 注册分支事务
    • 记录undo-log(数据快照)
    • 执行业务sql并提交
    • 报告事务状态

    阶段二提交时RM的工作:

    • 删除undo-log即可

    阶段二回滚时RM的工作:

    • 根据undo-log恢复数据到更新前

    4.2.2.流程梳理

    我们用一个真实的业务来梳理下AT模式的原理。

    比如,现在又一个数据库表,记录用户余额:

    idmoney
    1100

    其中一个分支业务要执行的SQL为:

    update tb_account set money = money - 10 where id = 1
    
    • 1

    AT模式下,当前分支事务执行流程如下:

    一阶段:

    1)TM发起并注册全局事务到TC

    2)TM调用分支事务

    3)分支事务准备执行业务SQL

    4)RM拦截业务SQL,根据where条件查询原始数据,形成快照。

    {
        "id": 1, "money": 100
    }
    
    • 1
    • 2
    • 3

    5)RM执行业务SQL,提交本地事务,释放数据库锁。此时 money = 90

    6)RM报告本地事务状态给TC

    二阶段:

    1)TM通知TC事务结束

    2)TC检查分支事务状态

    ​ a)如果都成功,则立即删除快照

    ​ b)如果有分支事务失败,需要回滚。读取快照数据({"id": 1, "money": 100}),将快照恢复到数据库。此时数据库再次恢复为100

    流程图:

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    4.2.3.AT与XA的区别

    简述AT模式与XA模式最大的区别是什么?

    • XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
    • XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
    • XA模式强一致;AT模式最终一致

    4.2.4.脏写问题

    在多线程并发访问AT模式的分布式事务时,有可能出现脏写问题,如图:

    在这里插入图片描述


    解决思路 就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。


    在这里插入图片描述

    4.2.5.优缺点

    AT模式的优点:

    • 一阶段完成直接提交事务,释放数据库资源,性能比较好
    • 利用全局锁实现读写隔离
    • 没有代码侵入,框架自动完成回滚和提交

    AT模式的缺点:

    • 两阶段之间属于软状态,属于最终一致
    • 框架的快照功能会影响性能,但比XA模式要好很多

    4.2.6.实现AT模式

    AT模式中的快照生成、回滚等动作都是由框架自动完成,没有任何代码侵入,因此实现非常简单。

    只不过,AT模式需要一个表来记录全局锁、另一张表来记录数据快照undo_log。

    1)导入数据库表,记录全局锁

    导入如下的Sql语句,其中lock_table导入到TC服务关联的数据库,undo_log表导入到微服务关联的数据库:

    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    -- ----------------------------
    -- Table structure for undo_log
    -- ----------------------------
    DROP TABLE IF EXISTS `undo_log`;
    CREATE TABLE `undo_log`  (
      `branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
      `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
      `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'undo_log context,such as serialization',
      `rollback_info` longblob NOT NULL COMMENT 'rollback info',
      `log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
      `log_created` datetime(6) NOT NULL COMMENT 'create datetime',
      `log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
      UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;
    
    -- ----------------------------
    -- Records of undo_log
    -- ----------------------------
    
    -- ----------------------------
    -- Table structure for lock_table
    -- ----------------------------
    DROP TABLE IF EXISTS `lock_table`;
    CREATE TABLE `lock_table`  (
      `row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
      `xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `transaction_id` bigint(20) NULL DEFAULT NULL,
      `branch_id` bigint(20) NOT NULL,
      `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `gmt_create` datetime NULL DEFAULT NULL,
      `gmt_modified` datetime NULL DEFAULT NULL,
      PRIMARY KEY (`row_key`) USING BTREE,
      INDEX `idx_branch_id`(`branch_id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
    
    
    SET FOREIGN_KEY_CHECKS = 1;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    2)修改application.yml文件,将事务模式修改为AT模式即可:

    seata:
      data-source-proxy-mode: AT # 默认就是AT
    
    • 1
    • 2

    3)重启服务并测试

    5.高可用

    Seata 的 TC 服务作为分布式事务核心,一定要保证集群的高可用性。

    5.1.高可用架构模型

    搭建TC服务集群非常简单,启动多个TC服务,注册到nacos即可。

    但集群并不能确保100%安全,万一集群所在机房故障怎么办?所以如果要求较高,一般都会做异地多机房容灾。

    比如一个TC集群在上海,另一个TC集群在杭州:

    在这里插入图片描述

    微服务基于事务组(tx-service-group)与TC集群的映射关系,来查找当前应该使用哪个TC集群。当SH集群故障时,只需要将vgroup-mapping中的映射关系改成HZ。则所有微服务就会切换到HZ的TC集群了。

    5.2.实现高可用

    总结

    分布式事务:
    	在分布式架构中一个业务需要跨对个微服务,需要这多个微服务执行的状态保持一致.
    分布式事务理论基础:
    	CAP定理:
    		C: 一致性
              在微服务架构中,从任何节点读取到的数据应当是一致的.
            A: 可用性
              在服务不宕机的情况下,必须返回预期的结果
            P: 分区容错
              在微服务架构中是避免不了的.
    	BASE理论:
    		- Basically Available 基本可用
    		- Soft State 软状态
    		- Eventually Consistent 最终一致性
      方案:
    	AP: 可用性
          软状态: 允许不同的分支事务短时间内状态不一致
          但软状态过后需要保证最终一致.
          各个分支执行自己的操作,执行完毕后,直接提交事务即可.
          如果有分支出现失败的请求,事务协调者会通过各个分支,进行补救
    	CP: 强一致性
          在所有的分支事务执行完毕后,等待协调事务发送最终提交或回滚事务的通知
    ===========================Seata======================
    seata-TC: 事务协调者
      连接mysql数据库: 将事务的各种状态信息保存到mysql数据库表中
      记录全局事务
      记录全局事务包含的分支事务
    
    seata-TM: 事务管理器
      开启全局事务
      提交/回滚全局事务
    seata-RM: 资源管理器
      分支事务管理
    ----------------------------------
    Seata-分布式事务模式:
      XA模式: CP思想(强一致性)
      AT模式: AP思想(最终一致)
        软状态: 每个分支事务都可以独立提交
        TM--->TC: 开启全局事务
        TM--->分支
          获取全局事务锁
          undo_log快照:
    		before-image: 在分支事务对数据库中的数据操作前进行前置快照
            after-image: 在分支事务对数据库中的数据操作后进行后置快照
          如果各个分支都执行成功了---> 删除快照
          如果执行失败--->恢复快照(执行事务的补偿过程)
          释放全局事务锁
      	注意事项: 主要使用在关系型数据库中
      TCC模式: AP思想
        try:
    			判断cancel是否执行,如果未执行,正常执行即可
    				校验,冻结
          如果执行过:
    				不再执行任何逻辑,删除冻结数据
    		confirm: 执行成功
    			删除冻结数据
    		cancel: 执行失败
    			执行补偿过程
          判断try是否执行了,如果执行了
          		恢复冻结数据
          		删除冻结数据
         	如果try没有执行:
    					空回滚: 添加一条冻结数据,并设置状态
      SAGA模式: AP思想
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    0.回顾

    在分布式(微服务)系统中,往往一个业务需要调用多个微服务,且还需要保证当前业务的完整性.
    一个业务(事务):
    	组成这个业务的各个单元,要么都成功,要么都失败.
    指导思想:
    	CAP定理:
    	BASE理论:
    Alibaba:CAP定理和BASE理论的基础上进行实践后的产物.
    	Seata: 
    Seata实现方式:
    	Seata角色:
    		TC: 事务协调者
                安装Seata(服务)软件
            TM: 事务管理器
            RM: 资源管理器(分支事务管理器)
            注意: 当我们在微服务项目中导入Seata的依赖后,就相当于拥有了TMRM两个角色
        Seata模式:
    		XA模式: CP思想,强一致性
            AT模式: AP思想,最终一致
            TCC模式: AP思想,最终一致
            SAGA模式: Ap思想,最终一致
        Seata模式原理
        Seata模式实现方式
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    管理器
    开启全局事务
    提交/回滚全局事务
    seata-RM: 资源管理器
    分支事务管理

    Seata-分布式事务模式:
    XA模式: CP思想(强一致性)
    AT模式: AP思想(最终一致)
    软状态: 每个分支事务都可以独立提交
    TM—>TC: 开启全局事务
    TM—>分支
    获取全局事务锁
    undo_log快照:
    before-image: 在分支事务对数据库中的数据操作前进行前置快照
    after-image: 在分支事务对数据库中的数据操作后进行后置快照
    如果各个分支都执行成功了—> 删除快照
    如果执行失败—>恢复快照(执行事务的补偿过程)
    释放全局事务锁
    注意事项: 主要使用在关系型数据库中
    TCC模式: AP思想
    try:
    判断cancel是否执行,如果未执行,正常执行即可
    校验,冻结
    如果执行过:
    不再执行任何逻辑,删除冻结数据
    confirm: 执行成功
    删除冻结数据
    cancel: 执行失败
    执行补偿过程
    判断try是否执行了,如果执行了
    恢复冻结数据
    删除冻结数据
    如果try没有执行:
    空回滚: 添加一条冻结数据,并设置状态
    SAGA模式: AP思想

    
    ## 0.当前存在的问题?
    
    ```java
    在分布式(微服务)系统中,往往一个业务需要调用多个微服务,且还需要保证当前业务的完整性.
    一个业务(事务):
    	组成这个业务的各个单元,要么都成功,要么都失败.
    指导思想:
    	CAP定理:
    	BASE理论:
    Alibaba:在CAP定理和BASE理论的基础上进行实践后的产物.
    	Seata: 
    Seata实现方式:
    	Seata角色:
    		TC: 事务协调者
                安装Seata(服务)软件
            TM: 事务管理器
            RM: 资源管理器(分支事务管理器)
            注意: 当我们在微服务项目中导入Seata的依赖后,就相当于拥有了TM和RM两个角色
        Seata模式:
    		XA模式: CP思想,强一致性
            AT模式: AP思想,最终一致
            TCC模式: AP思想,最终一致
            SAGA模式: Ap思想,最终一致
        Seata模式原理
        Seata模式实现方式
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26



    在这里插入图片描述



  • 相关阅读:
    Activiti7 代码创建流程定义及生成bpmn及svg文件
    ajax之Content-Type示例
    K8S中的pod自动扩容与缩容
    11.摆花
    公寓报修系统(IDEA,SSM,MySQL)
    成功案例 | 安超云助力兰州大学第二医院搭建新型IT基础设施平台 提升医疗信息资源利用率
    04 Opencv图像操作
    05_css选择器的使用
    Android MeasureSpec测量规格
    Linux内核基础 - list_splice_tail_init函数详解
  • 原文地址:https://blog.csdn.net/m0_60915009/article/details/132624509