如何防止掉进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阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容