先看一段作者张亮,对sharding-jdbc分布式事务理解:
张亮:分布式事务这块,我们认为XA多阶段提交的方式,虽然对分布式数据的完整性有比较好的保障,但会极大的降影响应用性能,并未考虑采用。我们采用的是两种方式,一种称之为弱XA,另一种是柔性事务,即BASE。
弱XA就是分库之后的数据库各自负责自己事务的提交和回滚,没有统一的调度器集中处理。这样做的好处是天然就支持,对性能也没有影响。但一旦出问题,比如两个库的数据都需要提交,一个提交成功,另一个提交时断网导致失败,则会发生数据不一致的问题,而且这种数据不一致是永久存在的。
柔性事务是对弱XA的有效补充。柔性事务类型很多。
Sharding-JDBC主要实现的是最大努力送达型。即认为事务经过反复尝试一定能够成功。如果每次事务执行失败,则记录至事务库,并通过异步的手段不断的尝试,直至事务成功(可以设置尝试次数,如果尝试太多仍然失败则入库并需要人工干预)。在尝试的途中,数据会有一定时间的不一致,但最终是一致的。通过这种手段可以在性能不受影响的情况下牺牲强一致性,达到数据的最终一致性。最大努力送达型事务的缺点是假定事务一定是成功的,无法回滚,因此不够灵活。
还有一种柔性事务类型是TCC,即Try Confirm Cancel。可以通过事务管理器控制事务的提交或回滚,更加接近原生事务,但仍然是最终一致性。其缺点是需要业务代码自行实现Try Confirm Cancel的接口,对现有业务带来一定冲击。未来Sharding-JDBC会带来对TCC的支持。
还有一些其他的分布式事务,如google提出的F1等,由于Shariding-JDBC仍然使用数据库的原有存储引擎,并未改变,因此暂时不考虑对此类型事务的支持。
再看一下sharding-jdbc最大努力送达型架构图:
SoftTransactionManager柔性事务管理器这个类是入口,init()方法
1、init方法
/**
* 初始化事务管理器.
*/
public void init() throws SQLException {
//DML类SQL执行时的事件发布总线,雏形是guava的EventBus,包含register监听器,post发布事件等基本操作,消息投递员EventPostman类调用这些方法
//采用事件监听器模式,将事务提交执行,做成事件队列。
DMLExecutionEventBus.register(new BestEffortsDeliveryListener());
//事务日志有内存和RDB数据库存储2种形式,如果是RDB,需执行建表语句
if (TransactionLogDataSourceType.RDB == transactionConfig.getStorageType()) {
//guava断言
Preconditions.checkNotNull(transactionConfig.getTransactionLogDataSource());
createTable();
}
//重要对象用的都是普通factory模式,此处做最大努力送达型Job的zk环境和JobScheduler初始化
if (transactionConfig.getBestEffortsDeliveryJobConfiguration().isPresent()) {
new NestedBestEffortsDeliveryJobFactory(transactionConfig).init();
}
}
2、根据枚举类型获取柔性事务方法getTransaction()
/**
* 获取柔性事务管理器.
*
* @param type 柔性事务类型
* @return 柔性事务
*/
public AbstractSoftTransaction getTransaction(final SoftTransactionType type) {
AbstractSoftTransaction result;
switch (type) {
case BestEffortsDelivery:
result = new BEDSoftTransaction();
break;
case TryConfirmCancel:
result = new TCCSoftTransaction();
break;
default:
throw new UnsupportedOperationException(type.toString());
}
// TODO 目前使用不支持嵌套事务,以后这里需要可配置
if (getCurrentTransaction().isPresent()) {
throw new UnsupportedOperationException("Cannot support nested transaction.");
}
ExecutorDataMap.getDataMap().put(TRANSACTION, result);
ExecutorDataMap.getDataMap().put(TRANSACTION_CONFIG, transactionConfig);
return result;
}
BEDSoftTransaction继承AbstractSoftTransaction,自己扩展了一个方法,begin开启事务方法,其实就是调用父类公用beginInternal方法,另外父类end方法直接继承使用。
这里提一下2个扩展ThreadLocal的类,用来做线程级别数据共享的。BEDSoftTransaction里的ExecutorDataMap和AbstractSoftTransaction里的ExecutorExceptionHandler
ExecutorDataMap做线程级别缓存transaction对象和transactionConfig
ExecutorDataMap.getDataMap().put(TRANSACTION, result);
ExecutorDataMap.getDataMap().put(TRANSACTION_CONFIG, transactionConfig);
ExecutorExceptionHandler做线程级别异常处理,里面3个方法,setExceptionThrown可能在A类设置,在B类根据isExceptionThrown判断是抛出还是处理,最后传入handleException处理,整个设计就是用户只需关心setExceptionThrown,handleException都是一样的。
/**
* 设置是否将异常抛出.
*
* @param isExceptionThrown 是否将异常抛出
*/
public static void setExceptionThrown(final boolean isExceptionThrown) {
ExecutorExceptionHandler.IS_EXCEPTION_THROWN.set(isExceptionThrown);
}
/**
* 获取是否将异常抛出.
*
* @return 是否将异常抛出
*/
public static boolean isExceptionThrown() {
return null == IS_EXCEPTION_THROWN.get() ? true : IS_EXCEPTION_THROWN.get();
}
/**
* 处理异常.
*
* @param ex 待处理的异常
*/
public static void handleException(final Exception ex) {
if (isExceptionThrown()) {
throw new ShardingJdbcException(ex);
}
log.error("exception occur: ", ex);
}
重点看一下BestEffortsDeliveryListener类,这个方法是同步event的处理方法
@Subscribe
@AllowConcurrentEvents
public void listen(final DMLExecutionEvent event) {
if (!isProcessContinuously()) {
return;
}
//config主要内容是1、最大尝试次数2、事务日志类型默认RDB 3、存储事务日志的数据源
//4、内嵌的最大努力送达型异步作业配置对象(异步最大执行次数、异步执行延迟毫秒数、zk目录和端口)
SoftTransactionConfiguration transactionConfig = SoftTransactionManager.getCurrentTransactionConfiguration().get();
TransactionLogStorage transactionLogStorage = TransactionLogStorageFactory.createTransactionLogStorage(transactionConfig.buildTransactionLogDataSource());
BEDSoftTransaction bedSoftTransaction = (BEDSoftTransaction) SoftTransactionManager.getCurrentTransaction().get();
switch (event.getEventExecutionType()) {
case BEFORE_EXECUTE:
//TODO 对于批量执行的SQL需要解析成两层列表
transactionLogStorage.add(new TransactionLog(event.getId(), bedSoftTransaction.getTransactionId(), bedSoftTransaction.getTransactionType(),
event.getDataSource(), event.getSql(), event.getParameters(), System.currentTimeMillis(), 0));
return;
case EXECUTE_SUCCESS:
//执行成功删除tranlog
transactionLogStorage.remove(event.getId());
return;
case EXECUTE_FAILURE:
//失败继续执行
boolean deliverySuccess = false;
for (int i = 0; i < transactionConfig.getSyncMaxDeliveryTryTimes(); i++) {
if (deliverySuccess) {
return;
}
boolean isNewConnection = false;
Connection conn = null;
PreparedStatement preparedStatement = null;
try {
conn = bedSoftTransaction.getConnection().getConnection(event.getDataSource(), SQLStatementType.UPDATE);
//验证不通过重新获取conn
if (!isValidConnection(conn)) {
bedSoftTransaction.getConnection().releaseBrokenConnection(conn);
conn = bedSoftTransaction.getConnection().getConnection(event.getDataSource(), SQLStatementType.UPDATE);
isNewConnection = true;
}
preparedStatement = conn.prepareStatement(event.getSql());
//TODO 对于批量事件需要解析成两层列表
for (int parameterIndex = 0; parameterIndex < event.getParameters().size(); parameterIndex++) {
preparedStatement.setObject(parameterIndex + 1, event.getParameters().get(parameterIndex));
}
preparedStatement.executeUpdate();
deliverySuccess = true;
transactionLogStorage.remove(event.getId());
} catch (final SQLException ex) {
log.error(String.format("Delivery times %s error, max try times is %s", i + 1, transactionConfig.getSyncMaxDeliveryTryTimes()), ex);
} finally {
close(isNewConnection, conn, preparedStatement);
}
}
return;
default:
throw new UnsupportedOperationException(event.getEventExecutionType().toString());
}
}
这段代码很简单,但是满足不断重试,数据最终一致,对事务操作,还是有一定要求的。
最大努力送达型,要求多次操作不涉及状态累积或变迁
(1)、要求update操作,幂等性:
一个幂等的操作典型如:
把编号为5的记录的A字段设置为0
这种操作不管执行多少次都是幂等的。
一个非幂等的操作典型如:
把编号为5的记录的A字段增加1
这种操作显然就不是幂等的。
要做到幂等性,从接口设计上来说不设计任何非幂等的操作即可。
譬如说需求是:
当用户点击赞同时,将答案的赞同数量+1。
改为:
当用户点击赞同时,确保答案赞同表中存在一条记录,用户、答案。
赞同数量由答案赞同表统计出来。
(2)、要求INSERT语句要求必须包含主键(不能是自增主键)。
(3)、DELETE语句无要求。
对应的有异步最大送达型类NestedBestEffortsDeliveryJob,
继承AbstractIndividualThroughputDataFlowElasticJob,这套异步作业使用了当当网另外一个分布式作业调度系统elastic-job,这里就不说了。主要方法processData
@Override
public boolean processData(final JobExecutionMultipleShardingContext context, final TransactionLog data) {
try {
//transactionLogStorage是一个接口,具体processData实现看RdbTransactionLogStorage和MemoryTransactionLogStorage这2个类都很简单,针对TransactionLog的操作。
return transactionLogStorage.processData(
transactionConfig.getTargetConnection(data.getDataSource()), data, transactionConfig.getBestEffortsDeliveryJobConfiguration().get().getAsyncMaxDeliveryTryTimes());
} catch (final SQLException ex) {
throw new ShardingJdbcException(ex);
}
}