Mysql和Oracle在一个事务中创建表时,即使事务失败回滚,表也不会删除,导致再跑changelog时出现表名冲突报错。
而PostgreSQL可以正常删除表。
可以在创建表的时候写好rollback:
<changeSet id="t13" author="vudsen">
<createTable tableName="table13">
<column name="col1" type="${type.int}"/>
<column name="col2" type="${type.int}"/>
<column name="col3" type="${type.int}"/>
</createTable>
<rollback>
<dropTable tableName="table13"/>
</rollback>
</changeSet>
xml
然后添加listener:
public class ChangeExecListenerImpl implements ChangeExecListener {
@Override
public void runFailed(ChangeSet changeSet, DatabaseChangeLog databaseChangeLog, Database database, Exception exception) {
List<Change> changes = changeSet.getChanges();
String shortName = database.getShortName();
if ("mysql".equals(shortName) || "oracle".equals(shortName)) {
boolean shouldRollbackNow = false;
for (Change change : changes) {
if (change instanceof CreateTableChange) {
shouldRollbackNow = true;
break;
}
}
if (!shouldRollbackNow) {
return;
}
log.info("正在执行回滚逻辑");
RollbackContainer rollback = changeSet.getRollback();
try {
database.setAutoCommit(false);
database.rollback();
for (Change change : rollback.getChanges()) {
database.executeStatements(change, databaseChangeLog, Collections.emptyList());
}
database.commit();
} catch (LiquibaseException e) {
try {
database.rollback();
} catch (DatabaseException ex) {
throw new RuntimeException(ex);
}
throw new RuntimeException(e);
}
}
}
}
java
出现这种情况应该是schema的版本和文档不一致:
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsdtext
只要把后面的版本和依赖版本配置成一样的就好了。
用于给Changelog标记一个版本号,便于回滚时使用:
<changeSet author="liquibase-docs" id="tagDatabase-example">
<tagDatabase tag="version_1.3"/>
</changeSet>
xml
回滚时有一个根据tag来回滚的方法,没有根据changeSet id回滚的方法。
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| name | ✅ | ✅ | ✅ | 指定列名 | |
| type | ✅ | ✅ | ✅ | 指定列属性 | 见下方备注 |
| value | ✅ | ✅ | ✅ | 指定该列的值 | 仅用于update、insert,使用时会自动套引号,手动加引号会被转义 |
| afterColumn | ❌ | ❌ | ❌ | 将该列置于某列之后 | 不起作用 |
| beforeColumn | ❌ | ❌ | ❌ | 将该列置于某列之前 | 不起作用 |
| position | ❌ | ❌ | ❌ | 设置该列的位置 | 不起作用 |
| autoIncrement | ✅ | ✅ | ✅ | 自动增长 | |
| computed | ❌ | ❌ | ❌ | 声明name为函数调用而不是字符串 | 仍然被当做字符串提交了 |
| defaultValue | ✅ | ✅ | ✅ | 默认值 | |
| defaultValueBoolean | ✅ | ✅ | ✅ | 默认布尔值 | |
| defaultValueComputed | ✅ | ✅ | ✅ | 声明默认函数值 | 一般用于Date类型的now |
| defaultValueConstraintName | ? | ? | ? | 声明默认的约束名称 | 文档中有这个字段,但实际用不了 |
| defaultValueDate | ✅ | ✅ | ✅ | 声明默认日期 | |
| defaultValueNumeric | ✅ | ✅ | ✅ | 声明默认数字 | |
| descending | ❌ | ✅ | ✅ | 声明索引是升序还是降序 | 默认升序,mysql8.0+才支持 |
| remark | ✅ | ✅ | ✅ | 备注 | |
| incrementBy | ❌ | ✅ | ✅ | 自增主键每次增长多少 | 和startWith一起用oracle会报错 |
| startWith | ❌ | ❌ | ✅ | 自增主键从哪里开始 | 和incrementBy一起用oracle会报错 |
| encoding | ✅ | ❌ | ✅ | 指定valueClobFile字符集 | |
| valueBlobFile | ✅ | ✅ | ✅ | 从文件中读取二进制数据 | 插入数据时使用 |
| valueClobFile | ✅ | ✅ | ✅ | 从文件中读取大对象 | 插入数据时使用 |
| valueComputed | ✅ | ✅ | ✅ | 使用函数获取value值 | 插入数据时使用 |
| valueDate | ✅ | ✅ | ✅ | 使用date | 插入数据时使用 |
| valueNumeric | ✅ | ✅ | ✅ | 使用数字 | 插入数据时使用 |
备注:type可以直接使用如下值,会被自动转换成对应数据库的数据结构:
BIGINTBLOBBOOLEANCLOBCURRENCYDATEDATETIMETIMEUUID也可以直接使用java类,如:
java.sql.Types.TIMESTAMPjava.sql.Types.VARCHAR(255)| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| primaryKey | ✅ | ✅ | ✅ | 主键 | |
| nullable | ✅ | ✅ | ✅ | 是否可空 | |
| checkConstraint | ❌ | ❌ | ❌ | check约束 | |
| primaryKeyTablespace | ? | ? | ? | 指定主键表空间 | 未测试 |
| foreignKeyName | ? | ? | ? | 外键名称 | 推荐使用addForeignKeyConstraint标签 |
| deleteCascade | ? | ? | ? | 当外键删除时是否跟着删除对应的行 | 推荐使用addForeignKeyConstraint标签 |
| unique | ✅ | ✅ | ✅ | 是否唯一 | |
| uniqueConstraintName | ✅ | ✅ | ✅ | 唯一键名称 | |
| references | ? | ? | ? | 定义外键 | 不推荐使用,该属性会将值原封不动的贴到SQL语句上 |
| initiallyDeferred | ❌ | ? | ? | 立即检查约束 | MySql不支持,Oracle文档上支持,但没有测试出结果 |
| deferrable | ❌ | ? | ? | 延迟检查约束 | MySql不支持,Oracle文档上支持,但没有测试出结果 |
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| catalogName | ✅ | ✅ | ✅ | 目录名 | CREATE INDEX xx ON catalogName.column ... |
| clustered | ❌ | ? | ? | 是否为聚簇索引 | PostgreSQL的聚簇索引不允许在事务下创建,oracle的聚簇索引由于liquibase拼接sql有误导致创建失败,mysql忽视了该语句(可能是没有这个功能) |
| indexName | ✅ | ✅ | ✅ | 索引名称 | |
| schemaName | ? | ? | ? | schema名称 | 未测试,可以使用catalogName代替 |
| tableName | ✅ | ✅ | ✅ | 要给哪个表加索引 | |
| tablespace | ? | ? | ? | 表空间 | 未测试 |
| unique | ✅ | ✅ | ✅ | 是否为唯一键 |
存储过程只能通过手写sql来创建:createProcedure (liquibase.com)
视图只能通过手写sql来创建:createView (liquibase.com)
createSequence (liquibase.com)
创建一个序列。MySQL不支持,PostgreSQL和Oralce支持。
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| tableName | ✅ | ✅ | ✅ | 要创建的表名 | |
| catalogName | ✅ | ✅ | ✅ | 目录名 | |
| schemaName | ? | ? | ? | schema名称 | 未测试,可以使用catalogName代替 |
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| baseTableName | ✅ | ✅ | ✅ | 给哪个表加外键 | |
| baseColumnNames | ✅ | ✅ | ✅ | 给哪个列加外键 | |
| constraintName | ✅ | ✅ | ✅ | 外键名称 | |
| referencedTableName | ✅ | ✅ | ✅ | 外键指向哪个表 | |
| referencedColumnNames | ✅ | ✅ | ✅ | 外键指向表的哪个字段 | |
| deleteCascade | ✅ | ✅ | ✅ | 当外键指向的表中,某行被删除后,删除对应其它所有指向它的行 | |
| onUpdate | ❌ | ❌ | ❌ | 当外键被更新后的操作 | |
| onDelete | ✅ | ✅ | ✅ | 当外键被删除后的操作 | |
| validate | ? | ? | ? | 添加约束时,是否检查所有的行是否满足约束 | 文档有,但是代码用了会报错 |
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| catalogName | ✅ | ✅ | ✅ | 目录名 | |
| columnNames | ✅ | ✅ | ✅ | 要给哪些列加主键 | 复合主键使用逗号分割列 |
| constraintName | ❌ | ✅ | ✅ | 主键名称 | Mysql主键固定为PRIMARY |
| schemaName | ? | ? | ? | schema | 未测试,可以使用catalogName代替 |
| tableName | ✅ | ✅ | ✅ | 要给哪个表加主键 | |
| tablespace | ? | ? | ? | 表空间 | 未测试 |
| validate | ? | ? | ? | 添加约束时,是否检查所有的行是否满足约束 | 文档有,但是代码用了会报错 |
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| catalogName | ✅ | ✅ | ✅ | 目录名 | |
| columnName | ✅ | ✅ | ✅ | 列名 | |
| defaultValue | ✅ | ✅ | ✅ | 默认值 | 此处省略了其它defaultValue*属性 |
| tableName | ✅ | ✅ | ✅ | 要给哪个表加默认值 |
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| tableName | ✅ | ✅ | ✅ | 表名称 | |
| columnNames | ✅ | ✅ | ✅ | 要给哪个列加唯一键 | 多个列用逗号分割 |
| constraintName | ✅ | ✅ | ✅ | 约束名称 | |
| catalogName | ✅ | ✅ | ✅ | 目录名 |
功能和添加外键相同,但是这个标签会自动把数据同步过去:
<changeSet id="t11" author="vudsen">
<createTable tableName="table9">
<column name="col1" type="${type.int}"/>
<column name="col2" type="${type.int}"/>
</createTable>
<insert tableName="table9">
<column name="col1" value="1"/>
<column name="col2" value="1"/>
</insert>
<addLookupTable existingTableName="table9" existingColumnName="col1" newTableName="t11_lut" newColumnName="col1_lut" newColumnDataType="${type.int}"/>
</changeSet>
xml
在运行后,会给table9创建一个FK_TABLE9_T11_LUT的外键,指向表t11_lut(col1_lut),同时,t11_lut中也会有table9的数据。
Mysql、PostgreSQL和Oracle都支持。
Oracle不支持,MySQL和PostgreSQL支持
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| tableName | ✅ | ✅ | ❌ | 表名 | |
| columnName | ✅ | ✅ | ❌ | 列名 | |
| incrementBy | ❌ | ❌ | ❌ | 每次自增多少 | |
| startWith | ❌ | ❌ | ❌ | 从哪里开始自增 | |
| columnDataType | ✅ | ✅ | ❌ | 列的类型 | 必填 |
| catalogName | ✅ | ✅ | ❌ | 目录名 |
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| tableName | ✅ | ✅ | ✅ | 表名 | |
| columnName | ✅ | ✅ | ✅ | 列名 | |
| constraintName | ❌ | ❌ | ❌ | 约束名称 | 这个属性会被忽略 |
| columnDataType | ✅ | ✅ | ✅ | 列类型 | 必填 |
| defaultNullValue | ❌ | ❌ | ❌ | 默认空值 | 这个属性会被忽略 |
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| tableName | ✅ | ✅ | ✅ | 要插入到哪个表 | |
| dbms | ✅ | ✅ | ✅ | 指定数据库进行插入 | |
| catalogName | ✅ | ✅ | ✅ | 目录名 |
update使用示例:
<changeSet id="t13" author="vudsen">
<createTable tableName="table13">
<column name="col1" type="${type.int}"/>
<column name="col2" type="${type.int}"/>
<column name="col3" type="${type.int}"/>
</createTable>
<insert tableName="table13" catalogName="">
<column name="col1" value="1"/>
<column name="col2" value="2"/>
<column name="col3" value="3"/>
</insert>
<update tableName="table13">
<column name="col1" valueComputed="col1 + 1"/>
<where>col1 = :value AND col3 = :value</where>
<whereParams>
<param valueNumeric="1"/>
<param valueNumeric="3"/>
</whereParams>
</update>
<rollback>
<dropTable tableName="table13"/>
</rollback>
</changeSet>
xml
whereParams中只能在where用:value来对应,whereParams里的name没有用处。
推荐参数都在whereParams里写,liquibase会自动转换日期等类型(mysql日期可以直接写字符串,oracle需要借助函数)
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| tableName | ✅ | ✅ | ✅ | 表名 | |
| catalogName | ✅ | ✅ | ✅ | 目录名 | |
| cascadeConstraints | ❌ | ✅ | ✅ | 删除时是否删除指向它的外键所对应的行 |
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| baseTableName | ✅ | ✅ | ✅ | 要删除哪个表的外键 | |
| baseTableCatalogName | ✅ | ✅ | ✅ | 目录名 |
如果要删除单个外键可以用dropForeignKeyConstraint
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| tableName | ✅ | v | ✅ | 表名 | |
| columnName | ✅ | ✅ | ✅ | 列名 | |
| catalogName | ✅ | ✅ | ✅ | 目录名 |
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| tableName | ✅ | ✅ | ✅ | 表名 | |
| columnName | ✅ | ✅ | ✅ | 列名 | |
| columnDataType | ✅ | ✅ | ✅ | 列属性 |
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| tableName | ✅ | ✅ | ✅ | 表名 | |
| indexName | ✅ | ✅ | ✅ | 索引名 | |
| associatedWith | ? | ? | ? | 不清楚啥玩意,文档没有,但是代码提示有 |
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| baseTableName | ✅ | ✅ | ✅ | 表名 | |
| constraintName | ✅ | ✅ | ✅ | 外键名称 |
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| tableName | ✅ | ✅ | ✅ | 表名 | |
| columnName | ✅ | ✅ | ✅ | 列名 | |
| columnDataType | ✅ | ✅ | ✅ | 列类型 | 必填 |
所有数据库均可使用,但是在MySQL中无法删除自动增长的主键,PostgreSQL和Oracle可以直接删除。
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| tableName | ✅ | ✅ | ✅ | 表名 | |
| constraintName | ✅ | ✅ | ✅ | 约束名 | |
| uniqueColumns | ✅ | ✅ | ✅ | 对应的列 |
未测试
未测试
未测试
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| tableName | ✅ | ✅ | ✅ | 表名 | |
| columnName | ✅ | ✅ | ✅ | 列名 | |
| newDataType | ✅ | ✅ | ✅ | 新的数据类型 |
修改前后的数据类型不同,在不同的数据库可能报错,例如从int -> varchar,mysql和PostgreSQL在有数据的情况下不会报错,而oracle会报错。
但是对于varchar -> varchar,在oracle中即使有数据也不会报错。
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| tableName | ✅ | ✅ | ✅ | 表名 | |
| oldColumnName | ✅ | ✅ | ✅ | 旧列名 | |
| newColumnName | ✅ | ✅ | ✅ | 新列名 | |
| columnDataType | ✅ | ✅ | ✅ | 列的数据类型 |
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| oldTableName | ✅ | ✅ | ✅ | 旧表名 | |
| newTableName | ✅ | ✅ | ✅ | 新表名 |
未测试
未测试
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| tableName | ✅ | ✅ | ✅ | 表名 | |
| columnName | ✅ | ✅ | ✅ | 列名 | |
| remarks | ✅ | ✅ | ✅ | 备注名称 | |
| columnDataType | ✅ | ✅ | ✅ | 列类型 | 必填 |
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| tableName | ✅ | ✅ | ✅ | 表名 | |
| remarks | ✅ | ✅ | ✅ | 备注名 |
合并两个列,合并后原来的两个列会消失。
| 属性名 | MySql | PostgreSQL | Oracle | 功能说明 | 备注 |
|---|---|---|---|---|---|
| tableName | ✅ | ✅ | ✅ | 表名 | |
| column1Name | ✅ | ✅ | ✅ | 第一个列名 | |
| joinString | ✅ | ✅ | ✅ | 连接符 | |
| column2Name | ✅ | ✅ | ✅ | 第二个列名 | |
| finalColumnName | ✅ | ✅ | ✅ | 最终合并后的列名 | |
| finalColumnType | ✅ | ✅ | ✅ | 合并后的类型 | 一般只能是字符串 |
以下标签全可用:
未测试:
由于公司的依赖升级总是等着一段时间直接全量扫描依赖,每次都是一大堆依赖让我们手动升级,工作量都特别很大,而且很容易升漏。所以这里考虑引入 Renovate 主动出击,自动化依赖的升级。
常用链接:
安装很简单,我是使用的 docker-compose 的方式安装的,这里主要讲一下配置。
要使用 ssh, 需要把 gitUrl 设置为 ssh,换成环境变量就是 RENOVATE_GIT_URL。换完后需要配置 ssh,这里直接用网上的教程就可以了,需要额外注意的是需要把用户的 .ssh 目录映射到容器里面,以 root 用户为例:
volumes:
- /root/.ssh:/root/.ssh:ro
yaml
实际在使用时会有一个问题: Renovate 在克隆时会使用 root@your.gitlab.com,这里的用户使用的是 root 用户,很显然这样是克隆不了的,我们需要换成 git 用户才行。
这里需要修改启动命令:
/bin/sh -c 'git config --global url."ssh://git".insteadOf "ssh://root" && ./node_opts_exec.sh node_default src/community.js'
bash
这里前面的 git 命令可以将 ssh://root 替换为 ssh://git,这样我们就可以克隆项目了。
关于后面的命令,是从 docker inspect 查看镜像的启动命令查到的,照着填上去就可以了。
要想过滤指定的项目,例如只使用 myApp 组下的仓库,需要将环境变量 MEND_RNV_AUTODISCOVER_FILTER 的值设置为 myApp/*.
为什么会说这个呢?是因为在官方的文档中没有写这个!这个是在 Github 的文档中找到的:configuration-options.md。
要想开启 API 功能,需要提供如下的配置:
MEND_RNV_ADMIN_API_ENABLED=true MEND_RNV_SERVER_API_SECRET=<your secret>env
之后在认证时,带上请求头即可:
Authorization: <your secret>plaintext
这里调用 POST /api/job/add 接口就可以手动触发同步任务,调用时需要提供具体的仓库。
Note 参考: https://blog.csdn.net/qq_41786285/article/details/109304126
mkdir -p /data/mysql/ mkdir /data/mysql/mysql20001 mkdir /data/mysql/mysql20002 mkdir /data/mysql/mysql20003shell
mysql20001 是主数据库,mysql20002 和 mysql20003 是从数据库。
修改主节点配置文件(/data/mysql/my1.cnf):
[client] default-character-set=utf8mb4 [mysql] default-character-set=utf8mb4 [mysqld] init_connect='SET collation_connection = utf8mb4_unicode_ci' init_connect='SET NAMES utf8mb4' character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci skip-character-set-client-handshake lower_case_table_names = 1 # 可选 binlog_format = mixed # 下面的配置是必须的 server-id = 1 log_bin = mysql-bin binlog_ignore_db=information_schema,performance_schema,mysql,syscnf
修改从节点配置(/data/mysql/my2.cnf):
[client] default-character-set=utf8mb4 [mysql] default-character-set=utf8mb4 [mysqld] init_connect='SET collation_connection = utf8mb4_unicode_ci' init_connect='SET NAMES utf8mb4' character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci skip-character-set-client-handshake lower_case_table_names = 1 # 下面的配置是必须的 server-id = 2 relay-log = relay-log-bin relay-log-index = slave-relay-bin.indexcnf
Important 记住配置另外一个从库的时候需要修改 server-id
最后修改目录权限:
useradd mysql groupadd mysql chown -R mysql /data/mysql chgrp -R mysql /data/mysqlshell
Important 可以使用我自己上传的镜像:
docker pull ccr.ccs.tencentyun.com/icebing-repo/mysql:5.7.42
ID=1 docker run --name mysql2000$ID -u 27:27 -p 2000$ID:3306 -e MYSQL_ROOT_PASSWORD=123456 -v /data/mysql/mysql2000${ID}/:/var/lib/mysql/ -v /data/mysql/my${ID}.cnf:/etc/mysql/my.cnf -d mysql:5.7.42shell
注意参数中的 -u 27:27,这里是使用了 uid:gid 的格式,使用其它格式会导致权限不足:
2024-12-03 14:36:24+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.42-1.el7 started. 2024-12-03 14:36:24+00:00 [Note] [Entrypoint]: Initializing database files mysqld: Can't create directory '/var/lib/mysql/' (Errcode: 17 - File exists) 2024-12-03T14:36:24.891299Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details). 2024-12-03T14:36:24.893084Z 0 [ERROR] Abortinglog
查看 uid 和 gid:
[root@localhost mysql]# id mysql uid=27(mysql) gid=27(mysql) groups=27(mysql)shell
同样的方式启动另外两台从节点。
登录主节点执行:
grant replication slave on *.* to 'myslave'@'172.17.0.%' identified by '123456';
flush privileges;
sql
注意这里的 ip 地址,容器默认情况下是桥接模式,容器的 ip 不是宿主机的 ip,具体 ip 网段可以用下面的命令看:
docker inspect mysql20002 | grep Networks -A 16shell
找到 Gateway 字段,单独使用这个 ip 或者直接使用对应的网段。
查看主节点状态:
# 主库执行
show master status;
sql

这里需要记住上面的 File 和 Position,之后在从节点中执行下面的 sql 来加入集群:
# 从库执行
change master to master_host ='10.77.0.38',
master_port = 20001,
master_user ='myslave',
master_password ='123456',
master_log_file ='mysql-bin.000004',
master_log_pos =154;
show slave status;
start slave;
sql
这里我们先只让一台加进去,后面一台我们待会尝试中途加入。

这里等到打框框的这两个值都变成 YES 就完成了。但是我这里把配置搞错了...导致一直加不进去,所以这里额外研究了一下节点怎么退出集群。
使用下面的命令之一退出集群
# 停止从主节点的同步,直到使用 START SLAVE
STOP SLAVE;
# 删除同步的内容
RESET SLAVE;
sql
部分情况下由于初始的错误配置可能导致停不掉,这时候就直接强制停止从节点重新部署一个就行了,对主节点没有影响的。
在主库执行下面的 sql:
create database slave_test;
use slave_test;
CREATE table slave_data(
id varchar(20) primary key ,
tm timestamp
);
INSERT INTO slave_data value ('1', CURRENT_TIMESTAMP());
sql
然后可以查看从节点是否创建相关的数据。
Note 参考: https://blog.csdn.net/anddyhua/article/details/116240478
我们这里留了一台节点还没有加入集群,这里我们专门来测试从节点的水平扩容。这里我们使用 xtrabackup 来进行备份。
首先下载 xtrabackup,这里 Mysql 5.7 只能使用 2.4 版本的。下载完成后解压,进入 bin 目录,执行下列命令进行备份:
./xtrabackup --backup --target-dir=/data/mysql/backup/bakup_`date +"%F_%H_%M_%S"` --user=root --host=10.77.0.38 --port=20001 --password=123456 --datadir=/data/mysql/mysql20001shell
备份完成后查看目录:
[root@localhost bakup_2024-12-05_00_28_08]# ll total 77876 -rw-r----- 1 root root 487 Dec 5 00:28 backup-my.cnf -rw-r----- 1 root root 661 Dec 5 00:28 ib_buffer_pool -rw-r----- 1 root root 79691776 Dec 5 00:28 ibdata1 drwxr-x--- 2 root root 4096 Dec 5 00:28 mysql drwxr-x--- 2 root root 8192 Dec 5 00:28 performance_schema drwxr-x--- 2 root root 64 Dec 5 00:28 slave_test drwxr-x--- 2 root root 8192 Dec 5 00:28 sys -rw-r----- 1 root root 21 Dec 5 00:28 xtrabackup_binlog_info -rw-r----- 1 root root 138 Dec 5 00:28 xtrabackup_checkpoints -rw-r----- 1 root root 587 Dec 5 00:28 xtrabackup_info -rw-r----- 1 root root 2560 Dec 5 00:28 xtrabackup_logfileshell
可以发现几乎是和 mysql 数据目录一样的结构。之后我们将备份的内容复制到第二个从节点的数据目录中(/data/mysql/mysql20003)。这里需要注意权限问题,复制过来后需要把权限给 mysql 用户。
启动从库(和之前相同的命令):
ID=3 docker run --name mysql2000$ID -u 27:27 -p 2000$ID:3306 -e MYSQL_ROOT_PASSWORD=123456 -v /data/mysql/mysql2000${ID}/:/var/lib/mysql/ -v /data/mysql/my${ID}.cnf:/etc/mysql/my.cnf -d mysql:5.7.42shell
由于我们在备份的过程中,主节点仍然可以写入数据,所以导致我们备份的数据不一定是最新的,所以在加入集群的时候不能直接使用 show master status 中的 binlog 位置。这里我们需要根据备份目录中 xtrabackup_info 文件来确定具体的位置:
[root@localhost mysql20003]# cat xtrabackup_info uuid = c333e3de-b25c-11ef-81e2-0242ac110002 name = tool_name = xtrabackup tool_command = --backup --target-dir=/data/mysql/backup/bakup_2024-12-05_00_28_08 --user=root --host=10.77.0.38 --port=20001 --password=... --datadir=/data/mysql/mysql20001 tool_version = 2.4.29 ibbackup_version = 2.4.29 server_version = 5.7.42-log start_time = 2024-12-05 00:28:08 end_time = 2024-12-05 00:28:15 lock_time = 4 binlog_pos = filename 'mysql-bin.000006', position '154' innodb_from_lsn = 0 innodb_to_lsn = 12232730 partial = N incremental = N format = file compact = N compressed = N encrypted = Nshell
这里可以看到 binlog_pos 的值,表示当前 binlog 读到了哪里,我们根据这个值来加入主节点:
# 从库执行
change master to master_host ='10.77.0.38',
master_port = 20001,
master_user ='myslave',
master_password ='123456',
master_log_file ='mysql-bin.000006',
master_log_pos =154;
start slave;
show slave status;
sql
最终等待 SLAVE_IO 和 SALVE_SQL 都变成 YES, 从库就加入成功了。
主主架构是指系统中有两个 mysql 主数据库,它们相互读写,互相同步数据。相比于单纯的一主多从,主主架构最大的优势就是提供了主节点的高可用
对于简单的集群,例如上面的一主两从,我们可以使用下面的方式在应用的层面实现读写分离:
jdbc:mysql:replication://[source host][:port],[replica host 1][:port][,[replica host 2][:port]]...[/[database]] [?propertyName1=propertyValue1[&propertyName2=propertyValue2]...]plaintext
仅 Java 可用:官方文档。
但是有些语言的驱动可能没有提供读写分离的功能,这个时候可以选择自己在应用层实现。可以参考上面 mysql 文档中的实现,当关闭了只读模式和自动提交,那么后续的 sql 都发送到主节点,否则发送到从节点。
如果不想写代码,可以通过加一个中间件来实现。目前我可以找到的,距离上一次提交最近的中间件是:mycat。
其它的这些全都没有维护了:
个人还是推荐直接在应用层解决吧,毕竟这里还要单独启动一个服务,搞不好中间件就成性能瓶颈了?
架构图:
图是自动生成的,可能会有点丑
主主架构相比与一主多从架构,最大的区别就是提供了主节点的高可用,搭配 keepalived 等其它工具,可以快速切换主节点,大大降低了系统的恢复时间。而一主多从架构,只能手动切换主节点,容灾能力低。
但这种架构有下面的缺点:
这里我们还是用之前一主两从的配置,先重置掉所有数据:
docker rm mysql20001 -f
docker rm mysql20002 -f
docker rm mysql20003 -f
rm -rf /data/mysql/mysql20001/*
rm -rf /data/mysql/mysql20002/*
rm -rf /data/mysql/mysql20003/*
bash
修改 /data/mysql/my1.cnf,添加下面的内容:
auto_increment_increment=1 auto_increment_offset=1cnf
修改 /data/mysql/my2.cnf,添加下面的内容:
auto_increment_increment=2 auto_increment_offset=2 log_bin=mysql-bin binlog_format=mixed binlog_ignore_db=information_schema,performance_schema,mysql,sys log_slave_updates=ONcnf
这里出现了两个个新的参数:auto_increment_increment、auto_increment_offset 作用分别如下:
auto_increment_increment: 自增主键每次增长多少auto_increment_offset: 自增主键从哪里开始增长A -> B -> C 的数据库架构。相同的命令启动容器:
ID=1
docker run --name mysql2000$ID -u 27:27 -p 2000$ID:3306 -e MYSQL_ROOT_PASSWORD=123456 -v /data/mysql/mysql2000${ID}/:/var/lib/mysql/ -v /data/mysql/my${ID}.cnf:/etc/mysql/my.cnf -d mysql:5.7.42
ID=2
docker run --name mysql2000$ID -u 27:27 -p 2000$ID:3306 -e MYSQL_ROOT_PASSWORD=123456 -v /data/mysql/mysql2000${ID}/:/var/lib/mysql/ -v /data/mysql/my${ID}.cnf:/etc/mysql/my.cnf -d mysql:5.7.42
ID=3
docker run --name mysql2000$ID -u 27:27 -p 2000$ID:3306 -e MYSQL_ROOT_PASSWORD=123456 -v /data/mysql/mysql2000${ID}/:/var/lib/mysql/ -v /data/mysql/my${ID}.cnf:/etc/mysql/my.cnf -d mysql:5.7.42
bash
在 mysql20001 和 mysql20002 上创建账号:
grant replication slave on *.* to 'myslave'@'172.17.0.%' identified by '123456';
sql
网段还是要用容器的网段,不是宿主机的!最后相互连接,注意参数需要根据实际情况修改:
change master to master_host ='10.77.0.38',
master_port = 20001,
master_user ='myslave',
master_password ='123456',
master_log_file ='mysql-bin.000004',
master_log_pos =154;
start slave ;
show slave status ;
sql
测试同步,任意主数据库上执行:
create database slave_test;
use slave_test;
CREATE table slave_data(
id varchar(20) primary key ,
tm timestamp
);
INSERT INTO slave_data value ('1', CURRENT_TIMESTAMP());
sql
在另外一个数据库执行:
use slave_test;
INSERT INTO slave_data value ('2', CURRENT_TIMESTAMP());
sql
可以发现两个主库都可以写,并且可以相互同步,而且没有出现任何报错。
最后使用相同的方法,让从库连接上备用主库,进行读写分离(注意加入备用主库时的 pos 需要用最开始的位置或者手动使用 xtrabackup 进行备份后再加入也可以)
最后我们只需要用 keepalived 给两台主库配置上高可用,我们的主主架构就完成了!
至此,我们已经学会 mysql 集群中最基础的部署架构了,在 mysql 中,主从只是一个相对的概念,一个节点既可以是"主"又可以"从",例如我们在主主中提过一嘴的级联架构:
A -> B -> C | -> D | -> Eplaintext
其中 A 为一个主库,B、C、D 和 E 都是从库,但是 C、D、E 的"主库" 是 B,这种架构可以分担 A 的 IO 压力,让写操作更快。
另附配置文件(my1.cnf):
[client] default-character-set=utf8mb4 [mysql] default-character-set=utf8mb4 [mysqld] init_connect='SET collation_connection = utf8mb4_unicode_ci' init_connect='SET NAMES utf8mb4' character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci skip-character-set-client-handshake lower_case_table_names = 1 server_id=1 log_bin=mysql-bin binlog_format=mixed log-slave-updates=true binlog_ignore_db=information_schema,performance_schema,mysql,sys auto_increment_increment=1 auto_increment_offset=1cnf
my2.cnf:
[client] default-character-set=utf8mb4 [mysql] default-character-set=utf8mb4 [mysqld] init_connect='SET collation_connection = utf8mb4_unicode_ci' init_connect='SET NAMES utf8mb4' character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci skip-character-set-client-handshake lower_case_table_names = 1 relay-log = relay-log-bin relay-log-index = slave-relay-bin.index server-id = 2 auto_increment_increment=2 auto_increment_offset=2 log_bin=mysql-bin binlog_format=mixed binlog_ignore_db=information_schema,performance_schema,mysql,sys log_slave_updates=ONcnf
my3.cnf 没有什么好看的,就是寻常的从库配置。
我们前面提到开启 log_slave_updates 后, mysql 会把从副本服务器同步的数据也写到自己的 binlog 里面,那么会不会出现这种情况:假设 A 和 B 两个主库都开启这个配置 A 写入一条数据, B 同步后写入 binlog, 然后 A 发现 B binlog 写了新东西,然后去同步,同步完后 B 发现 A 又写了新东西...
就和之前说的一样, mysql 中的主从都是相对的, 所以 mysql 也没有限制这种循环依赖(实际不会这么用,有点像吃饱了没事干...)。
这里如果感兴趣可以自己部署试一下,实际的效果是并不会出现上面的无限同步效果,但是开启后两台主库的 binlog 状态(show master status)会保持相同,只有一台开的时候是不会相同的。
那么 mysql 怎么避免的呢?很简单,判断 binlog 的 server-id,如果是自己的就不同步。
例如在 server-id 为 1 的主库执行下面的 sql 语句:
INSERT INTO slave_data value ('6', CURRENT_TIMESTAMP());
sql
使用 mysqlbinlog 查看 binlog(无关的内容被省略):
#241209 21:17:24 server id 1 end_log_pos 606 CRC32 0xec624c47 Anonymous_GTID last_committed=1 sequence_number=2 rbr_only=no SET @@SESSION.GTID_NEXT= 'ANONYMOUS'/*!*/; # at 606 #241209 21:17:24 server id 1 end_log_pos 705 CRC32 0xb3303223 Query thread_id=5 exec_time=0 error_code=0 SET TIMESTAMP=1733750244/*!*/; BEGIN /*!*/; # at 705 #241209 21:17:24 server id 1 end_log_pos 897 CRC32 0x54a64039 Query thread_id=5 exec_time=0 error_code=0 SET TIMESTAMP=1733750244/*!*/; /* ApplicationName=IntelliJ IDEA 2024.3 */ INSERT INTO slave_data value ('5', CURRENT_TIMESTAMP()) /*!*/; # at 897 #241209 21:17:24 server id 1 end_log_pos 928 CRC32 0x941d43ff Xid = 77 COMMIT/*!*/; SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/; DELIMITER ; # End of log file /*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/; /*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;log
可以发现 binlog 是带上了 server-id 的,这样就可以完美避免上面的循环同步的问题了。
GTID (Golobal Transaction ID) 可以简单理解为使用雪花算法计算的一个全局唯一 id。
再简单理解一点就是:
格式为:数据库UUID:XID。
它的主要作用就是方便我们创建从库,我们在使用 change master 命令时只需要将 master_auto_position 设置为 1 就可以了,不再需要我们去看主库的状态了。
相当于把 binlog file 和 position 这种不连续的值转换成连续的 id, 只需要告诉 mysql id 从哪里开始就行了, mysql 可以自己推断出下一个 id 然后去同步。
这个 id 直接保存在表中,使用select * from mysql.gtid_executed就可以看到,所以说我们备份的时候相当于直接记录了起始位置到表中。
这里我们简单搭个一主一从。主库配置(/data/mysql/my1.cnf):
[client] default-character-set=utf8mb4 [mysql] default-character-set=utf8mb4 [mysqld] init_connect='SET collation_connection = utf8mb4_unicode_ci' init_connect='SET NAMES utf8mb4' character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci skip-character-set-client-handshake lower_case_table_names = 1 server_id=1 log_bin=mysql-bin binlog_format=mixed binlog_ignore_db=information_schema,performance_schema,mysql,sys # 开启 gtid gtid-mode=on enforce_gtid_consistency = oncnf
my2.cnf(就是寻常从库配置):
[client] default-character-set=utf8mb4 [mysql] default-character-set=utf8mb4 [mysqld] init_connect='SET collation_connection = utf8mb4_unicode_ci' init_connect='SET NAMES utf8mb4' character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci skip-character-set-client-handshake lower_case_table_names = 1 server_id=2 # 开启 gtid gtid-mode=on enforce_gtid_consistency = oncnf
启动容器,主库创建账号,这里命令大家翻前面的吧。。。都是一样的,实在懒得写了。
最后,从库加入主库:
change master to master_host ='10.77.0.38',
master_port = 20001,
master_user ='myslave',
master_password ='123456',
master_auto_position = 1;
sql
使用 select @@server_uuid 既可查看当前数据库的 uuid,在主库使用 show global variables like 'gtid_executed' 既可查看当前 gtid 状态:

可以发现后面是 1-8,说明有 8 条数据,我们再来看下从库的执行状态,使用 select * from mysql.gtid_executed:

可以发现正好 8 条数据。这里还有一个 interval_end 数据,这里是 mysql 在后续数据量大了之后,会帮我们对数据压缩,具体的实现就是将 start 和 end 设置成一个范围。
最后推荐看一下这个:MySQL 中的集群部署方案,我们前面相当于只介绍了 MySQL Replication 和 MMM, 实际还有很多部署方案。
因为我也没有在生产上实际用过 mysql 集群。。。所以我无法给出一个具体的评价, 但无论如何, 绝大部分架构都是根据MySQL Replication 和 MMM 这两种架构衍生的,所以还是很有必要掌握的。
关于这个操作,网上的操作要么是直接用自带的 删除重复值,或者 高级筛选 直接创建一个新的表格。但是这俩个操作有一个最大的问题:重复的值都被删除了。
假如我有一个列保存文件名,另外一个列保存文件中的一些关键内容,一个文件中可能有多个关键内容,也就是一个文件有多行。我删除重复值只是想看一下有哪些文件,看完又需要恢复,那么这个操作就非常不友好了。
这个时候就要请出我们的 COUNTIF 了。
先直接看效果:

Note 这里绿色背景是其它的效果,这里直接忽略就行.
可以发现,所有项目的第一个都会被标记出来,之后我们直接使用普通的 筛选,筛选出值为 FIRST 的行就可以了。
使用到的函数:=IF(COUNTIF(A:A,A2)>1,IF(COUNTIF(A$2:A2,A2)=1,"FIRST",""),"FIRST")
COUNTIF(range, criteria): 计算区域内满足条件的单元格数量。
range: 选中一个范围criteria: 判断条件,可以是一个字符串,返回单元格内等于该字符串的数量。也可以是一个判断,例如 >5,则判断 range 内大于 5 的单元格数量。IF(condition, val1, val2): 如果满足条件,则返回 val1 否则返回 val2。
所以我们这里的思路就是:对于一个单元格,统计它上面和它自己所有单元格的范围内,值和自己相等的单元格数量。当值为 1 时,就说明该单元格的值是所有重复的值中的第一个,这时我们给他打上一个标记,就可以利用 Excel 的筛选功能就可以做到不删除数据,又能筛选出重复的第一个了。
由于我们的判断条件是当前单元格和它上面的区域,也就是说如果我们对单元格进行排序或者其它操作,那么我们的标记就会发生变动。如果你只想给重复的第一个值进行一些其它的修改,那么可能一次排序就会毁掉你的所有修改。
这个问题的解决方法也很简单,我们只需要给所有行新建一列,名字叫做序号,值从任意值开始递增,在顺序被我们打乱后,只需要按照序号进行排序就可以恢复到原来的状态。
挖个坑在这里,后面碰到了新的需要再来写。
Note
由于我原本是一个搞 Java 的,未来想要转型搞 GoLang。结果在使用的时候发现一个对我这种搞 Java 的非常难以理解的情况:
type Object struct {
data int
}
func updateObj(obj Object) {
obj.data++
}
func main() {
var obj Object
// expect 0
fmt.Println(obj.data)
updateObj(obj)
// expect 1
fmt.Println(obj.data)
}
go
"正常情况"下,这段代码应该依次打印 0 和 1. 但是实际却是:
\
可以发现输出了两个 0.
可以发现 GoLang 里面参数传递没有和 Java 一样那么无脑。。。很显然,这里直接把对象复制了一遍然后传给了函数,如果大家对 C/C++ 稍微了解过的话,就可以发现这个逻辑是一样的:调用函数是值传递。
这里我直接说结论了,对于一个变量来说,它有两个关键属性:
例如下面的代码:
var obj Object
var objRef = &obj
go
对 obj 来说,它的值为结构体的数据,这里为了方便我们称它为 a, obj 的地址这里假设为 b。那么对于 objRef 来说,它的值就是 b,地址就是内存中的另外一块地址。

如上图所示,蓝色方框里面代表变量当前的值。
对于 obj 的值具体是什么样的,个人猜测这里应该是一个 8 字节的指针指向结构体内存地址开始的位置(不一定都是全部表示开始位置,可能还会有其它信息),然后底层根据结构体大小信息读取相应范围内的数据,就能够表示一个结构体了。
但是我们在复制这个 值 的时候,不能仅只复制第一个 8 字节,也就是那个指针,也必须要把后面跟着的那一大块全部全部复制。
Important 这里只是我为了方便记忆根据个人经验写出来的!没有依据!没有依据!没有依据!
再来看一个例子(Object结构体省略了):
func updateObj(arr []Object) {
arr[0] = Object{data: 1}
}
func main() {
arr := make([]Object, 1)
fmt.Println(arr[0].data)
updateObj(arr)
fmt.Println(arr[0].data)
}
go
输出:
0 1log
可以发现切片使用函数传递后还能够影响原来的值。其实根据切片的结构就可以发现(internal/unsafeheader/unsafeheader.go):
type Slice struct {
Data unsafe.Pointer
Len int
Cap int
}
go
可以发现这个结构体里面还有一个指针指向了真正的数据。这一点让我想起当初刚学 C 语言的时候用 malloc 声明一串连续的内存地址然后用来当数组的时候。。。
所以我们将切片传给函数时,其实也复制了值,但是复制的没这么多,就只有结构体这三个字段,在 64 位系统上也就 24 字节。
所以切片你想用引用就用,但是一般的习惯是不用,因为也浪费不了多少空间,而且后面用的时候解引用也麻烦。
除了切片外 string、map 和 chan 也可以这样使用。
再来看个有意思的例子:
func updateObj(arr []Object) {
arr[0].data++
}
func main() {
arr := make([]Object, 1)
val := Object{data: 0}
arr[0] = val
updateObj(arr)
fmt.Println(val.data)
}
go
输出:
0log
理论上这里应该输出 1,但是却输出了 0,这不是和我们之前得出的结论相违背吗?
不知道你还记不记得我之前说在 C 里面声明一串连续的内存地址,在这里,切片元素的类型是 Object,所以这一串内存中存的就是 Object 具体的值,而不是 val 的内存地址。
如果你将 arr[0].data 打印,可以发现它的值确实自增了。
所以说我们将值添加到切片中时,也会发生值的复制。
那么既然入参会复制值,那么返回值会怎么样呢?
type Object struct {
data int
data2 int
data3 int
}
func createObj() Object {
var obj = Object{data: 2}
fmt.Printf("函数中的内存地址为: %p\n", &obj)
return obj
}
func main() {
r := createObj()
fmt.Printf("函数返回后的内存地址为: %p\n", &r)
fmt.Println(&r)
}
go
输出:
函数中的内存地址为: 0xc0000ae018 函数返回后的内存地址为: 0xc0000ae000go
可以发现两个内存地址相差 18, 转换为十进制,就是 24, 而我们的结构体也正好是 24 字节,说明返回时也发生了复制。
map、string 和 chan 可以不用引用直接传递。当然也可以用引用,区别不大。map等第二点提到的数据结构前,应该将值更新至最新状态后再添加,因为每次添加到这些结构中都会发生一次复制。