seata
如果不是很了解的话可以直接到官网查看官方文档说明:http://seata.io/zh-cn/docs/overview/what-is-seata.html
如果你还对SpringBoot
、Dubbo
、Nacos
、Seata
、Mybatis
不是很了解的话,这里我为大家整理个它们的官网网站,如下
-
SpringBoot:https://spring.io/projects/spring-boot
-
Seata:https://seata.io/zh-cn/
本例子主要模块为以下版本:
springboot版本:2.2.5.RELEASE
spring-cloud版本:Hoxton.SR3
spring-cloud-alibaba版本:2.2.1.RELEASE
seata版本:1.4.0
nacos版本:1.4.0
如果还没安装好nacos环境的可以参考文档:nacos下载与安装
整个业务逻辑由4个微服务提供支持:
- 库存服务(storage):扣除给定商品的存储数量。
- 订单服务(order):根据购买请求创建订单。
- 帐户服务(account):借记用户帐户的余额。
- 业务服务(business):处理业务逻辑。
- 公共模块(common):公共模块。
一、下载并安装seata-server
1,下载seata
https://github.com/seata/seata/releases
2,解压并移动到 /usr/local/ 目录下
tar -zxvf seata-server-1.4.0.tar.gz
mv seata seata-1.4.0
mv seata-1.4.0/ /usr/local/
cd /usr/local/seata-1.4.0/
3,修改 conf/registry.conf 配置
目前seata支持如下的file、nacos 、apollo、zk、consul的注册中心和配置中心。这里我们以nacos
为例。
将 type 改为 nacos
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10
nacos {
application = "seata-server"
serverAddr = "192.168.2.112:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "192.168.2.112:8848"
namespace = ""
group = "SEATA_GROUP"
username = ""
password = ""
}
}
- serverAddr = “192.168.2.112:8848” :nacos 的地址
- namespace = “” :nacos的命名空间默认为``
- cluster = “default” :集群设置未默认
default
4,修改 config.txt 文件
config.txt 配置文件需要自行下载,下载后放到seata根目录下面,下载地址:https://github.com/seata/seata/blob/develop/script/config-center/config.txt
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
store.mode=file
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true
store.db.user=username
store.db.password=password
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.database=0
store.redis.password=null
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
配置的详细说明参考官网:http://seata.io/zh-cn/docs/user/configurations.html
这里主要修改了如下几项:
- store.mode :存储模式 默认file 这里我修改为db 模式 ,并且需要三个表
global_table
、branch_table
和lock_table
- store.db.driverClassName=com.mysql.jdbc.Driver
- store.db.db-type=mysql : 存储数据库的类型为
mysql
- store.db.url=jdbc:mysql://192.168.2.112:3306/seata_config?useUnicode=true : 修改为自己的数据库
url
、port
、数据库名称
- store.db.user=root :数据库的账号
- store.db.password=123456 :数据库的密码
- service.vgroupMapping.order-seata-tx-group=default
- service.vgroupMapping.account-seata-tx-group=default
- service.vgroupMapping.storage-seata-tx-group=default
- service.vgroupMapping.business-seata-tx-group=default
注意:如果mysql版本为8.0以上的版本,需要使用8.x以上的驱动包,并且将 store.db.driverClassName=com.mysql.jdbc.Driver 配置改为 store.db.driverClassName=com.mysql.cj.jdbc.Driver
然后直接到seata/lib目录下,将8版本的jar包粘贴到目录下
db模式下的所需的三个表的数据库脚本下载地址:https://github.com/seata/seata/blob/develop/script/server/db/mysql.sql
5、将 Seata 配置添加到 Nacos 中
脚本下载地址:https://github.com/seata/seata/blob/develop/script/config-center/nacos/nacos-config.sh
下载后将脚本放到 seata bin/ 目录下,并将脚本权限改成具有可执行权限
cd bin
chmod 755 nacos-config.sh
执行 nacos-config.sh 脚本,将配置上传到nacos
sh ./nacos-config.sh
看到输出如下内容,说明脚本已经执行成功
登陆到nacos控制台也能够看到上传的配置内容
6、使用db模式启动 seata
sh seata-server.sh -h 192.168.2.112 -p 8091 -m db
Options:
--host, -h
The host to bind,主机绑定ip,nacos的注册地址
Default: 0.0.0.0
--port, -p
The port to listen.
Default: 8091
--storeMode, -m
log store mode : file、db
Default: file
--help
Server端存储模式(store.mode)现有file、db、redis三种,file模式无需改动,直接启动即可,下面专门讲下db和redis启动步骤。
注: file模式为单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高;
db模式为高可用模式,全局事务会话信息通过db共享,相应性能差些;
redis模式Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险,请提前配置合适当前场景的redis持久化配置.
二、代码构建
项目地址:
https://gitee.com/hwm0717/springcloud_seata_demo
4个微服务提供支持:
- 库存服务(storage):扣除给定商品的存储数量。
- 订单服务(order):根据购买请求创建订单。
- 帐户服务(account):借记用户帐户的余额。
- 业务服务(business):处理业务逻辑。
- 公共模块(common):公共模块。
我们只需要在业务处理的方法handleBusiness添加一个注解 @GlobalTransactional 即可实现分布式事物的处理
请求逻辑架构:
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">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>seata.demo</groupId>
<artifactId>springcloud_seata_demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
<springboot.version>2.2.5.RELEASE</springboot.version>
<mysql.connector.java.version>8.0.21</mysql.connector.java.version>
<druid-spring-boot-starter.version>1.1.22</druid-spring-boot-starter.version>
<seata.version>1.4.0</seata.version>
<!-- Spring Settings -->
<spring-cloud.version>Hoxton.SR3</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
</properties>
<modules>
<module>business</module>
<module>account</module>
<module>order</module>
<module>storage</module>
<module>common</module>
</modules>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- springboot 整合web组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- mybatisplus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>
<!-- mybatis分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.12</version>
</dependency>
<!--引入数据库连接 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.connector.java.version}</version>
</dependency>
<!-- 使用 druid数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- swagger整合:https://github.com/SpringForAll/spring-boot-starter-swagger -->
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>1.7.0.RELEASE</version>
</dependency>
<!-- 项目中引入seata依赖 begin -->
<!-- 如果你的微服务是dubbo引入依赖 -->
<!-- <dependency>-->
<!-- <groupId>io.seata</groupId>-->
<!-- <artifactId>seata-spring-boot-starter</artifactId>-->
<!-- <version>${seata.version}</version>-->
<!-- </dependency>-->
<!-- 如果你是springcloud:-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.2.0.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>
<!-- 项目中引入seata依赖 end -->
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${springboot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
1,库存服务:
@Data
public class TStorage {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String commodityCode;
private String name;
private Integer count;
}
public interface TStorageMapper extends BaseMapper<TStorage> {
/**
* 扣减商品库存
* @Param: commodityCode 商品code count扣减数量
* @Return:
*/
int decreaseStorage(@Param("commodityCode") String commodityCode, @Param("count") Integer count);
}
@RestController
@RequestMapping("/storage")
@Slf4j
public class StorageServiceImpl extends ServiceImpl<TStorageMapper, TStorage> {
@PostMapping("/decreaseStorage")
@GlobalTransactional
@Transactional
public ObjectResponse decreaseStorage(@RequestBody CommodityDTO commodityDTO) {
log.info("seata全局xid={}", RootContext.getXID());
int storage = baseMapper.decreaseStorage(commodityDTO.getCommodityCode(), commodityDTO.getCount());
ObjectResponse<Object> response = new ObjectResponse<>();
if (storage > 0) {
response.setStatus(RspStatusEnum.SUCCESS.getCode());
response.setMessage(RspStatusEnum.SUCCESS.getMessage());
return response;
}
response.setStatus(RspStatusEnum.FAIL.getCode());
response.setMessage(RspStatusEnum.FAIL.getMessage());
return response;
}
}
@SpringBootApplication
@EnableFeignClients
@MapperScan("seata.demo.storage.mappers")
public class StorageApplication {
public static void main(String[] args) {
SpringApplication.run(StorageApplication.class, args);
}
}
<?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="seata.demo.storage.mappers.TStorageMapper">
<update id="decreaseStorage">
update t_storage set count = count-${count} where commodity_code = #{commodityCode}
</update>
</mapper>
2,订单服务
@Data
public class TOrder {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String orderNo;
private String userId;
private String commodityCode;
private Integer count;
private Double amount;
}
public interface TOrderMapper extends BaseMapper<TOrder> {
/**
* 创建订单
* @Param: order 订单信息
* @Return:
*/
void createOrder(@Param("order") TOrder order);
}
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> {
@Autowired
private AccountServiceFeign accountServiceFeign;
@PostMapping("/createOrder")
@GlobalTransactional
@Transactional
public ObjectResponse<OrderDTO> createOrder(@RequestBody OrderDTO orderDTO) {
log.info("seata全局xid={}", RootContext.getXID());
ObjectResponse<OrderDTO> response = new ObjectResponse<>();
//扣减用户账户
AccountDTO accountDTO = new AccountDTO();
accountDTO.setUserId(orderDTO.getUserId());
accountDTO.setAmount(orderDTO.getOrderAmount());
ObjectResponse objectResponse = accountServiceFeign.decreaseAccount(accountDTO);
//生成订单号
orderDTO.setOrderNo(UUID.randomUUID().toString().replace("-",""));
//生成订单
TOrder tOrder = new TOrder();
BeanUtils.copyProperties(orderDTO,tOrder);
tOrder.setCount(orderDTO.getOrderCount());
tOrder.setAmount(orderDTO.getOrderAmount().doubleValue());
try {
baseMapper.createOrder(tOrder);
} catch (Exception e) {
e.printStackTrace();
response.setStatus(RspStatusEnum.FAIL.getCode());
response.setMessage(RspStatusEnum.FAIL.getMessage());
return response;
}
if (objectResponse.getStatus() != 200) {
response.setStatus(RspStatusEnum.FAIL.getCode());
response.setMessage(RspStatusEnum.FAIL.getMessage());
return response;
}
response.setStatus(RspStatusEnum.SUCCESS.getCode());
response.setMessage(RspStatusEnum.SUCCESS.getMessage());
return response;
}
}
@SpringBootApplication
@EnableFeignClients
@MapperScan("seata.demo.order.mappers")
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
<?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="seata.demo.order.mappers.TOrderMapper">
<!--创建订单-->
<insert id="createOrder" parameterType="seata.demo.order.entity.TOrder">
insert into t_order values(null,#{order.orderNo},#{order.userId},#{order.commodityCode},${order.count},${order.amount})
</insert>
</mapper>
3,账户服务
@Data
public class TAccount {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String userId;
private Double amount;
}
public interface TAccountMapper extends BaseMapper<TAccount> {
/**
* 减少账户余额
* @param userId
* @param amount
* @return
*/
int decreaseAccount(@Param("userId") String userId, @Param("amount") Double amount);
}
@RestController
@RequestMapping("/account")
@Slf4j
public class AccountServiceImpl extends ServiceImpl<TAccountMapper, TAccount> {
@PostMapping("/decreaseAccount")
@GlobalTransactional
@Transactional
public ObjectResponse decreaseAccount(@RequestBody AccountDTO accountDTO) {
log.info("seata全局xid={}", RootContext.getXID());
int account = baseMapper.decreaseAccount(accountDTO.getUserId(), accountDTO.getAmount().doubleValue());
ObjectResponse<Object> response = new ObjectResponse<>();
if (account > 0) {
response.setStatus(RspStatusEnum.SUCCESS.getCode());
response.setMessage(RspStatusEnum.SUCCESS.getMessage());
return response;
}
response.setStatus(RspStatusEnum.FAIL.getCode());
response.setMessage(RspStatusEnum.FAIL.getMessage());
return response;
}
}
@SpringBootApplication
@EnableFeignClients
@MapperScan("seata.demo.account.mappers")
public class AccountApplication {
public static void main(String[] args) {
SpringApplication.run(AccountApplication.class, args);
}
}
<?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="seata.demo.account.mappers.TAccountMapper">
<update id="decreaseAccount">
update t_account set amount = amount-${amount} where user_id = #{userId}
</update>
</mapper>
4,业务服务
@RestController
@RequestMapping("/business")
@Slf4j
public class BusinessServiceImpl {
@Autowired
private StorageServiceFeign storageServiceFeign;
@Autowired
private OrderServiceFeign orderServiceFeign;
/**
* 处理业务逻辑 正常的业务逻辑
*
* @Param:
* @Return:
*/
@GlobalTransactional
@PostMapping("/handleBusiness")
@ApiOperation("处理业务逻辑 正常的业务逻辑")
public ObjectResponse handleBusiness(@RequestBody BusinessDTO businessDTO) {
log.info("开始全局事务,XID = " + RootContext.getXID());
ObjectResponse<Object> objectResponse = new ObjectResponse<>();
//1、扣减库存
CommodityDTO commodityDTO = new CommodityDTO();
commodityDTO.setCommodityCode(businessDTO.getCommodityCode());
commodityDTO.setCount(businessDTO.getCount());
ObjectResponse storageResponse = storageServiceFeign.decreaseStorage(commodityDTO);
//2、创建订单
OrderDTO orderDTO = new OrderDTO();
orderDTO.setUserId(businessDTO.getUserId());
orderDTO.setCommodityCode(businessDTO.getCommodityCode());
orderDTO.setOrderCount(businessDTO.getCount());
orderDTO.setOrderAmount(businessDTO.getAmount());
ObjectResponse<OrderDTO> response = orderServiceFeign.createOrder(orderDTO);
if (storageResponse.getStatus() != 200 || response.getStatus() != 200) {
throw new DefaultException(RspStatusEnum.FAIL);
}
objectResponse.setStatus(RspStatusEnum.SUCCESS.getCode());
objectResponse.setMessage(RspStatusEnum.SUCCESS.getMessage());
objectResponse.setData(response.getData());
return objectResponse;
}
/**
* 出处理业务服务,出现异常回顾
*
* @param businessDTO
* @return
*/
@GlobalTransactional
@PostMapping("/handleBusiness2")
@ApiOperation("出处理业务服务,出现异常回顾")
@Transactional
public ObjectResponse handleBusiness2(@RequestBody BusinessDTO businessDTO) {
log.info("开始全局事务,XID = " + RootContext.getXID());
ObjectResponse<Object> objectResponse = new ObjectResponse<>();
//1、扣减库存
CommodityDTO commodityDTO = new CommodityDTO();
commodityDTO.setCommodityCode(businessDTO.getCommodityCode());
commodityDTO.setCount(businessDTO.getCount());
ObjectResponse storageResponse = storageServiceFeign.decreaseStorage(commodityDTO);
//2、创建订单
OrderDTO orderDTO = new OrderDTO();
orderDTO.setUserId(businessDTO.getUserId());
orderDTO.setCommodityCode(businessDTO.getCommodityCode());
orderDTO.setOrderCount(businessDTO.getCount());
orderDTO.setOrderAmount(businessDTO.getAmount());
ObjectResponse<OrderDTO> response = orderServiceFeign.createOrder(orderDTO);
// 打开注释测试事务发生异常后,全局回滚功能
if (true) {
throw new RuntimeException("测试抛异常后,分布式事务回滚!");
}
if (storageResponse.getStatus() != 200 || response.getStatus() != 200) {
throw new DefaultException(RspStatusEnum.FAIL);
}
objectResponse.setStatus(RspStatusEnum.SUCCESS.getCode());
objectResponse.setMessage(RspStatusEnum.SUCCESS.getMessage());
objectResponse.setData(response.getData());
return objectResponse;
}
}
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableSwagger2Doc
@EnableFeignClients
public class BusinessApplication {
public static void main(String[] args) {
SpringApplication.run(BusinessApplication.class, args);
}
}
5,公共模块
@Data
public class AccountDTO implements Serializable {
private Integer id;
private String userId;
private BigDecimal amount;
}
@Data
public class BusinessDTO implements Serializable {
private String userId;
private String commodityCode;
private String name;
private Integer count;
private BigDecimal amount;
}
@Data
public class CommodityDTO implements Serializable {
private Integer id;
private String commodityCode;
private String name;
private Integer count;
}
@Data
public class OrderDTO implements Serializable {
private String orderNo;
private String userId;
private String commodityCode;
private Integer orderCount;
private BigDecimal orderAmount;
}
public enum RspStatusEnum {
/**
* SUCCESS
*/
SUCCESS(200,"成功"),
/**
* Fail rsp status enum.
*/
FAIL(999,"失败"),
/**
* Exception rsp status enum.
*/
EXCEPTION(500,"系统异常");
private int code;
private String message;
RspStatusEnum(int code, String message) {
this.code = code;
this.message = message;
}
/**
* Gets code.
*
* @return the code
*/
public int getCode() {
return code;
}
/**
* Gets message.
*
* @return the message
*/
public String getMessage() {
return message;
}
}
public class DefaultException extends RuntimeException{
private RspStatusEnum rspStatusEnum;
public DefaultException(String message, Throwable cause) {
super(message, cause);
}
public DefaultException(RspStatusEnum rspStatusEnum) {
super(rspStatusEnum.getMessage());
this.rspStatusEnum = rspStatusEnum;
}
public DefaultException(RspStatusEnum rspStatusEnum, Throwable cause) {
super(rspStatusEnum.getMessage(), cause);
this.rspStatusEnum = rspStatusEnum;
}
public RspStatusEnum getRspStatusEnum() {
return rspStatusEnum;
}
public void setRspStatusEnum(RspStatusEnum rspStatusEnum) {
this.rspStatusEnum = rspStatusEnum;
}
}
@FeignClient(value = "account", path = "/account")
public interface AccountServiceFeign {
/**
* 从账户扣钱
*/
@ApiOperation("从账户扣钱")
@PostMapping("/decreaseAccount")
ObjectResponse decreaseAccount(@RequestBody AccountDTO accountDTO);
}
@FeignClient(value = "order", path = "/order")
public interface OrderServiceFeign {
/**
* 创建订单
*/
@ApiOperation("创建订单")
@PostMapping("/createOrder")
ObjectResponse<OrderDTO> createOrder(@RequestBody OrderDTO orderDTO);
}
@FeignClient(value = "storage", path = "/storage")
public interface StorageServiceFeign {
/**
* 扣减库存
*/
@ApiOperation("扣减库存")
@PostMapping("/decreaseStorage")
ObjectResponse decreaseStorage(@RequestBody CommodityDTO commodityDTO);
}
@Data
public class BaseResponse implements Serializable {
private int status = 200;
private String message;
}
@Data
public class ObjectResponse<T> extends BaseResponse implements Serializable {
private T data;
}
6,配置文件
主要三个配置文件,每个应用里面复制一份,内容基本一致,只需要根据自己配置改下扫描的包和应用名称即可
application-global.properties
#服务端口号
server.port=8080
spring.profiles.include=global,seata
#服务名称
spring.application.name=business
#nacos
spring.cloud.nacos.discovery.server-addr=192.168.2.112:8848
#日志配置
logging.level.root=info
logging.file.path=C:\\www\\logs\\${spring.application.name}
#数据库连接信息
spring.datasource.url=jdbc:mysql://192.168.2.112:3306/springcloud_seata_demo?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&failOverReadOnly=false&useSSL=true&serverTimezone=Asia/Shanghai&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=123456
#swagger
swagger.enabled=true
swagger.title=业务逻辑处理
swagger.description=业务逻辑处理
swagger.version=1.0.0
swagger.base-package=seata.demo.business.service
#设置feign超时时间,解决第一次调用时超时问题
feign.client.config.default.connectTimeout=5000
feign.client.config.default.readTimeout=5000
application-seata.yml
#====================================Seata Config===============================================
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: ${spring.application.name}-seata-tx-group # 事务群组(可以每个应用独立取名,也可以使用相同的名字)
registry:
file:
name: file.conf
type: nacos
nacos:
server-addr: 192.168.2.112:8848
namespace:
cluster: default
config:
file:
name: file.conf
type: nacos
nacos:
namespace:
server-addr: 192.168.2.112:8848
application.properties
#服务端口号
server.port=8080
#引入外部配置文件
spring.profiles.include=global,seata
#服务名称
spring.application.name=business
#nacos
spring.cloud.nacos.discovery.server-addr=192.168.2.112:8848
#日志配置
logging.level.root=info
logging.file.path=C:\\www\\logs\\${spring.application.name}
#数据库连接信息
spring.datasource.url=jdbc:mysql://192.168.2.112:3306/springcloud_seata_demo?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&failOverReadOnly=false&useSSL=true&serverTimezone=Asia/Shanghai&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=123456
#swagger
swagger.enabled=true
swagger.title=业务逻辑处理
swagger.description=业务逻辑处理
swagger.version=1.0.0
swagger.base-package=seata.demo.business.service
#设置feign超时时间,解决第一次调用时超时问题
feign.client.config.default.connectTimeout=5000
feign.client.config.default.readTimeout=5000
7,准备数据库
注意: MySQL必须使用InnoDB engine.
创建数据库 并导入数据库脚本
这里为了简化我将这个三张表创建到一个库中,使用是三个数据源来实现。
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`amount` double(14,2) DEFAULT '0.00',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_account
-- ----------------------------
INSERT INTO `t_account` VALUES ('1', '1', '4000.00');
-- ----------------------------
-- Table structure for t_order
-- ----------------------------
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_no` varchar(255) DEFAULT NULL,
`user_id` varchar(255) DEFAULT NULL,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT '0',
`amount` double(14,2) DEFAULT '0.00',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=64 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_order
-- ----------------------------
-- ----------------------------
-- Table structure for t_storage
-- ----------------------------
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_storage
-- ----------------------------
INSERT INTO `t_storage` VALUES ('1', 'C201901140001', '水杯', '1000');
-- ----------------------------
-- Table structure for undo_log
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
-- ----------------------------
DROP TABLE IF EXISTS `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;
-- ----------------------------
-- Records of undo_log
-- ----------------------------
SET FOREIGN_KEY_CHECKS=1;
三、测试
使用swagger发送一个下单请求,调用 /business/handleBusiness 方法:http://localhost:8080/swagger-ui.html#/business-service-impl/handleBusinessUsingPOST
参数内容:
{
"amount": 100,
"commodityCode": "C201901140001",
"count": 100,
"name": "水杯",
"userId": "1"
}
返回结果:
{
"status": 200,
"message": "成功",
"data": null
}
控制台打印
事物提交成功,查看数据变化
数据没有问题,全部按照正常流程提交了
2,测试数据回滚:
参数一样,通过swagger调用 /business/handleBusiness2 异常回滚方法
返回结果:
控制台输出
再次查看数据库数据,已经回滚,和上面的数据一致。