一、索引设计:数据库的「高速公路」
1. 索引基础原理
- B+Tree索引
- 结构特点:叶子节点形成链表,适合范围查询(如
WHERE age > 20
) - 文件排序:非叶子节点只存索引值,叶子节点存完整数据或主键(决定是否回表)
- 结构特点:叶子节点形成链表,适合范围查询(如
- 哈希索引
- 适用场景:精确匹配(如
WHERE user_id = 1001
) - 限制:不支持范围查询,内存消耗大(常用于内存表)
- 适用场景:精确匹配(如
2. 索引设计策略
场景 | 优化方案 | 示例 |
---|---|---|
高频查询 | 联合索引覆盖查询字段 | INDEX (user_id, status) 覆盖 SELECT status FROM orders WHERE user_id=123 |
排序优化 | 索引顺序匹配排序字段 | INDEX (create_time DESC) 优化 ORDER BY create_time DESC |
模糊查询 | 倒序存储+前缀索引 | 手机号 138****1234 存为 4321****831 ,用 LIKE '4321%' 避免左模糊 |
3. 索引失效的典型场景
-
隐式类型转换
sql-- user_id 是字符串类型,但用数字查询(导致全表扫描) SELECT * FROM users WHERE user_id = 10086;
-
函数操作索引列
sql-- 索引失效:对 create_time 使用函数 SELECT * FROM logs WHERE DATE(create_time) = '2023-10-01';
二、慢查询分析:数据库的「体检报告」
1. 诊断工具链
-
慢查询日志配置
sql-- 开启慢查询日志(阈值设为1秒) SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;
-
执行计划解读
sqlEXPLAIN SELECT * FROM orders WHERE amount > 100;
- 关键字段解析:
type
:const
(主键) >ref
(普通索引) >ALL
(全表扫描)rows
:预估扫描行数(越小越好)Extra
:Using filesort
(需优化排序字段索引)
- 关键字段解析:
2. 高频性能问题及优化
-
深度分页优化
sql-- 低效写法(扫描前100000行) SELECT * FROM orders LIMIT 100000, 10; -- 优化写法(利用覆盖索引) SELECT * FROM orders WHERE id > (SELECT id FROM orders ORDER BY id LIMIT 100000, 1) LIMIT 10;
-
大表COUNT优化
- 方案:使用
Redis
计数器 或MySQL
汇总表(定期更新总数)
- 方案:使用
三、分库分表:数据库的「分布式扩展」
1. 何时需要分库分表?
指标 | 阈值参考 | 处理方案 |
---|---|---|
单表行数 | > 500万 | 分区表 → 读写分离 → 分片 |
数据体积 | > 50GB | 归档历史数据 → 分库分表 |
QPS峰值 | > 5000 | 缓存优化 → 分库分表 |
2. ShardingSphere核心功能
分片策略
-
分片键选择
- 要求:离散度高(如用户ID)、业务相关性(如订单时间)
-
分片算法
yaml# 按用户ID取模分库(2个库) shardingAlgorithms: user_id_mod: type: MOD props: sharding-count: 2
分布式事务
- XA模式:强一致性,性能较低(适合资金交易)
- Seata AT模式:最终一致性,高性能(适合订单状态更新)
3. 分库分表后的问题与解法
- 跨分片查询
- 方案:
- 业务层合并结果(如查询用户最近3月订单)
- 建立全局索引表(牺牲写性能换查询效率)
- 方案:
何为全局索引表
以下以 MySQL 为例,展示创建全局索引表的 SQL 语句:
sql
-- 创建全局索引表
CREATE TABLE global_index_table (
-- 全局唯一标识,通常使用 UUID 或业务主键
global_id VARCHAR(36) NOT NULL PRIMARY KEY,
-- 用于查询的索引字段,例如用户 ID
index_field VARCHAR(20) NOT NULL,
-- 分库标识
db_shard_id INT NOT NULL,
-- 分表标识
table_shard_id INT NOT NULL,
-- 其他必要的信息
other_info VARCHAR(255),
-- 创建索引,提高查询性能
INDEX idx_index_field (index_field)
);
同步数据到全局索引表
在数据插入、更新或删除时,需要同步更新全局索引表。可以通过以下几种方式实现:
- 应用层代码实现:在应用程序中编写代码,在数据操作时同时更新全局索引表。例如,在插入订单数据时,同时插入一条对应的全局索引记录。
java
// Java 示例代码
public void insertOrder(Order order) {
// 插入订单数据到分库分表
orderDao.insert(order);
// 插入全局索引记录
GlobalIndex globalIndex = new GlobalIndex();
globalIndex.setGlobalId(UUID.randomUUID().toString());
globalIndex.setIndexField(order.getUserId());
globalIndex.setDbShardId(order.getDbShardId());
globalIndex.setTableShardId(order.getTableShardId());
globalIndexDao.insert(globalIndex);
}
- 数据库触发器实现:在数据库中创建触发器,当数据发生变化时自动更新全局索引表。例如,在订单表上创建插入触发器:
sql
-- 创建插入触发器
DELIMITER //
CREATE TRIGGER insert_order_trigger
AFTER INSERT ON order_table
FOR EACH ROW
BEGIN
INSERT INTO global_index_table (global_id, index_field, db_shard_id, table_shard_id)
VALUES (UUID(), NEW.user_id, NEW.db_shard_id, NEW.table_shard_id);
END //
DELIMITER ;
- 数据校验和修复:定期对全局索引表进行数据校验,发现不一致的数据及时进行修复。
使用全局索引表进行查询
在进行查询时,首先通过全局索引表定位到数据所在的分库分表,然后再到相应的分库分表中查询具体的数据。
sql
-- 根据用户 ID 查询订单信息
SELECT *
FROM order_table
WHERE db_shard_id = (SELECT db_shard_id FROM global_index_table WHERE index_field = 'user_id')
AND table_shard_id = (SELECT table_shard_id FROM global_index_table WHERE index_field = 'user_id');
- 数据倾斜
- 解法:动态分片(如按热点用户ID单独分库)
演进路径
- 单库阶段
- 索引优化:
(user_id, create_time)
联合索引 - 查询优化:避免
SELECT *
,使用覆盖索引
- 索引优化:
- 读写分离
- 主库处理写操作,3个从库负载均衡读请求
- 分库分表
- 垂直分库:拆分为
订单库
、用户库
- 水平分表:订单表按
user_id % 8
分8个库,每个库按月份
分12张表
- 垂直分库:拆分为
工具推荐
- 压测工具 :
sysbench
(模拟高并发订单创建) - 监控平台 :
Prometheus
+Grafana
(跟踪慢查询与分片负载)