当前位置: 首页 > article >正文

自动同步多服务器下SQL脚本2.0

考虑到1.0的适用场景太过苛刻,一次只支持读取至多一个版本的脚本变化,想涉及多个脚本的连续读取就有困难,于是有了2.0。

该版本支持读取多个版本的sql脚本,并且如果某一脚本出现sql问题【如重复插入相同名称的字段】,则当前版本回滚,同时hd_version表不留痕,以便下次部署的时候可以再次插入。

2.0版本主要变动就是实现类: 

考虑到随着版本的变动,后续脚本文件维护的版本过多,不采用顺序读,改用二分查找
当然,也可以一个大版本对应一个SQL文件,也可以用倒序查找,都可以的...这里只是提供一个方案。用二分单纯看上了它的时间复杂度O(logN)。

方法讲解:

主要分为两个方法:

一个是通过二分寻找到最小【未执行过sql脚本的】版本号
一个是通过当前版本号以及sql脚本,去执行sql语句。

通过二分找到最小的版本号

/**
 * 二分与倒序查询的比较:
 * 二分可以在短的时间内,快速找到目标值,但是倒序排序,理论上还是O(N)的时间复杂度
 * 如果场景是,每个迭代只进行最后一至两个版本的sql脚本,那么倒序排序更好,二分的话,时间复杂度比较稳定
 */
    @Override
    @Transactional
    public void run(ApplicationArguments args) throws Exception {
        if (!databaseAutoFillSwitch) {
            log.info("database auto fill switch is false,skip auto fill");
            return;
        }
        String basePath = "/dbVersion/MySQL.sql";
        InputStream inputStream = this.getClass().getResourceAsStream(basePath);
        String sqlScript = IoUtil.readUtf8(inputStream);
        if (null == inputStream) {
            log.info("inputStream is null");
            return;
        }
        inputStream.close();
        List<String> versionList = new ArrayList<>();
        String[] lines = sqlScript.split("\n");
        for (String line : lines) {
            if (line.toLowerCase().contains(PREFIX)) {
                versionList.add(line.substring(line.lastIndexOf("-") + 1).trim().toLowerCase());
            }
        }
        int left = 0 , right = versionList.size() - 1;
        // 最终得到的left,表示不在库中的最小版本号,如果left == list.size() 则还需要去查询库中是否真正存在
        while(left <= right){
            int mid = left + (right- left)/2;
            if( 0 == hdCommonDao.selectVersion(versionList.get(mid))){
                // 库中无对应版本号
                right = mid - 1;
            }else{
                // 库中存在对应版本号
                left = mid + 1;
            }
        }
        if(left == versionList.size()){
            return;
        }
        String result = "";
        // 现在开始,从left指针开始遍历所有的sql脚本
        while(left < versionList.size()){
            // 得到版本号整串
            String latestVersion = versionList.get(left);
            // 写入数据库的版本号前缀【过滤掉无效字符,统一版本号】
            String version = latestVersion.substring(latestVersion.lastIndexOf("-") + 1).trim().toLowerCase();
            // 获取版本号在sql脚本中的位置
            int index = sqlScript.indexOf(latestVersion);
            if (index == -1) {
                log.info("current version exception:{}", version);
                LogUtil.info(version, "current version exception");
                return;
            }
            index += latestVersion.length();
            String nextVersion = "";
            if (left + 1 < versionList.size()) {
                nextVersion = versionList.get(left + 1);
                int nextIndex = sqlScript.indexOf(nextVersion);
                if (nextIndex != -1) {
                    result = sqlScript.substring(index, nextIndex).trim();
                    ((HdSchemaExecutor)AopContext.currentProxy()).executeSqlScript(result, version);
                } else {
                    log.info("next version not found:{}", nextVersion);
                    LogUtil.info(version, "next version not found");
                }
            } else {
                // 没有下一个版本,提取剩余部分
                result = sqlScript.substring(index).trim();
                ((HdSchemaExecutor)AopContext.currentProxy()).executeSqlScript(result, version);
            }
            left++;
        }
        log.info("auto deploying sql finished...");
    }

 写库方法

根据sql脚本以及对应的版本号完成写入功能。

    @Transactional(rollbackFor = Exception.class)
    public void executeSqlScript(String sqlScript, String version) throws Exception {
        String[] resultList = sqlScript.split(";");
        for (String line : resultList) {
            if (!line.toLowerCase().contains("drop") && !line.toLowerCase().contains("delete") && line.length() > 10 && !line.contains("--")) {
                // 开始执行插入操作
                try {
                    hdCommonDao.updateSql(line.trim());
                    log.info("version:{}, start sql script:{}", version, line.trim());
                    LogUtil.info("version, sql script:", version, line.trim());
                } catch (Exception e) {
                    log.info("version:{}, sql执行异常:{}", version, line.trim());
                    LogUtil.info("sql执行异常", line.trim());
                    throw new Exception("sql auto exception:"+ line.trim());
                }
            }
        }
        // 如果所有 SQL 语句都成功执行,插入版本记录
        HdVersionEntity entity = new HdVersionEntity();
        entity.setVersion(version);
        entity.setCreated(new Date());
        hdCommonDao.insertVersion(entity);
    }

细节说明

这里主要一个点,事务失效

首先,我们在方法A去调用方法B的时候,不是简单的在方法A上加@Transactional注解就可以的,需要两步:①在启动类上开启暴露代理的开关②在调用方法B的时候,改用代理对象去调用方法B【默认是this对象】

@Order(1)
@Component
@EnableAspectJAutoProxy(exposeProxy = true)
@Slf4j
public class HdSchemaExecutor implements ApplicationRunner

 注意这里需要通过代理对象调用方法B~

((HdSchemaExecutor)AopContext.currentProxy()).executeSqlScript(result, version);

如果你想在方法B完成手动throw错误,还需要在方法B上添加事务监听的范围。

@Transactional(rollbackFor = Exception.class)
public void executeSqlScript(String sqlScript, String version) throws Exception

写在最后

Mysql不支持DDL事务,只支持DML事务....


http://www.kler.cn/a/581206.html

相关文章:

  • Ubuntu 22.04 无法进入图形界面的解决方法
  • linux下的网络抓包(tcpdump)介绍
  • Nature | 新方法moscot:单细胞时空图谱的“导航系统”
  • Maven 私服 Nexus 简单使用
  • 标准卷积(Standard Convolution)
  • nodejs使用WebSocket实现聊天效果
  • IDEA 创建SpringCloud 工程(图文)
  • GWO-CNN-BiLSTM-Attention多变量多步时间序列预测 | Matlab实现灰狼算法优化卷积双向长短期记忆融合注意力机制
  • 《深入解析Java synchronized死锁:从可重入锁到哲学家就餐问题》
  • 邮件发送IP信誉管理:避免封号
  • 【Linux篇】初识Linux指令(上篇)
  • Spring(4)——响应相关
  • SAP学习笔记 - 豆知识16 - Msg 番号 V1320 - 明細Category不能使用 (Table T184 OR DIEN TAP)
  • wireguard搭配udp2raw部署内网
  • AI辅助工具Trae和Cursor的区别
  • MySQL事务深度解析:ACID特性、隔离级别与MVCC机制
  • wireshark 如何关闭混杂模式 wireshark操作
  • ubuntu软件
  • 有必要使用 Oracle 向量数据库吗?
  • Python Openpyxl给Excel增加条件规则