使用JdbcTemplate数据迁移,事务控制以及不生效原因

问题:多个database的多张表需要进行加密操作,必须保证事务,否则会导致部分库的部分数据加密

代码地址:https://gitee.com/edward_liu/migration-data/tree/master

环境JDK1.8:

        
            tk.mybatis
            mapper-spring-boot-starter
            2.0.2
        
        
            tk.mybatis
            mapper
            4.0.4
        
        
            com.alibaba
            druid
            1.1.6
        
        
            mysql
            mysql-connector-java
            runtime
        

spring声明式事务控制

service的bean以及方法上添加@Transactional,事务的力度比较粗,多个库需要加密因此需要针对每个connection进行事务控制,力度要尽量的小

尝试一

使用spring中jdbcTemplate,每个库对应一个jdbcTemplate,通过jdbcTemplate获取connection进行事务控制达到事务一致的控制

public Object getInfo() throws Exception {
        List> maps = new ArrayList<>();
        List migrationDatabaseInfos = mapper.selectAll();
        for (MigrationDatabaseInfo databaseInfo : migrationDatabaseInfos) {
            JdbcTemplate jdbcTemplate = DbConnect.getJdbcTemplate(databaseInfo);
            MigrationTableInfo tableInfo = new MigrationTableInfo();
            tableInfo.setDataCode(databaseInfo.getDataCode());
            List migrationTableInfos = migrationTableInfoService.selectByCondition(tableInfo);
            //获取连接 步骤一
            Connection connection = jdbcTemplate.getDataSource().getConnection();
            System.out.println(connection);
            try {
                //开启事务
                connection.setAutoCommit(false);
                System.out.println(connection);
                for (MigrationTableInfo migrationTableInfo : migrationTableInfos) {
                    String sql = "select id ," + migrationTableInfo.getTableColumn() + " from " + migrationTableInfo.getTableName();
                    log.info("执行的sql是:{}", sql);
                    maps = jdbcTemplate.queryForList(sql);
                    for (Map map : maps) {
                        StringBuilder update = new StringBuilder("update ").append(migrationTableInfo.getTableName()).append(" set ");
                        Object id = map.remove("id");
                        boolean allParamIsNull = true;
                        for (Map.Entry entry : map.entrySet()) {
                            Object value = entry.getValue();
                            //参数全为空 不更新
                            allParamIsNull = allParamIsNull && (value == null);
                            if (value == null) {
                                continue;
                            }
                            value = Sm4Util.encryptEcb(sm4Key, value.toString());
                            update.append(entry.getKey()).append(" = '").append(value).append("', ");
                        }
                        if(!allParamIsNull){
                            String updateSQL = update.substring(0, update.lastIndexOf(",")) + " where id = " + id + ";";
                            log.info("update sql is {}", updateSQL);
                            //真正执行sql 步骤二
                            jdbcTemplate.update(updateSQL);
                        }
                    }
                }
                connection.commit();
            } catch (Exception e) {
                e.printStackTrace();
                connection.rollback();
            } finally {
                if(connection != null){
                    connection.setAutoCommit(true);
                }
            }

        }
        return maps;
    }

实际效果事务不生效,大胆猜测:Connection connection = jdbcTemplate.getDataSource().getConnection();获取的connection与步骤二真正执行时获取的connection并不是同一个。

上代码开启debug模式,开启小心验证环节

使用JdbcTemplate数据迁移,事务控制以及不生效原因_第1张图片

步骤二debug后会进入execute这个方法中

使用JdbcTemplate数据迁移,事务控制以及不生效原因_第2张图片

使用JdbcTemplate数据迁移,事务控制以及不生效原因_第3张图片

使用的是druidDatasource

使用JdbcTemplate数据迁移,事务控制以及不生效原因_第4张图片

最终获取到的connection是DruidPooledConnection,从字面上的意思应该是从一个连接池里获取的连接,步骤一和步骤二中获取到的connection应该都是从连接池中获取的连接,因此可以猜测事务不生效的因为是:步骤一获取的connection和步骤二获取connection不是同一个连接,导致无法控制事务,我们进一步继续看源码

使用JdbcTemplate数据迁移,事务控制以及不生效原因_第5张图片

直接看返回值,最后的结果是返回新创建的对象,这就更加诡异了,怎么每次创建都会创建一个新对象呢?匪夷所思,我们小心求证,看一下DruidPooledConnection这个实体的构造方法。

使用JdbcTemplate数据迁移,事务控制以及不生效原因_第6张图片

使用JdbcTemplate数据迁移,事务控制以及不生效原因_第7张图片

了然了,这是从holder中获取connection这个真正的连接,那我们看一下holder的实现过程

for (boolean createDirect = false;;) {
            //false不执行
            if (createDirect) {
                if (createCountUpdater.compareAndSet(this, 0, 1)) {
                    PhysicalConnectionInfo pyConnInfo = DruidDataSource.this.createPhysicalConnection();
                    holder = new DruidConnectionHolder(this, pyConnInfo);
                    holder.lastActiveTimeMillis = System.currentTimeMillis();

                    createCountUpdater.decrementAndGet(this);
                    directCreateCountUpdater.incrementAndGet(this);

                    if (LOG.isDebugEnabled()) {
                        LOG.debug("conn-direct_create ");
                    }

                    boolean discard = false;
                    lock.lock();
                    try {
                        if (activeCount < maxActive) {
                            activeCount++;
                            if (activeCount > activePeak) {
                                activePeak = activeCount;
                                activePeakTime = System.currentTimeMillis();
                            }
                            break;
                        } else {
                            discard = true;
                        }
                    } finally {
                        lock.unlock();
                    }

                    if (discard) {
                        JdbcUtils.close(pyConnInfo.getPhysicalConnection());
                    }
                }
            }

            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                connectErrorCountUpdater.incrementAndGet(this);
                throw new SQLException("interrupt", e);
            }

            try {
                if (maxWaitThreadCount > 0
                        && notEmptyWaitThreadCount >= maxWaitThreadCount) {
                    connectErrorCountUpdater.incrementAndGet(this);
                    throw new SQLException("maxWaitThreadCount " + maxWaitThreadCount + ", current wait Thread count "
                            + lock.getQueueLength());
                }

                if (onFatalError
                        && onFatalErrorMaxActive > 0
                        && activeCount >= onFatalErrorMaxActive) {
                    connectErrorCountUpdater.incrementAndGet(this);

                    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    String errorMsg = "onFatalError, activeCount " + activeCount
                            + ", onFatalErrorMaxActive " + onFatalErrorMaxActive;

                    if (lastFatalErrorTimeMillis > 0) {
                        errorMsg += ", time '" + format.format(new Date(lastFatalErrorTimeMillis)) + "'";
                    }

                    if (lastFatalErrorSql != null) {
                        errorMsg += ", sql \n" + lastFatalErrorSql;
                    }

                    throw new SQLException(errorMsg, lastFatalError);
                }

                connectCount++;

                if (createScheduler != null
                        && poolingCount == 0
                        && activeCount < maxActive
                        && createCountUpdater.get(this) == 0
                        && createScheduler instanceof ScheduledThreadPoolExecutor) {
                    ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) createScheduler;
                    if (executor.getQueue().size() > 0) {
                        createDirect = true;
                        continue;
                    }
                }
                //获取holder的真正代码 步骤三

                if (maxWait > 0) {
                    holder = pollLast(nanos);
                } else {
                    holder = takeLast();
                }

                if (holder != null) {
                    activeCount++;
                    if (activeCount > activePeak) {
                        activePeak = activeCount;
                        activePeakTime = System.currentTimeMillis();
                    }
                }
            } catch (InterruptedException e) {
                connectErrorCountUpdater.incrementAndGet(this);
                throw new SQLException(e.getMessage(), e);
            } catch (SQLException e) {
                connectErrorCountUpdater.incrementAndGet(this);
                throw e;
            } finally {
                lock.unlock();
            }

            break;
        }

步骤三是真正的获取holder实例,看一下实现过程:

使用JdbcTemplate数据迁移,事务控制以及不生效原因_第8张图片

正真的原因与我们的猜测是一致的,每一次获取connection都是从holder中获取一个新的连接,因此代码中控制事务的connection与真正的执行connection不同,解决事务不生效思路:获取同一个connection

大胆尝试一:把连接池设为1不就可以了吗?

数据库连接池都设为1,代码调试中会报错,原因:池子里最大只有一个连接conn,而它又未被释放,导致jdbcTemplate内部再去从池子里获取con时,一直在等待已有连接conn的释放,一直等不到释放,所以等待了max-wait的时间,最后报错

大胆尝试一:获取真正的connection

JdbcTemplate是如何控制事务的呢?答案就是TransactionTemplate

public Object getInfoTransaction(){
        List migrationDatabaseInfos = mapper.selectAll();
        for (MigrationDatabaseInfo databaseInfo : migrationDatabaseInfos) {
            DataSource datasource = DbConnect.getDatasource(databaseInfo);
            TransactionTemplate transactionTemplate = DbConnect.getTransactionTemplate(datasource);
            JdbcTemplate jdbcTemplate = DbConnect.getJdbcTemplate(datasource);
            MigrationTableInfo tableInfo = new MigrationTableInfo();
            tableInfo.setDataCode(databaseInfo.getDataCode());
            List migrationTableInfos = migrationTableInfoService.selectByCondition(tableInfo);
            transactionTemplate.execute( (TransactionStatus transactionStatus) ->{
                Object savepoint = transactionStatus.createSavepoint();
                try {
                    for (MigrationTableInfo migrationTableInfo : migrationTableInfos) {
                        String sql = "select id ," + migrationTableInfo.getTableColumn() + " from " + migrationTableInfo.getTableName();
                        log.info("执行的sql是:{}", sql);
                        List> maps = jdbcTemplate.queryForList(sql);
                        for (Map map : maps) {
                            StringBuilder update = new StringBuilder("update ").append(migrationTableInfo.getTableName()).append(" set ");
                            Object id = map.remove("id");
                            boolean allParamIsNull = true;
                            for (Map.Entry entry : map.entrySet()) {
                                Object value = entry.getValue();
                                //参数全为空 不更新
                                allParamIsNull = allParamIsNull && (value == null);
                                if (value == null) {
                                    continue;
                                }
                                value = Sm4Util.encryptEcb(sm4Key, value.toString());
                                update.append(entry.getKey()).append(" = '").append(value).append("', ");
                            }
                            if (!allParamIsNull) {
                                String updateSQL = update.substring(0, update.lastIndexOf(",")) + " where id = " + id + ";";
                                log.info("update sql is {}", updateSQL);
                                jdbcTemplate.update(updateSQL);
                            }
                        }
                    }
                }catch (Exception e){
                    log.error("异常信息是: {}", e.getMessage());
                    transactionStatus.setRollbackOnly();
                    throw new RuntimeException(e);
                }
                return null;
            });
        }
        return "success";
    }

 

欢迎关注作者公众号交流

回复  8888可以领取面试资料

使用JdbcTemplate数据迁移,事务控制以及不生效原因_第9张图片

你可能感兴趣的:(数据库)