- GreatSQL 社区原创内容未经受权不得随便应用,转载请分割小编并注明起源。
MySQL 作为最风行的开源关系型数据库,有大量的拥趸。其生态曾经相当欠缺,各项个性在圈内都有大量钻研。每次新个性公布,都会有业界大咖对其进行全面扫视、解读、钻研,本文要讲的 MySQL binlog 解析也有很多的前辈开发过优良的工具进行解析过(例如 canal),本文再提旧案未免有造轮子嫌疑。
然而我作为菜鸟,通过 MySQL Internals 手册来钻研一下 MySQL 的 binlog 的协定、event 类型、存储格局,并通过 MySQL Internals 手册的形容来窥探 MySQL 的数据存储格局,并且学习 Golang 语言个性,应该还有肯定的学习意义。
所以不揣冒昧,对解析 binlog 的过程编辑了以下,来梳理我对 binlog 只知其一; 不知其二,心愿不会贻笑大方之家。
波及到的工具版本信息:
IDE: GoLand 2021.1.1 试用版
Golang: go1.12.10
DB: mysql 8.0.23
MySQL Internals 手册:
https://dev.mysql.com/doc/int…
Talk is cheap,show the code.
一、MySQL binlog
binlog 是对数据库执行的所有 DDL、DML 语句以事件(event)模式记录的的日志,事务平安,目标是用来进行复制和复原应用,是 MySQL 重要的个性之一。
在解析 binlog 之前数据库须要开启 binlog。
配置如下参数:
[mysqld]
log-bin=mysql-bin
server-id=1
binlog_format=ROW
~ 注:binlog_format 格局还有 statement 和 mixed 格局,篇幅所限,这里只探讨 RBR 状况。~
解析 binlog,MySQL 提供了 mysqlbinlog 工具来解析 binlog 文件。但这里我是通过程序模仿一个从库(slave)角色来解析流式的 binlog,所以须要创立账号,用来连贯主库和申请 binlog。其中:
- 1、获取主库 binlog,必须有 REPLICATION SLAVE 权限。
- 2、获取 binlog filename 和 position 必须有 REPLICATION CLIENT 或 SUPER 权限
倡议的受权:
CREATE USER mysqlsync@'%' IDENTIFIED BY 'mysqlsync';
GRANT REPLICATION SLAVE, REPLICATION CLIENT, SELECT ON *.* TO 'mysqlsync'@'%';
二、解析流程
解析形式时模仿从库(slave),MySQL 的从库在通过 ip、port、user、password 连贯主库后,向主库发送:
binlogPosition
binlogFlag
serverId
而后获取主库 binlog。
程序实现解析 binlog 首先是连贯主库注册 slave 身份。而后向主库发送 COM_BINLOG_DUMP 或 COM_BINLOG_DUMP_GTID 申请 binlog。
COM_BINLOG_DUMP 与 COM_BINLOG_DUMP_GTID 的区别在于 COM_BINLOG_DUMP_GTID 如果发送的通信包内不蕴含 binlog-filename,主库则从已知的第一个 binlog 发送 binlog-stream。
手册 14.9.6 介绍:
If the binlog-filename is empty, the server will send the binlog-stream of the first known binlog.
流程如下:
- 1、连贯 Master 数据库
输出数据库 IP:PORT,用户名明码连贯到 Master 数据库。 - 2、设置相干参数
master_binlog_checksum:MySQL 在 5.6 版本之后为 binlog 引入了 checksum 机制,从库须要与主库相干参数保持一致。
server_id:
解析程序相当于一个从库,须要向主库发送 set 注册
@master_binlog_checksum= @@global.binlog_checksum. - 3、注册从节点
告知客户端的 host、port、用户名与明码、serverId 发送 COM_BINLOG_DUMP 向 Master 申请 binlog stream. - 4、接管 binlog stream
接管 binlog 后,依据 event type 解析获取数据输入。
三、解析 binlog
3.1 连贯主库
程序应用 tcp 协定来连贯主库,所以须要构建 mysql 协定的通信包来与主库进行三次握手。在通信包构建以及连贯、发送和读取通信包,调用了 kingshard 的源码,见:
https://github.com/flike/king…
其中,kingshard/mysql 蕴含 MySQL 的 client/server 协定的实现,kingshard/backend 蕴含了基于 tcp 连贯的三次握手获取与 MySQL 的连贯以及发送命令,获取并解析后果集等。
协定内容在手册中可参看:
https://dev.mysql.com/doc/int…
程序中调用前先引入:
import (
"github.com/flike/kingshard/mysql"
"github.com/flike/kingshard/backend"
)
申明一个一般连贯函数,返回参数为:连接结构体(struct)以及三个字符串,对应连贯,地址,账号,明码。
func newConn(addr string, user string, pass string) *backend.Conn {c := new(backend.Conn)
if err := c.Connect(addr, user, pass, ""); err != nil {fmt.Println("Connect failed ____________________")
} else {fmt.Println("Connect success ____________________")
}
return c
}
有了下面的连贯函数,就能够在 main 函数中调用 newConn 函数来获取一个连贯,地址,账户,明码。
addr := "127.0.0.1:3306"
user := "mysqlsync"
pass := "mysqlsync"
c := newConn(addr, user, pass)
3.2 设置 checksum
MySQL 在 5.6 之后版本为 binlog 引入了 checksum 机制,例如 crc32,咱们的程序作为 mysql slave,须要与服务端相干参数保持一致,须要执行:set @master_binlog_checksum= @@global.binlog_checksum。
应用获取的连贯执行设置 checksum:
_, err := c.Execute("set @master_binlog_checksum= @@global.binlog_checksum")
if err != nil {fmt.Println(err)
}
3.3 获取 binlog 以后的 binlog_filename,binlog_pos
向 master 发送 show master status 语句查问以后的 binlog_filename,binlog_pos。调用的 ShowMasterStatus 是查问主库以后的 binlog_fileneme 和 binlog_pos。
show master status 后果集的第一行第一列为 inlog_file,第一行第二列为 binlog_pos,返回后果如下:
mysql> show master status;
+---------------+----------+--------------+------------------+--------------------------------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+---------------+----------+--------------+------------------+--------------------------------------------+
| binlog.000007 | 192 | | | bd3f8dcf-c9d2-11eb-9a2d-080027dcd936:1-278 |
+---------------+----------+--------------+------------------+--------------------------------------------+
1 row in set (0.00 sec)
在 main 函数中增加调用,来接管获取到的 binlog_filename,binlog_pos。代码如下:
binlogFileV, binlogPosV, err := c.ShowMasterStatus("show master status")
if err != nil {panic(err)
}
其中 ShowMasterStatus 函数代码为获取后果集的第一行的前两列并返回:
func (c *Conn) ShowMasterStatus(com string) (string, uint64, error) {
var res *mysql.Result
res, err := c.exec(com)
if err != nil {panic(err)
}
var n = len(res.Resultset.Values)
if n == 1 {fileStr, _ := res.Resultset.GetValue(0, 0)
posStr, _ := res.Resultset.GetValue(0, 1)
fileStrV, okf := fileStr.(string)
if !okf {panic(err)
}
posStrV, okp := posStr.(uint64)
if !okp {panic(err)
}
return fileStrV, posStrV, nil
}
return "", 0, fmt.Errorf("invalid resultset")
}
3.4 封装 COM_BINLOG_DUMP 通信 packet
取得了 Master 的 binlog_filename 和 binlog_pos,通过发送 COM_BINLOG_DUMP 到 Matser 申请 binlog。在 internal 手册中,COM_BINLOG_DUMP 解释:
https://dev.mysql.com/doc/int…
1 [12] COM_BINLOG_DUMP
4 binlog-pos
2 flags
4 server-id
string[EOF] binlog-filename
此时,须要咱们本人来封装 packet,在组成发送到 Master 通信的 packet 中,第 1 个字节地位存储 COM_BINLOG_DUMP 标识,第 2 到第 5 字节存储 binlog-pos。
第 6 到第 7 字节存储 slave 的 server-id,残余变长字节用来存储 binlog-filename。
在 main 函数中,创立字节数组,用获取到的 binlog-filename、binlog-pos,与 COM_BINLOG_DUMP、server-id 组成申请 binlog 通信 packet 向 Master 申请 binlog。
在 main 函数中减少如下代码:
binlogFile := []byte(binlogFileV)
var binlogFlag uint16 = 0
var serverId uint32 = 698148981
length := len(binlogFile)
data := make([]byte, 1+4+2+4+length)
data[0] = mysql.COM_BINLOG_DUMP
data[1] = byte(binlogPosV & 0xFFFFFFFF) // binlogPosV & 0xFFFFFFFF uint64 --> uint32
data[2] = byte((binlogPosV & 0xFFFFFFFF) >> 8)
data[3] = byte((binlogPosV & 0xFFFFFFFF) >> 16)
data[4] = byte((binlogPosV & 0xFFFFFFFF) >> 24)
data[5] = byte(binlogFlag)
data[6] = byte(binlogFlag >> 8)
data[7] = byte(serverId)
data[8] = byte(serverId >> 8)
data[9] = byte(serverId >> 16)
data[10] = byte(serverId >> 24)
copy(data[1+4+2+4:], binlogFile)
至此,申请 binlog 的 packet 组装实现,将其发送给 Master,Master 就会继续一直向本程序通信应用的 IP 和端口发送 binlog network stream,除非遇到命令进行或者网络问题中断。
在 main 函数中减少调用 BinlogDump 函数申请 binlog stream 的代码:
c.BinlogDump(data, addr, user, pass)
上面就是 main 函数调用的申请 binlog 的函数(此为局部,后续该函数中还将减少解析 binlog 代码):
func (c *Conn) BinlogDump(arg []byte, addr string, user string, pass string) ([]byte, error) {if err := c.writeCommandBuf(arg[0], arg[1:]); err != nil {fmt.Println("WriteCommandBuf failed")
} else {fmt.Println("WriteCommandBuf success")
}
}
其中 writeCommandBuf 函数是依据 tcp 通信协议封装 packet:
func (c *Conn) writeCommandBuf(command byte, arg []byte) error {
c.pkg.Sequence = 0
length := len(arg) + 1
data := make([]byte, length+4)
data[4] = command
copy(data[5:], arg)
return c.writePacket(data)
}
3.5 解析 event
MySQL Binlog 的 event packet 分为 event header 和 event data 两局部。在不同的 Binlog Version 中 event header 长度不同。
在 MySQL 5.0.0+ 版本中,应用的是 Version 4,其 event header 占用字节长度是 19。event data 则依据 event 类型不同,占用字节长度也不尽相同,后文会对局部 event 进行解析。
binlog header 的组成在 internal 手册中解释:
https://dev.mysql.com/doc/int…
4 timestamp
1 event type
4 server-id
4 event-size
if binlog-version > 1:
4 log pos
2 flags
在 binlog 的 event header 之前,应用一个字节位来存储 packet 标识,蕴含 OK_Packet,EOF_Packet。EOF_Packet 用来标识一个查问操作的完结,在 MySQL 5.7.5 当前的版本曾经过期。手册中对此解释:
These rules distinguish whether the packet represents OK or EOF:
OK: header = 0 and length of packet > 7
EOF: header = 0xfe and length of packet < 9
具体请参考:https://dev.mysql.com/doc/int…
依据手册介绍,咱们对 event stream 进行接管、解析。对上文中的 BinlogDump 函数进行扩大,golang 没有提供 while 循环,这里应用 for 循环继续接管 binlog event。
减少的解析 event header 的代码:
func (c *Conn) BinlogDump(arg []byte, addr string, user string, pass string) ([]byte, error) {if err := c.writeCommandBuf(arg[0], arg[1:]); err != nil {fmt.Println("WriteCommandBuf failed")
} else {fmt.Println("WriteCommandBuf success")
}
for {data, err := c.readPacket()
if err != nil {return nil, err}
if data[0] == mysql.EOF_HEADER && len(data) < 9 {fmt.Println("ReadEOF Success" + "Length:" + strconv.Itoa(len(data)))
return nil, nil
}
if data[0] == mysql.OK_HEADER {if _, err := c.handleOKPacket(data); err != nil {fmt.Println("ReadOk failed" + "length:" + strconv.Itoa(len(data)))
return nil, err
} else {timeStamp := data[1:5]
eventType := data[5]
serverId := data[6:10]
eventSize := data[10:14]
logPos := data[14:18]
flags := data[18:20]
}
}
}
}
这样,就实现了对 event header 的解析,这里取得的重要信息就是 event type,这将决定后续对 event 的解析形式。
依据 eventType,就能够 event data 进行解析了。
MySQL binlog 中,共有三十几种 event type,在 MySQL 8.0.23 中,在开启 GTID 时,执行一个事务,在 binlog 中会写入以下几种 event:
- GTID_LOG_EVENT:蕴含 last_committed,sequence_number 等组提交信息的 event。
- QUERY_EVENT:在 binlog_format=statement 时,执行的语句即存储在 QUERY_EVENT 中,例如 BEGIN、START TRANSACTION 等。
- ROWS_QUERY_LOG_EVENT:记录原始 SQL 语句,并蕴含语句对应的行变更具体内容
- TABLE_MAP_EVENT:蕴含事务波及的表的 ID 和内部结构定义信息
- WRITE_ROWS_EVENT_V2:MySQL 5.6.x 当前版本的 ROWS_EVENT
- UPDATE_ROWS_EVENT_V2:MySQL 5.6.x 当前版本的 ROWS_EVENT
- DELETE_ROWS_EVENT_V2:MySQL 5.6.x 当前版本的 ROWS_EVENT
- XID_EVENT:用来标识 xa 事务的,在两阶段提交中,当执行 commit 时,会在 binlog 中记录 XID_EVENT
3.5.1 筛选 event(对事务波及的 event 进行解析)
在本文中,着重对:
- QUERY_EVENT
- TABLE_MAP_EVENT
- WRITE_ROWS_EVENT_V2
- UPDATE_ROWS_EVENT_V2
- DELETE_ROWS_EVENT_V2
几个类型的 event 进行解析,也是事务波及到的几种 event type。
在 BinlogDump 函数中的 OK_HEADER 判断中减少 golang 的 switch 代码,来判断过滤 event type,便于前面的解析。switch 示例如下:
switch eventType {
case mysql.XID_EVENT:
fmt.Printf("EVENT TYPE: %v\n", "XID_EVENT")
default:
fmt.Printf("EVENT TYPE: %v\n", "This event will ignored!")
}
在每一个蕴含 event 的 packet 中,第 1 个字节为 OK_Packet 标识(值为 0),第 2 -19 字节为 event header,而第 20 当前的字节则为 event data 局部。
在程序中减少两个全局变量 schema,table:
var schema string // global para
var table string // global para
3.5.2 解析 QUERY_EVENT
首先对 QUERY_EVENT 进行解析。在 QUERY_EVENT 中存储 DDL 及 begin 等语句,局部信息略过没有解析。代码解析如下:case mysql.QUERY_EVENT:
fmt.Printf("EVENT TYPE: %v\n", "QUERY_EVENT")
pos := 20
// threadId := data[20:24] // 4 bytes.
// createTime := data[24:28] // 4 bytes.
pos += 8
schemaLen := data[pos : pos+1] // 1 byte.
// errorCode := data[29:31] // 2 bytes.
pos += 3
staVarLen := data[pos : pos+2] // 2 bytes (not present in v1, v3).
pos += 2
//statusVar := data[33 : 33+Byte2Int(staVarLen)]
database := data[pos+utils.Byte2Int(staVarLen) : pos+utils.Byte2Int(staVarLen)+utils.Byte2Int(schemaLen)]
query := data[pos+utils.Byte2Int(staVarLen)+utils.Byte2Int(schemaLen)+1:]
schema = string(database)
sql = string(query)
fmt.Printf("SCHEMA: %v\n", schema)
fmt.Printf("SQL: %v\n", sql)
解析 QUERY_EVENT,次要获取波及到的 DDL 等语句。
3.5.3 解析 TABLE_MAP_EVENT
对 TABLE_MAP_EVENT 进行解析,获取操作的 schema、table。
MySQL 在 binlog 中的 TABLE_MAP_EVENT 记录了操作波及的 schema 和 table 名,但未记录表的 column 信息。
在失常的主从同步中,这些表的 column 信息是能够从 slave 的数据字典中查问到的。然而程序模仿的 slave 则须要通过 schema 和 table 名查问 Master 来获取这些信息。
当然为了防止每次解析都要查问 Master,也能够对其进行缓存。为了缩小程序复杂度,本文没有连贯 Master 获取 column 信息。
TABLE_MAP_EVENT 的 Payload 信息在手册中形容:
https://dev.mysql.com/doc/int…
post-header:
if post_header_len == 6 {4 table id} else {6 table id}
2 flags
payload:
1 schema name length
string schema name
1 [00]
1 table name length
string table name
1 [00]
lenenc-int column-count
string.var_len [length=$column-count] column-def
lenenc-str column-meta-def
n NULL-bitmask, length: (column-count + 8) / 7
依据形容咱们解析第 28 个字节获取 schema 名称长度(schema name length),在而后依据长度解析出 schema 名称。
依据手册再顺延程序向后,用雷同的形式解析出 table 名称,并赋值给全局变量。
代码参考如下:
case mysql.TABLE_MAP_EVENT:
fmt.Printf("EVENT TYPE: %v\n", "TABLE_MAP_EVENT")
pos := 20
// tableId := data[20:26]
pos += 6
pos += 2
schemaNameLen := data[pos : pos+1]
pos += 1 // 1 byte schema name length
schema = string(data[pos : pos+utils.Byte2Int(schemaNameLen)])
pos += utils.Byte2Int(schemaNameLen)
pos += 1 // 0x00 skip 1 byte
tableNameLen := data[pos : pos+1]
pos += 1 // 1 byte table name length
table = string(data[pos : pos+utils.Byte2Int(tableNameLen)])
pos += utils.Byte2Int(tableNameLen) // table name
pos += 1 // 0x00 skip 1 byte
columnCount := utils.Byte2Int(data[pos : pos+1])
pos += 1 // column-count
colType := data[pos : pos+columnCount]
pos += columnCount
// var n int
var err error
var metaData []byte
if metaData, _, _, err = GetColumnMetaArray(data[pos:]); err != nil {return err}
colMeta, err := GetMeta(metaData, columnCount, colType)
if err != nil {return err}
ColumnType = colType
ColumnMeta = colMeta
schema = string(schema)
table = string(table)
fmt.Printf("SCHEMA: %v\n", string(schema))
fmt.Printf("TABLE: %v\n", string(table))
fmt.Printf("columnCount: %v\n", columnCount)
fmt.Println("-----------------")
for i := 0; i < len(colType); i++ {fmt.Printf("ColumnType: %v\n", colType[i])
}
fmt.Println("-----------------")
fmt.Println("&&&&&&&&&&&&&&&&&")
for i := 0; i < len(colMeta); i++ {fmt.Printf("ColumnMeta: %v\n", colMeta[i])
}
fmt.Println("&&&&&&&&&&&&&&&&&")
fmt.Printf("TABLE: %v\n", string(table))
对 TABLE_MAP_EVENT 解析次要是获取事务波及的 schema 和 table 信息。
3.5.4 解析 ROWS_EVENT
对 ROWS_EVENT 的三个 event(
WRITE_ROWS_EVENT_V2 /
UPDATE_ROWS_EVENT_V2 /
DELETE_ROWS_EVENT_V2)进行解析。
手册对 event 形容如下:
https://dev.mysql.com/doc/int…
header:
if post_header_len == 6 {4 table id} else {6 table id}
2 flags
if version == 2 {
2 extra-data-length
string.var_len extra-data
}
body:
lenenc_int number of columns
string.var_len columns-present-bitmap1, length: (num of columns+7)/8
if UPDATE_ROWS_EVENTv1 or v2 {string.var_len columns-present-bitmap2, length: (num of columns+7)/8
}
rows:
string.var_len nul-bitmap, length (bits set in 'columns-present-bitmap1'+7)/8
string.var_len value of each field as defined in table-map
if UPDATE_ROWS_EVENTv1 or v2 {string.var_len nul-bitmap, length (bits set in 'columns-present-bitmap2'+7)/8
string.var_len value of each field as defined in table-map
}
... repeat rows until event-end
ROWS_EVENT 的三种 event,其 packet 的格局是雷同的。第 20 到第 26 字节存储的 table id,后续 4 字节与本文解析日志内容关联不大,跳过。
第 30 到 31 字节存储表中列的数量。其后存储的是 SQL 语句用到的列,是应用 bitmap 来存储的。
在 UPDATE_ROWS_EVENT_V2 中,存储了更新前后的列的数据,所以应用两个 bitmap。bitmap 长度依照手册形容为:
length: (num of columns+7)/8
列的数量和列是否用到 bitmap 组成了 event 的 body 局部,前面就是 event 真正存储的数据局部 event data 了,如果是多行,则循环排列,直至 event 完结。
注:这里代码中没有思考列应用 unsigned 状况,在理论利用场景中这须要思考。
在 main 函数的对 event type 的 switch 判断中,减少如下 case:
case mysql.WRITE_ROWS_EVENT_V2, mysql.UPDATE_ROWS_EVENT_V2, mysql.DELETE_ROWS_EVENT_V2:
switch eventType {
case mysql.WRITE_ROWS_EVENT_V2:
fmt.Printf("EVENT TYPE: %v\n", "WRITE_ROWS_EVENT_V2")
case mysql.UPDATE_ROWS_EVENT_V2:
fmt.Printf("EVENT TYPE: %v\n", "UPDATE_ROWS_EVENT_V2")
case mysql.DELETE_ROWS_EVENT_V2:
fmt.Printf("EVENT TYPE: %v\n", "DELETE_ROWS_EVENT_V2")
}
pos := 20
// tableId := data[pos : pos+6] // 6 bytes
pos += 6
// flags := data[pos:pos+2] // 2 byte
pos += 2
extraDataLen := data[pos : pos+2] // 2 bytes
pos += utils.Byte2Int(extraDataLen)
columnLen := data[pos : pos+1] // 1 byte
fmt.Printf("columnLen: %v\n", columnLen)
pos += 1
colPreBitmap1 := data[pos : pos+(utils.Byte2Int(columnLen)+7)/8] // columns-present-bitmap1, length: (num of columns+7)/8
pos += (utils.Byte2Int(columnLen) + 7) / 8
var colPreBitmap2 []byte
if eventType == mysql.UPDATE_ROWS_EVENT_V2 {colPreBitmap2 = data[pos : pos+(utils.Byte2Int(columnLen)+7)/8] // columns-present-bitmap2, length: (num of columns+7)/8
pos += (utils.Byte2Int(columnLen) + 7) / 8
}
var rows1 [][]interface{}
var rows2 [][]interface{}
for pos < len(data) {
// repeat rows until event-end
rows1Re, n1, err := GetRows(rows1, data[pos:], utils.Byte2Int(columnLen), ColumnType, colPreBitmap1, ColumnMeta)
if err != nil {return err}
pos += n1
for i := 0; i < len(rows1Re); i++ {fmt.Printf("ColumnMeta: %v\n", rows1Re[i])
}
if len(colPreBitmap2) > 0 {rows2Re, n2, err := GetRows(rows2, data[pos:], utils.Byte2Int(columnLen), ColumnType, colPreBitmap2, ColumnMeta)
if err != nil {return err}
pos += n2
for i := 0; i < len(rows2Re); i++ {fmt.Printf("ColumnMeta: %v\n", rows2Re[i])
}
}
}
代码中,for pos < len(data)局部就是在解析 event body 后对真正存储的行数据进行循环解析。
解析是调用 GetRows()函数进行的,在 GetRows 办法中,对不同数据类型,因占用不同字节长度,以及局部类型应用了压缩算法,采纳了不同的解码形式。
解码这部分代码繁冗,干燥,本文篇幅所限就不介绍了,有趣味可参考手册以及其余开源工具,例如阿里开源的 canal 等
这里对解析后果间接进行了控制台输入,在理论利用中能够格式化为 json 或其余格局输入,以利于浏览或者利用。
3.6 测试程序
在数据库中执行简略 sql:
mysql> use wl
Database changed
mysql> update name set first='202106171325' where id = 48;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
控制台输入:
TABLE: name
EVENT TYPE: UPDATE_ROWS_EVENT_V2
columnLen: [3]
ColumnMeta: [48 20210617 <nil>]
ColumnMeta: [48 202106171325 <nil>]
EVENT TYPE: XID_EVENT
能够看到 UPDATE_ROWS_EVENT_V2 记录了更改前后的数据值,列值为空时,记录为 <nil>
至此,对 binlog 的简略解析就完结了。
后记
代码中,对字符串类型转换,通信 packet 的封装,字节解码别离参考了:
https://github.com/siddontang…
https://github.com/flike/king…
https://github.com/alibaba/canal
感激开源社区!
Enjoy GreatSQL :)
文章举荐:
GreatSQL MGR FAQ
https://mp.weixin.qq.com/s/J6…
万答 #12,MGR 整个集群挂掉后,如何能力主动选主,不必手动干涉
https://mp.weixin.qq.com/s/07…
『2021 数据技术嘉年华·ON LINE』:《MySQL 高可用架构演进及实际》
https://mp.weixin.qq.com/s/u7…
一条 sql 语句慢在哪之抓包剖析
https://mp.weixin.qq.com/s/AY…
万答 #15,都有哪些状况可能导致 MGR 服务无奈启动
https://mp.weixin.qq.com/s/in…
技术分享 | 为什么 MGR 一致性模式不举荐 AFTER
https://mp.weixin.qq.com/s/rN…
对于 GreatSQL
GreatSQL 是由万里数据库保护的 MySQL 分支,专一于晋升 MGR 可靠性及性能,反对 InnoDB 并行查问个性,是实用于金融级利用的 MySQL 分支版本。
Gitee:
https://gitee.com/GreatSQL/Gr…
GitHub:
https://github.com/GreatSQL/G…
Bilibili:
https://space.bilibili.com/13…
微信 &QQ 群:
可搜寻增加 GreatSQL 社区助手微信好友,发送验证信息“加群”退出 GreatSQL/MGR 交换微信群
QQ 群:533341697
微信小助手:wanlidbc
本文由博客一文多发平台 OpenWrite 公布!