IceOfSummerの博客还是自己搭的博客靠谱后面一辈子的博客都在这了!

发现的坑

部分数据库在创建表时事务会失效

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.xsd
text

只要把后面的版本和依赖版本配置成一样的就好了。

changeSet

tagDatabase

用于给Changelog标记一个版本号,便于回滚时使用:

<changeSet author="liquibase-docs" id="tagDatabase-example"> <tagDatabase tag="version_1.3"/> </changeSet>
xml

回滚时有一个根据tag来回滚的方法,没有根据changeSet id回滚的方法

createTable

column

属性名MySqlPostgreSQLOracle功能说明备注
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可以直接使用如下值,会被自动转换成对应数据库的数据结构:

  • BIGINT
  • BLOB
  • BOOLEAN
  • CLOB
  • CURRENCY
  • DATE
  • DATETIME
  • TIME
  • UUID

也可以直接使用java类,如:

  • java.sql.Types.TIMESTAMP
  • java.sql.Types.VARCHAR(255)
constraints
属性名MySqlPostgreSQLOracle功能说明备注
primaryKey主键
nullable是否可空
checkConstraintcheck约束
primaryKeyTablespace???指定主键表空间未测试
foreignKeyName???外键名称推荐使用addForeignKeyConstraint标签
deleteCascade???当外键删除时是否跟着删除对应的行推荐使用addForeignKeyConstraint标签
unique是否唯一
uniqueConstraintName唯一键名称
references???定义外键不推荐使用,该属性会将值原封不动的贴到SQL语句上
initiallyDeferred??立即检查约束MySql不支持,Oracle文档上支持,但没有测试出结果
deferrable??延迟检查约束MySql不支持,Oracle文档上支持,但没有测试出结果

createIndex

属性名MySqlPostgreSQLOracle功能说明备注
catalogName目录名CREATE INDEX xx ON catalogName.column ...
clustered??是否为聚簇索引PostgreSQL的聚簇索引不允许在事务下创建,oracle的聚簇索引由于liquibase拼接sql有误导致创建失败,mysql忽视了该语句(可能是没有这个功能)
indexName索引名称
schemaName???schema名称未测试,可以使用catalogName代替
tableName要给哪个表加索引
tablespace???表空间未测试
unique是否为唯一键

createProcedure

存储过程只能通过手写sql来创建:createProcedure (liquibase.com)

createView

视图只能通过手写sql来创建:createView (liquibase.com)

createSequence

createSequence (liquibase.com)

创建一个序列。MySQL不支持,PostgreSQL和Oralce支持。

addColumn

属性名MySqlPostgreSQLOracle功能说明备注
tableName要创建的表名
catalogName目录名
schemaName???schema名称未测试,可以使用catalogName代替

addForeignKeyConstraint

属性名MySqlPostgreSQLOracle功能说明备注
baseTableName给哪个表加外键
baseColumnNames给哪个列加外键
constraintName外键名称
referencedTableName外键指向哪个表
referencedColumnNames外键指向表的哪个字段
deleteCascade当外键指向的表中,某行被删除后,删除对应其它所有指向它的行
onUpdate当外键被更新后的操作
onDelete当外键被删除后的操作
validate???添加约束时,是否检查所有的行是否满足约束文档有,但是代码用了会报错

addPrimaryKey

属性名MySqlPostgreSQLOracle功能说明备注
catalogName目录名
columnNames要给哪些列加主键复合主键使用逗号分割列
constraintName主键名称Mysql主键固定为PRIMARY
schemaName???schema未测试,可以使用catalogName代替
tableName要给哪个表加主键
tablespace???表空间未测试
validate???添加约束时,是否检查所有的行是否满足约束文档有,但是代码用了会报错

addDefaultValue

属性名MySqlPostgreSQLOracle功能说明备注
catalogName目录名
columnName列名
defaultValue默认值此处省略了其它defaultValue*属性
tableName要给哪个表加默认值

addUniqueConstraint

属性名MySqlPostgreSQLOracle功能说明备注
tableName表名称
columnNames要给哪个列加唯一键多个列用逗号分割
constraintName约束名称
catalogName目录名

addLookupTable

功能和添加外键相同,但是这个标签会自动把数据同步过去:

<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都支持。

addAutoIncrement

Oracle不支持,MySQL和PostgreSQL支持

属性名MySqlPostgreSQLOracle功能说明备注
tableName表名
columnName列名
incrementBy每次自增多少
startWith从哪里开始自增
columnDataType列的类型必填
catalogName目录名

addNotNullConstraint

属性名MySqlPostgreSQLOracle功能说明备注
tableName表名
columnName列名
constraintName约束名称这个属性会被忽略
columnDataType列类型必填
defaultNullValue默认空值这个属性会被忽略

insert & update & delete

属性名MySqlPostgreSQLOracle功能说明备注
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需要借助函数)

dropTable

属性名MySqlPostgreSQLOracle功能说明备注
tableName表名
catalogName目录名
cascadeConstraints删除时是否删除指向它的外键所对应的行

dropAllForeignKeyConstraints

属性名MySqlPostgreSQLOracle功能说明备注
baseTableName要删除哪个表的外键
baseTableCatalogName目录名

如果要删除单个外键可以用dropForeignKeyConstraint

dropColumn

属性名MySqlPostgreSQLOracle功能说明备注
tableNamev表名
columnName列名
catalogName目录名

dropDefaultValue

属性名MySqlPostgreSQLOracle功能说明备注
tableName表名
columnName列名
columnDataType列属性

dropIndex

属性名MySqlPostgreSQLOracle功能说明备注
tableName表名
indexName索引名
associatedWith???不清楚啥玩意,文档没有,但是代码提示有

dropForeignKeyConstraint

属性名MySqlPostgreSQLOracle功能说明备注
baseTableName表名
constraintName外键名称

dropNotNullConstraint

属性名MySqlPostgreSQLOracle功能说明备注
tableName表名
columnName列名
columnDataType列类型必填

dropPrimaryKey

所有数据库均可使用,但是在MySQL中无法删除自动增长的主键,PostgreSQL和Oracle可以直接删除。

dropUniqueConstraint

属性名MySqlPostgreSQLOracle功能说明备注
tableName表名
constraintName约束名
uniqueColumns对应的列

dropProcedure

未测试

dropSequence

未测试

dropView

未测试

modifyDataType

属性名MySqlPostgreSQLOracle功能说明备注
tableName表名
columnName列名
newDataType新的数据类型

修改前后的数据类型不同,在不同的数据库可能报错,例如从int -> varchar,mysql和PostgreSQL在有数据的情况下不会报错,而oracle会报错。

但是对于varchar -> varchar,在oracle中即使有数据也不会报错。

renameColumn

属性名MySqlPostgreSQLOracle功能说明备注
tableName表名
oldColumnName旧列名
newColumnName新列名
columnDataType列的数据类型

renameTable

属性名MySqlPostgreSQLOracle功能说明备注
oldTableName旧表名
newTableName新表名

renameSequence

未测试

renameView

未测试

setColumnRemarks

属性名MySqlPostgreSQLOracle功能说明备注
tableName表名
columnName列名
remarks备注名称
columnDataType列类型必填

setTableRemarks

属性名MySqlPostgreSQLOracle功能说明备注
tableName表名
remarks备注名

mergeColumns

合并两个列,合并后原来的两个列会消失

属性名MySqlPostgreSQLOracle功能说明备注
tableName表名
column1Name第一个列名
joinString连接符
column2Name第二个列名
finalColumnName最终合并后的列名
finalColumnType合并后的类型一般只能是字符串

preConditions

以下标签全可用:

  • tableIsEmpty:是否为空表
  • tableExists:表是否存在
  • columnExists:列是否存在
  • primaryKeyExists:主键是否存在
  • rowCount:计算有几行
  • foreignKeyConstraintExists:外键是否存在
  • indexExists:索引是否存在
  • uniqueConstraintExists:唯一约束是否存在

未测试:

  • viewExists:视图是否存在

由于公司的依赖升级总是等着一段时间直接全量扫描依赖,每次都是一大堆依赖让我们手动升级,工作量都特别很大,而且很容易升漏。所以这里考虑引入 Renovate 主动出击,自动化依赖的升级。

常用链接:

配置

安装很简单,我是使用的 docker-compose 的方式安装的,这里主要讲一下配置。

使用 SSH 克隆

要使用 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 触发任务

API 文档

要想开启 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/mysql20003
shell

mysql20001 是主数据库,mysql20002mysql20003 是从数据库。

修改主节点配置文件(/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,sys
cnf

修改从节点配置(/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.index
cnf
Important

记住配置另外一个从库的时候需要修改 server-id

最后修改目录权限:

useradd mysql groupadd mysql chown -R mysql /data/mysql chgrp -R mysql /data/mysql
shell

启动容器

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.42
shell

注意参数中的 -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] Aborting
log

查看 uidgid:

[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 16
shell

找到 Gateway 字段,单独使用这个 ip 或者直接使用对应的网段。


查看主节点状态:

# 主库执行 show master status;
sql

主节点状态

这里需要记住上面的 FilePosition,之后在从节点中执行下面的 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/mysql20001
shell

备份完成后查看目录:

[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_logfile
shell

可以发现几乎是和 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.42
shell

由于我们在备份的过程中,主节点仍然可以写入数据,所以导致我们备份的数据不一定是最新的,所以在加入集群的时候不能直接使用 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 = N
shell

这里可以看到 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

其它的这些全都没有维护了:

个人还是推荐直接在应用层解决吧,毕竟这里还要单独启动一个服务,搞不好中间件就成性能瓶颈了?

多主架构

架构图:

应用[cloud][database][folder]备用主库[database]主库[database]mysql20001mysql20002mysql20003mysql...客户端相互读写同步数据主主架构

图是自动生成的,可能会有点丑

主主架构相比与一主多从架构,最大的区别就是提供了主节点的高可用,搭配 keepalived 等其它工具,可以快速切换主节点,大大降低了系统的恢复时间。而一主多从架构,只能手动切换主节点,容灾能力低。

但这种架构有下面的缺点:

  1. 假设上图中的 mysql20001 挂了后,keepalived 自动将写流量切换到 mysql20002 上。当 mysql20001 恢复后重新加入集群,如果不做任何调整,此后将没有任何流量会发送到 mysql20001,除非 mysql20002 挂了或者手动切换主备。
  2. 主节点挂了后,如果备用的主节点数据还没同步完或者还没开始同步,则会导致数据不一致,这里可以通过修改复制方式来缓解或解决。

准备配置文件

这里我们还是用之前一主两从的配置,先重置掉所有数据:

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=1
cnf

修改 /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=ON
cnf

这里出现了两个个新的参数:auto_increment_incrementauto_increment_offset 作用分别如下:

  • auto_increment_increment: 自增主键每次增长多少
  • auto_increment_offset: 自增主键从哪里开始增长
  • log_slave_updates: 是否将副本(从)服务器从副本源服务器收到的更新写到 binlog 中,默认关闭(mysql8 默认开启,坑了我一波,一开始看错文档了)。这个配置常用于级联架构,例如你可以给一个从库开启这个,然后把这个从库"当成主库",给它加几个从库,达到类似于 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 | -> E
plaintext

其中 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=1
cnf

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=ON
cnf

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

GTID (Golobal Transaction ID) 可以简单理解为使用雪花算法计算的一个全局唯一 id

再简单理解一点就是:

  • 全局唯一 id
  • 递增

格式为:数据库UUID:XID

它的主要作用就是方便我们创建从库,我们在使用 change master 命令时只需要将 master_auto_position 设置为 1 就可以了,不再需要我们去看主库的状态了。

相当于把 binlog file 和 position 这种不连续的值转换成连续的 id, 只需要告诉 mysql id 从哪里开始就行了, mysql 可以自己推断出下一个 id 然后去同步。

这个 id 直接保存在表中,使用select * from mysql.gtid_executed就可以看到,所以说我们备份的时候相当于直接记录了起始位置到表中。

配置 GTID

这里我们简单搭个一主一从。主库配置(/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 = on
cnf

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 = on
cnf

启动容器,主库创建账号,这里命令大家翻前面的吧。。。都是一样的,实在懒得写了。

最后,从库加入主库:

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 状态:

主库gtid状态

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

主库gtid状态

可以发现正好 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

  • condition: 判断条件
  • 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

"正常情况"下,这段代码应该依次打印 01. 但是实际却是:

执行结果\

可以发现输出了两个 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 1
log

可以发现切片使用函数传递后还能够影响原来的值。其实根据切片的结构就可以发现(internal/unsafeheader/unsafeheader.go):

type Slice struct { Data unsafe.Pointer Len int Cap int }
go

可以发现这个结构体里面还有一个指针指向了真正的数据。这一点让我想起当初刚学 C 语言的时候用 malloc 声明一串连续的内存地址然后用来当数组的时候。。。

所以我们将切片传给函数时,其实也复制了值,但是复制的没这么多,就只有结构体这三个字段,在 64 位系统上也就 24 字节。

所以切片你想用引用就用,但是一般的习惯是不用,因为也浪费不了多少空间,而且后面用的时候解引用也麻烦

除了切片外 stringmapchan 也可以这样使用。

真的不用引用吗

再来看个有意思的例子:

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

输出:

0
log

理论上这里应该输出 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 函数返回后的内存地址为: 0xc0000ae000
go

可以发现两个内存地址相差 18, 转换为十进制,就是 24, 而我们的结构体也正好是 24 字节,说明返回时也发生了复制

总结

  1. 能用引用就用引用,不管是返回值还是方法参数,避免对象过多的复制。不需要担心垂悬引用,GoLang 会为方法进行逃逸分析,根据分析结果决定将对象创建在堆中还是在栈上。
  2. 切片、mapstringchan 可以不用引用直接传递。当然也可以用引用,区别不大。
  3. 在更新切片、map等第二点提到的数据结构前,应该将值更新至最新状态后再添加,因为每次添加到这些结构中都会发生一次复制。
123
...
7