数据库水平分表方案

数据库分表有很多策略,如下:

数据库分表是处理大型数据库中数据量过大的一种常见策略,它可以提高查询性能、减少锁竞争、降低维护成本等。以下是一些常见的数据库分表方案:

  1. **垂直分表(Vertical Partitioning)**:
  • 将表中的一部分列拆分到新的表中,通常是根据列的使用频率或者数据类型。

  • 适用于查询中只涉及部分列的情况。

  1. **水平分表(Horizontal Partitioning)**:
  • 将表中的行拆分到多个表中,每个表结构相同,但数据行不同。

  • 可以基于某个键值(如用户ID、日期等)进行分片。

  1. **基于范围的分表**:
  • 按照数据的某个属性值的范围进行分表,例如按照时间范围(每月一个表)。
  1. **基于列表的分表**:
  • 根据某个离散的属性值进行分表,例如用户ID或地区ID。
  1. **复合分表**:
  • 结合垂直分表和水平分表,先按列分表,再按行分表。
  1. **哈希分表**:
  • 使用哈希函数根据某个键值将数据均匀分配到多个表中。
  1. **列表分表**:
  • 根据数据的某个属性值(如枚举类型)将数据分配到不同的表中。
  1. **一致性哈希分表**:
  • 适用于分布式系统中,通过一致性哈希算法将数据分配到不同的节点或表中。
  1. **分区分表**:
  • 在数据库中创建不同的分区,每个分区可以独立查询。
  1. **按业务逻辑分表**:
  • 根据业务模块或逻辑将数据分配到不同的表中。

实施分表策略时,需要考虑以下因素:

  • **查询模式**:分析应用的查询模式,确保分表策略能够优化这些查询。

  • **数据访问频率**:经常访问的数据应该放在容易访问的位置。

  • **数据增长**:预测数据增长趋势,确保分表策略能够适应未来的数据量。

  • **维护成本**:分表会增加维护的复杂性,需要权衡性能提升和维护成本。

  • **事务一致性**:分表可能会影响事务的处理,需要确保数据一致性。

  • **硬件资源**:考虑硬件资源的分配,确保分表策略能够充分利用硬件资源。

在实施分表后,可能还需要使用数据库中间件或应用层的逻辑来管理数据的路由和查询,以确保数据能够正确地分布在不同的表中。


本篇文章主要讲一下水平分表方案

水平分表时,我们需要选一个字段作为分表主键。这种情况下用数据库的自增id肯定不合适了,我们可以使用UUID,或者雪花算法id。

这里我们使用雪花算法生成的id.

在MySQL数据库中,使用雪花算法(Snowflake)作为分表键时,确实存在一些潜在的问题,尤其是在低并发的情况下。雪花算法生成的ID是趋势递增的,但在低并发环境下,由于每次请求的时间戳可能不同,导致生成的ID序列在分表时可能会集中在某些表中,从而造成数据分布不均匀,这种现象称为数据倾斜。

为什么会发生数据倾斜呢?

这是雪花id的组成

| 符号位 | 时间戳 | 工作机器ID | 序列号 |
|--------|--------|------------|---------|
| 0      | 41位    | 10位       | 12位    |

而256张表,二进制位100000000。只有9位

雪花id和256进行与运算或者模运算的时候真正参与运算的只有9位

那么也就是雪花id序列号的部分,那么低并发下,序列号可能一直是一个值或者几个值例如000000000001

000000000001
AND 0000000100000000
-------------------
000000000001

那么这样的情况下就会导致table_1的表存储了大量的数据,而table_2,3,4,5,6......256分不到数据。

说到数据倾斜,我们需要了解基本的位运算(与、或、非、模)

与(AND)、或(OR)、非(NOT)、模(Modulo)这四种位运算都可以用来确定一个范围内的值,但它们适用的场景和实现方式有所不同。以下是每种运算符如何用于确定一个特定范围内的值:

  1. **模运算(Modulo)**:

模运算是最直接的方式来将一个数值映射到一个给定的范围。例如,如果你有一个很大的数值,你想将其映射到1-256的范围,你可以使用模运算:

```java

int value = someLargeNumber % 256;

// value 现在是0-255,加1使其变为1-256

int rangeValue = value + 1;

```

  1. **与运算(Bitwise AND)**:

与运算可以用来提取数值的特定位。如果你想要限制数值在一个范围内,你可以使用与运算来获取数值的低位:

```java

int value = someNumber & (256 - 1); // 256 - 1 是0xFF,即11111111二进制

// value 现在是0-255

int rangeValue = value + 1; // 使其变为1-256

```

  1. **或运算(Bitwise OR)**:

或运算通常用于设置特定位,而不是限制数值范围。不过,你可以结合其他运算来使用或运算。例如,你可以先将数值与一个掩码进行与运算,然后与一个值进行或运算来设置高位:

```java

int value = (someNumber & (256 - 1)) | 0x100; // 将高位设置为1

// value 现在是256-511,但通常我们不需要这样来确定范围值

```

  1. **非运算(Bitwise NOT)**:

非运算用于反转位,它本身不直接用于确定一个范围内的值。但是,它可以与其他位运算结合使用来实现复杂的位操作:

```java

int value = ~someNumber & (256 - 1);

// 这将反转someNumber的位,然后限制在0-255范围内

int rangeValue = value + 1; // 使其变为1-256

```

在实际应用中,模运算是最常用于将数值映射到特定范围的方法。与运算也可以实现类似的功能,特别是当你想要保留数值的低位时。或运算和非运算通常用于其他类型的位操作,而不是直接用于范围限制,但它们可以与其他运算结合使用来实现复杂的逻辑。

选择哪种运算取决于你的具体需求,例如数据的当前范围、目标范围、以及你是否需要保留数值的某些位。在设计分表策略时,通常会根据数据分布的均匀性和系统的扩展性来选择合适的方法。


在数据库分表和分库的场景中,除了基本的位运算(与、或、非、模),以下是一些额外的概念和知识点,它们对于设计和实现高效的分片策略非常重要:

  1. **一致性哈希(Consistent Hashing)**:

一致性哈希是一种特殊的哈希算法,用于分布式系统中,可以在节点(如数据库分片)增加或删除时最小化数据迁移。它通过将数据映射到一个环状空间来实现。

  1. **虚拟节点(Virtual Nodes)**:

在一致性哈希中,为了解决节点分布不均匀的问题,通常会引入虚拟节点。每个物理节点可以对应多个虚拟节点,这些节点均匀分布在哈希环上。

  1. **数据倾斜(Data Skew)**:

数据倾斜是指数据在不同的分片之间分布不均匀,导致某些分片负载过高,而其他分片则相对空闲。需要采取措施来避免或减轻数据倾斜。

  1. **范围分片(Range Sharding)**:

根据数据的某个连续范围(如时间戳、ID范围)来分配到不同的分片。这种方法简单直观,但可能会导致某些分片成为热点。

  1. **列表分片(List Sharding)**:

根据数据的某个离散值(如国家代码、用户类型)来分配到不同的分片。这种方法适用于值域较小且分布均匀的场景。

  1. **哈希分片(Hash Sharding)**:

使用哈希函数将数据均匀分配到不同的分片。这种方法可以很好地分散负载,但需要注意选择合适的哈希函数以避免数据倾斜。

  1. **键分片(Key Sharding)**:

根据数据的某个键值(如用户ID、订单ID)来分配到不同的分片。这种方法可以保持相关数据的局部性,便于查询。

  1. **分布式ID生成策略**:

在分布式系统中,需要生成全局唯一的ID,常用的方法包括UUID、雪花算法(Snowflake)、递增序列等。

  1. **跨分片查询(Cross-Shard Query)**:

当查询条件不落在单个分片的范围内时,可能需要跨多个分片进行查询,这会增加查询的复杂性和成本。

  1. **分布式事务管理**:

在分片的环境中,保持事务的一致性是一个挑战。可能需要使用两阶段提交(2PC)、补偿事务(TCC)或最终一致性模型。

  1. **数据迁移策略**:

随着业务的发展,可能需要对分片进行扩容或缩容,这涉及到数据的迁移。需要设计高效的数据迁移策略以最小化对业务的影响。

  1. **元数据管理**:

在分片环境中,需要管理分片的元数据,如分片的映射关系、分片的健康状态等。

了解这些概念和策略有助于在设计分库分表方案时做出更合理的决策,从而提高系统的性能、可扩展性和稳定性。


雪花算法为了避免数据倾斜,导致部分数据全部存储到几张表中。

随机化序列号:在低并发情况下,可以通过在算法中引入随机化序列号来使ID分布更加均匀。例如,美团的Leaf分布式ID生成服务就采用了这种方法,通过在每个毫秒内为序列号添加一个随机偏移量来实现ID的随机化分布。

美团的代码https://github.com/Meituan-Dianping/Leaf/blob/master/leaf-core/src/main/java/com/sankuai/inf/leaf/snowflake/SnowflakeIDGenImpl.java

package com.sankuai.inf.leaf;

import com.sankuai.inf.leaf.common.Result;

public interface IDGen {
    Result get(String key);
    boolean init();
}

package com.sankuai.inf.leaf.snowflake;

import com.google.common.base.Preconditions;
import com.sankuai.inf.leaf.IDGen;
import com.sankuai.inf.leaf.common.Result;
import com.sankuai.inf.leaf.common.Status;
import com.sankuai.inf.leaf.common.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Random;

public class SnowflakeIDGenImpl implements IDGen {

    @Override
    public boolean init() {
        return true;
    }

    private static final Logger LOGGER = LoggerFactory.getLogger(SnowflakeIDGenImpl.class);

    private final long twepoch;
    private final long workerIdBits = 10L;
    private final long maxWorkerId = ~(-1L << workerIdBits);//最大能够分配的workerid =1023
    private final long sequenceBits = 12L;
    private final long workerIdShift = sequenceBits;
    private final long timestampLeftShift = sequenceBits + workerIdBits;
    private final long sequenceMask = ~(-1L << sequenceBits);
    private long workerId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;
    private static final Random RANDOM = new Random();

    public SnowflakeIDGenImpl(String zkAddress, int port) {
        //Thu Nov 04 2010 09:42:54 GMT+0800 (中国标准时间) 
        this(zkAddress, port, 1288834974657L);
    }

    /**
     * @param zkAddress zk地址
     * @param port      snowflake监听端口
     * @param twepoch   起始的时间戳
     */
    public SnowflakeIDGenImpl(String zkAddress, int port, long twepoch) {
        this.twepoch = twepoch;
        Preconditions.checkArgument(timeGen() > twepoch, "Snowflake not support twepoch gt currentTime");
        final String ip = Utils.getIp();
        SnowflakeZookeeperHolder holder = new SnowflakeZookeeperHolder(ip, String.valueOf(port), zkAddress);
        LOGGER.info("twepoch:{} ,ip:{} ,zkAddress:{} port:{}", twepoch, ip, zkAddress, port);
        boolean initFlag = holder.init();
        if (initFlag) {
            workerId = holder.getWorkerID();
            LOGGER.info("START SUCCESS USE ZK WORKERID-{}", workerId);
        } else {
            Preconditions.checkArgument(initFlag, "Snowflake Id Gen is not init ok");
        }
        Preconditions.checkArgument(workerId >= 0 && workerId <= maxWorkerId, "workerID must gte 0 and lte 1023");
    }

    @Override
    public synchronized Result get(String key) {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                try {
                    wait(offset << 1);
                    timestamp = timeGen();
                    if (timestamp < lastTimestamp) {
                        return new Result(-1, Status.EXCEPTION);
                    }
                } catch (InterruptedException e) {
                    LOGGER.error("wait interrupted");
                    return new Result(-2, Status.EXCEPTION);
                }
            } else {
                return new Result(-3, Status.EXCEPTION);
            }
        }
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                //seq 为0的时候表示是下一毫秒时间开始对seq做随机
                sequence = RANDOM.nextInt(100);
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            //如果是新的ms开始
            sequence = RANDOM.nextInt(100);
        }
        lastTimestamp = timestamp;
        long id = ((timestamp - twepoch) << timestampLeftShift) | (workerId << workerIdShift) | sequence;
        return new Result(id, Status.SUCCESS);

    }

    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    protected long timeGen() {
        return System.currentTimeMillis();
    }

    public long getWorkerId() {
        return workerId;
    }

}

sequence = RANDOM.nextInt(100);

就是对每毫秒起始的sequence取随值,美团的随机范围是0到100。最终的效果就是生成的id会均匀分布在tb_0到tb_100。而我们如果分表数是256,则需要改成

sequence = RANDOM.nextInt(256);

或者用下面这个简化版本

import java.util.concurrent.ThreadLocalRandom;

public class RandomizedSnowflakeIdWorker {
    private long lastTimestamp = -1L;
    private long sequence = 0L;
    private final long workerIdBits = 5L;
    private final long datacenterIdBits = 5L;
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    private final long sequenceBits = 12L;
    private final long workerIdShift = sequenceBits;
    private final long datacenterIdShift = sequenceBits + workerIdBits;
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);
    private final long twepoch = 1288834974657L;
    private long workerId;
    private long datacenterId;

    public RandomizedSnowflakeIdWorker(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    public synchronized long nextId() {
        long timestamp = timeGen();

        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = ThreadLocalRandom.current().nextLong(sequenceMask + 1);
        }

        lastTimestamp = timestamp;

        return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
    }

    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    protected long timeGen() {
        return System.currentTimeMillis();
    }

    public static void main(String[] args) {
        RandomizedSnowflakeIdWorker idWorker = new RandomizedSnowflakeIdWorker(0, 0);
        for (int i = 0; i < 1000; i++) {
            long id = idWorker.nextId();
            System.out.println(Long.toBinaryString(id));
            System.out.println(id);
        }
    }
}

在这个示例中,sequence 在每次调用 nextId 方法时都会随机化,而不是自增。这是通过 ThreadLocalRandom.current().nextLong(sequenceMask + 1) 实现的,它会在每个毫秒内生成一个随机的序列号。这样可以确保在高并发情况下,生成的ID更加分散,减少数据倾斜的风险。

请注意,这个示例是一个简化的版本,实际应用中可能需要更复杂的逻辑来确保ID的全局唯一性和趋势递增性。此外,时钟回拨的处理也需要根据实际需求进行设计。

雪花id+hash散列表

int tableIndex = (int)(snowflakeId.hashCode() & 0xFFFFFFFF) % 256;


关于雪花id时钟回拨的问题有一下几个解决方案

雪花算法(Snowflake)是一种广泛使用的分布式唯一ID生成方法,它通过结合时间戳、机器ID和序列号来生成一个64位的长整型ID。这种算法能够确保在分布式系统中生成全局唯一的ID,并且具有高性能和高可用性的特点。

然而,雪花算法的一个潜在问题是时钟回拨,即服务器时间意外地回退到之前的时间。这可能导致生成重复的ID,因为算法依赖于时间戳来保证ID的唯一性。以下是几种解决时钟回拨问题的策略:

  1. **直接抛出异常**:

如果检测到时钟回拨,算法可以拒绝生成新的ID并直接抛出异常。这是一种简单直接的方法,但可能会导致服务中断。

  1. **等待策略**:

当检测到时钟回拨时,服务可以等待直到系统时钟恢复到正常状态。这种方法可以确保ID的严格递增性,但可能会在时钟调整期间暂停服务。

  1. **序列号持久化**:

将序列号持久化存储(例如在数据库中),这样即使发生时钟回拨,也可以从持久化存储中恢复最后一个序列号,从而继续生成新的ID。

  1. **使用历史时间戳**:

在某些实现中,可以使用一个"历史时间戳"来代替当前时间戳,每次请求只增加序列号,当序列号用完时,再增加历史时间戳。

  1. **增加容忍时钟回拨的时间阈值**:

在算法中设置一个容忍时钟回拨的时间阈值,如果回拨时间在这个阈值内,算法可以等待或采取其他措施,而不是立即抛出异常。

  1. **备用机方案**:

如果当前机器出现时钟回拨,可以尝试切换到备用机器上继续提供服务。

  1. **采用之前最大时间**:

当检测到时钟回拨时,可以采用之前记录的最大时间戳和序列号继续生成ID。

  1. **基于时钟序列的方案**:

将机器ID拆分为时钟序列和机器码,发生时间回拨时,增加时钟序列的值,从而生成新的ID。

这些策略可以单独使用,也可以组合使用,以提供更强的容错能力和更高的可用性。在实际应用中,应根据业务需求和系统特性选择最合适的解决方案。

相关推荐
java_heartLake6 小时前
PostgreSQL数据库参数调优实践
数据库·postgresql·调优
xnuscd7 小时前
Milvus概念
数据库·学习·mysql
代码欢乐豆7 小时前
软件工程第13章小测
服务器·前端·数据库·软件工程
小gpt&8 小时前
实现qt拖拽显示或者播放
数据库·qt·音视频
望获linux9 小时前
在 ARM 平台上如何实现Linux系统的1秒启动
linux·服务器·开发语言·数据库·操作系统·嵌入式操作系统·arm平台
漫天转悠9 小时前
MySQL 七种JOIN连接详解
数据库·mysql
尘浮生9 小时前
Java项目实战II基于Java+Spring Boot+MySQL的智能停车计费系统(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·mysql·微信小程序·maven
quintin-lee9 小时前
PostgreSQL中的内存上下文管理
数据库·postgresql
蚂蚁在飞-9 小时前
一个高度可扩展的 Golang ORM 库【GORM】
数据库·oracle·golang
hai405879 小时前
Spring Boot整合Redis Stack构建本地向量数据库相似性查询
数据库·spring boot·redis