Fescar分布式事务框架--结合dubbo解决微服务下的分布式事务问题
一, FescarFescar是阿里巴巴开源的分布式事务中间件,以高效并且对业务0侵入的方式,解决微服务场景下面临的分布式事务问题。二, 下载源码, 并导入IDE源码下载可以看到, fescar对dubbo的分布式事务支持, 其实是扩展dubbo的filter接口实现的.fescar-examples分为三个后台服务,一个服务调用者① StorageService商品库...
一, 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®ister.ip=192.168.99.1&side=consumer&timeout=200×tamp=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框架保证了分布式环境下出异常时的数据一致性, 完整性.
更多推荐
所有评论(0)