一.简单介绍
本demo大部分采用官网的例子,涉及到一个业务入口服务(business),两个微服务(订单服务-order,仓库服务-stock),采用nacos配置,分布式事务用Seata。
相关版本:nacos 采用1.1.4 ,Seata采用seata-1.4.0,
二.相关数据库准备
需要搭建两个数据库(采用mysql数据库),订单服务连接seata-order,仓库服务连接seata-stock,
undo_log建表语句如下(官网地址):
-- 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';
注意:undo_log表需要在两个数据库都执行。
业务表order_tbl建表sql如下:
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
业务表storage_tbl建表sql如下:
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 初始化库存模拟数据
INSERT INTO storage_tbl (id, commodity_code, count) VALUES (1, 'product-1', 1000);
INSERT INTO storage_tbl (id, commodity_code, count) VALUES (2, 'product-2', 0);
建表成功之后,如下图所示:
三.客服端框架搭建
具体的seata客服端框架,可以参考官网给的例子,springCloud_nacos_seata
直接用idea搭建一个demo,组建common模块,business业务入口模块,order,stock服务模块
1.搭建基础模块(common)
直接看common模块pom.xml,主要是访问数据库的相关jar,和seata-all.jar
seate-all-parent
com.seate.info
1.0-SNAPSHOT
4.0.0
com.seata
common
0.0.1-SNAPSHOT
common
Demo project for Spring Boot
com.alibaba
druid-spring-boot-starter
1.1.10
com.alibaba
druid
mysql
mysql-connector-java
5.1.39
com.baomidou
mybatis-plus-boot-starter
3.1.1
io.seata
seata-all
1.4.0
2.搭建order服务模块(stock服务依赖的包一模一样)
pom.xml如下:
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.0.6.RELEASE
com.seate
order
0.0.1-SNAPSHOT
order
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.cloud
spring-cloud-starter-alibaba-nacos-discovery
0.9.0.RELEASE
com.alibaba.nacos
nacos-client
com.alibaba.nacos
nacos-client
1.1.4
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
2.1.0.RELEASE
io.seata
seata-all
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
com.seata
common
0.0.1-SNAPSHOT
1.8
UTF-8
UTF-8
1.8
Finchley.SR2
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
src/main/java
**/*.yml
**/*.properties
**/*.xml
**/*.conf
false
src/main/resources
**/*.yml
**/*.properties
**/*.xml
**/*.conf
false
3.搭建business模块
不需要连接数据库,所以不需要引用common,但是需要单独依赖seata-all.jar
pom.xml如下:
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.0.6.RELEASE
com.seate
business
0.0.1-SNAPSHOT
business
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.cloud
spring-cloud-starter-alibaba-nacos-discovery
0.9.0.RELEASE
com.alibaba.nacos
nacos-client
com.alibaba.nacos
nacos-client
1.1.4
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
2.1.0.RELEASE
io.seata
seata-all
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
io.seata
seata-all
1.4.0
org.springframework.cloud
spring-cloud-openfeign-core
1.8
UTF-8
UTF-8
1.8
Finchley.SR2
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
src/main/java
**/*.yml
**/*.properties
**/*.xml
**/*.conf
false
src/main/resources
**/*.yml
**/*.properties
**/*.xml
**/*.conf
false
上述服务搭建完成之后的目录如下:
四.客服端配置修改
1.registry.conf文件
这个文件需要存放到根目录,order,stock,business都需要copy一份到根目录,并且这个文件跟seata服务端的一样。服务端配置可以参考上一篇文章(Seata服务端配置(nacos))
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10
nacos {
application = "seata-server"
serverAddr = "nacos的ip地址:8848"
namespace = "df2011b0-ed94-4fd2-9a33-baa6f97f5af5"
group = "SEATA_GROUP"
cluster = "default"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "nacos的ip地址:8848"
namespace = "df2011b0-ed94-4fd2-9a33-baa6f97f5af5"
group = "SEATA_GROUP"
}
}
注:registry中的group和cluster两个属性很重要,不然后面服务启动后会报如下错误:no available service found in cluster 'default', please make sure registry config correct and keep your seata server running
2.application.yml文件
order服务和stock服务配置如下:
server:
port: 8090
servlet:
context-path: /order
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: nacos地址:8848
namespace: df2011b0-ed94-4fd2-9a33-baa6f97f5af5
alibaba:
seata:
tx-service-group: order-tx-grp
datasource:
druid:
url: jdbc:mysql://数据库地址:3306/seata-order?useUnicode=true
driver-class-name: com.mysql.jdbc.Driver
username: username
password: password
feign:
hystrix:
enabled: false
stock服务的配置如下:
server:
port: 8092
servlet:
context-path: /stock
spring:
application:
name: stock-service
cloud:
alibaba:
seata:
tx-service-group: stock-tx-grp
nacos:
discovery:
server-addr: nacos地址:8848
namespace: df2011b0-ed94-4fd2-9a33-baa6f97f5af5
datasource:
druid:
url: jdbc:mysql://数据库地址:3306/seata-stock?useUnicode=true
driver-class-name: com.mysql.jdbc.Driver
username: username
password: password
feign:
hystrix:
enabled: false
注:配置文件中的tx-service-group配置的【stock-tx-grp】必须和seata中在nacos配置的service.vgroupMapping.{事务组名称},一致
business服务配置如下:
server:
port: 8093
servlet:
context-path: /business
spring:
application:
name: business-service
cloud:
alibaba:
seata:
tx-service-group: order-tx-grp
nacos:
discovery:
server-addr: nacos地址:8848
namespace: df2011b0-ed94-4fd2-9a33-baa6f97f5af5
3.数据库代理配置文件(Java文件)
配置文件需要放到项目中,如下:
package com.seate.stock.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
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 javax.sql.DataSource;
@Configuration
public class MyBatisConfig {
/**
* @param sqlSessionFactory SqlSessionFactory
* @return SqlSessionTemplate
*/
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
/**
* 从配置文件获取属性构造datasource,注意前缀,这里用的是druid,根据自己情况配置,
* 原生datasource前缀取"spring.datasource"
*
* @return
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSource druidDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
/**
* 构造datasource代理对象,替换原来的datasource
* @param druidDataSource
* @return
*/
@Primary
@Bean("dataSource")
public DataSourceProxy dataSourceProxy(DataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSourceProxy);
SqlSessionFactory factory = null;
try {
factory = bean.getObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
return factory;
}
/**
* MP 自带分页插件
* @return
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor page = new PaginationInterceptor();
page.setDialectType("mysql");
return page;
}
}
注:AT模式下需要配置
这个配置文件需要放到order,stock服务内,如下图:
配置类配置好之后,在应用启动类中关闭SpringBoot的DataSource自动装载
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(scanBasePackages = {"com.seate"}, exclude = DataSourceAutoConfiguration.class)
public class StockApplication {
public static void main(String[] args) {
SpringApplication.run(StockApplication.class, args);
}
}
五.编写业务代码
order,stock中的业务代码可以参考官网demo。下面只粘贴对应service相关代码,controller代码参考官网。
business服务中代码如下:
package com.seate.business.service;
import com.seate.business.feign.OrderFeignClient;
import com.seate.business.feign.StorageFeignClient;
import com.seate.business.vo.CommintRequest;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 功能描述:
*
* @Author:
* @Date: 2020/12/7 17:58
*/
@Service
@Slf4j
public class BusinessService {
@Resource
private OrderFeignClient orderFeignClient;
@Resource
private StorageFeignClient storageFeignClient;
/**
* 下单:创建订单、减库存,涉及到两个服务
*
* @param userId
* @param commodityCode
* @param count
*/
@GlobalTransactional(rollbackFor = Exception.class)
public void placeOrder(String userId, String commodityCode, Integer count) {
CommintRequest request=new CommintRequest();
request.setUserId(userId);
request.setCommodityCode(commodityCode);
request.setCount(count);
orderFeignClient.placeOrderCommit(request);
storageFeignClient.deduct(commodityCode, count);
}
}
注:
@GlobalTransactional 开启全局事务,放在business入口处,其中通过feign调用了order和stock服务
order服务的业务处理方法如下:
package com.seate.order.service;
import com.seate.order.dao.OrderInfoDao;
import com.seate.order.feign.StorageFeignClient;
import com.seate.order.model.OrderInfo;
import com.seate.order.vo.CommintRequest;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoProperties;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
/**
* 功能描述:
*
* @Author:
* @Date: 2020/12/7 17:04
*/
@Slf4j
@Service
public class OrderInfoService {
@Resource
private OrderInfoDao orderInfoDao;
public void placeOrder(CommintRequest request) {
BigDecimal orderMoney = new BigDecimal(request.getCount()).multiply(new BigDecimal(5));
OrderInfo order = new OrderInfo()
.setUserId(request.getUserId())
.setCommodityCode(request.getCommodityCode())
.setCount(request.getCount())
.setMoney(orderMoney);
orderInfoDao.insert(order);
}
}
order服务模拟订单创建操作
stock服务业务代码如下:
package com.seate.stock.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.seate.stock.dao.StorageInfoDao;
import com.seate.stock.model.StorageInfo;
import com.seate.stock.service.IStorageInfoService;
import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
/**
* 功能描述:
*
* @Author:
* @Date: 2020/12/7 17:44
*/
@Service
@Slf4j
public class StorageInfoService implements IStorageInfoService {
@Resource
private StorageInfoDao storageInfoDao;
/**
* 减库存
*
* @param commodityCode
* @param count
*/
@Override
public void deduct(String commodityCode, int count) {
log.info(">>>>>>StorageInfoService begin,XID为" + RootContext.getXID());
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.setEntity(new StorageInfo().setCommodityCode(commodityCode));
StorageInfo storage = storageInfoDao.selectOne(wrapper);
storage.setCount(storage.getCount() - count);
storageInfoDao.updateById(storage);
if (commodityCode.equals("product-2")) {
throw new RuntimeException("异常:模拟业务异常:Storage branch exception");
}
}
}
在stock服务中模拟抛出了异常,看order服务创建的订单是否会回滚
六.进行全局事务验证
启动nacos,seata服务,然后启动stock,order,business服务,
查看nacos服务列表如下:
上述表示各个服务都启动正常,并注册到nacos成功,
查看Seata服务端日志如下:
查看order,stock,business服务,出现如下日志表示本地事务注册成功:
1.验证回滚功能
初始值:订单表order_tbl 没有数据,仓库表库存为10
访问接口:http://localhost:8093/business/placeOrder/rollback
返回:
{
timestamp: "2020-12-15T11:35:48.771+0000"
status: 500
error: "Internal Server Error"
message: "status 500 reading StorageFeignClient#deduct(String,Integer); content: {"timestamp":"2020-12-15T11:35:48.663+0000","status":500,"error":"Internal Server Error","message":"异常:模拟业务异常:Storage branch exception","path":"/stock/storage/deduct"}"
path: "[/business/placeOrder/rollback](chrome-extension://gpifhhbaillafhgecomgdilnmplnoelg/business/placeOrder/rollback "Click to insert into URL field")"
}
查看order数据库看订单是否创建成功:
查看stock数据库看库存是否扣减:
查看order服务日志如下:
查看stock服务日志如下:
查看business服务日志如下:
2.验证提交功能
初始值:订单表order_tbl 没有数据,仓库表库存为10
访问接口:http://localhost:8093/business/placeOrder/commit
返回值:true
查看order数据库订单是否创建成功:
查看stock数据库库存是否扣减:
查看order服务日志如下:
查看stock服务日志如下:
查看business服务日志如下:
七.搭建过程遇到的些异常情况
1.nacos配置读取不到
[imeoutChecker_1] i.s.c.r.netty.NettyClientChannelManager : no available service 'null' found, please make sure registry config correct
出现上述异常,可能是nacos配置没有读取到,需要确认下nacos的版本,看下nacos客服端版本是否和服务端不一致,或者版本较低。本demo中的nacos版本1.1.4版本,更nacos服务端版本一致
2.seata客服端和服务端版本不一致
[imeoutChecker_1] i.s.c.r.netty.NettyClientChannelManager : no available service 'default' found, please make sure registry config correct
nacos配置可以读取,但是seata客服端版本太低,本demo服务端版本是1.4.0,所以客服端版本需要升级。版本不一致也会造成上述异常
3.seata客服端版本太低
org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.seata.spring.annotation.GlobalTransactionScanner]: Factory method 'globalTransactionScanner' threw exception; nested exception is io.seata.common.exception.ShouldNeverHappenException: Can't find any object of class org.springframework.context.ApplicationContext
这个原因是引用的spring-cloud-starter-alibaba-seata包中的seata-all版本太低,和服务端版本对不上,需要剔除掉包中低版本的seata-all
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
2.1.0.RELEASE
io.seata
seata-all
本demo到这里就结束了,如果想看seata服务端搭建可以参考上一篇文章(Seata服务端搭建(nacos))