mysql数据库数据同步更新

444 阅读6分钟

背景

项目上需求是一个数据库的一个表中的数据发生新增和修改时,同步到另一个数据的表中。这里两边都是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也可以加入协程来提高处理速度。

现在一个简单的同步工具就成型了。虽然功能很简单,但是可以大致了解同步的基本流程和简单知识。欢迎讨论。