sharding-jdbc源码阅读之soft transaction

先看一段作者张亮,对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最大努力送达型架构图:
sharding-jdbc源码阅读之soft transaction_第1张图片

代码阅读

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);
        }
    }

你可能感兴趣的:(sharding-jdbc)