概述
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/bash
wget https://handson.oss-cn-shanghai.aliyuncs.com/seata-server-1.4.1.zip -O .seata-server-1.4.1.zip
unzip .seata-server-1.4.1.zip
nohup 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
*/
@Data
public class Account {
private Integer id;
private String userId;
private Integer money;
}
实体类 订单Order
package com.alibaba.seata.sample.common.entity;
import lombok.Data;
/**
* @author slievrly
*/
@Data
public 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
*/
@Data
public 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
@SpringBootApplication
public 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
*/
@Service
public class BusinessServiceImpl implements IBusinessService {
private static final int INIT_COUNT = 100;
@Autowired
private IOrderService orderService;
@Autowired
private IStorageService storageService;
private String opCommodityCode;
@PostConstruct
public 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);
}
}
@Override
public 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
*/
@RestController
public class HomeController {
@Autowired
private 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.DruidDataSource
url: jdbc:mysql://rm-2zetd9474ydd1g5955o.mysql.rds.aliyuncs.com:3306/seata_training?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
username: seata
password: seata123
max-wait: 60000
max-active: 100
min-idle: 10
initial-size: 10
seata:
enabled: true
application-id: business
tx-service-group: my_test_tx_group
service:
vgroup-mapping:
my_test_tx_group: default
grouplist:
default: 127.0.0.1:8091
disable-global-transaction: false
bootstrap.yml
spring:
application:
name: business-service
main:
allow-bean-definition-overriding: true
cloud:
loadbalancer:
retry:
enabled: false
server:
port: 60000
ribbon:
ConnectTimeout: 100000
ReadTimeout: 1000000
OkToRetryOnAllOperations: false
feign:
httpclient:
enabled: true
connection-timeout: 60000
client:
config:
default:
connectTimeout: 60000
readTimeout: 60000
loggerLevel: 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
@EnableDiscoveryClient
public 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
*/
@Service
public class OrderServiceImpl implements IOrderService {
@Autowired
private OrderMapper orderMapper;
@Override
public void createOrder(Order order) {
orderMapper.insert(order);
}
@Override
public List<Order> selectOrderByCode(String commodityCode) {
return orderMapper.selectOrderByCode(commodityCode);
}
}
数据maper
import org.apache.ibatis.annotations.Mapper;
/**
* @author slievrly
*/
@Mapper
public 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
*/
@RestController
public class OrderController {
@Autowired
private 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.DruidDataSource
url: jdbc:mysql://rm-2zetd9474ydd1g5955o.mysql.rds.aliyuncs.com:3306/seata_training?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
username: seata
password: seata123
max-wait: 60000
max-active: 100
min-idle: 10
initial-size: 10
mybatis:
mapper-locations: classpath*:mapper/*Mapper.xml
seata:
enabled: true
application-id: order
tx-service-group: my_test_tx_group
service:
vgroup-mapping:
my_test_tx_group: default
grouplist:
default: 127.0.0.1:8091
disable-global-transaction: false
bootstrap.yml
spring:
application:
name: order-service
main:
allow-bean-definition-overriding: true
cloud:
loadbalancer:
retry:
enabled: false
server:
port: 8082
ribbon:
ConnectTimeout: 100000
ReadTimeout: 1000000
OkToRetryOnAllOperations: 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
*/
@SpringBootApplication
public 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
*/
@Service
public class StorageServiceImpl implements IStorageService {
@Autowired
private StorageMapper storageMapper;
@Override
public int reduceStock(String commodityCode, int count) {
System.out.println("storage-service 当前正在执行的事务xid:" + RootContext.getXID());
return storageMapper.updateStockByCode(commodityCode, count);
}
@Override
public int initStock(String commodityCode, int count) {
return storageMapper.insert(new Storage().setCommodityCode(commodityCode).setCount(count));
}
@Override
public 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
*/
@Mapper
public 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_tbl
WHERE 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_tbl
WHERE 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
*/
@RestController
public class StorageController {
@Autowired
IStorageService 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.DruidDataSource
url: jdbc:mysql://rm-2zetd9474ydd1g5955o.mysql.rds.aliyuncs.com:3306/seata_training?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
username: seata
password: seata123
max-wait: 60000
max-active: 100
min-idle: 10
initial-size: 10
mybatis:
mapper-locations: classpath*:mapper/*Mapper.xml
seata:
enabled: true
application-id: storage
tx-service-group: my_test_tx_group
service:
vgroup-mapping:
my_test_tx_group: default
grouplist:
default: 127.0.0.1:8091
disable-global-transaction: false
bootstrap.yml
spring:
application:
name: storage-service
main:
allow-bean-definition-overriding: true
cloud:
loadbalancer:
retry:
enabled: false
server:
port: 8083
ribbon:
ConnectTimeout: 100000
ReadTimeout: 1000000
OkToRetryOnAllOperations: 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