二十、SpringCloud Alibaba Seata處理分佈式事務
分佈式事務問題
Seata簡介
Seata-Server安裝
下載:https://github.com/seata/seata/releases
數據庫建庫建表
mysql8的同學需要修改file.conf的驅動配置store.db.driver-class-name;並lib目錄下刪除mysql5驅動,添加mysql8驅動。
啓動nacos
啓動seata
docker下載安裝
mysql5.6:
#啓動數據庫容器(注意,我這裏數據庫暴露的是3305端口)
docker start 數據庫容器ID
#docker run -p 3305:3306 --name mysql5.6 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.6
#進入mysql5.6容器
docker exec -it 容器ID /bin/bash
#進入mysql
mysql -uroot -p123456 --default-character-set=utf8
#創建seata數據庫
create database seata character set utf8;
use seata;
#創建seata數據庫需要的表(三張表)
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;
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;
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;
#因爲之前已經弄過了nacos的持久化,已建了nacos_config數據庫了,所以這裏就不再贅述。
#退出數據庫
exit
#退出容器
exit
nacos1.3:
#啓動nacos
docker start nacos容器ID
#docker run --env MODE=standalone --name mynacos -d -p 8848:8848 -e MYSQL_SERVICE_HOST=10.211.55.26 -e MYSQL_SERVICE_PORT=3305 -e MYSQL_SERVICE_DB_NAME=nacos_config -e MYSQL_SERVICE_USER=root -e MYSQL_SERVICE_PASSWORD=123456 -e SPRING_DATASOURCE_PLATFORM=mysql -e MYSQL_DATABASE_NUM=1 nacos/nacos-server
seata:
#拉取seata鏡像(此時最新版爲1.2)
docker pull seataio/seata-server
#運行seata
docker run --name myseata -d -h 10.211.55.26 -p 8091:8091 seataio/seata-server
#進入seata容器
docker exec -it 容器ID /bin/bash
cd resources
#因爲容器沒有裝vim,所以我們要先安裝vim
apt-get update
apt-get install vim
#備份文件
cp file.conf file.conf.bk
cp registry.conf registry.conf.bk
#修改file.conf文件(看下圖)
vim file.conf
#seata1.2的file.conf裏沒有service模塊,store的mode支持了redis
#mysql8的同學需要修改file.conf的驅動配置store.db.driver-class-name;並lib目錄下刪除mysql5驅動,添加mysql8驅動。
#按esc鍵然後:wq!退出
#修改文件(看下圖)
vim registry.conf
#按esc鍵然後:wq!退出
#退出容器
exit
#重啓容器
docker restart seata容器ID
file.conf
#service {
# vgroupMapping.my_test_tx_group = "fsp_tx_group"
# default.grouplist = "10.211.55.26:8091"
# enableDegrade = false
# disableGlobalTransaction = false
#}
jdbc:mysql://10.211.55.26:3305/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
registry.conf
http://10.211.55.26:8848/nacos
,到nacos後臺看seata是否成功註冊進nacos。
查看註冊進nacos的seata信息是否正確。
訂單/庫存/賬戶業務數據庫準備
#進入mysql5.6容器
docker exec -it 容器ID /bin/bash
#進入mysql
mysql -uroot -p123456 --default-character-set=utf8
#創建業務數據庫和對應的業務表
#order
create database 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 DEFAULT CHARSET=utf8;
select * from t_order;
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';
#storage
create database 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=2 DEFAULT CHARSET=utf8;
INSERT INTO t_storage(`id`,`product_id`,`total`,`used`,`residue`)VALUES('1','1','100','0','100');
SELECT * FROM t_storage;
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';
#account
create database seata_account;
use seata_account;
CREATE TABLE t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY 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 '0' COMMENT '剩餘可用額度'
)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO t_account(`id`,`user_id`,`total`,`used`,`residue`)VALUES('1','1','1000','0','1000');
SELECT * FROM t_account;
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';
#退出mysql
exit
#退出容器
exit
訂單/庫存/賬戶業務微服務準備
訂單模塊
-
新建模塊seata-order-service2001
-
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> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>1.2.0</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--jdbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</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>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
-
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 seaver的 地址配置,此處可以集羣配置是個數組 default: 10.211.55.26:8091 nacos: discovery: server-addr: 10.211.55.26:8848 #nacos datasource: # 當前數據源操作類型 type: com.alibaba.druid.pool.DruidDataSource # mysql驅動類 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://10.211.55.26:3305/seata_storage?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: 123456 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 = "10.211.55.26: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 = "10.211.55.26:8848" #nacos namespace = "" username = "" password = "" } eureka { serviceUrl = "http://localhost:8761/eureka" weight = "1" } redis { serverAddr = "localhost:6379" db = "0" password = "" timeout = "0" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } consul { serverAddr = "127.0.0.1:8500" } etcd3 { serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig type = "file" nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "" password = "" } consul { serverAddr = "127.0.0.1:8500" } apollo { appId = "seata-server" apolloMeta = "http://192.168.1.204:8801" namespace = "application" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } }
-
domain
CommonResult@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); } }
Order
@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 public interface OrderDao { //1 新建訂單 int create(Order order); //2 修改訂單狀態,從0改爲1 int update(@Param("userId") Long userId, @Param("status") Integer status); }
-
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.angenin.springcloud.dao.OrderDao"> <resultMap id="BaseResultMap" type="com.angenin.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.angenin.springcloud.domain.Order" useGeneratedKeys="true" keyProperty="id"> insert into t_order(`user_id`, `product_id`, `count`, `money`, `status`) values(#{userId}, #{productId}, #{count}, #{money}, 0); </insert> <update id="update" parameterType="com.angenin.springcloud.domain.Order"> update t_order set `status` = 1 where `user_id` = #{userId} and `status` = #{status}; </update> </mapper>
-
service
StorageService@FeignClient(value = "seata-storage-service") public interface StorageService { //減庫存 @PostMapping(value = "/storage/decrease") CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count); }
AccountService
@FeignClient(value = "seata-account-service") public interface AccountService { @PostMapping(value = "/account/decrease") CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money); }
OrderService
public interface OrderService { void create(Order order); }
-
impl
OderServiceImpl@Slf4j @Service public class OderServiceImpl implements OrderService { @Resource private OrderDao orderDao; @Resource private StorageService storageService; @Resource private AccountService accountService; @Override public void create(Order order) { //1. 新建訂單 log.info("-------> 開始新建訂單"); orderDao.create(order); //2. 扣減庫存 log.info("-------> 訂單微服務開始調用庫存,做扣減count"); storageService.decrease(order.getProductId(), order.getCount()); log.info("-------> 訂單微服務開始調用庫存,做扣減完成"); //3. 扣減賬號餘額 log.info("-------> 訂單微服務開始調用賬號,做扣減money"); accountService.decrease(order.getUserId(), order.getMoney()); log.info("-------> 訂單微服務開始調用賬號,做扣減完成"); //4. 修改訂單狀態,1代表已完成 log.info("-------> 修改訂單狀態"); orderDao.update(order.getUserId(), 0); log.info("-------> 修改訂單狀態完成"); log.info("-------> 新建訂單完成"); } }
-
controller
OrderController@RestController public class OrderController { @Resource private OrderService orderService; @GetMapping("/order/create") public CommonResult create(Order order){ orderService.create(order); return new CommonResult(200, "訂單創建成功!"); } }
-
config
MybatisConfig@MapperScan("com.angenin.springcloud.dao") @Configuration public class MybatisConfig { }
DataSourceProxyConfig
//使用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 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(); } }
-
主啓動類
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //取消數據源的自動創建 @EnableDiscoveryClient @EnableFeignClients public class SeataOrderMain2001 { public static void main(String[] args) { SpringApplication.run(SeataOrderMain2001.class,args); } }
-
啓動2001
官方列舉的常見問題:https://seata.io/zh-cn/docs/overview/faq.html
庫存模塊
-
新建模塊seata-storage-service2002
-
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> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>1.2.0</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--jdbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</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>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
-
yml
server: port: 2002 spring: application: name: seata-storage-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 seaver的 地址配置,此處可以集羣配置是個數組 default: 10.211.55.26:8091 nacos: discovery: server-addr: 10.211.55.26:8848 #nacos datasource: # 當前數據源操作類型 type: com.alibaba.druid.pool.DruidDataSource # mysql驅動類 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://10.211.55.26:3305/seata_account?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: 123456 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 = "10.211.55.26: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 = "10.211.55.26:8848" #nacos namespace = "" username = "" password = "" } eureka { serviceUrl = "http://localhost:8761/eureka" weight = "1" } redis { serverAddr = "localhost:6379" db = "0" password = "" timeout = "0" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } consul { serverAddr = "127.0.0.1:8500" } etcd3 { serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig type = "file" nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "" password = "" } consul { serverAddr = "127.0.0.1:8500" } apollo { appId = "seata-server" apolloMeta = "http://192.168.1.204:8801" namespace = "application" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } }
-
domain
CommonResult@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); } }
Storage
@Data @AllArgsConstructor @NoArgsConstructor public class Storage { private Long id; private Long productId; private Integer total; private Integer used; private Integer residue; }
-
dao
@Mapper public interface StorageDao { void decrease(@Param("productId") Long productId, @Param("count") Integer count); }
-
mapper
StorageMapper.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.angenin.springcloud.dao.StorageDao"> <resultMap id="BaseResultMap" type="com.angenin.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>
-
service
StorageServicepublic interface StorageService { void decrease(Long productId, Integer count); }
-
impl
StorageServiceImpl@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("----> StorageService中扣減庫存"); storageDao.decrease(productId, count); LOGGER.info("----> StorageService中扣減庫存完成"); } }
-
controller
StorageController@RestController public class StorageController { @Resource private StorageService storageService; @RequestMapping("/storage/decrease") public CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count){ storageService.decrease(productId, count); return new CommonResult(200, "扣減庫存成功!"); } }
-
config
MyBatisConfig@Configuration @MapperScan({"com.angenin.springcloud.dao"}) public class MyBatisConfig { }
DataSourceProxyConfig
@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(); } }
-
主啓動類
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @EnableFeignClients @EnableDiscoveryClient public class SeataStorageMain2002 { public static void main(String[] args) { SpringApplication.run(SeataStorageMain2002.class,args); } }
-
啓動2002
賬戶模塊
-
新建模塊seata-account-service2003
-
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> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>1.2.0</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--jdbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</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>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
-
yml
server: port: 2003 spring: application: name: seata-account-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 seaver的 地址配置,此處可以集羣配置是個數組 default: 10.211.55.26:8091 nacos: discovery: server-addr: 10.211.55.26:8848 #nacos datasource: # 當前數據源操作類型 type: com.alibaba.druid.pool.DruidDataSource # mysql驅動類 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://10.211.55.26:3305/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: 123456 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 = "10.211.55.26: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 = "10.211.55.26:8848" #nacos namespace = "" username = "" password = "" } eureka { serviceUrl = "http://localhost:8761/eureka" weight = "1" } redis { serverAddr = "localhost:6379" db = "0" password = "" timeout = "0" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } consul { serverAddr = "127.0.0.1:8500" } etcd3 { serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig type = "file" nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "" password = "" } consul { serverAddr = "127.0.0.1:8500" } apollo { appId = "seata-server" apolloMeta = "http://192.168.1.204:8801" namespace = "application" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } }
-
domain
CommonResult@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); } }
Account
@Data @AllArgsConstructor @NoArgsConstructor public class Account { private Long id; private Long userId; private BigDecimal total; private BigDecimal used; private BigDecimal residue; }
-
dao
AccountDao@Mapper public interface AccountDao { void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money); }
-
mapper
AccountMapper.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.angenin.springcloud.dao.AccountDao"> <resultMap id="BaseResultMap" type="com.angenin.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 used = used + #{money}, residue = residue - #{money} where user_id = #{userId}; </update> </mapper>
-
service
AccountServicepublic interface AccountService { void decrease(Long userId, BigDecimal money); }
-
impl
AccountServiceImpl@Service public class AccountServiceImpl implements AccountService { private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class); @Resource private AccountDao accountDao; @Override public void decrease(Long userId, BigDecimal money) { LOGGER.info("---> AccountService中扣減賬戶餘額"); accountDao.decrease(userId, money); LOGGER.info("---> AccountService中扣減賬戶餘額完成"); } }
-
controller
AccountController@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, "扣減庫存成功!"); } }
-
config
MyBatisConfig@Configuration @MapperScan({"com.angenin.springcloud.dao"}) public class MyBatisConfig { }
DataSourceProxyConfig
@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(); } }
-
主啓動類
SeataAccountMain2003@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @EnableFeignClients @EnableDiscoveryClient public class SeataAccountMain2003 { public static void main(String[] args) { SpringApplication.run(SeataAccountMain2003.class,args); } }
-
啓動2003
Test
正常下單
啓動2001,2002,2003
在瀏覽器輸入:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=10
超時異常
-
停止2003。
-
在2003的AccountServiceImpl裏的decrease中添加
//模擬超時異常,暫停20秒 try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
-
重新啓動2003。
-
刷新頁面
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=10
超時異常後,order添加了訂單,而且storage的庫存和account的餘額都發生了變化。
因爲feign調用時間默認是1秒,超過1秒就不等待,直接返回超時異常,但是account在20秒後還是會去扣餘額,而且沒有回滾,所以order添加了訂單,storage的庫存也發生了變化。
而且feign有超時重試機制,所以可能會多次扣款。
-
停止2001。
-
在2001的OderServiceImpl裏的create方法上加上:
//name隨便命名,只要不重複即可 //rollbackFor = Exception.class表示出現所有異常都回滾 //rollbackFor表示哪些需要回滾 //noRollbackFor表示哪些不需要回滾 @GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)
-
重啓2001。
-
刷新頁面
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=10
訂單沒有添加,storage和account也沒變化,回滾成功。
補充
Seata
TC/TM/RM三組件
分佈式事務的執行流程
seata文檔:http://seata.io/zh-cn/docs/overview/what-is-seata.html
下一篇筆記:SpringCloud入門學習筆記(21高級部分,雪花算法【snowflake】)
學習視頻(p138-p148):https://www.bilibili.com/video/BV18E411x7eT?p=138