1、Seata分布式事务
本篇是一篇小白教程
1.1、Seata 简介
Seata
是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata
将为用户提供了 AT
、TCC
、SAGA
和 XA
事务模式,为用户打造一站式的分布式解决方案。
1.2、Seata事务处理模型
关于Seata
事务处理模型,详见http://seata.io/zh-cn/blog/seata-at-mode-design.html
Seata
内部定义了 3
个模块来处理全局事务和分支事务的关系和处理过程,这三个组件分别是:
- Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
- Transaction Manager ™: 事务管理器,控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
- Resource Manager (RM): 资源管理器,控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
简要说说整个全局事务的执行步骤:
TM
向TC
申请开启一个全局事务,TC
创建全局事务后返回全局唯一的XID
,XID
会在全局事务的上下文中传播;RM
向TC
注册分支事务,该分支事务归属于拥有相同XID
的全局事务;TM
向TC
发起全局提交或回滚;TC
调度XID
下的分支事务完成提交或者回滚。
1.3、安装配置环境启动
一、下载
下载地址:https://seata.io/zh-cn/blog/download.html
我当前下载的版本为:1.3.0 (2020-07-14), binary
后续如果存在问题,再对其解释更换
二、安装配置
下载完毕后,解压到目录
主要修改:事务日志存储模式为db
+数据库连接信息+配置中心连接信息
1.3.1、file.conf日志存储模式为db
修改seata-server-1.3.0\seata\conf
目录下的file.conf
文件,建议先先备份原始file.conf
文件,修改conf
目录下的file.conf
配置文件
原:
## transaction log store, only used in seata-server
store {
## store mode: file、db、redis
mode = "file"
## file store property
file {
## store location dir
...
}
}
修改为:
mode = "db"
1.3.2、file.conf配置数据库连接信息
数据库连接信息:
原:
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "mysql"
password = "mysql"
minConn = 5
maxConn = 30
}
修改为:
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "123456"
1.3.3、file.conf新增service配置
代码:单独配置一块,由于我使用1.3.0
版本,没有该配置,所以对其配置
service {
#transaction service group mapping
vgroupMapping.my_test_tx_group = "default"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
如果没有该配置,等下连接seata
会抛出endpoint format should like ip:port
异常信息
- 该篇地址会有介绍,跟
vgroupMapping.my_test_tx_group = "default"
有关,等下进行业务模块配置yaml
就明白了
1.3.4、搭建数据库环境
根据上方连接信息搭建对应数据库:seata
CREATE DATABASE seata;
创建数据库表:前往https://github.com/seata/seata/blob/develop/script/server/db/mysql.sql复制对应代码
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
1.3.5、registry.conf配置注册中心连接信息
修改seata-server-1.3.0\seata\conf
目录下的registry.conf
文件
原:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "file"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
....
}
修改为:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
username = "nacos"
password = "nacos"
}
}
1.3.6、启动
进入seata-server-1.3.0\seata\bin
目录,启动seata-server.bat
注意启动前:需要先启动nacos
服务,否则会抛出连接失败异常
E:\program_jar\seata-server-1.3.0\seata\bin>seata-server.bat
Java HotSpot(TM) 64-Bit Server VM warning: Cannot open file E:\program_jar\seata-server-1.3.0\seata\bin\\../logs/seata_gc.log due to No such file or directory
1.4、业务环境搭建
分布式事务业务说明
这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单, 然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
从下订单->减库存->扣余额->改(订单)状态
1.4.1、三个数据库相关配置
我们需要创建三个业务数据库
- seata_ order:存储订单的数据库;
- seata_ storage:存储库存的数据库;
- seata_ account:存储账户信息的数据库。
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
seata_ order
订单表:
USE seata_order;
CREATE TABLE t_order(
id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
user_id BIGINT(11) DEFAULT NULL COMMENT '用户id',
product_id BIGINT(11) DEFAULT NULL COMMENT '产品id',
count INT(11) DEFAULT NULL COMMENT '数量',
money DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
status INT(1) DEFAULT NULL COMMENT '订单状态:0创建中,1已完结'
)ENGINE=InnoDB AUTO_INCREMENT=7 CHARSET=utf8;
SELECT * FROM t_order;
seata_ storage
库存表:
USE seata_storage;
CREATE TABLE t_storage(
id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
product_id BIGINT(11) DEFAULT NULL COMMENT '产品id',
total INT(11) DEFAULT NULL COMMENT '总库存',
used INT(11) DEFAULT NULL COMMENT '已用库存',
residue INT(11) DEFAULT NULL COMMENT '剩余库存'
)ENGINE=InnoDB AUTO_INCREMENT=7 CHARSET=utf8;
INSERT INTO t_storage(id, product_id, total, used, residue) VALUES(1,1,100,0,100);
SELECT * FROM t_storage;
seata_ account
账户信息表:
USE seata_account;
CREATE TABLE t_account(
id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
user_id BIGINT(11) DEFAULT NULL COMMENT '用户id',
total DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
used DECIMAL(10,0) DEFAULT NULL COMMENT '已用额度',
residue DECIMAL(10,0) DEFAULT 0 COMMENT '剩余可用额度'
)ENGINE=InnoDB AUTO_INCREMENT=7 CHARSET=utf8;
INSERT INTO t_account(id, user_id, total, used, residue) VALUES(1,1,1000,0,1000);
SELECT * FROM t_account;
接着需要为每个数据库,建立一个回滚日志表,如何键,阅读seata-server-1.3.0\seata\conf\README-zh.md
文件,找到undo_log
建表语句指向地址
所以前往https://github.com/seata/seata/blob/develop/script/client/at/db/mysql.sql
在每个业务数据库中执行:
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
1.4.2、订单业务模块搭建
模块名:seata-order-service2001
1.4.2.1、pom
代码:
<dependencies>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<!-- 排除内置的Seata -->
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<!-- 根据您Seata服务的版本进行选择 -->
<version>1.3.0</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web-actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--mysql-druid-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
1.4.2.2、yaml
代码:
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
enabled: true
application-id: ${spring.application.name}
#自定义事务组名称需要与seata-server的file.conf的service组中的vgroupMapping后面那串
#对应
tx-service-group: my_test_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order
username: root
password: 123456
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
1.4.2.3、file.conf、registry.conf复制到模块中
我们需要把配置完毕的seata
服务的file.conf
、registry.conf
复制到resources
目录下
1.4.2.4、domain实体类
数据格式返回类:
package com.migu.springcloud.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>
{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message)
{
this(code,message,null);
}
}
对应数据库订单表实体类:
package com.migu.springcloud.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order
{
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status; //订单状态:0:创建中;1:已完结
}
1.4.2.5、dao接口配置
dao
层:
package com.migu.springcloud.dao;
import com.migu.springcloud.domain.Order;
import org.apache.ibatis.annotations.Param;
public interface OrderDao {
// 1、新建订单
void create(Order order);
// 2、修改订单状态( 0 --> 1)
void update(@Param("userId") Long userId,@Param("status") Integer status);
}
mapper
层:根据上方yaml
配置到对应目录进行创建,也就是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.migu.springcloud.dao.OrderDao">
<resultMap id="BaseResultMap" type="com.migu.springcloud.domain.Order">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="count" property="count" jdbcType="INTEGER"/>
<result column="money" property="money" jdbcType="DECIMAL"/>
<result column="status" property="status" jdbcType="INTEGER"/>
</resultMap>
<insert id="create" parameterType="com.migu.springcloud.domain.Order">
insert into t_order(user_id,product_id,`count`,money,status)
values (#{userId},#{productId},#{count},#{money},0)
</insert>
<update id="update">
update t_order set status=1
where user_id = #{userId} and status=0
</update>
</mapper>
1.4.2.6、service模块
我们除了创建当前订单模块的OrderService
还需要创建AccountService
、StorageService
因为我们将通过openFeign
进行远程接口调用
一、OrderService
package com.migu.springcloud.service;
import com.migu.springcloud.domain.Order;
public interface OrderService {
void create(Order order);
}
二、OrderServiceImpl
package com.migu.springcloud.service.impl;
import com.migu.springcloud.dao.OrderDao;
import com.migu.springcloud.domain.Order;
import com.migu.springcloud.service.AccountService;
import com.migu.springcloud.service.OrderService;
import com.migu.springcloud.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 测试一次订单交易流程,涉及多个数据库
* 下订单 --> 减库存 --> 减余额 --> 改状态
* @param order
*/
@Override
public void create(Order order) {
log.info("开始新建订单....");
orderDao.create(order);
log.info("订单微服务 调用 库存微服务,做扣减......");
storageService.decrease(order.getProductId(),order.getCount());
log.info("执行扣减完毕......");
log.info("订单微服务 调用 账户微服务,做付款......");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("执行付款完毕......");
log.info("修改订单状态.....");
orderDao.update(order.getUserId(),0);
log.info("订单流程结束.....");
}
}
三、StorageService
package com.migu.springcloud.service;
import com.migu.springcloud.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "seata-storage-service")
public interface StorageService {
/**
* 根据productID对指定产品进行减库存count操作
*/
@PostMapping("storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId,
@RequestParam("count") Integer count);
}
四、AccountService
package com.migu.springcloud.service;
import com.migu.springcloud.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
@FeignClient(value = "seata-account-service")
public interface AccountService {
/**
* 根据userId对指定用户进行付款money操作
*/
@PostMapping("account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money);
}
1.4.2.7、controller层
代码:
package com.migu.springcloud.controller;
import com.migu.springcloud.domain.CommonResult;
import com.migu.springcloud.domain.Order;
import com.migu.springcloud.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("order/create")
public CommonResult create(Order order) {
orderService.create(order);
return new CommonResult(200,"订单OK");
}
}
1.4.2.8、自定义数据源配置
由于我们需要采用seata
做事务处理,所以数据源就需要采用seata
实现:
package com.migu.springcloud.config;
import com.alibaba.druid.pool.DruidDataSource;
// 注意这边使用`seata`
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
/**
* 使用Seata对数据源进行代理
*/
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
1.4.2.9、主启动类
代码:
package com.migu.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableDiscoveryClient
@EnableFeignClients
// 我们上方自定义了数据源,由于SpringBoot mybatis启动器有默认加载,所以对其排除
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SeataOrderMainApp2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMainApp2001.class,args);
}
}
这样就测试环境就搭建完毕了,其余两个模块步骤也类似
1.4.3、库存业务模块搭建
1.4.3.1、同一个套路
模块取名为:seata-storage-service2002
pom
依赖:与上方一样
yaml
配置:需要修改三处
- 端口号改为
2002
- 服务名:与订单业务的
service
模块的StorageService
的远程接口调用的服务名保持一致,所以为:seata-storage-service
- 连接数据库表修改为
seata_storage
依然需要复制file.conf
、registry.conf
到项目中的resources
目录下
自定义数据源一样,就那个config
下的DataSourceProxyConfig
类
主启动类也是类似
其余就是业务相关的测试了
1.4.3.2、domain实体类
代码:
package com.migu.springcloud.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage {
private Long id;
/**
* 产品id
*/
private Long productId;
/**
* 总库存
*/
private Integer total;
/**
* 已用库存
*/
private Integer used;
/**
* 剩余库存
*/
private Integer residue;
}
CommonResult
数据返回类,也同上方一样
1.4.3.3、dao接口配置
代码:
package com.migu.springcloud.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface StorageDao {
//扣减库存
void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
mapper
层:
<?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.migu.springcloud.dao.StorageDao">
<resultMap id="BaseResultMap" type="com.migu.springcloud.domain.Storage">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="total" property="total" jdbcType="INTEGER"/>
<result column="used" property="used" jdbcType="INTEGER"/>
<result column="residue" property="residue" jdbcType="INTEGER"/>
</resultMap>
<update id="decrease">
UPDATE
t_storage
SET
used = used + #{count},residue = residue - #{count}
WHERE
product_id = #{productId}
</update>
</mapper>
1.4.3.4、service模块
代码:
package com.migu.springcloud.service;
public interface StorageService {
/**
* 扣减库存
*/
void decrease(Long productId, Integer count);
}
实现类:
- 这边老师使用了不一样的日志进行输出信息
package com.migu.springcloud.service.impl;
import com.migu.springcloud.dao.StorageDao;
import com.migu.springcloud.service.StorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class StorageServiceImpl implements StorageService {
private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class);
@Resource
private StorageDao storageDao;
/**
* 扣减库存
*/
@Override
public void decrease(Long productId, Integer count) {
LOGGER.info("------->storage-service中扣减库存开始");
storageDao.decrease(productId,count);
LOGGER.info("------->storage-service中扣减库存结束");
}
}
1.4.3.5、controller层
代码:
- 这些
controller
资源路径也就根据订单业务模块接口调用对应配置
package com.migu.springcloud.controller;
import com.migu.springcloud.domain.CommonResult;
import com.migu.springcloud.service.StorageService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class StorageController {
@Resource
private StorageService storageService;
/**
* 扣减库存
*/
@RequestMapping("/storage/decrease")
public CommonResult decrease(Long productId, Integer count) {
storageService.decrease(productId, count);
return new CommonResult(200,"扣减库存成功!");
}
}
1.4.4、账户业务模块搭建
不想多说啥了,这边就提供测试环境
1.4.4.1、domain实体类
代码:
package com.migu.springcloud.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long id;
/**
* 用户id
*/
private Long userId;
/**
* 总额度
*/
private BigDecimal total;
/**
* 已用额度
*/
private BigDecimal used;
/**
* 剩余额度
*/
private BigDecimal residue;
}
CommonResult
别忘记了
1.4.4.2、dao接口配置
代码:
package com.migu.springcloud.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
@Mapper
public interface AccountDao {
/**
* 扣减账户余额
*/
void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
mapper
层:
<?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.migu.springcloud.dao.AccountDao">
<resultMap id="BaseResultMap" type="com.migu.springcloud.domain.Account">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="total" property="total" jdbcType="DECIMAL"/>
<result column="used" property="used" jdbcType="DECIMAL"/>
<result column="residue" property="residue" jdbcType="DECIMAL"/>
</resultMap>
<update id="decrease">
UPDATE t_account
SET
residue = residue - #{money},used = used + #{money}
WHERE
user_id = #{userId};
</update>
</mapper>
1.4.4.3、service模块
代码:
package com.migu.springcloud.service;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
public interface AccountService {
/**
* 扣减账户余额
* @param userId 用户id
* @param money 金额
*/
void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
实现类:
package com.migu.springcloud.service.impl;
import com.migu.springcloud.dao.AccountDao;
import com.migu.springcloud.service.AccountService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
/**
*/
@Service
public class AccountServiceImpl implements AccountService {
private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);
@Resource
AccountDao accountDao;
/**
* 扣减账户余额
*/
@Override
public void decrease(Long userId, BigDecimal money) {
LOGGER.info("------->account-service中扣减账户余额开始");
accountDao.decrease(userId,money);
LOGGER.info("------->account-service中扣减账户余额结束");
}
}
1.4.4.4、controller层
代码:
package com.migu.springcloud.controller;
import com.migu.springcloud.domain.CommonResult;
import com.migu.springcloud.service.AccountService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.math.BigDecimal;
@RestController
public class AccountController {
@Resource
AccountService accountService;
/**
* 扣减账户余额
*/
@RequestMapping("/account/decrease")
public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
accountService.decrease(userId,money);
return new CommonResult(200,"扣减账户余额成功!");
}
}
1.5、分布式事务测试
1.5.1、数据库当前基本情况
一、**seata_order
数据库的t_order
**表
SELECT * FROM t_order
二、**seata_storage
数据库的t_storage
**表
SELECT * FROM t_storage
三、**seata_account
数据库的t_account
**表
SELECT * FROM t_account;
感觉老师这边字段写错了好像
1.5.2、演示正常下单
正常下单 :用户ID
未1
的账户,购买产品ID
为1
的产品,购买十件,花了100
块
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
打印:
{"code":200,"message":"订单OK","data":null}
查看数据库状态:
生成了一个这样的订单:userId
为1
的用户,购买了product_id
的产品,购买了10
件,花费了100
元,status
状态为1
,表示成功
库存表:
账户信息表:
查看IDEA
控制台输出:
1.5.3、演示超时异常
在最后一步操作,AccountServiceImpl
的付款操作进行,超时设置
/**
* 扣减账户余额
*/
@Override
public void decrease(Long userId, BigDecimal money) {
LOGGER.info("------->account-service中扣减账户余额开始");
// openFeign默认的超时时间为 1s ,如果超时了,测试是否会进行回滚
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
accountDao.decrease(userId,money);
LOGGER.info("------->account-service中扣减账户余额结束");
}
再对其测试:
在数据进行查看时,发现库存减少了,钱也扣了
但是订单状态却为0
:
上面情况还是算很好,但我们需要考虑每种情况,那如何解决了,加一个@GlobalTransactional
注解即可
1.5.4、异常回滚解决方案
只需在处理所有业务逻辑的总方法上,加上一个@GlobalTransactional
注解
import io.seata.spring.annotation.GlobalTransactional;
/**
* 测试一次订单交易流程,涉及多个数据库
* 下订单 --> 减库存 --> 减余额 --> 改状态
* @param order
*/
@Override
// name: 唯一自定义 rollbackFor: 处理所有异常
@GlobalTransactional(name = "seata-create-order",rollbackFor = Exception.class)
public void create(Order order) {
...
}
此时运行测试时,我抛出了一个异常:
### Error updating database. Cause: java.sql.SQLException: java.sql.SQLException: io.seata.common.exception.NotSupportYetException: undo_log needs to contain the primary key.
### The error may involve com.migu.springcloud.dao.OrderDao.create-Inline
### The error occurred while setting parameters
### SQL: insert into t_order(id,user_id,product_id,count,money,status) values (null,?,?,?,?,0)
### Cause: java.sql.SQLException: java.sql.SQLException: io.seata.common.exception.NotSupportYetException: undo_log needs to contain the primary key.
; uncategorized SQLException; SQL state [null]; error code [0];
说undo_log
表没有主键,我确实没有主键,所以此时我为数据库的每个undo_log
表都添加了id
主键
同时又要为id
设置为自增
此时需要重启服务,之后再测试运行,会发现,抛出了连接超时异常:
此时再次查看数据库,啥也没有,证明回滚成功,没有创建订单