文章目录
- [0. 前言](#0. 前言)
-
- [1. 垂直分库分表](#1. 垂直分库分表)
- [2. 水平分库分表](#2. 水平分库分表)
- [1. 理解过程及实现方案](#1. 理解过程及实现方案)
-
-
- 问题讨论
- 衍生出分库分表策略
- 借助成熟组件使用
- 分库分表阶段完成后面临的问题
-
- [1. 异地多活问题](#1. 异地多活问题)
- [2. 数据迁移问题](#2. 数据迁移问题)
- [3. 分布式事务问题](#3. 分布式事务问题)
- [4. join查询的问题](#4. join查询的问题)
- 分库分表的策略实现示例
-
- [2. 参考文档](#2. 参考文档)
0. 前言
假设有一个电商网站,随着用户量和订单量的增加,单一数据库难以承载如此庞大的数据量,查询速度也逐渐降低,这时就需要进行分库分表。
1. 垂直分库分表
电商网站有用户模块和订单模块,可以将用户表和订单表拆分到不同的数据库中。例如,原本在同一个数据库中的用户表(包含用户基础信息和用户扩展信息)和订单表,可以拆分为两个数据库,一个数据库存储用户的基础信息,另一个数据库存储用户的扩展信息和订单信息。
最常见的博客文章都是以电商系统作为案例,因为他是最具备代表性,电商网站的用户模块和订单模块是可以拆分到不同的数据库中的。这种拆分方式可以提高系统的可扩展性和性能。
假设有一个电商网站,它有一个数据库,其中包含两个表:用户表(User)和订单表(Order)。用户表包含用户的基础信息(如用户名、密码等)和用户的扩展信息(如用户的购物偏好、历史订单等)。订单表包含订单的详细信息(如订单号、购买的商品、数量、价格等)。
原始结构可能如下:
shell
Database: EcommerceDB
Table: User
- UserID
- Username
- Password
- Preferences
- HistoryOrders
Table: Order
- OrderID
- UserID
- Product
- Quantity
- Price
可以将这个数据库拆分为两个数据库:一个数据库(UserDB)存储用户的基础信息,另一个数据库(OrderDB)存储用户的扩展信息和订单信息。
拆分后的结构可能如下:
shell
Database: UserDB
Table: UserBasic
- UserID
- Username
- Password
Database: OrderDB
Table: UserExtension
- UserID
- Preferences
- HistoryOrders
Table: Order
- OrderID
- UserID
- Product
- Quantity
- Price
这样,当用户的基础信息和订单信息需要进行大量的读写操作时,这两个操作可以在不同的数据库上并行进行,从而提高系统的性能。同时,由于用户的基础信息和订单信息被存储在不同的数据库中,因此,如果其中一个数据库出现问题,也不会影响到另一个数据库的正常运行,从而提高了系统的可用性。
2. 水平分库分表
假设订单表中有上百万条数据,查询和写入速度逐渐下降。此时,可以根据订单ID进行水平分表,比如订单ID为奇数的存入订单表1,订单ID为偶数的存入订单表2。这样,原本一个表需要处理的数据量就减半了,可以提高查询和写入速度。
在进行分库分表后,对于程序查询也需要做相应的调整。例如在进行水平分表后,查询某个订单信息时,需要先判断订单ID是奇数还是偶数,然后再决定查询哪个表。
Database: OrderDB
Table: Order
- OrderID
- UserID
- Product
- Quantity
- Price
shell
Database: OrderDB
Table: Order0
- OrderID
- UserID
- Product
- Quantity
- Price
Table: Order1
- OrderID
- UserID
- Product
- Quantity
- Price
在这种情况下,Order0用于存储ID为偶数的订单,Order1用于存储ID为奇数的订单。
查询和写入的伪代码可能如下所示:
python
def get_order(order_id):
if order_id % 2 == 0:
# Query Order0 table
return query_order_0(order_id)
else:
# Query Order1 table
return query_order_1(order_id)
这样,我们可以根据订单ID的奇偶性来决定查询或写入哪个表,从而将原本一个表需要处理的数据量减半,提高查询和写入速度。
实际的分库分表会更复杂。同时,分库分表也可能会带来一些问题,如数据一致性问题、跨库事务问题等。因此,在设计分库分表方案时,需要进行充分的考虑,并可能需要引入其他的技术(如分布式事务)来解决这些问题。
那么,让一起深入了解一下数据库知识应用实践之分库分表。
1. 理解过程及实现方案
记住一句话,不是所有系统一上来就搞分库分表,包括淘宝,京东。尤其这种超前设计,对小公司来说就是累赘,甚至隐患。不仅人才成本,资源成本,运维成本。甚至学习成本都不容小觑。所以基本上都是不断衍生到分库分表,才算是中小公司正确的技术路线和最优实践。
我们以一个场景:例如有一个电子商务网站,随着业务的发展,用户数量、商品数量和交易数量都在快速增长。原来单一的数据库已经无法满足需求,查询速度慢,系统负载高,甚至出现宕机情况。这样的情况下,为了提高系统的性能和稳定性,就需要进行数据库的分库分表。
问题讨论
分库分表的目的是为了解决单个数据库无法承受大量数据和高并发的问题。但是分库分表也会带来一些问题,例如数据一致性问题、分布式事务问题、跨库跨表查询问题、数据迁移问题等。
衍生出分库分表策略
-
垂直分库分表:按业务模块进行分库,将大表按照字段进行分表,例如用户库、商品库等。
-
水平分库分表:将数据按一定的规则分散到不同的数据库或表中,例如按用户ID的hash值分库,按订单时间分表等。
借助成熟组件使用
常见框架:
- MyCat:一个开源的分库分表中间件,支持自定义分片规则,可以实现读写分离、事务和SQL的完全透明。
- Sharding-JDBC:一款轻量级的Java框架,提供了强大的分库分表、读写分离、分布式事务和分布式序列等功能。
- Shardingsphere:包含了Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar这三款独立的产品,可以满足不同场景下的数据分片需求。
分库分表的复杂问题:
分库分表阶段完成后面临的问题
当技术演进到我们已经解决了分库分表基本问题,解决了并发问题,解决性能问题,接下来我们基本上面临如下几个问题。关于这些问题又需要我们继续去学习和研究尝试更多方案。此处我们不做展开。
1. 异地多活问题
在数据库地理分布、灾备等问题上,如果是多库多表的情况,数据同步和备份的复杂度将会增加。
2. 数据迁移问题
.在分库分表后,如何对旧数据进行迁移以及如何在迁移过程中保证业务的正常运行是个大问题。
3. 分布式事务问题
在传统的单体数据库中,事务是保证数据一致性的重要手段。但在分库分表的环境下,原有的事务机制不能再使用,需要引入新的分布式事务解决方案。
4. join查询的问题
在分库分表后,原本在一个库或一个表中可以做的join查询就变得困难,需要通过应用层去做关联和组合。
分库分表的策略实现示例
- 函数法(哈希、取模等):根据某个字段(比如用户ID)的函数结果进行分库分表。
- 范围法(枚举、区间):比如根据日期范围,订单ID范围等进行分库分表。
- 一致性哈希:在新增或减少数据库节点的时候,能够最小化数据的迁移。
我们用Java写一些伪代码方便大家理解。
假设我们有多种根据某个值(如用户ID或订单ID)来确定应将数据存储在哪个数据库的方式。我们分别用函数法,范围法和一致性哈希法三种方法模拟一下。
- 函数法:我们可以将用户ID除以数据库数量的余数作为数据库的索引。
java
public String getDatabase(int userId) {
int dbCount = 2; // 假设我们有2个数据库
int dbIndex = userId % dbCount;
return "db" + dbIndex;
}
- 范围法:我们可以使用订单ID作为范围进行划分,比如订单ID小于10000的存储在一个数据库,大于等于10000的存储在另一个数据库。
java
public String getDatabase(int orderId) {
if (orderId < 10000) {
return "db0";
} else {
return "db1";
}
}
- 一致性哈希:这种方法需要使用到特殊的数据结构,例如TreeMap。首先,我们将每个数据库(称为节点)添加到一个TreeMap中,然后计算键(如用户ID)的哈希值,并在TreeMap中找到该哈希值对应的数据库。如果没有找到,则返回哈希值最接近的数据库。
java
public class ConsistentHashing {
private TreeMap<Integer, String> nodes = new TreeMap<>();
public void addNode(String nodeName) {
int hash = nodeName.hashCode();
nodes.put(hash, nodeName);
}
public void removeNode(String nodeName) {
int hash = nodeName.hashCode();
nodes.remove(hash);
}
public String getDatabase(String key) {
if (nodes.isEmpty()) {
return null;
}
int hash = key.hashCode();
if (!nodes.containsKey(hash)) {
SortedMap<Integer, String> tailMap = nodes.tailMap(hash);
hash = tailMap.isEmpty() ? nodes.firstKey() : tailMap.firstKey();
}
return nodes.get(hash);
}
}
其实真实场景中我们大多都使用分库分表组件或者中间件,最著名的和大家最常用的sharding-jdbc就是解决这类场景的一个优秀的,轻量级的分库分表组件,虽然有很多bug或者不足,但是足以解决我们90%的问题了。后面有一个章节我会写一个详细教程关于sharding-jdbc使用详解。除了它还有一些国产的中间件也不错。如下
- TDDL:阿里巴巴开源的分库分表中间件,支持复杂的分库分表策略。
- Oceanus:网易云数据库的分库分表解决方案,支持自动分库分表、读写分离、分布式事务等功能。
2. 参考文档
-
"MySQL分库分表方案" - InfoQ。介绍了 MySQL 的分库分表方案,并对实施这种方案的步骤进行了详细的讨论。链接:https://www.infoq.cn/article/solution-of-mysql-sub-database-and-sub-table
-
"MySQL 分库分表实践" - 阿里云社区的一篇文章也可以参考学习。介绍了 MySQL 分库分表的实践和经验。链接:https://developer.aliyun.com/article/776687
-
"分库分表架构设计" 详细介绍了分库分表架构的设计和实施。链接:https://www.jianshu.com/p/d7f3d3808f25
-
ShardingSphere 是 Apache 的一个开源项目包包含了我上面说的sharding-jdbc,提供了分库分表的解决方案。链接:https://shardingsphere.apache.org/document/current/cn/overview/