文章目录
- SpringCloud Alibaba Seata处理分布式事务
- 1.分布式事务问题由来
- 2.什么是Seata?
- 3.分布式事务的相关概念
- 4.Seata Server的安装和使用
- 4.1 Seata修改配置文件
- 4.2seata的启动
- 5.Seata实例
- 5.1业务声明
- 5.2数据库准备
- 5.3业务逻辑编写
- 5.3.1 seata-order-service
- 5.3.2 seata-storage-service
- 5.3.3seata-account-service
- 6.Seata@GlobalTranscational验证
- 6.1没加@GlobalTranscational
- 6.2加上@GlobalTranscational之后
参考尚硅谷
SpringCloud Alibaba Seata处理分布式事务
1.分布式事务问题由来
我们之前都是一个应用程序连一个库,而且应用程序和数据库被放在同一台机器上,但是在分布式微服务的系统架构中,将原来的三个模块拆分成三个独立的应用,分别使用三个独立的数据源。如下图所示
1.仓储服务:对给定商品扣除仓储数量。
2.订单服务:根据采购需求创建订单。
3.账户服务:从用户账户中扣除余额。
上述操作都需要操作相应的数据库,但是如何保证数据库的全局一致性是一个很大的问题。一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
2.什么是Seata?
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
3.分布式事务的相关概念
分布式事务处理过程的一ID+三组件模型,一ID即Transaction ID XID,全局唯一的事务ID。
三组件:
1.TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
2.TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
3.RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
然后我们看一下Seata处理流程
如图所示一个TC管理全局,一个XID带着TC,TM,RM。
1.TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID。
2.XID在微服务调用链路的上下文中传播。
3.RM向TC注册分支事务,将其纳入XID对应的全局事务的管辖。
4.TM向TC发起针对XID的全局提交或回滚决议。
5.TC调度XID下管辖的全部分支事务完成提交或者回滚请求。
4.Seata Server的安装和使用
下载地址Tags · seata/seata (github.com)
4.1 Seata修改配置文件
下载之后解压到自己的目录下,然后修改file.conf文件
修改service模块的group自己随便写一个。
然后修改store模块,他之前是mode是file文件存储,我们把他改成数据库存储。
然后是修改db模块将他的数据库配置改成你自己的主要是账号密码。然后就是建表这个不用你自己写人家给你写好了。
然后你复制粘贴里面的内容放到你自己的数据库下面执行就行了。
然后你再找到解压文件夹里面的registry.conf修改注册信息
4.2seata的启动
因为上面配置文件说这个东西得注册到nacos里面所以要想启动seata必须先启动nacos否则会报错。
启动成功后应该是这样的
5.Seata实例
5.1业务声明
这里我们创建三个服务,一个订单服务,一个库存服务,一个账户服务。当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
5.2数据库准备
首先我们创建数据库
create database seata_order;
create database seata_storage;
create database seata_account;
然后分别在对应的库下面建立相应的表
USE
seata_order;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order`
(
`int` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) 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:已完结',
PRIMARY KEY (`int`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '订单表' ROW_FORMAT = Dynamic;
USE
seata_storage;
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage`
(
`int` bigint(11) NOT NULL AUTO_INCREMENT,
`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 '剩余库存',
PRIMARY KEY (`int`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '库存' ROW_FORMAT = Dynamic;
INSERT INTO `t_storage`
VALUES (1, 1, 100, 0, 100);
USE
seata_account;
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account`
(
`id` bigint(11) NOT NULL COMMENT 'id',
`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 NULL COMMENT '剩余可用额度',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '账户表' ROW_FORMAT = Dynamic;
INSERT INTO `t_account`
VALUES (1, 1, 1000, 0, 1000);
然后分别建立相应的回滚日志
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;
最后数据库结构应该是这样的
5.3业务逻辑编写
5.3.1 seata-order-service
新建一个module端口号为2001。
pom.xml
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<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>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</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>
<version>2.0.0</version>
</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>
这个pom文件一定要加上这三个依赖,nacos,seata,feign,我们将seata注册到nacos里,所以得有nacos,通过feign调用微服务。
yml文件
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
tx-service-group: fsp_tx_group #自定义事务组名称需要与seata-server中的对应
nacos:
discovery:
server-addr: localhost:8848
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.gjt.mm.mysql.Driver
url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
除了要填加yml文件以外seata还要配置file.conf,registry.conf,这两个文件就是一开始seata-server服务器得配置文件
file.conf
service {
#transaction service group mapping
vgroup_mapping.fsp_tx_group = "default" #修改自定义事务组名称
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#disable seata
disableGlobalTransaction = false
}
## transaction log store, only used in seata-server
store {
## store mode: file、db
mode = "db"
## file store property
file {
## store location dir
dir = "sessionStore"
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://localhost:3306/seata"
user = "root"
password = "root"
}
}
registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
然后接下来实现我们的业务方法,我们的业务是这样的,下订单之后我们要做两个操作1.新建订单2.修改订单状态从0改为1
@Mapper
public interface OrderDao {
//1 新建订单
void create(Order order);
//2 修改订单状态,从零改为1
void update(@Param("userId") Long userId, @Param("status") Integer status);
}
mapper.xml
<mapper namespace="com.wjz.springcloud.alibaba.dao.OrderDao">
<resultMap id="order" type="com.wjz.springcloud.alibaba.domain.Order">
<result property="id" column="id" jdbcType="BIGINT"/>
<result property="userId" column="user_id" jdbcType="BIGINT"/>
<result property="productId" column="product_id" jdbcType="BIGINT"/>
<result property="count" column="count" jdbcType="INTEGER"/>
<result property="money" column="money" jdbcType="BIGINT"/>
<result property="status" column="status" jdbcType="INTEGER"/>
</resultMap>
<insert id="create">
insert into t_order(user_id, product_id, count, money, status)
value (#{userId},#{productId},#{count},#{money},0)
</insert>
<update id="update">
update t_order
set status = 1
where user_id = #{userId}
and status = #{status}
</update>
</mapper>
然后是order的service层和Impl层
public interface OrderService {
void create(Order order);
}
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
* 简单说:下订单->扣库存->减余额->改状态
* 注释掉 @GlobalTransactional 的时候,需要注意下方这个方法里面手动模拟了延时,也需要注释掉
* com.atguigu.springcloud.alibaba.service.impl.AccountServiceImpl#decrease(java.lang.Long, java.math.BigDecimal)
*/
@Override
public void create(Order order) {
log.info("----->开始新建订单");
//1 新建订单
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 修改订单状态,从零到1,1代表已经完成
log.info("----->修改订单状态开始");
orderDao.update(order.getUserId(), 0);
log.info("----->修改订单状态结束");
log.info("----->下订单结束了,O(∩_∩)O哈哈~");
}
}
新建订单之后我们要扣减库存和扣减账户所以我们要写storageService和accountService这两个的实现。
@FeignClient(value = "seata-storage-service")
public interface StorageService {
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
@FeignClient(value = "seata-account-service")
public inte