# 数据库如何实现高性能?

在这里插入图片描述

# 读写分离和分库分表

在数据库的高并发和海量数据场景下,读写分离和分库分表是两种非常经典的架构优化手段。简单来说,它们一个是解决“并发压力大”的问题,一个是解决“数据量太大”的问题

# 读写分离

# 什么是读写分离

在大多数互联网应用中,读(Select)的压力远大于写(Insert/Update/Delete)(通常是 8:2 甚至 9:1)。读写分离的核心思想就是把“写操作”和“读操作”分给不同的数据库服务器来处理。

主库(Master): 只负责处理写操作,以及少量的实时读操作。

从库(Slave): 只负责处理读操作,可以有多个。

# 核心原理:主从复制

简单来说,就搞一个主库,挂多个从库,然后我们就单单只是写主库,然后从库读取binlog进行重放,这样主库和从库数据就一样

在这里插入图片描述

总的来说,MySQL复制有三个步骤

  1. 主库把所有的写操作记录到 Binlog(二进制日志) 中
  2. 从库的 I/O 线程去读取主库的 Binlog,并写入到自己的 Relay Log(中继日志) 中
  3. 从库的 SQL 线程重放 Relay Log 中的操作,从而与主库保持数据一致

# 带来的挑战:主从延迟

主从延迟:数据同步需要时间。如果主库刚写完,用户立马去从库读,可能会读到旧数据

解决方案如下:

1. 强制路由主库(最常用)

对于某些对实时性要求极高的场景,不走从库,直接查主库

2. 关键路径判断

利用 Redis 配合做一个“延迟标记”。

  • 某个用户执行了写操作(比如发帖),在 Redis 中记录一个 Key:user_lock:userId = 1,过期时间设置为 2 秒(预估的主从延迟最大时间)。
  • 当该用户接下来读数据时,先看 Redis 里有没有这个 user_lock 标记。
  • 如果有,说明他刚写过数据,为了防止主从延迟,强制路由到主库。
  • 2 秒后,Key 过期消失,该用户的读请求恢复路由到从库

# 分库分表

# 1. 什么时候进行分库分表?

不要过早优化。在决定分库分表之前,先确认是否已经尝试过以下低成本手段

  • 代码及SQL优化:检查是否有慢查询,索引是否建立合理
  • 提高硬件配置:提升CPU、内存、换SSD固态硬盘
  • 读写分离/主从架构:如果读并发高,可以通过增加从库解决
  • 缓存(Redis等):拦截高并发的读请求
  • 历史数据归档:将1年前或不常用的数据迁移到冷备库

行业经验指标:单表数据量超过 500万行 或 数据容量超过 2GB,且经过上述优化后性能仍无法满足要求时,再考虑分库分表

# 2. 拆分策略:垂直拆分 vs 水平拆分

分库分表主要有两种维度,通常是结合使用

在这里插入图片描述

# 垂直拆分(按业务/字段拆)

垂直分库: 按照业务模块,把一个大数据库拆分成多个业务库。例如把一个电商大库拆分为用户库,商品库,订单库

垂直分表:把一张表里不常用的字段、或者大字段(如 text、blob)拆分到另一张辅助表中,大表变小表

# 水平拆分(按数据行拆)

水平拆分是结构不变,只拆分数据行,也就是我们常说的 Sharding

水平分表: 单表数据量太大(如超过 2000 万行),把这一张表的数据拆分到多张结构完全一样的表中。例子: t_order 拆分为 t_order_0、t_order_1、t_order_2

水平分库:如果不仅数据量大,高并发写入也把单台服务器的 CPU/内存/IO 撑爆了,就把这些表再分散到不同的物理数据库服务器上

# 3. 水平拆分的核心:Sharding Key(分片键)与路由算法

选择合适的 分片键(Sharding Key) 是分库分表最关键的一步,它决定了数据的分布规则

# 常见的分片算法

策略 举例 优点 缺点
范围分片 按 user_id 范围 1~10000 表1,10001~20000 表2 顺序扩展方便 容易热点不均
哈希取模 user_id % 4 数据均匀 扩容时需大量迁移
一致性哈希 虚拟节点环 扩容影响小 实现复杂
日期分片 按月分表 order_202401 便于归档 近期热点

# 经典业务场景下的分片键选择

以“用户”为中心的业务(如社交、博客)

通常以 user_id 作为分片键。这样同一个用户的所有数据都在同一个库表里,查询方便

# 4. 分库分表带来的副作用

# 分库后出现的问题

跨库join问题

不同库之间的表无法直接进行 JOIN 操作 解决办法:

  1. 字段冗余:把经常需要JOIN的字段直接冗余到表里(反规范化设计)
  2. 全局表(广播表):字典表、配置表等数据量小但常被关联的表,在每个数据库都保存一份,修改时同步更新
  3. 应用层组装:先查出A表数据,拿着ID去代码里拼装查询B表数据

跨库事务问题

原本一个事务可以搞定的操作,现在跨了多个数据库

解决办法:尽量通过合理的设计规避分布式事务(让相关高频操作在同一个库)。如果无法规避,采用 Seata 等分布式事务框架,或者使用 MQ 柔性事务(最终一致性)

# 分表后出现的问题

分布式全局唯一id问题

单表时可以使用数据库自增ID,分库分表后各表自增会导致ID冲突

解决办法:使用全局唯一 ID 生成器(如雪花算法 Snowflake)

跨分片分页、排序、函数问题

如果查询条件没有带上 Sharding Key(如:查询全表按时间排序前10条),中间件需要去所有分片把数据查出来,在内存中重新排序分页

解决办法:针对这类复杂的多维查询、大分页、数据统计,不要走关系型数据库。应该通过数据异构,将数据同步到 Elasticsearch、ClickHouse中查询

# 5. 分库分表中间件选择

实现层面上,无论是读写分离还是分库分表,核心要解决的问题都是“路由”——即把一条 SQL 语句,在正确的时间发送到正确的数据库(和表)上

实现方式 核心原理 代表产品 优点 缺点
客户端模式 在应用内部以Jar包形式存在,侵入应用但性能更高 ShardingSphere-JDBC 1. 性能好,无网络代理损耗
2. 部署简单,与应用一体
1. 与特定语言(如Java)绑定
2. 配置分散在各应用,升级维护成本高
代理模式 作为独立服务部署在应用和数据库之间,对应用透明 Mycat, ShardingSphere-Proxy 1. 对应用完全透明,支持异构语言
2. 便于集中管理和运维(如监控、升级)
1. 多一跳网络延迟,有性能损耗
2. 代理本身需考虑高可用,避免单点故障

客户端模式

在这里插入图片描述

代理模式

在这里插入图片描述