本文的 原始地址 ,传送门
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
每天新增100w订单,如何的分库分表?
10亿级数据,如何的分库分表?
所以,这里尼恩给大家做一下系统化、体系化的梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”。也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典PDF》V173版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,后台回复:领电子书
尼恩解密:面试官的考察意图
(1)对分布式数据库设计的理解
分库分表是解决大规模数据存储和高性能查询的常见策略。面试官希望了解候选人是否熟悉这些概念,并能够根据业务需求设计合理的分库分表方案。
考察候选人是否能够权衡不同的分库分表策略(如水平分片、垂直分片、时间范围分表等)的优缺点,并选择最适合的方案。
(2)对业务需求的分析能力
每天新增100万订单是一个具体的业务场景,面试官希望候选人能够结合实际业务需求进行分析,例如:
- 订单数据的访问模式(读多写少、热点数据等)。
- 数据的生命周期(短期高频访问、长期归档)。
数据一致性要求(是否需要分布式事务)。
考察候选人是否能够从业务角度出发,设计出既能满足性能需求,又能兼顾扩展性和维护性的方案。
(3)对技术细节的掌握
分库分表涉及多个技术细节,如数据分片键的选择、分布式事务处理、数据迁移、查询优化等。
面试官希望了解候选人是否熟悉这些技术细节,并能够针对具体问题提出解决方案。例如:
- 如何避免全库路由问题?
- 如何处理跨表查询?
- 如何保证数据一致性?
一:分库分表 背景分析
1、场景分析
在当今数字化商业环境中,各类电商平台、在线服务提供商以及金融交易系统等业务场景下,订单处理是核心业务流程之一。
随着业务的快速发展和市场规模的不断扩大,订单数据量呈现出爆发式增长的态势。
以一个中等规模以上的电商平台为例,每天新增的订单数量可能达到 100 万条, 甚至更多。
这些订单数据包含了丰富的信息,如订单编号、用户信息、商品详情、交易金额、交易时间等,对于 运营管理、决策分析以及客户服务等方面都具有重要的价值。
2、 数据增长预测
短期趋势
在未来 1 - 2 年内,随着市场推广力度的加大、用户数量的持续增加以及业务拓展到新的领域,预计订单数据将以每年 30% - 50% 的速度增长。
这意味着: 1 - 2 年内 ,每天新增的订单数量可能在达到 130 万 - 150 万条。 订单总量 在 1亿级别。
中期趋势
在 3 - 5 年的中期阶段,随着业务的稳定发展和市场份额的进一步扩大,订单数据的增长速度可能会有所放缓,但仍然会保持在每年 20% - 30% 的水平。
这意味着: 在 3 - 5 年内 ,每天新增订单数量可能接近 250 万条。订单总量 在 10亿级别。
长期趋势
从 5 - 10 年的长期来看,考虑到市场的饱和以及竞争的加剧,订单数据的增长速度可能会逐渐稳定在每年 10% - 20% 左右。
这意味着: 在 5 - 10 年内 ,每天新增订单数量可能接近 600 万条。订单总量 在 100亿级别。
3、面临的问题与挑战
1:查询劣化
随着订单数据量的不断增加,单一数据库表中的数据行数会急剧增长。
当数据量达到数百万甚至数千万级别时,简单的查询操作(如按订单编号查询、按用户 ID 查询订单列表等)的响应时间会显著增加。
例如,在一个包含 1000 万条订单记录的单表中,一次简单的查询操作可能需要数秒甚至数十秒才能完成,这严重影响了系统的实时性和用户体验。
2:写入劣化
在高并发场景下,单一数据库的写入性能会受到严重限制,可能会出现写入延迟、数据丢失等问题。
例如,当多个用户同时下单时,数据库可能无法及时处理所有的写入请求,导致订单处理失败或延迟。
单一数据库的并发处理能力也是有限的,无法满足日益增长的高并发订单处理需求。
当并发请求数超过数据库的处理能力时,系统会出现性能下降、响应时间延长等问题,甚至可能导致系统崩溃。
3:容量瓶颈
单一数据库的存储容量是有限的,随着订单数据的不断积累,很快会达到数据库的存储上限。一旦存储容量不足,就需要进行数据库扩容,这不仅会带来高昂的成本,还会影响系统的正常运行。
4:数据备份困难
在数据量巨大的情况下,数据库的备份和恢复操作变得非常困难和耗时。
一次完整的数据库备份可能需要数小时甚至数天才能完成,而且备份数据的存储和管理也需要大量的资源。
5:数据清理难困难
随着时间的推移,大量的历史订单数据会占据数据库的存储空间,影响系统的性能。
但在单一数据库中,进行数据归档和清理操作会非常复杂,需要考虑数据的关联性、业务需求等多方面因素。
二、分库分表三大策略
一致性hash取模策略
按照时间范围分库分表
组合模式分库分表(ID取模分库、时间范围分表)实操
三、一致性hash取模分库分表
20亿数据,128张表 , 按照 id 一致性hash取模分库分表,如何设计?
1 前期规划
- 分库数量:
假设我们有N
个数据库,可以根据实际的硬件资源和性能需求来确定。一般来说,可以先初步设定为 8 个库 。
- 分表数量:
由于总共有 128 张表,若分 8 个库,则每个库中平均有 16 张表。
- 一致性哈希算法:
选择一个合适的一致性哈希算法库,如MurmurHash
等。该算法能将数据的id
映射为一个固定范围(通常是 0 到 2^32-1)内的哈希值。
2 分库策略
分库数量:根据数据量和业务需求,建议分为 16个库(16是一个便于扩展的数字,后续可以按需扩容)。
分库规则:对ID进行一致性哈希取模,公式为:
db_index = hash(id) % 16
其中,hash(id)
使用一致性哈希算法(如MurmurHash)保证数据分布均匀。
3 分表策略
计算哈希值与取模
对于每一条数据的id
,使用选定的一致性哈希算法计算出其哈希值。
然后,将 哈希值对数据库数量N
取模,得到的结果即为该数据应该存储的数据库编号。
例如,若取模结果为 3,则该数据应存储在第 3 个数据库中。
在确定了数据库之后,再将哈希值对每个数据库中的表数量取模,得到该数据应该存储的表编号。例如,若每个库中有 16 张表,取模结果为 12,则该数据应存储在对应数据库的第 12 张表中。
分表数量:每个库分为 8张表,总表数为 16库 × 8表 = 128表。
分表规则:在分库的基础上,对ID进行二次取模,公式为:
table_index = hash(id) % 8
最终表名为:
db_index.table_{table_index}
4:一致性哈希 优势
4.1. 数据分布相对均匀
一致性哈希算法能够将数据均匀地映射到不同的数据库或表中。通过对数据的键(如 id
)进行哈希计算,使得各个节点(数据库或表)承担的数据量相对均衡。例如,在一个分布式数据库系统中,有多个数据库节点,使用一致性哈希取模可以避免某些节点数据过多而其他节点数据过少的情况,有效提高系统的整体性能和资源利用率。
4.2. 节点增减时数据迁移量小
当需要增加或减少数据库节点时,传统的取模算法可能需要对大量数据进行重新计算和迁移。而一致性哈希算法在节点变化时,只有部分数据需要迁移。例如,在一个有 10 个节点的系统中,增加一个新节点,只会影响到该新节点在哈希环上相邻的部分数据,其他大部分数据仍然可以保持在原节点,大大减少了数据迁移的工作量和对系统的影响。
4.3. 高扩展性
一致性哈希取模分库分表具有良好的扩展性。随着业务的发展,数据量不断增加,可以方便地通过增加数据库节点来扩展系统的存储和处理能力。
新节点可以平滑地加入到系统中,不会对现有数据的分布和访问造成太大的影响,保证了系统的可扩展性和灵活性。
4.4. 容错性强
当某个数据库节点出现故障时,一致性哈希算法可以将原本分配到该节点的数据自动转移到其他节点上。
由于数据的迁移范围较小,对系统的影响也相对较小,能够在一定程度上保证系统的可用性和数据的可靠性。
5:一致性哈希 劣势
1. 算法复杂度较高
一致性哈希算法的实现相对复杂,需要考虑哈希函数的选择、哈希环的构建和维护等问题。
与简单的取模算法相比,一致性哈希算法的计算量更大,特别是在高并发场景下,可能会对系统的性能产生一定的影响。
2. 数据分布并非绝对均匀
虽然一致性哈希算法可以使数据分布相对均匀,但在实际应用中,由于哈希函数的特性和节点数量的限制,可能会出现数据分布不均匀的情况。
例如,当节点数量较少时,哈希环上的节点分布可能不够均匀,导致部分节点承担的数据量相对较大。为了缓解这个问题,通常需要引入虚拟节点的概念,但这会增加系统的复杂度和管理成本。
3. 维护成本较高
一致性哈希取模分库分表需要对哈希环和节点信息进行维护。
当节点发生变化时,需要及时更新哈希环的状态,确保数据能够正确地路由到相应的节点。
此外,还需要对虚拟节点进行管理和调整,以保证数据的均匀分布。这些维护工作增加了系统的管理成本和复杂度。
4. 难以处理范围查询
一致性哈希算法是基于数据的键进行哈希计算和数据分片的,对于范围查询(如按照时间范围、数值范围进行查询)的支持较差。
在进行范围查询时,可能需要对多个节点进行扫描和合并结果,增加了查询的复杂度和时间开销。
相比之下,按范围分库分表更适合处理范围查询。
四:按照时间范围分库分表
在20亿数据的情况下,按照时间范围进行分库分表的设计,可以充分利用时间的有序性和业务需求,实现数据的高效存储与查询。
1.分库策略
分库数量:建议分为16个库。
分库规则:根据时间范围划分库。例如,按年分库:
db_index = (year - 2023) % 16
其中,year 是数据的时间字段(如创建时间)的年份部分。
2.分表策略
- 分表数量:
每个库分为 8张表,总表数为 16库 × 8表 = 128表。
- 分表规则
在分库的基础上,进一步按时间范围分表。
例如,按月分表:
table_index = (month - 1)
最终表名为:
db_{db_index}.table__{table_index}
3. 路由策略
时间单位:根据业务需求选择合适的时间单位(如年、月、周、天)。
时间单位:根据业务需求选择合适的时间单位(如年、月、周、天)。
时间范围映射:为每个时间范围分配一个唯一的db_index和table_index
例如:
2023年1月:
db_index = 0
,table_index = 0
2023年2月:
db_index = 0
,table_index = 1
...
2024年1月:
db_index = 1
,table_index = 0
4. 时间范围进行分库优点
(1) 提升查询性能
按时间范围分表后,查询特定时间段的数据时,可以直接定位到对应的表,避免了全表扫描,显著提升查询效率。
(2) 方便数据归档和备份
历史数据可以按时间范围归档到不同的表或库中,便于管理和备份,同时可以减少主表的存储压力。
(3) 易于扩容和扩展
随着时间推移,可以按需新增表或库来存储新数据,扩容过程相对简单,且对现有数据影响较小。
(4) 适合时间序列数据
对于日志、订单、监控数据等时间序列数据,按时间范围分表可以更好地利用数据的时间局部性,提高读写性能。
5. 时间范围进行分库不足
(1) 热点问题
最新的时间范围表(如最近一个月的表)可能会集中大部分的读写请求,导致热点问题,影响性能。
(2) 跨表查询复杂
如果查询涉及多个时间范围,需要在多个表中分别执行查询并合并结果,增加了开发和维护的复杂性。
(3) 数据迁移和维护成本高
随着时间推移,表的数量会不断增加,需要定期清理老旧数据,且数据迁移和备份的复杂度也会增加。
(4) 应用逻辑复杂度增加
分表策略需要在应用层或中间件中实现数据路由逻辑,增加了系统的复杂性。
(5) 分布式事务问题
如果涉及跨库操作,可能需要引入分布式事务管理工具,增加了系统的复杂性和开发成本。
五、组合模式分库分表(ID取模分库、时间范围分表)
20亿数据,128张表 , 按照 组合模式(ID取模分库、时间范围分表)分库分表,如何设计?
通过ID取模分库,将数据均匀分布到多个数据库中;
按时间范围分表,将数据按时间段划分到不同的表中。
以下是具体的设计思路和实现步骤:
1.前期规划
分库数量需要根据业务的并发量、硬件资源以及未来的扩展性来综合考虑。
一般来说,如果数据量为 20 亿且有一定的并发访问,可先设定为 8 个数据库,分别命名为 db_0
到 db_7
。
根据业务查询特点和数据增长规律,选择合适的时间粒度进行分表。若业务常按月份进行数据统计和查询,可按月分表。
表名采用 t_YYYYMM
的格式,如 t_202502
代表 2025 年 2 月的数据表。
2. 分库策略:ID取模分库
- 分片键选择:
选择业务中具有高基数的字段(如用户ID、订单ID)作为分片键。
- 分库数量:
根据数据量和业务需求,假设分库数量为N
(例如4个库)。
- 分库算法:
使用哈希取模算法,shard_id = id % N
,将数据均匀分布到不同的库中。
3. 分表策略:时间范围分表
- 分表依据:
根据时间字段(如create_time
)进行分表,例如按月分表。
- 表命名规则:
表名可以设计为order_YYYYMM
,例如order_202401
、order_202402
。
- 分表数量:
假设按月分表,一年最多12张表,结合业务需求调整。
60分 (菜鸟级) 答案
尼恩提示,讲完 3大分库分表策略, 可以得到 60分了。
但是要直接拿到大厂offer,或者 offer 直提,需要 120分答案。
尼恩带大家继续,挺进 120分,让面试官 口水直流。
六、如何使用 shardingsphere 实现组合策略分库分表
ShardingSphere 提供三种主要的使用模式,分别是 JDBC 模式、Proxy 模式和 Sidecar 模式。
以下是它们的特点:
1. JDBC 模式 (Sharding-JDBC)
- 定位:作为轻量级 Java 框架,提供增强版的 JDBC 驱动。
- 特点:直接嵌入 Java 应用中,以 Jar 包形式提供服务,无需额外部署。完全兼容 JDBC 和各种 ORM 框架,使用客户端直连数据库。
- 适用场景:适合基于 Java 的应用开发,尤其是已有系统需要无缝集成分库分表功能时。
2. Proxy 模式(ShardingSphere)
- 定位:类似于 Mycat,是一个透明化的数据库代理端。
- 特点:封装了数据库的二进制协议,支持异构语言。提供 MySQL 和 PostgreSQL 版本,支持使用兼容 MySQL/PostgreSQL 协议的客户端(如 MySQL Command Client、MySQL Workbench、Navicat 等)进行操作,对 DBA 更友好。
- 适用场景:适合多种语言的开发环境,尤其是需要跨语言支持的场景。
3. Sidecar 模式
- 定位:作为 Kubernetes 的云原生数据库代理。
- 特点:以 Sidecar 的形式代理所有对数据库的访问,提供无中心、零侵入的数据库交互层(Database Mesh)。目前仍处于规划阶段。
- 适用场景:适合 Kubernetes 环境下的云原生应用。
在实际项目中, 尼恩选择了 JDBC 模式(Sharding-JDBC),因为它与现有的 Java 应用集成最为便捷,且完全兼容现有的 ORM 框架。
Sharding-JDBC 5 种分片策略
分片策略主要包含分片键和分片算法。
或者说,“分片键 + 分片算法”,这两者组合起来就是所谓的分片策略。
Sharding - JDBC 是一个开源的分布式数据库中间件,它提供了 5 种分片策略,以下为你详细介绍:
1. 标准分片策略(StandardShardingStrategy)
这是一种较为常用的分片策略,适用于单分片键的场景,它能对 SQL 语句中的多种比较和范围操作进行分片处理。
支持的 SQL 操作
支持 =
, >
, <
, >=
, <=
, IN
和 BETWEEN AND
这些操作符的分片操作。
分片算法
- PreciseShardingAlgorithm:必选算法,主要用于处理
=
和IN
操作的分片。当 SQL 语句中使用=
或IN
来筛选数据时,该算法会根据分片键的值精确地确定数据应该存储在哪个分片上。 - RangeShardingAlgorithm:可选算法,用于处理
BETWEEN AND
,>
,<
,>=
,<=
操作的分片。如果不配置这个算法,SQL 中的BETWEEN AND
操作将按照全库路由处理,也就是会在所有分片上进行查询。
示例代码(Java)
// 精确分片算法示例
public class MyPreciseShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
for (String each : availableTargetNames) {
if (each.endsWith(shardingValue.getValue() % 2 + "")) {
return each;
}
}
throw new UnsupportedOperationException();
}
}
// 范围分片算法示例
public class MyRangeShardingAlgorithm implements RangeShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Long> shardingValue) {
Collection<String> result = new LinkedHashSet<>();
Range<Long> range = shardingValue.getValueRange();
for (Long i = range.lowerEndpoint(); i <= range.upperEndpoint(); i++) {
for (String each : availableTargetNames) {
if (each.endsWith(i % 2 + "")) {
result.add(each);
}
}
}
return result;
}
}
2. 复合分片策略(ComplexShardingStrategy)
当需要使用多个分片键进行分片时,就可以使用复合分片策略。它为处理复杂的分片逻辑提供了支持。
支持的 SQL 操作
支持 =
, >
, <
, >=
, <=
, IN
和 BETWEEN AND
这些操作符的分片操作。
特点
由于多分片键之间的关系复杂,该策略没有进行过多的封装,而是直接将分片键值组合以及分片操作符传递给分片算法,由应用开发者根据具体业务需求自行实现分片逻辑,提供了很大的灵活性。
示例代码(Java)
public class MyComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Long> shardingValue) {
// 自定义多分片键的分片逻辑
Collection<String> result = new ArrayList<>();
// 实现具体的分片逻辑
return result;
}
}
3. Hint 分片策略(HintShardingStrategy)
在某些情况下,无法从 SQL 语句中提取分片键的值,或者希望手动指定分片的目标,这时就可以使用 Hint 分片策略。
分片方式
通过 HintManager
来指定分片值,而不是从 SQL 语句中提取分片值。这种方式可以绕过 SQL 解析,直接指定数据要存储或查询的分片。
示例代码
try (HintManager hintManager = HintManager.getInstance()) {
hintManager.addDatabaseShardingValue("table_name", 1);
// 执行 SQL 操作
}
4. 不分片策略(NoneShardingStrategy)
这是一种最简单的策略,即不进行分片操作。当某些表不需要进行数据分片,或者只在一个数据库或分片中存储时,可以使用该策略。
示例配置(YAML)
tables:
non_sharding_table:
actualDataNodes: ds_0.non_sharding_table
tableStrategy:
none:
5. 行表达式分片策略(InlineShardingStrategy)
行表达式分片策略是一种基于 Groovy 表达式的简单分片策略,它允许通过简单的表达式来定义分片规则。
特点
使用简洁,适合简单的分片场景,通过配置行表达式可以快速实现分片逻辑。
示例配置(YAML)
tables:
order_table:
actualDataNodes: ds_${
0..1}.order_table_${
0..1}
tableStrategy:
inline:
shardingColumn: order_id
algorithmExpression: order_table_${
order_id % 2}
这 5 种分片策略各有特点,开发者可以根据具体的业务需求和数据特点选择合适的分片策略来实现数据的分布式存储和查询。
七、实操:组合模式分库分表(ID取模分库、时间范围分表)实操
以订单为例
db 以 订单id 取模 分库
table 以 订单创建时间 分表
写两个类,实现 PreciseShardingAlgorithm 精确分片算法,一个用于db取模,一个用于table按月份分片。
分库算法 DBShardingAlgorithm (取模分库)
DBShardingAlgorithm 实现 PreciseShardingAlgorithm 接口,用于根据订单 ID 进行数据库的精确分片,数据库分库。
PreciseShardingAlgorithm: 主要用于处理 =
和 IN
操作的分片。当 SQL 语句中使用 =
或 IN
来筛选数据时,该算法会根据分片键的值精确地确定数据应该存储在哪个分片上。
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
// 实现 PreciseShardingAlgorithm 接口,用于根据订单 ID 进行数据库的精确分片
public class DBShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
// 使用 SLF4J 日志框架记录日志
private static final Logger log = LoggerFactory.getLogger(DBShardingAlgorithm.class);
/**
* 实现精确分片的核心方法
* @param availableTargetNames 可用的数据库名称集合
* @param shardingValue 分片键的值,包含逻辑表名、分片列名和具体的值
* @return 分片后要使用的数据库名称
*/
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
// 记录日志,表示进入数据库精确分片算法
log.info("DB PreciseShardingAlgorithm");
// 遍历可用的数据库名称集合,并记录每个数据库名称,方便调试查看
availableTargetNames.forEach(item -> log.info("actual node db:{}", item));
// 记录逻辑表名和分片列名,方便调试
log.info("logic table name:{},rout column:{}", shardingValue.getLogicTableName(), shardingValue.getColumnName());
// 记录分片键的具体值,方便调试
log.info("column value:{}", shardingValue.getValue());
// 获取订单 ID
long orderId = shardingValue.getValue();
// 对订单 ID 进行取模操作,确定要使用的数据库索引,这里是hash( orderId) 对 16 取模
long dbIndex = hash( orderId) % 16;
// 生成目标数据库名称,格式为 "db_" 加上数据库索引
String targetDb = "db_" + dbIndex;
// 遍历可用的数据库名称集合
for (String each : availableTargetNames) {
// 如果找到与目标数据库名称匹配的数据库
if (each.equals(targetDb)) {
// 返回该数据库名称
return each;
}
}
// 如果没有找到匹配的数据库,抛出异常
throw new IllegalArgumentException();
}
}
TableShardingAlgorithm
月份分表
实现 PreciseShardingAlgorithm 接口,用于根据订单创建时间进行表的精确分片
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
// 实现 PreciseShardingAlgorithm 接口,用于根据订单创建时间进行表的精确分片
public class TableShardingAlgorithm implements PreciseShardingAlgorithm<Date> {
// 使用 SLF4J 日志框架记录日志
private static final Logger log = LoggerFactory.getLogger(TableShardingAlgorithm.class);
/**
* 实现精确分片的核心方法
* @param availableTargetNames 可用的表名称集合
* @param shardingValue 分片键的值,包含逻辑表名、分片列名和具体的值
* @return 分片后要使用的表名称
*/
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {
// 记录日志,表示进入表精确分片算法
log.info("table PreciseShardingAlgorithm");
// 遍历可用的表名称集合,并记录每个表名称,方便调试查看
availableTargetNames.forEach(item -> log.info("actual node table:{}", item));
// 记录逻辑表名和分片列名,方便调试
log.info("logic table name:{},rout column:{}", shardingValue.getLogicTableName(), shardingValue.getColumnName());
// 记录分片键的具体值,方便调试
log.info("column value:{}", shardingValue.getValue());
// 初始化表名前缀,为逻辑表名加上 "_"
String tbName = shardingValue.getLogicTableName() + "_";
// 获取订单创建时间
Date date = shardingValue.getValue();
// 创建日期格式化对象,用于格式化年份
SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");
// 创建日期格式化对象,用于格式化月份
SimpleDateFormat monthFormat = new SimpleDateFormat("MM");
// 格式化年份
String year = yearFormat.format(date);
// 格式化月份
String month = monthFormat.format(date);
// 生成完整的表名,格式为逻辑表名加上年份和月份
tbName = tbName + year + "_" + month;
// 记录生成的表名,方便调试
log.info("tb_name:{}", tbName);
// 遍历可用的表名称集合
for (String each : availableTargetNames) {
// 如果找到与生成的表名匹配的表
if (each.equals(tbName)) {
// 返回该表名称
return each;
}
}
// 如果没有找到匹配的表,抛出异常
throw new IllegalArgumentException();
}
}
application.properties
配置
db 以 订单id 取模
table 以 订单创建时间 分库
//配置 16 个数据源,数据源名称分别为 db_0 到 db_15
spring.shardingsphere.datasource.names=db_0,db_1,db_2,db_3,db_4,db_5,db_6,db_7,db_8,db_9,db_10,db_11,db_12,db_13,db_14,db_15
//设置默认数据源为 db_0
spring.shardingsphere.sharding.default-data-source-name=db_0
// 配置 db_0 数据源的基本信息
// 数据源类型为 HikariDataSource,这是一个高性能的 JDBC 连接池
spring.shardingsphere.datasource.db_0.type=com.zaxxer.hikari.HikariDataSource
// 数据库驱动类名,使用 MySQL 的 JDBC 驱动
spring.shardingsphere.datasource.db_0.driver-class-name=com.mysql.cj.jdbc.Driver
// 数据库连接 URL,指定数据库地址、端口、数据库名以及一些连接参数
spring.shardingsphere.datasource.db_0.jdbc-url=jdbc:mysql://localhost:3306/db_0?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&relaxAutoCommit=true&zeroDateTimeBehavior=convertToNull
// 数据库用户名
spring.shardingsphere.datasource.db_0.username=root
// 数据库密码
spring.shardingsphere.datasource.db_0.password=root
// 其他 15 个数据源配置... (这里省略了其他数据源的详细配置,可按照 db_0 的配置方式依次添加)
// 配置 t_order 表的实际数据节点,数据会分布在 16 个数据库中,每个数据库中有 2025 年到 2026 年每个月的表
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=db_$->{
0..15}.t_order_$->{
2025..2026}_$->{
1..12}
// 自定义 分片算法
// 分库分片健 database-strategy 数据库策略
// 指定分库的分片列名为 order_id
spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.sharding-column=order_id
// 自定义 分片 策略
// 指定分库的精确分片算法类的全限定名
spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.precise-algorithm-class-name=com.example.sharding.DBShardingAlgorithm
// table-strategy 表 的 策略
// 指定分表的分片列名为 create_time
spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.sharding-column=create_time
// 指定分表的精确分片算法类的全限定名
spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.precise-algorithm-class-name=com.example.sharding.TableShardingAlgorithm
// 使用 SNOWFLAKE 算法生成主键
// 指定主键生成的列名为 order_id
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
// 指定主键生成算法类型为 SNOWFLAKE
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE
// 雪花算法的 workId 机器为标识 0 - 1024
// 设置雪花算法的工作机器 ID 为 123
spring.shardingsphere.sharding.tables.t_order.key-generator.props.worker.id=123
大功告成
八:优化、如何避免ID查询时的全库路由问题?
通过时间范围分表和ID取模分库的组合策略,可以有效提升查询性能和数据管理效率。
然而,如果没有时间 只有id,会发生全库路由问题。
如何优化 呢?
可以采用以下优化策略:
1. 引入异构索引表
异构索引表是一种“用空间换时间”的设计。
通过在写入数据时,将ID和时间范围信息同步存储到一张索引表中,可以在查询时快速定位到目标数据所在的库和表。
1 创建索引表:
创建一张按时间范围分区的索引表,存储时间范围
和对应的ID列表
。
CREATE TABLE order_time_index (
day DATE NOT NULL,
order_id VARCHAR(64) NOT NULL,
PRIMARY KEY (day, order_id)
) ENGINE=InnoDB
PARTITION BY RANGE (TO_DAYS(day)) (
PARTITION p20230901 VALUES LESS THAN (TO_DAYS('2023-09-02')),
PARTITION p20230902 VALUES LESS THAN (TO_DAYS('2023-09-03'))
);
2 写入时同步索引:
在写入数据时,同时将order_id
和时间范围插入到索引表中。
3 查询优化:
查询时,先通过索引表定位到具体的order_id
,再根据ID直接查询目标表。
2. 使用时间基因法
在ID中嵌入时间基因,使同一时间段的ID集中分布到特定分片。
例如,将订单ID设计为日期前缀 + 唯一序号
,如20230901_0001
。
步骤如下:
1 生成含时间基因的ID:在生成订单ID时,嵌入时间信息。
2 分片规则:分表的时候,直接 根据日期前缀进行分片,
例如:
shard_id = (year_month) % 12 -- 例如202309 → 9 % 12 = 9
3 查询优化:通过时间基因直接定位到目标分片,避免全库扫描。
80分 答案 (高手级)
尼恩提示,讲完 组合模式的分库分表 , 可以得到 80分了。
但是要直接拿到大厂offer,或者 offer 直提,需要 120分答案。
尼恩带大家继续,挺进 120分,让面试官 口水直流。
九、使用 雪花id的时间基因, 解决ID查询时的全库路由问题?
雪花id 里边,其实就有时间基因。雪花 ID(Snowflake ID)是一种分布式唯一 ID 生成算法,其生成的 ID 包含了时间戳信息。
在使用雪花算法(Snowflake Algorithm)生成ID时,可以通过解析ID中的时间戳部分来计算其对应的时间范围。以下是具体的实现方法和原理:
1. 雪花算法ID结构
雪花算法生成的64位ID由以下几部分组成:
- 符号位(1位):
始终为0,表示ID为正数。
- 时间戳(41位):
表示自基准时间(如2024年1月1日)以来的毫秒数。
- 数据中心ID(5位)
用于标识数据中心。
- 机器ID(5位):
用于标识同一数据中心内的机器。
- 序列号(12位):
用于同一毫秒内生成的多个ID。
2. 如何计算ID的时间范围
雪花算法的时间戳部分记录了生成ID时的毫秒级时间戳。
2.1 提取时间戳
假设基准时间是2024-01-01 00:00:00
,时间戳部分占41位,可以使用以下公式提取时间戳:
long timestamp = (id >> 22) + twepoch;
其中:
id
是生成的雪花ID。twepoch
是基准时间戳(如1640995200000L
,对应2024年1月1日)。22
是序列号部分的位数(12位)加上数据中心ID和机器ID的位数(10位)。
2.2 转换为具体时间
将提取的时间戳转换为具体的时间:
Date date = new Date(timestamp);
2.3 确定时间范围
根据提取的时间戳,可以确定ID生成的具体时间范围。
例如:如果ID生成于2025年1月1日,那么时间范围可以是2025-01-01 00:00:00
到2025-01-01 23:59:59
。
3 雪花id 里边的时间戳值 作为分表基因的实操
使用 雪花id 里边的时间戳值,作为 上面案例中 时间范围分表 的 日期输入, 如何实现?
要使用雪花 ID 里的时间戳值作为上面案例中时间范围分表的日期输入,可按以下步骤实现:
3.1. 解析雪花 ID 中的时间戳
雪花 ID 的结构通常包含了一个时间戳部分,不同的雪花 ID 实现可能会有细微差异,但一般都可以从 ID 中提取出时间戳信息。
首先定义一个parse,用于从雪花 ID 中提取时间戳并转换为日期:
public class SnowflakeIdParser {
// 假设雪花 ID 中时间戳部分从第 22 位开始,长度为 41 位
private static final long TIMESTAMP_SHIFT = 22;
// 雪花 ID 算法的起始时间戳(2020-01-01 00:00:00)
private static final long EPOCH = 1577836800000L;
public static java.util.Date getDateFromSnowflakeId(long snowflakeId) {
// 提取时间戳部分
long timestamp = (snowflakeId >> TIMESTAMP_SHIFT) + EPOCH;
return new java.util.Date(timestamp);
}
}
3.2. 修改表分片算法
在之前的 TableShardingAlgorithm
类基础上,修改 doSharding
方法,使其可以接收雪花 ID 并从中提取时间戳来确定表名。
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
public class TableShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
private static final Logger log = LoggerFactory.getLogger(TableShardingAlgorithm.class);
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
log.info("table PreciseShardingAlgorithm");
availableTargetNames.forEach(item -> log.info("actual node table:{}", item));
log.info("logic table name:{},rout column:{}", shardingValue.getLogicTableName(), shardingValue.getColumnName());
log.info("column value:{}", shardingValue.getValue());
String tbName = shardingValue.getLogicTableName() + "_";
// 从雪花 ID 中提取日期
Date date = SnowflakeIdParser.getDateFromSnowflakeId(shardingValue.getValue());
SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");
SimpleDateFormat monthFormat = new SimpleDateFormat("MM");
String year = yearFormat.format(date);
String month = monthFormat.format(date);
tbName = tbName + year + "_" + month;
log.info("tb_name:{}", tbName);
for (String each : availableTargetNames) {
if (each.equals(tbName)) {
return each;
}
}
throw new IllegalArgumentException();
}
}
3.3. 修改配置文件
确保 application.properties
中表分片的分片列配置为雪花 ID 所在的列(通常为 order_id
),并且精确分片算法类指向修改后的 TableShardingAlgorithm
类。
//其他配置保持不变
spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.sharding-column=order_id
spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.precise-algorithm-class-name=com.example.sharding.TableShardingAlgorithm
如果没有时间 只有id,也不会发生全库路由问题了 。
未完待续:读写分离架构
在高并发场景中,读写分离是提高系统性能的有效手段。可以采用主从架构:
- 主库:
主库负责写入操作。
如何 实操实现? 尼恩下一篇文章给大家介绍。
- 从库:
主库负责读取操作。
如何 实操实现? 尼恩下一篇文章给大家介绍。
通过这种方式,可以将写请求和读请求分开,降低主库的负载。
主库的写入请求会自动同步到从库,确保数据一致性。
未完待续:动态扩容架构与实现
- 监控与调优:
定期监控数据库的性能指标,及时调整分库分表策略,以应对不断变化的业务需求。
监控与调优 如何 和读写分离同时实现? 尼恩下一篇文章给大家介绍。
- 动态扩展:
设计方案需支持动态扩展,随着数据量的增长,可以增加更多的数据库实例和表。
动态扩展 如何 和读写分离同时实现? 尼恩下一篇文章给大家介绍。
120分殿堂答案 (塔尖级):
尼恩提示,讲完 动态库容、灰度切流 , 可以得到 120分了。
下一篇文章, 尼恩带大家继续,挺进 120分,让面试官 口水直流。
遇到问题,找老架构师取经
借助此文,尼恩给解密了一个高薪的 秘诀,大家可以 放手一试。保证 屡试不爽,涨薪 100%-200%。
后面,尼恩java面试宝典回录成视频, 给大家打造一套进大厂的塔尖视频。
通过这个问题的深度回答,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。
很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。
遇到职业难题,找老架构取经, 可以省去太多的折腾,省去太多的弯路。
尼恩指导了大量的小伙伴上岸,前段时间,刚指导一个40岁+被裁小伙伴,拿到了一个年薪100W的offer。
狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。