歡迎關注專欄:后端架構技術精選。里面有大量關于的Java高級架構知識點分享,還有各種面試趣聞以及程序員身邊事,如有好文章也歡迎投稿哦。
前言
博主最近在搞Dubbo分布式業務,相信來看此篇文章的開發朋友們對分布式這個名詞肯定不陌生,在分布式業務中肯定就會牽涉到分布式事務,對于分布式事務博主開始聽了這個詞就覺得很難,但是其實還好,就是在整合Dubbo與Seata的其中踩了一些坑,并沒有如同官方那么一帆風順,那么本次就將整合步驟以及一些坑給大家爆出來,以防大家重蹈覆轍~
整合步驟
前提說明
我的業務框架是 Dubbo
+ Mybatis-Plus
+ Zookeeper
+ Nacos
+ Seata
,至于為什么要同時使用 Zookeeper
+ Nacos
呢,因為前期沒有整合分布式事務的時候用的zk做的服務注冊中心,后面可能進行移除,換為全局 Nacos
作為注冊中心
安裝Nacos
關于 Zookeeper
我就不多于說明了,因為本文主要是講述 Dubbo
與 Seata
的集成方面的業務。
Nacos
我是用的 Docker
安裝的,相關命令如下:
#拉取nacos鏡像
docker pull nacos/nacos-server
# 啟動鏡像
docker run --env MODE=standalone --name nacos -d -p 8848:8848 nacos/nacos-server
# 默認賬戶密碼是:nacos/nacos</pre>
啟動好 Nacos
之后直接訪問 http://{ip}:8848/nacos/index.html
即可登錄:
下載/配置/啟動Seata
進入到 https://github.com/seata/seata/releases 下載seata的發行版,我這里使用的0.9.0版本。
下載完成之后進行解壓,其中 bin
目錄下存放為啟動腳本, conf
目錄下存放為配置文件以及相關SQL和配置注入腳本, lib
目錄下是seata的相關依賴。
進入到conf目錄修改registry.conf
registry {
type = "nacos"
nacos {
serverAddr = "127.0.0.1" #nacos地址ip
namespace = "public" #nacos的命名空間,默認為public
cluster = "default" #集群,由于沒有所以填寫default
}
file {
name = "file.conf"
}
}
config {
type = "nacos"
nacos {
serverAddr = "127.0.0.1" #nacos地址ip
cluster = "default" #集群,由于沒有所以填寫default
}
file {
name = "file.conf"
}
}
注意:在registry中config沒有namespace屬性,否則會出現服務啟動失敗或no available!
接著我們修改file.conf,其配置主要為:
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
#thread factory for netty
thread-factory {
boss-thread-prefix = "NettyBoss"
worker-thread-prefix = "NettyServerNIOWorker"
server-executor-thread-prefix = "NettyServerBizHandler"
share-boss-worker = false
client-selector-thread-prefix = "NettyClientSelector"
client-selector-thread-size = 1
client-worker-thread-prefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
boss-thread-size = 1
#auto default pin or 8
worker-thread-size = 8
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#transaction service group mapping
vgroup_mapping.service-user-provider-group = "default"
vgroup_mapping.service-order-provider-group = "default"
vgroup_mapping.service-storage-provider-group = "default"
#這里是你的事務分組配置,格式為vgroup_mapping.${YOUR_SERVICE_NAME}-group,當然`${YOUR_SERVICE_NAME}-group`部分你可以自定
#下面是你的seata的服務列表
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
disableGlobalTransaction = false
}
client {
async.commit.buffer.limit = 10000
lock {
retry.internal = 10
retry.times = 30
}
report.retry.count = 5
tm.commit.retry.count = 1
tm.rollback.retry.count = 1
}
## transaction log store, only used in seata-server
store {
## store mode: file、db
mode = "db"
## file store property
file {
## store location dir
dir = "sessionStore"
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
## 此處為你的數據庫配置
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "root"
min-conn = 1
max-conn = 3
global.table = "global_table"
branch.table = "branch_table"
lock-table = "lock_table"
query-limit = 100
}
}
support {
## spring
spring {
# auto proxy the DataSource bean
datasource.autoproxy = false
}
}
配置好上述配置文件之后,我們將conf目錄下的 db_store.sql
文件導入到我們的數據庫,我這里的數據庫名為 seata
(上述配置文件可以看出)
接著我們再修改目錄下的 nacos-config.txt
,這個文件其實就是將 file.conf
翻譯成properties格式的,這里我就不做過多的說明了,寫好之后我們將配置寫入到nacos中:
# 在conf目錄下執行
sh nacos-config.sh {Nacos-Server-IP} #將{Nacos-Server-IP}換成你的IP</pre>
寫入成功之后,你會看到這樣一行小綠字:
init nacos config finished, please start seata-server.
啟動seata-server
# 在bin目錄下執行
sh seata-server.sh
# or
sh seata-server.sh -h 127.0.0.1 -p 8091 -m db
# 下面的是帶參啟動可以覆蓋配置文件里面的數據</pre>
啟動成功之后,你會看到Nacos的「控制臺」-「服務列表」中會新增一項服務名為 serverAddr
的服務,如圖:
業務整合
業務架構分為
service-order-provider # 訂單服務
service-storage-provider # 庫存服務
service-user-provider # 用戶服務
service-user-consumer # 用戶業務調用</pre>
導入日志數據表
將seata的conf目錄下的 db_undo_log.sql
到你的業務數據庫
業務配置
我們要在三個 provider
服務中寫入如下配置:
/resources/file.conf
file.conf與seata的conf目錄下一致
/resources/registry.conf
registry.conf與seata的conf目錄下一致
pom.xml
引入需要的依賴包
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.1.4</version>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency></pre>
SeataAutoConfig.java
進行Seata的配置,包括數據庫資源/數據庫代理設置/SqlSessionFactory等
/**
* @author .
* . ._. __ .__.. ,
* | | / `| | \./
* |____|_\__.|__| |
* @version 2019/12/23
*/
@Configuration
public class SeataAutoConfig {
@Value("${spring.application.name}")
private String appName;
@Autowired
private DataSourceProperties dataSourceProperties;
/**
* init durid datasource
*
* @Return: druidDataSource datasource instance
*/
@Bean
@Primary
public DruidDataSource druidDataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUrl(dataSourceProperties.getUrl());
druidDataSource.setUsername(dataSourceProperties.getUsername());
druidDataSource.setPassword(dataSourceProperties.getPassword());
druidDataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
druidDataSource.setInitialSize(0);
druidDataSource.setMaxActive(180);
druidDataSource.setMaxWait(60000);
druidDataSource.setMinIdle(0);
druidDataSource.setValidationQuery("Select 1 from DUAL");
druidDataSource.setTestOnBorrow(false);
druidDataSource.setTestOnReturn(false);
druidDataSource.setTestWhileIdle(true);
druidDataSource.setTimeBetweenEvictionRunsMillis(60000);
druidDataSource.setMinEvictableIdleTimeMillis(25200000);
druidDataSource.setRemoveAbandoned(true);
druidDataSource.setRemoveAbandonedTimeout(1800);
druidDataSource.setLogAbandoned(true);
try {
Driver driver = new Driver();
druidDataSource.setDriver(driver);
} catch (SQLException e) {
e.printStackTrace();
}
return druidDataSource;
}
@Bean
public DataSourceProxy dataSourceProxy(DruidDataSource druidDataSource){
return new DataSourceProxy(druidDataSource);
}
// 因為我使用的是MybatisPlus,所以需要注入此部分
@Bean
public MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean(DataSourceProxy proxy) throws IOException {
MybatisSqlSessionFactoryBean mybatisPlus = new MybatisSqlSessionFactoryBean();
mybatisPlus.setDataSource(proxy);
mybatisPlus.setVfs(SpringBootVFS.class);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
mybatisPlus.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
GlobalConfig globalConfig = new GlobalConfig();
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
// ID 策略 AUTO->`0`("數據庫ID自增") INPUT->`1`(用戶輸入ID") ID_WORKER->`2`("全局唯一ID") UUID->`3`("全局唯一ID")
//使用ID_WORKER_STR,因為前后端分離使用整形,前端JS會有精度丟失
dbConfig.setIdType(IdType.ID_WORKER_STR);
globalConfig.setDbConfig(dbConfig);
mybatisPlus.setGlobalConfig(globalConfig);
MybatisConfiguration mc = new MybatisConfiguration();
// 對于完全自定義的mapper需要加此項配置,才能實現下劃線轉駝峰
mc.setMapUnderscoreToCamelCase(true);
mc.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
mybatisPlus.setConfiguration(mc);
return mybatisPlus;
}
@Bean
public GlobalTransactionScanner globalTransactionScanner(){
return new GlobalTransactionScanner(this.appName, String.format("%s-group", this.appName));
}
}
業務接口
order服務下有創建訂單的接口
/**
* 創建訂單
* @param order 訂單
*/
ClientOrder create(ClientOrder order);</pre>
storage服務下有減少庫存的接口
/**
* 扣除庫存
* @param productId 產品ID
* @param total 扣除數量
*/
void decrease(String productId, Integer total);
user服務下有減少賬戶余額以及購買的接口
/**
* 扣除賬戶余額
* @param userId 用戶ID
* @param money 扣除金額
*/
void decreaseMoney(String userId, BigDecimal money);
/**
* 購買產品
* @param productId 產品ID
* @param uid 用戶ID
* @param totalCount 購買數量
*/
void buy(String productId, String uid, Integer totalCount);
創建訂單/扣除庫存/扣除賬戶余額這三個接口我就不在此展示了,因為都是基本的CURD+業務判斷,主要展示一下購買產品的業務接口實現,因為我們需要對此業務的過程中處理分布式事務:
@Override
@GlobalTransactional(name = "service-user-provider")
public void buy(String productId, String uid, Integer totalCount) {
log.info("開始全局事務"+ RootContext.getXID());
ClientOrder order = new ClientOrder();
BigDecimal money = new BigDecimal(200);
order.setMoney(money);
order.setPid(productId);
order.setUid(uid);
order.setTotal(totalCount);
log.info("====創建訂單====");
ClientOrder order1 = this.orderService.create(order);
log.info("====創建訂單完成====");
log.info("====扣除庫存====");
this.storageService.decrease(productId, totalCount);
log.info("====庫存扣除完成====");
log.info("====扣除賬戶余額====");
this.decreaseMoney(uid, money);
log.info("====賬戶余額扣除完成====");
log.info("====購買成功====");
}
由上述代碼可以看出,我們只需要添加一個@GlobalTransactional注解就可以進行分布式事務控制,其中name為該項目 spring.application.name
的值。
對于事務回滾,我們只需要將用戶的余額設置為0,這個時候扣除余額就會失敗,那么業務失敗,就會進行事務回滾,當操作完成之后我們看到數據庫的訂單和庫存并沒有創建和減少,就代表我們的分布式事務Seata配置完成并可以成功使用。
后記
在配置Seata的時候確實踩了不少坑,現在回頭過來有些都已經忘卻(當時只顧得解決BUG,沒有記錄下來),所以此篇文章關于坑的展示并沒有自己想的那么多,如果大家遇到了這方面的問題,可以在文章下方評論,博主將會盡可能的幫助你解決你的燃眉之急!