事务大家都很熟悉,小范围来说就是数据库为了保证数据的一致性而做的操作规范和限制。
常见的就是AICD四大特性:
分布式事务顾名思义就是分布式系统当中的事务如何保证这些特性。
分布式事务分为两块来理解:
分布式系统有一个很著名的CAP理论:
所有节点在相同时间看到的数据是相同的,这个可以看作是强一致性,而不是最终一致性。
分布式系统在遇到节点或者网络分区故障的时候,仍然可以提供一致性和可用性的服务。
很好的一篇文章介绍cap的
https://blog.csdn.net/w372426096/article/details/80437198
提供cap原理可以知道,不可能cap全部保证的。所以引出了base理论。
Base理论 :
响应时间上的损失、功能上的损失
)支付中
、处理中
等)支付中
变为支付成功
)经过以上对分布式系统的cap和base理论的理解,可以得知分布式事务只能保证最终一致性。
常见的分布式事务解决方案都不能脱离这两个理论。
分布式事务一些大神也给出了一些理论知识
两阶段提交2PC:
--协调者(Coordinater):事务管理器(TM)
--参与者(participants):资源管理器(RM)
存在的问题
性能问题/同步阻塞
无论是第一阶段还是第二阶段,所以参与者和协调者资源都是被锁定的状态,这个过程比较慢的话,会影响整个系统的性能。
单点故障/数据不一致
一旦协调者在某个阶段出现故障,参与者将处与一直阻塞的状态,尤其第二阶段则参与者处以资源锁定状态。
2pc出现单点问题的三种情况
协调者正常,参与者宕机
参与者中的一个或者几个宕机就无法给协议者反馈,那么进入超时机制,一旦参与者在指定时间内会没有反馈,协调者就发送终止事务请求。
协调者宕机,参与者正常
一旦协调者宕机,无论处于哪个阶段,所以参与者将都会阻塞。需要进入协调者备份,且记录操作日志。检测一段时间还没有恢复,则进行激活备份机器,查看操作日志,重新发起请求。
协调者宕机,参与者也宕机
此种情况又可以分为三种情况:
因为发生在第一阶段,从参与者选择一个作为协调者,重新发起第一阶段和第二阶段接可以了。
选出新的协调者重新执行第一阶段和第二阶段
此时会出现数据不一致的现象部分提交了,部分没有提交,此种情况2pc是无法解决。
三阶段提交3pc:是对两阶段2pc提交的一个改进版本。
主要是针对阻塞/性能问题进行了改进,2pc存在的问题是当协调者故障的时候,参与者会一直阻塞直到协调者恢复。
主要改动点
主要分为3个阶段
协调者发起是否可以进行提交的请求,参与者本地获取锁,成功返回消息给协调者
协调者在收到可以进行预提交消息之后,进行预提交
真正的执行阶段,进行提交或者回滚
环境jdk1.8 idea 构建 springboot项目
pom.xml配置:
mysql
mysql-connector-java
5.0.8
org.springframework.boot
spring-boot-starter-jdbc
org.springframework.boot
spring-boot-starter-jta-atomikos
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
junit
junit
RELEASE
test
项目源码:
package com.liu.transactionmarket;
import com.mysql.jdbc.jdbc2.optional.MysqlXADataSource;
import com.mysql.jdbc.jdbc2.optional.MysqlXid;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.sql.XAConnection;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import java.sql.Connection;
import java.sql.Statement;
/**
* xa实现
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = TransactionMarketApplication.class)
public class XATransactionn {
@Test
public void xATest() throws Exception {
XAConnection xaConnection1 = null;
XAConnection xaConnection2 = null;
XAResource xaResource1 = null;
XAResource xaResource2 = null;
Connection connection1 = null;
Connection connection2 = null;
Statement statement1 = null;
Statement statement2 = null;
Xid xid1 = null;
Xid xid2 = null;
try {
//数据源1
MysqlXADataSource mysqlXADataSource1 = new MysqlXADataSource();
mysqlXADataSource1.setUrl(“jdbc:mysql:172.20.xxxx.xxx:3306/law_online?useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=convertToNull”);
mysqlXADataSource1.setUser(“aaaa”);
mysqlXADataSource1.setPassword(“aaaaxxxxx”);
//数据源2
MysqlXADataSource mysqlXADataSource2 = new MysqlXADataSource();
mysqlXADataSource2.setUrl("jdbc:mysql://192.168.xx.xxx:6606/sccs?useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=convertToNull");
mysqlXADataSource2.setUser("xxxx");
mysqlXADataSource2.setPassword("xxxxxx");
//获取数据库链接1
xaConnection1 = mysqlXADataSource1.getXAConnection();
xaResource1 = xaConnection1.getXAResource();
connection1 = xaConnection1.getConnection();
statement1 = connection1.createStatement();
//获取数据库链接2
xaConnection2 = mysqlXADataSource2.getXAConnection();
xaResource2 = xaConnection2.getXAResource();
connection2 = xaConnection2.getConnection();
statement2 = connection2.createStatement();
//创建分支事务id
xid1 = new MysqlXid("72891".getBytes(), "72891".getBytes(), 1);
xid2 = new MysqlXid("72892".getBytes(), "72892".getBytes(), 1);
//分支事务1关联事务处理sql
xaResource1.start(xid1, XAResource.TMNOFLAGS);
int result1 = statement1.executeUpdate("update base_info set status=0 where id=1");
xaResource1.end(xid1, XAResource.TMSUCCESS);
//分支事务2关联事务处理sql
xaResource2.start(xid2, XAResource.TMNOFLAGS);
int result2 = statement2.executeUpdate("update sccs_borrower_info set sex=1 where id=57");
xaResource2.end(xid2, XAResource.TMSUCCESS);
//两阶段提交协议第一阶段-预提交
//分支事务1预提交
int isOk1 = xaResource1.prepare(xid1);
//分支事务2预提交
int isOk2 = xaResource2.prepare(xid2);
//两阶段提交协议第二阶段-提交/回滚
if (XAResource.XA_OK == isOk1 & XAResource.XA_OK == isOk2) {
xaResource1.commit(xid1, false);
xaResource2.commit(xid2, false);
} else {
xaResource1.rollback(xid1);
xaResource2.rollback(xid2);
}
} catch (Exception e) {
//记录错误日志,进行回滚
xaResource1.rollback(xid1);
xaResource2.rollback(xid2);
} finally {
if (null != xaConnection1) {
xaConnection1.close();
}
if (null != xaConnection2) {
xaConnection2.close();
}
}
}
}
开源的实现 TM 提供商:
package com.liu.transactionmarket;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.transaction.SystemException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Properties;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = TransactionMarketApplication.class)
public class JTATransaction {
@Test
public void jtaTest() {
Connection lawConnetion = null;
Connection sccsConnection = null;
String sql = null;
int lawResult;
int sccsResult;
AtomikosDataSourceBean lawOnlineDataSource = new AtomikosDataSourceBean();
lawOnlineDataSource.setUniqueResourceName("lawOnlineDataSource");
lawOnlineDataSource.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
Properties properties1 = new Properties();
properties1.setProperty("url", "jdbc:mysql://172.20.xx.xx:3306/law_online?useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=convertToNull");
properties1.setProperty("user", "bbbxxx");
properties1.setProperty("password", "aaaxxx");
lawOnlineDataSource.setXaProperties(properties1);
AtomikosDataSourceBean sccsDataSource = new AtomikosDataSourceBean();
sccsDataSource.setUniqueResourceName("sccsDataSource");
sccsDataSource.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
Properties properties2 = new Properties();
properties2.setProperty("url", "jdbc:mysql://192.168.xx.xxx:6606/sccs?useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=convertToNull");
properties2.setProperty("user", "xxx");
properties2.setProperty("password", "xxxx!");
sccsDataSource.setXaProperties(properties2);
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(true);
try {
//开启一个全局事务
userTransactionManager.begin();
sccsConnection = sccsDataSource.getConnection();
sql = "update sccs_borrower_info set sex=1 where id=57";
PreparedStatement clmgPreparedStatement = sccsConnection.prepareStatement(sql);
sccsResult = clmgPreparedStatement.executeUpdate();
lawConnetion = lawOnlineDataSource.getConnection();
sql = "update base_info set status=0 where id=1";
PreparedStatement lawPreparedStatement = lawConnetion.prepareStatement(sql);
lawResult = lawPreparedStatement.executeUpdate();
if (lawResult > 0 && sccsResult > 0) {
userTransactionManager.commit();
} else {
userTransactionManager.rollback();
}
} catch (Exception e) {
try {
userTransactionManager.rollback();
} catch (SystemException ex) {
ex.printStackTrace();
}
} finally {
if (null != lawConnetion) {
try {
lawConnetion.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (null != sccsConnection) {
try {
sccsConnection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
XA或者JTA这种实现是强一致性的体现
按照互联网的实际场景都是最终一致性,柔性事务
常见的分布式事务组件
接下来介绍一下柔性事务 Tcc-transaction
tcc原理还是来源于两阶段提交
业务架构
Saga的核心是补偿,与TCC不同的是Saga不需要Try,而是直接进行confirm
、cancel
操作。
- Confirm:依次按顺序依次执行资源操作,各个资源直接处理本地事务,如无问题,二阶段什么都不用做;
- Cancel:异常情况下需要调用的补偿事务(逆操作)来保证数据的一致性。
可以看出,Saga和TCC有些类似,都是补偿型事务
优势:
- 一阶段提交本地事务,无锁,高性能;
- 事件驱动模式,参与者可异步执行,高吞吐;
- 应用成本低,补偿服务易于实现;
劣势:
- 无法保证隔离性(脏写)
有一些情况,服务间调用时异步的,服务A将消息发送到MQ,服务B进行消息的消费。这时我们就需要用到可靠消息最终一致性
来解决分布式事务问题。首先字面理解,
- 可靠消息:即这个消息一定是可靠的,并且最终一定需要被消费的。
- 最终一致性:过程中数据存在一定时间内的不一致,但超过限定时间后,需要最终会保持一致。
确保以上两点情况下,通过消息中间件(RocketMQ)来完成分布式事务处理,因为RocketMQ支持事务消息,可以方便的让我们进行分布式事务控制。
因此首先需要了解一下,RocketMQ的事务消息的原理。
摘自网络
half message:半消息,此时消息不能被consumer所发现和消费,需
producer
进行二次消息确认。
producer
发送half message
给MQ Server
;producer
根据MQ Server
应答结果判断half message
是否发送成功;producer
处理本地事务;producer
发送最终确认消息commit / rollback
;
commit
:consumer
对消息可见并进行消费;
rollback
:discard
抛弃消息,consumer
无法进行消息消费;
- 如遇异常情况下
step4
最终确认消息为达到MQ Server
,MQ Server
会定期查询当前处于半消息状态下的消息,主动进行消息回查来询问producer
该消息的最终状态;producer
检查本地事务执行的最终结果;producer
根据检查到的结果,再次提交确认消息,MQ Server
仍然按照step4
进行后续操作。
事务消息发送对应步骤1、2、3、4,事务消息回查对应步骤5、6、7。
由以上步骤可以看出通过事务性消息的两步操作,避免了消息直接投递所产生一些问题。最终投递到MQ Server的消息,是真实可靠且必须被消费的。
1.配置整理
1.1 首先到github上面下载最新的tcc-transaction项目源码
下载地址:https://github.com/changmingxie/tcc-transaction
最新版本地址
https://github.com/changmingxie/tcc-transaction/tree/master-1.2.x
1.2 修改源码里面的主pom.xml配置文件
修改tcc-transaction-server里面的pom文件
1.3 清理项目并且打包install到本地
1.4 进行本地dubbo测试验证
1.4.1执行sql脚本
一共有4个脚本文件,需要创建4个数据库
为什么是这些表?结合实际业务场景就明白了。
tcc-transaction :示例演示在下完订单后,使用红包帐户和资金帐户来付款,红包帐户服务和资金帐户服务在不同的系统中。示例中,有两个SOA提供方,一个是CapitalTradeOrderService,代表着资金帐户服务,另一个是RedPacketTradeOrderService,代表着红包帐户服务.
create_db_red.sql:红包账户相关的表,总共包括两个表,红包账户表red_red_packet_account和红包交易表red_trade_order
create_db_cap.sql: 资金账户相关的表,总共包括两个表,商家账户表cap_capital_account和账户交易表cap_trade_order
create_db_ord.sql: 订单相关的表,总共包括四个表,商家表ord_shop、商品表ord_product、订单商品数量表ord_order_line和订单详细信息表ord_order
create_db_tcc.sql:tcc-transaction框架事务相关的表,总共包括四个表,订单事务表tcc-transaction_ord、红包事务表tcc-transaction_red、账户事务表tcc_transaction_cap和事务重试表tcc_transaction_ut
create_db_ord 数据源配置
create_db_cap 数据源配置
transaction_red 数据源配置
数据源都配置完成之后启动应用
tcc_transaction_dubbo_capital
启动成功 hello tcc transacton dubbo sample capital
tcc_transaction_dubbo_redpacket
启动成功hello tcc transacton dubbo sample red packet
tcc-transaction-dubbo-order
启动成功
由于没有配置好数据源导致异常了,刚好也验证事务回滚机制
红包没有减少,账户也没有减少。看一下各表的状态
数据表不存在
tcc-transaction的核心代码在tcc-transaction-core里面
其中非常重要的两个拦截器ResourceCoordinatorInterceptor,CompensableTransactionInterceptor
后续继续和spring整合以及 seata的研究
阿里开源的Seata 是一款分布式事务解决方案,提供了 AT、TCC、SAGA 和 XA 事务模式。
Seata架构的亮点主要有几个:
- 应用层基于SQL解析实现了自动补偿,从而最大程度的降低业务侵入性;
- 将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚(支持多种注册中心形式以及本地文件形式);
- 通过全局锁实现了写隔离与读隔离。
中文官方文档:http://seata.io/zh-cn/docs/overview/what-is-seata.html
首先下载seata demo
https://github.com/seata/seata-samples
使用zk作为注册中心的配置文件的修改localhost:2181改为自己的zk服务器地址配置
分别在三个业务库执行sql,并修改jdbc.properties配置
注意:如果分为多个业务库,则每个业务库都需要执行undo_log.sql
CREATE TABLE undo_log (
id bigint(20) NOT NULL AUTO_INCREMENT,
branch_id bigint(20) NOT NULL,
xid varchar(100) NOT NULL,
context varchar(128) NOT NULL,
rollback_info longblob NOT NULL,
log_status int(11) NOT NULL,
log_created datetime NOT NULL,
log_modified datetime NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid,branch_id)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
下载服务器端server或者修改源码seata-server
server有两种配置启动方式 默认是file也可以改为db
下面看一下db方式持久化方式
修改conf/file.conf
改动两个地方store.mode=‘db’
同时db配置 url, user,password需要修改为执行持久化表
drop table global_table;
create table global_table (
xid varchar(128)not null,
transaction_id bigint,
status tinyint not null,
application_id varchar(64),
transaction_service_group varchar(64),
transaction_name varchar(128),
timeout int,
begin_time bigint,
application_data varchar(2000),
gmt_create datetime,
gmt_modified datetime,
primary key(xid),
key idx_gmt_modified_status (gmt_modified, status),
key idx_transaction_id (transaction_id)
);
–- the table to store BranchSession data
drop table branch_table;
create table branch_table (
branch_id bigint not null,
xid varchar(128)not null,
transaction_id bigint,
resource_group_id varchar(128),
resource_id varchar(256),
lock_key varchar(256),
branch_type varchar(8),
status tinyint,
client_id varchar(64),
application_data varchar(2000),
gmt_create datetime,
gmt_modified datetime,
primary key(branch_id),
key idx_xid (xid)
);
–- the table to store lock data
drop table lock_table;
create table lock_table (
row_key varchar(128)not null,
xid varchar(128),
transaction_id long,
branch_id long,
resource_id varchar(256),
table_name varchar(64),
pk varchar(128),
gmt_create datetime,
gmt_modified datetime,
primary key(row_key)
);
配置改为之后启动server
本地db持久化方式启动
./seata-server.sh -h 127.0.0.1 -p 8091 -m db
服务端启动成功之后,那么客户端demo就可以运行了。
非别按照顺序启动DubboAccountServiceStarter,DubboStorageServiceStarter,DubboOrderServiceStarter,最后启动DubboBusinessTester
账户扣减日志
库存扣减日志
插入订单成功日志
到此简单的demo完成。
原文:https://blog.csdn.net/qingmuqingnian/article/details/106221705
参考:https://www.jianshu.com/p/60a100eee74a
●史上最强Tomcat8性能优化
●阿里巴巴为什么能抗住90秒100亿?--服务端高并发分布式架构演进之路
●B2B电商平台--ChinaPay银联电子支付功能
●学会Zookeeper分布式锁,让面试官对你刮目相看
●SpringCloud电商秒杀微服务-Redisson分布式锁方案
查看更多好文,进入公众号--撩我--往期精彩
一只 有深度 有灵魂 的公众号0.0