自动化同步多服务器数据库表结构

当项目每次进行版本升级的时候,如果在这次迭代中涉及表结构变更,需要将不同的生产环境下,都需要同步表结构的DDL语句,比较麻烦,而且还有可能忘记同步脚本,导致生产环境报错....

该方案采用SpringBoot+Mybatis/MybatisPlus框架,完成在项目启动时,自动化执行sql脚本,并且同时支持版本号【如果当前版本号高于该sql文件,则不执行】。

1、先创建一张表,专门用来记录已经同步过的sql脚本文件名、对应的版本号。

sql 复制代码
CREATE TABLE `hd_version` (
  `id` varchar(64) NOT NULL,
  `version` varchar(64) DEFAULT NULL COMMENT '版本号',
  `created` datetime DEFAULT NULL COMMENT '创建时间',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据版本';
java 复制代码
import java.util.Date;
import lombok.Data;
@Data
public class HdVersionEntity {
    /**
     * 主键id
     */
    private String id;
    /**
     * 版本号(一般是文件名去掉文件后缀)
     */
    private String version;
    /**
     * 文件名
     */
    private String remark;
    /**
     * 创建时间
     */
    private Date created;
}
java 复制代码
import lombok.Data;

@Data
public class SchemaData {
    /**
     * 版本号
     */
    public String version;

    /**
     * 文件名
     */
    public String fileName;

    public SchemaData(String version, String fileName) {
        this.version = version;
        this.fileName = fileName;
    }
}

2、接着编写dao层

java 复制代码
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

@Mapper
public interface HdCommonDao  {
    /**
     * 查询表中是否存在当前版本号
     * @param version
     * @return
     */
    int selectVersion(@Param("version") String version);

    /**
     * 插入版本
     * @param entity
     * @return
     */
    int insertVersion(HdVersionEntity entity);

    /**
     * 执行sql,可以是DML、DDL
     * @param sql
     */
    @Update("${sql}")
    void updateSql(@Param("sql") String sql);
}

3、以及对应的Mapper文件

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

             <!--这里文件空间命名改成自己路径下的-->
<mapper namespace="com.xxx.DatabaseAutoFill.HdCommonDao">

    <select id="selectVersion" resultType="int">
        selecT count(1) from hd_version
        where version = #{version}
    </select>
    <select id="selectTableExist" resultType="int">
        select count(*) count  from information_schema.TABLES where TABLE_NAME = #{tableName} and  table_schema = (select database())
    </select>

    <insert id="insertVersion">
        insert into hd_version(id,version, remark, created) values (uuid(),#{version}, #{remark}, #{created})
    </insert>
    
</mapper>

4、 编写实现类

注意,这里是将整段逻辑放在ApplicationRunner接口下执行,即当Spring容器加载完之后,会立即执行该方法。

java 复制代码
@Order(1)
@Component
@Slf4j
public class HdSchemaExecutor implements ApplicationRunner {

    @Autowired
    HdCommonDao hdCommonDao;
    // 数据库脚本文件列表

    private static final String PREFIX = "--v";

    @Override
    @Transactional
    public void run(ApplicationArguments args) throws IOException {
        String basePath = "/dbVersion/MySQL.sql";
        InputStream inputStream = this.getClass().getResourceAsStream(basePath);
        String sqlScript = IoUtil.readUtf8(inputStream);
        assert inputStream != null;
        inputStream.close();
        /**
         * 一次至多只会执行一个版本,其实我们可以拿到所有的版本并执行最后一个版本即可
         */
        List<String> versionList = new ArrayList<>();
        String[] lines = sqlScript.split("\n");
        for (String line : lines) {
            if(line.toLowerCase().contains(PREFIX)){
                versionList.add(line);
            }
        }
        // 得到版本号整串
        String latestVersion = versionList.get(versionList.size()-1);
        // 写入数据库的版本号前缀
        String version = latestVersion.substring(latestVersion.lastIndexOf("-")+1).trim().toLowerCase();
        int index = sqlScript.lastIndexOf(latestVersion); // 查找s2在s1中的起始位置
        String result = "";
        if (index != -1) {
            // 截取s2在s1中结束位置之后的部分
            result = sqlScript.substring(index + latestVersion.length());
        } else {
            log.info("current version exception:{}",version);
            LogUtil.info(version, "current version exception");
        }
        //String[] resultList = result.split("\n");
        String[] resultList = result.split(";");
        int cnt = hdCommonDao.selectVersion(version);
        boolean successInsert = false;
        // 说明不需要写入库
        if(cnt ==1 )return;
        for (String line : resultList) {
            if(!line.toLowerCase().contains("drop") && !line.toLowerCase().contains("delete") && line.length() > 25 && !line.contains("--")) {
                //开始执行插入操作
                try {
                    hdCommonDao.updateSql(line.trim());
                    successInsert = true;
                    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());
                }
            }
        }
        if(successInsert){
            HdVersionEntity entity = new HdVersionEntity();
            entity.setVersion(version);
            entity.setCreated(new Date());
            hdCommonDao.insertVersion(entity);
        }
        log.info("auto deploying sql finished...");
    }
}

这里主要干三件事:

读取指定路径下的文件夹中的所有文件

根据这些文件的文件名去表里查,是否插入过,没有说明需要被插入,即需要执行的sql脚本

执行sql脚本

我这里的路径是resources下的相对路径,因为我这个代码是要打包放到线上环境的,用绝对路径可能会报(FILE NOT FOUND ERROR)FNFE

PS

以上方法对于Spring容器加载时,没有强依赖的表,是可以通用的 (可能有点拗口)。

即,如果Spring容器启动时,如果需要依赖某张表,否则启动失败的话怎么办,还能用我们上述方法吗?

理论上是不行的,我这里将容器启动时,必须强依赖的表(Quartz框架)删去,启动时报错。

那对应这种情况,该怎么解决呢?

其实这种框架,都会提供注解,如:

表明,在项目启动的时候,会自动完成jdbc的初始化,即如果你没有表,会先给你执行表的创建,因此不需要我们去考虑。

复制代码
spring.quartz.jdbc.initialize-schema=always

Quartz也起来了。

写在最后

由于这个工程是临时突加的,我也不好随便就测试环境的库来删删改改,因此我在本地windows上用docker部署了mysql,来测试的。以下是在windows上的docker部署mysql步骤:

docker pull mysql:8.0

在c盘用户目录下,创建conf、data、logs三个文件夹

在conf目录下,创建my.cnf文件,里面编写如下内容。

XML 复制代码
[mysql]
#设置mysql客户端默认字符集
default-character-set=UTF8MB4
[mysqld]
#设置3306端口
port=3306
#允许最大连接数
max_connections=200
#允许连接失败的次数
max_connect_errors=10
#默认使用"mysql_native_password"插件认证
default_authentication_plugin=mysql_native_password
#服务端使用的字符集默认为8比特编码的latin1字符集
character-set-server=UTF8MB4
#开启查询缓存
explicit_defaults_for_timestamp=true
#创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
#等待超时时间秒
wait_timeout=60
#交互式连接超时时间秒
interactive-timeout=600
# 对数据库表大小写不敏感设置,默认设置为小写,比较也全部设置为小写在比较
lower-case-table-names=1
# 设置默认时区
default-time_zone='+8:00'

启动容器,注意在windows下 需要把每行后面的 `\`删去,否在windows下会启动失败
docker run --name mysql8.0 \

-v D:\docker\data\mysql8.0\config\my.cnf:/etc/mysql/my.cnf \

-v D:\docker\data\mysql8.0\data:/var/lib/mysql \

-v D:\docker\data\mysql8.0\logs:/logs -p 3306:3306 \

-e MYSQL_ROOT_PASSWORD=123456 \

-e TZ=Asia/Shanghai \

-d mysql:8.0 \

--lower-case-table-names=1

这样,理论上就能启动成功了。

分享几个常用的命令:

docker exec -it 容器名称/容器id bash #进入容器

docker logs 容器名称/容器id -f -n=100 查看容器最后一百行日志

相关推荐
纷飞梦雪27 分钟前
使用chroot预安装软件到ubuntu22中
linux·运维·ubuntu
明矾java4 小时前
MySQL进阶-关联查询优化
数据库·mysql
冰糖码奇朵4 小时前
大数据表高效导入导出解决方案,mysql数据库LOAD DATA命令和INTO OUTFILE命令详解
java·数据库·sql·mysql
迷路的小犀牛4 小时前
【MYSQL数据库异常处理】执行SQL语句报超时异常
数据库·sql·mysql
jiarg5 小时前
linux 内网下载 yum 依赖问题
linux·运维·服务器
yi个名字5 小时前
Linux第一课
linux·运维·服务器
极限实验室6 小时前
INFINI Labs 产品更新 | Easysearch 增加异步搜索等新特性
数据库
m0_748246876 小时前
maven导入spring框架
数据库·spring·maven
前后相随6 小时前
springboot集成maven多模块开发
数据库·oracle
菜鸟xy..6 小时前
linux 基本命令教程,巡查脚本,kali镜像
linux·运维·服务器·kali镜像·巡查脚本·nmtui