CAP定律以及分布式事务解决seata

  •   产生背景 
     

          在微服务中,会将项目拆分成多个不同的服务,服务与服务之间通讯采用RPC远程调用技术,每个服务中都有自己的数据源库和本地事务,他们之间互不影响的。由此产生了分布式事务问题。
    目前分布式事务存在两大理论依据:CAP定律 BASE理论。 
     
  •   CAP定律 
     

          这个定理的内容是指的是在一个分布式系统中、Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。 
          CAP理论的主要场景是在分布式环境下,在单机环境下,基本可不考虑CAP问题。
  1.  一致性(C)
    所有节点上的数据须时刻保持一致。 比如:  在执行数据更新操作后,访问任何节点看到的数据都是最新的。这里是指强一致性
  2.  可用性(A)
    指每一个请求,都能在一定时间内返回响应结果,结果可以是成功或者失败,也不需要确保返回的是最新数据。 
    比如:  节点B在等待数据同步, 此时请求过来了, 可以直接将当前没同步的数据返回给客户端。
  3.  分区容错性(P)
    这里的分区是指网络意义上的分区。表示由于网络不可靠, 节点之间出现不能通讯的情况。 当发生部分节点不能通信时,要保证系统还可以继续提供服务。
     
    如果系统不能在有效的时限内达成数据一致性,也意味着发生了分区的情况,必须就当前操作在C和A之间做出选择;
  4.   总结
    CAP 原则指是指,这三个要素最多只能同时实现两点,不可能三者兼顾。要么是CP或者AP
    CP:当你网络出现故障之后,不能保证数据可用性,但是不能保证一致性; 如: zk
    AP:当你网络出现故障之后,不能保证数据一致性,但是能够保证可用性; 如: eureka
  •  BASE理论 
     

          BASE是Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结, 是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
  1.  基本可用 

    基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性----注意,这绝不等价于系统不可用。比如:

    (1)响应时间上的损失。正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒

    (2)系统功能上的损失:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面

     
  2.  软状态 
        软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时
     
  3.  最终一致性 
          最终一致性强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
    例子:  A数据库更新了数据,其他数据库可以暂时不更新,但是最终数据需要能够更新。
     
  •  分布式事务解决方案 
  1.  基于rabbitmq的形式解决
     采用最终一致性
  2.  基于rocketmq解决分布式事务
     基于rocketmq的事务消息
  3.  LCN
  4.  Alibaba的Seata
  •  两阶段提交协议(2PC:Two-Phase Commit) 
     

    该协议将一个分布式的事务过程拆分成两个阶段: 
     第一阶段:准备阶段    
     第二阶段:提交阶段 
  1.  准备阶段
    1) 协调者向所有的参与者询问是否可以执行操作,并等待参与者反馈结果;
    2) 参与者收到询问后,执行本地事务方法但不提交,并记录事务日志;
    3) 参与者将结果反馈给协调者,同时阻塞等待协调者的后续指令。
     
  2.  提交阶段
    经过准备阶段的讯问之后,参与者会回复协调者自己事务的执行情况,这时候存在以下可能性:  
     
    一) 所有参与者执行事务成功,此时:  
     2pc-success
          1)  协调者向各个参与者发送 commit 通知,请求提交事务;
          2)  参与者收到事务提交通知之后执行 commit 操作,然后释放占有的资源;
          3)  参与者向协调者返回事务 commit 结果信息。 
     
    二) 有一个或多个参与者执行失败、或因网络问题致协调者等待超时  ,此时: 
         2pc-failed
     
          1)  协调者向各个参与者发送事务 rollback 通知,请求回滚事务;
          2)  参与者收到事务回滚通知之后执行 rollback 操作,然后释放占有的资源;
          3)  参与者向协调者返回事务 rollback 结果信息。
     
  3.  两段协议的缺点
     1)  协调者在整个两阶段提交过程中扮演着举足轻重的作用,一旦协调者所在服务器宕机,参与者们将一直处于阻塞状态 
     2)  参与者都需要听从协调者的统一调度,期间处于阻塞状态而不能从事其他操作,这样效率极其低下
     3)  在提交阶段中,假设协调者发出了事务 commit 通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。 
     
  •  三阶段提交协议(3PC:Three-Phase Commit) 
     

          3PC,三阶段提交协议,是2PC的改进版本,即将事务的提交过程分为CanCommit、PreCommit、do Commit三个阶段来进行处理。
     
  1.  阶段1:CanCommit
     1)  协调者向所有参与者发出包含事务内容的CanCommit请求,询问是否可以提交事务,并等待所有参与者答复。
     2)  参与者收到CanCommit请求后,如果认为可以执行事务操作,则反馈YES并进入预备状态,否则反馈NO。
     
  2.  阶段2:PreCommit
     此阶段分两种情况: 
     1)   所有参与者均反馈YES,即执行事务预提交。 
           1.1) 协调者向所有参与者发出PreCommit请求,进入准备阶段。
      1.2)  参与者收到PreCommit请求后,执行事务操作,将Undo和Redo信息记入事务日志中(但不提交事务)。
      1.3)各参与者向协调者反馈Ack响应或No响应,并等待最终指令。
         
     2)   任何一个参与者反馈NO,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务。 
           2.1)  协调者向所有参与者发出abort请求。
      2.2)  无论收到协调者发出的abort请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。
  3.  阶段3:do Commit

    此阶段也存在两种情况:
      1)  所有参与者均反馈Ack响应,即执行真正的事务提交。
                 1.1)  如果协调者处于工作状态,则向所有参与者发出commit请求。
                 1.2)  参与者收到commit请求后,会正式执行事务提交,并释放整个事务期间占用的资源。        
                 1.3)  各参与者向协调者反馈Ack完成的消息。
                 1.4)  协调者收到所有参与者反馈的Ack消息后,即完成事务提交。

      2)  任何一个参与者反馈NO,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务。
                2.1)   如果协调者处于工作状态,向所有参与者发出rollback请求。
                2.2)   参与者使执行rollback操作,并释放整个事务期间占用的资源。
                2.3)   各参与者向协调者反馈Ack完成的消息。
                2.4)   协调者收到所有参与者反馈的Ack消息后,即完成事务中断。 
     注意:进入阶段三后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的do Commit请求或rollback请求。此时,参与者都会在等待超时之后,继续执行事务提交。
     

  4.   3PC的优点和缺陷 
    优点:降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段3中协调者出现问题时,参与者会继续提交事务。
     
    缺点:网络问题依然存在,即在参与者收到PreCommit请求后等待最终指令,如果此时协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
     
  5.   2PC与3PC资料文档
      
     转载推荐文档: https://blog.51cto.com/11821908/2058651
     转载推荐文档:  https://segmentfault.com/a/1190000012534071
  1.  Seata有3个基本组成部分:

    1.事务协调器TC:维护全局事务和分支事务的状态,驱动全局提交或回滚,事务的协调者。
    2.事务管理器TM:定义全局事务的范围:开始全局事务,提交或回滚全局事务,事务的发起方。
    3.资源管理器RM:管理分支事务正在处理的资源,与TC进行对话以注册分支事务并报告分支事务的状态,并驱动分支事务的提交或回滚,事务的参与方
     
     
  2.  Seata实现原理 
    1)  TM(发起方) 和 TC(协调者)申请全局事务,并创建全局事务xid
    2)  TM(发起方)执行本地业务的sql,并提交本地事务,然后将全局事务xid、业务的逆向sql等信息保存undo_log表中 。
    3)  RM(参与方)获取全局事务xid,执行业务sql,提交本地事务, 并将xid、逆向sql等保存undo_log表中 。
    4)  TM(发起方)将本地事务的结果反馈给协调者,此时会有下面三个情况:  
      
     返回成功    则:
           协调者通知所有事务参与者,根据全局事务的xid和本地分支事务的id, 删除对应的undo_log表的记录 
     返回失败    则: 
           协调者通知所有事务参与者,根据全局事务的xid和本地分支事务的id,找到uodo_log表生成的逆向sql来将本地事务提交的数据删除(或恢复),同时删除对应的undo_log日志
    等待超时    则:
             TM(发起方) 和 RM(参与方)都会根据本地undo_log表,同样删除(或恢复)本地事务提交的数据和对应的undo_log日志
      
     
     什么是逆向sql:如果发起方执行的是insert语句,对应的逆向语句就是删除这条记录的delete语句
                               如果发起方执行的是update语句,对应的逆向语句就是恢复之前记录的语句
      
     注意: 发起方/参与方都是在自己本地方法执行完成后就会提交本地事务,而不会等待协调者通知commit或rollback。
     如果发起方/参与方 提交本地事务后,协调者通知rollback,  发起方/参与方会根据本地库中undo_log的信息,删除或恢复已交提交的本地事务数据。
  •   Seata与Lcn的区别 
       

    Lcn:  参与方在执行方法时,不会提交本地事务,同时等待协调者的结果,容易造成数据的死锁。
    Seata:   参与方在执行方法时,不会等待协调者的结果,直接提交本地事务,通过undo_logd的逆向sql实现回滚,避免死锁现象但是容易出现脏读。
  •  Seata环境搭建
     
  1.  下载seata
    https://github.com/seata/seata/releases
  2.  修改注册中心地址
     修改conf/registry.conf,选择注册中心类型和注册中心地址 
     
      
  3.  配置seata的server端数据库
     1) 创建seata数据库,并执行conf/db_store.sql文件
     
    2) 修改conf/file.conf  
     
      
  4.  启动
     启动nacos
     启动seata:   seata-server.bat 
     
     登录nacos查看是否启动成功!!!默认端口8091
  •  客户端整合  
     
      

     
     库存项目
  1.   配置库存数据库
     将订单数据库执行seata\conf\db_undo_log.sql文件,创建undo_log表  
      
     
      undo_log表主要记录事务的回滚信息
     
  2.   创建库存项目
     pom.xml依赖如下: 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.11.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
      </parent>
    
      <properties>
        <boot-version>2.1.11.RELEASE</boot-version>
        <mybatis.starter.version>2.0.0</mybatis.starter.version>
        <mapper.starter.version>2.1.5</mapper.starter.version>
      </properties>
    
      <dependencies>
        <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <optional>true</optional>
        </dependency>
        <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--  springboot 整合web组件-->
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
    <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>1.3.1</version>
            </dependency>
            <!-- mysql 依赖 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-seata</artifactId>
                <version>2.1.1.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
    
    
      </dependencies>
    
      <dependencyManagement>
        <dependencies>
          <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
          </dependency>
          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${boot-version}</version>
            <type>pom</type>
            <scope>import</scope>
          </dependency>
          <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.1.1.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
          </dependency>
          <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.44</version>
          </dependency>
        </dependencies>
      </dependencyManagement>

     
    application.yml配置  
    spring:
      application:
        ###服务的名称
        name: pitch2-stock
      datasource:
        url: jdbc:mysql://localhost:3306/2020_stock?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: 123456
        driver-class-name: com.mysql.jdbc.Driver
      cloud:
        nacos:
          discovery:
            ###nacos注册地址
            server-addr: 127.0.0.1:8848
        alibaba:
          seata:
            ### 定义tx组,需要与seata/config/file.conf文件中的一致
            tx-service-group: my_test_tx_group
    server:
      port: 9001
  3. 配置seata 
    将seata/conf/file.conf文件配置到项目resources目录下 
    将seata/conf/registry.conf文件配置到项目resources目录下 
      
    注意: file.conf配置tx组信息,和seata的连接地址,如果不一样可以直接修改  
  4. 创建DataSourceProxyConfig.java
    生成代理数据源,拦截数据库操作,生成反向sql
    import com.alibaba.druid.pool.DruidDataSource;
    import io.seata.rm.datasource.DataSourceProxy;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import javax.sql.DataSource;
    
    @Configuration
    public class DataSourceProxyConfig {
    
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource")
        public DataSource dataSource() {
            return new DruidDataSource();
        }
    
        @Bean
        public DataSourceProxy dataSourceProxy(DataSource dataSource) {
            return new DataSourceProxy(dataSource);
        }
    
        @Bean
        public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dataSourceProxy);
            sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
            return sqlSessionFactoryBean.getObject();
        }
    }
  5.  扣库存代码 
        public String update(Integer id){
            stockMapper.update(id);
            return "更新商品Id:"+id+"  的库存为";
        }
        @Update("update tbl_stock set number = number-1 WHERE id=#{id}")
        int update(@Param("id") Integer id);
  6. 启动类
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
    import org.springframework.cloud.openfeign.EnableFeignClients;
    
    /**
     * Hello world!
     *
     */
    @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
    @EnableFeignClients
    @MapperScan("com.pitch.mapper")
    public class App2Stock {
        public static void main( String[] args ) {
            SpringApplication.run(App2Stock.class);
        }
    }
    
     
     

        订单项目

  1. 配置数据库 , 同上
  2. 创建订单项目,同上
  3. 配置seata, 同上
  4. 创建DataSourceProxyConfig.java, 同上
  5.  用户下单并通过feign调用库存服务减库存
    import com.pitch.entity.OrderEntity;
    import com.pitch.mapper.OrderMapper;
    import com.pitch.openfeign.OpenFeignStockService;
    import com.pitch.service.OrderService;
    import io.seata.spring.annotation.GlobalTransactional;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.web.bind.annotation.RestController;
    import java.util.List;
    
    /**
     * @author xiaobo
     * @Description OrderServiceImpl
     * @createTime 2020-04-04 19:27
     */
    @RestController
    public class OrderServiceImpl implements OrderService {
        @Value("${server.port}")
        private String port;
    
        @Autowired
        private OrderMapper orderMapper;
        @Autowired
        private OpenFeignStockService openFeignStockService;
    
        @Override
        public String order() {
            return "订单服务!!!"+port;
        }
    
        @Override
        @GlobalTransactional
        public String add(Integer stockId, String buyer) {
            OrderEntity orderEntity = new OrderEntity();
            orderEntity.setId(null);
            orderEntity.setStockId(stockId);
            orderEntity.setBuyer(buyer);
            // 用户下单
            orderMapper.addOrder(orderEntity);
            // 通过feign调用库存服务进行减库存操作
            openFeignStockService.update(stockId);
            int s = 10/0;
            return "用户:"+buyer+"  下单成功!!!"+orderEntity.getId();
        }
        @Override
        public List<OrderEntity> findAll() {
            return orderMapper.findAll();
        }
    }
     使用debug调试,当代码执行到 int s = 10/0时;  数据是已经持久化到数据库的。并将逆向sql信息保存到undo_log表。
     
     注意: 这里只需要在事务的发起方配置@GlobalTransactional注解即可,seata就会管理整个事务
  6.  启动项目
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
    import org.springframework.cloud.openfeign.EnableFeignClients;
    
    /**
     * Hello world!
     *
     */
    @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
    @EnableFeignClients
    @MapperScan("com.pitch.mapper")
    public class App2Order {
        public static void main( String[] args ) {
            SpringApplication.run(App2Order.class);
        }
    }
  7.  启动测试
     
    结果:当订单下单失败时,库存也会回滚事务。 
     
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值