数据库分库分表的艺术:提升性能与可扩展性的秘籍

在这篇博客中,我们介绍分库分表。所谓分库分表,就是随着业务发展有些表,单表记录超过了几百万条。而且随着时间推移,该表记录会越来越大。
那么就要将业务数据从存在一个表里变成存在多个库多个表中。

分片策略

分片策略,一种是基于时间进行分片还有根据商户分片。这边我们分别讨论两种分片优缺点。为了方便讨论,这边假设的背景是Sasa平台,ToB平台。目前有3000多个商户,各个商户每天有一百多万订单量。
一天100万,那么一年就有3.65亿数据。单表就有3亿条数据,这是非常可怕的。

时间分片

根据订单的创建时间,比如当天分片。那么就是每天订单生成在一个表里,例如 t_order_20240829,第二天订单生成t_order_20240830。这种分片优点是可以查询多个商户的,缺点就是如果要查询一周数据,因为是分片那么需要查询7次的表。
而如果要查询一个月那么需要查询30次。而且每次查询都要带上,时间范围。

商户分片

商户分片是根据商户id进行分片,分片是根据商户id进行hash分片。进行商户分片,那么查询都要带上商户id。进行多个商户查询,那么也要查询多次。可以看出如果对于单商户来说,那么和之前一模一样。

    /**
     * shardingKey获取数据库index
     *
     * @param shardingKey   shardingKey
     * @param shardingCount 分片数量
     * @return int
     * @author jisl on 2024/8/23 15:40
     **/
    public static int indexDbByShardingKey(String shardingKey, int shardingCount) {
        final int shardingKeyMod = getShardingKeyMod(shardingKey);
        return shardingKeyMod / 32 % shardingCount;
    }

    /**
     * shardingKey获取表index
     *
     * @param shardingKey   订单号
     * @param shardingCount 分片数量
     * @return int
     * @author jisl on 2024/8/23 15:40
     **/
    public static int indexTableByShardingKey(String shardingKey, int shardingCount) {
        final int shardingKeyMod = getShardingKeyMod(shardingKey);
        return shardingKeyMod % 32 % shardingCount;
    }

通过以上对比,发现分片都要将分片信息。进行商户分片那么多商户不友好;进行时间分片,时间范围不友好。商户分片还有个好处,随着业务扩展商户不断增多,商户分片也是可以扩展的。

分片算法

这边我们确定了分片策略,那么该如何进行分片呢。如果参考网上的分片,是分成32库,每个库32张表。也就是一共分成了1024张表。理想情况下,业务数据量有1000万,那么分片到每张表只有1万条数据了。
这边分库分表,一个原则就是尽可能分完之后能够坚持5-10年都满足条件要求。就是一开始分,会往大的分。
这个条件是理想情况,网上的资料是ToC的业务,用户数量一般是有几百万。他们使用的方法是取用户id后四位,取模 1024。这边笔者运行了3000条虚拟商户,用雪花id作为商户id和uuid当做商户id。然而实际情况是好多槽都没分配到商户,好多槽分配了太多商户。

        final int[] dp = new int[1024];
        for (int i = 0; i < 3000; i++) {
            final int shardingKeyMod = (int) (UniqueIdUtil.getUniqueId()%1024);
            dp[shardingKeyMod] += 1;
        }
        Arrays.sort(dp);
        System.out.println(Arrays.toString(dp));
        System.out.println("差值:" + (dp[1023] - dp[0]));
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 3, 6, 47, 53, 54, 55, 679, 681, 691, 727]
差值:727

从上面看到可以,基本上所有的数据都集中到了几个表上,大部分的表都没数据。这种一看就是不符合我们要要求的。
这就是hash不均匀,ToB的商户一般都比较少。

笔者这边使用的是网上看到,有的使用CRC32

    public static int getShardingKeyMod(String shardingKey) {
        // 创建CRC32对象
        CRC32 crc32 = new CRC32();
        // 更新CRC32对象的校验和
        crc32.update(shardingKey.getBytes());
        // 获取计算得到的CRC32校验和值
        long crcValue = crc32.getValue();
        return (int) (crcValue % MOD);
    }

我们看下分片结果

        final int[] dp = new int[1024];
        for (int i = 0; i < 3000; i++) {
            final int shardingKeyMod = UniqueIdUtil.getShardingKeyMod(UniqueIdUtil.getUniqueId() + "");
            dp[shardingKeyMod] += 1;
        }
        Arrays.sort(dp);
        System.out.println(Arrays.toString(dp));
    	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 9, 9, 11]
差值:11

这个分片虽然还是存在没有分配到数据,但是总体来说已经可以使用了。

id生成方案

在讨论id生成方案,我们先看下分库分表查询的过程。我们这边是按照商户id进行分库分表,那么如果传了商户id那么我们能够知道当前查询的数据库在哪个库,哪个表。那么现在我传订单主键,要查询订单。这边没有商户id,那么要将每个库每个表都查询过去。也就是有32个表要查询32遍,1024个表要查询1024遍。这显然不符合我们要求,一个方法是改现有业务代码,所有查询都加上商户id。我们分库分表初衷是不改变代码的情况下,实现分库分表。那么要商户id那么能否在订单id里加上商户id,那么不就是订单主键有商户id信息了。

上面讨论的在订单id里加上商户id就是基因法。
我们看下id方案,这边再说下分库分表规划是32库,每个库32张表,一共就是1024个表。

日期+商户id+随机数

id=时间戳+商户id(最后4位)+4位随机数
这边需要的随机数位数根据自己的业务需要,增加对应的位数。

这边将生成的id发现超过了long,也就是说要使用这种方案,需要改业务代码。而且还是将主键类型改了,不符合我们的设计初衷。

推特SnowflakeId

我们这里的订单id就是需要根据商户id,生成1024长度内的数据。因为分库分表,表个数是1024那么最后携带的基因也只要1024就行了。

第一位为未使用(符号位表示正数),接下来的41位为毫秒级时间(41位的长度可以使用69)<br>
 * 然后是5位datacenterId和5workerId(10位的长度最多支持部署1024个节点)<br>
 * 最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096ID序号)
符号位(1bit)- 时间戳相对值(41bit)- 数据中心标志(5bit)- 机器标志(5bit)- 递增序号(12bit)
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
并且可以通过生成的id反推出生成时间,datacenterId和workerId

推特雪花id,有workerId和dataCenterId用来区分不同的实例,保证不同实例生成的id不会重复。而workerId和dataCenterId默认是有32可以设置,两个32那么实际刚好就是1024.太好了,直接将workerId就是库的索引,dataCenterId就是表的索引。一下就解决了,真的太好了。
但是发现问题,有workerId和dataCenterId用来区分不同的实例。当生成id有多个实例,如果workerId和dataCenterId还是一样的,那么会存在id重复的概率。特别是随着业务发展,重复的概率就越大。那么我们将workerId设置成10位,那么workerId也就是存储了1024个信息。

我这边修改的方案是 
	private static final long WORKER_ID_BITS = 10L;   多了5//    5位改成4位,支持16
	private static final long DATA_CENTER_ID_BITS = 4L;  少了1//    12位改成8位支持256
	private static final long SEQUENCE_BITS = 8L;   少了4位

这边 DATA_CENTER_ID_BITS = 4L,也就是最多只支持生成id的实例就16台,同时每毫秒生成的id数从4096变成256.

这边我没有修改时间戳,还是想着尽可能让支持的时间长些。我们知道41位时间戳也就是69年[2^41/(1000360024*365)],这里也讨论一下超过了怎么办。我们知道现在很多互联网平台订单数就保存半年或者一年,也就是在快超过的时候,修改雪花id的起始时间,接着续命69年。这边就是会存在新生成的订单id,比老订单小。不过如果订单就保存1年,那么一年后就不存在了。也可以。

前面我们讨论的是订单表,这边如果是用户表。那么用户id需要如何处理,在69年总不能删了吧。不过也确实可能存在该用户注销了,这边用户id需要如何处理。还是我们这边提前在设计用户表,增加用户号作为将来用户id。这边可以做个讨论。目前大厂是如何处理用户id的。

代码实现

UniqueId

就是我们前面讨论的订单id,这边将1024商户id信息存在workerId里,同时workerId / 32表示库索引,workerId % 32表索引也就是将1024拆分成两部分了。

    /**
     * 通过uniqueId获取 数据库index
     *
     * @param uniqueId      生成的订单id
     * @param shardingCount 分片数量
     * @return int  分片索引
     * @author jisl on 2023/9/1 22:19
     **/
    public static int indexDbByUniqueId(Long uniqueId, int shardingCount) {
        final long workerId = Singleton.get(SnowflakeId.class).getWorkerId(uniqueId);
        int shardingKeyMod = (int) workerId;
        return shardingKeyMod / 32 % shardingCount;
    }

    /**
     * 通过uniqueId获取 表index
     *
     * @param uniqueId      生成的订单id
     * @param shardingCount 分片数量
     * @return int
     * @author jisl on 2024/8/23 15:39
     **/
    public static int indexTableByUniqueId(Long uniqueId, int shardingCount) {
        final long workerId = Singleton.get(SnowflakeId.class).getWorkerId(uniqueId);
        int shardingKeyMod = (int) workerId;
        return shardingKeyMod % 32 % shardingCount;
    }

UniqueNoUtil

这边再加上个字符的工具,是因为笔者之前订单号产品都要求是日期开头yyyyMMdd。这边也做了一个生成方式,比较简单为了不考虑不同实例直接就是加随机数长度。这也导致了长度特别长,1小时100万订单长度需要26.长度很长,不是很好用。同时为了能校验输入的订单号是否合法,加入了Luhn算法校验。

    private static String getUniqueNo(String shardingKey, int length) {
        String shardingKeyModStr = StringUtils.isEmpty(shardingKey) ? "" : String.format("%04d", UniqueIdUtil.getShardingKeyMod(shardingKey));
        StringBuilder sb = new StringBuilder(length);
        sb.append(shardingKeyModStr);
        // 生成前n-1位随机数字
        for (int i = 0; i < length - 1 - shardingKeyModStr.length(); i++) {
            int digit = (int) (Math.random() * 10);
            sb.append(digit);
        }
        // 计算校验和,使最后一位满足Luhn算法
        int sum = 0;
        for (int i = 0; i < length - 1; i++) {
            int digit = Integer.parseInt(sb.substring(i, i + 1));
            if ((i % 2) == (length % 2)) {
                digit *= 2;
                if (digit > 9) {
                    digit -= 9;
                }
            }
            sum += digit;
        }
        int checksum = (10 - (sum % 10)) % 10;
        // 添加校验和到末尾
        sb.append(checksum);
        return sb.toString();
    }

分片算法

ShardingSphere 和 MyCat 都是用于数据库分片(sharding)的解决方案。ShardingSphere 支持的功能较多,同时社区活跃度也较高。目前基本上数据库分片用的是ShardingSphere 。

库分片算法

分片算法,目前实现的是两个组合键订单id和用户id,实际如果订单表有订单号那么可能还需要一个订单号。

public class DBHashSlotComplexAlgorithm implements ComplexKeysShardingAlgorithm {


    @Override
    public Collection<String> doSharding(Collection availableTargetNames, ComplexKeysShardingValue shardingValue) {
        final int shardingCount = availableTargetNames.size();
        final Object[] availableTargetNameArray = availableTargetNames.toArray();
        final Map shardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap();
        List rest = new ArrayList<>();
//        主键
        final Collection orderIds = (Collection) shardingValuesMap.get(ShardingConstants.DEFAULT_PRIMARY_KEY);
        if (orderIds != null && !orderIds.isEmpty()) {
            for (Object obj : orderIds) {
                Long id = Long.valueOf(obj.toString());
                final int index = UniqueIdUtil.indexDbByUniqueId(id, shardingCount);
                rest.add(availableTargetNameArray[index]);
            }
        }
        if (!rest.isEmpty()) {
            return rest;
        }
        final Collection userIds = (Collection) shardingValuesMap.get(ShardingConstants.USER_ID);
        if (userIds != null && !userIds.isEmpty()) {
            for (Object obj : userIds) {
                final int index = UniqueIdUtil.indexDbByShardingKey(obj.toString(), shardingCount);
                rest.add(availableTargetNameArray[index]);
            }
        }
        return rest;
    }
}

表分片算法

@Component
public class TableHashSlotComplexAlgorithm implements ComplexKeysShardingAlgorithm {


    @Override
    public Collection<String> doSharding(Collection availableTargetNames, ComplexKeysShardingValue shardingValue) {
        final int shardingCount = availableTargetNames.size();
        final Object[] availableTargetNameArray = availableTargetNames.toArray();
        final Map shardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap();
        List rest = new ArrayList<>();
//        主键
        final Collection orderIds = (Collection) shardingValuesMap.get(ShardingConstants.DEFAULT_PRIMARY_KEY);
        if (orderIds != null && !orderIds.isEmpty()) {
            for (Object obj : orderIds) {
                Long id = Long.valueOf(obj.toString());
                final int index = UniqueIdUtil.indexTableByUniqueId(id, shardingCount);
                rest.add(availableTargetNameArray[index]);
            }
        }
        if (!rest.isEmpty()) {
            return rest;
        }
        final Collection userIds = (Collection) shardingValuesMap.get(ShardingConstants.USER_ID);
        if (userIds != null && !userIds.isEmpty()) {
            for (Object obj : userIds) {
                final int index = UniqueIdUtil.indexTableByShardingKey(obj.toString(), shardingCount);
                rest.add(availableTargetNameArray[index]);
            }
        }
        return rest;
    }
}

shardingsphere配置

  shardingsphere:
    sharding:
      tables:
        t_order:
          # 真实表
          actualDataNodes: m$->{0..1}.t_order_$->{0..31}
          # 分库策略
          databaseStrategy:
            complex:
              sharding-columns: order_id,user_id
              algorithm-class-name: com.github.ji.shardingjdbc.core.sharding.DBHashSlotComplexAlgorithm
          # 分表策略
          tableStrategy:
            complex:
              sharding-columns: order_id,user_id
              algorithm-class-name: com.github.ji.shardingjdbc.core.sharding.TableHashSlotComplexAlgorithm
      default-data-source-name: m0 # 默认数据源,仅在执行 CREATE TABLE 创建单表时有效。缺失值为空,表示随机单播路由。

tables里就是配置要进行分库分片的表明,上面配置的是t_order表。

t_order是实体里的表名,也就是分库分表前的表名。
actualDataNodes: m$->{0..1}.t_order_$->{0..31} 真实的表名,也就是我们实际要存入的数据库表名。上面配置表示数据库m0到m1,表名t_order_0到t_order_31

 databaseStrategy:
   complex:
     sharding-columns: order_id,user_id
     algorithm-class-name: com.github.ji.shardingjdbc.core.sharding.DBHashSlotComplexAlgorithm
sharding-columns: order_id,user_id  就是我们前面 DBHashSlotComplexAlgorithm 配置的字段,也就是分片的字段

具体参考官网配置说明shardingsphere

项目启动

初始化表数据

在JShardingJDBC/script/ji_helper.sql 目录下
在这里插入图片描述

修改数据库连接信息

这边为了方便,并没有生成32个库。只使用了2个库,作为演示是可以的了。
在这里插入图片描述

##运行效果

    @Test
    public void testInsert() {
        Long userId =1830426957730264832L;
        for (int i = 0; i < 1; i++) {
            Order order = new Order();
            order.setOrderId(uniqueIdManager.getUniqueId(userId));
            order.setUserId(userId);
            order.setState(0);
            order.setTotalPrice(new BigDecimal((i + 1) * 5));
            order.setCreateTime(new Date());
            order.setUpdateTime(order.getCreateTime());
            orderService.save(order);
        }
    }



2024-09-03 09:01:19.189  INFO 7136 --- [           main] ShardingSphere-SQL                       : Logic SQL: INSERT INTO t_order  ( order_id,
user_id,
total_price,
state,
create_time,
update_time )  VALUES  ( ?,
?,
?,
?,
?,
? )

2024-09-03 09:01:19.190  INFO 7136 --- [           main] ShardingSphere-SQL                       : Actual SQL: m0 ::: INSERT INTO t_order_7  ( order_id,
user_id,
total_price,
state,
create_time,
update_time )  VALUES  (?, ?, ?, ?, ?, ?) ::: [1830773027970680576, 1830426957730264832, 5, 0, 2024-09-03 09:01:18.643, 2024-09-03 09:01:18.643]

我们看到sql日志有两个,一个是Logic SQL,就是我们这边业务提交的sql代码;Actual SQL就是转换成实际要插入哪个库哪个表。shardingjdbc实际就是一个代理,根据我们的业务提交的sql。进行一直转换,变成实际要插入的数据库和表。这就是分库分表的原理。

接下来我们用订单id和商户id查询,发现每次也都是查询一次,达到了我们要的效果。也都是到我们插入的0库和7表这里查询。

订单id查询

    @Test
    public void testListByIds() {
//        主键查询需要查每个表过去
        List<Long> idList = Arrays.asList(1830773027970680576L, 1830432169966634753L);
        List<Order> orderList = orderService.listByIds(idList);
        System.out.println("orderList返回:" + orderList);
//        ChronoUnit.DAYS
    }

订单id查询

商户id查询

    @Test
    public void testSelectList() {
        final LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Order::getUserId, 1830426957730264832L);
        queryWrapper.between(Order::getCreateTime, "2023-09-02 09:38:14", "2024-09-03 09:38:14");
        List<Order> orderList = orderService.list(queryWrapper);
        System.out.println("orderList返回条数:" + orderList.size());
        System.out.println("orderList返回:" + orderList);
    }

在这里插入图片描述

没有商户id和订单id查询

上面我们看到查询sql语句只执行了一次,这次我们不加入这些语句看下查询结果

    @Test
    public void testSelectList() {
        final LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<>();
//        queryWrapper.eq(Order::getUserId, 1830426957730264832L);
        queryWrapper.between(Order::getCreateTime, "2023-09-02 09:38:14", "2024-09-03 09:38:14");
        List<Order> orderList = orderService.list(queryWrapper);
        System.out.println("orderList返回条数:" + orderList.size());
        System.out.println("orderList返回:" + orderList);
    }

在这里插入图片描述
没有传入分片信息,那么就是将所有的库表遍历一遍过去。目前配置的有2个库,32张表。也就是有64个表,一个查询要执行64次。也就是分库分表限制了很多复杂查询,像表关联和子查询都会变得更加复杂。但是在分库分表前,对于订单表这种大表也是要求要单表查询,通过多次查询封装业务数据。在写代码养成良好的习惯。

没有分库分表的表

实际业务,也就是和订单表相关是大表。大多数,也都是小表。那么没有分库分表的表,是否需要处理呢。这边建议配置一个默认数据库,比较好。如果没有配置,它会随机路由。最后会导致有的数据查询不出来。这边建议就是没有分库分表的表,在其他库也不要创建。防止出现问题无法定位。

default-data-source-name: m0 # 默认数据源,仅在执行 CREATE TABLE 创建单表时有效。缺失值为空,表示随机单播路由。

源码地址

GitHub地址

结尾

这边说得分库分表实际还遗留问题
(1)id生成的方案,导致最多只能1024个库。将来如果业务井喷式发展,无法满足条件那么还需要修改。目前设计理论支持一天一千万的订单数据。不过因为是hash映射,那么会存在有的表数据很多,而有的分配很少。笔者不太喜欢主键id设计成string类型,这个性能占用空间都比较差。各有利弊吧
(2)数据迁移。大多数应用在一开始设计没有将id和订单号加入分片信息,那么所有id和订单号都要修改。同时将原来一张表里数据都迁移到分片后的表。这个过程特别像hashmap里的rehash过程,笔者个人觉得整个思想也是借鉴了他们。一般数据迁移都要求不停机更新,这边数据迁移过程比较麻烦。迁移一般用阿里的canal,用binlog进行同步。
(3)以上是抛砖引玉,如果哪些有不完善或者有更好的实现方案。欢迎大家一起讨论,将这个问题解决方案做得更好。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值