# 第02天:理解数据库路由和分库分表 ## 📚 今日目标 1. 理解什么是数据库路由 2. 理解为什么需要分库分表 3. 理解路由的基本原理 4. 画图理解分库分表架构 --- ## 🎯 知识点1:什么是数据库路由? ### 生活中的例子 **快递分拣**: - 快递员根据地址,把包裹送到不同的分拣中心 - 北京 → 北京分拣中心 - 上海 → 上海分拣中心 **数据库路由**: - 根据路由键(如用户ID),把数据存到不同的数据库/表 - 用户ID 1-1000万 → 数据库1,表1 - 用户ID 1000万-2000万 → 数据库2,表2 ### 核心概念 **路由键(Router Key)**: - 用来决定数据存在哪里的字段 - 通常是业务主键(如用户ID、订单ID) - 必须保证:相同路由键 → 相同库表 **路由算法**: - 根据路由键计算目标库和表 - 常用算法:哈希、取模、范围等 --- ## 🎯 知识点2:为什么需要分库分表? ### 问题场景 **单库单表的问题**: ``` 用户表(user) ├── 1亿条数据 ├── 查询越来越慢 ├── 索引越来越大 └── 数据库压力巨大 ``` **性能瓶颈**: 1. **查询慢**:数据量大,即使有索引也很慢 2. **写入慢**:插入数据需要维护索引 3. **锁竞争**:高并发时锁竞争激烈 4. **单点故障**:一个数据库挂了,整个系统不可用 ### 解决方案:分库分表 **分表(水平分表)**: ``` 原来:user(1亿条) 现在: ├── user_01(2500万条) ├── user_02(2500万条) ├── user_03(2500万条) └── user_04(2500万条) ``` **分库(水平分库)**: ``` 原来:db1(1亿条) 现在: ├── db1(2500万条) ├── db2(2500万条) ├── db3(2500万条) └── db4(2500万条) ``` **效果**: - 单表数据量减少 → 查询更快 - 分散到多个库 → 并发能力提升 - 单个库故障 → 不影响其他库 --- ## 🎯 知识点3:路由算法 ### 算法1:哈希路由 **原理**: ```java int dbIndex = Math.abs(userId.hashCode()) % dbCount; int tbIndex = Math.abs(userId.hashCode()) % tbCount; ``` **例子**: ``` userId = "12345678" hashCode = 12345678.hashCode() = 12345678 dbCount = 2 tbCount = 4 dbIndex = 12345678 % 2 = 0 → db01 tbIndex = 12345678 % 4 = 2 → user_02 ``` **优点**: - 数据分布均匀 - 相同userId总是路由到相同位置 **缺点**: - 扩容困难(需要重新分布数据) ### 算法2:取模路由 **原理**: ```java int dbIndex = userId % dbCount; int tbIndex = userId % tbCount; ``` **例子**: ``` userId = 12345678 dbCount = 2 tbCount = 4 dbIndex = 12345678 % 2 = 0 → db01 tbIndex = 12345678 % 4 = 2 → user_02 ``` **优点**: - 简单直接 - 计算快速 **缺点**: - 只适用于数字类型路由键 - 扩容困难 ### 算法3:范围路由 **原理**: ```java if (userId >= 1 && userId <= 10000000) { dbIndex = 0; // db01 } else if (userId > 10000000 && userId <= 20000000) { dbIndex = 1; // db02 } ``` **优点**: - 扩容相对容易(只需添加新范围) - 查询范围数据方便 **缺点**: - 数据分布可能不均匀 - 需要维护范围配置 --- ## 🛠️ 实践任务:画图理解架构 ### 任务1:画分库分表架构图 用纸笔或画图工具画出: ``` 用户请求(userId=12345678) ↓ 路由计算 ↓ dbIndex = 0, tbIndex = 2 ↓ 选择数据源:db01 ↓ 修改SQL:user → user_02 ↓ 执行SQL ``` ### 任务2:理解数据分布 假设: - 2个数据库(db01, db02) - 每个库4张表(user_01, user_02, user_03, user_04) **问题**: 1. userId=1 应该存在哪里? 2. userId=10000001 应该存在哪里? 3. 如何保证相同userId总是路由到相同位置? **答案**: ``` userId=1: hashCode = 1 dbIndex = 1 % 2 = 1 → db02 tbIndex = 1 % 4 = 1 → user_01 结果:db02.user_01 userId=10000001: hashCode = 10000001 dbIndex = 10000001 % 2 = 1 → db02 tbIndex = 10000001 % 4 = 1 → user_01 结果:db02.user_01 ``` --- ## 🎓 知识点拓展 ### 拓展1:垂直分表 vs 水平分表 **垂直分表**: ``` 原来:user表 ├── id ├── name ├── age ├── email ├── address └── description(大字段) 现在: ├── user_base(id, name, age) └── user_detail(id, email, address, description) ``` **场景**:某些字段很大但不常用 **水平分表**: ``` 原来:user(1亿条) 现在: ├── user_01(2500万条) ├── user_02(2500万条) ├── user_03(2500万条) └── user_04(2500万条) ``` **场景**:数据量大,需要分散 ### 拓展2:分库分表的挑战 **1. 跨库查询** ``` 问题:查询所有订单总金额 原来:SELECT SUM(amount) FROM order 现在:需要查询多个库,然后汇总 ``` **解决方案**: - 避免跨库查询(设计时考虑) - 使用中间件(如ShardingSphere) - 数据汇总表 **2. 分布式事务** ``` 问题:用户下单需要: 1. 扣减库存(db1) 2. 创建订单(db2) 3. 扣减余额(db3) 如何保证原子性? ``` **解决方案**: - 避免跨库事务(尽量单库完成) - 使用分布式事务(如Seata) - 最终一致性(消息队列) **3. 扩容** ``` 问题:2个库不够用了,要扩容到4个库 如何迁移数据? ``` **解决方案**: - 双写方案(新旧库同时写) - 数据迁移工具 - 平滑扩容 ### 拓展3:路由键选择 **好的路由键**: - ✅ 业务主键(用户ID、订单ID) - ✅ 分布均匀 - ✅ 查询时经常用到 **不好的路由键**: - ❌ 时间戳(数据倾斜) - ❌ 随机值(无法定位) - ❌ 很少查询的字段 **例子**: ``` 订单表路由键选择: ✅ 用户ID(userId) - 用户查询自己的订单 - 数据分布均匀 ❌ 订单时间(createTime) - 最近的数据都在一个库 - 数据倾斜严重 ``` --- ## ✅ 今日检查清单 - [ ] 理解了数据库路由的概念 - [ ] 理解了为什么需要分库分表 - [ ] 理解了路由算法的原理 - [ ] 画出了分库分表架构图 - [ ] 完成了拓展阅读 - [ ] 思考了路由键的选择 --- ## 🎯 明日预告 明天我们将学习: - Java注解(Annotation)基础 - 如何自定义注解 - 注解的元注解 --- ## 💡 思考题 1. 如果路由键是字符串(如手机号),如何计算路由? 2. 分库分表后,如何保证全局ID唯一? 3. 如果某个库的数据量特别大,怎么办? --- ## 📚 参考资源 - [分库分表原理](https://www.cnblogs.com/littlecharacter/p/9342369.html) - [数据库分片策略](https://shardingsphere.apache.org/document/current/cn/features/sharding/concept/sharding/) - [分布式系统设计](https://github.com/donnemartin/system-design-primer)