序言:
分布式事务对于现在的分布式的项目来说很常见的一个问题,此次采用的是阿里开源的seata,seata官网,
此次集成不需要其seata在客户端配置相关的file.conf 、registry.conf 配置文件。
不需要配置相关的配置文件时需要引入:seata-spring-boot-starter 的pom依赖,这个是我推荐使用的集成方式。
在此工程中实践分布式事务的流程是为:
———today服务创建订单,yesterday服务进行扣减余额,若yesterday服务扣减余额发生异常,则两个服务所执行的SQL数据进行回滚。
一、工程中的代码部分:
1、父工程的依赖:
<spring-boot.version>2.3.3.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
<dubbo.version>2.7.8</dubbo.version>
<seata.version>1.4.0</seata.version>
<dependencyManagement>
<dependencies>
<!--spring boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--seata-分布式事务管理-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
2、子工程的依赖:
说明:即today服务和yesterday服务需要注册到nacos中的接口所在的工程,today和yesterday都会引入此工程
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
3、today服务和yesterday服务的启动类:
import io.seata.spring.annotation.datasource.EnableAutoDataSourceProxy;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
//开启dubbo
@EnableDubbo
//扫描工程中所依赖的包中的bean,将注册到springioc中
@ComponentScan("com.day")
//开启分布式事务的代理,将事务交给seata
@EnableAutoDataSourceProxy
//参考了pig-cloud开源项目中的动态数据源
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class XXXXXXXApplication {
public static void main(String[] args) {
SpringApplication.run(DayTodayApplication.class, args);
}
}
4、today服务和yesterday服务的配置文件:
默认的配置文件:application.properties
公共的配置参数的配置文件:application-common.properties
当前开发环境的配置文件:application-dev.properties
其seata配置信息主要参考了seata官网:http://seata.io/zh-cn/docs/user/registry/nacos.html
在这里主要写出dubbo的配置信息和seata的配置信息:
#dubbo配置
dubbo.application.name=today-provider
dubbo.config-center.timeout=500000
dubbo.registry.address=nacos://${nacos.discovery.server-addr}
dubbo.protocol.name=dubbo
dubbo.protocol.port=20886
dubbo.scan.base-packages=com.day.api.provider.today
dubbo.consumer.check=false
dubbo.application.qos-enable=false
dubbo.application.qos-accept-foreign-ip=false
#seata配置
nacos.discovery.server-addr=47.104.78.115:8848
#seata-config
seata.application-id=${spring.application.name}
seata.tx-service-group=day_today_tx_group
seata.service.vgroup-mapping.day_today_tx_group=seata-days
seata.service.grouplist.seata-days=127.0.0.1:8091
seata.registry.type=nacos
seata.registry.nacos.application=seata-server
seata.registry.nacos.server-addr=${nacos.discovery.server-addr}
seata.registry.nacos.group=SEATA_GROUP
seata.registry.nacos.username=nacos
seata.registry.nacos.password=nacos
5、today服务中调用yesterday服务的代码:
控制层:
@RestController
@Api(tags = "今天的控制层信息")
@RequestMapping("/today")
public class TodayController {
private static final Logger log = LoggerFactory.getLogger(TodayController.class);
@DubboReference(group = DubboNacosGroup.YESTERDAY_DUBBO_NACOS)
private YesterdayProvider yesterdayProvider;
@Autowired
private IOrderService orderService;
@ApiOperation(value = "测试分布式事务")
@RequestMapping(value = "testDistributedTransaction", method = RequestMethod.POST)
public JsonResult testDistributedTransaction() {
Order order = new Order();
order.setUserId("1001");
order.setAmount(300d);
return JsonResult.success(orderService.saveOrder(order));
}
}
service层:
/**
* 用于测试分布式事务功能
*
* @param order
* @author wangjunming
* @since 2020/11/8 14:46
*/
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public boolean saveOrder(Order order) {
log.info("开始创建订单");
final boolean save = save(order);
boolean updataAmount = false;
if(save){
log.info("完成创建订单");
log.info("开始扣减余额");
AccountApiEntity accountApiEntity = new AccountApiEntity();
accountApiEntity.setUserId(order.getUserId());
accountApiEntity.setAmount(order.getAmount());
updataAmount = yesterdayProvider.updataAmountByUserId(accountApiEntity);
log.info("完成扣减余额");
}
log.info("开始更新订单状态");
// int i = 12/0;
boolean update = false;
if(updataAmount){
order.setStatus(Integer.valueOf("1"));
update = update(order);
log.info("完成更新订单状态");
}
return save && updataAmount && update;
}
6、yesterday服务注册到nacos服务中的接口和类:
其中使用到的model和接口都在第二步中所创建的子工程中。这个时候需要调用在yesterday服务中的用户余额服务进行扣减余额。
接口:
public interface YesterdayProvider {
/**
* 用于测试分布式事务
*
* @author wangjunming
* @since 2020/11/8 14:52
*/
boolean updataAmountByUserId(AccountApiEntity accountApiEntity);
}
实现类:
import com.day.api.config.DubboNacosGroup;
import com.day.api.entitys.AccountApiEntity;
import com.day.api.provider.yesterday.YesterdayProvider;
import com.day.yesterday.persistence.entity.Account;
import com.day.yesterday.persistence.service.IAccountService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@DubboService(group = DubboNacosGroup.YESTERDAY_DUBBO_NACOS)
public class YesterdayProviderImpl implements YesterdayProvider {
@Autowired
private IAccountService accountService;
/**
* 用于测试分布式事务
*
* @param accountApiEntity 扣减金额
* @author wangjunming
* @since 2020/11/8 14:52
*/
@Override
public boolean updataAmountByUserId(AccountApiEntity accountApiEntity) {
Account account = new Account();
account.setUserId(accountApiEntity.getUserId());
account.setAmount(accountApiEntity.getAmount());
return accountService.updataAmountByUserId(account) != null;
}
}
7、yesterday服务中调用用户服务进行扣减余额
/**
* 用于测试分布式事务,业务为根据用户ID扣减相应的余额
*
* @param account 扣减金额
* @author wangjunming
* @since 2020/11/8 14:18
*/
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public Account updataAmountByUserId(Account account){
final Account selOneAccount = selOne(account);
if (selOneAccount == null) {
log.error("余额不足!");
return null;
}
if (account.getAmount().compareTo(selOneAccount.getAmount()) > 0) {
log.error("余额不足!");
return null;
}
final double amountPoor = selOneAccount.getAmount() - account.getAmount();
selOneAccount.setAmount(amountPoor);
final boolean update = accountMapper.updateById(selOneAccount) > 0;
int i = 12/0;
return update ? selOneAccount : null;
}
--------------------------至此在代码方面的工作到此结束。
二、部署seata的server端的部分:
8、初始化seata服务端和各个服务端所需要的数据库
————获取seata服务端(server)初始化SQL语句的官网地址:https://github.com/seata/seata/blob/1.4.0/script/server/db/mysql.sql
-- 创建数据库:
CREATE DATABASE `seata`;
-- -------------------------------- 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(6),
`gmt_modified` DATETIME(6),
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;
————创建today服务中的数据库和订单表:
# -----------创建订单表---------------------------------------------------------------
# 创建 seata_order 数据库
CREATE DATABASE `seata_order`;
# 创建订单表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_order
-- ----------------------------
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_no` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`commodity_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` int(5) NULL DEFAULT 0,
`amount` double(14, 2) NULL DEFAULT 0.00,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB
AUTO_INCREMENT = 1
CHARACTER SET = utf8
COLLATE = utf8_general_ci
ROW_FORMAT = Dynamic;
————创建yesterday服务中的数据库和用户钱包表:
# --------------创建用户钱包表---------------------------------------------------------
#创建 seata_pay 数据库
CREATE DATABASE `seata_pay`;
#创建用户钱包表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for account
-- ----------------------------
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`amount` double(14, 2) NULL DEFAULT 0.00,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB
AUTO_INCREMENT = 3
CHARACTER SET = utf8
COLLATE = utf8_general_ci
ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of account
-- ----------------------------
#初始化数据
INSERT INTO `account`
VALUES (1, '1', 4000.00);
INSERT INTO `account`
VALUES (2, '1001', 50000.00);
SET FOREIGN_KEY_CHECKS = 1;
以及在today服务的数据库(seata_order)和yesterday服务的数据库(seata_pay)所创建的undo_log表
获取创建(client端)表的SQL语句官网地址:https://github.com/seata/seata/blob/1.4.0/script/client/at/db/mysql.sql
-- for AT mode you must to init this sql for you business database. the seata server not need it.
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';
9、配置seata的server端所需要的配置信息
下载seata的server端地址:https://github.com/seata/seata/releases/tag/v1.4.0
其配置信息参考:http://seata.io/zh-cn/docs/user/registry/nacos.html
在其解压后修改conf文件下的 file.conf 和 registry.conf
1.修改 file.conf ,其中红色部分为此次修改的内容(就是将刚刚创建好的数据库):
## transaction log store, only used in seata-server
store {
## store mode: file、db、redis
mode = "db"
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "123456"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
2.修改 registry.conf 文件,其中红色部分是此次所需要修改的地方,以及与项目中配置文件的对应关系,在下方也有图片标注:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "seata-days"
# 重点说明:cluster = "seata-days"
# 其项目配置文件中的 seata.service.grouplist.seata-days=127.0.0.1:8091 ,此配置信息的 seata-days ,需要对应此配置文件中 cluster = "seata-days" 中 的 "seata-days" 保持一致。
# 其项目配置文件中的 seata.service.vgroup-mapping.day_today_tx_group=seata-days ,此配置信息的 seata-days ,需要对应此配置文件中 cluster = "seata-days" 中 的 "seata-days" ,保持一致,
# 也就是 与 seata.service.grouplist.seata-days=127.0.0.1:8091 ,此配置信息的 seata-days 需要保持一致。
username = "nacos"
password = "nacos"
}
}
此配置文件与项目的配置信息对应关系:
10、启动seata服务端
注意:在启动之前必须启动成功nacos1.3.0,本地则以单机模式启动nacos
启动seata服务:seata-server.bat
在nacos(本地启动nacos命令:startup.cmd -m standalone )中注册的seata服务是:
————————至此则创建完成数据库,以及成功部署seata服务。
三、测试分布式事务是否成功
11、启动today服务和yesterday服务,并查看是否已注册
12、测试:
抛出异常:
AT事务在此得到的效果,
——————至此springboot集成dubbo2.7.8+nacos1.3.2+seata1.4.0 分布式事务解决方案,已实现效果。
docker中安装nacos1.3.1
参考:
1、https://www.cnblogs.com/binz/p/12295346.html
2、https://www.jianshu.com/p/e053f016371a
命令:
1.docker search nacos
2.docker pull nacos/nacos-server:1.3.1
3.docker run -d -p 8848:8848 -e MODE=standalone -v /usr/nacos/properties/custom.properties:/home/nacos/init.d/custom.properties -v /usr/nacos/logs:/home/nacos/logs --restart always --name nacos nacos/nacos-server:1.3.1
项目中使用的整合参考:
springboot集成dubbo2.7.8+nacos1.3.2
参考: https://blog.csdn.net/lwb314/article/details/108233863
springboot整合mybatis-plus3.3.0+mybatis-plus动态数据源+druid1.1.22
参考: https://gitee.com/log4j/pig/tree/master/pig-common/pig-common-datasource 此子工程。