背景
项目上需求是一个数据库的一个表中的数据发生新增和修改时,同步到另一个数据的表中。这里两边都是Mysql数据库。限制条件是只要新增和修改的数据同步,删除数据时不同步。
方案
- 开源的方案有很多,比如:Canal、Otter、DataX、Kettle、FlinkX
- 自开发
由于限制了只要新增和修改的数据同步,所以我觉得学习其他组件较为复杂,索性自己开发个小工具。
前置知识
Mysql已经做好了数据变更的记录,就是Mysql的binlog文件。我们首先要开启Mysql的binlog功能,并配置好我们想要同步的数据库名称。以下作为示例:
mysql
- 开启binlog
- 在my.ini中加入以下配置
log-bin=mysql-bin # 开启 binlog binlog-format=ROW # 选择 ROW 模式 binlog-do-db=db_name # 需要监控的数据库名称- 重启数据库
- 校验是否开启成功
- 用navicat在查询中依次执行每条命令
-- 查看开启binlog是否成功 SHOW variables like 'log_bin'; -- value 应该是ON SHOW VARIABLES LIKE 'binlog_format'; -- 结果应该是ROW SHOW VARIABLES LIKE '%log%'; -- 所有binlog信息 SHOW master status; -- 应该是 binlog_do_db=sca_dy_center
以上我们开启了Mysql的binlog模式。每当数据有变化时,Mysql就会在这个文件记一笔变化的内容。我们就可以根据这个来做同步的操作。
Slave
有了binlog文件,我们要做的就是装作Slave也就是Mysql的备用主机,通知Mysql在binlog内容一有变化,就发送变化的内容给我们。我们接收到变化的内容就做对应的逻辑处理就好。
编码
由于最近在用golang,所以本次选择golang来开发。现在我们需要将程序做为Slave模式,怎么处理呢?
在golang的mysql库中已经处理的这样的协议。 go-mysql 在库的介绍中我们可以看到具备复制,增量备份等等一系列功能
go-mysql功能
先看主要逻辑
func main() {
// 读取yaml配置
etc.C = etc.NewConfig()
// 加载日志
xlog.NewLogger()
// 尝试连接两个数据库,连接异常则中止启动
xmysql.ConnectDbTest()
// 读取源数据库binlog最新的文件和位置
binLog := xmysql.GetBinLogPosition()
// 启动gorm
xmysql.G = xmysql.NewXGorm()
// 开始同步 新增和修改的数据
xsync.Sync(binLog)
}
配置文件
# 源数据库
srcDatabase:
host: 192.168.99.126
port: 3306
username: root
password: root
database: center # 数据库
table:
- zhwl_device # 同步的表名称
# 目标数据库
destDatabase:
host: 192.168.99.227
port: 3306
username: root
password: password
database: asset # 数据库
log:
out: both # 日志输出到 console file both (控制台 文件 两个都输出)
coroutine: 4 # 线程数 最大是10
说明:
- 我们先做了基础的读取配置(就是源数据库和目标数据库以及日志的配置)
- 然后尝试连接两个数据库,连接异常则中止启动
- 读取源数据库binlog最新的文件和位置
这里注意一点: 在每次启动时我们都会读到源数据库binlog从头到尾的数据变化过来,但是我们想的是每次启动是都是按此时之后变化的数据才推送。
在源数据库Mysql的binlog中我们执行命令
SHOW MASTER STATUS
tmp@127.0.0.1 ((none))> show master status\G;
*************************** 1. row ***************************
File: mysql-bin.000013
Position: 269728976
Binlog_Do_DB:
Binlog_Ignore_DB:
Executed_Gtid_Set: 108cc4a4-0d40-11ea-9598-2016d8c96b66:1-5,
c42216ad-0d37-11ea-b163-2016d8c96b56:1-9,
ceabbacf-0c77-11ea-b49f-2016d8c96b46:1-1662590
1 row in set (0.00 sec)
上面的File表示当前binlog的文件名,Position表示在binlog文件中的记录位置偏移量。也就是说binlog这个记录就像一个账本一样,记录了每条数据的变化过程,以及当前记录的最新位置。那我们在项目的启动的时候读取到这个Position,告诉Mysql我们要从这个最新的位置往后读取数据不就行了吗。说干就干。
首先我们只需执行 SHOW MASTER STATUS 命令即可得到源数据库的信息。
func GetBinLogPosition() *BinLog {
var (
file string
position uint32
binlogDoDB string
binlogIgnoreDB string
executedGtidSet []uint8
)
connStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s",
etc.C.SrcDatabase.Username,
etc.C.SrcDatabase.Password,
etc.C.SrcDatabase.Host,
etc.C.SrcDatabase.Port,
etc.C.SrcDatabase.Database,
)
db, err := sql.Open("mysql", connStr)
if err != nil {
xlog.LogErr.Error("Failed to connect to MySQL:", err)
panic(err)
}
defer db.Close()
err = db.QueryRow("SHOW MASTER STATUS").Scan(&file, &position, &binlogDoDB, &binlogIgnoreDB, &executedGtidSet)
if err != nil {
xlog.LogErr.Error("Failed to SHOW MASTER STATUS:", err)
panic(err)
}
xlog.LogInfo.Infof("Current binlog position: %s,%d", file, position)
return &BinLog{
File: file,
Position: position,
}
}
我们的得到了Position就要开启同步了
func Sync(b *xmysql.BinLog) {
cfg := replication.BinlogSyncerConfig{
ServerID: 100,
Flavor: "mysql",
Host: etc.C.SrcDatabase.Host,
Port: etc.C.SrcDatabase.Port,
User: etc.C.SrcDatabase.Username,
Password: etc.C.SrcDatabase.Password,
}
syncer := replication.NewBinlogSyncer(cfg)
streamer, err := syncer.StartSync(mysql.Position{Name: b.File, Pos: b.Position})
if err != nil {
xlog.LogErr.Error("Failed to Sync Mysql:", err)
panic(err)
}
xlog.LogInfo.Info("Start syncing")
// 启动数据处理器
DataChan.run()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-c
cancel()
}()
for {
ev, err := streamer.GetEvent(ctx)
if err != nil {
if err == context.Canceled {
xlog.LogErr.Error("Sync stopped")
return
}
xlog.LogErr.Errorf("Get event error: %v", err)
continue
}
// 只处理指定的表数据
switch e := ev.Event.(type) {
case *replication.RowsEvent:
// 数据库.表名
tableName := string(e.Table.Schema) + "." + string(e.Table.Table)
filterTable := xmysql.FilterTable(tableName)
if !filterTable {
continue
}
}
// 接收数据变更
switch ev.Header.EventType {
case replication.WRITE_ROWS_EVENTv1, replication.WRITE_ROWS_EVENTv2:
// 处理插入操作
rowsEvent := ev.Event.(*replication.RowsEvent)
for _, row := range rowsEvent.Rows {
device := model.ConvertRowToIotZhwlDevice(row)
//fmt.Printf("Insert row: %v\n", device)
DataChan.InsertChan <- device
}
case replication.UPDATE_ROWS_EVENTv1, replication.UPDATE_ROWS_EVENTv2:
// 处理更新操作
uev := ev.Event.(*replication.RowsEvent)
// 遍历行数据
for i, row := range uev.Rows {
// 检查行索引是否为偶数 偶数行为更新后的数据 奇数行为更新前的数据
if i%2 == 1 {
device := model.ConvertRowToIotZhwlDevice(row)
//fmt.Printf("Update row: %v\n", device)
DataChan.UpdateChan <- device
}
}
}
}
}
上面的
streamer, err := syncer.StartSync(mysql.Position{Name: b.File, Pos: b.Position})
就利用读取到的Position来指定位置读取数据。
其中定义了新增数据事件和修改数据事件的监听
// 新增数据
case replication.WRITE_ROWS_EVENTv1, replication.WRITE_ROWS_EVENTv2:
// 修改数据
case replication.UPDATE_ROWS_EVENTv1, replication.UPDATE_ROWS_EVENTv2:
注意: 由于我们只需要处理指定表的数据变化,但是这里会将整个库中所有表的变化都传递过来。所以我们还要做一步:数据表名称的过滤。这里没有做正则表达式的处理。
// 只处理指定的表数据
switch e := ev.Event.(type) {
case *replication.RowsEvent:
// 数据库.表名
tableName := string(e.Table.Schema) + "." + string(e.Table.Table)
filterTable := xmysql.FilterTable(tableName)
if !filterTable {
continue
}
}
现在我们接收到了数据变化,接着就做数据的同步处理,就是将数据新增或修改到目标库。简单的orm。这里我启动了多个协程来处理数据。
func (d *DataHandler) run() {
for i := 0; i < etc.C.Coroutine; i++ {
go processInsert(d.InsertChan)
go processUpdate(d.UpdateChan)
}
xlog.LogInfo.Infof("Start DataChan run goroutine num %d", etc.C.Coroutine)
}
func processInsert(ch <-chan *model.PropertyIot) {
for insertData := range ch {
// 处理新增数据
xlog.LogInfo.Infof("Insert: %v", insertData)
model.InsertDevice(insertData)
}
}
总结
同步Mysql的数据。首先要开启Mysql的binlog模式。程序伪装为Mysql的Slave来获取binlog的数据变化。接着通过命令获取当前binlog的Position,启动项目时指定从Position后读取数据,接着过滤指定表名的数据。最后做orm也可以加入协程来提高处理速度。
现在一个简单的同步工具就成型了。虽然功能很简单,但是可以大致了解同步的基本流程和简单知识。欢迎讨论。