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`
可以看到order订单表里没有数据,此时还没有用户下单。
SELECT * FROM `seata_storage`.`t_storage`
id为1(id=1表示这条记录在数据库中id为1,product_id = 1表示产品id为1)的产品总数量为100,已经使用0个,还剩100个。
SELECT * FROM `seata_account`.`t_account`;
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
3、查看数据库情况
t_account
1号用户一共有1000,花了200还剩800
t_order
1号用户购买1号产品,买了10个花了100,交易成功(status=1)
t_storage
1号产品一共有100个,卖出20个,还剩80个
可以看出我们的数据库里各个数据都是符合逻辑的,说明我们前面的微服务搭建没有问题。
那么我们就要开始测试事务回滚了。
4、模拟错误
在AccountServiceImpl添加睡眠,模拟超时异常
5、再次下单,并查看数据库
下单当然不成功 ,关键是数据库里的数据如何变化。
首先要清楚,我们希望的是在出现错误后数据回滚,数据库里所有的数据都不要发生变化。
由于没有事务处理操作,这显然是不可能的,让我们来看看数据库里变成什么了。
t_account
用户账户还是被扣钱了
t_order
交易记录还是出现了,并且是一个不成功(status=0)的记录
t_storage
货物并没有变化。
这就相当于钱扣了,货没来,这显然不符合逻辑。 并且而且由于feign的重试机制,账户余额还有可能被多次扣减。
那么如何添加事务操作?
6、添加注解@GlobalTransactional
在订单的入口(OrderServiceImpl 的 create方法)上添加@GlobalTransactional 注解即可完成分布式事务的处理
7、再次下单测试
这个时候可以看到,数据库中的所有数据都没有发生变化。
这样,我们就完成了分布式事务的处理。
八、Seata原理浅析
1、再看TC/TM/RM三大组件
在我们这次的实战中,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的四种模式
4、AT模式如何做到对业务的无侵入
(1)一阶段加载
在一阶段,Seata 会拦截“业务 SQL”,
1 解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,
2 执行“业务 SQL”更新业务数据,在业务数据更新之后,
3 其保存成“after image”,最后生成行锁。
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
(2)二阶段提交
假如二阶段顺利提交,因为“业务 SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
(3)二阶段回滚
假如二阶段回滚,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。
回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。