数据库事务(简称:事务),Transactional是指数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
起初,事务仅限于对单一数据库资源的访问控制,架构服务话以后,事务的概念延伸到了服务中。倘若将一个单一的服务操作作为一个事务,那么整个服务操作只能涉及到一个单一的数据库资源,这类基于单个服务单一数据库资源访问的事务,被称为本地事务(Local Transaction)。
原子性(Atomicity):事务作为一个整体去执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一状态。一致状态是指数据库中的数据应满足完整性约束。除此之外,一致性还有另外一层语义,就是事务的中间状态下不能被观察到(这层语义也应该属于原子性)。
**隔离性(lsolation):**多个事务并发执行时,一个事务的执行不应影响其他事物的执行,如同只有这一个操作在被数据库所执行一样。
执行性(Durability):已被提交的事物对数据库的修改应该是永久性的保存在数据库中。在事务结束后,此操作不可逆转。
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,且属于不同的应用,分布式事务需要保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
最早的分布式事务应用架构很简单,不涉及服务间的访问调用,仅仅是服务内操作涉及到对多个数据库资源的访问。
对于上面介绍的分布式事务应用架构,尽管—个服务操作会访问多个数据库资源,但是毕竟整个事务还是控制在单一服务的内部。如果一个服务操作需要调用另外一个服务,这时的事务就需要跨越多个服务了。在这种情况下,起始于某个服务的事务在调用另外一个服务的时候,需要以某种机制流转到另外一个服务,从而使被调用的服务访问的资源也自动加入到该事务当中来。下图反映了这样一个跨越多个服务的分布式事务:
如果将上面这两种场景(—个服务可以调用多个数据库资源,也可以调用其他服务)结合在一起,对此进行延伸,整个分布式事务的参与者将会组成如下图所示的树形拓扑结构。在一个跨服务的分布式事务中,事务的发起者和提交均系同一个,它可以是整个调用的客户端,也可以是客户端最先调用的那个服务。
CAP定理是在1998年加州大学计算机科学家EricBrewer提出的,分布式系统有3个指标
这三个指标首字母加起来就是CAP,但是这不可能同时做到CAP,这个结论就叫做CAP定理。
大多数分布式系统都分不在多个子网络中,每个子网络就叫做一个区。分区容错性的意思就是,区间通信可能失败。比如,一台服务器放在中国,一台放在美国,这就是两个区,她们之间可能无法通信。
G1和G2两台跨区服务器。G1向G2发送一条消息,G2可能无法收。系统设计时,必须考虑该情况。
一般来说,分区容错无法避免,因此认为,CAP的P总是成立的,C和A不能同时做到。
可用性,只要接收到用户的请求,服务器就必须给出回应。
用户可以选择向G1或G2发起读操作,不管是哪台服务器,只要收到请求,就必须告诉用户,到底是V0还是V1,否则就不满足可用性。但是这样可能会导致数据不准确。
写操作之后的读操作,必须返回该值。
CAP中的一致性是指强一致性。
eg:某条记录是V0,用户向G1发起一个读操作,将其改为V1.
用户有可能向G2发起读操作,由于G2没有发生变化,因此返回的还是V0。
G1和G2的读操作结果不一致,这就不满足一致性了。
为了让G1也能变为V1,就要在G1写操作时,让G1发送一条消息给G2,要求G2也改为V1
但是同步数据时,可能网络故障,数据就不能保持一致性,要么就是等待服务器恢复后再去请求,但是这样就不能保证可用性了。
BASE:全称: Basically Available(基本可用),Soft state(软状态) 和 Eventuallv consistent (最终一致性)三个短语的缩写,来自ebpy 的架构师提出。BASE 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大型互联网分布式实践的总结,是基于 CAP 定理逐步演化而来的。其核心思想是:
即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务待点,采用适当的方式来使系统达到最终一致性 (Eventual consistency)
eg:类似于12306,查看票时,有票但是一下单就没票了,这样可以保证可用性的同时保证一致性。
Basically Available(基本可用)
什么是基本可用呢?假设系统,出现了不可预知的故障,但还是能用,相比较正常的系统而言:
1,响应时间上的损失:正常情况下的搜索引擎 0.5 秒即返回给用户结果,而基本可用的搜索引擎可以在1秒左右返回结果。
2. 功能上的损失:在一个电商网站上,正常情况下,用户可以顺利完成每一笔订单,但是到了大促期间,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。到证了基本可用。
Soft state (软状态)
什么是软状态呢?相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种“硬状态”。
软状态指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
Eventually consistent (最终—致性)
系统能路保证在没有其他新的更新操作的情况下,致据地终一定能够达到一致的状态,因此所有客户端对系统的数据访问最终都能获取到最新的值。
第一阶段:TM要求所有的RM准备自己对应的事务分支,询问每个RM能否成功提交当前的事务分支,RM根据自己的情况返回TM状态,失败返回NO,RM就会对这个分支就行会滚,当前的这个分支就没了
第二阶段:TM根据第一阶段提交的结果就行处理,是提交还是会滚,所有的RM都返回成功,TM会通知所有的RM提交事务,只要有一个RM返回NO,则通知所有的RM会滚事务。
优点:尽量保证了数据的强一致性,适合对数据强一致要求很高的关键领域,(其实也不能100%保证强一致性)
缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发场景。
TCC其实就是采用补偿机制,其核心思想是:针对每个操作,都要注册一个与其对一个的确认和补偿(撤销)操作,分为3阶段:
举例:A和B转账
我们有一个本地方法,里面依次调用
1、首先在try阶段,要先调用远程接口把B和A的钱冻结起来。
2、在confirm阶段,执行远程调用的转账操作,转账成功解冻。
3、在执行第2步成功时,那么转账成功,如果执行失败,则调用远程接口对应的解冻方法(Cancel)
流程
1、Try接口,先查看A账户余额是否充足,充足则冻结金额,并扣减。B账户需要检查账户是否可用
2、刚才的操作都没问题,就需要去走Confirm接口:A:刚才冻结的金额,解冻并真正扣减;B:把对应的钱加起来。这样事务就提交成功了。
3、如果Try接口中任何一方有异常,就不会走Confirm接口,走Cancel接口,A:把金额解冻回来,并且增加回来,将数据进行会滚操作。
消息最终一致性核心思想是将分布式事务拆成本地事务进行处理,这种思想来源ebay,示意图:
基本思路就是:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
跨数据库、多数据源的统一调度。
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立应用,分别使用三个独立的数据源,
业务操作需要调用3个服务来完成。此时每个服务内部的数据一致性由本地事物保证,但是全局的数据一致性问题无法得到保证!
一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
示例:
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供技术支持:
官网地址
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务解决方案
维护全局和分支事务的状态,驱动全局事务提交或回滚。
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
1、TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;
2、XID在微服务调用链路的上下文中传播;
3、RM向TC注册分支事务,将其纳入XID对应的全局事务的管辖;
4、TM向TC发起针对XID的全局提交或回滚决议;
5、TC调度XID下管辖的全局分支事务完成提交或回滚请教;
发布地址
1.0.0tar下载地址
GitHub官网下载换缓慢,可以通过github官网下载加速器加速下载
tar -zxvf seata-server-1.0.0.tar.gz
修改conf\file.conf文件
先备份,再做修改
cp file.conf file.conf.bak
主要修改自定义事务组名称+事务日志存储模式为DB+数据库连接信息
事务日志存储模式默认为文件,修改为存储到DB中
原厂默认配置:
service {
#transaction service group mapping
vgroup_mapping.my_test_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
}
修改vgroup_mapping.my_test_tx_group的value,自定义即可
service {
#transaction service group mapping
vgroup_mapping.my_test_tx_group = "seata_tx_group"
#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://127.0.0.1:3306/seata"
user = "root"
password = "xxxxx"
}
}
由于我们将存储模式从文件修改为了db,所以我们需要创建对应的数据库
-- 创建seata库
CREATE DATABASE IF NOT EXISTS seata;
0.9版本建表SQL存储在db_store.sql在seata目录下的conf目录里面
但是我的1.0.0版本conf目录下,并没有对应的sql文件,查看官网:
去github上获取对应的sql语句
1.0.0版本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,
`gmt_modified` DATETIME,
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(96),
`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;
guthub:https://github.com/seata/seata/blob/1.0.0/script/server/db/mysql.sql
将此脚本直接执行即可生成对应的表文件
先做备份
cp registry.conf registry.conf.bak
原文件:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
cluster = "default"
}
...
}
修改为nacos,并配置nacos的信息
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
...
}
先启动Nacos,等Nacos启动完成,再启动Seata
负责会启动报错:
sh /root/development/nacos/bin/startup.sh -m standalone
sh /root/development/seata/bin/seata-server.sh
创建3个微服务,一个订单服务,一个库存服务,一个账户服务
当用户下订单时,会在订单服务中创建一个订单,然后通过远程调用来库存来扣件下单商品库存,
再通过远程调用账户服务来扣件用户账户里的余额。
最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,存在分布式事务问题。
下订单---->减库存---->扣余额---->改状态
准备订单、库存、账户业务数据库准备
对应的建库语句:
CREATE DATABASE IF NOT EXISTS `seata_order`;
CREATE DATABASE IF NOT EXISTS `seata_storage`;
CREATE DATABASE IF NOT EXISTS `seata_account`;
业务表:
USE seata_order;
CREATE TABLE IF NOT EXISTS t_order(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
`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=1 DEFAULT CHARSET=utf8;
USE seata_storage;
CREATE TABLE IF NOT EXISTS t_storage(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`userd` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-- 添加测试数据
INSERT INTO t_storage VALUES(NULL,1,100,0,100);
USE seata_account;
CREATE TABLE IF NOT EXISTS t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`total` INT(11) DEFAULT NULL COMMENT '总额度',
`userd` INT(11) DEFAULT NULL COMMENT '已用余额',
`residue` INT(11) DEFAULT NULL COMMENT '剩余可用额度'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-- 添加测试数据
INSERT INTO t_account VALUES(NULL,1,1000,0,1000);
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci 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',
`id` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 26 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of undo_log
-- ----------------------------
pom
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>seata-demoartifactId>
<groupId>com.xiugroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>seata-orderartifactId>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>com.xiugroupId>
<artifactId>seata-commonartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>0.9.0version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
dependencies>
project>
application.yml
server:
port: 2001
spring:
application:
name: seata-order
cloud:
alibaba:
seata:
tx-service-group: seata_tx_group #事务组名称与seata配置file.conf文件配置的保持一致
nacos:
discovery:
server-addr: 172.16.138.100:8848
datasource: #数据库配置信息
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://172.16.138.100:3306/seata_order
username: root
password: xxxxx
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
启动类:
package com.xiu.order;
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;
/**
* Order订单模块主启动类
* 取消数据源自动创建
*
* @author zhangzengxiu
* @date 2023/2/19
*/
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class OrderApp {
public static void main(String[] args) {
SpringApplication.run(OrderApp.class, args);
}
}
OrderService接口
import com.xiu.common.model.order.domain.Order;
/**
* @author zhangzengxiu
* @date 2023/2/19
*/
public interface OrderService {
/**
* 创建新订单
*
* @param order
* @return
*/
boolean createOrder(Order order);
}
实现类:
import com.xiu.common.model.order.domain.Order;
import com.xiu.common.model.order.enums.OrderStatusEnum;
import com.xiu.order.feign.AccountFeign;
import com.xiu.order.feign.StorageFeign;
import com.xiu.order.mapper.OrderMapper;
import com.xiu.order.service.OrderService;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author zhangzengxiu
* @date 2023/2/19
*/
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountFeign accountFeign;
@Autowired
private StorageFeign storageFeign;
@GlobalTransactional(name = "seata_tx_group", rollbackFor = Exception.class)
@Override
public boolean createOrder(Order order) {
//创建新订单
orderMapper.addOrder(order);
//库存扣减
storageFeign.decreaseStorage(order.getProductId(), order.getCount());
//账户扣减
accountFeign.decreaseAccount(order.getUserId(), order.getMoney());
//修改订单状态
orderMapper.updateOrderStatus(order.getId(), OrderStatusEnum.END.getStatusCode());
return true;
}
}
mapper
package com.xiu.order.mapper;
import com.xiu.common.model.order.domain.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
/**
* @author zhangzengxiu
* @date 2023/2/19
*/
@Mapper
@Repository
public interface OrderMapper {
/**
* 创建新订单
*
* @param order
* @return
*/
Integer addOrder(Order order);
/**
* 修改订单状态
*
* @param id 订单id
* @param status 订单新状态
* @return
*/
Integer updateOrderStatus(@Param("id") Long id, @Param("status") Integer status);
}
mapper.xml
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiu.order.mapper.OrderMapper">
<resultMap id="BaseResultMap" type="com.xiu.common.model.order.domain.Order">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="user_id" jdbcType="BIGINT" property="userId" javaType="long"/>
<result column="product_id" jdbcType="BIGINT" property="productId" javaType="long"/>
<result column="count" jdbcType="INTEGER" property="count" javaType="int"/>
<result column="money" jdbcType="DECIMAL" property="money" javaType="bigDecimal"/>
<result column="status" jdbcType="INTEGER" property="status" javaType="int"/>
resultMap>
<insert id="addOrder" parameterType="com.xiu.common.model.order.domain.Order" useGeneratedKeys="true" keyProperty="id">
INSERT INTO t_order
(id,
user_id,
product_id,
count,
money,
status)
VALUES
(
NULL,
#{userId},
#{productId},
#{count},
#{money},
#{status}
)
insert>
<update id="updateOrderStatus">
UPDATE t_order
SET status = #{status}
WHERE id = #{id}
update>
mapper>
controller
import com.xiu.common.CommonResult;
import com.xiu.common.model.order.domain.Order;
import com.xiu.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author zhangzengxiu
* @date 2023/2/23
*/
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/createOrder")
public CommonResult createOrder(@RequestBody Order order) {
boolean res = orderService.createOrder(order);
if (res) {
return new CommonResult(200, "下订单成功", null);
}
return new CommonResult(444, "下订单失败", null);
}
}
config
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* @author zhangzengxiu
* @date 2023/2/19
*/
@Configuration
@ComponentScan("com.xiu.common.config")
public class SeataConfig {
}
feign
import com.xiu.common.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;
/**
* @author zhangzengxiu
* @date 2023/2/20
*/
@FeignClient("seata-account")
public interface AccountFeign {
/**
* 账户扣减
*
* @param userId 账户id
* @param money 扣减金额
* @return
*/
@PostMapping("/account/decreaseAccount")
CommonResult decreaseAccount(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
import com.xiu.common.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @author zhangzengxiu
* @date 2023/2/20
*/
@FeignClient("seata-storage")
public interface StorageFeign {
/**
* 库存扣减
*
* @param productId 产品id
* @param count 扣减库存数量
* @return
*/
@PostMapping("/storage/decreaseStorage")
CommonResult decreaseStorage(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
pom
<artifactId>seata-storageartifactId>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>com.xiugroupId>
<artifactId>seata-commonartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>0.9.0version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
dependencies>
application.yml
server:
port: 2002
spring:
application:
name: seata-storage
cloud:
alibaba:
seata:
tx-service-group: seata_tx_group #事务组名称与seata配置file.conf文件配置的保持一致
nacos:
discovery:
server-addr: 172.16.138.100:8848
datasource: #数据库配置信息
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://172.16.138.100:3306/seata_storage
username: root
password: xxxxx
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
启动类
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;
/**
* 库存服务主启动类
*
* @author zhangzengxiu
* @date 2023/2/23
*/
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class StorageApp {
public static void main(String[] args) {
SpringApplication.run(StorageApp.class, args);
}
}
service
import com.xiu.storage.mapper.StorageMapper;
import com.xiu.storage.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author zhangzengxiu
* @date 2023/2/23
*/
@Service
public class StorageServiceImpl implements StorageService {
@Autowired
private StorageMapper storageMapper;
@Override
public boolean decreaseStorage(Long productId, Integer count) {
int res = storageMapper.decreaseStorage(productId, count);
return res == 1;
}
}
mapper
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
/**
* @author zhangzengxiu
* @date 2023/2/23
*/
@Mapper
@Repository
public interface StorageMapper {
/**
* 库存扣减
*
* @param productId 产品id
* @param count 扣减数量
* @return
*/
int decreaseStorage(@Param("productId") Long productId, @Param("count") Integer count);
}
mapper.xml
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiu.storage.mapper.StorageMapper">
<resultMap id="BaseResultMap" type="com.xiu.common.model.storage.domain.Storage">
<id column="id" jdbcType="BIGINT" javaType="long" property="id"/>
<result column="product_id" jdbcType="BIGINT" javaType="long" property="productId"/>
<result column="total" jdbcType="INTEGER" javaType="int" property="total"/>
<result column="userd" jdbcType="INTEGER" javaType="int" property="userd"/>
<result column="residue" jdbcType="INTEGER" javaType="int" property="residue"/>
resultMap>
<update id="decreaseStorage">
update t_storage
set userd = userd + #{count}, residue = residue - #{count}
where product_id = #{productId}
update>
mapper>
controller
import com.xiu.common.CommonResult;
import com.xiu.storage.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author zhangzengxiu
* @date 2023/2/23
*/
@RestController
public class StorageController {
@Autowired
private StorageService storageService;
@PostMapping("/storage/decreaseStorage")
public CommonResult decreaseStorage(@RequestParam("productId") Long productId, @RequestParam("count") Integer count) {
boolean res = storageService.decreaseStorage(productId, count);
if (res) {
return new CommonResult(200, "库存扣减成功", null);
}
return new CommonResult(444, "库存扣减失败", null);
}
}
config
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* @author zhangzengxiu
* @date 2023/2/23
*/
@Configuration
@ComponentScan("com.xiu.common.config")
public class SeataConfig {
}
pom
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>seata-demoartifactId>
<groupId>com.xiugroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>seata-accountartifactId>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>com.xiugroupId>
<artifactId>seata-commonartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>0.9.0version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
dependencies>
project>
application.yml
server:
port: 2003
spring:
application:
name: seata-account
cloud:
alibaba:
seata:
tx-service-group: seata_tx_group #事务组名称与seata配置file.conf文件配置的保持一致
nacos:
discovery:
server-addr: 172.16.138.100:8848
datasource: #数据库配置信息
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://172.16.138.100:3306/seata_account
username: root
password: xxxxx
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
启动类
package com.xiu.account;
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;
/**
* @author zhangzengxiu
* @date 2023/2/23
*/
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class AccountApp {
public static void main(String[] args) {
SpringApplication.run(AccountApp.class, args);
}
}
service
package com.xiu.account.service.impl;
import com.xiu.account.mapper.AccountMapper;
import com.xiu.account.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
* @author zhangzengxiu
* @date 2023/2/23
*/
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
public boolean decreaseAccount(Long userId, BigDecimal money) {
try {
//模拟业务超时
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int res = accountMapper.decreaseAccount(userId, money);
return res == 1;
}
}
mapper
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
/**
* @author zhangzengxiu
* @date 2023/2/23
*/
@Mapper
@Repository
public interface AccountMapper {
/**
* 账户扣减
*
* @param userId 用户id
* @param money 扣减金额
* @return
*/
int decreaseAccount(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
mapper.xml
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiu.account.mapper.AccountMapper">
<update id="decreaseAccount">
update t_account
set userd = userd + #{money}, residue = residue - #{money}
where user_id = #{userId}
update>
mapper>
controller
package com.xiu.account.controller;
import com.xiu.account.service.AccountService;
import com.xiu.common.CommonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
/**
* @author zhangzengxiu
* @date 2023/2/23
*/
@RestController
public class AccountController {
@Autowired
private AccountService accountService;
/**
* 账户扣减
*
* @param userId 账户id
* @param money 扣减金额
* @return
*/
@PostMapping("/account/decreaseAccount")
public CommonResult decreaseAccount(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money) {
boolean res = accountService.decreaseAccount(userId, money);
if (res) {
return new CommonResult(200, "账户扣减成功", null);
}
return new CommonResult(444, "账户扣减失败", null);
}
}
config
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* @author zhangzengxiu
* @date 2023/2/23
*/
@Configuration
@ComponentScan("com.xiu.common.config")
public class SeataConfig {
}
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.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
/**
* @author zhangzengxiu
* @date 2023/2/19
*/
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource getDruidDataSource() {
return new DruidDataSource();
}
/**
* 创建数据源代理
* Primary:防止本地事务失效
*
* @param dataSource
* @return
*/
@Primary
@Bean
public DataSourceProxy getDataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory getSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/*.xml"));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
storage
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author zhangzengxiu
* @date 2023/2/19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Storage implements Serializable {
private static final long serialVersionUID = 7103617841362037153L;
/**
* 主键id
*/
private Long id;
/**
* 产品id
*/
private Long productId;
/**
* 总库存
*/
private Integer total;
/**
* 已用库存
*/
private Integer userd;
/**
* 剩余库存
*/
private Integer residue;
}
account
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author zhangzengxiu
* @date 2023/2/19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account implements Serializable {
private static final long serialVersionUID = -1515870651167229775L;
/**
* 主键
*/
private Long id;
/**
* 用户id
*/
private Long userId;
/**
* 总额度
*/
private Integer total;
/**
* 已用余额
*/
private Integer userd;
/**
* 剩余可用额度
*/
private Integer residue;
}
order
import com.xiu.common.model.order.enums.OrderStatusEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
* @author zhangzengxiu
* @date 2023/2/19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
/**
* 主键id
*/
private Long id;
/**
* 用户id
*/
private Long userId;
/**
* 产品id
*/
private Long productId;
/**
* 数量
*/
private Integer count;
/**
* 金额
*/
private BigDecimal money;
/**
* 订单状态
*
* @see OrderStatusEnum
*/
private Integer status;
}
枚举
import java.util.Objects;
/**
* 订单状态枚举类
*
* @author zhangzengxiu
* @date 2023/2/19
*/
public enum OrderStatusEnum {
CREATING(0, "创建中"), END(1, "已完结");
/**
* 订单状态编码
*/
private Integer statusCode;
/**
* 订单状态
*/
private String status;
OrderStatusEnum(Integer statusCode, String status) {
this.statusCode = statusCode;
this.status = status;
}
public Integer getStatusCode() {
return statusCode;
}
public String getStatus() {
return status;
}
/**
* 根据订单状态编码获取订单状态
*
* @param statusCode
* @return
*/
public static String getStatusByCode(Integer statusCode) {
if (Objects.equals(statusCode, null)) {
return null;
}
OrderStatusEnum[] values = values();
if (values == null || values.length <= 0) {
return null;
}
for (OrderStatusEnum orderStatusEnum : values) {
if (orderStatusEnum == null) {
continue;
}
if (Objects.equals(orderStatusEnum.getStatus(), statusCode)) {
return orderStatusEnum.getStatus();
}
}
return null;
}
}
内置GlobalTransactionScanner自动初始化功能,若外部实现初始化,请参考SeataAutoConfiguration保证依赖加载顺序 默认开启数据源自动代理,可配置seata.enable-auto-data-source-proxy: false关闭
查看版本说明 2.1.0内嵌seata-all 0.7.1,2.1.1内嵌seata-all 0.9.0,2.2.0内嵌seata-spring-boot-starter 1.0.0, 2.2.1内嵌seata-spring-boot-starter 1.1.0
2.1.0和2.1.1兼容starter解决方案: @SpringBootApplication注解内exclude掉spring-cloud-starter-alibaba-seata内的com.alibaba.cloud.seata.GlobalTransactionAutoConfiguration
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
<version>最新版version>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<version>最新版本version>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
exclusion>
exclusions>
dependency>
file.conf
将seata目录conf下的file.conf.example文件拷贝到资源目录中,该文件与之前修改的file.conf内容不完全一致。
将文件修改名称为file.conf
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
#thread factory for netty
thread-factory {
boss-thread-prefix = "NettyBoss"
worker-thread-prefix = "NettyServerNIOWorker"
server-executor-thread-prefix = "NettyServerBizHandler"
share-boss-worker = false
client-selector-thread-prefix = "NettyClientSelector"
client-selector-thread-size = 1
client-worker-thread-prefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
boss-thread-size = 1
#auto default pin or 8
worker-thread-size = 8
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#transaction service group mapping
vgroup_mapping.seata_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
}
client {
rm {
async.commit.buffer.limit = 10000
lock {
retry.internal = 10
retry.times = 30
retry.policy.branch-rollback-on-conflict = true
}
report.retry.count = 5
table.meta.check.enable = false
report.success.enable = true
}
tm {
commit.retry.count = 5
rollback.retry.count = 5
}
undo {
data.validation = true
log.serialization = "jackson"
log.table = "undo_log"
}
log {
exceptionRate = 100
}
support {
# auto proxy the DataSource bean
spring.datasource.autoproxy = false
}
}
## transaction log store
store {
## store mode: file、db
mode = "file"
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
max-branch-session-size = 16384
# globe session size , if exceeded throws exceptions
max-global-session-size = 512
# file buffer size , if exceeded allocate new buffer
file-write-buffer-cache-size = 16384
# when recover batch read size
session.reload.read_size = 100
# async, sync
flush-disk-mode = async
}
## 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://172.16.138.100:3306/seata" #修改db信息
user = "root"
password = "xxxxx"
min-conn = 1
max-conn = 10
global.table = "global_table"
branch.table = "branch_table"
lock-table = "lock_table"
query-limit = 100
}
}
server {
recovery {
#schedule committing retry period in milliseconds
committing-retry-period = 1000
#schedule asyn committing retry period in milliseconds
asyn-committing-retry-period = 1000
#schedule rollbacking retry period in milliseconds
rollbacking-retry-period = 1000
#schedule timeout retry period in milliseconds
timeout-retry-period = 1000
}
undo {
log.save.days = 7
#schedule delete expired undo_log in milliseconds
log.delete.period = 86400000
}
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}
## metrics settings
metrics {
enabled = false
registry-type = "compact"
# multi exporters use comma divided
exporter-list = "prometheus"
exporter-prometheus-port = 9898
}
修改信息:
registry.conf
将seata目录conf下的registry.conf文件拷贝到资源目录中,该文件与之前修改的file.conf内容基本一致。
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "172.16.138.100: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"
}
}
seata版本一定要与pom依赖的版本保持一致!!!
Seata控制台报错:
排除包内自带的seata,重新引入与自己版本一直的依赖包。
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
<version>1.0.0version>
dependency>
报错:not support register type: null
注意SpringCloudAlibaba版本与Seata之间的版本对应关系
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.2.0.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
版本依赖关系
我用的是1.0.0版本的seata,与之对应的spring-cloud-alibaba版本应该是2.0.0.RELEASE。切换版本
代理源对象报错:
seata版本问题:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>1.0.0version>
dependency>
将版本号从1.0.0改为0.9.0:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>0.9.0version>
dependency>
解决:
seata:
enabled: false #取消自动装配数据源
报错:not support register type: null
解决:实在没解决办法了。
版本回退,seata改为0.9.0。
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>0.9.0version>
dependency>
file.conf和registry.conf配置同上。
成功启动
全部微服务启动完成:
基础数据
t_account表数据:
t_order表数据:
t_storage表数据:
发送post请求:http://localhost:2001/order/createOrder
请求参数:json
{
"id": null,
"userId": 1,
"productId": 1,
"count": 10,
"money": "100.0",
"status": 0
}
t_account表数据:
t_order表数据:
t_storage表数据:
正常下订单没问题,整体流程需要保证分布式事务,加起来需要考虑到高并发JUC的问题。
常见全局事务问题:
模拟超时异常:
import com.xiu.account.mapper.AccountMapper;
import com.xiu.account.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
* @author zhangzengxiu
* @date 2023/2/23
*/
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
public boolean decreaseAccount(Long userId, BigDecimal money) {
try {
//模拟业务超时
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int res = accountMapper.decreaseAccount(userId, money);
return res == 1;
}
}
发送请求:
t_order订单表:新建了订单,但是状态仍然未改变为已完成。
t_account账户表:账户未扣减
t_storage库存表:库存被扣减
添加注解:@GlobalTransactional 来控制全局事务
@GlobalTransactional(name = "seata_tx_group", rollbackFor = Exception.class)
@Override
public boolean createOrder(Order order) {
//创建新订单
orderMapper.addOrder(order);
//库存扣减
storageFeign.decreaseStorage(order.getProductId(), order.getCount());
//账户扣减
accountFeign.decreaseAccount(order.getUserId(), order.getMoney());
//修改订单状态
orderMapper.updateOrderStatus(order.getId(), OrderStatusEnum.END.getStatusCode());
return true;
}
再次发送请求,本次请求并未被三张表造成任何写操作,全局事务控制成功!
0.9版本Seata不支持集群!!!
TC:seata服务器
TM:事务发起方(添加@GlobalTransactional注解的方法)
RM:每个数据库就是一个RM,事务的参与方
执行流程:
两阶段提交协议的演变:
第一阶段,Seata会拦截“业务SQL”
1、解析SQL语义,找到业务SQL要更新的数据,在业务数据被更新前,将其保存成“before image”
2、执行业务SQL,更新业务数据,在业务数据更新之后
3、将其保存成“after image”,最后生成行锁(lock_table会有体现)
以上操作全部在一个数据库的事务中完成,这样保证了一阶段的操作原子性
因为业务SQL在第一阶段已经提交到数据库,所以seata框架需要将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
不加锁了就可以提交事务了。
二阶段如果是回滚操作的话,Seata就需要回滚到一阶段已经执行的业务SQL还原数据。(反向补偿)
回滚方式便是用before image还原业务数据;但是还原前,要首先校验脏写,对比数据库当前业务数据和after image,如果两份数据完全一致就说明没有脏写,可以还原数据,如果不一致,说明有脏写,需要人工处理。