概述
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务
中文开发文档http://seata.io/zh-cn/
Github地址:https://github.com/seata/seata
Seata术语
| TC (Transaction Coordinator) - 事务协调者 | 维护全局和分支事务的状态,驱动全局事务提交或回滚。 |
|---|---|
| TM (Transaction Manager) - 事务管理器 | 定义全局事务的范围:开始全局事务、提交或回滚全局事务。 |
| RM (Resource Manager) - 资源管理器 | 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。 |
环境准备
二进制启动
release版本下载地址https://github.com/seata/seata/releases
#!/bin/bashwget https://handson.oss-cn-shanghai.aliyuncs.com/seata-server-1.4.1.zip -O .seata-server-1.4.1.zipunzip .seata-server-1.4.1.zipnohup sh ~/seata/bin/seata-server.sh -p 8091 -m file &
docker
dockerhub地址https://hub.docker.com/r/seataio/seata-server
快速创建方式:
docker run -d --name seata-server -p 8091:8091 seataio/seata-server:1.4.1
根pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.alibaba.seata</groupId><artifactId>seata-sample</artifactId><packaging>pom</packaging><version>0.0.1-SNAPSHOT</version><modules><module>business-service</module><module>storage-service</module><module>order-service</module><module>common-service</module></modules><name>seata-sample</name><description>seata sample project for Spring Boot</description><properties><java.version>1.8</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>2.3.2.RELEASE</spring-boot.version><alibaba.cloud.version>2.2.3.RELEASE</alibaba.cloud.version><seata.version>1.4.0</seata.version><mysql.version>8.0.11</mysql.version><lombok.version>1.16.4</lombok.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>${alibaba.cloud.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId><version>${alibaba.cloud.version}</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-ribbon</artifactId><version>${alibaba.cloud.version}</version></dependency><dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId><version>${seata.version}</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql.version}</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.0</version></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>1.8</source><target>1.8</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>2.3.0.RELEASE</version></plugin></plugins></build></project>
工程说明
common-service
pom.xml
<?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>seata-sample</artifactId><groupId>com.alibaba.seata</groupId><version>0.0.1-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>common-service</artifactId><dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies></project>
常量定义
package com.alibaba.seata.sample.common;/*** @author slievrly*/public interface Constants {String SUCCESS_RESPONSE = "ok";String FAIL_RESPONSE = "fail";}
实体类 Account
package com.alibaba.seata.sample.common.entity;import lombok.Data;/*** @author slievrly*/@Datapublic class Account {private Integer id;private String userId;private Integer money;}
实体类 订单Order
package com.alibaba.seata.sample.common.entity;import lombok.Data;/*** @author slievrly*/@Datapublic class Order {private Integer id;private String userId;private String commodityCode;private Integer count;private Integer money;}
Storage
package com.alibaba.seata.sample.common.entity;import lombok.Data;/*** @author slievrly*/@Datapublic class Storage {private Integer id;private String commodityCode;private Integer count;}
business-service
pom.xml
<?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>seata-sample</artifactId><groupId>com.alibaba.seata</groupId><version>0.0.1-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>business-service</artifactId><dependencies><dependency><groupId>${project.groupId}</groupId><artifactId>common-service</artifactId><version>${project.version}</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-ribbon</artifactId></dependency><dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.1</version><configuration><source>1.8</source><target>1.8</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>${spring-boot.version}</version><configuration><mainClass>com.alibaba.seata.sample.business.service.BusinessApplication</mainClass><layout>ZIP</layout></configuration><executions><execution><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins></build></project>
入口main类 BusinessApplication
package com.alibaba.seata.sample.business.service;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.openfeign.EnableFeignClients;/*** @author slievrly*/@EnableFeignClients@SpringBootApplicationpublic class BusinessApplication {public static void main(String[] args) {SpringApplication.run(BusinessApplication.class, args);}}
service
package com.alibaba.seata.sample.business.service.service;/*** @author slievrly*/public interface IBusinessService {void reduceStockAndCreateOrder(int count, boolean mockException);String checkData();}
继承接口
package com.alibaba.seata.sample.business.service.service.impl;import java.util.List;import java.util.concurrent.ThreadLocalRandom;import java.util.concurrent.TimeUnit;import javax.annotation.PostConstruct;import com.alibaba.seata.sample.business.service.feign.IOrderService;import com.alibaba.seata.sample.business.service.feign.IStorageService;import com.alibaba.seata.sample.business.service.service.IBusinessService;import com.alibaba.seata.sample.common.entity.Order;import com.alibaba.seata.sample.common.entity.Storage;import io.seata.core.context.RootContext;import io.seata.spring.annotation.GlobalTransactional;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import static com.alibaba.seata.sample.common.Constants.SUCCESS_RESPONSE;/*** @author slievrly*/@Servicepublic class BusinessServiceImpl implements IBusinessService {private static final int INIT_COUNT = 100;@Autowiredprivate IOrderService orderService;@Autowiredprivate IStorageService storageService;private String opCommodityCode;@PostConstructpublic void init() throws InterruptedException {for (int i = 0; i < 100; i++) {try {ThreadLocalRandom random = ThreadLocalRandom.current();String commodityCode = "C" + random.nextInt(999999);storageService.initStock(commodityCode, INIT_COUNT);opCommodityCode = commodityCode;break;} catch (Exception ignore) {ignore.printStackTrace();TimeUnit.MILLISECONDS.sleep(2);}}}@Override@GlobalTransactional(timeoutMills = 300000, name = "spring-cloud-alibaba-seata")public void reduceStockAndCreateOrder(int count, boolean mockException) {String xid = RootContext.getXID();System.out.println("business-service 当前正在执行的事务xid:" + xid);String result = storageService.reduceStock(opCommodityCode, count);if (!SUCCESS_RESPONSE.equals(result)) {throw new RuntimeException("库存扣减失败! xid:" + xid);}Order order = new Order().setCommodityCode(opCommodityCode).setCount(count).setMoney(count * 100).setUserId("system");result = orderService.createOrder(order);if (!SUCCESS_RESPONSE.equals(result)) {throw new RuntimeException("创建订单失败! xid:" + xid);}if (mockException) {throw new RuntimeException("模拟用户业务异常! xid:" + xid);}}@Overridepublic String checkData() {StringBuilder sb = new StringBuilder();List<Order> orders = orderService.selectOrderByCode(opCommodityCode);List<Storage> storages = storageService.selectStockByCode(opCommodityCode);int orderTotal = 0;int storageTotal = 0;sb.append("<strong>当前您操作的CommodityCode是: <font color=\"#FF0000\">" + opCommodityCode + "</font></strong>");sb.append("<hr>");sb.append("<strong>数据表:</strong>");sb.append("<br/>");sb.append("<br/>");sb.append("<strong>order_tbl:</strong>");sb.append("<table border=\"2\">");sb.append("<tr>");sb.append("<td>Id</td><td>user_id</td><td>commodity_code</td><td>count</td><td>money</td>");sb.append("</tr>");for (Order order : orders) {sb.append("<tr>");sb.append("<td>" + order.getId() + "</td><td>" + order.getUserId() + "</td><td>" + order.getCommodityCode()+ "</td><td>" + order.getCount() + "</td><td>" + order.getMoney() + "</td>");sb.append("</tr>");orderTotal += order.getCount();}sb.append("</table>");sb.append("<br/>");sb.append("<strong>storage_tbl:</strong>");sb.append("<table border=\"2\">");sb.append("<tr>");sb.append("<td>Id</td><td>commodity_code</td><td>count</td>");sb.append("</tr>");for (Storage storage : storages) {sb.append("<tr>");sb.append("<td>" + storage.getId() + "</td><td>" + storage.getCommodityCode() + "</td><td>" + storage.getCount()+ "</td>");sb.append("</tr>");storageTotal += storage.getCount();}sb.append("</table>");sb.append("<hr>");sb.append("<strong>数据一致性校验:</strong>");sb.append("<br/>");sb.append("<br/>");sb.append("<strong>调用前库存:</strong>");sb.append("<br/>");sb.append("初始化库存(storage_tbl): <strong><font color=\"#FF0000\">" + INIT_COUNT + "</font></strong>");sb.append("<br/>");sb.append("<br/>");sb.append("<strong>调用后库存:</strong>");sb.append("<br/>");sb.append("已卖出库存数量(order_tbl): <strong><font color=\"#FF0000\">" + orderTotal + "</font></strong>");sb.append("<br/>");sb.append("剩余库存(storage_tbl): <strong><font color=\"#FF0000\">" + storageTotal + "</font></strong>");sb.append("<br/>");sb.append("<br/>");sb.append("<strong>结论:</strong> 库存量调用前后 <strong><font color=\"#FF0000\">" + (INIT_COUNT == orderTotal + storageTotal? "一致" : "不一致") + "</font></strong>");sb.append("<hr>");return sb.toString();}}
feign
package com.alibaba.seata.sample.business.service.feign;import java.util.List;import com.alibaba.seata.sample.common.entity.Order;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;/*** @author slievrly*/@FeignClient(name = "order-service",url = "http://127.0.0.1:8082")public interface IOrderService {@RequestMapping(value = "/createOrder")String createOrder(@RequestBody Order orders);@RequestMapping(value = "/selectOrderByCode")List<Order> selectOrderByCode(@RequestParam(name = "commodityCode") String commodityCode);}
存储
package com.alibaba.seata.sample.business.service.feign;import java.util.List;import com.alibaba.seata.sample.common.entity.Storage;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;/*** @author slievrly*/@FeignClient(name = "storage-service",url = "http://127.0.0.1:8083")public interface IStorageService {@RequestMapping("/reduceStock")String reduceStock(@RequestParam(name = "commodityCode") String commodityCode,@RequestParam(name = "count") Integer count);@RequestMapping("/initStock")int initStock(@RequestParam(name = "commodityCode") String commodityCode,@RequestParam(name = "count") Integer count);@RequestMapping(value = "/selectStockByCode")List<Storage> selectStockByCode(@RequestParam(name = "commodityCode") String commodityCode);}
controller
package com.alibaba.seata.sample.business.service.controller;import java.io.IOException;import javax.servlet.http.HttpServletResponse;import com.alibaba.seata.sample.business.service.service.IBusinessService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;/*** @author slievrly*/@RestControllerpublic class HomeController {@Autowiredprivate IBusinessService businessService;@GetMapping(value = "/seata/feign", produces = "application/json")public String feign(int count, boolean mockException) {StringBuilder result = new StringBuilder();if (count <= 0) {return "expect: count>0 ";}try {businessService.reduceStockAndCreateOrder(count, mockException);} catch (Exception exx) {result.append("失败:").append(exx.getMessage()).append("事务发生回滚!");}if (result.length() == 0) {result.append("调用扣减库存生成订单服务成功!");}return result.toString();}@GetMapping(value = "/seata/check", produces = "application/json")public void check(HttpServletResponse response) throws IOException {response.setContentType("text/html; charset=gb2312");response.getWriter().write(businessService.checkData());}}
application.yml
spring:datasource:type: com.alibaba.druid.pool.DruidDataSourceurl: jdbc:mysql://rm-2zetd9474ydd1g5955o.mysql.rds.aliyuncs.com:3306/seata_training?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTCdriver-class-name: com.mysql.cj.jdbc.Driverusername: seatapassword: seata123max-wait: 60000max-active: 100min-idle: 10initial-size: 10seata:enabled: trueapplication-id: businesstx-service-group: my_test_tx_groupservice:vgroup-mapping:my_test_tx_group: defaultgrouplist:default: 127.0.0.1:8091disable-global-transaction: false
bootstrap.yml
spring:application:name: business-servicemain:allow-bean-definition-overriding: truecloud:loadbalancer:retry:enabled: falseserver:port: 60000ribbon:ConnectTimeout: 100000ReadTimeout: 1000000OkToRetryOnAllOperations: falsefeign:httpclient:enabled: trueconnection-timeout: 60000client:config:default:connectTimeout: 60000readTimeout: 60000loggerLevel: FULL
order-service
pom.xml
<?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>seata-sample</artifactId><groupId>com.alibaba.seata</groupId><version>0.0.1-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>order-service</artifactId><dependencies><dependency><groupId>${project.groupId}</groupId><artifactId>common-service</artifactId><version>${project.version}</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-ribbon</artifactId></dependency><dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.1</version><configuration><source>1.8</source><target>1.8</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>${spring-boot.version}</version><configuration><mainClass>com.alibaba.seata.sample.order.service.OrderApplication</mainClass><layout>ZIP</layout></configuration><executions><execution><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins></build></project>
入口类
package com.alibaba.seata.sample.order.service;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.openfeign.EnableFeignClients;/*** @author slievrly*/@SpringBootApplication@EnableFeignClients@EnableDiscoveryClientpublic class OrderApplication {public static void main(String[] args) {SpringApplication.run(OrderApplication.class, args);}}
service
package com.alibaba.seata.sample.order.service.service;import java.util.List;import com.alibaba.seata.sample.common.entity.Order;/*** @author slievrly*/public interface IOrderService {void createOrder(Order order);List<Order> selectOrderByCode(String commodityCode);}
服务实现类
package com.alibaba.seata.sample.order.service.service.impl;import java.util.List;import com.alibaba.seata.sample.common.entity.Order;import com.alibaba.seata.sample.order.service.mapper.OrderMapper;import com.alibaba.seata.sample.order.service.service.IOrderService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;/*** @author slievrly*/@Servicepublic class OrderServiceImpl implements IOrderService {@Autowiredprivate OrderMapper orderMapper;@Overridepublic void createOrder(Order order) {orderMapper.insert(order);}@Overridepublic List<Order> selectOrderByCode(String commodityCode) {return orderMapper.selectOrderByCode(commodityCode);}}
数据maper
import org.apache.ibatis.annotations.Mapper;/*** @author slievrly*/@Mapperpublic interface OrderMapper {int insert(Order order);List<Order> selectOrderByCode(String commodityCode);}
resources/mapper/OrderMapper.xml
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace="com.alibaba.seata.sample.order.service.mapper.OrderMapper"><resultMap id="BaseResultMap" type="com.alibaba.seata.sample.common.entity.Order"><id column="id" property="id" jdbcType="INTEGER"/><result column="user_id" property="userId" jdbcType="VARCHAR"/><result column="commodity_code" property="commodityCode" jdbcType="VARCHAR"/><result column="count" property="count" jdbcType="INTEGER"/><result column="money" property="money" jdbcType="INTEGER"/></resultMap><insert id="insert" parameterType="com.alibaba.seata.sample.common.entity.Order">insert into order_tbl (user_id,commodity_code,count,money)values (#{userId,jdbcType=VARCHAR}, #{commodityCode,jdbcType=VARCHAR}, #{count,jdbcType=INTEGER}, #{money,jdbcType=INTEGER})</insert><select id="selectOrderByCode" parameterType="java.lang.String" resultMap="BaseResultMap">select * from order_tbl where commodity_code=#{commodityCode};</select></mapper>
控制类
package com.alibaba.seata.sample.order.service.controller;import java.util.List;import com.alibaba.seata.sample.common.Constants;import com.alibaba.seata.sample.common.entity.Order;import com.alibaba.seata.sample.order.service.service.IOrderService;import io.seata.core.context.RootContext;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RestController;/*** @author slievrly*/@RestControllerpublic class OrderController {@Autowiredprivate IOrderService orderService;@RequestMapping(value = "/createOrder", method = RequestMethod.POST, produces = "application/json")public String create(@RequestBody Order order) {System.out.println("order-service 当前正在执行的事务xid:" + RootContext.getXID());try {orderService.createOrder(order);} catch (Exception exx) {exx.printStackTrace();return Constants.FAIL_RESPONSE;}return Constants.SUCCESS_RESPONSE;}@RequestMapping(value = "/selectOrderByCode", method = RequestMethod.GET, produces = "application/json")public List<Order> selectOrderByCode(String commodityCode) {return orderService.selectOrderByCode(commodityCode);}}
application.yml
spring:datasource:type: com.alibaba.druid.pool.DruidDataSourceurl: jdbc:mysql://rm-2zetd9474ydd1g5955o.mysql.rds.aliyuncs.com:3306/seata_training?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTCdriver-class-name: com.mysql.cj.jdbc.Driverusername: seatapassword: seata123max-wait: 60000max-active: 100min-idle: 10initial-size: 10mybatis:mapper-locations: classpath*:mapper/*Mapper.xmlseata:enabled: trueapplication-id: ordertx-service-group: my_test_tx_groupservice:vgroup-mapping:my_test_tx_group: defaultgrouplist:default: 127.0.0.1:8091disable-global-transaction: false
bootstrap.yml
spring:application:name: order-servicemain:allow-bean-definition-overriding: truecloud:loadbalancer:retry:enabled: falseserver:port: 8082ribbon:ConnectTimeout: 100000ReadTimeout: 1000000OkToRetryOnAllOperations: false
storage-service
pom.xml
<?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>seata-sample</artifactId><groupId>com.alibaba.seata</groupId><version>0.0.1-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>storage-service</artifactId><dependencies><dependency><groupId>${project.groupId}</groupId><artifactId>common-service</artifactId><version>${project.version}</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-ribbon</artifactId></dependency><dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.1</version><configuration><source>1.8</source><target>1.8</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>${spring-boot.version}</version><configuration><mainClass>com.alibaba.seata.sample.storage.service.StorageApplication</mainClass><layout>ZIP</layout></configuration><executions><execution><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins></build></project>
入口类
package com.alibaba.seata.sample.storage.service;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;/*** @author slievrly*/@SpringBootApplicationpublic class StorageApplication {public static void main(String[] args) {SpringApplication.run(StorageApplication.class, args);}}
service 接口
package com.alibaba.seata.sample.storage.service.service;import java.util.List;import com.alibaba.seata.sample.common.entity.Storage;/*** @author slievrly*/public interface IStorageService {int reduceStock(String commodityCode, int count);int initStock(String commodityCode, int count);List<Storage> selectStockByCode(String commodityCode);}
接口实现
package com.alibaba.seata.sample.storage.service.service.impl;import java.util.List;import com.alibaba.seata.sample.common.entity.Storage;import com.alibaba.seata.sample.storage.service.mapper.StorageMapper;import com.alibaba.seata.sample.storage.service.service.IStorageService;import io.seata.core.context.RootContext;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;/*** @author slievrly*/@Servicepublic class StorageServiceImpl implements IStorageService {@Autowiredprivate StorageMapper storageMapper;@Overridepublic int reduceStock(String commodityCode, int count) {System.out.println("storage-service 当前正在执行的事务xid:" + RootContext.getXID());return storageMapper.updateStockByCode(commodityCode, count);}@Overridepublic int initStock(String commodityCode, int count) {return storageMapper.insert(new Storage().setCommodityCode(commodityCode).setCount(count));}@Overridepublic List<Storage> selectStockByCode(String commodityCode) {return storageMapper.selectStockByCode(commodityCode);}}
数据mapper
package com.alibaba.seata.sample.storage.service.mapper;import java.util.List;import com.alibaba.seata.sample.common.entity.Storage;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Param;/*** @author slievrly*/@Mapperpublic interface StorageMapper {Storage selectById(@Param("id") Integer id);List<Storage> selectStockByCode(@Param("commodityCode") String commodityCode);int updateStockByCode(@Param("commodityCode") String commodityCode, @Param("count") int count);int insert(Storage storage);}
resource/mapper/StorageMapper.xml
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace="com.alibaba.seata.sample.storage.service.mapper.StorageMapper"><resultMap id="BaseResultMap" type="com.alibaba.seata.sample.common.entity.Storage"><id column="id" property="id" jdbcType="INTEGER"/><result column="commodity_code" property="commodityCode" jdbcType="VARCHAR"/><result column="count" property="count" jdbcType="INTEGER"/></resultMap><select id="selectById" resultMap="BaseResultMap">select id, commodity_code, count from storage_tblWHERE id = #{id}</select><select id="selectAll" resultMap="BaseResultMap">select id, commodity_code, count from storage_tbl</select><select id="selectStockByCode" parameterType="java.lang.String" resultMap="BaseResultMap">select id, commodity_code, count from storage_tblWHERE commodity_code = #{commodityCode}</select><update id="updateStockByCode">update storage_tbl set count = count- #{count}WHERE commodity_code = #{commodityCode} and count >= #{count}</update><insert id="insert" parameterType="com.alibaba.seata.sample.common.entity.Storage">insert into storage_tbl (commodity_code, count)values (#{commodityCode,jdbcType=VARCHAR}, #{count,jdbcType=INTEGER})</insert><delete id="delete" parameterType="java.lang.String">delete from storage_tbl where commodity_code=#{commodityCode}</delete></mapper>
控制类
package com.alibaba.seata.sample.storage.service.controller;import java.util.List;import com.alibaba.seata.sample.common.Constants;import com.alibaba.seata.sample.common.entity.Storage;import com.alibaba.seata.sample.storage.service.service.IStorageService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;/*** @author slievrly*/@RestControllerpublic class StorageController {@AutowiredIStorageService storageService;@RequestMapping("/reduceStock")public String reduceStock(@RequestParam(name = "commodityCode") String commodityCode,@RequestParam(name = "count") Integer count) {return storageService.reduceStock(commodityCode, count) == 1 ? Constants.SUCCESS_RESPONSE: Constants.FAIL_RESPONSE;}@RequestMapping("/initStock")public int initStock(@RequestParam(name = "commodityCode") String commodityCode,@RequestParam(name = "count") Integer count) {return storageService.initStock(commodityCode, count);}@RequestMapping("/selectStockByCode")public List<Storage> selectStockByCode(@RequestParam(name = "commodityCode") String commodityCode) {return storageService.selectStockByCode(commodityCode);}}
application.yml
spring:datasource:type: com.alibaba.druid.pool.DruidDataSourceurl: jdbc:mysql://rm-2zetd9474ydd1g5955o.mysql.rds.aliyuncs.com:3306/seata_training?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTCdriver-class-name: com.mysql.cj.jdbc.Driverusername: seatapassword: seata123max-wait: 60000max-active: 100min-idle: 10initial-size: 10mybatis:mapper-locations: classpath*:mapper/*Mapper.xmlseata:enabled: trueapplication-id: storagetx-service-group: my_test_tx_groupservice:vgroup-mapping:my_test_tx_group: defaultgrouplist:default: 127.0.0.1:8091disable-global-transaction: false
bootstrap.yml
spring:application:name: storage-servicemain:allow-bean-definition-overriding: truecloud:loadbalancer:retry:enabled: falseserver:port: 8083ribbon:ConnectTimeout: 100000ReadTimeout: 1000000OkToRetryOnAllOperations: false
本实验提供了3个工程,其调用关系如下:
如图所示,服务链路中包含三个微服务:business(微服务入口)、order(订单服务)和 storage(库存服务)。
业务逻辑
- 通过url 调用 business 服务,business 服务会通过 openFeign 的方式分别调用 storage 和 order 服务。
- 每个服务调用后会根据正常(ok)或异常(fail)的返回值决定是否抛出异常,若返回结果不为 ok 那么 business 将抛出异常,触发整个事务回滚,预期数据至 business 方法执行前的数据并且库存总量和与初始值一致。
- 当order 和 storage 两个服务调用正常,用户可以根据 url 中的 mockException=true 或 false 来注入一个 mock 异常,当注入异常后,期望数据回滚至 business 方法执行前的数据并且库存总量和与初始值一致。
异常逻辑
- 在 bussiness 中请求超过现有库存,通过参数 count 来指定要扣减的库存数量,当超出库存,将抛出异常进行事务回滚。
- 在 bussiness 中请求中加入 mockException=true 参数,触发事务回滚。
undo.sql
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,PRIMARY KEY (id),UNIQUE KEY ux_undo_log (xid, branch_id)) ENGINE = InnoDB;
参考
https://doc.ruoyi.vip/ruoyi-cloud/cloud/seata.html#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8
