一, Fescar

Fescar是阿里巴巴开源的分布式事务中间件,以高效并且对业务0侵入的方式,解决微服务场景下面临的分布式事务问题。

二, 下载源码, 并导入IDE

源码下载

可以看到, fescar对dubbo的分布式事务支持, 其实是扩展dubbo的filter接口实现的.

fescar-examples分为三个后台服务,一个服务调用者

① StorageService商品库存服务--管理商品的库存,

② OrderService订单服务--管理用户购买的订单,

③ AccountService账户服务--管理用户的账户余额.

④ BusinessService服务调用者--模拟一次购买商品的行为, 调用storage服务扣减库存, 调用order服务创建订单, account服务扣减用户账户余额, 基于这样的一种典型的购买商品的业务场景, 实现整个应用范围内的业务数据的完整性.

三, 图示, 整个购买业务的完整流程, 服务调用链路情况

每个服务内部的数据一致性仍有本地事务自己来保证, 而整个应用的业务层面的数据一致性可以使用哪些方式来保证呢? 目前主流的几种分布式事务解决方案:

① 基于mq可靠消息的最终一致性方案;

② TCC补偿性事务, 比较好的开源项目, TCC-Transaction, hmily, 都是基于这种补偿性的框架

③ 两阶段提交(2PC) -- 基于XA协议

④ saga

四, 如何利用fescar结合dubbo解决分布式事务问题, 保证数据一致性的

1 配置storage商品库存服务. dubbo-storage-service.xml

<dubbo:application name="dubbo-demo-storage-service"  />
    <!-- 不使用注册中心, 采用直连的方式 -->
    <dubbo:registry address="N/A"/>
    <dubbo:protocol name="dubbo" port="20880" />
    <!-- 暴露接口 -->
    <dubbo:service interface="com.alibaba.fescar.tm.dubbo.StorageService" ref="service" timeout="10000"/>
    <bean id="service" class="com.alibaba.fescar.tm.dubbo.impl.StorageServiceImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"/>
    </bean>
    <!-- 使用fescar代理数据源 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="storageDataSourceProxy" />
    </bean>
    <!-- 必须配置fescar代理数据源, 获取connection等连接, sentinel才能接管事务的commit和rollback -->
    <bean id="storageDataSourceProxy" class="com.alibaba.fescar.rm.datasource.DataSourceProxy">
        <constructor-arg ref="storageDataSource" />
    </bean>
    <bean name="storageDataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.storage.url}"/>
        <property name="username" value="${jdbc.storage.username}"/>
        <property name="password" value="${jdbc.storage.password}"/>
        <property name="driverClassName" value="${jdbc.storage.driver}"/>
        <property name="initialSize" value="0" />
        <property name="maxActive" value="180" />
        <property name="minIdle" value="0" />
        <property name="maxWait" value="60000" />
        <property name="validationQuery" value="Select 'x' from DUAL" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="testWhileIdle" value="true" />
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <property name="minEvictableIdleTimeMillis" value="25200000" />
        <property name="removeAbandoned" value="true" />
        <property name="removeAbandonedTimeout" value="1800" />
        <property name="logAbandoned" value="true" />
        <property name="filters" value="mergeStat" />
    </bean>
    <bean class="com.alibaba.fescar.spring.annotation.GlobalTransactionScanner">
        <constructor-arg value="dubbo-demo-storage-service"/>
        <constructor-arg value="my_test_tx_group"/>
    </bean>

2 配置account账户服务. dubbo-account-service.xml

<dubbo:application name="dubbo-demo-account-service"  />
    <dubbo:registry address="N/A"/>
    <dubbo:protocol name="dubbo" port="20882" />
    <!-- 暴露account服务 -->
    <dubbo:service interface="com.alibaba.fescar.tm.dubbo.AccountService" ref="service" timeout="10000"/>
    <bean id="service" class="com.alibaba.fescar.tm.dubbo.impl.AccountServiceImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"/>
    </bean>
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="accountDataSourceProxy" />
    </bean>
    <bean id="accountDataSourceProxy" class="com.alibaba.fescar.rm.datasource.DataSourceProxy">
        <constructor-arg ref="accountDataSource" />
    </bean>
    <bean name="accountDataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.account.url}"/>
        <property name="username" value="${jdbc.account.username}"/>
        <property name="password" value="${jdbc.account.password}"/>
        <property name="driverClassName" value="${jdbc.account.driver}"/>
        <property name="initialSize" value="0" />
        <property name="maxActive" value="180" />
        <property name="minIdle" value="0" />
        <property name="maxWait" value="60000" />
        <property name="validationQuery" value="Select 'x' from DUAL" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="testWhileIdle" value="true" />
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <property name="minEvictableIdleTimeMillis" value="25200000" />
        <property name="removeAbandoned" value="true" />
        <property name="removeAbandonedTimeout" value="1800" />
        <property name="logAbandoned" value="true" />
        <property name="filters" value="mergeStat" />
    </bean>
    <bean class="com.alibaba.fescar.spring.annotation.GlobalTransactionScanner">
        <constructor-arg value="dubbo-demo-account-service"/>
        <constructor-arg value="my_test_tx_group"/>
    </bean>

3 配置order订单服务. dubbo-service-service.xml

<dubbo:application name="dubbo-demo-order-service"/>
    <dubbo:registry address="N/A"/>
    <dubbo:protocol name="dubbo" port="20881"/>
    <!-- 暴露Order订单服务 -->
    <dubbo:service interface="com.alibaba.fescar.tm.dubbo.OrderService" ref="service" timeout="10000"/>
    <bean id="service" class="com.alibaba.fescar.tm.dubbo.impl.OrderServiceImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"/>
        <property name="accountService" ref="accountService"/>
    </bean>
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="orderDataSourceProxy"/>
    </bean>
    
    <bean id="orderDataSourceProxy" class="com.alibaba.fescar.rm.datasource.DataSourceProxy">
        <constructor-arg ref="orderDataSource"/>
    </bean>
    
    <bean name="orderDataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.order.url}"/>
        <property name="username" value="${jdbc.order.username}"/>
        <property name="password" value="${jdbc.order.password}"/>
        <property name="driverClassName" value="${jdbc.order.driver}"/>
        <property name="initialSize" value="0"/>
        <property name="maxActive" value="180"/>
        <property name="minIdle" value="0"/>
        <property name="maxWait" value="60000"/>
        <property name="validationQuery" value="Select 'x' from DUAL"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>
        <property name="testWhileIdle" value="true"/>
        <property name="timeBetweenEvictionRunsMillis" value="60000"/>
        <property name="minEvictableIdleTimeMillis" value="25200000"/>
        <property name="removeAbandoned" value="true"/>
        <property name="removeAbandonedTimeout" value="1800"/>
        <property name="logAbandoned" value="true"/>
        <property name="filters" value="mergeStat"/>
    </bean>

	<!-- 引用account账户服务 -->
    <dubbo:reference id="accountService" check="false" interface="com.alibaba.fescar.tm.dubbo.AccountService" url="dubbo://127.0.0.1:20882"/>

    <bean class="com.alibaba.fescar.spring.annotation.GlobalTransactionScanner">
        <constructor-arg value="dubbo-demo-order-service"/>
        <constructor-arg value="my_test_tx_group"/>
    </bean>

4 配置businessService服务调用者, 这里采用直连的方式调用服务. dubbo-business.xml

<dubbo:application name="dubbo-demo-app"  />
	<!-- 引用orderService服务 -->
    <dubbo:reference id="orderService" check="false" interface="com.alibaba.fescar.tm.dubbo.OrderService" url="dubbo://127.0.0.1:20881"/>
    <!-- 引用storageService服务 -->
    <dubbo:reference id="storageService" check="false" interface="com.alibaba.fescar.tm.dubbo.StorageService" url="dubbo://127.0.0.1:20880"/>
    <bean id="business" class="com.alibaba.fescar.tm.dubbo.impl.BusinessServiceImpl">
        <property name="orderService" ref="orderService"/>
        <property name="storageService" ref="storageService"/>
    </bean>
	<!-- 开启全局事务扫描器, 并传入两个固定的字符串, applicationId, txServiceGroup -->
    <bean class="com.alibaba.fescar.spring.annotation.GlobalTransactionScanner">
        <constructor-arg value="dubbo-demo-app"/>
        <constructor-arg value="my_test_tx_group"/>
    </bean>

5 配置数据库properties文件, 这里仅使用本地的一个库fescar_demo.

# account db config
jdbc.account.url=jdbc:mysql://localhost:3306/fescar_demo
jdbc.account.username=root
jdbc.account.password=root
jdbc.account.driver=com.mysql.jdbc.Driver
# storage db config
jdbc.storage.url=jdbc:mysql://localhost:3306/fescar_demo
jdbc.storage.username=root
jdbc.storage.password=root
jdbc.storage.driver=com.mysql.jdbc.Driver
# order db config
jdbc.order.url=jdbc:mysql://localhost:3306/fescar_demo
jdbc.order.username=root
jdbc.order.password=root
jdbc.order.driver=com.mysql.jdbc.Driver

五, 数据库

1 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` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8;

初始化时, storage_tbl表数据: C00321号商品库存剩余100件

2 account_tbl账户表

CREATE TABLE `account_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `money` int(11) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;

初始化时, account_tbl表数据:  U100001用户剩余余额1000

3 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 AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

初始化时, order_tbl表数据为空, 即还没有订单.

4 undo_log回滚日志表

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_unionkey` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

六, 结果

1 启动步骤,

① 首先启动fescar-server

② 启动storage服务, 对外暴露存储服务

③ 启动account服务, 对外暴露用户账户服务

④ 启动order服务, 对外暴露订单服务

⑤ 运行businessService服务调用者, 模拟一次购买行为.

2 正常流程, 各个服务均正常执行, 并commit提交.

businessService模拟U100001用户购买2件C00321号商品.

public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"dubbo-business.xml"});
        final BusinessService business = (BusinessService)context.getBean("business");
        business.purchase("U100001", "C00321", 2);
    }

购买商品的业务入口:

@GlobalTransactional(timeoutMills = 300000, name = "dubbo-demo-tx")
public void purchase(String userId, String commodityCode, int orderCount) {
        LOGGER.info("purchase begin ... xid: " + RootContext.getXID());
        storageService.deduct(commodityCode, orderCount);
        orderService.create(userId, commodityCode, orderCount);
        //throw new RuntimeException("xxx");
    }

order_tbl订单表: 生成一个订单

storage_tbl商品库存表: C00321商品扣减2件库存

account_tbl用户账户表: U100001用户扣减400

3 异常流程, fescar控制各个服务事务回滚

这里设置order订单服务调用account账户服务时, 超时时间为200ms, 而account扣减账户余额时休眠2s, 导致超时异常, 查看事务是否回滚.

dubbo-order-service.xml引用account账户服务, timeout=200ms

<!-- 引用account账户服务, 超时时间200ms -->
    <dubbo:reference id="accountService" timeout="200" check="false" interface="com.alibaba.fescar.tm.dubbo.AccountService" url="dubbo://127.0.0.1:20882"/>

AccountServiceImpl扣减用户的账户余额时, 休眠2s

public void debit(String userId, int money) {
        LOGGER.info("Account Service ... xid: " + RootContext.getXID());
        LOGGER.info("Deducting balance SQL: update account_tbl set money = money - {} where user_id = {}",money,userId);
        
        // 休眠2s, 导致超时, 事务回滚
        try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

        jdbcTemplate.update("update account_tbl set money = money - ? where user_id = ?", new Object[] {money, userId});
        LOGGER.info("Account Service End ... ");
    }

超时异常, 报错:

Exception in thread "main" org.apache.dubbo.rpc.RpcException: Invoke remote method timeout. method: debit, provider: dubbo://127.0.0.1:20882/com.alibaba.fescar.tm.dubbo.AccountService?application=dubbo-demo-order-service&check=false&dubbo=2.0.2&group=&interface=com.alibaba.fescar.tm.dubbo.AccountService&methods=debit&pid=9384&register.ip=192.168.99.1&side=consumer&timeout=200&timestamp=1550292740604, cause: Waiting server-side response timeout. start time: 2019-02-16 13:10:55.977, end time: 2019-02-16 13:10:56.179, client elapsed: 1 ms, server elapsed: 201 ms, timeout: 200 ms, request: Request [id=10, version=2.0.2, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=debit, parameterTypes=[class java.lang.String, int], arguments=[U100001, 400], attachments={input=280, path=com.alibaba.fescar.tm.dubbo.AccountService, TX_XID=192.168.99.1:8091:4020856, interface=com.alibaba.fescar.tm.dubbo.OrderService, version=0.0.0, timeout=200}]], channel: /192.168.99.1:63382 -> /192.168.99.1:20882

查看数据库数据, 发现订单没有被创建, C00321号商品库存没有被扣减, U100001用户的账户也没有扣减余额, fescar框架保证了分布式环境下出异常时的数据一致性, 完整性.
 

 

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐