云岚到家 秒杀抢购-分布式事务

什么是分布式事务

当前遇到的问题

下单时核销优惠券,创建订单和核销优惠券需要保证事务一致性,要么两者都成功,要么两者都失败,现在的代码如下:

@Transactional(rollbackFor = Exception.class)
public void addWithCoupon(Orders orders, Long couponId) {
    CouponUseReqDTO couponUseReqDTO = new CouponUseReqDTO();
    couponUseReqDTO.setOrdersId(orders.getId());
    couponUseReqDTO.setId(couponId);
    couponUseReqDTO.setTotalAmount(orders.getTotalAmount());
    //优惠券核销
    CouponUseResDTO couponUseResDTO = couponApi.use(couponUseReqDTO);
    // 设置优惠金额
    orders.setDiscountAmount(couponUseResDTO.getDiscountAmount());
    // 计算实付金额
    BigDecimal realPayAmount = orders.getTotalAmount().subtract(orders.getDiscountAmount());
    orders.setRealPayAmount(realPayAmount);
    //保存订单
    add(orders);
}

当调用couponApi.use(couponUseReqDTO)成功表示优惠券核销成功,当此方法向下继续执行如果抛出异常那么订单信息进行回滚,优惠券核销信息是否会回滚?

创建订单在订单管理服务,优惠券核销在优惠券服务,业务数据处在两个数据库中,创建订单和优惠券核销互为分布式事务,对上边addWithCoupon方法进行事务控制只是控制了订单数据库的事务,而没有控制优惠券数据库的事务。

 

什么是本地事务?

要理解什么是分布式事务首先理解什么是本地事务。

平常我们在程序中通过spring去控制事务是利用数据库本身的事务特性来实现的,因此叫数据库事务,由于应用主要靠关系数据库来控制事务,此数据库只属于该应用,所以基于本应用自己的关系型数据库的事务又被称为本地事务。

本地事务具有ACID四大特性,数据库事务在实现时会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元,该执行单元中的所有操作 要么都成功,要么都失败,只要其中任一操作执行失败,都将导致整个事务的回滚。

回顾一下数据库事务的四大特性 ACID:

  • A(Atomic):原子性,构成事务的所有操作,要么都执行完成,要么全部不执行,不可能出现部分成功部分失败的情况。
  • C(Consistency):一致性,在事务执行前后,数据库的一致性约束没有被破坏。比如:张三向李四转100元,转账前和转账后的数据是正确状态这叫一致性,如果出现张三转出100元,李四账户没有增加100元这就出现了数据错误,就没有达到一致性。
  • I(Isolation):隔离性,数据库中的事务一般都是并发的,隔离性是指并发的两个事务的执行互不干扰,一个事务不能看到其他事务运行过程的中间状态。通过配置事务隔离级别可以避脏读、重复读等问题。
  • D(Durability):持久性,事务完成之后,该事务对数据的更改会被持久化到数据库,且不会被回滚。

什么是分布式事务

下边我们以下单扣减库存为例来说明什么是分布式事务。

在单体架构下实现下单减库存,如下图:

如果是在微服务架构下,如下图:

用户请求订单服务下单,订单服务请求库存服务扣减库存。

 

设想: 当远程调用扣减库存成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚了创建订单的操作,此时订单没有创建成功而库存却扣减了,最终就导致了下单扣减库存整个事务的数据不一致。

因此在分布式架构下,基于数据库的事务控制无法满足要求,下单操作是一次本地事务,扣减库存是一次本地事务,两次本地事务组成一个完整的事务即下单扣减库存,数据库的本地事务只能控制一次本地事务即下单操作控制下单的本地事务,扣减库存操作控制扣减库存的本地事务,无法保证下单和扣减库存整体事务的原子性和一致性。

像这种,在分布式系统环境下由多个服务通过网络通信协作去完成一次事务,这称之为分布式事务。

造成分布式事务无法控制的根本原因是不同业务的数据通常不在一个数据库中或者不在一个系统中,一次事务需要由多个服务或多个系统远程调用协作去完成,远程协作依赖网络,由于网络问题会导致整体事务不能正常完成。

非典型分布式事务场景

微服务架构下的分布式事务场景是典型的分布式事务场景,还有非典型的分布式事务场景也需要了解下。

1)单服务请求多数据库完成一次事务

下图中虽然没有跨服务远程调用但一次事务请求两个不同的数据库也属于分布式事务的场景,创建订单会和订单数据库创建数据库连接通过一次本地事务提交数据,减库存会和商品数据库创建数据库连接通过一次本地事务提交数据,这里仍然是多次本地事务共同完成一个完整的事务即下单扣减库存。

3)多服务请求单数据库完成一次事务

下图中虽然用的一个数据库但是通过跨服务远程调用去完成一次事务,也属于分布式事务的场景。

思考下这种场景为什么也属于分布式事务?

因为分布式事务:在分布式系统环境下由多个服务通过网络通信协作去完成一次事务

 

分布式事务的场景有哪些?

多个微服务之间通过远程调用完成一次分布式事务。

单服务请求多数据库完成一次事务。

多服务请求单数据库完成一次事务。

什么是CAP

遇到了分布式事务的场景我们该如何去进行事务控制呢,本节学习如何选型分布式事务的控制方案。

CAP是 Consistency、Availability、Partition tolerance三个词语的缩写,分别表示一致性、可用性、分区容忍性。

  • 一致性: 向系统写一个新数据再次读取到的也一定是这个新数据。拿上图举例,请求订单服务下单,订单服务请求库存服务扣减库存,只要下单成功则库存扣减成功。
  • 可用性:任何时间都可以访问订单服务和库存服务,系统保证可用。
  • 分区容忍性:也叫分区容错性,分布式系统在部署时服务器可能部署在不同的网络分区,比如上图中订单服务在北京,库存服务在上海,由于处于不同的网络分区如果网络通信异常就会导致节点 之间无法通信,当出现由于网络问题导致节点 之间无法通信,此时仍然是对外提供服务的这叫做满足分区容忍性。

CAP理论要强调在分布式系统中C、A、P这三点不能全部满足。

由于是分布式系统就要满足分区容忍性,因为分布式系统难免存在网络分区,不能因为网络异常导致整个系统不可用,所以P是一定要满足的。

满足P,那么C和A不能同时满足。

拿上图举例说明:

当订单服务与库存服务出现网络通信异常,订单服务无法访问库存服务,此时如果要保证数据一致性则下单接口必须不可用,如果要保证可用性数据将出现不一致。

 

分布式事务控制如何应用CAP原理

学习了CAP理论我们知道进行分布式事务控制要在C和A中作出取舍,进行分布式事务控制要么保证CP要么保证AP

具体要根据应用场景进行判断,下边举例说明CP和AP业务场景的例子。

符合CP的场景:满足C舍弃A,强调一致性。

金融系统:一般需要在多个账户之间进行交易或资金转移的操作通常需要满足CP,这是因为在这种场景下,数据的一致性是至关重要的,确保不会发生资金丢失、重复扣款或其他意外情况,源账号和目标账号的转账结果要么都成功要么都失败,不会存在一个成功一个失败的情况。

库存系统:在多个仓库之间进行库存转移或销售操作时,需要确保库存的一致性,防止商品超卖或库存混乱。

订票系统:需要确保预订信息的一致性,避免出现同一个资源被多次预订的问题。

Zookeeper:可作为注册中心,支持CP,拿主节点选举举例,当主节点异常进行选举,选举期间所有节点不可用,保证数据的一致性。

Redis:Redis主从模式是CP模式,当主从通信异常此时无法向主节点写数据。

Nacos:Nacos也支持CP,不过它默认是AP模式,当客户端注册为非临时节点时此时为CP模式,注册为非临时节点就需要实时上报心跳,即使在一段时间内未收到心跳信息,该实例仍然会保留在服务列表中,适用于配置中心。

符合AP的场景:满足A舍弃C,强调可用性。

AP强调的是可用性,允许短暂的不一致但是要保证最终一致性,在实际应用中符合AP的场景较多。

订单退款:退款后状态为退款中,24小时后退款金额到帐。

积分系统:注册送积分,注册成功积分在24小时后到账。

跨行转账:一般转账支持CP,还有的支持AP,源账号扣减金额后需要等一段时间目标账户才到账,或者源账号扣款后由于目标账号有问题过一段时间将转账金额退回到源账户。

MySQL主从复制:支持AP,向主节点写数据,异步同步到的从节点。

Nacos:默认支持AP,即临时节点的情况,会实时上报心跳,如果一段时间内未收到心跳信息,Nacos 会将该实例标记为不可用并从服务列表中移除。

在生产中AP场景应用的更多,强调的是可用性,允许出现短暂不一致,最终达到数据一致性。

小结

什么是CAP原理?

CAP分别表示一致性、可用性、分区容忍性.

CAP理论要强调在分布式系统中C、A、P这三点不能全部满足,要么满足AP、要么满足CP。

CAP原理对分布式事务控制有什么帮助?

根据需求确定是保证CP还是AP,再选择具体的技术方案。

优惠券核销事务控制方案

1)优惠券核销满足AP还是CP

根据我们的需求,创建订单和优惠券核销两个操作构成分布式事务,要对它们进行分布式事务控制基于CAP理论我们要满足CP还是AP?

如下图:

满足CP的要求:

创建订单和优惠券核销要么都成功要么都失败,不能存在一个成功一个失败,如果要实现CP需要在下单和核销优惠券操作前进行一次预操作,如果预操作成功将优惠券锁定避免在执行事务期间优惠券被其它订单使用。

满足AP的要求:

创建订单和优惠券核销要么都成功要么都失败,可以暂时存在一个成功一个失败,最终要保证数据的一致性,如果要实现AP,不需要提前锁定资源,在执行事务期间有一个失败则么另一个操作回滚即可,最终实现数据一致性。

基于上边的分析,实现CP更麻烦,实现AP同样满足的需求,本项目优惠券核销操作实现AP。

2)用什么技术方案实现AP

  1. 一方先成功另一方最终成功

对于一方先成功另一方最终成功的需求,比如:注册成功送积分,支付成功发送短信等这些场景,注册成功后向MQ发送消息,积分服务接收到消息后增加积分。

针对上边的需求还可以使用定时任务完成。

一方成功另一个方失败,成功方进行回滚

对于一方成功另一方失败,为保证最终一致性,成功一方需要回滚,如:下单扣库存,下单成功扣减库存失败此时下单业务回滚;优惠券核销,下单成功优惠券核销失败,此时下单操作回滚,或者下单失败,优惠券核销成功此时优惠券核销操作回滚。

针对上边的需求使用Seata进行分布式事务控制。

Seata提供了四种不同的分布式事务解决方案:

  • XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入

  • TCC模式:最终一致的分阶段事务模式,有业务侵入

  • AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式

  • SAGA模式:长事务模式,有业务侵入

下边说明Seata的AT模式:

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

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

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

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

说明:

1.事务管理器(TM)请求TC开启全局事务

2.事务管理器(TM)请求RM执行分支事务

下单RM:先向事务协调器(TC)注册下单分支事务,并执行sql(记录undo log),并向TC报告事务状态

undo log:用于回滚事务,seata根据undo log会生成反向sql进行事务回滚,举例:下单sql正常是向订单插入一条数据,seata生成的反向sql即删除订单记录,通过执行反向sql实现事务回滚。

优惠券RM:先向事务协调器(TC)注册优惠券核销分支事务,并执行sql(记录undo log),并向TC报告事务状态

3.事务管理器(TM)请求TC提交全局事务

4.TC检查分支事务状态,如果发现都成功则请求每个RM删除undo log,如果发现其中有失败记录则请求每个RM回滚事务,RM回滚事务的方法是通过undo log执行反向sql。

Seata执行的细节如下:

小结

优惠券核销满足AP还是CP?

实现AP的技术方案有哪些?

  • 基于MQ的方案
  • 基于定时任务的方案
  • 使用Seata。

本项目实现优惠券核销使用什么技术方案?

采用Seata实现分布式事务控制

 优惠券核销事务控制实现

启动seata 的事务协调器

启动seata容器(TC): docker start seata-server

如果没有使用下发的虚拟机需要自行安装Seata

拉取镜像:

docker pull seataio/seata-server:1.5.2

创建目录:

mkdir -p /data/soft/seata-jzo2o/data /data/soft/seata-jzo2o/config

将配置resources.tar 上传到服务器解压到/data/soft/seata-jzo2o/config

解压resources.tar : tar xvf resources.tar

注意修改resources中application.yml中nacos的配置

server:
  port: 7091

spring:
  application:
    name: seata-server

console:
  user:
    username: seata
    password: seata

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${user.home}/logs/seata

seata:
  config:
    # support: nacos 、 consul 、 apollo 、 zk  、 etcd3
    type: nacos
    nacos:
      server-addr: 192.168.101.68:8848
      namespace: 75a593f5-33e6-4c65-b2a0-18c403d20f63
      group: DEFAULT_GROUP
      username: nacos
      password: nacos
      ##if use MSE Nacos with auth, mutex with username/password attribute
      #access-key: ""
      #secret-key: ""
      data-id: seata-server.properties
  registry:
    # support: nacos 、 eureka 、 redis 、 zk  、 consul 、 etcd3 、 sofa
    type: nacos
    #preferred-networks: 30.240.*
    nacos:
      application: seata-server
      server-addr: 192.168.101.68:8848
      group: DEFAULT_GROUP
      namespace: 75a593f5-33e6-4c65-b2a0-18c403d20f63
      cluster: default
      username: nacos
      password: nacos
      ##if use MSE Nacos with auth, mutex with username/password attribute
      #access-key: ""
      #secret-key: ""
      
  server:
    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
    max-commit-retry-timeout: -1
    max-rollback-retry-timeout: -1
    rollback-retry-timeout-unlock-enable: false
    enableCheckAuth: true
    retryDeadThreshold: 130000
    xaerNotaRetryTimeout: 60000
    recovery:
      handle-all-session-period: 1000
    undo:
      log-save-days: 7
      log-delete-period: 86400000
    session:
      branch-async-queue-size: 5000 #branch async remove queue size
      enable-branch-async-remove: false #enable to asynchronous remove branchSession
      
  metrics:
    enabled: false
    registry-type: compact
    exporter-list: prometheus
    exporter-prometheus-port: 9898
  transport:
    rpc-tc-request-timeout: 30000
    enable-tc-server-batch-send-response: false
    shutdown:
      wait: 3
    thread-factory:
      boss-thread-prefix: NettyBoss
      worker-thread-prefix: NettyServerNIOWorker
      boss-thread-size: 1
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login

登录nacos配置seata-server.properties配置文件,内容如下:

store.mode = db
store.db.datasource = druid
store.db.dbType = mysql
store.db.driverClassName = com.mysql.cj.jdbc.Driver
store.db.url = jdbc:mysql://192.168.101.68:3306/seata?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
store.db.user = root
store.db.password = mysql
store.db.minConn = 5
store.db.maxConn = 100
store.db.globalTable = global_table
store.db.branchTable = branch_table
store.db.lockTable = lock_table
store.db.distributedLockTable = distributed_lock
store.db.queryLimit = 100
store.db.maxWait = 5000
#seata.tm.global_transaction_timeout = 60000
#seata.tm.beginTimeout = 5000

创建容器,seata端口号:8091(程序交互端口,根据情况进行修改),7091(管理端工具端口,根据情况进行修改

docker run -d \
--name seata-server \
--restart always \
-p 8091:8091 \
-p 7091:7091 \
-v /data/soft/seata-jzo2o/config/resources:/seata-server/resources  \
-e SEATA_IP=192.168.101.65 \
seataio/seata-server:1.5.2
配置seata环境

修改订单管理服务的bootstrap.yml,添加seata配置文件

在订单的三个数据库和优惠券数据库中创建undo_log表,此表记录每个分支事务的undo_log信息。

create table `数据库名`.undo_log
(
    id            bigint auto_increment
        constraint `PRIMARY`
        primary key,
    branch_id     bigint       not null,
    xid           varchar(100) not null,
    context       varchar(128) not null,
    rollback_info longblob     not null,
    log_status    int          not null,
    log_created   datetime     not null,
    log_modified  datetime     not null,
    ext           varchar(100) null,
    constraint ux_undo_log
        unique (xid, branch_id)
)
    charset = utf8;

 

开启全局事务测试

修改下单方法,在核销优惠券方法中开启全局事务

@GlobalTransactional
public void addWithCoupon(Orders orders, Long couponId) {
    CouponUseReqDTO couponUseReqDTO = new CouponUseReqDTO();
    couponUseReqDTO.setOrdersId(orders.getId());
    couponUseReqDTO.setId(couponId);
    couponUseReqDTO.setTotalAmount(orders.getTotalAmount());
    //优惠券核销
    CouponUseResDTO couponUseResDTO = couponApi.use(couponUseReqDTO);
    // 设置优惠金额
    orders.setDiscountAmount(couponUseResDTO.getDiscountAmount());
    // 计算实付金额
    BigDecimal realPayAmount = orders.getTotalAmount().subtract(orders.getDiscountAmount());
    orders.setRealPayAmount(realPayAmount);
    //保存订单
    add(orders);
    //模拟异常
    int i=1/0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值