You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
382 lines
9.8 KiB
382 lines
9.8 KiB
/*
|
|
* Copyright 2020-2021 the original author(https://github.com/wj596)
|
|
*
|
|
* <p>
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
* </p>
|
|
*/
|
|
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/juju/errors"
|
|
"github.com/siddontang/go-mysql/canal"
|
|
"go.uber.org/atomic"
|
|
"log"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
|
|
"go-mysql-transfer/global"
|
|
"go-mysql-transfer/model"
|
|
"go-mysql-transfer/service/endpoint"
|
|
"go-mysql-transfer/util/dates"
|
|
"go-mysql-transfer/util/logs"
|
|
)
|
|
|
|
// 存量数据
|
|
type StockService struct {
|
|
canal *canal.Canal
|
|
endpoint endpoint.Endpoint
|
|
|
|
queueCh chan []*model.RowRequest
|
|
counter map[string]int64
|
|
lockOfCounter sync.Mutex
|
|
totalRows map[string]int64
|
|
wg sync.WaitGroup
|
|
shutoff *atomic.Bool
|
|
}
|
|
|
|
func NewStockService() *StockService {
|
|
return &StockService{
|
|
queueCh: make(chan []*model.RowRequest, global.Cfg().Maxprocs),
|
|
counter: make(map[string]int64),
|
|
totalRows: make(map[string]int64),
|
|
shutoff: atomic.NewBool(false),
|
|
}
|
|
}
|
|
|
|
func (s *StockService) Run() error {
|
|
canalCfg := canal.NewDefaultConfig()
|
|
canalCfg.Addr = global.Cfg().Addr
|
|
canalCfg.User = global.Cfg().User
|
|
canalCfg.Password = global.Cfg().Password
|
|
canalCfg.Charset = global.Cfg().Charset
|
|
canalCfg.Flavor = global.Cfg().Flavor
|
|
canalCfg.ServerID = global.Cfg().SlaveID
|
|
canalCfg.Dump.ExecutionPath = global.Cfg().DumpExec
|
|
canalCfg.Dump.DiscardErr = false
|
|
canalCfg.Dump.SkipMasterData = global.Cfg().SkipMasterData
|
|
|
|
if c, err := canal.NewCanal(canalCfg); err != nil {
|
|
errors.Trace(err)
|
|
} else {
|
|
s.canal = c
|
|
}
|
|
|
|
if err := s.completeRules(); err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
s.addDumpDatabaseOrTable()
|
|
|
|
endpoint := endpoint.NewEndpoint(s.canal)
|
|
if err := endpoint.Connect(); err != nil {
|
|
log.Println(err.Error())
|
|
return errors.Trace(err)
|
|
}
|
|
s.endpoint = endpoint
|
|
|
|
startTime := dates.NowMillisecond()
|
|
log.Println(fmt.Sprintf("bulk size: %d", global.Cfg().BulkSize))
|
|
for _, rule := range global.RuleInsList() {
|
|
if rule.OrderByColumn == "" {
|
|
return errors.New("empty order_by_column not allowed")
|
|
}
|
|
|
|
exportColumns := s.exportColumns(rule)
|
|
fullName := fmt.Sprintf("%s.%s", rule.Schema, rule.Table)
|
|
log.Println(fmt.Sprintf("开始导出 %s", fullName))
|
|
|
|
res, err := s.canal.Execute(fmt.Sprintf("select count(1) from %s", fullName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
totalRow, err := res.GetInt(0, 0)
|
|
s.totalRows[fullName] = totalRow
|
|
log.Println(fmt.Sprintf("%s 共 %d 条数据", fullName, totalRow))
|
|
|
|
s.counter[fullName] = 0
|
|
|
|
var batch int64
|
|
size := global.Cfg().BulkSize
|
|
if batch%size == 0 {
|
|
batch = totalRow / size
|
|
} else {
|
|
batch = (totalRow / size) + 1
|
|
}
|
|
|
|
var processed atomic.Int64
|
|
for i := 0; i < global.Cfg().Maxprocs; i++ {
|
|
s.wg.Add(1)
|
|
go func(_fullName, _columns string, _rule *global.Rule) {
|
|
for {
|
|
processed.Inc()
|
|
requests, err := s.export(_fullName, _columns, processed.Load(), _rule)
|
|
if err != nil {
|
|
logs.Error(err.Error())
|
|
s.shutoff.Store(true)
|
|
break
|
|
}
|
|
|
|
s.imports(_fullName, requests)
|
|
if processed.Load() > batch {
|
|
break
|
|
}
|
|
}
|
|
s.wg.Done()
|
|
}(fullName, exportColumns, rule)
|
|
}
|
|
}
|
|
|
|
s.wg.Wait()
|
|
|
|
fmt.Println(fmt.Sprintf("共耗时 :%d(毫秒)", dates.NowMillisecond()-startTime))
|
|
|
|
for k, v := range s.totalRows {
|
|
vv, ok := s.counter[k]
|
|
if ok {
|
|
fmt.Println(fmt.Sprintf("表: %s,共:%d 条数据,成功导入:%d 条", k, v, vv))
|
|
if v > vv {
|
|
fmt.Println("存在导入错误的数据,具体请至日志查看")
|
|
}
|
|
}
|
|
}
|
|
|
|
s.endpoint.Close() // 关闭客户端
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *StockService) export(fullName, columns string, batch int64, rule *global.Rule) ([]*model.RowRequest, error) {
|
|
if s.shutoff.Load() {
|
|
return nil, errors.New("shutoff")
|
|
}
|
|
|
|
offset := s.offset(batch)
|
|
sql := s.buildSql(fullName, columns, offset, rule)
|
|
logs.Infof("export sql : %s", sql)
|
|
resultSet, err := s.canal.Execute(sql)
|
|
if err != nil {
|
|
logs.Errorf("数据导出错误: %s - %s", sql, err.Error())
|
|
return nil, err
|
|
}
|
|
rowNumber := resultSet.RowNumber()
|
|
requests := make([]*model.RowRequest, 0, rowNumber)
|
|
for i := 0; i < rowNumber; i++ {
|
|
rowValues := make([]interface{}, 0, len(rule.TableInfo.Columns))
|
|
request := new(model.RowRequest)
|
|
for j := 0; j < len(rule.TableInfo.Columns); j++ {
|
|
val, err := resultSet.GetValue(i, j)
|
|
if err != nil {
|
|
logs.Errorf("数据导出错误: %s - %s", sql, err.Error())
|
|
break
|
|
}
|
|
rowValues = append(rowValues, val)
|
|
request.Action = canal.InsertAction
|
|
request.RuleKey = global.RuleKey(rule.Schema, rule.Table)
|
|
request.Row = rowValues
|
|
}
|
|
requests = append(requests, request)
|
|
}
|
|
|
|
return requests, nil
|
|
}
|
|
|
|
// 构造SQL
|
|
func (s *StockService) buildSql(fullName, columns string, offset int64, rule *global.Rule) string {
|
|
size := global.Cfg().BulkSize
|
|
if len(rule.TableInfo.PKColumns) == 0 {
|
|
return fmt.Sprintf("select %s from %s order by %s limit %d,%d", columns, fullName, rule.OrderByColumn, offset, size)
|
|
}
|
|
|
|
i := rule.TableInfo.PKColumns[0]
|
|
n := rule.TableInfo.GetPKColumn(i).Name
|
|
t := "select b.* from (select %s from %s order by %s limit %d,%d) a left join %s b on a.%s=b.%s"
|
|
sql := fmt.Sprintf(t, n, fullName, rule.OrderByColumn, offset, size, fullName, n, n)
|
|
return sql
|
|
}
|
|
|
|
func (s *StockService) imports(fullName string, requests []*model.RowRequest) {
|
|
if s.shutoff.Load() {
|
|
return
|
|
}
|
|
|
|
succeeds := s.endpoint.Stock(requests)
|
|
count := s.incCounter(fullName, succeeds)
|
|
log.Println(fmt.Sprintf("%s 导入数据 %d 条", fullName, count))
|
|
}
|
|
|
|
func (s *StockService) exportColumns(rule *global.Rule) string {
|
|
if rule.IncludeColumnConfig != "" {
|
|
var columns string
|
|
includes := strings.Split(rule.IncludeColumnConfig, ",")
|
|
for _, c := range rule.TableInfo.Columns {
|
|
for _, e := range includes {
|
|
var column string
|
|
if strings.ToUpper(e) == strings.ToUpper(c.Name) {
|
|
column = c.Name
|
|
} else {
|
|
column = "null as " + c.Name
|
|
}
|
|
|
|
if columns != "" {
|
|
columns = columns + ","
|
|
}
|
|
columns = columns + column
|
|
}
|
|
}
|
|
return columns
|
|
}
|
|
|
|
if rule.ExcludeColumnConfig != "" {
|
|
var columns string
|
|
excludes := strings.Split(rule.ExcludeColumnConfig, ",")
|
|
for _, c := range rule.TableInfo.Columns {
|
|
for _, e := range excludes {
|
|
var column string
|
|
if strings.ToUpper(e) == strings.ToUpper(c.Name) {
|
|
column = "null as " + c.Name
|
|
} else {
|
|
column = c.Name
|
|
}
|
|
|
|
if columns != "" {
|
|
columns = columns + ","
|
|
}
|
|
columns = columns + column
|
|
}
|
|
}
|
|
return columns
|
|
}
|
|
|
|
return "*"
|
|
}
|
|
|
|
func (s *StockService) offset(currentPage int64) int64 {
|
|
var offset int64
|
|
|
|
if currentPage > 0 {
|
|
offset = (currentPage - 1) * global.Cfg().BulkSize
|
|
}
|
|
|
|
return offset
|
|
}
|
|
|
|
func (s *StockService) Close() {
|
|
s.canal.Close()
|
|
}
|
|
|
|
func (s *StockService) incCounter(name string, n int64) int64 {
|
|
s.lockOfCounter.Lock()
|
|
defer s.lockOfCounter.Unlock()
|
|
|
|
c, ok := s.counter[name]
|
|
if ok {
|
|
c = c + n
|
|
s.counter[name] = c
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
func (s *StockService) completeRules() error {
|
|
wildcards := make(map[string]bool)
|
|
for _, rc := range global.Cfg().RuleConfigs {
|
|
if rc.Table == "*" {
|
|
return errors.Errorf("wildcard * is not allowed for table name")
|
|
}
|
|
|
|
if regexp.QuoteMeta(rc.Table) != rc.Table { //通配符
|
|
if _, ok := wildcards[global.RuleKey(rc.Schema, rc.Schema)]; ok {
|
|
return errors.Errorf("duplicate wildcard table defined for %s.%s", rc.Schema, rc.Table)
|
|
}
|
|
|
|
tableName := rc.Table
|
|
if rc.Table == "*" {
|
|
tableName = "." + rc.Table
|
|
}
|
|
sql := fmt.Sprintf(`SELECT table_name FROM information_schema.tables WHERE
|
|
table_name RLIKE "%s" AND table_schema = "%s";`, tableName, rc.Schema)
|
|
res, err := s.canal.Execute(sql)
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
for i := 0; i < res.Resultset.RowNumber(); i++ {
|
|
tableName, _ := res.GetString(i, 0)
|
|
newRule, err := global.RuleDeepClone(rc)
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
newRule.Table = tableName
|
|
ruleKey := global.RuleKey(rc.Schema, tableName)
|
|
global.AddRuleIns(ruleKey, newRule)
|
|
}
|
|
} else {
|
|
newRule, err := global.RuleDeepClone(rc)
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
ruleKey := global.RuleKey(rc.Schema, rc.Table)
|
|
global.AddRuleIns(ruleKey, newRule)
|
|
}
|
|
}
|
|
|
|
for _, rule := range global.RuleInsList() {
|
|
tableMata, err := s.canal.GetTable(rule.Schema, rule.Table)
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
if len(tableMata.PKColumns) == 0 {
|
|
if !global.Cfg().SkipNoPkTable {
|
|
return errors.Errorf("%s.%s must have a PK for a column", rule.Schema, rule.Table)
|
|
}
|
|
}
|
|
if len(tableMata.PKColumns) > 1 {
|
|
rule.IsCompositeKey = true // 组合主键
|
|
}
|
|
rule.TableInfo = tableMata
|
|
rule.TableColumnSize = len(tableMata.Columns)
|
|
|
|
if err := rule.Initialize(); err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
|
|
if rule.LuaEnable() {
|
|
if err := rule.CompileLuaScript(global.Cfg().DataDir); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *StockService) addDumpDatabaseOrTable() {
|
|
var schema string
|
|
schemas := make(map[string]int)
|
|
tables := make([]string, 0, global.RuleInsTotal())
|
|
for _, rule := range global.RuleInsList() {
|
|
schema = rule.Table
|
|
schemas[rule.Schema] = 1
|
|
tables = append(tables, rule.Table)
|
|
}
|
|
if len(schemas) == 1 {
|
|
s.canal.AddDumpTables(schema, tables...)
|
|
} else {
|
|
keys := make([]string, 0, len(schemas))
|
|
for key := range schemas {
|
|
keys = append(keys, key)
|
|
}
|
|
s.canal.AddDumpDatabases(keys...)
|
|
}
|
|
}
|
|
|