如何防止掉進Dubbo與Seata集成坑里?

歡迎關注專欄:后端架構技術精選。里面有大量關于的Java高級架構知識點分享,還有各種面試趣聞以及程序員身邊事,如有好文章也歡迎投稿哦。

前言

博主最近在搞Dubbo分布式業務,相信來看此篇文章的開發朋友們對分布式這個名詞肯定不陌生,在分布式業務中肯定就會牽涉到分布式事務,對于分布式事務博主開始聽了這個詞就覺得很難,但是其實還好,就是在整合Dubbo與Seata的其中踩了一些坑,并沒有如同官方那么一帆風順,那么本次就將整合步驟以及一些坑給大家爆出來,以防大家重蹈覆轍~

image

整合步驟

前提說明

我的業務框架是 Dubbo + Mybatis-Plus + Zookeeper + Nacos + Seata ,至于為什么要同時使用 Zookeeper + Nacos 呢,因為前期沒有整合分布式事務的時候用的zk做的服務注冊中心,后面可能進行移除,換為全局 Nacos 作為注冊中心

安裝Nacos

關于 Zookeeper 我就不多于說明了,因為本文主要是講述 DubboSeata 的集成方面的業務。

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 即可登錄:

image

下載/配置/啟動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.

image

啟動seata-server

# 在bin目錄下執行
sh seata-server.sh
# or
sh seata-server.sh -h 127.0.0.1 -p 8091 -m db
# 下面的是帶參啟動可以覆蓋配置文件里面的數據</pre>

啟動成功之后,你會看到Nacos的「控制臺」-「服務列表」中會新增一項服務名為 serverAddr 的服務,如圖:

image

業務整合

業務架構分為

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,沒有記錄下來),所以此篇文章關于坑的展示并沒有自己想的那么多,如果大家遇到了這方面的問題,可以在文章下方評論,博主將會盡可能的幫助你解決你的燃眉之急!

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,947評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,094評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,268評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,817評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,039評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,551評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,502評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,662評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,907評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,255評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,637評論 2 370

推薦閱讀更多精彩內容