假设有一个电商网站,随着用户量和订单量的增加,单一数据库难以承载如此庞大的数据量,查询速度也逐渐降低,这时就需要进行分库分表。
电商网站有用户模块和订单模块,可以将用户表和订单表拆分到不同的数据库中。例如,原本在同一个数据库中的用户表(包含用户基础信息和用户扩展信息)和订单表,可以拆分为两个数据库,一个数据库存储用户的基础信息,另一个数据库存储用户的扩展信息和订单信息。
最常见的博客文章都是以电商系统作为案例,因为他是最具备代表性,电商网站的用户模块和订单模块是可以拆分到不同的数据库中的。这种拆分方式可以提高系统的可扩展性和性能。
假设有一个电商网站,它有一个数据库,其中包含两个表:用户表(User)和订单表(Order)。用户表包含用户的基础信息(如用户名、密码等)和用户的扩展信息(如用户的购物偏好、历史订单等)。订单表包含订单的详细信息(如订单号、购买的商品、数量、价格等)。
原始结构可能如下:
Database: EcommerceDB
Table: User
- UserID
- Username
- Password
- Preferences
- HistoryOrders
Table: Order
- OrderID
- UserID
- Product
- Quantity
- Price
可以将这个数据库拆分为两个数据库:一个数据库(UserDB)存储用户的基础信息,另一个数据库(OrderDB)存储用户的扩展信息和订单信息。
拆分后的结构可能如下:
Database: UserDB
Table: UserBasic
- UserID
- Username
- Password
Database: OrderDB
Table: UserExtension
- UserID
- Preferences
- HistoryOrders
Table: Order
- OrderID
- UserID
- Product
- Quantity
- Price
这样,当用户的基础信息和订单信息需要进行大量的读写操作时,这两个操作可以在不同的数据库上并行进行,从而提高系统的性能。同时,由于用户的基础信息和订单信息被存储在不同的数据库中,因此,如果其中一个数据库出现问题,也不会影响到另一个数据库的正常运行,从而提高了系统的可用性。
假设订单表中有上百万条数据,查询和写入速度逐渐下降。此时,可以根据订单ID进行水平分表,比如订单ID为奇数的存入订单表1,订单ID为偶数的存入订单表2。这样,原本一个表需要处理的数据量就减半了,可以提高查询和写入速度。
在进行分库分表后,对于程序查询也需要做相应的调整。例如在进行水平分表后,查询某个订单信息时,需要先判断订单ID是奇数还是偶数,然后再决定查询哪个表。
Database: OrderDB
Table: Order
- OrderID
- UserID
- Product
- Quantity
- Price
Database: OrderDB
Table: Order0
- OrderID
- UserID
- Product
- Quantity
- Price
Table: Order1
- OrderID
- UserID
- Product
- Quantity
- Price
在这种情况下,Order0用于存储ID为偶数的订单,Order1用于存储ID为奇数的订单。
查询和写入的伪代码可能如下所示:
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的奇偶性来决定查询或写入哪个表,从而将原本一个表需要处理的数据量减半,提高查询和写入速度。
实际的分库分表会更复杂。同时,分库分表也可能会带来一些问题,如数据一致性问题、跨库事务问题等。因此,在设计分库分表方案时,需要进行充分的考虑,并可能需要引入其他的技术(如分布式事务)来解决这些问题。
那么,让一起深入了解一下数据库知识应用实践之分库分表。
记住一句话,不是所有系统一上来就搞分库分表,包括淘宝,京东。尤其这种超前设计,对小公司来说就是累赘,甚至隐患。不仅人才成本,资源成本,运维成本。甚至学习成本都不容小觑。所以基本上都是不断衍生到分库分表,才算是中小公司正确的技术路线和最优实践。
我们以一个场景:例如有一个电子商务网站,随着业务的发展,用户数量、商品数量和交易数量都在快速增长。原来单一的数据库已经无法满足需求,查询速度慢,系统负载高,甚至出现宕机情况。这样的情况下,为了提高系统的性能和稳定性,就需要进行数据库的分库分表。
分库分表的目的是为了解决单个数据库无法承受大量数据和高并发的问题。但是分库分表也会带来一些问题,例如数据一致性问题、分布式事务问题、跨库跨表查询问题、数据迁移问题等。
垂直分库分表:按业务模块进行分库,将大表按照字段进行分表,例如用户库、商品库等。
水平分库分表:将数据按一定的规则分散到不同的数据库或表中,例如按用户ID的hash值分库,按订单时间分表等。
常见框架:
当技术演进到我们已经解决了分库分表基本问题,解决了并发问题,解决性能问题,接下来我们基本上面临如下几个问题。关于这些问题又需要我们继续去学习和研究尝试更多方案。此处我们不做展开。
在数据库地理分布、灾备等问题上,如果是多库多表的情况,数据同步和备份的复杂度将会增加。
.在分库分表后,如何对旧数据进行迁移以及如何在迁移过程中保证业务的正常运行是个大问题。
在传统的单体数据库中,事务是保证数据一致性的重要手段。但在分库分表的环境下,原有的事务机制不能再使用,需要引入新的分布式事务解决方案。
在分库分表后,原本在一个库或一个表中可以做的join查询就变得困难,需要通过应用层去做关联和组合。
我们用Java写一些伪代码方便大家理解。
假设我们有多种根据某个值(如用户ID或订单ID)来确定应将数据存储在哪个数据库的方式。我们分别用函数法,范围法和一致性哈希法三种方法模拟一下。
public String getDatabase(int userId) {
int dbCount = 2; // 假设我们有2个数据库
int dbIndex = userId % dbCount;
return "db" + dbIndex;
}
public String getDatabase(int orderId) {
if (orderId < 10000) {
return "db0";
} else {
return "db1";
}
}
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使用详解。除了它还有一些国产的中间件也不错。如下
“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/