在分布式架构的系统中,通常会存在分布式事务的问题,也就是一次业务操作可能需要跨多个数据源或多个系统进行,这就是分布式事务问题,多个数据源在物理上是分开的,但是在业务上必须保证是整体的,否则就会出现错误的数据。
Seata官网:http://seata.io/zh-cn/
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata在分布式事务处理过程中,会用到一个全局唯一事务ID(Transaction ID)+三个组件(TC、TM、RM)。
TC(事务协调者):维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM(事务管理器):定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM(资源管理器):管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
Seata分布式事务处理流程:
这块,阳哥举了个例子,感觉不错,记录一下。把每个Microservice看做一个组,每个组里有若干学生,把TC当做阳哥(授课老师),把TM当做班主任。
下载地址:https://github.com/seata/seata/releases,有Windows和Linux版本。下载之后,进入conf目录下,修改file.conf的service模块和store模块,目的是:自定义事务组名称、事务日志存储模式改为DB、配置数据库连接信息。修改registry.conf的registry模块。
service模块:修改vgroup_mapping.my_test_tx_group的值为***_tx_group,其中***表示任意英文字符(高版本没有service模块,这个我没有研究)。
store模块:修改mode="db",表示使用db存储日志信息。在下面的db模块中,写上自己的url、user、password,其他的默认。
registry模块:修改type="nacos",nacos模块指明nacos连接信息。
新建一个名称为seata的数据库,在Seata的GitHub地址,找到script/server/db/mysql.sql文件,在seata数据库里执行SQL完成初始化操作。建议使用MySQL 5.7版本,MySQL 8可能不支持。
这里我还是用Docker吧。
因为要修改配置文件,但是容器内vi命令无法使用,之前说过挂载的方式(这里我也用过挂载方式,不过出问题了,一直没有解决),这里介绍另一种方式:将容器的文件复制到容器外,在容器外修改完成后,再复制进去。这里要修改的文件有:registry.conf和file.conf。
启动MySQL 5.7的镜像,创建一个名称为seata的数据库,脚本可以在Seata的GitHub地址获取,找到script/server/db/mysql.sql文件,在seata数据库里执行SQL完成初始化操作。Docker启动nacos-server,最后启动seata-server。
[root@localhost ~]# docker pull seataio/seata-server:1.2.0 # 下载seata-server 1.2.0的镜像
[root@localhost ~]# docker run -d -h 192.168.0.123 -p 8091:8091 seataio/seata-server:1.2.0 # 运行seata-server,记得指定-h,否则后面找不到服务
[root@localhost /]# docker exec -it 容器id sh # 进到容器里面
/seata-server # ls -l # 查看文件
total 0
drwxr-xr-x 4 root root 32 Jan 1 1970 classes
drwxr-xr-x 1 root root 57 May 10 13:32 libs
drwxr-xr-x 4 root root 151 Jan 1 1970 resources
drwxr-xr-x 2 root root 23 Jul 18 09:36 sessionStore
/seata-server # cd resources/ # 进导resources文件夹下
/seata-server/resources # ls -l # 查看文件
total 24
drwxr-xr-x 3 root root 22 Jan 1 1970 META-INF
-rw-r--r-- 1 root root 1327 Jan 1 1970 README-zh.md
-rw-r--r-- 1 root root 1324 Jan 1 1970 README.md
-rw-r--r-- 1 root root 1165 Jan 1 1970 file.conf
-rw-r--r-- 1 root root 2929 Jan 1 1970 file.conf.example
drwxr-xr-x 3 root root 19 Jan 1 1970 io
-rw-r--r-- 1 root root 2152 Jan 1 1970 logback.xml
-rw-r--r-- 1 root root 1631 Jan 1 1970 registry.conf
/seata-server/resources # exit # 退出容器,这里进一遍容器的目的是查看一下配置文件的位置,等下复制时候要用
[root@localhost /]# docker cp 容器id:/seata-server/resources/file.conf / # 拷贝容器内/seata-server/resources/file.conf文件到容器外的根路径下
[root@localhost /]# docker cp 容器id:/seata-server/resources/registry.conf / # 拷贝容器内/seata-server/resources/registry.conf文件到容器外的根路径下
然后,我们在虚拟机的根路径下就看到了registry.conf和file.conf了,对它们进行修改,这里我去掉了多余的部分。
file.conf
## transaction log store, only used in seata-server
store {
## store mode: file、db
mode = "db"
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://192.168.0.123:3306/seata"
user = "root"
password = "root"
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "192.168.0.123:8848"
namespace = ""
cluster = "default"
username = ""
password = ""
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
file {
name = "file.conf"
}
}
配置文件已经修改完了,我们将文件再复制到容器里面。
[root@localhost /]# docker cp file.conf 容器id:/seata-server/resources/file.conf
[root@localhost /]# docker cp registry.conf 容器id:/seata-server/resources/registry.conf
[root@localhost /]# docker restart dc50d1acd7ed # 将容器重启一下,这样就以新的配置文件进行启动了
[root@localhost /]# docker logs -f 容器id # 查看容器启动日志,如果没有报错,说明启动成功了
回到Nacos的控制台的服务列表查看服务注册情况,如果有seata-server说明注册成功了。
说明:创建3个微服务模块:订单、库存、账户。用户下单,订单微服务创建一个订单,调用库存微服务减库存,调用账户服务扣账户余额,最后订单服务修改订单状态为完成。该操作会修改3个数据库,做两次远程调用,因此会存在分布式事务问题。
创建3个数据库seata_order、seata_storage、seata_account。在3个库下创建对应的业务表和回滚日志表,完整SQL在下面。
-- 创建3个数据库
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
-- 切换seata_order库,创建t_order表和undo_log表
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` BIGINT(11) DEFAULT NULL COMMENT '数量',
`money` BIGINT(11) DEFAULT NULL COMMENT '金额',
`status` BIGINT(11) DEFAULT NULL COMMENT '订单状态: 0创建中 1已完结'
) ENGINE = INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) 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';
-- 切换seata_storage库,创建t_storage表和undo_log表,对t_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` BIGINT(11) DEFAULT NULL COMMENT '总库存',
`used` BIGINT(11) DEFAULT NULL COMMENT '已用库存',
`residue` BIGINT(11) DEFAULT NULL COMMENT '剩余库存'
) ENGINE = INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO t_storage( `id`,`product_id`, `total`,`used`,`residue`) values(1,1,100,0,100);
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) 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';
-- 切换seata_account库,创建t_account表和undo_log表,对t_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` BIGINT(11) DEFAULT NULL COMMENT '总额度',
`used` BIGINT(11) DEFAULT NULL COMMENT '已用额度',
`residue` BIGINT(11) DEFAULT NULL COMMENT '剩余额度'
) ENGINE = INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO t_account( `id`,`user_id`, `total`,`used`,`residue`) values(1,1,1000,0,1000);
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) 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';
新建cloudalibaba-seata-order-service2001模块,修改pom.xml。
cloud2020
com.atguigu.springcloud
1.0-SNAPSHOT
4.0.0
cloudalibaba-seata-order-service2001
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
io.seata
seata-all
io.seata
seata-all
1.2.0
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
com.alibaba
druid-spring-boot-starter
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-jdbc
org.springframework.boot
spring-boot-starter-actuator
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
添加application.yml、file.conf、registry.conf文件到resources目录下。
application.yml
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
# 自定义事务组名称需要与seata-server中的对应
tx-service-group: my_test_tx_group # 因为seata的file.conf文件中没有service模块,事务组名默认为my_test_tx_group
# service要与tx-service-group对齐,vgroupMapping和grouplist在service的下一级,my_test_tx_group在再下一级
service:
vgroupMapping:
# 要和tx-service-group的值一致
my_test_tx_group: default
grouplist:
# seata server的地址配置,此处可以集群配置是个数组
default: 192.168.0.123:8091
nacos:
discovery:
server-addr: 192.168.0.123:8848 # nacos的地址
datasource:
# 当前数据源操作类型
type: com.alibaba.druid.pool.DruidDataSource
# mysql驱动类
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.0.123:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath*:mapper/*.xml
file.conf
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = true
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
vgroupMapping.my_test_tx_group = "default"
default.grouplist = "192.168.0.123:8091"
enableDegrade = false
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
sagaBranchRegisterEnable = false
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
degradeCheck = false
degradeCheckPeriod = 2000
degradeCheckAllowTimes = 10
}
undo {
dataValidation = true
onlyCareUpdateColumns = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}
registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "192.168.0.123:8848"
namespace = ""
username = ""
password = ""
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
type = "file"
file {
name = "file.conf"
}
}
添加主启动类。
package com.springcloud.alibaba;
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;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.springcloud.alibaba.dao")
public class SeataOrderMain2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMain2001.class, args);
}
}
添加实体类CommonResult和Order。
package com.springcloud.alibaba.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult {
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message) {
this(code, message, null);
}
}
package com.springcloud.alibaba.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:已完结
}
新建dao接口和对应的mapper.xml(在resources下新建mapper文件夹添加OrderMapper.xml)。
package com.springcloud.alibaba.dao;
import com.springcloud.alibaba.domain.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface OrderDao {
/**
* 新建订单
*/
int create(Order order);
/**
* 修改订单状态,从0改为1
*/
int update(@Param("id") Long id, @Param("status") Integer status);
}
insert into t_order(user_id,product_id,count,money,status) values (#{userId},#{productId},#{count},#{money},0);
update t_order set status=1 where id=#{id} and status=#{status};
创建OrderService.java接口,StorageService.java接口、AccountService.java接口,因为使用OpenFeign,所以要在Order模块里编写StorageService.java接口、AccountService.java接口,再添加一个OrderServiceImpl.java实现类,暂时注释掉@GlobalTransactional注解。
package com.springcloud.alibaba.service;
import com.springcloud.alibaba.domain.Order;
public interface OrderService {
/**
* 创建订单
*/
void create(Order order);
}
package com.springcloud.alibaba.service;
import com.springcloud.alibaba.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 {
/**
* 减库存
*/
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
package com.springcloud.alibaba.service;
import com.springcloud.alibaba.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 {
/**
* 减余额
*/
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
package com.springcloud.alibaba.service.impl;
import com.springcloud.alibaba.dao.OrderDao;
import com.springcloud.alibaba.domain.Order;
import com.springcloud.alibaba.service.AccountService;
import com.springcloud.alibaba.service.OrderService;
import com.springcloud.alibaba.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 AccountService accountService;
@Resource
private StorageService storageService;
/**
* 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
* 简单说:
* 下订单->减库存->减余额->改状态
* @GlobalTransactional seata开启分布式事务,异常时回滚,name保证唯一即可
* @param order 订单对象
*/
@Override
// @GlobalTransactional(name = "wsy-create-order", rollbackFor = Exception.class)
public void create(Order order) {
// 1 新建订单
log.info("----->开始新建订单");
orderDao.create(order);
// 2 扣减库存
log.info("----->订单微服务开始调用库存,做扣减Count");
storageService.decrease(order.getProductId(), order.getCount());
log.info("----->订单微服务开始调用库存,做扣减End");
// 3 扣减账户
log.info("----->订单微服务开始调用账户,做扣减Money");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("----->订单微服务开始调用账户,做扣减End");
// 4 修改订单状态,从0到1,1代表已完成
log.info("----->修改订单状态开始");
orderDao.update(order.getId(), 0);
log.info("----->下订单结束了,O(∩_∩)O哈哈~");
}
}
创建OrderController.java。
package com.springcloud.alibaba.controller;
import com.springcloud.alibaba.domain.CommonResult;
import com.springcloud.alibaba.domain.Order;
import com.springcloud.alibaba.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, "订单创建成功");
}
}
添加数据源配置类,使用Seata对数据源进行代理。
package com.springcloud.alibaba.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
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 org.springframework.core.io.support.ResourcePatternResolver;
import javax.sql.DataSource;
@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 druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSourceProxy);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
bean.setMapperLocations(resolver.getResources(mapperLocations));
return bean.getObject();
}
}
新建cloudalibaba-seata-storage-service2002模块,pom.xml和cloudalibaba-seata-order-service2001一样,application.yml修改端口号、服务名称、数据库名称,file.conf和registry.conf和cloudalibaba-seata-order-service2001一样。
添加主启动类。
package com.springcloud.alibaba;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
@MapperScan("com.springcloud.alibaba.dao")
public class SeataStorageMain2002 {
public static void main(String[] args) {
SpringApplication.run(SeataStorageMain2002.class, args);
}
}
添加实体类CommonResult和Storage。
package com.springcloud.alibaba.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult {
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message) {
this(code, message, null);
}
}
package com.springcloud.alibaba.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage {
private Long id;
private Long productId;
private Integer total;
private Integer used;
private Integer residue;
}
添加StorageDao.java接口以及对应的mapper.xml(在resources下新建mapper文件夹添加StorageMapper.xml)。
package com.springcloud.alibaba.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);
}
update t_storage
set used = used + #{count}, residue = residue - #{count}
where product_id= #{productId};
添加StorageService.java接口和StorageServiceImpl.java实现类。
package com.springcloud.alibaba.service;
public interface StorageService {
void decrease(Long productId, Integer count);
}
package com.springcloud.alibaba.service.impl;
import com.springcloud.alibaba.dao.StorageDao;
import com.springcloud.alibaba.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Resource
private StorageDao storageDao;
@Override
public void decrease(Long productId, Integer count) {
log.info("----> StorageService中扣减库存");
storageDao.decrease(productId, count);
log.info("----> StorageService中扣减库存完成");
}
}
添加StorageController.java。
package com.springcloud.alibaba.controller;
import com.springcloud.alibaba.domain.CommonResult;
import com.springcloud.alibaba.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, "扣减库存成功!");
}
}
添加数据源配置类,使用Seata对数据源进行代理,和cloudalibaba-seata-order-service2001一样。
新建cloudalibaba-seata-account-service2003模块,pom.xml和cloudalibaba-seata-order-service2001一样,application.yml修改端口号、服务名称、数据库连接,file.conf和registry.conf和cloudalibaba-seata-order-service2001一样。
添加主启动类。
package com.springcloud.alibaba;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
@MapperScan("com.springcloud.alibaba.dao")
public class SeataAccountMain2003 {
public static void main(String[] args) {
SpringApplication.run(SeataAccountMain2003.class, args);
}
}
添加实体类CommonResult和Account。
package com.springcloud.alibaba.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult {
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message) {
this(code, message, null);
}
}
package com.springcloud.alibaba.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long id;
private Long userId;
private BigDecimal total;
private BigDecimal used;
private BigDecimal residue;
}
添加AccountDao.java接口以及对应的mapper.xml(在resources下新建mapper文件夹添加AccountMapper.xml)。
package com.springcloud.alibaba.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
@Mapper
public interface AccountDao {
void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
update t_account
set used = used + #{money}, residue = residue - #{money}
where user_id = #{userId};
添加AccountService.java接口和AccountServiceImpl.java实现类。
package com.springcloud.alibaba.service;
import java.math.BigDecimal;
public interface AccountService {
void decrease(Long userId, BigDecimal money);
}
package com.springcloud.alibaba.service.impl;
import com.springcloud.alibaba.dao.AccountDao;
import com.springcloud.alibaba.service.AccountService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Resource
private AccountDao accountDao;
@Override
public void decrease(Long userId, BigDecimal money) {
log.info("---> AccountService中扣减账户余额");
accountDao.decrease(userId, money);
log.info("---> AccountService中扣减账户余额完成");
}
}
添加AccountController.java。
package com.springcloud.alibaba.controller;
import com.springcloud.alibaba.domain.CommonResult;
import com.springcloud.alibaba.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
private AccountService accountService;
@RequestMapping("/account/decrease")
public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money) {
accountService.decrease(userId, money);
return new CommonResult(200, "扣减库存成功!");
}
}
添加数据源配置类,使用Seata对数据源进行代理,和cloudalibaba-seata-order-service2001一样。
6.Test
初始的时候,t_order表是空的,t_storage表有一条记录,t_account表有一条记录。
然后启动Nacos、MySQL、Seata服务,再启动3个微服务。
正常情况下,通过浏览器发送一个请求:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100,请求发送给cloudalibaba-seata-order-service2001模块,2001模块通过Feign调用2002模块的接口和2003模块的接口。
如果一切正常的话,通过微服务的控制台,可以看到日志信息,查看数据库,可以看到订单创建完成, 库存扣减完成,账户扣减完成。
下面我们来测试一下出错的情况。
给2003模块添加一个sleep,并重启2003模块,浏览器访问http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100,发现页面报错了,查看数据库,生成了一条订单记录,但是订单状态是未完成,用户账户发生了扣款,库存也发生了扣减,此时,订单还是未完成的,这就矛盾了。
package com.springcloud.alibaba.service.impl;
import com.springcloud.alibaba.dao.AccountDao;
import com.springcloud.alibaba.service.AccountService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Resource
private AccountDao accountDao;
@Override
public void decrease(Long userId, BigDecimal money) {
log.info("---> AccountService中扣减账户余额");
try {
Thread.sleep(5000); // sleep5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
accountDao.decrease(userId, money);
log.info("---> AccountService中扣减账户余额完成");
}
}
下面,我们来验证一下添加了@GlobalTransactional后的效果,将2001模块的OrderServiceImpl.java里的@GlobalTransactional注释打开,重启2001模块。浏览器访问http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100。
页面依旧报错,查看数据库,订单表没有多出订单,库存也没有被多扣,账户余额也没有被多扣,一切正常。另外,在控制台能看到如下内容。
2020-07-18 21:10:00.446 INFO 24784 --- [tch_RMROLE_1_16] i.s.core.rpc.netty.RmMessageListener : onMessage:xid=192.168.0.123:8091:2017231421,branchId=2017231423,branchType=AT,resourceId=jdbc:mysql://192.168.0.123:3306/seata_order,applicationData=null
2020-07-18 21:10:00.446 INFO 24784 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 192.168.0.123:8091:2017231421 2017231423 jdbc:mysql://192.168.0.123:3306/seata_order
2020-07-18 21:10:00.458 INFO 24784 --- [tch_RMROLE_1_16] i.s.r.d.undo.AbstractUndoLogManager : xid 192.168.0.123:8091:2017231421 branch 2017231423, undo_log deleted with GlobalFinished
2020-07-18 21:10:00.459 INFO 24784 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked
2020-07-18 21:10:00.481 INFO 24784 --- [nio-2001-exec-3] i.seata.tm.api.DefaultGlobalTransaction : [192.168.0.123:8091:2017231421] rollback status: Rollbacked
Seata支持4种模式:AT模式、TCC模式、SAGA模式、XA模式,它们有各自的应用场景,默认使用的是AT模式。它是一种两阶段提交协议的演变。
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
在第一阶段,Seata会拦截业务SQL,然后对SQL进行解析,找到需要更新的业务数据,在数据被更新前,保存一个before image快照,然后执行业务SQL,SQL执行完毕后,保存一个after image快照,最后生成一个行锁,以上操作都在一个事务内进行,保证操作的原子性。这里的before image和after image有点像Spring AOP思想,将业务用通知方法包裹起来,程序员不比关心before image和after image。
在第二阶段,如果程序执行成功的话,Seata只需要删除before image和after image以及行锁即可,因为一阶段的SQL已经提交了,所以二阶段提交操作可以非常快速完成。
在第二阶段,如果程序报错了,Seata需要进行回滚,这里便用到了before image,在还原回before image的时候,需要校验脏写,对比当前数据库业务数据和after image,如果两份数据一样,说明没有发生脏写,此时直接还原回before iamge即可,如果不一样,说明有脏写,此时需要人工处理。
下面,我们在2003模块去掉Thead.sleep代码,在accountDao.decrease()方法后,添加一个断点,debug模式进行调试,当程序走到断点的时候,我们查看seata数据库的3张表的内容。
此时,查看undo_log表,可以发现也有信息,其中有一个字段是rollback_info,它里面存储的就是before image和after image的信息。