分布式事务Seata

一、本地事务@Transactional

1、本地事务案例演示

本案例演示下单流程,使用本地事务远程调用库存服务,正常下单成功后,订单和库存的数据库表都做扣减操作,模拟下单时发生运行时异常后,订单数据库表做了回滚,但是库存数据库表没有做回滚操作,此处的 @Transactional 是本地事务,只能对自己服务的数据库回滚有效,需要分布式事务才可解决跨服务事务回滚问题。

	@Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
        //将订单提交页面的数据放入此 threadLocal 中,用于共享数据
        orderSubmitVoThreadLocal.set(vo);
        //调用 ThreadLocal,获取同一线程的数据,得到当前登录的用户
        MemberRespVo memberRespVo = LoginUserIntereptor.loginUser.get();
        //生成提交订单后返回的封装类
        SubmitOrderResponseVo response = new SubmitOrderResponseVo();
        //没有抛出异常就设置状态码为0,代表成功
        response.setCode(0);
        //1、验证令牌【令牌的对比和删除必须保证原子性】
        //对比防重删令牌,返回0和1,0代表令牌失败、1代表令牌删除成功
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        //获取防重令牌
        String orderToken = vo.getOrderToken();
        //redis执行lua脚本,将页面输入令牌和redis中保存的令牌做对比,验证成功返回1,失败返回0
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
        if (result == 0l){
            //验证失败
            response.setCode(1);
            return response;
        }else {
            //1、令牌验证成功,解决重复提交订单成功问题,下单、创建订单、验令牌、验价格、锁库存
            OrderCreateTo order = createOrder();
            //2、验价,获取到应付金额
            BigDecimal payAmount = order.getOrder().getPayAmount();
            //获取页面提交的应付金额
            BigDecimal payPrice = vo.getPayPrice();
            //如果数据库订单里的应付金额和页面提交的金额差值小于0.01,就验证价格成功,因为是省略到小数点后两位,如果算相等的话,可能会出现 0.12和0.123456...
            double value = Math.abs(payAmount.subtract(payPrice).doubleValue());
            //如果验价成功
            if (value < 0.01){
                //TODO 三、保存订单
                saveOrder(order);
                // 四、库存锁定,stock_locked 锁定库存数 <= stock 库存数
                //库存锁定,只要有异常回滚订单数据  (订单号,所有订单项)
                WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
                //封装订单号
                wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
                List<OrderItemVo> locks = order.getOrderItems().stream().map((item) -> {
                    OrderItemVo orderItemVo = new OrderItemVo();
                    orderItemVo.setSkuId(item.getSkuId());
                    orderItemVo.setCount(item.getSkuQuantity());
                    orderItemVo.setTitle(item.getSkuName());
                    return orderItemVo;
                }).collect(Collectors.toList());
                //封装订单项
                wareSkuLockVo.setLocks(locks);
                //TODO 4、远程锁库存
                //远程调用库存服务调用锁定库存方法
                R r = wareFeignService.orderLockStock(wareSkuLockVo);
                if (r.getCode() == 0){
                    //如果锁定成功
                    response.setOrder(order.getOrder());
                    //-------------------------------模拟异常-------------------------------------
                    //TODO 5、远程扣减积分
                    int i = 10/0;
					
                    return response;
                }else {
                    response.setCode(3);
                    return response;
                }
            }else {
                //如果验价失败
                response.setCode(2);
                return response;
            }
        }
    }

本地事务:在分布式系统中,只能控制自己服务的数据库回滚,不能控制其他服务服务的数据库

分布式事务:可以控制所有服务的数据库回滚操作

2、图解分布式事务

微服务系统当中,假如不存在分布式事务,会发生什么呢?让我们以互联网中常用的交易业务为例子:
分布式事务Seata_第1张图片
上图中包含了库存和订单两个独立的微服务,每个微服务维护了自己的数据库。在交易系统的业务逻辑中,一个商品在下单之前需要先调用库存服务,进行扣除库存,再调用订单服务,创建订单记录。

正常情况下,两个数据库各自更新成功,两边数据维持着一致性。
但是如果发生异常,回滚的时候就会发生订单DB可以回滚,但是库存DB缺不能回滚

坑记录

在同一个类里面,编写两个方法,内部调用的时候,会导致事务设置失效。原因是没有用到代理对象的缘故。
解决:
0)、导入 spring-boot-starter-aop
1)、@EnableTransactionManagement(proxyTargetClass = true)
2)、@EnableAspectJAutoProxy(exposeProxy=true)
3)、AopContext.currentProxy() 调用方法

二、分布式事务框架Seata

步骤 1:创建 UNDO_LOG 表

每一个微服务必须先创建 undo_log 数据库表

-- 注意此处0.3.0+ 增加唯一索引 ux_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;

步骤 2:导入pom


<dependency>
   <groupId>com.alibaba.cloudgroupId>
   <artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>

查看seata-all的版本是 0.7.1
分布式事务Seata_第2张图片
所以要下载的seata-server版本也要是 0.7.1,seata-server就是tc(transaction coordinate)事务协调者
在这里插入图片描述

步骤 3:解压并启动 seata-server

从 https://github.com/seata/seata/releases,下载服务器软件包,将其解压缩

seata-server就是TC(Transactional coordinate)
分布式事务Seata_第3张图片
此处只在 registry.conf中配置了nacos注册中心

启动nacos,再启动seata服务器,seata已注册到nacos中
分布式事务Seata_第4张图片

步骤 4:seata代理数据源

所有要用到分布式事务的微服务,都需要让seata代理数据源

package com.lian.gulimall.ware.config;

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;

/**
 * seata 配置代理数据源
 *
 */
@Configuration
public class MySeataConfig {

    @Autowired
    DataSourceProperties dataSourceProperties;

    @Bean
    public DataSource dataSource(){
        HikariDataSource hikariDataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())){
            hikariDataSource.setPoolName(dataSourceProperties.getName());
        }
        //将创建好的数据源交给seata代理
        return new DataSourceProxy(hikariDataSource);
    }

}

步骤 5:file.conf和registry.conf

1、将seata-server中的 file.conf和registry.conf都复制到微服务下
2、修改 file.conf 的vgroup

#订单服务
service {
  # 每一个微服务就是rm,都注入到 tc 中,服务名是 gulimall-order-fescar-service-group
  vgroup_mapping.gulimall-order-fescar-service-group = "default"

或者在配置文件中修改,最好和服务名保持一致

	#服务分组
    alibaba:
      seata:
        tx-service-group: gulimall-order-fescar-service-group

步骤 6:主业务方法上加 @GlobalTransactional注解

适用场景
seata的分布式事务适合于在分布式服务中有多个远程调用的场景下,可以达到要么都成功,要么都失败的效果,例如商品服务的保存spu信息的业务方法,有多个远程调用,而且商品服务不是高并发场景
seata不适合用于高并发的场景下,中间件适合高并发场景,可以弥补seata的不足
例如:下单服务就是高并发场景,不适合使用seata,适合用中间件,例如用rabbitmq替代seata

你可能感兴趣的:(分布式事务,seata)