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

背景

国产化浪潮方兴未艾,为了避免被卡脖子。我们内部的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)关联
使用OLAP进行关联
或者拆分多个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,注意,一定要在事务外就确认走新的还是老的,避免有事务问题。

夜深了。后续我在完善

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容