DataX-Mysql主键UUID类型切分主键

一、问题背景

之前公司在使用datax时,需要从rds同步数据到hive,但是数据库中的主键id是uuid类型的字符串,使用datax默认的字符串分隔方式,其实会有很大的问题,所以官方也不推荐使用。

二. 分析源码

跟踪底层源码最终可以定位到这个RangeSplitUtil类上

public static String[] doAsciiStringSplit(String left, String right, int expectSliceNumber) {
        int radix = 128;

        BigInteger[] tempResult = doBigIntegerSplit(stringToBigInteger(left, radix),
                stringToBigInteger(right, radix), expectSliceNumber);
        String[] result = new String[tempResult.length];

        //处理第一个字符串(因为:在转换为数字,再还原的时候,如果首字符刚好是 basic,则不知道应该添加多少个 basic)
        result[0] = left;
        result[tempResult.length - 1] = right;

        for (int i = 1, len = tempResult.length - 1; i < len; i++) {
            result[i] = bigIntegerToString(tempResult[i], radix);
        }

        return result;
}

    /**
     * 由于只支持 ascii 码对应字符,所以radix 范围为[1,128]
     */
    public static BigInteger stringToBigInteger(String aString, int radix) {
        if (null == aString) {
            throw new IllegalArgumentException("参数 bigInteger 不能为空.");
        }

        checkIfBetweenRange(radix, 1, 128);

        BigInteger result = BigInteger.ZERO;
        BigInteger radixBigInteger = BigInteger.valueOf(radix);

        int tempChar;
        int k = 0;

        for (int i = aString.length() - 1; i >= 0; i--) {
            tempChar = aString.charAt(i);
            if (tempChar >= 128) {
                throw new IllegalArgumentException(String.format("根据字符串进行切分时仅支持 ASCII 字符串,而字符串:[%s]非 ASCII 字符串.", aString));
            }
            result = result.add(BigInteger.valueOf(tempChar).multiply(radixBigInteger.pow(k)));
            k++;
        }

        return result;
    }

    /**
     * 把BigInteger 转换为 String.注意:radix 和 basic 范围都为[1,128], radix + basic 的范围也必须在[1,128].
     */
    private static String bigIntegerToString(BigInteger bigInteger, int radix) {
        if (null == bigInteger) {
            throw new IllegalArgumentException("参数 bigInteger 不能为空.");
        }

        checkIfBetweenRange(radix, 1, 128);

        StringBuilder resultStringBuilder = new StringBuilder();

        List<Integer> list = new ArrayList<Integer>();
        BigInteger radixBigInteger = BigInteger.valueOf(radix);
        BigInteger currentValue = bigInteger;

        BigInteger quotient = currentValue.divide(radixBigInteger);
        while (quotient.compareTo(BigInteger.ZERO) > 0) {
            list.add(currentValue.remainder(radixBigInteger).intValue());
            currentValue = currentValue.divide(radixBigInteger);
            quotient = currentValue;
        }
        Collections.reverse(list);

        if (list.isEmpty()) {
            list.add(0, bigInteger.remainder(radixBigInteger).intValue());
        }

        Map<Integer, Character> map = new HashMap<Integer, Character>();
        for (int i = 0; i < radix; i++) {
            map.put(i, (char) (i));
        }

//        String msg = String.format("%s 转为 %s 进制,结果为:%s", bigInteger.longValue(), radix, list);
//        System.out.println(msg);

        for (Integer aList : list) {
            resultStringBuilder.append(map.get(aList));
        }

        return resultStringBuilder.toString();
    }

看过源码之后其实可以很清楚的了解到它的思路

  1. stringToBigInteger方法:主键id最小值-left,主键id最大值-right,将字符串整体看做一个128进制数(根据ascii码位数),依次将每个字符的数值乘以对应位置n的128的n次方,累加求和。最终可以得到2个bigint值,将这两个当做区间的左右边界
  2. bigIntegerToString:根据需要切分的个数,求出步长 step =(right-left)/ splitnum , 求出切分点 (left + step + 1),并将切分点重新按照步骤1的方式,反向除128求出每位对应的acsii码对应的字符,拼接起来组成一个新的字符串
  3. 拼接sql,  切点1 <= splitkey < 切点2 and ...

 

但这里有一个很严重的问题,我们都知道UUID的字符串其实是只有字母,数字和中划线组成的,但是当你将一个UUID的字符串转成int时,加上一个步长,重新除128计算出来的ascii码并不是刚好落在字母,数字和中划线上,这就导致你计算出来的切割点的字符串id并不是一个标准的UUID字符串,而是一个乱码的字符串,如下所示:

可以自己尝试一下
doAsciiStringSplit("0122ed89-adb0-4599-84b7-89cfeb544637","8d89bb48-b290-4d92-a9af-80b3f8bfc036", 5)

返回的分隔点结果为:

0122ed89-adb0-4599-84b7-89cfeb544637

:eLVA[\u001E(-b('\u0006z\u001Ai\u000EtG\rN%@G\u00048sw(X4W\u001A\u000Bp\u0003

E\u0019fz\u001DR\u0004\u0017-bkk]G\u0001\u001Cd/`bghI`P8\u0004\u0007kN3y\u007Fa,O

ON\u0001\u001DyHj\u0006-c/04\u0013gP9jz8\u0001+Rz\u001C7\u0014\u0018.D3\u001Ce6i\u001A

Z\u0002\u001BAU?Ou-cru\n`N\u0004\u000F&\u0014\r\u001An\\\u0013h6$(q:2?K\f%e

8d89bb48-b290-4d92-a9af-80b3f8bfc036

这样的分隔点,使用mysql语句去做查询的结果肯定是有问题的。因为我们知道mysql的字符排序与其设置的字符排序集有关,但如果按照ascii码大小排序来说,他这个切分的结果也是不准确的, '0'  <  ':' < 'E' < 'O' < 'Z' > '8' ,实际在数据库排序集utf8mb4_general_ci下的结果是 ':' < '0' < '8' < 'E' < 'O' < 'Z',因此使用原生切分后的切点作为范围去查询,在这个例子中就会出现有些task没有数据,有些task分到了全部的数据 ;肯定也会存在部分切点范围有交集的情况,就会多读数据,存在重复数据。

三. 改造源码

其实datax本身的思路没有什么问题,就是没有考虑到这个ascii码反向转换后的问题,因此我基于这个思路,改造了一下他的源码。

主要想法:UUID其实去掉中划线之后,是一个32位的16进制数(主要由0-9,a-f构成);而数据库中0-9,a-z的排序结果是一定的,即0<9<a<z

1、将原先的left和right去掉中划线后,转换为16进制数

2、求步长,求出切点大小

3、将计算后的切点的十六进制整数转为字符串,并按照UUID格式在相应的位置上加上中划线

 

public static String[] doUUIDStringSplit(String left, String right, int expectSliceNumber) {
        BigInteger leftInteger = new BigInteger(left.replace("-", ""), 16);
        BigInteger rightInteger = new BigInteger(right.replace("-", ""), 16);
        BigInteger[] tempResult = doBigIntegerSplit(leftInteger, rightInteger, expectSliceNumber);
        String[] result = new String[tempResult.length];

        result[0] = left;
        result[tempResult.length - 1] = right;

        for (int i = 1, len = tempResult.length - 1; i < len; i++) {
            result[i] = bigIntegerToString(tempResult[i]);
        }

        return result;
 }


private static String bigIntegerToString(BigInteger bigInteger) {
        String hex = bigInteger.toString(16);
        List<String> list = new ArrayList<>();
        Pattern pattern = Pattern.compile("([a-z0-9]{8})([a-z0-9]{4})([a-z0-9]{4})([a-z0-9]{4})([a-z0-9]{12})");
        Matcher matcher = pattern.matcher(hex);
        List<String> list = new ArrayList<>();
        if (matcher.matches()){
            for (int i = 1; i <=matcher.groupCount(); i++){
                list.add(matcher.group(i));
            }
        }
        return StringUtils.join(list, "-");
}

 

查看切分之后的结果

取一个范围跨度大的例子:
doUUIDStringSplit("0122ed89-adb0-4599-84b7-89cfeb544637","d65e165d-d69a-47da-b41e-4549401b0ab0", 5)


切分后的结果:
0122ed89-adb0-4599-84b7-89cfeb544637

2bc85c1a-82ab-dfa6-8e32-7c1b62af3a50

566dcaab-57a7-79b3-97ad-6e66da0a2e68

8113393c-2ca3-13c0-a128-60b251652280

abb8a7cd-019e-adcd-aaa3-52fdc8c01698

d65e165d-d69a-47da-b41e-4549401b0ab0

四、大数据量测试

首先准备一个测试表,多线程生成UUID入库,根据改良后的字符串分割进行计算切点

数据总数:

min(id), max(id):

取5个分片,生成切分点:

('0000049e-8bda-4111-a06e-c25f42403f57' <= id AND id < '333336ba-a3b7-dacb-9a1b-4fef4f6ed44e')
('333336ba-a3b7-dacb-9a1b-4fef4f6ed44e' <= id AND id < '666668d6-bb95-7485-93c7-dd7f5c9d6945')
('666668d6-bb95-7485-93c7-dd7f5c9d6945' <= id AND id < '99999af2-d373-0e3f-8d74-6b0f69cbfe3b')
('99999af2-d373-0e3f-8d74-6b0f69cbfe3b' <= id AND id < 'cccccd0e-eb50-a7f9-8720-f89f76fa9331')
('cccccd0e-eb50-a7f9-8720-f89f76fa9331' <= id AND id <= 'ffffff2b-032e-41b3-80cd-862f84292827')

分别count数量:

切点范围切点1切点2切点3切点4切点5
count16026831604252160353816021211607406

最后将每个cout数量累加计算,可得和实际的数据量大小相等。

五、总结

这种方式其实也不是特别的完美,分隔的后每个task分到的数据也不一定会很均衡尤其在数据量不大的情况下,在数据量大的情况下会分布均匀一些,但是至少克服了原先字符串分隔会出现的,重复读数据或者少读数据的情况。

如果真的想要做到每个task分布均匀的话,就需要借助自己去实现mysql的rownum函数,以及具体代码实现,这里提供一些思路

1、求出left,right对应的rownum
2、计算步长,以及切点对应的rownum
3、根据切点对应的rownum去数据库查询对应步长的id

SELECT id from
(
SELECT 
@rownum:=@rownum+1 AS rownum, 
t.id
FROM 
(SELECT @rownum:=0) r, 
(SELECT id from table order by id asc) t
) a where a.rownum = {切点的rownum}

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值