SpringCloud Alibaba Seata 处理分布式事务

一、分布式事务问题

① 分布式前

​ 单机单库没这个问题 从 1: 1 -> 1:N -> N: N

② 分布式之后

​ 单体应用被拆分成微服务应用, 原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源。

​ 业务操作需要调用三个服务来完成。 此时每个服务内部的数据一致性由本地事务来保证, 但是全局的数据一致性问题没法保证。

image-20201115231222106

image-20201115231230163

③ 一句话

​ 一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用, 就会产生分布式事务问题 。

二、Seata 简介

① 是什么

​ Seata 是一款开源的分布式事务解决方案, 致力于在微服务架构下提供高性能和简单易用的分布式事务服务

​ 官网地址 http://seata.io/zh-cn/

② 能干嘛

​ 一个典型的分布式事务过程

  1. 分布式事务处理过程的-ID+三组件模型
  • Transaction ID XID

    全局唯一的事务 ID

  • 3 组件概念

    ==Transaction Coordinator(TC)==
    事务协调器, 维护全局事务的运行状态, 负责协调并驱动全局事务的提交或回滚;

    ==Transaction Manager(TM)==
    控制全局事务的边界, 负责开启一个全局事务, 并最终发起全局提交或全局回滚的决议;

    ==Resource Manager(RM)==
    控制分支事务, 负责分支注册, 状态汇报, 并接收事务协调器的指令, 驱动分支(本地) 事务的提交和回滚;

  1. 处理过程

image-20201115232305577

  1. 去哪下

    发布说明: https://github.com/seata/seata/releases

  2. 怎么玩

  • Spring 本地@Transactional
  • 全局@GlobalTransactional

image-20201115232603341

三、Seata-Server 安装

① 官网地址

1
http://seata.io/zh-cn/

② 下载版本

image-20201115232734543

③ 修改配置文件

  • seata-server-0.9.0.zip 解压到指定目录并修改 conf 目 录下的 file.conf 配置文件
  1. 先备份原始 file.conf 文件 、

主要修改: 自定义事务组名称+事务日志存储模式为 db+数据库连接信息

file.conf

  1. service 模块
1
vgroup_mapping.fsp_tx_group = "default"

image-20201116205819062

  1. store 模块
1
2
3
4
mode = "db"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "你自己的密码"

image-20201116152647090

④ mysql5.7 数据库新建库 seata

1
create database seata;

⑤ 在 seata 库里建表

建 表 db_store.sql 在 \seata-server-0.9.0\seata\conf 目 录 里 面 db_store.sq

image-20201116153112748

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
-- the table to store GlobalSession data
drop table if exists `global_table`;
create table `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`)
);

-- the table to store BranchSession data
drop table if exists `branch_table`;
create table `branch_table` (
`branch_id` bigint not null,
`xid` varchar(128) not null,
`transaction_id` bigint ,
`resource_group_id` varchar(32),
`resource_id` varchar(256) ,
`lock_key` varchar(128) ,
`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`)
);

-- the table to store lock data
drop table if exists `lock_table`;
create table `lock_table` (
`row_key` varchar(128) not null,
`xid` varchar(96),
`transaction_id` long ,
`branch_id` long,
`resource_id` varchar(256) ,
`table_name` varchar(32) ,
`pk` varchar(36) ,
`gmt_create` datetime ,
`gmt_modified` datetime,
primary key(`row_key`)
);

⑥ 修 改 seata-server-0.9.0\seata\conf 目 录 下 的 registry.conf 配置文件 、

1
2
3
4
5
6
7
8
9
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"

nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}

image-20201116153247607

==目的是==: 指明注册中心为 nacos, 及修改 nacos 连接信息

⑦ 先启动 Nacos 端口号 8848

⑧ 再启动 seata-server

  • seata-server.bat

image-20201116154328861

四、订单/库存/账户业务数据库准备

① 以下演示都需要先启动 Nacos 后启动 Seata, 保证两个都 OK

image-20201116154605362

② 分布式事务业务说明

==业务说明==

image-20201116154704659

下订单–>扣库存–>减账户( 余额)

③ 创建业务数据库

  • seata_order: 存储订单的数据库
  • seata_storage:存储库存的数据库
  • seata_account: 存储账户信息的数据库

建表 SQL

1
2
3
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;

④ 按照上述 3 库分别建对应业务表

  • seata_order 库下建 t_order 表
1
2
3
4
5
6
7
8
9
10
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;
  • seata_storage 库下建 t_storage 表
1
2
3
4
5
6
7
8
9
10
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;
  • seata_account 库下建 t_account 表
1
2
3
4
5
6
7
8
9
10
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 seata_account.t_account(`id`,`user_id`,`total`,`used`,`residue`)VALUES('1','1','1000','0','1000')
SELECT * FROM t_account;

⑤ 按照上述 3 库分别建对应的回滚日志表

  • 订单-库存-账户 3 个库下都需要建各自的回滚日志表
  • \seata-server-0.9.0\seata\conf 目录下的 db_undo_log.sql

建表 SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
drop table `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;

⑥ 最终效果

image-20201116161835626

五、订单/库存/账户业务微服务准备

1. 业务需求

  • 下订单->减库存->扣余额->改(订单) 状态

2. 新建订单 Order-Module

建 Module : seata-order-service2001

POM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<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>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web-actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--mysql-druid-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<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>

</project>

YML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
server:
port: 2001

spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
# 自定义事务组名称需要与seata-server中的对应
tx-service-group: fsp_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
# 当前数据源操作类型
type: com.alibaba.druid.pool.DruidDataSource
# mysql驱动类
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order
username: root
password: 6090
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info

mybatis:
mapper-locations: classpath:mapper/*.xml

file.conf

​ 拷贝 seata-server/conf 目录下的 file.conf 到项目 resources 里面

image-20201116163527578

registry.conf

​ 拷贝 seata-server/conf 目录下的 registry.conf 到项目 resources 里面

domain

  • CommonResult
1
2
3
4
5
6
7
8
9
10
11
12
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>{
private Integer code;
private String message;
private T date;

public CommonResult(Integer code, String message){
this(code, message,null);
}
}
  • Order
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {

private Long userId;

private Long productId;

private Integer count;

private BigDecimal money;

private Integer status; //订单状态: 0 创建中; 1:已完结

}

Dao 接口及实现

  • OrderDao
1
2
3
4
5
6
7
8
9
@Mapper
public interface OrderDao {

// 新建订单
void create(Order order);

// 修改订单状态,从零开始
void update(@Param("userId")Long userId, @Param("status") Integer status);
}
  • resources 文 件 夹 下 新 建 mapper 文 件 夹 后 添 加 OrderMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?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.oy.springcloud.dao.OrderDao">

<resultMap id="BaseResultMap" type="com.oy.springcloud.domain.Order">
<id column="id" property="id" jdbcType="BIGINT"></id>
<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>

<!-- void create(Order order);-->
<insert id="create">
insert into t_order(id,user_id, product_id, count, money,status) values(null,#{userId},#{productId},#{count},#{money},0);
</insert>
<!-- void update(@Param("useId")Long userId, @Param("status") Integer status);-->
<update id="update">
update t_order set status = 1 where user_id=#{userId} and status = #{status};
</update>

</mapper>

Service 接口及实现

  • OrderService
1
2
3
public interface OrderService {
void create(Order order);
}
  • OrderServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

@Resource
private OrderDao orderDao;

@Resource
private StorageService storageService;

@Resource
private AccountService accountService;

@Override
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
public void create(Order order) {
log.info("----->开始新建订单");
//新建订单
orderDao.create(order);
//扣减库存
log.info("----->订单微服务开始调用库存, 做扣减 Count");
storageService.decrease(order.getProductId(),order.getCount());
log.info("----->订单微服务开始调用库存, 做扣减 end");

//扣减账户
log.info("----->订单微服务开始调用账户, 做扣减 Money");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("----->订单微服务开始调用账户, 做扣减 end");
//修改订单状态, 从零到 1 代表已经完成
log.info("----->修改订单状态开始");
orderDao.update(order.getUserId(),0);
log.info("----->修改订单状态结束");
log.info("----->下订单结束了");
}
}
  • StorageService
1
2
3
4
5
6
@FeignClient(value = "seata-storage-service")
public interface StorageService {

@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
  • AccountService
1
2
3
4
5
6
@FeignClient(value = "seata-account-service")
public interface AccountService {

@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
@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
1
2
3
4
5
@Configuration
@MapperScan({"com.oy.springcloud.dao"})
public class MyBatisConfig {

}
  • DataSourceProxyConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.oy.springcloud.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.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

@Configuration
public class DataSourceProxyConfig {

@Value("${mybatis.mapperLocations}")
private String mapperLocations;

@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}

@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}

@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}

}

主启动

1
2
3
4
5
6
7
8
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消数据源自动创建的配置
public class SeataOrderMainApp2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}

3.新建库存 Storage-Module

**seata-order-service2002 **

POM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<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>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<!--feign-->
<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-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</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.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

YML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
server:
port: 2002

spring:
application:
name: seata-storage-service
cloud:
alibaba:
seata:
tx-service-group: fsp_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage
username: root
password: 123456

logging:
level:
io:
seata: info

mybatis:
mapperLocations: classpath:mapper/*.xml

file.conf

registry.conf

  • 拷贝 seata-server/conf 目录下的file.confregistry.conf 到项目 resources 里面

image-20201116173327366

domain

  • CommonResult
1
2
3
4
5
6
7
8
9
10
11
12
13
@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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
public class Storage {

private Long id;

// 产品id
private Long productId;

// 总库存
private Integer total;

// 已用库存
private Integer used;

// 剩余库存
private Integer residue;
}

Dao 接口及实现

  • StorageDao
1
2
3
4
5
6
7
8
@Mapper
public interface StorageDao {

/**
* 扣减库存信息
*/
void decrease(@Param("productId")Long productId, @Param("count") Integer count);
}
  • resources 文 件 夹 下 新 建 mapper 文 件 夹 后 添 加 StorageMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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.oy.springcloud.dao.StorageDao">

<resultMap id="BaseResultMap" type="com.oy.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>

<!--void decrease(@Param("productId")Long productId, @Param("count") Integer count);-->
<update id="decrease">
UPDATE t_storage SET used = used + #{count}, residue = residue - #{count} WHERE product_id = #{productId}
</update>
</mapper>

Service 接口及实现

  • StorageService
1
2
3
4
5
6
7
public interface StorageService {

/**
* 扣减库存
*/
void decrease(Long productId, Integer count);
}
  • StorageServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {

private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class);

@Resource
private StorageDao storageDao;

/**
* 扣减库存
*/
@Override
public void decrease(Long productId, Integer count) {
LOGGER.info("------->storage-service 中扣减库存开始");
storageDao.decrease(productId, count);
LOGGER.info("------->storage-service 中扣减库存结束");
}
}

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class StorageController {

@Autowired
private StorageService storageService;

/**
* 扣减库存
*/
@RequestMapping("/storage/decrease")
public CommonResult decrease(Long productId, Integer count){
storageService.decrease(productId, count);
return new CommonResult(200,"扣减库存成功! ");
}
}

Config 配置

  • MyBatisConfig
1
2
3
4
@Configuration
@MapperScan({"com.oy.springcloud.dao"})
public class MyBatisConfig {
}
  • DataSourceProxyConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.oy.springcloud.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.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
* @Author OY
* @Date 2020/11/16
*/
@Configuration
public class DataSourceProxyConfig {

@Value("${mybatis.mapperLocations}")
private String mapperLocations;

@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}

@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}

@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}

}

主启动

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class SeataStorageServiceApplication2002 {
public static void main(String[] args) {
SpringApplication.run(SeataStorageServiceApplication2002.class, args);
}
}

4.新建账户 Account-Module

seata-account-service2003

POM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
    <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>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<!--feign-->
<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-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</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.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

</project>

YML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
server:
port: 2003

spring:
application:
name: seata-account-service
cloud:
alibaba:
seata:
tx-service-group: fsp_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account
username: root
password: 6090

feign:
hystrix:
enabled: false

logging:
level:
io:
seata: info

mybatis:
mapperLocations: classpath:mapper/*.xml

file.conf

registry.conf

image-20201116223930267

domain

  • CommonResult
1
2
3
4
5
6
7
8
9
10
11
12
13
@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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long id;
/**
* 用户 id
*/
private Long userId;
/**
* 总额度
*/
private BigDecimal total;
/**
* 已用额度
*/
private BigDecimal used;
/**
* 剩余额度
*/
private BigDecimal residue;
}

Dao 接口及实现

  • AccountDao
1
2
3
4
5
6
7
@Mapper
public interface AccountDao {
/**
* 扣减账户余额
*/
void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
  • resources 文 件 夹 下 新 建 mapper 文 件 夹 后 添 加 AccountMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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.oy.springcloud.dao.AccountDao">
<resultMap id="BaseResultMap"
type="com.oy.springcloud.domain.Account">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="total" property="total" jdbcType="DECIMAL"/>
<result column="used" property="used" jdbcType="DECIMAL"/>
<result column="residue" property="residue" jdbcType="DECIMAL"/>
</resultMap>
<update id="decrease">
UPDATE t_account SET residue = residue - #{money},used = used + #{money} WHERE user_id = #{userId};
</update>
</mapper>

Service 接口及实现

  • AccountService
1
2
3
4
5
6
public interface AccountService {
/**
* 扣减账户余额
*/
void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
  • AccountServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AccountServiceImpl implements AccountService {
private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);
@Resource
AccountDao accountDao;

@Override
public void decrease(Long userId, BigDecimal money) {
LOGGER.info("------->account-service 中扣减账户余额开始");
try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e)
{ e.printStackTrace(); }
accountDao.decrease(userId,money);
LOGGER.info("------->account-service 中扣减账户余额结束");
}
}

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class AccountController {
@Resource
AccountService accountService;
/**
* 扣减账户余额
*/
@RequestMapping("/account/decrease")
public CommonResult decrease(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money){
accountService.decrease(userId,money);return new CommonResult(200,"扣减账户余额成功! ");
}
}

Config 配置

  • MyBatisConfig
1
2
3
4
5
@Configuration
@MapperScan({"com.oy.springcloud.dao.AccountDao"})
public class MyBatisConfig {

}
  • DataSourceProxyConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.oy.springcloud.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.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;


@Configuration
public class DataSourceProxyConfig {

@Value("${mybatis.mapperLocations}")
private String mapperLocations;

@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}

@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}

@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}

}

主启动

1
2
3
4
5
6
7
8
9
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataAccountMainApp2003 {

public static void main(String[] args) {
SpringApplication.run(SeataAccountMainApp2003.class, args);
}
}

注意:如果报错 A component required a bean of type 'com.oy.springcloud.dao.AccountDao' that could not be found.

请在启动类上加:

image-20201117225144074

六、Test

① 下订单->减库存->扣余额->改(订单) 状态

image-20201117225245162

我们只需要使用一个 @GlobalTransactional 注解在业务方法上;

② 数据库初始化情况

image-20201117225437068

③ 正常下单

数据库情况

image-20201117231613480

④ 超时异常, 没加@GlobalTransactional

AccountServiceImpl 添加超时

  • 数据库情况

    余额和库存都已经扣除,但是订单状态还是未支付,结果就是,这个订单仍要再次支付

  • 故障情况

  1. 当库存和账户余额扣减后, 订单状态并没有设置为已经完成,没有从零改为 1
  2. 而且由于 feign 的重试机制, 账户余额还有可能被多次扣减

⑤ 超时异常, 添加@GlobalTransactional

AccountServiceImpl 添加超时

OrderServiceImpl @GlobalTransactional

下单后数据库数据并没有任何改变, 记录都添加不进来

七、Seata 之原理简介

① Seata

​ 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案 Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架 2020 起初, 参加工作后用 1.0 以后的版本

image-20201117232534917

② 再看 TC/TM/RM 三大组件

分布式事务的执行流程

  1. TM 开启分布式事务(TM 向 TC 注册全局事务记录)
  2. 换业务场景, 编排数据库, 服务等事务内资源(RM 报资源准备状态)
  3. TM 结束分布式事务, 事务一阶段结束(TM 通知 TC 滚分布式事务)
  4. TC 汇总事务信息, 决定分布式事务是提交还是回滚
  5. TC 通知所有 RM 提交/回滚资源, 事务二阶段结束

③ AT 模式如何做到对业务的无侵入

image-20201117232704461

  • 一阶段加载

image-20201117232724911

  • 二阶段提交

    image-20201117232741406

  • 二阶段回滚

image-20201117232755908

④ 补充

image-20201117232817071