4.3 分库分表策略:单表千万级数据如何高效查询?
在构建高并发、大数据量的分布式系统时,单表存储千万级甚至亿级数据会带来严重的性能问题。查询速度慢、索引效率低、锁竞争激烈等问题会严重影响系统的响应时间和吞吐量。分库分表是一种有效的解决方案,通过将数据分散到多个数据库和表中,可以显著提升系统的性能和可扩展性。本节将深入探讨分库分表的策略和实现方法。
分库分表基础概念
什么是分库分表?
分库分表是数据库水平拆分的两种方式:
- 分库(Sharding):将数据按照一定规则分散到多个数据库实例中
- 分表(Partitioning):将数据按照一定规则分散到同一个数据库的多个表中
分库分表的优势
- 提升性能:分散数据存储和查询压力,减少单点瓶颈
- 增强可扩展性:通过增加数据库实例和表来扩展存储容量
- 提高可用性:单个数据库或表的故障不会影响整个系统
- 优化资源利用:充分利用多台服务器的计算和存储资源
分库分表的挑战
- 事务一致性:跨库事务难以保证ACID特性
- 查询复杂性:跨库跨表查询实现复杂
- 数据迁移:重新分片时数据迁移困难
- 维护成本:系统复杂度增加,维护成本上升
分库分表策略
1. 哈希分片
哈希分片通过哈希函数将数据均匀分布到多个分片中。
// HashShardingRule 哈希分片规则typeHashShardingRulestruct{// 分片键shardingKeystring// 分片数量shardingCountint// 数据源列表dataSources[]string// 表前缀tablePrefixstring}// ShardingResult 分片结果typeShardingResultstruct{// 数据源DataSourcestring// 表名TableNamestring// 分片索引ShardIndexint}// NewHashShardingRule 创建哈希分片规则funcNewHashShardingRule(shardingKeystring,shardingCountint,dataSources[]string,tablePrefixstring)*HashShardingRule{return&HashShardingRule{shardingKey:shardingKey,shardingCount:shardingCount,dataSources:dataSources,tablePrefix:tablePrefix,}}// CalculateShard 计算分片func(h*HashShardingRule)CalculateShard(keyinterface{})(*ShardingResult,error){// 将键转换为字符串keyStr:=fmt.Sprintf("%v",key)// 计算哈希值varhashValueuint32hasher:=fnv.New32a()if_,err:=hasher.Write([]byte(keyStr));err!=nil{returnnil,fmt.Errorf("failed to calculate hash: %w",err)}hashValue=hasher.Sum32()// 计算分片索引shardIndex:=int(hashValue)%h.shardingCount// 计算数据源索引dataSourceIndex:=shardIndex%len(h.dataSources)dataSource:=h.dataSources[dataSourceIndex]// 计算表名tableName:=fmt.Sprintf("%s_%d",h.tablePrefix,shardIndex)return&ShardingResult{DataSource:dataSource,TableName:tableName,ShardIndex:shardIndex,},nil}// GetShardIndexes 获取所有分片索引func(h*HashShardingRule)GetShardIndexes()[]int{indexes:=make([]int,h.shardingCount)fori:=0;i<h.shardingCount;i++{indexes[i]=i}returnindexes}2. 范围分片
范围分片根据数据的范围将数据分布到不同的分片中。
// RangeShardingRule 范围分片规则typeRangeShardingRulestruct{// 分片键shardingKeystring// 范围配置ranges[]*RangeConfig// 数据源列表dataSources[]string// 表前缀tablePrefixstring}// RangeConfig 范围配置typeRangeConfigstruct{// 起始值Startinterface{}// 结束值Endinterface{}// 分片索引ShardIndexint}// NewRangeShardingRule 创建范围分片规则funcNewRangeShardingRule(shardingKeystring,ranges[]*RangeConfig,dataSources[]string,tablePrefixstring)*RangeShardingRule{return&RangeShardingRule{shardingKey:shardingKey,ranges:ranges,dataSources:dataSources,tablePrefix:tablePrefix,}}// CalculateShard 计算分片func(r*RangeShardingRule)CalculateShard(keyinterface{})(*ShardingResult,error){shardIndex:=-1// 根据键值查找对应的分片switchk:=key.(type){caseint,int32,int64:keyValue:=toInt64(k)for_,rng:=ranger.ranges{iftoInt64(rng.Start)<=keyValue&&keyValue<=toInt64(rng.End){shardIndex=rng.ShardIndexbreak}}casestring:for_,rng:=ranger.ranges{ifstr,ok:=rng.Start.(string);ok&&strings.Compare(str,k)<=0{ifendStr,ok:=rng.End.(string);ok&&strings.Compare(k,endStr)<=0{shardIndex=rng.ShardIndexbreak}}}default:returnnil,fmt.Errorf("unsupported key type: %T",key)}ifshardIndex==-1{returnnil,fmt.Errorf("no shard found for key: %v",key)}// 计算数据源索引dataSourceIndex:=shardIndex%len(r.dataSources)dataSource:=r.dataSources[dataSourceIndex]// 计算表名tableName:=fmt.Sprintf("%s_%d",r.tablePrefix,shardIndex)return&ShardingResult{DataSource:dataSource,TableName:tableName,ShardIndex:shardIndex,},nil}// GetShardIndexes 获取所有分片索引func(r*RangeShardingRule)GetShardIndexes()[]int{indexes:=make([]int,0,len(r.ranges))for_,rng:=ranger.ranges{indexes=append(indexes,rng.ShardIndex)}returnindexes}// toInt64 将接口类型转换为int64functoInt64(valueinterface{})int64{switchv:=value.(type){caseint:returnint64(v)caseint32:returnint64(v)caseint64:returnvdefault:return0}}3. 标签分片
标签分片根据数据的标签属性将数据分布到不同的分片中。