zl程序教程

您现在的位置是:首页 >  其他

当前栏目

SpringCloud Alibaba学习(十二):Seata处理分布式事务(三万字提供 介绍、搭建、实战、原理一条龙服务)(下)

2023-03-14 22:52:08 时间

3、新建账户Account-Module 

               

(1)新建模块 

               

新建普通maven模块 seata-account-service2003


(2)修改pom文件 


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>com.shang.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
 
    <artifactId>seata-account-service2003</artifactId>
 
    <dependencies>
        <!--nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>seata-all</artifactId>
                    <groupId>io.seata</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>0.9.0</version>
        </dependency>
        <!--feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.37</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
 
 
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
 
</project>


(3)编写yml文件 


server:
  port: 2003
 
spring:
  application:
    name: seata-account-service
  cloud:
    alibaba:
      seata:
        tx-service-group: fsp_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3308/seata_account
    username: root
    password: root
 
feign:
  hystrix:
    enabled: false
 
logging:
  level:
    io:
      seata: info
 
mybatis:
  mapperLocations: classpath:mapper/*.xml
 


(4)粘贴conf文件(1.0版本后已经支持yml) 

                 

将seata目录下的 file.conf 和 registry.conf 粘到resource目录下


(5) domain包

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>{
    private Integer code;
    private String  message;
    private T       data;
 
    public CommonResult(Integer code, String message)
    {
        this(code,message,null);
    }
}


@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
 
    private Long id;
 
    /**
     * 用户id
     */
    private Long userId;
 
    /**
     * 总额度
     */
    private BigDecimal total;
 
    /**
     * 已用额度
     */
    private BigDecimal used;
 
    /**
     * 剩余额度
     */
    private BigDecimal residue;
}


(6)dao包 


@Mapper
public interface AccountDao {
 
    /**
     * 扣减账户余额
     */
    void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}


(7)service包 


public interface AccountService {
 
    /**
     * 扣减账户余额
     * @param userId 用户id
     * @param money 金额
     */
    void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}


(8)service.impl包 


@Service
public class AccountServiceImpl implements AccountService {
 
    private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);
 
 
    @Resource
    AccountDao accountDao;
 
    /**
     * 扣减账户余额
     */
    @Override
    public void decrease(Long userId, BigDecimal money) {
        LOGGER.info("------->account-service中扣减账户余额开始");
        //模拟超时异常,全局事务回滚
        //暂停几秒钟线程
        //try { TimeUnit.SECONDS.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); }
        accountDao.decrease(userId,money);
        LOGGER.info("------->account-service中扣减账户余额结束");
    }
}


(9)controller包 


@RestController
public class AccountController {
 
    @Resource
    AccountService accountService;
 
    /**
     * 扣减账户余额
     */
    @RequestMapping("/account/decrease")
    public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
        accountService.decrease(userId,money);
        return new CommonResult(200,"扣减账户余额成功!");
    }
}


(10)config包 


同2001 


(11)主启动类 


@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataAccountMainApp2003{
    public static void main(String[] args) {
        SpringApplication.run(SeataAccountMainApp2003.class, args);
    }
}


七、测试


       

按顺序启动Nacos、Seata、数据库、三个微服务。

 

1、先看数据库初始情况 

      

SELECT *  FROM  `seata_order`.`t_order`


5f742119f57142b18c0f2b75189ec7ad.png

可以看到order订单表里没有数据,此时还没有用户下单。

        

SELECT * FROM `seata_storage`.`t_storage`


06905d9141a8468da02d52cec0bbc70f.png


 id为1(id=1表示这条记录在数据库中id为1,product_id = 1表示产品id为1)的产品总数量为100,已经使用0个,还剩100个。 

        

SELECT *  FROM  `seata_account`.`t_account`;

140589c85beb4339b1f96f241a93a96f.png


 id为1的用户账户中一共有1000,用了0,还剩1000


2、正常下单 

       

浏览器访问:


http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

        表示id为1的用户购买id为1的产品,买了10个花了100


d7954a7920504c9aaf3426ae6b996a20.png


3、查看数据库情况 

        

t_account

fdfc794c42e44fbb9de58b4689336fea.png


 1号用户一共有1000,花了200还剩800 

       

 t_order

6e79205642b54cc99712a09206c48612.png


1号用户购买1号产品,买了10个花了100,交易成功(status=1) 

        

t_storage


cca44d5e2eac44e58a52e9c4096dd4ac.png

 1号产品一共有100个,卖出20个,还剩80个 

       

可以看出我们的数据库里各个数据都是符合逻辑的,说明我们前面的微服务搭建没有问题。

       

那么我们就要开始测试事务回滚了。


4、模拟错误 

       

在AccountServiceImpl添加睡眠,模拟超时异常

b09d5800aca348c280dbb7e243287690.png


5、再次下单,并查看数据库 

       

下单当然不成功 ,关键是数据库里的数据如何变化。


39266e86420544dc9590821b755afb00.png

首先要清楚,我们希望的是在出现错误后数据回滚,数据库里所有的数据都不要发生变化。

       

由于没有事务处理操作,这显然是不可能的,让我们来看看数据库里变成什么了。 

        

t_account

8af749d4e90b41ebb70e8291e9a29280.png

  用户账户还是被扣钱了 

        

t_order


5187f2c251824ef694af211bf6c89a7d.png

 交易记录还是出现了,并且是一个不成功(status=0)的记录 

        

t_storage

cca44d5e2eac44e58a52e9c4096dd4ac.png


货物并没有变化。

 这就相当于钱扣了,货没来,这显然不符合逻辑。 并且而且由于feign的重试机制,账户余额还有可能被多次扣减。


那么如何添加事务操作?


6、添加注解@GlobalTransactional

       

在订单的入口(OrderServiceImpl 的 create方法)上添加@GlobalTransactional 注解即可完成分布式事务的处理


       

7、再次下单测试

       

这个时候可以看到,数据库中的所有数据都没有发生变化。


       

这样,我们就完成了分布式事务的处理。


八、Seata原理浅析


       

1、再看TC/TM/RM三大组件

0adef6a987ec4cf6ac9c73ceb43249bc.png


在我们这次的实战中,TC其实就相当于我们的Seata;TM相当于我们的account微服务(@GlobalTransactional加上的地方);RM就是我们的三个数据库,每一个数据库就是一个RM


 2、分布式事务的执行流程

               

1. TM 开启分布式事务(TM 向 TC 注册全局事务记录);

           

2. 按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态 );

             

3. TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务);

               

4. TC汇总事务信息,决定分布式事务是提交还是回滚;


5. TC 通知所有 RM 提交/回滚 资源,事务二阶段结束。


3、Seata的四种模式

703d138b775a4313a1d42ee07023da36.png


4、AT模式如何做到对业务的无侵入

               

(1)一阶段加载


在一阶段,Seata 会拦截“业务 SQL”,

       

1  解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,

2  执行“业务 SQL”更新业务数据,在业务数据更新之后,

3  其保存成“after image”,最后生成行锁。


以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。  


e390cd5e8e49457cbace694eee1615f9.png


(2)二阶段提交 


        假如二阶段顺利提交,因为“业务 SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。 

915a4277ffff4a039de2d4bef7529877.png



(3)二阶段回滚


假如二阶段回滚,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。

回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。  



e8f7ba7185a840dcad166f3586e80ae0.png