Mysql分库分表

109 阅读9分钟

分库分表简介

分库分表是一种数据库拆分技术,用于解决单一数据库在数据量大、并发访问高时出现的性能瓶颈。分库分表的主要目的是将大表拆分为多个小表,将大数据库分为多个小数据库,以提高数据库的查询性能、写入性能和可扩展性。

分库分表有两种主要的策略:

  1. 垂直拆分(Vertical Splitting):将一个数据库中的不同表拆分到不同的数据库中。这种方法适用于数据表之间关联不紧密、访问特点差异较大的场景。垂直拆分可以使得每个数据库的表数量减少,降低单个数据库的复杂性和管理成本。但是,垂直拆分不会改变单表的数据量,对于单表数据量过大的问题,垂直拆分无法解决。
  2. 水平拆分(Horizontal Splitting):将一个表中的数据按照某种规则拆分到多个相同结构的表中。这种方法适用于单表数据量过大、访问压力较高的场景。水平拆分可以将单表的数据量分散到多个表中,降低单表的查询和写入压力。水平拆分的关键是选择合适的拆分规则(路由规则),使得数据能够均匀地分布在多个表中。
  • 垂直分库:从业务的角度,将一个库拆成不同的库,如拆成订单库,商品信息库
  • 水平分库:从库的角度对库进行过拆分 将订单库拆分成订单库1,订单库2。。。。。
  • 垂直分表:按照业务为度,对表中的列进行拆分
  • 水平分表:将xx表拆分为xx表1,xx表2

有许多工具和中间件可以帮助实现分库分表,如:MyCAT、ShardingSphere(原Sharding-JDBC)、Vitess等。这些工具提供了一定程度的透明性,使得应用程序在一定程度上无需关心数据库的拆分细节。

分库分表实践

以下是一个使用MyCAT进行分库分表的例子。在这个例子中,我们将一个名为user的表进行水平拆分,将其数据分布在两个数据库中的四个表上。

准备MySQL数据库

假设我们有两个MySQL数据库(db1和db2),分别运行在两台服务器上。在db1和db2上创建相同的数据库mydb,并在每个数据库中创建两个相同结构的表(user_0和user_1)。

安装MyCAT

下载MyCAT

访问MyCAT的GitHub仓库(github.com/MyCATApache…),在“Releases”页面下载最新版本的MyCAT。也可以直接使用wget命令下载:

wget <https://github.com/MyCATApache/Mycat-Server/releases/download/><latest-version>/mycat-<latest-version>-bin.tar.gz

请将<latest-version>替换为实际的版本号。

解压MyCAT

解压下载的MyCAT压缩包:

tar -zxvf mycat-<latest-version>-bin.tar.gz

配置MyCAT与分库分表规则

在MyCAT的conf/schema.xml文件中,配置分库分表规则。首先,配置数据节点(dataNode)信息:

<dataNode name="dataNode1" dataHost="dataHost1" database="mydb1" />
<dataNode name="dataNode2" dataHost="dataHost1" database="mydb2" />
<dataNode name="dataNode3" dataHost="dataHost2" database="mydb1" />
<dataNode name="dataNode4" dataHost="dataHost2" database="mydb2" />

然后,在<schema>标签中配置表信息:

<schema name="mydb" checkSQLschema="false" sqlMaxLimit="100">
    <table name="user" dataNode="dataNode1,dataNode2,dataNode3,dataNode4" rule="sharding-by-id" />
</schema>

接下来,配置分片规则。在conf/rule.xml文件中,定义一个名为sharding-by-id的分片规则:

<tableRule name="sharding-by-id">
    <rule>
        <columns>id</columns>
        <algorithm>rangemod</algorithm>
    </rule>
</tableRule>

最后,在conf/function.xml文件中,定义分片算法:

<function name="rangemod" class="io.mycat.route.function.PartitionByRangeMod">
    <property name="mapFile">partition-range-mod.properties</property>
</function>

在MyCAT的conf目录中,创建一个名为partition-range-mod.properties的文件,配置分片映射规则:

# 格式:key = value
# key:分片键的取值范围
# value:数据节点索引
0-49=0
50-99=1
100-149=2
150-199=3

这个例子中,我们将根据user表的id列进行分片,使用取模的方法将数据分布在四个数据节点上。

启动MyCAT

在MyCAT的目录中,运行以下命令启动MyCAT:

./bin/mycat start

现在MyCAT将根据配置的分库分表规则将数据分布在两个数据库的四个表上。

将应用程序连接到MyCAT

将应用程序连接到MyCAT而不是直接连接到MySQL服务器。修改应用程序的数据库连接配置,将其指向MyCAT服务器的IP地址和监听端口。例如,在Java应用程序中,可以修改application.properties文件:

spring.datasource.url=jdbc:mysql://<mycat-server-ip>:<mycat-server-port>/<your-database-name>?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=utf8
spring.datasource.username=<mycat-username>
spring.datasource.password=<mycat-password>
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

<mycat-server-ip><mycat-server-port><your-database-name><mycat-username><mycat-password>替换为实际MyCAT和数据库信息。

现在,应用程序将通过MyCAT与MySQL数据库通信,并自动使用配置的分库分表规则。

分布式ID(全局唯一ID)

在分库分表中,我们需要一个全局的唯一ID用于某条数据路由到具体的库表上.

在分布式系统中,生成全局唯一ID(如订单号、用户ID等)是一个常见的需求。以下是一些常用的全局唯一ID生成方法:

  • Mysql自增ID
  • Redis自增ID
  • UUID(Universally Unique Identifier)
  • 雪花算法 (Snowflake)
  • Leaf
  • uid-generator

Mysql自增ID

  • 专门建一个表,这个表的主要作用在于生成自增id。
  • 插入数据前,先往这个表插入一条没有什么业务含义的数据,获取到一个自增id,拿到自增id后再往分库分表写数据

优点:简单

缺点:id由单库单表产生,高并发下这个单表会成为瓶颈。

Redis自增ID

在分布式环境中,也可以使用Redis作为全局唯一ID生成器。利用Redis的原子操作(如INCR、INCRBY等),可以实现分布式的ID生成。例如,可以使用以下Redis命令生成自增ID:

INCR my_unique_id

在多个Redis实例之间,可以使用Redis分布式锁或其他同步机制来确保ID的全局唯一性。

UUID(Universally Unique Identifier)

UUID是一种通用的唯一识别码, 一个UUID通常由32个16进制数字组成,以连字号分为五段,形式为8-4-4-4-12,示例:550e8400-e29b-41d4-a716-446655440000。

在Java中,可以使用java.util.UUID类生成UUID:

import java.util.UUID;

public class UUIDExample {
    public static void main(String[] args) {
        UUID uuid = UUID.randomUUID();
        System.out.println("Generated UUID: " + uuid.toString());
    }
}

优点:性能非常高,本地生成,没有网络消耗。

缺点:

  1. UUID 16字节128位,通常以36长度的字符串表示,占用空间比较大,无业务含义。
  2. 基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
  3. 如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。

雪花算法 (Snowflake)

Snowflake 算法是 twitter 开源的分布式 id 生成算法, Snowflake生成的ID是一个64位整数,由以下几部分组成:

  • 1位符号位(未使用)
  • 41位时间戳(毫秒级,可使用约69年)
  • 10位机器ID(可部署1024个节点)
  • 12位序列号(支持每节点每毫秒生成4096个ID)

下面是一个简单实现

public class SnowflakeIdGenerator {

    private final long workerId;
    private final long datacenterId;
    private final long epoch = 1577836800000L; // 2020-01-01T00:00:00.000Z

    private long sequence = 0L;
    private long lastTimestamp = -1L;

    private static final long workerIdBits = 5L;
    private static final long datacenterIdBits = 5L;
    private static final long sequenceBits = 12L;

    private static final long workerIdShift = sequenceBits;
    private static final long datacenterIdShift = sequenceBits + workerIdBits;
    private static final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    private static final long sequenceMask = ~(-1L << sequenceBits);

    public SnowflakeIdGenerator(long workerId, long datacenterId) {
        if (workerId > 31 || workerId < 0) {
            throw new IllegalArgumentException("workerId must be between 0 and 31");
        }
        if (datacenterId > 31 || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId must be between 0 and 31");
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();

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

        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = waitUntilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }

        lastTimestamp = timestamp;
        return ((timestamp - epoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
    }

    private long waitUntilNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }

    public static void main(String[] args) {
        SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1, 1);
        for (int i = 0; i < 10; i++) {
            System.out.println(generator.nextId());
        }
    }
}

在这个例子中,我们创建了一个名为SnowflakeIdGenerator的类,它包含了基本的雪花算法实现。我们在main方法中创建了一个生成器实例,并生成了10个全局唯一ID。需要注意的是,这个实现不是线程安全的,如果需要在多线程环境中使用,请确保nextId()方法是线程安全的(例如,使用synchronized关键字. 不想自己实现的话, 也可以使用开源库snowflake-idworker

优点:毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。

缺点:强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

Leaf

Leaf是美团点评开源的一个分布式ID生成服务,它提供了全局唯一、趋势递增的ID,可以用于分布式系统中的订单、优惠券等业务场景。Leaf提供了两种ID生成方式:基于MySQL的Segment模式和基于Snowflake算法的自增模式。

以下是如何使用Leaf生成全局唯一标识符的步骤:

克隆Leaf源码

首先,从GitHub上克隆Leaf项目的源码:

git clone <https://github.com/Meituan-Dianping/Leaf.git>

搭建Leaf服务

进入Leaf项目的目录,并编译打包项目:

cd Leaf
mvn clean install -DskipTests

然后在leaf-server目录下找到target文件夹,将leaf-server-{version}.jar以及config文件夹复制到你的部署目录。修改config文件夹下的leaf.properties文件,根据需要选择ID生成方式。

基于MySQL的Segment模式:

在MySQL数据库中创建Leaf所需的表结构,可以参考leaf-core/src/main/resources/sql/leaf_alloc.sql文件。然后在leaf.properties中配置好数据库连接信息。

基于Snowflake算法的自增模式:

leaf.properties中配置机器ID和数据中心ID:

leaf.snowflake.enable=true
leaf.snowflake.workerId=1
leaf.snowflake.dataCenterId=1

启动Leaf服务

在部署目录下,使用以下命令启动Leaf服务:

java -jar leaf-server-{version}.jar

Leaf服务将在8080端口启动。你可以通过访问http://localhost:8080/api/segment/get/{biz_tag}http://localhost:8080/api/snowflake/get/{biz_tag}来获取全局唯一ID。

集成Leaf客户端

在你的应用中,可以使用HTTP客户端(如RestTemplateHttpClient等)调用Leaf服务来生成全局唯一ID。以下是一个简单的Java示例,使用RestTemplate调用Leaf服务:

import org.springframework.web.client.RestTemplate;

public class LeafClient {

    private static final String LEAF_SEGMENT_URL = "<http://localhost:8080/api/segment/get/{biz_tag}>";
    private static final String LEAF_SNOWFLAKE_URL = "<http://localhost:8080/api/snowflake/get/{biz_tag}>";

    public static void main(String[] args) {
        RestTemplate restTemplate = new RestTemplate();
        String bizTag = "your_biz_tag";

        // 使用Segment模式生成ID
        String segmentId = restTemplate.getForObject(LEAF_SEGMENT_URL, String.class, bizTag);
        System.out.println("Segment ID: " + segmentId);

        // 使用Snowflake模式生成ID
        String snowflakeId = restTemplate.getForObject(LEAF_SNOWFLAKE_URL, String.class, bizTag);
        System.out.println("Snowflake ID: " + snowflakeId);
    }
}

在这个例子中,我们创建了一个名为LeafClient的类,它使用RestTemplate调用Leaf服务来生成全局唯一ID。使用时,请替换your_biz_tag为你的业务标识。

uid-generator

uid-generator是百度开源的一个全局唯一ID生成器,它基于Snowflake算法实现,提供了高性能、低延时的ID生成服务。以下是如何在Java项目中使用uid-generator生成全局唯一标识符的步骤:

添加uid-generator依赖

在你的build.gradle文件中添加uid-generator的依赖:

dependencies {
    implementation 'com.baidu.fsg:uid-generator:1.0.0'
}

配置UidGenerator

创建一个Spring配置类(如 UidGeneratorConfig.java),并配置UidGenerator的Bean:

import com.baidu.fsg.uid.UidGenerator;
import com.baidu.fsg.uid.impl.CachedUidGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class UidGeneratorConfig {

    @Bean
    public UidGenerator uidGenerator() {
        CachedUidGenerator uidGenerator = new CachedUidGenerator();
        uidGenerator.setWorkerIdAssigner(workerIdAssigner());
        uidGenerator.setTimeBits(28);
        uidGenerator.setWorkerBits(22);
        uidGenerator.setSeqBits(13);
        uidGenerator.setEpochStr("2022-01-01");
        return uidGenerator;
    }

    @Bean
    public WorkerIdAssigner workerIdAssigner() {
        // 这里可以自定义WorkerIdAssigner实现来分配workerId
        // 例如,从数据库或配置文件中分配workerId
        return new DefaultWorkerIdAssigner();
    }
}

在这个配置类中,我们创建了一个UidGenerator的Bean,并配置了CachedUidGenerator。你可以根据需要调整timeBitsworkerBitsseqBits的值。此外,还需要提供一个WorkerIdAssigner实现类,用于分配workerId。在这个例子中,我们使用了一个简单的DefaultWorkerIdAssigner,它始终返回0作为workerId。在实际项目中,你可能需要根据你的分布式系统来自定义workerId分配策略(例如,从数据库或配置文件中分配workerId)。

使用UidGenerator生成ID

在你的服务或控制器类中,通过@Autowired注解注入UidGenerator实例,并使用getUID()方法生成全局唯一ID:

import com.baidu.fsg.uid.UidGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UidController {

    @Autowired
    private UidGenerator uidGenerator;

    @GetMapping("/uid")
    public String getUid() {
        long uid = uidGenerator.getUID();
        return String.valueOf(uid);
    }
}

在这个例子中,我们创建了一个名为UidController的控制器类,它使用UidGenerator生成全局唯一ID,并通过/uid端点返回生成的ID。

参考

Leaf--美团点评分布式ID生成系统