一、前言
数据水平分片作为一种水平扩展策略,通过将数据分散到多个物理节点上,有效解决了存储容量和性能瓶颈问题。
而分片键(Sharding Key)作为数据分片的核心,决定了数据如何在各个分片中分布,直接影响到分片系统的性能、数据分布均衡性以及查询效率。
本文将分享4种数据分片策略。
二、哈希分片
2.1 原理
哈希分片通过对分片键值应用哈希函数,然后对分片数量取模,将数据均匀分布到各个分片。
scss
分片索引 = hash(分片键值) % 分片数量
2.2 SpringBoot实现
在SpringBoot中,我们可以使用ShardingSphere-JDBC实现哈希分片。首先添加依赖:
xml
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.1.2</version>
</dependency>
然后在application.yml
中配置哈希分片策略:
yaml
spring:
shardingsphere:
datasource:
names: ds0,ds1,ds2,ds3
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/order_db_0
username: root
password: password
# 其他数据源配置...
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds${0..3}.t_order
database-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order-id-hash
sharding-algorithms:
order-id-hash:
type: HASH_MOD
props:
sharding-count: 4
在实体类和Repository中使用:
kotlin
@Entity
@Table(name = "t_order")
public class Order {
@Id
private Long orderId; // 分片键
private Long userId;
private BigDecimal amount;
private String status;
// getters and setters
}
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
// 查询方法
}
2.3 优缺点分析
优点:
- 数据分布均匀:哈希函数确保数据在各分片上分布均衡
- 读写负载均衡:避免热点分片问题
- 简单易实现:配置简单,无需复杂的分片算法
缺点:
- 不支持范围查询:相邻的键值会被分散到不同分片,导致范围查询需要访问所有分片
- 分片扩缩容困难:增加或减少分片数量时,需要重新哈希和迁移大量数据
- 不易直观理解:数据分布不直观,难以预测特定记录在哪个分片
2.4 适用场景
- 读写操作频繁且均衡的OLTP系统
- 点查询(如通过主键或唯一索引查询)为主的应用
- 对数据分布均匀性要求高的系统
- 分片数量相对稳定的环境
三、范围分片
3.1 原理
范围分片键通过将数据按照分片键的值范围划分到不同的分片中。每个分片负责存储特定范围内的数据,数据在逻辑上保持有序。
markdown
如:订单ID 1-1000000 存储在分片1
订单ID 1000001-2000000 存储在分片2
...
3.2 SpringBoot实现
使用ShardingSphere-JDBC实现范围分片:
yaml
spring:
shardingsphere:
datasource:
names: ds0,ds1,ds2,ds3
# 数据源配置...
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds${0..3}.t_order
database-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order-id-range
sharding-algorithms:
order-id-range:
type: RANGE
props:
strategy: standard
range-lower: 0,1000000,2000000,3000000
range-upper: 999999,1999999,2999999,3999999
自定义更复杂的范围分片算法:
kotlin
@Component
public class CustomRangeShardingAlgorithm implements StandardShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
Long orderId = shardingValue.getValue();
for (String targetName : availableTargetNames) {
if (targetName.endsWith("0") && orderId <= 1000000) {
return targetName;
} else if (targetName.endsWith("1") && orderId > 1000000 && orderId <= 2000000) {
return targetName;
} else if (targetName.endsWith("2") && orderId > 2000000 && orderId <= 3000000) {
return targetName;
} else if (targetName.endsWith("3") && orderId > 3000000) {
return targetName;
}
}
throw new UnsupportedOperationException("No available target name for order id: " + orderId);
}
// 其他必要的方法实现...
}
3.3 优缺点分析
优点:
- 支持范围查询:同一范围的数据存储在同一分片,范围查询效率高
- 数据局部性好:相关数据聚集在一起,提高查询效率
- 易于理解和维护:分片规则直观,数据分布清晰
缺点:
- 数据倾斜风险:如果数据分布不均,可能导致某些分片负载过重
- 热点分片问题:新数据往往落在最新范围的分片上,造成访问热点
- 分片边界固定:预先定义的范围边界难以动态调整
3.4 适用场景
- 时间序列数据或自增ID数据的存储
- 范围查询频繁的应用
- 历史数据访问频率低但需要保留的系统
- 可以预测数据增长模式的业务
四、复合分片
4.1 原理
复合分片键使用多个字段的组合作为分片依据,提供更精细和灵活的分片控制。可以同时考虑多个业务维度,使分片更贴合业务特性。
常见的复合分片策略包括:
- 多字段哈希组合
- 一级字段范围+二级字段哈希
- 字段组合后再应用分片算法
4.2 SpringBoot实现
使用ShardingSphere-JDBC实现复合分片:
yaml
spring:
shardingsphere:
datasource:
names: ds0,ds1,ds2,ds3
# 数据源配置...
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds${0..3}.t_order
database-strategy:
complex:
sharding-columns: user_id,order_date
sharding-algorithm-name: complex-algorithm
sharding-algorithms:
complex-algorithm:
type: CLASS_BASED
props:
strategy: COMPLEX
algorithmClassName: com.example.CustomComplexShardingAlgorithm
自定义复合分片算法:
dart
@Component
public class CustomComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm<Comparable<?>> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
ComplexKeysShardingValue<Comparable<?>> shardingValue) {
Map<String, Collection<Comparable<?>>> columnNameAndShardingValuesMap =
shardingValue.getColumnNameAndShardingValuesMap();
Collection<Comparable<?>> userIds = columnNameAndShardingValuesMap.get("user_id");
Collection<Comparable<?>> orderDates = columnNameAndShardingValuesMap.get("order_date");
List<String> result = new ArrayList<>();
for (Comparable<?> userId : userIds) {
for (Comparable<?> orderDate : orderDates) {
// 使用用户ID和订单日期的组合确定分片
LocalDate date = (LocalDate) orderDate;
long hash = (long) userId * 31 + date.getYear() * 12 + date.getMonthValue();
int shardingKey = (int) (hash % 4); // 分4个片
for (String targetName : availableTargetNames) {
if (targetName.endsWith(String.valueOf(shardingKey))) {
result.add(targetName);
}
}
}
}
return result;
}
}
实体类定义:
kotlin
@Entity
@Table(name = "t_order")
public class Order {
@Id
private Long orderId;
private Long userId; // 复合分片键之一
private LocalDate orderDate; // 复合分片键之一
private BigDecimal amount;
private String status;
// getters and setters
}
4.3 优缺点分析
优点:
- 分片更精细:可以结合多个业务维度进行数据分布
- 数据分布更均衡:减少单维度分片可能带来的数据倾斜
- 查询灵活性:支持多种条件的查询优化
- 更贴合业务:可以根据实际业务特性定制分片策略
缺点:
- 实现复杂:需要自定义复杂的分片算法
- 维护困难:分片逻辑复杂,难以理解和维护
- 查询成本高:如果查询条件不包含所有分片键,可能需要查询多个分片
- 测试难度大:需要全面测试各种查询场景
4.4 适用场景
- 单一分片键无法满足均衡性要求的系统
- 多维度查询频繁的应用
- 数据有明显多维度特性的业务
- 需要精细化控制数据分布的场景
五、时间序列分片
5.1 原理
时间序列分片键使用时间相关字段(如创建时间、交易日期)作为分片依据,通常将特定时间段的数据存储在同一分片中。这种方式特别适合具有明显时间属性的数据,如日志、订单、交易记录等。
常见的时间分片策略包括:
- 按年/月/周/日分片
- 时间窗口滚动分片
- 时间+其他字段组合分片
5.2 SpringBoot实现
使用ShardingSphere-JDBC实现时间序列分片:
yaml
spring:
shardingsphere:
datasource:
names: ds0,ds1,ds2,ds3
# 数据源配置...
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds${0..3}.t_order
database-strategy:
standard:
sharding-column: create_time
sharding-algorithm-name: time-sharding
sharding-algorithms:
time-sharding:
type: INTERVAL
props:
datetime-pattern: yyyy-MM-dd HH:mm:ss
datetime-lower: 2023-01-01 00:00:00
datetime-upper: 2023-12-31 23:59:59
sharding-suffix-pattern: yyyyMM
datetime-interval-amount: 3
datetime-interval-unit: MONTHS
自定义更复杂的时间分片算法:
ini
@Component
public class QuarterlyShardingAlgorithm implements StandardShardingAlgorithm<Date> {
private static final Map<Integer, Integer> QUARTER_MAP = new HashMap<>();
static {
// 将月份映射到季度
QUARTER_MAP.put(1, 1);
QUARTER_MAP.put(2, 1);
QUARTER_MAP.put(3, 1);
QUARTER_MAP.put(4, 2);
QUARTER_MAP.put(5, 2);
QUARTER_MAP.put(6, 2);
QUARTER_MAP.put(7, 3);
QUARTER_MAP.put(8, 3);
QUARTER_MAP.put(9, 3);
QUARTER_MAP.put(10, 4);
QUARTER_MAP.put(11, 4);
QUARTER_MAP.put(12, 4);
}
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {
Date date = shardingValue.getValue();
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1;
int quarter = QUARTER_MAP.get(month);
// 将分片策略设计为按季度循环到不同数据源
// 例如:2023Q1->ds0, 2023Q2->ds1, 2023Q3->ds2, 2023Q4->ds3, 2024Q1->ds0, ...
int shardIndex = ((year - 2023) * 4 + quarter - 1) % 4;
for (String targetName : availableTargetNames) {
if (targetName.endsWith(String.valueOf(shardIndex))) {
return targetName;
}
}
throw new UnsupportedOperationException("No available target name for date: " + date);
}
// 其他必要的方法实现...
}
实体类定义:
kotlin
@Entity
@Table(name = "t_order")
public class Order {
@Id
private Long orderId;
private Long userId;
@Temporal(TemporalType.TIMESTAMP)
private Date createTime; // 时间分片键
private BigDecimal amount;
private String status;
// getters and setters
}
5.3 优缺点分析
优点:
- 数据生命周期管理:便于实现数据的生命周期管理和归档
- 高效的时间范围查询:同一时间段的数据位于同一分片,时间范围查询高效
- 分片扩展自然:随着时间推移自然扩展到新分片
- 冷热数据分离:可以针对不同时间段的数据采用不同的存储策略
缺点:
- 数据分布不均:如果时间分布不均匀,可能导致分片数据量差异大
- 写入热点问题:当前时间段的分片成为写入热点
- 历史分片访问少:旧分片利用率低但仍占用资源
- 跨时间段查询复杂:大范围的时间查询可能需要访问多个分片
5.4 适用场景
- 具有明显时间属性和时间局部性的数据
- 日志、订单、交易等时间序列数据
- 需要定期归档历史数据的系统
- 查询通常限定在特定时间范围内的应用
六、分片策略对比
6.2 适用场景分析
分片类型 | 数据分布均衡性 | 查询效率 | 扩展性 | 实现复杂度 | 最适合场景 |
---|---|---|---|---|---|
哈希分片 | 高 | 点查询高,范围查询低 | 中 | 低 | 点查询为主的OLTP系统 |
范围分片 | 中低 | 范围查询高,点查询中 | 中 | 中 | 范围查询频繁的系统 |
复合分片 | 高 | 多维查询高,单维查询中 | 高 | 高 | 多维查询和复杂业务场景 |
时间序列分片 | 中 | 时间范围查询高 | 高 | 中 | 时间相关数据和归档需求 |
6.3 扩容影响分析
分片类型 | 增加分片的数据迁移量 | 查询路由改变 | 应用改造复杂度 |
---|---|---|---|
哈希分片 | 高(约50%数据需迁移) | 大 | 中 |
范围分片 | 低(仅需调整边界数据) | 小 | 低 |
复合分片 | 中高(取决于算法设计) | 中 | 高 |
时间序列分片 | 低(仅影响新数据) | 小 | 低 |
七、总结
在实际应用中,应根据业务特点、查询模式、性能需求和扩展预期来选择最合适的分片策略。
无论选择哪种分片策略,都应保持分片逻辑的简洁性和可维护性,并在系统设计初期就考虑未来的扩展需求。
通过合理选择分片键,结合SpringBoot和ShardingSphere等工具,可以构建出既满足业务需求又具备良好扩展性的应用。