分库分表简介
分库分表是一种数据库拆分技术,用于解决单一数据库在数据量大、并发访问高时出现的性能瓶颈。分库分表的主要目的是将大表拆分为多个小表,将大数据库分为多个小数据库,以提高数据库的查询性能、写入性能和可扩展性。
分库分表有两种主要的策略:
- 垂直拆分(Vertical Splitting):将一个数据库中的不同表拆分到不同的数据库中。这种方法适用于数据表之间关联不紧密、访问特点差异较大的场景。垂直拆分可以使得每个数据库的表数量减少,降低单个数据库的复杂性和管理成本。但是,垂直拆分不会改变单表的数据量,对于单表数据量过大的问题,垂直拆分无法解决。
- 水平拆分(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());
}
}
优点:性能非常高,本地生成,没有网络消耗。
缺点:
- UUID 16字节128位,通常以36长度的字符串表示,占用空间比较大,无业务含义。
- 基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
- 如果作为数据库主键,在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客户端(如RestTemplate
、HttpClient
等)调用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
。你可以根据需要调整timeBits
、workerBits
和seqBits
的值。此外,还需要提供一个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。