1.引言
随着技术国产化的趋势不断加强,许多大型客户对于数据库的选择也开始倾向于国产产品,例如达梦,以响应国家的号召和政策法规。
在这种背景下,SAAS企业就需要及时将自己的产品系统与国产数据库作对接,以加强其在大客户领域的竞争力。
本文就以MySQL对接国产达梦数据库为例,讨论一种可能的适配方案。
2.构思
2.1 问题
信创数据库对接主要涉及到两类问题:
- DDL建表语句适配以及存量数据的迁移,这一部分属于一次性工作,一般由DBA负责,本文暂时不作讨论;
- 现有业务服务中用到的DML增删改查语句对新数据库的适配;
DML语句的适配也可以简单分为两类问题:
-
简单用字符串替换能解决的
- 不支持反引号:`,需要替换成双引号:"
- 不支持group_concat函数,需要使用达梦中的wm_concat函数代替
-
语句结构改变需要手动调整SQL的,例如:
- on duplicate key update
- replace into
- case when then else
公司基于MySQL的业务服务有50~60个,手动修改所有服务的代码去适配另一个数据库,不太现实,后期维护也是一个问题,于是就想能不能找一个统一的地方来处理这个适配工作。
2.2 思路
在尽量少侵入业务的前提下,有两种处理办法:
a. 在orm层作语法替换,让每个服务集成定制的orm库;
- 优点:难度小,只需要关心SQL语法替换,后端DB的连接和驱动不涉及;
- 缺点:
- 40多个服务都需要集成定制的ORM库,每次orm库变动所有服务都需要重新出包;
- 只能适用于golang, java、php以及其它语言的服务需要重新考虑方案;
b. 写一个中间件,来代理发给mysql的所有请求,在中间件上完成适配工作。这个中间件需要承担如下角色:
- 服务器:需要接收Mysql驱动的连接及数据包请求;
- 语法转换:完成异构数据库的语法替换;
- 代理:将CRUD的所有操作转发到后端的dm数据库;
- 协议转换:需要将后端dm返回的数据再封装成mysql协议数据包发给Mysql驱动;
- 需要支持一个中间件能代理多个数据库;
由于中间件对整个系统的侵入最小,所以我们优先尝试走中间件的方案。
3.设计
调研过多个开源项目,能符合我们需求的有go-mysql-server、 vitess、kingshard, 代码了解下来,vitess过于庞大,go-mysql-server代码不够直观且mysql协议部分依赖其它项目,最后我们选择了基于kingshard作二次开发。
3.1 中间件设计
中间件整体采用从上到下的分层依赖结构,几个核心包职责如下:
- server包负责实现一个mysql服务器,代理mysql的连接建立和数据包协议
- backend包负责代理与后端dm数据库的操作交互,采用database/sql(标准接口)+ 数据库驱动来与dm数据库进行交互
- sqlparser包负责sql语法的解析和替换
- mysql包负责将后端dm返回的数据重新封装成mysql协议的数据包;
3.2 server
server部分是基于kingshard的server模块作的二次修改,整体保留了kingshard的函数结构和代码执行流程。
- 先解析后端DB配置,每个DB建立一个BackendProxy代理实例,本质上就是调用sql.Open来创建一个sql.DB实例;
nodes :
-
# db alias name
name : uc_uniform
# db driver name
driver_name: dm
# default max conns for connection pool
max_conns_limit : 32
# master represents a real mysql master server
datasource : dm://uc_uniform/uc_uniform@192.168.23.216:5236?socketTimeout=10000
- 监听9696端口,每accept一个连接,就走onConn开启一个goroutine来处理此连接上的请求,包括用户名/密码验证、mysql协议握手;
- 启动连接的主循环,接收数据包,并解析指令,调用对应的方法来处理指令;
【mysql协议请求的数据包格式示意】
类型 | 字段 | 含义 |
---|---|---|
int<1> | COMMAND | 0x03:COM_QUERY |
string | query | SQL语句 |
【mysql常用指令及对应的处理】
命令 | 含义 | 处理函数 | 是否需要与数据库交互 |
---|---|---|---|
COM_INIT_DB | 连接建立时指定DB | handleInitDB() | 中间件处理 |
COM_PING | 心跳包 | handlePing() | 中间件处理 |
COM_QUERY | DDL/DML语句 | handleQuery() | 后端数据库处理 |
COM_STMT_PREPARE | 预编译SQL | handleStmtPrepare() | 中间件处理 |
COM_STMT_EXECUTE | 发送参数执行预编译SQL | handleStmtExecute() | 后端数据库处理 |
COM_STMT_CLOSE | 关闭预编译SQL | handleStmtClose() | 中间件处理 |
COM_QUIT | 连接关闭 | handleQuit() | 中间件处理 |
3.3 backend
基于database/sql的API定义了一套用于SQL处理的标准插件接口:
// db querier
type dbQuerier interface {
Prepare(query string) (*sql.Stmt, error)
Exec(query string, args ...interface{}) (sql.Result, error)
Query(query string, args ...interface{}) (*sql.Rows, error)
QueryRow(query string, args ...interface{}) *sql.Row
}
// transaction beginner
type txer interface {
Begin() (*sql.Tx, error)
}
// transaction ending
type txEnder interface {
Commit() error
Rollback() error
}
type SQLPlugin interface {
dbQuerier
txer
txEnder
}
只要实现这套接口,就可以当作一个插件灵活插入到SQL的处理流程中执行,例如:
- sql日志
- 语法替换
如果不需要了,也可以随时把它移除,对SQL整体处理没有影响。通过此插件设计实现了代码的分层和后续业务灵活扩展:
![](https://i-blog.csdnimg.cn/blog_migrate/55e63cb20c833108df1376b525224cb0.png)
3.4 sqlparser
sqlparser是SQL语法解析的开源项目,我们的改动主要是加了一个SQL语法的转换功能,并定义了一个标准转换接口:
type SQLConverter interface {
Convert(sql string) (string, error)
}
目前只实现了Mysql-to-Oracle的语法转换,转换流程简单示意如下:
- Parse(): 解析sql语法,将一条完整的语句解析成一个SQL语法树对象;
- convertStmt(): 将mysql语法的AST树转换成oracle语法的AST树;
- Format: 将oracle语法树对象再格式化成标准SQL语句;
![](https://i-blog.csdnimg.cn/blog_migrate/82479a88d3accdf6cfe3961600bd346a.png)
以开篇的SQL语句为例,我们示意下转换前和转换后的语法树结构。
# 转换前带on duplicate key update冲突语法的语句
`insert into webcal_live_info(cal_id,channelId,pullurl,password,extraInfo) values(634311,131722,'https://rlive1uat.rmeet.com.cn/activity/geeZWo3','','') on duplicate key update pullurl='https://rlive1uat.rmeet.com.cn/activity/geeZWo3', password='', extraInfo=''`
# 转换后的merge into语句
`merge into webcal_live_info as t using dual on t.cal_id = 634311 and t.channelId = 131722 when matched then update set t.pullurl = 'https://rlive1uat.rmeet.com.cn/activity/geeZWo3', t.password = '', t.extraInfo = '' when not matched then insert (cal_id, channelId, pullurl, password, extraInfo) values (634311, 131722, 'https://rlive1uat.rmeet.com.cn/activity/geeZWo3', '', '')`
Insert语法树示意:
merge语法树示意:
转换语法要做的工作就是从原语法树中提取出table、column、data信息,重新组装成一棵符合oracle语法的AST树。
- merge into是以多表合并数据的方式来解决单表插入或更新的问题。
- 单表变成多表语句后,每个字段需要加表的前缀,通过一个访问者模式来实现对指定信息的递归遍历
visit := func(node SQLNode) (kcontinue bool, err error) {
switch node.(type) {
case *ColName:
node.(*ColName).Qualifier = TableName{
Name: NewTableIdent("t"),
}
return true, nil
default:
return true, nil
}
}
Walk(visit, node)
3.5 mysql
mysql包是kingshard自带的mysql数据包读写协议封装,我们的改动主要是添加了对列数据封装成行数据的支持。之所以要做这个工作,主要是database/sql结果集与mysql结果集存在一定的差异。
【结构体对比示意】
差异点:
- database/sql中的Rows.Scan()方法只能按列来读取一条数据
- mysql.Resultset中的RowDatas需要的是以行为单位的二进制数据。
mysql包已经提供了对mysql.Result结构体的协议封装,我们需要做的就是把sql.Result或sql.Rows转换成mysql.Result。大概分为两步:
- 调用Rows.Scan()读取列数据,为减少对数据的操作,我们直接用database/sql中定义的二进制类型
sql.RawBytes
来接收列数据。 - 也是最核心的点,将database/sql中打散的列重组成一个行协议数据包。
将列封装成行的协议规则:
- NULL的列都按0xFB发送
- 其它非NULL列都转换为string,并按string发送。
func packetTextRowData(row []sql.RawBytes) (RowData, error) {
length := 0
for _, val := range row {
if val == nil {
length++
} else {
l := len(val)
length += LenEncIntSize(uint64(l)) + l
}
}
data := make([]byte, 0, length)
for _, val := range row {
if val == nil {
data = append(data, NullValue)
} else {
data = append(data, PutLengthEncodedString(val)...)
}
}
if len(data) != length {
return nil, fmt.Errorf("packet row: got %v bytes but expected %v", len(data), length)
}
return RowData(data), nil
}
关于MySQL数据传输协议,请参考文末附的MySQL协议文档链接。
小结
本文从信创数据库对接给现有业务系统带来的冲击角度出发,介绍了一种基于异构DB中间件来简化适配工作量的方法,并给出了中间件的整体设计构思。
中间件首先基于开源kingshard的server包改造了一个MySQL服务端,然后基于sqlparser开源项目完成了SQL语法的解析和转换,最后基于一篇文章Mysql协议介绍来实现了最后的MySQL响应数据包协议封装,一个能够自动完成MySQL和达梦语法协议转换的中间件就构建起来了。
这个中间件虽然是以MySQL和达梦的语法转换为案例来构造,但它通过对语法转换模型作抽象接口提取,理论上可以自由扩展对其它SQL型数据库的支持,详情见3.3节Backend包
。
关于sqlproxy的设计,暂时就介绍这么多,如果有兴趣了解更多,可以查看项目源码,下面参考阅读里有附源码链接。
参考阅读
- 项目源码:https://github.com/golfxiao/sqlproxy
- Mysql协议介绍:https://zhuanlan.zhihu.com/p/144657586
- kingshard: https://github.com/flike/kingshard
- vitess: https://github.com/vitessio/vitess
- zorm: https://gitee.com/chunanyong/zorm
- Mysql与Oracle数据类型对照表:https://blog.csdn.net/huangyinzhao/article/details/80739291