分布式事务

分布式事务的问题:

事务的ACID原则:

 验证分布式事务的ACID特性:

订单服务:请求创建订单

controller:

@PostMapping
public ResponseEntity<Long> createOrder(Order order){
    Long orderId = orderService.create(order);
    return ResponseEntity.status(HttpStatus.CREATED).body(orderId);
}

service:

@GlobalTransactional  //全局事务
public Long create(Order order) {
    // 创建订单
    orderMapper.insert(order);
    try {
        // 扣用户余额
        accountClient.deduct(order.getUserId(), order.getMoney());
        // 扣库存
        storageClient.deduct(order.getCommodityCode(), order.getCount());

    } catch (FeignException e) {
        log.error("下单失败,原因:{}", e.contentUTF8(), e);
        throw new RuntimeException(e.contentUTF8(), e);
    }
    return order.getId();
}

这里使用feign的客户端调用实现用户余额和库存的扣减

@FeignClient("account-service")
public interface AccountClient {

    @PutMapping("/account/{userId}/{money}")
    void deduct(@PathVariable("userId") String userId, @PathVariable("money") Integer money);
}
@FeignClient("storage-service")
public interface StorageClient {
    @PutMapping("/storage/{code}/{count}")
    void deduct(@PathVariable("code") String code, @PathVariable("count") Integer count);
}

账户服务:account-service:

controller:

/**
 * 扣减账户余额
 * @param userId
 * @param money
 * @return
 */
@PutMapping("/{userId}/{money}")
public ResponseEntity<Void> deduct(@PathVariable("userId") String userId, @PathVariable("money") Integer money){
    accountService.deduct(userId, money);
    return ResponseEntity.noContent().build();
}

库存服务:storage-service

controller:

/**
 * 扣减库存
 * @param code 商品编号
 * @param count 要扣减的数量
 * @return 无
 */
@PutMapping("/{code}/{count}")
public ResponseEntity<Void> deduct(@PathVariable("code") String code,@PathVariable("count") Integer count){
    storageService.deduct(code, count);
    return ResponseEntity.noContent().build();
}

数据库数据:

account表(账户表)

order表

storage表:

向订单服务发起以下请求

查看数据库数据:

storage:

account:

order:

可以发现在库存不足的情况下扣减账户余额成功,新增订单成功,扣减库存失败,违反了事物的ACID特性。

分布式服务的事务问题:

对于单体服务只需要一个@Trancational就可以解决,但是对于分布式事务,跨服务跨数据库的业务无法解决。

解决分布式事物的方法:

理论基础:

CAP定理:

当网络出现故障时,一定会出现分区问题(P),当出现分区时,系统的一致性和可用性不能同时满足。CP和AP原则。

elasticsearch是CP原则,当es集群出现分区时,故障节点会贝剔除集群,数据分片会重新分配到其他结点,保证数据一致,因此是CP。

BASE理论:

初始seata:分布式事务解决方案

seata部署:

下载seata-server:

修改配置:

registry.conf:

registry {//配置注册中心
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    application = "seata-tc-server"
    serverAddr = "localhost:8845"
    group = "DEFAULT_GROUP"
    namespace = ""
    cluster = "SH"
    username = "nacos"
    password = "nacos"
  }
 }
config { //配置中心,将配置文件交给nacos管理
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "localhost:8845"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties" //seata配置文件
  }
  
}

seata.properties文件:

# 数据存储方式,db代表数据库
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://localhost:3306/seta?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
# 事务、日志等配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000

# 客户端与服务端传输方式
transport.serialization=seata
transport.compressor=none
# 关闭metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

创建全局事务表和分支事务表:

tc服务在管理分布式事物的时候,需要记录事务相关数据到数据库中。

主要记录全局事务、分支事务、全局锁信息:

启动TC服务:

微服务集成seata:

1.首先引入依赖:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
        <!--版本较低,1.3.0,因此排除-->
        <exclusion>
            <artifactId>seata-spring-boot-starter</artifactId>
            <groupId>io.seata</groupId>
        </exclusion>
    </exclusions>
</dependency>
<!--seata starter 采用1.4.2版本-->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>${seata.version}</version>
</dependency>

2.配置application.yml,让微服务通过注册中心找到seata-tc-server:

seata:
  registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    # 参考tc服务自己的registry.conf中的配置
    type: nacos
    nacos: # tc
      server-addr: 127.0.0.1:8845
      namespace: ""
      group: DEFAULT_GROUP
      application: seata-tc-server # tc服务在nacos中的服务名称
      cluster: SH
  tx-service-group: seata-demo # 事务组,根据这个获取tc服务的cluster名称
  service:
    vgroup-mapping: # 事务组与TC服务cluster的映射关系
      seata-demo: SH

seata的动手实践:

XA模式原理:

XA模式第一阶段如果全部成功,则第二阶段进行提交,如果有一个事务失败,则将成功的事务进行回滚。XA模式基于数据库本身的特性,是强一致的特性。

seata的XA实现:

优点:具备强一致的特性,满足ACID特性,常用的数据库都支持,实现简单,并且没有代码侵入

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

实现:

1.修改application.yml文件,开启XA模式

data-source-proxy-mode: XA  #全局事务一致性策略

2.给发起全局事物的入口方法上添加@GlobalTransactional注解,本例中是OrderServiceImpl中的Create方法(创建订单的方法)。

@GlobalTransactional  //全局事务
    public Long create(Order order) {
        // 创建订单
        orderMapper.insert(order);
        try {
            // 扣用户余额
            accountClient.deduct(order.getUserId(), order.getMoney());
            // 扣库存
            storageClient.deduct(order.getCommodityCode(), order.getCount());

        } catch (FeignException e) {
            log.error("下单失败,原因:{}", e.contentUTF8(), e);
            throw new RuntimeException(e.contentUTF8(), e);
        }
        return order.getId();
    }

3.启动服务。

4.发起以下请求。

从上图可以看出事务回滚了,并且数据库的数据并没有发生变化,实现了一致性。

AT模式:

XA模式和AT模式的区别:

XA模式第一阶段不提交,锁定资源,AT模式第一阶段直接提交,不锁定资源。

XA模式依赖数据库机制实现回滚,AT模式利用数据快照实现数据的回滚。

XA模式是强一直,AT模式是最终一致。

AT模式实现:

1、创建lock_table(记录全局锁的信息),undo_log(记录快照信息)。气中lock_table放到TC服务关联的数据库中,undo_log放到微服务关联的数据库。

2.修改applicaion.yml文件,将事务模式改为AT模式

data-source-proxy-mode: AT #全局事务一致性策略

3.重启服务并测试

再次发起以下请求:

从上图可以看出账户回滚。

TCC模式:

TCC模式与AT模式相似,每阶段都是独立的事务,不同的是TCC模式通过人工编码实现数据的恢复。

Try:资源预留和检测

confirm:完成资源操作。要求try成功confirm一定成功。

cancel:预留资源释放,try的反向操作。

代码实现:

1.创建冻结金额表account_freeze_tabl

2.创建account_freeze实体类

@Data
@TableName("account_freeze_tbl")
public class AccountFreeze {
    @TableId(type = IdType.INPUT)
    private String xid;
    private String userId;
    private Integer freezeMoney;
    private Integer state;

    public static abstract class State {
        public final static int TRY = 0;
        public final static int CONFIRM = 1;
        public final static int CANCEL = 2;
    }
}

3.创建accountTccService


@LocalTCC
public interface AccountTccService {
    /**
     * try逻辑,@TwoPhaseBusinessAction中的name属性要与当前方法名一致,用于指定try逻辑对应的方法
     * @param
     * @BusinessActionContextParameter标记的属性都会放到上下文对象中confirm和cancel方法的context多可以获取到
     */
    @TwoPhaseBusinessAction(name = "deduct",commitMethod = "confirm",rollbackMethod = "cancel")
    void deduct(@BusinessActionContextParameter(paramName = "userId")String userId,@BusinessActionContextParameter(paramName = "money") int money);

    /**
     * 二阶段提交confirm确认方法,方法名与commitMethod的属性值一致。
     * @param context
     * @return
     */
    boolean confirm(BusinessActionContext context);

    /**
     * 二阶段回滚方法,要保证与rollbackMethod的属性值一致
     * @param context
     * @return
     */
    boolean cancel(BusinessActionContext context);

}

AccountServiceImpl:


@Service
@Slf4j
public class AccountTccServiceImpl implements AccountTccService {
    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private AccountFreezeMapper accountFreezeMapper;
    @Override
    public void deduct(String userId,int money) {
        //获取事务id
        String xid = RootContext.getXID();
        //判断freeze是否有冻结记录,如果有则一定是cancel执行过,拒绝业务
        AccountFreeze accountFreeze1 = accountFreezeMapper.selectById(xid);
        if (accountFreeze1!=null){
            //已经执行过cancel操作了,避免业务悬挂
            return;

        }
        //扣减可用余额
        accountMapper.deduct(userId,money);
        //记录冻结金额,事务状态
        AccountFreeze accountFreeze=new AccountFreeze();
        accountFreeze.setFreezeMoney(money);
        accountFreeze.setXid(xid);  //事务id
        accountFreeze.setState(AccountFreeze.State.TRY);
        accountFreeze.setUserId(userId);
        //插入数据
        accountFreezeMapper.insert(accountFreeze);
    }

    @Override
    public boolean confirm(BusinessActionContext context) {
        //获取事务id,根据id删除冻结事务记录
        String xid = context.getXid();
        int i = accountFreezeMapper.deleteById(xid);
        return i==1;
    }

    @Override
    public boolean cancel(BusinessActionContext context) {
        //查询冻结记录
        String xid = context.getXid();
        String userId1 = context.getActionContext("userId").toString();
        AccountFreeze accountFreeze = accountFreezeMapper.selectById(xid);
        //空回滚判断,判断freeze是否为null
        if (accountFreeze==null){
            //空回滚,记录回滚记录
            accountFreeze=new AccountFreeze();
            accountFreeze.setState(AccountFreeze.State.CANCEL);
            accountFreeze.setUserId(userId1);
            accountFreeze.setXid(xid);
            accountFreeze.setFreezeMoney(0);
            accountFreezeMapper.insert(accountFreeze);
            return true;
        }
        //判断幂等
        if (accountFreeze.getState()==AccountFreeze.State.CANCEL){
            //已经处理过一次了cancel,无需重复处理
            return true;
        }

        //恢复可用余额,冻结金额的获取可以从context中和freeze中获取



        String userId = accountFreeze.getUserId();
        Integer money = accountFreeze.getFreezeMoney();
        //恢复金额
        accountMapper.refund(userId, money);
        //将冻结金额清零,将状态改为cancel
        accountFreeze.setFreezeMoney(0);
        accountFreeze.setState(AccountFreeze.State.CANCEL);
        int i = accountFreezeMapper.updateById(accountFreeze);
        return i==1;
    }
}

4.启动测试:发送请求购买11个商品。

account_freeze_tbl:冻结金额表插入了一条新的数据

Saga模式:

四种方式对比:

  • 12
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值