9 changed files with 682 additions and 0 deletions
@ -0,0 +1,5 @@ |
|||
rem 编译为 Linux 64 位可执行文件 |
|||
set GOOS=linux |
|||
set GOARCH=amd64 |
|||
set CGO_ENABLED=0 |
|||
go build -o xttbinlog main.go |
|||
@ -0,0 +1,17 @@ |
|||
mysql: |
|||
addr: "127.0.0.1:3306" |
|||
user: "canal" |
|||
password: "canal_password" |
|||
database: "test" |
|||
|
|||
sync: |
|||
tables: |
|||
- "user" |
|||
- "order" |
|||
- "product" |
|||
batch_size: 100 |
|||
workers: 4 |
|||
|
|||
# storage: |
|||
# type: "redis" |
|||
# redis_url: "redis://localhost:6379/0" |
|||
@ -0,0 +1,39 @@ |
|||
package config |
|||
|
|||
import "github.com/go-mysql-org/go-mysql/canal" |
|||
|
|||
type Config struct { |
|||
MySQL struct { |
|||
Addr string `yaml:"addr"` |
|||
User string `yaml:"user"` |
|||
Password string `yaml:"password"` |
|||
Database string `yaml:"database"` |
|||
} `yaml:"mysql"` |
|||
|
|||
Sync struct { |
|||
Tables []string `yaml:"tables"` |
|||
BatchSize int `yaml:"batch_size"` |
|||
Workers int `yaml:"workers"` |
|||
} `yaml:"sync"` |
|||
|
|||
Storage struct { |
|||
Type string `yaml:"type"` // memory, redis, file
|
|||
FilePath string `yaml:"file_path"` |
|||
RedisURL string `yaml:"redis_url"` |
|||
} `yaml:"storage"` |
|||
} |
|||
|
|||
func (c *Config) ToCanalConfig() *canal.Config { |
|||
cfg := canal.NewDefaultConfig() |
|||
cfg.Addr = c.MySQL.Addr |
|||
cfg.User = c.MySQL.User |
|||
cfg.Password = c.MySQL.Password |
|||
cfg.Dump.ExecutionPath = "" |
|||
|
|||
// 只监听指定数据库
|
|||
if c.MySQL.Database != "" { |
|||
cfg.IncludeTableRegex = []string{c.MySQL.Database + "\\..*"} |
|||
} |
|||
|
|||
return cfg |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
module xttbinlog |
|||
|
|||
go 1.24.6 |
|||
|
|||
require ( |
|||
github.com/go-mysql-org/go-mysql v1.13.0 |
|||
github.com/go-redis/redis/v8 v8.11.0 |
|||
gopkg.in/yaml.v2 v2.4.0 |
|||
) |
|||
|
|||
require ( |
|||
filippo.io/edwards25519 v1.1.0 // indirect |
|||
github.com/BurntSushi/toml v1.3.2 // indirect |
|||
github.com/cespare/xxhash/v2 v2.1.1 // indirect |
|||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect |
|||
github.com/goccy/go-json v0.10.2 // indirect |
|||
github.com/google/uuid v1.3.0 // indirect |
|||
github.com/klauspost/compress v1.17.8 // indirect |
|||
github.com/pingcap/errors v0.11.5-0.20250318082626-8f80e5cb09ec // indirect |
|||
github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect |
|||
github.com/pingcap/log v1.1.1-0.20241212030209-7e3ff8601a2a // indirect |
|||
github.com/pingcap/tidb/pkg/parser v0.0.0-20250421232622-526b2c79173d // indirect |
|||
github.com/shopspring/decimal v1.2.0 // indirect |
|||
go.uber.org/atomic v1.11.0 // indirect |
|||
go.uber.org/multierr v1.11.0 // indirect |
|||
go.uber.org/zap v1.27.0 // indirect |
|||
golang.org/x/text v0.24.0 // indirect |
|||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect |
|||
) |
|||
@ -0,0 +1,158 @@ |
|||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= |
|||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= |
|||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= |
|||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= |
|||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= |
|||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= |
|||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= |
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= |
|||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= |
|||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= |
|||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= |
|||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= |
|||
github.com/go-mysql-org/go-mysql v1.13.0 h1:Hlsa5x1bX/wBFtMbdIOmb6YzyaVNBWnwrb8gSIEPMDc= |
|||
github.com/go-mysql-org/go-mysql v1.13.0/go.mod h1:FQxw17uRbFvMZFK+dPtIPufbU46nBdrGaxOw0ac9MFs= |
|||
github.com/go-redis/redis/v8 v8.11.0 h1:O1Td0mQ8UFChQ3N9zFQqo6kTU2cJ+/it88gDB+zg0wo= |
|||
github.com/go-redis/redis/v8 v8.11.0/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M= |
|||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= |
|||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= |
|||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= |
|||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= |
|||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= |
|||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= |
|||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= |
|||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= |
|||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= |
|||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= |
|||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= |
|||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= |
|||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= |
|||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= |
|||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= |
|||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= |
|||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= |
|||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= |
|||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= |
|||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= |
|||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= |
|||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= |
|||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= |
|||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= |
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= |
|||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= |
|||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= |
|||
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= |
|||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= |
|||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= |
|||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= |
|||
github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4= |
|||
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= |
|||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= |
|||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= |
|||
github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ= |
|||
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= |
|||
github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= |
|||
github.com/pingcap/errors v0.11.5-0.20250318082626-8f80e5cb09ec h1:3EiGmeJWoNixU+EwllIn26x6s4njiWRXewdx2zlYa84= |
|||
github.com/pingcap/errors v0.11.5-0.20250318082626-8f80e5cb09ec/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg= |
|||
github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 h1:tdMsjOqUR7YXHoBitzdebTvOjs/swniBTOLy5XiMtuE= |
|||
github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86/go.mod h1:exzhVYca3WRtd6gclGNErRWb1qEgff3LYta0LvRmON4= |
|||
github.com/pingcap/log v1.1.1-0.20241212030209-7e3ff8601a2a h1:WIhmJBlNGmnCWH6TLMdZfNEDaiU8cFpZe3iaqDbQ0M8= |
|||
github.com/pingcap/log v1.1.1-0.20241212030209-7e3ff8601a2a/go.mod h1:ORfBOFp1eteu2odzsyaxI+b8TzJwgjwyQcGhI+9SfEA= |
|||
github.com/pingcap/tidb/pkg/parser v0.0.0-20250421232622-526b2c79173d h1:3Ej6eTuLZp25p3aH/EXdReRHY12hjZYs3RrGp7iLdag= |
|||
github.com/pingcap/tidb/pkg/parser v0.0.0-20250421232622-526b2c79173d/go.mod h1:+8feuexTKcXHZF/dkDfvCwEyBAmgb4paFc3/WeYV2eE= |
|||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= |
|||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
|||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= |
|||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= |
|||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= |
|||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= |
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
|||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= |
|||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= |
|||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= |
|||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= |
|||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= |
|||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= |
|||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= |
|||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= |
|||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= |
|||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= |
|||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= |
|||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= |
|||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= |
|||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= |
|||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= |
|||
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= |
|||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= |
|||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= |
|||
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= |
|||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= |
|||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= |
|||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
|||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= |
|||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= |
|||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= |
|||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= |
|||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
|||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
|||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
|||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
|||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= |
|||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= |
|||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= |
|||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= |
|||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
|||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
|||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw= |
|||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
|||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
|||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= |
|||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= |
|||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
|||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= |
|||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
|||
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
|||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
|||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= |
|||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
|||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
|||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
|||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
|||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= |
|||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= |
|||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= |
|||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= |
|||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= |
|||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= |
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
|||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= |
|||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
|||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= |
|||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= |
|||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= |
|||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= |
|||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= |
|||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
|||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
|||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
|||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
|||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= |
|||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= |
|||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
|||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
|||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= |
|||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
|||
@ -0,0 +1,167 @@ |
|||
package handler |
|||
|
|||
import ( |
|||
"context" |
|||
"log" |
|||
"sync" |
|||
"time" |
|||
|
|||
"github.com/go-mysql-org/go-mysql/canal" |
|||
"github.com/go-mysql-org/go-mysql/mysql" |
|||
) |
|||
|
|||
type DataEvent struct { |
|||
Action string `json:"action"` |
|||
Schema string `json:"schema"` |
|||
Table string `json:"table"` |
|||
Timestamp time.Time `json:"timestamp"` |
|||
Data []interface{} `json:"data"` |
|||
OldData []interface{} `json:"old_data,omitempty"` |
|||
} |
|||
|
|||
type EventHandler struct { |
|||
canal.DummyEventHandler |
|||
eventChan chan *DataEvent |
|||
batchSize int |
|||
workers int |
|||
wg sync.WaitGroup |
|||
ctx context.Context |
|||
cancel context.CancelFunc |
|||
} |
|||
|
|||
func NewEventHandler(batchSize, workers int) *EventHandler { |
|||
ctx, cancel := context.WithCancel(context.Background()) |
|||
handler := &EventHandler{ |
|||
eventChan: make(chan *DataEvent, 1000), |
|||
batchSize: batchSize, |
|||
workers: workers, |
|||
ctx: ctx, |
|||
cancel: cancel, |
|||
} |
|||
|
|||
handler.startWorkers() |
|||
return handler |
|||
} |
|||
|
|||
func (h *EventHandler) startWorkers() { |
|||
for i := 0; i < h.workers; i++ { |
|||
h.wg.Add(1) |
|||
go h.worker(i) |
|||
} |
|||
} |
|||
|
|||
func (h *EventHandler) worker(id int) { |
|||
defer h.wg.Done() |
|||
|
|||
var batch []*DataEvent |
|||
batchTimer := time.NewTimer(100 * time.Millisecond) |
|||
defer batchTimer.Stop() |
|||
|
|||
for { |
|||
select { |
|||
case event, ok := <-h.eventChan: |
|||
if !ok { |
|||
// 通道关闭,处理剩余数据
|
|||
if len(batch) > 0 { |
|||
h.processBatch(batch) |
|||
} |
|||
return |
|||
} |
|||
|
|||
batch = append(batch, event) |
|||
|
|||
if len(batch) >= h.batchSize { |
|||
h.processBatch(batch) |
|||
batch = nil |
|||
batchTimer.Reset(100 * time.Millisecond) |
|||
} |
|||
|
|||
case <-batchTimer.C: |
|||
if len(batch) > 0 { |
|||
h.processBatch(batch) |
|||
batch = nil |
|||
} |
|||
batchTimer.Reset(100 * time.Millisecond) |
|||
|
|||
case <-h.ctx.Done(): |
|||
if len(batch) > 0 { |
|||
h.processBatch(batch) |
|||
} |
|||
return |
|||
} |
|||
} |
|||
} |
|||
|
|||
func (h *EventHandler) processBatch(events []*DataEvent) { |
|||
// 这里实现数据同步逻辑
|
|||
// 可以同步到其他数据库、消息队列、ES等
|
|||
|
|||
log.Printf("处理批次: %d 个事件", len(events)) |
|||
for _, event := range events { |
|||
// 示例:打印事件信息
|
|||
log.Printf("同步事件: %s.%s %s", event.Schema, event.Table, event.Action) |
|||
|
|||
// 实际应用中,这里可以同步到:
|
|||
// 1. 其他MySQL数据库
|
|||
// 2. Elasticsearch
|
|||
// 3. Redis缓存
|
|||
// 4. Kafka消息队列
|
|||
// 5. 其他数据存储
|
|||
} |
|||
} |
|||
|
|||
func (h *EventHandler) OnRow(e *canal.RowsEvent) error { |
|||
var event *DataEvent |
|||
|
|||
switch e.Action { |
|||
case "insert": |
|||
for _, row := range e.Rows { |
|||
event = &DataEvent{ |
|||
Action: "insert", |
|||
Schema: e.Table.Schema, |
|||
Table: e.Table.Name, |
|||
Timestamp: time.Now(), |
|||
Data: row, |
|||
} |
|||
h.eventChan <- event |
|||
} |
|||
|
|||
case "update": |
|||
for i := 0; i < len(e.Rows); i += 2 { |
|||
event = &DataEvent{ |
|||
Action: "update", |
|||
Schema: e.Table.Schema, |
|||
Table: e.Table.Name, |
|||
Timestamp: time.Now(), |
|||
OldData: e.Rows[i], |
|||
Data: e.Rows[i+1], |
|||
} |
|||
h.eventChan <- event |
|||
} |
|||
|
|||
case "delete": |
|||
for _, row := range e.Rows { |
|||
event = &DataEvent{ |
|||
Action: "delete", |
|||
Schema: e.Table.Schema, |
|||
Table: e.Table.Name, |
|||
Timestamp: time.Now(), |
|||
Data: row, |
|||
} |
|||
h.eventChan <- event |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (h *EventHandler) OnPosSynced(pos mysql.Position, force bool) error { |
|||
log.Printf("位置已同步: %s, force: %t", pos, force) |
|||
return nil |
|||
} |
|||
|
|||
func (h *EventHandler) Close() { |
|||
h.cancel() |
|||
close(h.eventChan) |
|||
h.wg.Wait() |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"log" |
|||
"os" |
|||
"os/signal" |
|||
"syscall" |
|||
|
|||
"xttbinlog/config" |
|||
"xttbinlog/syncer" |
|||
|
|||
"gopkg.in/yaml.v2" |
|||
) |
|||
|
|||
func main() { |
|||
// 读取配置
|
|||
cfg, err := loadConfig("config.yaml") |
|||
if err != nil { |
|||
log.Fatalf("加载配置失败: %v", err) |
|||
} |
|||
|
|||
// 创建数据同步器
|
|||
syncer, err := syncer.NewDataSyncer(cfg) |
|||
if err != nil { |
|||
log.Fatalf("创建同步器失败: %v", err) |
|||
} |
|||
|
|||
// 处理信号
|
|||
setupSignalHandler(syncer) |
|||
|
|||
// 启动同步
|
|||
if err := syncer.Start(); err != nil { |
|||
log.Fatalf("启动同步失败: %v", err) |
|||
} |
|||
} |
|||
|
|||
func loadConfig(filename string) (*config.Config, error) { |
|||
data, err := os.ReadFile(filename) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
var cfg config.Config |
|||
err = yaml.Unmarshal(data, &cfg) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return &cfg, nil |
|||
} |
|||
|
|||
func setupSignalHandler(syncer *syncer.DataSyncer) { |
|||
sigChan := make(chan os.Signal, 1) |
|||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) |
|||
|
|||
go func() { |
|||
sig := <-sigChan |
|||
log.Printf("接收到信号: %v", sig) |
|||
syncer.Stop() |
|||
os.Exit(0) |
|||
}() |
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
package storage |
|||
|
|||
import ( |
|||
"encoding/json" |
|||
"os" |
|||
"sync" |
|||
|
|||
"github.com/go-mysql-org/go-mysql/mysql" |
|||
"github.com/go-redis/redis/v8" |
|||
) |
|||
|
|||
type PositionStorage interface { |
|||
Save(position mysql.Position) error |
|||
Load() (mysql.Position, error) |
|||
} |
|||
|
|||
// 文件存储
|
|||
type FileStorage struct { |
|||
filePath string |
|||
mutex sync.RWMutex |
|||
} |
|||
|
|||
func NewFileStorage(filePath string) *FileStorage { |
|||
return &FileStorage{filePath: filePath} |
|||
} |
|||
|
|||
func (fs *FileStorage) Save(pos mysql.Position) error { |
|||
fs.mutex.Lock() |
|||
defer fs.mutex.Unlock() |
|||
|
|||
data, err := json.Marshal(pos) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return os.WriteFile(fs.filePath, data, 0644) |
|||
} |
|||
|
|||
func (fs *FileStorage) Load() (mysql.Position, error) { |
|||
fs.mutex.RLock() |
|||
defer fs.mutex.RUnlock() |
|||
|
|||
data, err := os.ReadFile(fs.filePath) |
|||
if err != nil { |
|||
if os.IsNotExist(err) { |
|||
return mysql.Position{}, nil |
|||
} |
|||
return mysql.Position{}, err |
|||
} |
|||
|
|||
var pos mysql.Position |
|||
err = json.Unmarshal(data, &pos) |
|||
return pos, err |
|||
} |
|||
|
|||
// Redis存储
|
|||
type RedisStorage struct { |
|||
client *redis.Client |
|||
key string |
|||
} |
|||
|
|||
func NewRedisStorage(redisURL, key string) (*RedisStorage, error) { |
|||
opt, err := redis.ParseURL(redisURL) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
client := redis.NewClient(opt) |
|||
return &RedisStorage{client: client, key: key}, nil |
|||
} |
|||
|
|||
func (rs *RedisStorage) Save(pos mysql.Position) error { |
|||
data, err := json.Marshal(pos) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return rs.client.Set(rs.client.Context(), rs.key, data, 0).Err() |
|||
} |
|||
|
|||
func (rs *RedisStorage) Load() (mysql.Position, error) { |
|||
data, err := rs.client.Get(rs.client.Context(), rs.key).Bytes() |
|||
if err == redis.Nil { |
|||
return mysql.Position{}, nil |
|||
} |
|||
if err != nil { |
|||
return mysql.Position{}, err |
|||
} |
|||
|
|||
var pos mysql.Position |
|||
err = json.Unmarshal(data, &pos) |
|||
return pos, err |
|||
} |
|||
@ -0,0 +1,112 @@ |
|||
package syncer |
|||
|
|||
import ( |
|||
"log" |
|||
"time" |
|||
|
|||
"github.com/go-mysql-org/go-mysql/canal" |
|||
"github.com/go-mysql-org/go-mysql/mysql" |
|||
|
|||
"xttbinlog/config" |
|||
"xttbinlog/handler" |
|||
"xttbinlog/storage" |
|||
) |
|||
|
|||
type DataSyncer struct { |
|||
canal *canal.Canal |
|||
handler *handler.EventHandler |
|||
storage storage.PositionStorage |
|||
config *config.Config |
|||
} |
|||
|
|||
func NewDataSyncer(cfg *config.Config) (*DataSyncer, error) { |
|||
// 创建canal实例
|
|||
c, err := canal.NewCanal(cfg.ToCanalConfig()) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
// 创建位置存储器
|
|||
var posStorage storage.PositionStorage |
|||
switch cfg.Storage.Type { |
|||
case "redis": |
|||
posStorage, err = storage.NewRedisStorage(cfg.Storage.RedisURL, "binlog_position") |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
case "file": |
|||
posStorage = storage.NewFileStorage(cfg.Storage.FilePath) |
|||
default: |
|||
posStorage = storage.NewFileStorage("./binlog.position") |
|||
} |
|||
|
|||
// 创建事件处理器
|
|||
eventHandler := handler.NewEventHandler(cfg.Sync.BatchSize, cfg.Sync.Workers) |
|||
|
|||
syncer := &DataSyncer{ |
|||
canal: c, |
|||
handler: eventHandler, |
|||
storage: posStorage, |
|||
config: cfg, |
|||
} |
|||
|
|||
c.SetEventHandler(eventHandler) |
|||
return syncer, nil |
|||
} |
|||
|
|||
func (ds *DataSyncer) Start() error { |
|||
// 加载保存的位置
|
|||
savedPos, err := ds.storage.Load() |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
var startPos mysql.Position |
|||
|
|||
if savedPos.Name != "" { |
|||
log.Printf("从保存的位置开始: %s", savedPos) |
|||
startPos = savedPos |
|||
} else { |
|||
// 第一次启动,从当前位置开始
|
|||
pos, err := ds.canal.GetMasterPos() |
|||
if err != nil { |
|||
return err |
|||
} |
|||
startPos = pos |
|||
log.Printf("第一次启动,从当前位置开始: %s", startPos) |
|||
} |
|||
|
|||
// 启动同步
|
|||
go ds.positionSaver() |
|||
|
|||
log.Printf("开始数据同步...") |
|||
return ds.canal.RunFrom(startPos) |
|||
} |
|||
|
|||
func (ds *DataSyncer) positionSaver() { |
|||
ticker := time.NewTicker(5 * time.Second) |
|||
defer ticker.Stop() |
|||
|
|||
for { |
|||
select { |
|||
case <-ticker.C: |
|||
pos := ds.canal.SyncedPosition() |
|||
if err := ds.storage.Save(pos); err != nil { |
|||
log.Printf("保存位置失败: %v", err) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
func (ds *DataSyncer) Stop() { |
|||
log.Printf("停止数据同步...") |
|||
|
|||
// 保存最后的位置
|
|||
pos := ds.canal.SyncedPosition() |
|||
if err := ds.storage.Save(pos); err != nil { |
|||
log.Printf("保存最后位置失败: %v", err) |
|||
} |
|||
|
|||
ds.handler.Close() |
|||
ds.canal.Close() |
|||
} |
|||
Loading…
Reference in new issue