企业华为TaurusDB数据库国产替代要点全记录

113 阅读13分钟

 背景

国产化浪潮方兴未艾,为了避免被卡脖子。我们内部的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还是有很大差距的。

规约表关联查询单条数据查询精确少量多条数据查询列表查询大批量更新数据数据分析
互联网企业1. 禁止使用业务库(OLTP)关联 2. 使用OLAP进行关联 3. 或者拆分多个SQL,多次查询,JAVA代码汇总主业务库查询(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,注意,一定要在事务外就确认走新的还是老的,避免有事务问题。

夜深了。后续我在完善