谷粒商城-分布式事务

目录

商城业务-分布式事务-本地事务在分布式下的问题

商城业务-分布式事务-本地事务隔离级别&传播行为等复习

商城业务-分布式事务-分布式CAP&Raft原理

商城业务-分布式事务-BASE

商城业务-分布式事务-分布式事务常见解决方案

商城业务-分布式事务-Seata&环境准备

商城业务-分布式事务-Seata分布式事务体验

商城业务-分布式事务-最终一致性库存解锁逻辑


商城业务-分布式事务-本地事务在分布式下的问题

本地事务会失效不回滚的两种情况:

①锁库存假失败,由于网络原因导致连接超时,但是锁库存已经操作成功。此时,订单数据回滚而锁库存数据没有回滚。

②其它远程服务调用失败,订单数据回滚,但是已经执行成功的远程服务调用的数据库数据无法回滚

谷粒商城-分布式事务_第1张图片

商城业务-分布式事务-本地事务隔离级别&传播行为等复习

一、事务的特性

原子性:一系列操作整体不可拆分,要么全做,要么全不做

一致性:数据在事务的前后,业务整体一致

例如:转账  A:1000 B:1000  转200 事务成功; A:800 B:1200

隔离性:事务与事务之间互相隔离

持久性:一旦事务成功,数据一定会落盘在数据库

二、 事务的隔离级别

READ UNCOMMITED(读未提交):该隔离级别下的事务会读到别的事务未提交的数据,此现象被称之为脏读

READ COMMITED (读已提交):一个事务可以读取其它事务提交的数据,多次读取导致

前后读取的数据不一致,此现象称之为不可重复读。OracleSQL Server的默认隔离级别为读已提交

REPEATABLE READ (可重复度):在一个事务中读取数据前后不一致,产生的原因是有另外一个事务进行了insert操作,此现象称之为幻读。MySQL的默认隔离级别为可重复读。

SERIALIZABLE (序列化):在该隔离级别下事务都是串行顺序执行的,MySQL数据库的innoDB引擎会给读操作隐式加一把共享锁,从而避免了脏读、不可重复读和幻读问题。

三、事务的传播行为 

1.PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新的事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。

2.PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。

3.PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。

4.PROPAGATION_REQUIRES_NEW:创建新的事务,无论当前是否存在事务,都创建新的事务。

5.PROPAGATION_NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,就把当前事务挂起。

6.PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

7.PROPAGATION_NESTED:如果当前存在事务,则嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

其中:PROPAGATION_REQUIREDPROPAGATION_REQUIRES_NEW是最常用的

案例一: 

方法B()和方法A()共用一个事务,方法C则创建一个新事务,若出现异常则方法B()和方法A()会回滚,方法C()则不会

谷粒商城-分布式事务_第2张图片

案例二: 

方法B()设置了事务的超时时间,但是方法B()和方法A()共用方法A()的事务,因此,以方法A设置的超时时间为准。

谷粒商城-分布式事务_第3张图片

SpringBoot事务的坑 

事务失效的原因:绕过了代理

①未启用事务

@EnableTransactionManagement 注解用来启用spring事务自动管理事务的功能,这个注解千万不要忘记写了

② 方法不是public类型的

@Transaction 可以用在类上、接口上、public方法上,如果将@Trasaction用在了非public方法上,事务将无效

③数据源未配置事务管理器

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
} 

④自身调用问题 

spring是通过aop的方式,对需要spring管理事务的bean生成了代理对象,然后通过代理对象拦截了目标方法的执行,在方法前后添加了事务的功能,所以必须通过代理对象调用目标方法的时候,事务才会起效。

看下面代码,大家思考一个问题:当外部直接调用m1的时候,m2方法的事务会生效么?

@Component
public class UserService {
    public void m1(){
        this.m2();
    }
    
    @Transactional
    public void m2(){
        //执行db操作
    }
}

显然不会生效,因为m1中通过this的方式调用了m2方法,而this并不是代理对象,this.m2()不会被事务拦截器,所以事务是无效的,如果外部直接调用通过UserService这个bean来调用m2方法,事务是有效的,上面代码可以做一下调整,如下,@1在UserService中注入了自己,此时会产生更为严重的问题:循环依赖

@Component
public class UserService {
    @Autowired //@1
    private UserService userService;
 
    public void m1() {
        this.userService.m2();
    }
 
    @Transactional
    public void m2() {
        //执行db操作
    }
}

⑤ 异常类型错误

spring事务回滚的机制:对业务方法进行try catch,当捕获到有指定的异常时,spring自动对事务进行回滚,那么问题来了,哪些异常spring会回滚事务呢?

并不是任何异常情况下,spring都会回滚事务,默认情况下,RuntimeExceptionError的情况下,spring事务才会回滚。

也可以自定义回滚的异常类型(需继承RuntimeException):

@Transactional(rollbackFor = {异常类型列表})

 ⑥异常被吞了

当业务方法抛出异常,spring感知到异常的时候,才会做事务回滚的操作,若方法内部将异常给吞了,那么事务无法感知到异常了,事务就不会回滚了。

如下代码,事务操作2发生了异常,但是被捕获了,此时事务并不会被回滚

@Transactional
public void m1(){
    事务操作1
    try{
        事务操作2,内部抛出了异常
    }catch(Exception e){
        
    }
}

⑦业务和spring事务代码必须在一个线程中

spring事务实现中使用了ThreadLocal,ThreadLocal大家应该知道吧,可以实现同一个线程中数据共享,必须是同一个线程的时候,数据才可以共享,这就要求业务代码必须和spring事务的源码执行过程必须在一个线程中,才会受spring事务的控制,比如下面代码,方法内部的子线程内部执行的事务操作将不受m1方法上spring事务的控制,这个大家一定要注意

@Transactional
public void m1() {
    new Thread() {
        一系列事务操作
    }.start();
}

解决方案:

本地事务失效的原因:同一个对象内事务方法互相调用默认失效,原因绕过了代理对象,事务使用代理对象来控制

解决:使用代理对象来调用事务方法

方法B()和方法C()的事务属性设置会失效,原因是绕过了代理,SpringBoot的事务是通过AOP代理实现的

谷粒商城-分布式事务_第4张图片

解决事务失效的步骤: 

1.引入aspectj依赖

 

    org.springframework.boot
    spring-boot-starter-aop

谷粒商城-分布式事务_第5张图片

谷粒商城-分布式事务_第6张图片

2. 开启aspectj动态代理功能,以后所有的动态代理都是aspectj创建的。通过设置exposeProxy暴露代理对象

谷粒商城-分布式事务_第7张图片

3. 本类互调用对象

  @Transactional(timeout = 30)
    public void A(){

//        B();
//        C();
        OrderServiceImpl service =(OrderServiceImpl)AopContext.currentProxy();
        service.B();
        service.C();
        int i = 10/0;
    }

    @Transactional(propagation = Propagation.REQUIRED,timeout = 20)
    public void B(){

    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void C(){

    }

谷粒商城-分布式事务_第8张图片

商城业务-分布式事务-分布式CAP&Raft原理

分布式系统经常出现异常的原因:

机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失...

CAP定理: 

一致性(Consitency):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)

可用性(Availability):在集群中一部分节点故障后,集群整体是否还能响应客户的读写请求。(对数据更新具备高可用性)

分区容错性(Partition tolerance):大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另外一台服务器放在美国,这就叫两个区,它们之间可能无法通信。

CAP原则指的是,这三个要素最多只能同时实现两个点,不可能三者兼得。

一般来说,分区容错不可避免,因此,可以认为CAP中的P总是成立。CAP定理告诉我们,剩下的C和A无法同时做到。

分布式系统中实现一致性的算法raft算法 

演示传送门:http://thesecretlivesofdata.com/raft/

Raft算法的原理说明: 

首先,在Raft中一个节点有三种角色:①追随者(Follower)候选人(Candidate)领导者(Leader)

一开始,所有节点都是追随者状态,如果没有领导者给他们发信息,他们可以变成候选人,候选人将会给追随者发起选举,追随者们将会投票给候选人,如果候选人得到了大多数票则它将会成为领导者。这个过程被成为:领导选举

追随者是如何成为候选人的呢?首先,节点有一个自旋超时时间(150ms-300ms),谁自旋结束的快谁就是候选者,候选人发起选举,如果节点在此轮选举中还没有投票,那么节点将会投票给它,一旦候选人收到大多数投票那么它将成为领导者。成为领导者之后则开始心跳联络,定期向节点发出我还在的消息,节点回复收到,这种状态直到领导者挂掉为止。

所有改变将需要听从领导者,假设客户端发来一条 SET 5 命令,首先,领导者会将这条命令保存到log中,然后会将 SET 5 命令发送给它的追随者,追随者们也是将命令保存至log中,领导者接收到大多数节点的回复--已经将这条命令写入log中了,此时,所有节点日志中的这条命令都是uncommited的。然后,领导会将这条命令commit并通知它的追随者让它们也去提交。这个过程被成为:日志复制

日志复制过程在分区中的体现:由于网络原因,A、B被划分为1区,C、D、E被划分为2区,1区和2区之间不能通信,A原来是领导者所以在1区它还是领导者,2区经过多轮选举选出了新的领导者,现在有Client1给1区发 SET 10 的命令,A保存命令至日志然后通知B也保存日志,但是通知没有得到大多数节点的回复因此是uncommited的状态,Client2给2区发 SET 100 命令,2区领导者保存命令至日志,同时通知其它节点页保存命令至日志并且收到大多数节点的回复,2区领导者将会commit并会通知其它节点也去commit的。最终,1区和2区的通信回复了,由于2区的领导者是经过多轮选举选出的所以它成为了所以节点的领导者,原来1区的领导者就变成了追随者,1区A、B节点发现跟领导者的日志不一致,马上回滚日志并更新新的日志和提交,至此所有节点的数据是一致的。

CP面临的问题: 

对于多数大型互联网应用场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到99.9999999(N个9),即保证P和A,舍弃C。舍弃C的含义是:保证数据的最终一致性而不是去追求强一致性。

商城业务-分布式事务-BASE 

谷粒商城-分布式事务_第9张图片

谷粒商城-分布式事务_第10张图片

商城业务-分布式事务-分布式事务常见解决方案

一、2PC模式

2PC(2 phrase commit 二阶段提交),又叫做:XA Transactions

MySQL从5.5版本开始支持,SQL Server 2005开始支持,Oracle 7 开始支持。

其中,XA 是一个二阶段提交协议,该协议分为以下两个阶段:

第一阶段:事务协调器要求每个涉及事务的数据库预提交(precommit)此操作,并反映是否可以提交。

第二阶段:事务协调器要求每个数据库提交数据。

其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息。

2PC模式在高并发场景下的不太理想,分布式场景下并不会选择这种模式 

谷粒商城-分布式事务_第11张图片

二、柔性事务-TCC事务补偿型方案 

刚性事务:遵循ACID原则,强一致性。

柔性事务:遵循BASE理论,最终一致性。

与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致性。

Try代码模块中需要Coder自己编写业务逻辑,Confirm代码块中会提交数据(例如:加2),那么在Cancel中则需要Coder编写回滚逻辑(例如:减2) 

一阶段 Prepare 行为:调用自定义的 prepare 逻辑 

二阶段 commit 行为:调用自定义的commit逻辑

二阶段 rollback 行为:调用自定义的rollback逻辑

所谓TCC模式,是指支持把自定义的分支事务纳入到全局事务的管理中。

谷粒商城-分布式事务_第12张图片

三、 柔性事务-最大努力通知型事务

按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种方案主要用在与第三方系统通信时,比如:调用微信或者支付宝支付后的支付结果。这种方案也是结合MQ进行实现,例如:通过MQ发送Http请求,设置最大通知次数。达到通知次数后即不再通知。

案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询核对、对账文件),支付宝的支付成功异步回调。

四、柔性事务-可靠消息+最终一致性方案(异步确保型)

实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。        

商城业务-分布式事务-Seata&环境准备

Seata使用的是2PC的模式

Seata快速开始传送门:Seata 快速开始

首先,让我们了解一下Seata中的专业术语:

谷粒商城-分布式事务_第13张图片

Seata的工作模式: 首先,TM会告诉TC全局事务开始了,由各个事务分支向TC汇报事务的状态,是成功还是回滚。如果有一个事务分支汇报回滚,则之前提交的事务都会回滚,回滚的依赖于Seata中的Magic表,用于记录提交之前的版本和数据。

谷粒商城-分布式事务_第14张图片

商城业务-分布式事务-Seata分布式事务体验

开启Seata分布式事务的步骤:

1.为每一个微服务创建undo_log

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

谷粒商城-分布式事务_第15张图片

谷粒商城-分布式事务_第16张图片

 2.导入Seata依赖

  
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-seata
            
                  
                
                    seata-all
                    io.seata
                
            
        
        
        
            io.seata
            seata-spring-boot-starter
            1.2.0
        

 3.安装seata server V1.2.0

传送门:https://github.com/seata/seata/releases

谷粒商城-分布式事务_第17张图片

4. 配置

将官方文档中V1.2.0的配置文件复制到conf,因为conf文件配置不全

传送门:seata/file.conf at 1.2.0 · seata/seata · GitHub

谷粒商城-分布式事务_第18张图片

谷粒商城-分布式事务_第19张图片

 file.conf

transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the client batch send request enable
  enableClientBatchSendRequest = true
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThread-prefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #transaction service group mapping
  vgroupMapping.my_test_tx_group = "default"
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

client {
  rm {
    asyncCommitBufferLimit = 10000
    lock {
      retryInterval = 10
      retryTimes = 30
      retryPolicyBranchRollbackOnConflict = true
    }
    reportRetryCount = 5
    tableMetaCheckEnable = false
    reportSuccessEnable = false
    sagaBranchRegisterEnable = false
  }
  tm {
    commitRetryCount = 5
    rollbackRetryCount = 5
  }
  undo {
    dataValidation = true
    logSerialization = "jackson"
    logTable = "undo_log"
  }
  log {
    exceptionRate = 100
  }
}

 registry.conf

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    cluster = "default"
    username = "naocs"
    password = "naocs"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"

  nacos {
    serverAddr = "localhost"
    namespace = ""
    group = "SEATA_GROUP"
    username = ""
    password = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    appId = "seata-server"
    apolloMeta = "http://192.168.1.204:8801"
    namespace = "application"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

将registry.conf中的type修改为nacos并修改serverAddr为本机注册中心地址

谷粒商城-分布式事务_第20张图片

配置:

命名规则:服务名-fescar-service-group

,修改file.conf中的配置:file.conf和yml中的配置要一致

谷粒商城-分布式事务_第21张图片

spring:
  cloud:
    alibaba:
      seata:
        tx-service-group: gulimall-order-fescar-service-group

谷粒商城-分布式事务_第22张图片

spring:
  cloud:
    alibaba:
      seata:
        tx-service-group: gulimall-ware-fescar-service-group

其它服务同理 

5.所有想要用到分布式事务的微服务使用seata DataSourceProxy代理自己的数据源

注意细节:高版本之后无需配置数据源,这步可忽略

import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;


@Configuration
public class MySeataConfig {
    @Autowired
    private DataSourceProperties dataSourceProperties;

    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties){
        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())) {
            dataSource.setPoolName(dataSourceProperties.getName());
        }
        return new DataSourceProxy(dataSource);
    }
}

谷粒商城-分布式事务_第23张图片

 谷粒商城-分布式事务_第24张图片

6.给分布式事务的大入口标注@GlobalTransactional

谷粒商城-分布式事务_第25张图片

7. 每一个远程的小事务@Transactional

回滚效果如下图所示:

谷粒商城-分布式事务_第26张图片

商城业务-分布式事务-最终一致性库存解锁逻辑

seata 的AT模式并不适合于高并发场景,原因在于:加锁导致整个线程变成串行化执行,效率太低下了

seata的TCC模式、SAGA模式可以自行学习:

传送门:https://github.com/seata/seata-samples

谷粒商城-分布式事务_第27张图片谷粒商城采用的是消息队列解锁库存,保证最终一致性而非seata 的AT的模式,只是带大家体验一下。

你可能感兴趣的:(尚硅谷谷粒商城,谷粒商城高级篇)