• 谷粒商城-分布式事务


    目录

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

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

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

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

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

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

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

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


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

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

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

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

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

    一、事务的特性

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

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

    例如:转账  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()则不会

    案例二: 

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

    SpringBoot事务的坑 

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

    ①未启用事务

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

    ② 方法不是public类型的

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

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

    1. @Bean
    2. public PlatformTransactionManager transactionManager(DataSource dataSource) {
    3.     return new DataSourceTransactionManager(dataSource);

    ④自身调用问题 

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

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

    1. @Component
    2. public class UserService {
    3. public void m1(){
    4. this.m2();
    5. }
    6. @Transactional
    7. public void m2(){
    8. //执行db操作
    9. }
    10. }

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

    1. @Component
    2. public class UserService {
    3. @Autowired //@1
    4. private UserService userService;
    5. public void m1() {
    6. this.userService.m2();
    7. }
    8. @Transactional
    9. public void m2() {
    10. //执行db操作
    11. }
    12. }

    ⑤ 异常类型错误

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

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

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

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

     ⑥异常被吞了

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

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

    1. @Transactional
    2. public void m1(){
    3. 事务操作1
    4. try{
    5. 事务操作2,内部抛出了异常
    6. }catch(Exception e){
    7. }
    8. }

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

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

    1. @Transactional
    2. public void m1() {
    3. new Thread() {
    4. 一系列事务操作
    5. }.start();
    6. }

    解决方案:

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

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

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

    解决事务失效的步骤: 

    1.引入aspectj依赖

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

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

    3. 本类互调用对象

    1. @Transactional(timeout = 30)
    2. public void A(){
    3. // B();
    4. // C();
    5. OrderServiceImpl service =(OrderServiceImpl)AopContext.currentProxy();
    6. service.B();
    7. service.C();
    8. int i = 10/0;
    9. }
    10. @Transactional(propagation = Propagation.REQUIRED,timeout = 20)
    11. public void B(){
    12. }
    13. @Transactional(propagation = Propagation.REQUIRES_NEW)
    14. public void C(){
    15. }

    商城业务-分布式事务-分布式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 

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

    一、2PC模式

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Seata使用的是2PC的模式

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

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

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

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

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

    1.为每一个微服务创建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. `ext` varchar(100) DEFAULT NULL,
    11. PRIMARY KEY (`id`),
    12. UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    13. ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

     2.导入Seata依赖

    1. com.alibaba.cloud
    2. spring-cloud-starter-alibaba-seata
    3. seata-all
    4. io.seata
    5. io.seata
    6. seata-spring-boot-starter
    7. 1.2.0

     3.安装seata server V1.2.0

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

    4. 配置

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

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

     file.conf

    1. transport {
    2. # tcp udt unix-domain-socket
    3. type = "TCP"
    4. #NIO NATIVE
    5. server = "NIO"
    6. #enable heartbeat
    7. heartbeat = true
    8. # the client batch send request enable
    9. enableClientBatchSendRequest = true
    10. #thread factory for netty
    11. threadFactory {
    12. bossThreadPrefix = "NettyBoss"
    13. workerThreadPrefix = "NettyServerNIOWorker"
    14. serverExecutorThread-prefix = "NettyServerBizHandler"
    15. shareBossWorker = false
    16. clientSelectorThreadPrefix = "NettyClientSelector"
    17. clientSelectorThreadSize = 1
    18. clientWorkerThreadPrefix = "NettyClientWorkerThread"
    19. # netty boss thread size,will not be used for UDT
    20. bossThreadSize = 1
    21. #auto default pin or 8
    22. workerThreadSize = "default"
    23. }
    24. shutdown {
    25. # when destroy server, wait seconds
    26. wait = 3
    27. }
    28. serialization = "seata"
    29. compressor = "none"
    30. }
    31. service {
    32. #transaction service group mapping
    33. vgroupMapping.my_test_tx_group = "default"
    34. #only support when registry.type=file, please don't set multiple addresses
    35. default.grouplist = "127.0.0.1:8091"
    36. #degrade, current not support
    37. enableDegrade = false
    38. #disable seata
    39. disableGlobalTransaction = false
    40. }
    41. client {
    42. rm {
    43. asyncCommitBufferLimit = 10000
    44. lock {
    45. retryInterval = 10
    46. retryTimes = 30
    47. retryPolicyBranchRollbackOnConflict = true
    48. }
    49. reportRetryCount = 5
    50. tableMetaCheckEnable = false
    51. reportSuccessEnable = false
    52. sagaBranchRegisterEnable = false
    53. }
    54. tm {
    55. commitRetryCount = 5
    56. rollbackRetryCount = 5
    57. }
    58. undo {
    59. dataValidation = true
    60. logSerialization = "jackson"
    61. logTable = "undo_log"
    62. }
    63. log {
    64. exceptionRate = 100
    65. }
    66. }

     registry.conf

    1. registry {
    2. # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
    3. type = "nacos"
    4. nacos {
    5. application = "seata-server"
    6. serverAddr = "127.0.0.1:8848"
    7. namespace = ""
    8. cluster = "default"
    9. username = "naocs"
    10. password = "naocs"
    11. }
    12. eureka {
    13. serviceUrl = "http://localhost:8761/eureka"
    14. application = "default"
    15. weight = "1"
    16. }
    17. redis {
    18. serverAddr = "localhost:6379"
    19. db = 0
    20. password = ""
    21. cluster = "default"
    22. timeout = 0
    23. }
    24. zk {
    25. cluster = "default"
    26. serverAddr = "127.0.0.1:2181"
    27. sessionTimeout = 6000
    28. connectTimeout = 2000
    29. username = ""
    30. password = ""
    31. }
    32. consul {
    33. cluster = "default"
    34. serverAddr = "127.0.0.1:8500"
    35. }
    36. etcd3 {
    37. cluster = "default"
    38. serverAddr = "http://localhost:2379"
    39. }
    40. sofa {
    41. serverAddr = "127.0.0.1:9603"
    42. application = "default"
    43. region = "DEFAULT_ZONE"
    44. datacenter = "DefaultDataCenter"
    45. cluster = "default"
    46. group = "SEATA_GROUP"
    47. addressWaitTime = "3000"
    48. }
    49. file {
    50. name = "file.conf"
    51. }
    52. }
    53. config {
    54. # file、nacos 、apollo、zk、consul、etcd3
    55. type = "file"
    56. nacos {
    57. serverAddr = "localhost"
    58. namespace = ""
    59. group = "SEATA_GROUP"
    60. username = ""
    61. password = ""
    62. }
    63. consul {
    64. serverAddr = "127.0.0.1:8500"
    65. }
    66. apollo {
    67. appId = "seata-server"
    68. apolloMeta = "http://192.168.1.204:8801"
    69. namespace = "application"
    70. }
    71. zk {
    72. serverAddr = "127.0.0.1:2181"
    73. sessionTimeout = 6000
    74. connectTimeout = 2000
    75. username = ""
    76. password = ""
    77. }
    78. etcd3 {
    79. serverAddr = "http://localhost:2379"
    80. }
    81. file {
    82. name = "file.conf"
    83. }
    84. }

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

    配置:

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

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

    1. spring:
    2. cloud:
    3. alibaba:
    4. seata:
    5. tx-service-group: gulimall-order-fescar-service-group

    1. spring:
    2. cloud:
    3. alibaba:
    4. seata:
    5. tx-service-group: gulimall-ware-fescar-service-group

    其它服务同理 

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

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

    1. import com.zaxxer.hikari.HikariDataSource;
    2. import io.seata.rm.datasource.DataSourceProxy;
    3. import org.springframework.beans.factory.annotation.Autowired;
    4. import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
    5. import org.springframework.context.annotation.Bean;
    6. import org.springframework.context.annotation.Configuration;
    7. import org.springframework.util.StringUtils;
    8. import javax.sql.DataSource;
    9. @Configuration
    10. public class MySeataConfig {
    11. @Autowired
    12. private DataSourceProperties dataSourceProperties;
    13. @Bean
    14. public DataSource dataSource(DataSourceProperties dataSourceProperties){
    15. HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    16. if (StringUtils.hasText(dataSourceProperties.getName())) {
    17. dataSource.setPoolName(dataSourceProperties.getName());
    18. }
    19. return new DataSourceProxy(dataSource);
    20. }
    21. }

     

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

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

    回滚效果如下图所示:

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

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

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

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

    谷粒商城采用的是消息队列解锁库存,保证最终一致性而非seata 的AT的模式,只是带大家体验一下。

  • 相关阅读:
    软件安装教程1——Neo4j下载与安装
    STP内容扩充 PVST MSTP 流量负载分担
    YAYA LIVE CTO 唐鸿斌:真正本地化,要让产品没有「产地」属性
    MyBatis快速入门
    jS屏蔽默认右键菜单并创建自定义右键菜单
    JSON和全局异常处理
    嵌入式 Linux LED 驱动开发实验学习
    idea常用配置 | 快捷注释
    OpenCV——总结《车牌识别》
    python爬虫快速学-4张思维图高清下载
  • 原文地址:https://blog.csdn.net/weixin_56674682/article/details/126511452