背景
国产化浪潮方兴未艾,为了避免被卡脖子。我们内部的SQLserver数据库也要下线。因为运维能力有限,优先选云厂商的产品,于是做了以下对比。
数据库对比
我们从成本、无运维化、sql兼容性、性能、索引依赖性、查询优化器、数据存储引擎、表关联性能、字段类型、索引、亿级数据查询能力、事务全方位对比下来看。
再考虑到我们的服务器也在华为云,因而敲定了TaurusDB(原GaussDB For Mysql)
|
数据库 |
SQL Server 兼容性 |
Oracle 兼容性 |
性能 |
索引依赖性 |
查询优化器 |
数据存储引擎 |
表关联性能 |
字段类型 |
索引 |
亿级数据查询性能 |
事务处理 |
SQL Server 数据库双向同步到 MySQL 的难度 |
Oracle 数据库双向同步到 MySQL 的难度 |
成本 |
|
阿里云数据库 |
— |
— |
— |
— |
— |
— |
— |
— |
— |
— |
— |
— |
— |
|
|
PolarDB |
高度兼容 |
高度兼容 |
高 |
中等 |
复杂查询优化能力较好 |
行存储与索引分离 |
支持复杂关联查询 |
支持常见类型 |
支持多种索引类型 |
较好 |
高并发性能较好 |
中等难度,需处理数据类型和事务差异 |
中等难度,需处理数据类型和事务差异 |
中等,按需付费模式 |
|
PolarDB-X |
兼容部分特性 |
兼容部分特性 |
高 |
高 |
基于分布式架构的优化策略,复杂查询优化能力较好 |
采用分布式存储架构 |
分布式场景下关联查询需合理分片和优化 |
与 MySQL 类似 |
支持常见索引 |
较好 |
高并发性能较好 |
中等难度,需处理分布式架构差异 |
中等难度,需处理分布式架构差异 |
中等,按需付费模式 |
|
AnalyticDB |
不兼容 |
不兼容 |
高 |
中等 |
复杂查询优化能力较好 |
列存储与行存储结合 |
优化复杂关联查询 |
支持常见类型 |
支持多种索引 |
较好 |
适用于大数据量分析场景 |
高难度,需处理分布式架构差异 |
高难度,需处理分布式架构差异 |
较高,按需付费模式 |
|
华为云数据库 |
— |
— |
— |
— |
— |
— |
— |
— |
— |
— |
— |
— |
— |
|
|
GaussDB |
兼容部分特性 |
支持 Oracle 协议 |
超高 |
中等 |
复杂查询优化能力较好 |
行存储与索引分离 |
支持复杂关联查询 |
支持常见类型 |
支持多种索引 |
超好 |
高并发性能较好 |
中等难度,需处理数据类型和事务差异 |
中等难度,需处理数据类型和事务差异 |
较高,按需付费模式 |
|
TaurusDB(原GaussDB For Mysql) |
不兼容 |
不兼容 |
高 |
高 |
简单,复杂查询优化不足 |
与 MySQL 存储引擎类似 |
分布式关联查询性能较好 |
与 MySQL 类似 |
支持常见索引类型 |
较好 |
中等 |
中等难度,需处理数据类型和事务差异 |
中等难度,需处理数据类型和事务差异 |
中等,按需付费模式 |
|
蚂蚁数据库 |
||||||||||||||
|
OceanBase |
一般,需进行语法转换 |
高 |
高 |
中等 |
复杂查询优化能力较好 |
采用分布式存储架构 |
较好 |
丰富 |
支持 BTree 索引等功能 |
较好 |
高并发性能较好 |
中等难度,需处理分布式架构差异 |
中等难度,需处理分布式架构差异 |
较高,按需付费模式 |
|
其他数据库 |
||||||||||||||
|
达梦数据库 |
部分兼容 |
部分兼容 |
中等 |
中等 |
简单,复杂查询优化不足 |
行存储与索引分离 |
一般 |
支持常见类型 |
支持 B+ 树等常见索引 |
一般 |
中等 |
中等难度,需处理数据类型和事务差异 |
高难度,需处理数据类型和事务差异 |
中等,授权费用相对较低 |
|
瀚高数据库 |
兼容性一般 |
兼容性一般 |
中等 |
中等 |
简单,复杂查询优化不足 |
行存储与索引分离 |
一般 |
常见类型齐全 |
支持 B 树、哈希等索引 |
一般 |
中等 |
中等难度,需处理数据类型和事务差异 |
高难度,需处理数据类型和事务差异 |
中等,授权费用相对较低 |
|
PingCAP TiDB |
不兼容 |
不兼容 |
高 |
高 |
基于分布式架构的优化策略,复杂查询优化能力较好 |
采用分布式存储架构 |
分布式关联查询性能有待提高 |
与 MySQL 类似 |
支持 B 树、哈希等索引 |
较好 |
高并发性能较好 |
中等难度,需处理分布式架构差异 |
中等难度,需处理分布式架构差异 |
开源免费,但大规模部署成本较高 |
|
星环科技 KunDB |
高度兼容 |
高度兼容 |
中等偏上 |
中等 |
复杂查询优化能力较好 |
行存储与索引分离 |
支持多表关联 |
支持常见类型 |
支持 B+ 树等索引 |
一般 |
中等偏上 |
中等难度,需处理数据类型和事务差异 |
中等难度,需处理数据类型和事务差异 |
较高,授权费用较高 |
|
MyCat |
不兼容 |
不兼容 |
中等 |
高 |
简单,复杂查询优化不足 |
依赖后端数据库存储引擎 |
分布式关联查询需合理设计 |
与 MySQL 相同 |
依赖后端数据库索引类型 |
一般 |
中等 |
高难度,需处理分布式架构差异 |
高难度,需处理分布式架构差异 |
开源免费,但运维成本较高 |
难点
语法改造
敲定了使用TaurusDB后,问题就来了。sqlserver 的语法基本上就废了。如何把SQL-SERVER语句转成mysql可以使用的语句呢?
当时我们这里有三种不同的意见
使用大模型进行转换
批量写代码替换
全局批量replace
因为当时大模型还是有很多问题,大模型的方案没有采用。批量写代码替换也存在风险。
所以当时使用了较笨的方法,全局替换函数及手动修改部分无法直接替换的内容。
然后写了一个批量测试sql的工具类,全量跑sql验证,但是我们很多dao 入参都是Map,反射基本上没有用。于是写了一个比较复杂的工具类。
工具类代码
package com.yunduo.mybatis.test;
import cn.hutool.core.exceptions.ExceptionUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import sun.reflect.generics.reflectiveObjects.TypeVariableImpl;
import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
@Slf4j
@Service
public class MybatisTest {
@Autowired
private ApplicationContext context;
@PostConstruct
public void init() {
System.out.println(context);
}
public void testAllDAOs() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Map<String, Object> daos = context.getBeansWithAnnotation(Mapper.class);
List<String> stringList = new ArrayList<>();
for (Map.Entry<String, Object> stringObjectEntry : daos.entrySet()) {
Object dao = stringObjectEntry.getValue();
if (!stringObjectEntry.getKey().toLowerCase().contains("gauss")) {
continue;
}
Class<?> daoClass = dao.getClass();
String daoName = null;
try {
daoName = dao.getClass().getInterfaces()[0].getName();
} catch (Exception e) {
log.error("daoname fail", e);
}
System.out.println("Testing DAO: " + daoClass.getSimpleName());
// Method[] methods = daoClass.getMethods();
Method[] methods = dao.getClass().getInterfaces()[0].getMethods();
Type leixing = null;
Type[] genericInterfaces = dao.getClass().getInterfaces()[0].getGenericInterfaces();
for (Type genericInterface : genericInterfaces) {
if (genericInterface instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) genericInterface;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
leixing = actualTypeArgument;
}
}
}
for (Method method : methods) {
if (Lists.newArrayList("getClass",
"wait",
"isProxyClass",
"hashCode",
"getProxyClass",
"equals",
"newProxyInstance",
"notifyAll",
"toString",
"getInvocationHandler",
"notify").contains(method.getName())) {
continue;
}
// 获取方法的参数类型
Type[] parameterTypes = method.getGenericParameterTypes();
// 根据参数类型填充参数值
Object[] args = new Object[parameterTypes.length];
Set<Object> set = new HashSet<>();
for (int i = 0; i < parameterTypes.length; i++) {
Object a = null;
Object b = null;
Object c = null;
try {
a = parameterTypes[i];
b = leixing;
c = set;
if (a == null) {
System.out.println(JSON.toJSONString(a));
}
args[i] = getParameterValue(parameterTypes[i], leixing, set);
} catch (Throwable e) {
log.error("ccc", e);
log.error("cao " + JSON.toJSONString(a) + "/n" + JSON.toJSONString(b) + "/n" + JSON.toJSONString(c));
args[i] = null;
}
}
try {
System.out.println("Test start for method " + method.getName() + " in DAO " + daoName);
Object result = method.invoke(dao, args);
System.out.println(result);
System.out.println("Test passed for method " + method.getName() + " in DAO " + daoName);
} catch (Exception e) {
System.out.println("Test failed for method " + method.getName() + " in DAO " + daoName + ": " + e.getMessage());
// 可以根据实际情况记录测试失败的信息
log.error("cc", e);
try {
Throwable cce = null;
if (e.getCause() == null) {
cce = e;
} else {
cce = e.getCause();
}
Boolean x = cce.getMessage().contains("Invalid bound statement (not found):");
x = x && (cce.getMessage().endsWith("getByCode")
|| cce.getMessage().endsWith("findList")
|| cce.getMessage().endsWith("list")
|| cce.getMessage().endsWith("queryForPage")
|| cce.getMessage().endsWith("insert")
|| cce.getMessage().endsWith("delete")
|| cce.getMessage().endsWith("update")
|| cce.getMessage().endsWith("get")
);
if (!x && !cce.getMessage().contains("Duplicate entry")
&& !cce.getMessage().contains("selectOne(), but found:")
) {
stringList.add( method.getName() + "," + daoName + "," + ExceptionUtil.getRootCauseMessage(e).replace("\n", "\t"));
}
} catch (Exception t) {
log.error("xs", t);
}
}
}
System.out.println(daoName + "," + "---------------------------------------------");
}
String str = "";
if (CollectionUtils.isEmpty(stringList)) {
try {
FileUtils.write(new File("xxx"), "sql没有错误", false);
} catch (IOException e) {
throw new RuntimeException(e);
}
return;
}
for (String s : stringList) {
System.out.println(s);
str = str + "\n" + s;
}
try {
FileUtils.write(new File("xxx"), str, false);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 根据参数类型填充参数值的示例方法
private Object getParameterValue(Type parameterType, Type leixing, Set<Object> set) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
// Class parameterType = ((Class)parameterType1);
// 这里可以根据参数类型的不同来填充不同的参数值
if (parameterType == int.class || parameterType == Integer.class) {
return 1; // 假设整型参数的默认值为0
} else if (parameterType == long.class || parameterType == Long.class) {
return 2L; // 假设整型参数的默认值为0
} else if (parameterType == double.class || parameterType == Double.class) {
return 2.2d; // 假设整型参数的默认值为0
} else if (parameterType == String.class) {
return "1"; // 假设字符串参数的默认值为"default"
} else if (parameterType == byte[].class) {
return new byte[] {1}; // 假设字符串参数的默认值为"default"
} else if (parameterType instanceof ParameterizedType) {
Object temp = null;
ParameterizedType parameterizedType = (ParameterizedType) parameterType;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
Type type = ((ParameterizedType) parameterType).getRawType();
if (type instanceof List || type.getTypeName().equals("java.util.List")) {
if (actualTypeArguments != null && actualTypeArguments.length > 0) {
if (actualTypeArguments[0] == null) {
System.out.println();
}
temp = getParameterValue(actualTypeArguments[0], leixing, set);
}
if (((ParameterizedType) parameterType).getRawType() instanceof Collection) {
return new ArrayList<>(); // 假设字符串参数的默认值为"default"
}
return Lists.newArrayList(temp); // 假设字符串参数的默认值为"default"
}
if (type instanceof Map || type.getTypeName().equals("java.util.Map")) {
if (actualTypeArguments != null && actualTypeArguments.length > 1) {
Object key = getParameterValue(actualTypeArguments[0], leixing, set);
try {
Object value = getParameterValue(actualTypeArguments[1], leixing, set);
} catch (Exception e) {
log.error("bbb " + JSON.toJSONString(actualTypeArguments[1] + JSON.toJSONString(leixing) + JSON.toJSONString(set)), e);
}
Object map = getParameterValue(Map.class, leixing, set);
return map; // 假设字符串参数的默认值为"default"
}
}
if (actualTypeArguments != null && actualTypeArguments.length > 0) {
temp = getParameterValue(actualTypeArguments[0], leixing, set);
}
return temp; // 假设字符串参数的默认值为"default"
} else if (parameterType == String[].class) {
return new String[] {"default"}; // 假设字符串参数的默认值为"default"
} else if (parameterType == JSONArray.class) {
return new JSONArray(); // 假设字符串参数的默认值为"default"
} else if (parameterType == JSONObject.class) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("id", getUuid());
return jsonObject; // 假设字符串参数的默认值为"default"
} else if (parameterType == Boolean.class) {
return true; // 假设字符串参数的默认值为"default"
} else if (parameterType == boolean.class) {
return true; // 假设字符串参数的默认值为"default"
} else if (parameterType == List.class) {
if (parameterType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) parameterType;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
if (actualTypeArguments != null && actualTypeArguments.length > 0) {
Object temp = getParameterValue(actualTypeArguments[0], leixing, set);
return Lists.newArrayList(temp); // 假设字符串参数的默认值为"default"
}
}
return new ArrayList<>();
} else if (parameterType == LocalDateTime.class) {
// 创建 LocalDate 实例
java.time.LocalDate date = LocalDate.now(); // 当前日期
// 创建 LocalTime 实例
java.time.LocalTime time = java.time.LocalTime.now(); // 当前时间
// 使用 LocalDateTime.of 方法组合日期和时间
LocalDateTime dateTime = LocalDateTime.of(date, time);
return dateTime; // 假设字符串参数的默认值为"default"
} else if (parameterType instanceof Map
|| (parameterType instanceof Class && ((Class<?>) parameterType).getTypeName().equals("java.util.Map"))
|| (parameterType instanceof Class && ((Class<?>) parameterType).newInstance() instanceof Map)) {
Map<String, Object> map = new HashMap<>();
Map<String, Object> t = new HashMap<>();
t.put("pageSize", 1);
t.put("pageIndex", 1);
t.put("firstResult", 1);
map.put("o", t);
map.put("user", t);
map.put("page", t);
map.put("pageSize", 1);
map.put("rowStart", 1);
map.put("rowStart2", 0);
return map; // 假设字符串参数的默认值为"default"
} else if (parameterType == Date.class) {
return new Date(); // 假设字符串参数的默认值为"default"
} else if (parameterType instanceof TypeVariableImpl) {
if (leixing != null) {
Object temp = getParameterValue(leixing, null, set);
return temp;
} else {
System.out.println();
}
Map<String, Object> map = new HashMap<>();
return map; // 假设字符串参数的默认值为"default"
} else {
Object obj = null;
try {
if (set.contains(parameterType.getTypeName())) {
return obj;
}
} catch (Exception e) {
log.error("cao1 " + JSON.toJSONString(set) + JSON.toJSONString(parameterType), e);
throw e;
}
if (!parameterType.getTypeName().equals("java.lang.Object")) {
set.add(parameterType.getTypeName());
}
try {
if (parameterType instanceof Class) {
obj = ((Class)parameterType).newInstance();
} else {
obj = parameterType.getClass().newInstance();
}
if (obj.getClass().getDeclaredFields().length == 0) {
if (leixing != null) {
obj = getParameterValue(leixing, null, set);
} else {
obj = new Object();
}
}
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (Exception e) {
log.error(leixing + JSON.toJSONString(set) + JSON.toJSONString(parameterType));
throw new RuntimeException(e);
}
if (obj != null
&& obj.getClass() != null
&& obj.getClass().getDeclaredFields() != null
&& obj.getClass().getDeclaredFields().length > 0) {
for (Field field : obj.getClass().getDeclaredFields()) {
field.setAccessible(true);
if (field.getName().equals("serialVersionUID")) {
continue;
}
Object temp = null;
if (field.getName().equals("sortField")) {
// temp = "id";
temp = "";
}
if (field.getName().equals("sortOrder")) {
// temp = "desc";
temp = "";
}
if (field.getGenericType() == String.class) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = sdf.format(new Date());
if (field.getName().toLowerCase().contains("date")) {
temp = date;
} else if (field.getName().toLowerCase().contains("time")) {
temp = date;
} else {
}
}
if (temp == null) {
temp = (field.getGenericType() == String.class && field.getName().toLowerCase().equals("id")) ? getUuid(): getParameterValue(field.getGenericType(), leixing, set);
}
try {
field.set(obj, temp);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
if (obj != null && obj.getClass() != null) {
Type genericSuperclass = obj.getClass().getGenericSuperclass();
if (genericSuperclass != null && obj.getClass().getSuperclass().getDeclaredFields() != null) {
for (Field declaredField : obj.getClass().getSuperclass().getDeclaredFields()) {
declaredField.setAccessible(true);
if (declaredField.getName().equals("serialVersionUID")) {
continue;
}
Object temp = null;
if (declaredField.getName().equals("sortField")) {
// temp = "id";
temp = "";
}
if (declaredField.getName().equals("sortOrder")) {
// temp = "desc";
temp = "";
}
if (declaredField.getGenericType() == String.class) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = sdf.format(new Date());
if (declaredField.getName().toLowerCase().contains("date")) {
temp = date;
} else if (declaredField.getName().toLowerCase().contains("time")) {
temp = date;
} else {
}
}
if (temp == null) {
temp = declaredField.getName().toLowerCase().equals("id") ? getUuid() : getParameterValue(declaredField.getGenericType(), leixing, set);;
}
try {
declaredField.set(obj, temp);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
return obj;
} else {
System.out.println();
}
return obj;
}
}
private Integer getIntRom() {
Random random = new Random();
return random.nextInt();
}
private static String getUuid() {
UUID uuid = UUID.randomUUID();
String reUuid = uuid.toString();
reUuid = reUuid.replaceAll("-", "");
return reUuid;
}
}
多表关联代码改造:互联网和传统企业的IT规约差异
本人是从互联网转到传统企业的,对于数据库的使用互联网和传统企业是一点都不一样的。互联网用业务库用表关联较少。如果迁移数据库难度并不大。但是企业级问题就打了。TaurusDB在查询优化器这点比起SQL-SERVER还是有很大差距的。
|
规约 |
表关联查询 |
单条数据查询 |
精确少量多条数据查询 |
列表查询 |
大批量更新数据 |
数据分析 |
|
互联网企业 |
禁止使用业务库(OLTP)关联 |
主业务库查询(OLTP) |
主业务库查询(OLTP) |
1.OLAP查询(分钟级延迟)2.备业务库查询(OLTP)(秒级) |
1.尽量使用消息消费方式,按主键慢慢更新。 |
离线数据 |
|
传统toB企业 |
业务库直接表关联 |
主业务库查询(OLTP) |
主业务库查询(OLTP) |
主业务库查询(OLTP) |
一股脑update,非常容易锁表 |
备业务库查询(OLTP) |
代码改造伪代码
我们往往有10多张表关联的场景,这种场景,基本上一个sql数据库就挂了。所以采用拆分sql的方式
举例说明:分页数据查询
现根据条件从主表中分页查询出10条记录,通过代码把10条记录的code,拼装成一个list,然后用这个list作为in条件或exist条件去查另外的几张表。查询回来后,for循环查询后的记录,拼接到主对象中,然后统一返回
// 主表分页查询
int pageSize = 10;
int pageNum = 1; // 假设查询第一页
List<MainTable> mainRecords = mainTableRepository.findMainRecordsByPage(pageSize, pageNum);
// 提取主表记录中的 code 拼装成 list
List<String> codeList = new ArrayList<>();
for (MainTable mainRecord : mainRecords) {
codeList.add(mainRecord.getCode());
}
// 查询关联表记录(用 codeList 作为 in 条件)
List<AssociateTable1> associateRecords1 = associateTable1Repository.findByCodeIn(codeList);
List<AssociateTable2> associateRecords2 = associateTable2Repository.findByCodeIn(codeList);
// for 循环拼接关联表记录到主记录对象中
for (MainTable mainRecord : mainRecords) {
for (AssociateTable1 associateRecord1 : associateRecords1) {
if (mainRecord.getCode().equals(associateRecord1.getCode())) {
mainRecord.setAssociateData1(associateRecord1.getData());
break; // 找到对应的关联记录后可退出循环
}
}
for (AssociateTable2 associateRecord2 : associateRecords2) {
if (mainRecord.getCode().equals(associateRecord2.getCode())) {
mainRecord.setAssociateData2(associateRecord2.getData());
break; // 找到对应的关联记录后可退出循环
}
}
}
// 返回处理好的主表记录集合
return mainRecords;
异构数据双向同步
接下来就是终点了。SQL-SERVER和TaurusDB做数据双向同步。
这里没有很好的开源解决方案,我们当时是找了一个三方公司,他们有专门的异构数据库双向同步能力。
这里简单讲下我们所踩的坑
表结构同步
同步表结构两种方式
自动转成对应的类型,建表
建表失败,人工处理
同步索引结构
两种方式
自动生成,错误索引跳过
生成失败人工介入
默认值
默认值会有失败的场景,需要人工处理
数据库字符集
表结构同步时,会有自动的默认字符集处理,会导致和原来不一致
同步数据特殊处理
sqlserverrowversion字段类型特殊处理,不同步(此类型手动插入会导致插入失败)
主表、子表有外键场景下,子表先同步失败的话,会有延时retry
同步消息打标、避免循环消费
表结构同步过后,不会在进行同步了。都需要手动加
压测及上线
后续就是不断的多轮压测,优化写法,既然mysql的查询优化器不行,就只能自己优化了。
回滚方案
最终我们采用开关控制的方式,来控制是走新的dao还是老的dao,两者走不同的数据源和不同的sql,注意,一定要在事务外就确认走新的还是老的,避免有事务问题。
夜深了。后续我在完善

















暂无评论内容