数据库查询优化
数据库查询优化
这篇文章适合刚刚入手项目的小伙伴,为大家如何提高数据库查询效率提供一些建议。
1.添加索引
1.1 索引是什么
对于索引基础薄弱的同学,我们可以从 “索引是什么” 简单类比:索引就像书籍的目录,能帮数据库快速定位到需要的数据,而不用逐行扫描全表。下面结合项目具体说明索引的创建逻辑和使用方式。
1.2 索引的作用
索引的核心功能是帮助数据库快速定位目标数据,避免全表扫描,从而大幅提升查询效率(类似书籍目录的作用)。
这里以复合索引举一个例子:
定义:由多个字段组合创建的索引,遵循 “最左前缀原则”(查询条件包含索引的前 N 个字段时才能生效)。
项目示例:
-- 订单表:按“状态+订单时间”创建复合索引
CREATE INDEX idx_orders_status_time ON orders(status, order_time DESC);
作用:优化包含 status
和 order_time
的查询,例如:
-- 能用到上述索引的查询
SELECT * FROM orders WHERE status = 'PENDING' ORDER BY order_time DESC;
SELECT * FROM orders WHERE status = 'PENDING' AND order_time > '2024-01-01';
1.3 索引的使用方式
1.3.1 索引提示(强制使用指定索引)
当数据库优化器未选择最优索引时,通过 /*+ INDEX(表名 索引名) */
强制指定:
<!-- 订单查询中强制使用 idx_orders_status_time 索引 -->
<select id="pageQuery" resultType="com.sky.entity.Orders">
SELECT /*+ INDEX(orders idx_orders_status_time) */
id, number, status, user_id, order_time, amount
FROM orders
WHERE status = #{status}
ORDER BY order_time DESC
</select>
1.3.2 配合查询优化使用索引
避免 SELECT *
:只查询需要的字段,让覆盖索引生效(否则会触发回表):
<!-- 优化后:只查索引包含的字段 -->
<select id="pageQuery" resultType="com.sky.entity.Orders">
SELECT id, number, status, order_time -- 这些字段都在 idx_orders_cover_list 中
FROM orders WHERE status = #{status}
</select>
1.3.3 优化分页查询
使用游标分页时,通过索引字段(如 order_time
)定位,避免大偏移量导致的全表扫描:
<!-- 基于 order_time 索引的游标分页 -->
<select id="pageQueryByCursor" resultType="com.sky.entity.Orders">
SELECT id, order_time FROM orders
WHERE order_time < #{lastOrderTime} -- 利用索引快速定位
ORDER BY order_time DESC
LIMIT #{limit}
</select>
1.4 索引的注意事项
1.4.1 索引不是越多越好
索引会增加写入(插入 / 更新 / 删除)的开销(因为要维护索引结构),项目中通过定期删除无用索引(如长期未被使用的索引)避免性能损耗。
1.4.2 索引失效场景
- 不满足最左前缀原则(如用
idx_orders_status_time
时,查询条件不含status
); - 在索引字段上使用函数(如
DATE(order_time) = '2024-01-01'
,需用函数索引解决); - 模糊查询以
%
开头(如name LIKE '%鱼香肉丝'
,索引失效,项目中通过idx_dish_name_status
优化前缀匹配)。
1.4.3 定期维护索引
项目建议通过 ANALYZE TABLE
更新表统计信息,帮助数据库优化器选择最优索引:
ANALYZE TABLE orders; -- 更新订单表统计信息
ANALYZE TABLE
是数据库(如 MySQL)的内置命令,作用是重新计算并更新表的统计信息(如行数、数据分布、索引基数等),这些信息会被数据库优化器直接使用,只需在 SQL 客户端中直接执行 ANALYZE TABLE 表名;
即可完成统计信息更新无需在应用层或数据库配置文件中做额外设置。
2.SQL查询优化
通过优化 SQL 语句逻辑,减少不必要的数据处理,提升执行效率:
- 避免
SELECT *
:只查询必要字段,减少数据传输量和回表操作。例如OrderMapper.xml
中,优化后的查询明确指定所需字段(如id, number, status
等),而非查询所有字段。 - 优化 JOIN 查询:用
INNER JOIN
替代LEFT JOIN
(适用场景),避免返回无效空数据,减少关联扫描范围。例如菜品查询中,通过INNER JOIN
关联菜品表和分类表,确保关联数据有效性。 - 索引提示:使用
/*+ INDEX(table_name index_name) */
强制优化器使用指定索引,避免索引选择偏差。例如订单统计查询中,通过/*+ INDEX(orders idx_orders_status_time) */
确保使用复合索引。
3.分页查询优化
针对传统分页(LIMIT offset, size
)在大偏移量时性能差的问题,采用游标分页:
- 原理:以上一次查询的最后一条记录的排序字段(如
order_time
)作为条件,避免扫描偏移量前的所有数据。 - 示例:
OrderMapper.xml
中pageQueryByCursor
方法通过WHERE order_time < #{lastOrderTime}
定位分页起点,配合LIMIT
获取数据,性能不受偏移量影响。
<!-- 游标分页查询,避免OFFSET性能问题 -->
<select id="pageQueryByCursor" resultType="com.sky.entity.Orders">
SELECT /*+ INDEX(orders idx_orders_status_time) */
id, number, status, user_id, address_book_id, order_time, checkout_time,
pay_method, pay_status, amount, remark, phone, address, consignee,
estimated_delivery_time, delivery_status, pack_amount, tableware_number, tableware_status
FROM orders
<where>
<if test="lastOrderTime != null">
and order_time < #{lastOrderTime}
</if>
<if test="status != null">
and status = #{status}
</if>
<if test="userId != null">
and user_id = #{userId}
</if>
</where>
ORDER BY order_time DESC
LIMIT #{limit}
</select>
4. 批量操作优化
减少与数据库的交互次数,降低网络开销:
批量插入 / 更新 / 删除:通过foreach
标签在 XML 中实现批量操作。例如菜品批量插入时,一次性插入多条数据,而非单条循环执行:
<insert id="batchInsert">
INSERT INTO dish (...) VALUES
<foreach collection="list" item="dish" separator=",">
(#{dish.name}, #{dish.categoryId}, ...)
</foreach>
</insert>
5.数据库配置优化(spring.datasource.druid)
在配置文件application.yml中配置。
5.1 Druid数据库连接池优化配置
Druid 是高性能连接池,该部分配置直接决定数据库连接的利用率、稳定性和查询效率,是数据库优化的 “基石”:
spring:
druid:
driver-class-name: ${sky.datasource.driver-class-name}
#rewriteBatchedStatements: true 优化 MySQL 批量插入 / 更新(将多条 SQL 合并为 1 条,减少网络交互)
url: jdbc:mysql://${sky.datasource.host}:${sky.datasource.port}/${sky.datasource.database}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&cachePrepStmts=true&prepStmtCacheSize=250&prepStmtCacheSqlLimit=2048&useServerPrepStmts=true
username: ${sky.datasource.username}
password: ${sky.datasource.password}
# 连接池优化配置
#系统启动时创建 10 个连接,避免首次请求时 “临时创建连接” 的延迟
initial-size: 10
#保留 20 个空闲连接,避免 “频繁创建 / 销毁连接” 的资源损耗
min-idle: 20
#限制并发连接上限,避免连接过多压垮数据库
max-active: 100
#避免线程无限阻塞在 “获取连接” 上,快速失败并释放资源
max-wait: 30000
time-between-eviction-runs-millis: 30000
min-evictable-idle-time-millis: 900000
#定期检查空闲连接是否可用,避免使用 “失效连接”(如网络波动导致的死连接)
test-while-idle: true
validation-query: SELECT 1
test-on-borrow: false
test-on-return: false
#缓存 SQL 预处理语句,避免重复解析 SQL(尤其高频查询,如 “查询在售菜品”)
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat,wall,slf4j
#记录执行时间超过 5 秒的 SQL,便于定位性能瓶颈
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
#避免 “连接泄漏”(如代码未关闭连接)导致连接池耗尽
remove-abandoned: true # 自动回收超时连接
remove-abandoned-timeout: 1800 # 超时时间30分钟
log-abandoned: true # 记录超时连接日志
5.2 MyBatis 执行优化配置(mybatis.configuration)
MyBatis 作为 ORM 框架,其配置直接影响 SQL 执行效率和内存使用,属于 “数据库操作层” 的优化:
mybatis:
#mapper配置文件
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.sky.entity
configuration:
#开启驼峰命名,自动将数据库字段(如 order_time)映射为 Java 驼峰属性(orderTime),避免手动配置 resultMap
map-underscore-to-camel-case: true
# 开启二级缓存,缓存 MyBatis 执行结果(跨 SqlSession 共享),减少重复查询数据库
cache-enabled: true
#开启 “按需延迟加载”,关联查询时(如 “查询订单 + 关联订单详情”),仅在使用关联数据时才查库,避免冗余数据加载
# 开启延迟加载
lazy-loading-enabled: true
# 设置积极的延迟加载
aggressive-lazy-loading: false
# 设置延迟加载的触发方法
lazy-load-trigger-methods: equals,clone,hashCode,toString
# 设置默认执行器为复用,复用 SQL 执行器(避免频繁创建执行器实例),减少对象创建销毁的资源损耗
default-executor-type: REUSE
# 设置默认语句超时时间,SQL执行超时=30秒,限制单条 SQL 的执行时间,避免长耗时查询阻塞数据库连接
default-statement-timeout: 30
# 结果集每次获取 100 行,分批读取查询结果,避免一次性加载大量数据导致内存溢出
default-fetch-size: 100
5.3 隐藏参数:数据库连接URL参数(spring.datasource.druid.url)
spring:
datasource:
druid:
url: jdbc:mysql://${sky.datasource.host}:${sky.datasource.port}/${sky.datasource.database}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&cachePrepStmts=true&prepStmtCacheSize=250&prepStmtCacheSqlLimit=2048&useServerPrepStmts=true
URL 参数 | 作用 | 优化效果(外卖场景) |
---|---|---|
rewriteBatchedStatements=true | 优化 MySQL 批量操作(将 INSERT INTO ...; INSERT INTO ... 合并为 INSERT INTO ... VALUES (...),(...) ) | 外卖系统 “批量添加购物车商品”“批量更新菜品库存” 时,网络交互次数从 N 次减少到 1 次,效率提升 10-100 倍 |
cachePrepStmts=true + prepStmtCacheSize=250 + prepStmtCacheSqlLimit=2048 | 开启客户端 PreparedStatement 缓存,缓存 SQL 模板和参数 | 高频查询(如 “查询菜品详情(dish_id=?)”)可复用缓存的 SQL 模板,避免重复解析 SQL 语法,提升查询速度 |
useServerPrepStmts=true | 使用 MySQL 服务器端预处理语句,而非客户端模拟 | 进一步减少 SQL 解析开销,尤其对复杂 SQL(如 “多条件筛选订单”),提升执行效率 |
zeroDateTimeBehavior=convertToNull | 将数据库中的 0000-00-00 时间转为 null | 避免因时间格式不兼容导致的 SQL 执行异常,保障订单时间查询的稳定性 |
5.4 总结:这些配置解决了数据库的哪些核心问题?
- 连接管理问题:通过 Druid 连接池参数(
max-active
/min-idle
/ 超时回收),避免 “连接耗尽” 或 “无效连接”,支撑外卖高峰的高并发; - SQL 效率问题:通过 MyBatis 二级缓存、延迟加载、执行器复用,减少数据库访问次数,降低高频查询的压力;
- 批量操作问题:
rewriteBatchedStatements
等 URL 参数,优化外卖系统的批量场景(如批量导入菜品、批量更新订单); - 问题定位问题:
slowSqlMillis
记录慢 SQL,便于后续优化(如给慢查询加索引、调整 SQL 逻辑)。
这些配置与之前提到的 “索引优化”“ANALYZE TABLE 统计信息” 形成互补,共同保障数据库在高并发下的稳定和高效。
6.数据库监控实现
数据库监控是干嘛的?简单说,数据库监控就是「给数据库操作装个 “记录仪”」,帮你实时掌握:
- 数据库连接够不够用?有没有连接超时、浪费的情况?
- 执行的 SQL 快不快?有没有 “拖慢系统” 的慢 SQL?
- 有没有危险的 SQL(比如注入攻击)?
- 每个 SQL 执行了多少次、查了多少数据、耗时多久?
这里我们通过Druid连接池(阿里开源的数据库连接池工具)和日志配置来内置这套“记录仪”。
6.1 核心实现工具:Druid连接池
项目中的数据库连接使用spring.datasource.druid 相关配置,而Druid本身自带强大的监控功能,不需要额外集成第三方工具(如Prometheus),只需要通过配置 “开启” 即可。
在项目application.yml中配置:
spring:
datasource:
druid:
# 1. 开启监控相关的过滤器(核心!监控的“入口”)
filters: stat,wall,slf4j
# 2. 监控参数配置(比如“慢SQL”的定义、是否合并重复SQL)
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 3. 超时连接监控(记录“没人要”的连接)
remove-abandoned: true # 自动回收超时连接
remove-abandoned-timeout: 1800 # 超时时间:30分钟(1800秒)
log-abandoned: true # 记录超时连接的日志
先拆解这几个核心配置的作用 —— 它们是监控功能的 “开关” 和 “规则”:
配置项 | 通俗解释 | 监控作用 |
---|---|---|
filters: stat,wall,slf4j | 给 Druid 装 3 个 “插件” | - stat :SQL 执行监控插件(核心),记录 SQL 执行耗时、次数、行数 - wall :防 SQL 注入插件(附带监控),拦截危险 SQL 并记录 - slf4j :日志输出插件,把监控数据输出到日志文件 |
druid.stat.slowSqlMillis=5000 | 定义 “慢 SQL”:执行时间超过 5000 毫秒(5 秒)的 SQL | 自动标记慢 SQL,方便排查 “拖慢系统” 的语句 |
druid.stat.mergeSql=true | 把相同的 SQL 合并统计(比如 “SELECT * FROM user WHERE id=1” 和 “id=2” 算一类) | 避免重复 SQL 刷屏,更清晰看 “哪类 SQL 执行最多 / 最慢” |
remove-abandoned + log-abandoned | 连接 “没人要”(比如代码没关连接)超过 30 分钟,就回收并记录日志 | 监控 “连接泄露” 问题(连接泄露会导致数据库连接不够用) |
6.2 具体监控内容
基于上面的配置,项目会监控 3 大类核心数据,每类都能直接帮你排查问题:
1.数据库连接池监控(看 “连接够不够用”)
连接池是 “管理数据库连接的容器”(比如你配置的 initial-size=10
是初始 10 个连接,max-active=100
是最多 100 个连接)。监控会实时统计:
- 当前活跃的连接数(正在用的连接)
- 空闲连接数(没被用的连接)
- 等待连接的请求数(连接不够时,排队等连接的请求)
- 连接超时 / 泄露的次数(比如超过 30 分钟没关的连接,会被记录)
这些数据的来源,就是你配置的 initial-size
「初始连接数」、max-active
「最大连接数」、remove-abandoned-timeout
「超时时间」等 ——Druid 会自动统计这些配置对应的实际使用情况。
2.SQL 执行监控(看 “SQL 快不快、多不多”)
这是最常用的监控,stat
过滤器会拦截每一次 SQL 执行,记录:
- 基础信息:执行的 SQL 语句(合并后的,比如 “SELECT * FROM user WHERE id=?”)
- 性能信息:
- 执行总次数(这个 SQL 跑了多少次)
- 平均耗时(每次执行平均花多久)
- 最大耗时(单次执行最久的时间)
- 慢 SQL 次数(超过 5 秒的次数)
- 数据量信息:每次 SQL 返回多少行数据、影响多少行数据(比如 INSERT/UPDATE 的行数)
举个例子:如果监控到 “SELECT * FROM order WHERE create_time < ‘2024-01-01’” 执行了 100 次,平均耗时 6 秒,那这个就是 “慢 SQL”,需要优化(比如加索引)。
3.SQL 注入防护监控(看 “有没有危险 SQL”)
wall
过滤器(防注入插件)会监控并拦截危险 SQL,比如:
- 用户输入的恶意语句(比如 “SELECT * FROM user WHERE name=‘admin’ OR 1=1”—— 这会查所有用户数据)
- 禁止的操作(比如 DROP TABLE 删表、ALTER TABLE 改表结构)
如果有危险 SQL 被拦截,slf4j
插件会把这个行为记录到日志里,方便你知道 “有人在尝试攻击数据库”。
6.3 查看监控数据
两种方式,新手优先看日志。
1. 看日志文件
我们的logging配置已经指定日志输出路径和级别,监控数据会自动写到我们在application.yml中指定的文件中。
logging:
level:
com.alibaba.druid: info # Druid的监控日志输出级别(info级别的日志会记录)
org.mybatis: debug # MyBatis的SQL日志(会打印执行的SQL语句)
file:
name: logs/szj.log # 日志文件路径
你只需要打开指定的文件就能看到:
- 慢 SQL 日志:比如
[DruidDataSourceStatLogger] slow sql 5001ms: SELECT * FROM order WHERE ...
(明确标出耗时 5.001 秒的慢 SQL) - 连接泄露日志:比如
[DruidDataSource] abandon connection, url:jdbc:mysql://..., timeout:1800秒
(标出超时 30 分钟的连接) - SQL 执行日志:MyBatis 的
debug
级别会打印 “执行的 SQL 语句 + 参数”,比如==> Preparing: SELECT * FROM user WHERE id=?
==> Parameters: 1(Integer)
2.Druid 可视化控制台(更直观)
如果觉得看日志不够直观,还可以开启 Druid 自带的 Web 控制台(类似一个网页,能看到图表化的监控数据)。只需要在你的项目中加一段配置(Spring Boot 项目),就能通过浏览器访问:
spring:
datasource:
druid:
# 开启Druid控制台
stat-view-servlet:
enabled: true # 开启控制台
url-pattern: /druid/* # 访问路径:http://你的项目地址:8080/druid
login-username: admin # 控制台登录账号(自己设)
login-password: 123456 # 控制台登录密码(自己设)
配置后,打开浏览器访问 http://localhost:8080/druid
(如果你项目的端口是 8080),登录后就能看到:
- 「数据源」:连接池的实时状态(活跃连接、空闲连接等)
- 「SQL 监控」:所有 SQL 的执行次数、耗时排行(能直接按 “慢 SQL” 筛选)
- 「SQL 防火墙」:被拦截的危险 SQL 记录
- 「Web 应用」:请求对应的 SQL 执行情况
这种方式对新手更友好,能一眼看到 “哪个 SQL 最慢”“连接够不够用”。
一番猛烈的操作下来,恭喜你!你项目的监控功能已经 “Ready” 了!
7. 高并发场景的进阶优化(适合数据量大 / 请求多的业务)
7.1 读写分离
7.1.1 介绍
读写分离的核心逻辑是:把数据库的 “写操作”(插入 / 更新 / 删除,即 INSERT/UPDATE/DELETE)和 “读操作”(查询,即 SELECT)分别交给不同的数据库实例处理。
- 负责 “写操作” 的库叫 主库(Master):所有新增、修改、删除数据的请求,都只发给主库。
- 负责 “读操作” 的库叫 从库(Slave):所有查询数据的请求,都发给从库(可以有多个从库)。
- 关键动作:主库会自动把 “写操作的数据变化” 同步给从库,保证从库的数据和主库一致(这个过程叫 “数据同步”)。
7.1.2 作用
大部分业务场景都是 “读多写少” 的:比如电商网站,用户浏览商品(读)、搜索商品(读)的次数,远大于下单(写)、付款(写)的次数;再比如新闻 APP,用户看新闻(读)的次数远大于发布新闻(写)的次数。
如果只用单台数据库(单库):
- 写操作会 “锁表 / 锁行”(比如更新一条数据时,其他请求要等它结束),会拖慢同时发生的读操作;
- 大量读请求挤在一台机器上,CPU、内存、IO 都会被占满,导致查询变慢(比如用户刷商品列表加载半天)。
而读写分离能解决这些问题:
- 主库只专注 “写”,不用处理大量读请求,写操作更高效;
- 多个从库分担 “读” 请求,就算读请求再多,也能分摊压力(比如 1 个主库 + 3 个从库,读压力直接减为原来的 1/3);
- 从库挂了不影响写操作(主库还在),主库挂了可以切换从库当主库(高可用)。
7.1.3 主库如何将数据同步给从库
以最常用的 MySQL 为例,数据同步靠的是 “binlog(二进制日志)” 机制,步骤很简单:
- 主库记录 binlog:主库执行任何写操作后,都会把这个操作的细节(比如 “在 user 表插入一条 id=100 的记录”)记录到本地的 binlog 日志里;
- 从库拉取 binlog:从库会启动一个 “IO 线程”,主动去主库拉取 binlog 日志,存到自己本地的 “中继日志(relay log)” 里;
- 从库执行中继日志:从库再启动一个 “SQL 线程”,读取中继日志里的内容,然后在自己的库上重复执行这些写操作(比如同样插入 id=100 的记录),最终实现和主库数据一致。
这个过程会有毫秒级的延迟(比如主库写完,从库要 100ms 才能同步完),但大部分业务能接受(比如用户下单后,100ms 后才在 “我的订单” 里看到,完全感知不到)。
7.1.4 具体实现
普通项目不用自己写代码,直接用现成的工具 / 中间件就能实现,主要分两类:
实现方案 | 核心逻辑 | 优点 | 缺点 | 适合场景 |
---|---|---|---|---|
应用层方案 | 在代码里判断请求类型:是写请求就连主库,是读请求就连从库(比如用 Spring 的AbstractRoutingDataSource ) | 实现简单,不用额外加中间件 | 代码耦合度高(数据库逻辑侵入业务代码),新增从库要改代码 | 小型项目、读从库数量少 |
中间件方案 | 在应用和数据库之间加一个 “中间件”(比如 Sharding-JDBC、MyCat、ProxySQL),中间件自动判断请求类型并转发 | 业务代码无感知(不用改代码),支持动态增删从库 | 需要部署和维护中间件,有一定学习成本 | 中大型项目、读从库多 |
举个简单例子:用 Sharding-JDBC 实现读写分离,只需要在配置文件里加几行配置(指定主库地址、从库地址、哪些操作走读库),业务代码里还是正常用 MyBatis 查询,中间件会自动把 SELECT 请求转发到从库。
7.1.5 注意事项
- 数据一致性问题:如果刚写完主库,马上读从库(比如用户刚下单,立刻刷新 “我的订单”),可能因为从库没同步完导致读不到数据。解决办法:
- 核心业务(如订单)强制读主库;
- 用 “延迟判断”(比如写操作后,等待 100ms 再读从库);
- 用支持 “读写一致性” 的中间件(如 Sharding-JDBC 的 “Hint 强制路由”)。
- 从库故障处理:如果某个从库挂了,要能自动把读请求转移到其他从库(中间件一般自带 “故障切换” 功能);
- 主库故障处理:主库挂了后,需要从从库里选一个 “最优” 的升级为主库(比如用 MGR、Keepalived 实现主从切换),避免写操作中断。
7.2 分库分表
读写分离解决的是 “读多写少” 的问题,但如果数据量太大(比如单表数据超过 1000 万行,单库数据超过 100GB),就算做了读写分离,性能还是会崩 —— 比如查询一个 1 亿行的订单表,就算走索引,也要扫描几十万行数据,耗时几秒;备份这个表要花几小时,甚至备份过程中会拖慢数据库。
这时候就需要 “分库分表”:把一个 “超大库” 拆成多个 “小库”,把一个 “超大表” 拆成多个 “小表”,让每个小库 / 小表的数据量保持在 “高效处理范围”(比如单表 500 万行以内)。
7.2.1 区分分库和分表
- 分库(Database Sharding):把一个数据库(比如
order_db
)拆成多个数据库(比如order_db_01
、order_db_02
、order_db_03
),每个小库都在不同的服务器上(避免单服务器资源瓶颈)。 - 分表(Table Sharding):把一个表(比如
order
表)拆成多个表(比如order_2023
、order_2024
、order_01
、order_02
),这些小表可以在同一个库,也可以在不同库。
实际项目中,分库和分表经常一起用(比如先分库,每个库再分表),叫 “分库分表”。
7.2.2 两种核心拆分方式
分库分表的关键是 “按什么规则拆”,主要有垂直拆分和水平拆分两种,适用场景完全不同。
(1)垂直拆分:按 “列” 拆(把表拆成 “窄表”)
核心逻辑:根据表的 “列职责”,把一个 “宽表”(列多的表)拆成多个 “窄表”(列少的表),每个表存不同维度的数据。
比如:用户表(
user
)有很多列 —— 基本信息(id、name、phone、password)、详情信息(avatar、address、birthday、hobby)、账户信息(balance、points、level)。
垂直拆分后,拆成 3 个表:user_base
:存基本信息(id、name、phone、password)—— 高频访问(比如登录、查询用户姓名);user_profile
:存详情信息(id、avatar、address、birthday、hobby)—— 低频访问(比如查看用户资料);user_account
:存账户信息(id、balance、points、level)—— 涉及金钱,需要更高安全性。
为什么这么拆?
- 减少 “无效列加载”:查询用户登录信息时,不用加载 avatar、address 这些无关列,减少 IO 消耗;
- 按访问频率拆分:高频表和低频表分开,避免低频表的大字段(比如 avatar 是图片 URL,字段长)拖慢高频查询;
- 按安全性拆分:账户表单独存储,方便单独加权限控制、备份策略。
垂直分库:如果拆后的表数据量还是很大,或者访问量很高,可以把这些表放到不同的库(比如
user_base
和user_profile
在user_db_01
,user_account
在user_db_02
),避免单库压力。
(2)水平拆分:按 “行” 拆(把表拆成 “小表”)
核心逻辑:根据 “行的某个字段(叫拆分键 / 分片键)”,把一个 “大表”(行多的表)拆成多个 “小表”,每个小表存一部分行数据,结构完全相同。
比如:订单表(
order
)有 1 亿行数据,按 “订单创建时间”(拆分键)水平拆分,拆成order_2022
、order_2023
、order_2024
三个表,分别存 2022 年、2023 年、2024 年的订单。再比如:用户表(
user
)按 “用户 id”(拆分键)水平拆分,拆成user_01
(id 结尾为 1-3)、user_02
(id 结尾为 4-6)、user_03
(id 结尾为 7-9/0),每个表存 300 万行数据。拆分键怎么选?
拆分键是水平拆分的 “灵魂”,选不好会导致 “数据倾斜”(比如某个小表存了 80% 的数据,其他表只存 20%,白拆了)。核心原则:选 “查询频率高、能均匀分配数据” 的字段。- 常用拆分键:时间(订单、日志)、用户 ID(用户相关表)、区域 ID(地域相关表)。
- 避坑:不要选 “查询少、分布不均匀” 的字段(比如按 “用户性别” 拆,男女比例可能 1:1,但查询时很少按性别查,反而要跨表查所有性别数据)。
水平分库:如果拆后的小表还是在同一个库,单库的 IO、CPU 还是会瓶颈,就把小表分到不同库(比如
order_2022
在order_db_01
,order_2023
在order_db_02
,order_2024
在order_db_03
)。
7.2.3 具体实现
分库分表比读写分离复杂(要判断 “数据存在哪个库 / 哪个表”“跨库跨表查询怎么处理”),必须用中间件实现,主流中间件有:
中间件 | 核心特点 | 优点 | 缺点 | 适合场景 |
---|---|---|---|---|
Sharding-JDBC | 轻量级,是 “JDBC 驱动增强”(嵌入在应用里,不用单独部署服务) | 无额外部署成本,性能好,对业务代码侵入少 | 不支持跨库事务(需要自己处理),不适合超大型分布式系统 | 中小型分布式项目 |
MyCat | 重量级,是 “数据库代理”(独立部署服务,应用连 MyCat,MyCat 连数据库) | 支持跨库事务、分库分表规则丰富,适合超大型系统 | 需要单独部署维护,性能比 Sharding-JDBC 略低 | 大型分布式项目、传统架构迁移 |
举个例子:用 Sharding-JDBC 实现订单表水平分表(按时间拆成 2023、2024 表),只需要在配置文件里指定:
- 拆分表:
order
拆成order_2023
、order_2024
; - 拆分键:
create_time
(订单创建时间); - 拆分规则:2023 年的订单存
order_2023
,2024 年的存order_2024
。
业务代码里还是写SELECT * FROM order WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31'
,中间件会自动转发到order_2023
表查询。
7.2.4 注意事项
- 跨库跨表查询问题:比如按时间拆分订单表后,要查 “2023 年 12 月到 2024 年 1 月的订单”,需要同时查
order_2023
和order_2024
,中间件会自动 “合并结果”(但性能比单表查慢)。解决办法:尽量按拆分键查询,避免跨表; - 跨库事务问题:比如用户下单时,要在
order
表(order_db_01)和payment
表(payment_db_01)分别插数据,万一一个成功一个失败,会导致数据不一致。解决办法:用 “最终一致性” 方案(比如消息队列重试),或中间件支持的分布式事务(如 MyCat 的 XA 事务); - 数据扩容问题:如果拆了 3 个表,后来数据又满了,需要拆成 5 个表,怎么把原来的数据迁移到新表?解决办法:提前规划 “分片策略”(比如用 “一致性哈希”,扩容时只迁移部分数据),或用工具(如 ShardingSphere 的 DataSphere Studio)自动迁移;
- 备份和运维复杂度:原来备份 1 个库 1 个表,现在要备份多个库多个表,运维成本翻倍,需要用自动化运维工具(如 Ansible、Jenkins)。
7.2.5 总结:读写分离 vs 分库分表,怎么选?
对比维度 | 读写分离 | 分库分表 |
---|---|---|
解决的核心问题 | 读多写少的性能瓶颈(读请求太多) | 数据量太大的存储 / 查询瓶颈(单库单表撑不住) |
数据拆分方式 | 不拆分库表,只拆分 “读写请求” | 拆分库或表(垂直 / 水平拆分) |
适用场景 | 数据量不大,但读请求频繁(如新闻 APP、博客) | 数据量超大(单表千万级 +)(如电商订单、支付) |
组合使用 | 可以和分库分表一起用(先分库分表,再做读写分离) | - |
比如:电商平台的订单系统,会先按 “时间” 水平分库分表(2023 订单库、2024 订单库,每个库再拆成 12 个月度表),然后在每个分库上做读写分离(主库写,从库读订单列表)—— 这样既解决了数据量大的问题,又解决了读请求多的问题。
最后记住:技术是为业务服务的,不要一上来就做读写分离或分库分表。如果你的项目单库单表能扛住(比如数据量几十万、QPS 几百),先不用折腾;等数据量或访问量上来了,再根据瓶颈选择对应的方案。