1. 引言
在大规模互联网应用中,随着数据量的不断增长,单库单表的架构无法满足高并发和大数据存储的需求。分库分表是一种常见的数据库架构优化方案,它可以提高数据库的吞吐量、减小单表数据量并提升查询效率。然而,分库分表也带来了很多复杂的问题,例如主键生成、分页查询、分布式事务、跨表查询、高可用性等。
本文将详细讲解分库分表的核心概念、关键技术及其实现方式,并结合示例代码,帮助开发者理解如何在实际项目中应用分库分表。
2. 分库分表概述
2.1 什么是分库分表?
分库分表是将原本存储在一个数据库表中的数据拆分到多个数据库或者多个表中的技术,主要有以下两种方式:
- 水平分库分表(Sharding):按照某个字段(如
user_id
)的范围或哈希值,将数据拆分到不同的数据库实例和表中。例如:- 用户ID
1~1000
存在db1.user_table_1
,1001~2000
存在db1.user_table_2
- 用户ID
2001~3000
存在db2.user_table_1
,3001~4000
存在db2.user_table_2
- 用户ID
- 垂直分库分表(Vertical Partitioning):按照业务模块拆分,如
用户表
和订单表
存储在不同的数据库中,适用于不同业务系统的数据隔离。
2.2 为什么要使用分库分表?
- 解决单库性能瓶颈:单个数据库无法承载大量并发请求,拆分数据库可以均衡流量压力。
- 提高读写效率:减少单表的数据量,提高查询和插入速度。
- 降低存储成本:可以使用多个较低配置的数据库实例替代高配数据库。
- 提升系统高可用性:多个数据库实例可以相互独立,避免单点故障。
3. 分库分表主键生成
在分库分表后,全局唯一的主键生成成为一个关键问题。常见的主键生成方式包括:
- UUID:全局唯一,但长度较长,存储开销大,不利于索引查询。
- 数据库自增ID:单机可用,但无法跨数据库唯一。
Snowflake(雪花算法):
- Twitter 提出的分布式 ID 生成算法,基于时间戳+机器ID+序列号
Go 实现示例:
package main import ( "fmt" "sync" "time" ) const ( workerIDBits = 5 datacenterIDBits = 5 sequenceBits = 12 maxWorkerID = -1 ^ (-1 << workerIDBits) maxDatacenterID = -1 ^ (-1 << datacenterIDBits) maxSequence = -1 ^ (-1 << sequenceBits) ) type Snowflake struct { mu sync.Mutex lastTimestamp int64 workerID int64 datacenterID int64 sequence int64 } func NewSnowflake(workerID, datacenterID int64) *Snowflake { return &Snowflake{ workerID: workerID, datacenterID: datacenterID, } } func (s *Snowflake) NextID() int64 { s.mu.Lock() defer s.mu.Unlock() timestamp := time.Now().UnixNano() / 1e6 if timestamp == s.lastTimestamp { s.sequence = (s.sequence + 1) & maxSequence if s.sequence == 0 { for timestamp <= s.lastTimestamp { timestamp = time.Now().UnixNano() / 1e6 } } } else { s.sequence = 0 } s.lastTimestamp = timestamp return ((timestamp << (workerIDBits + datacenterIDBits + sequenceBits)) | (s.datacenterID << (workerIDBits + sequenceBits)) | (s.workerID << sequenceBits) | s.sequence) } func main() { sf := NewSnowflake(1, 1) fmt.Println(sf.NextID()) }
- 数据库分段号段方式:
- 预分配 ID 段,比如
db1
生成1~10000
,db2
生成10001~20000
,适用于少量数据库的情况下。
- 预分配 ID 段,比如
4. 分库分表分页查询
分页查询在分库分表后变得复杂,因为数据分散在多个表和库中,常见的解决方案:
- 全库查询+合并排序:
SELECT * FROM ( SELECT id, name, create_time FROM db1.user_table_1 UNION ALL SELECT id, name, create_time FROM db1.user_table_2 ) AS temp ORDER BY create_time DESC LIMIT 10 OFFSET 0;
- 利用缓存:先查询主键 ID,再分库查询详细数据,减少跨库查询的开销。
5. 分库分表后的分布式事务
分布式事务的核心问题是多个数据库实例之间的事务一致性,常见解决方案:
- TCC(Try-Confirm-Cancel)模式
- 本地消息表+可靠消息队列
- Seata 分布式事务框架
示例:使用 golang
+ gorm
解决分布式事务问题:
func createOrder(db1, db2 *gorm.DB) error {
return db1.Transaction(func(tx *gorm.DB) error {
if err := tx.Exec("UPDATE users SET balance = balance - 100 WHERE id = ?", 1).Error; err != nil {
return err
}
if err := db2.Exec("INSERT INTO orders (user_id, amount) VALUES (?, ?)", 1, 100).Error; err != nil {
return err
}
return nil
})
}
9. 总结
本文详细介绍了分库分表的原理、主键生成、分页查询、分布式事务、无分表键查询、容量预估及高可用方案,并结合代码示例说明如何解决关键问题。
🔗 参考资料: