4. 数据库性能调优:SQL查询优化、索引设计与调优、数据库连接池配置、读写分离与分库分表

数据库,往往是风控系统里最脆弱的那个环节。我见过太多系统,业务逻辑写得花里胡哨,结果一到高峰期,数据库先扛不住了。说白了,风控就是跟时间赛跑,数据库慢一秒,可能就放过去一笔坏账。

这一章,咱们就聊聊数据库调优的四个核心方向。嗯,都是我在生产环境里真刀真枪踩过的坑。

4.1 SQL查询优化:别让数据库做无用功

很多开发同学写SQL,习惯性「先查出所有字段再说」。你想想看,风控系统一张表动辄几十个字段,你SELECT *一下,数据库得把整行数据都捞出来,网络传输也慢。我个人的习惯是:只查你需要的字段

核心原则:减少扫描行数,减少回表次数,减少数据传输量。

举个例子,一个常见的风控查询:

-- 不推荐
SELECT * FROM risk_order WHERE status = 'PENDING' AND create_time > '2024-01-01';

-- 推荐
SELECT id, user_id, amount, risk_level 
FROM risk_order 
WHERE status = 'PENDING' AND create_time > '2024-01-01'
LIMIT 1000;

这里我加了LIMIT,为什么?因为风控查询往往只需要处理最近的一批订单,全量扫描就是灾难。我在项目中遇到过,一个没加LIMIT的查询,直接把从库CPU打满,整个报表系统瘫痪了半小时。

另外,避免在WHERE子句中对字段做函数操作。比如:

-- 这会让索引失效
SELECT * FROM risk_order WHERE DATE(create_time) = '2024-01-15';

-- 应该写成范围查询
SELECT * FROM risk_order 
WHERE create_time >= '2024-01-15 00:00:00' 
  AND create_time < '2024-01-16 00:00:00';

为什么会这样?因为数据库在索引里存的是原始值,你套个函数,它没法直接定位,只能全表扫描。嗯,这个坑我年轻时也踩过。

4.2 索引设计与调优:用对索引,事半功倍

索引不是越多越好。我见过一张表建了十几个索引,结果写入慢得像蜗牛。你想想看,每次插入一条数据,数据库要维护十几个索引树,能不慢吗?

索引设计,我总结了三句话:

  • 高区分度的列放前面(比如用户ID、订单号)
  • 等值查询的列放前面,范围查询的列放后面
  • 覆盖索引能避免回表,性能提升明显

小技巧:EXPLAIN看执行计划,关注type字段。如果是ALL(全表扫描),赶紧优化。如果是refrange,基本合格。const是理想状态。

举个例子,风控系统里最常用的查询是按用户ID查最近订单:

-- 联合索引:(user_id, create_time)
CREATE INDEX idx_user_time ON risk_order(user_id, create_time);

-- 这个查询会用到索引,且只扫描少量数据
SELECT id, amount, risk_level 
FROM risk_order 
WHERE user_id = 12345 
ORDER BY create_time DESC 
LIMIT 10;

这里user_id是等值查询,放前面;create_time是排序,放后面。而且我查的字段(id, amount, risk_level)都在索引里,这就是覆盖索引,不需要回表查数据行,速度飞快。

注意:索引不是银弹。如果一张表的数据量超过千万,即使走索引,随机IO也可能成为瓶颈。这时候就要考虑后面的分库分表了。

4.3 数据库连接池配置:别让连接成为瓶颈

连接池这个东西,很多人觉得「配大点准没错」。其实不然。我见过一个系统,连接池配了200个,结果数据库连接数被打满,新的请求进不来,旧的请求在排队,整个系统雪崩。

连接池的核心参数,我重点说三个:

参数 说明 建议值
maximumPoolSize 最大连接数 CPU核心数 × 2 + 1(经验公式)
minimumIdle 最小空闲连接 通常等于最大连接数,避免频繁创建
connectionTimeout 获取连接的超时时间 1000ms(风控场景要快)

我个人习惯用HikariCP,性能确实好。配置示例:

# HikariCP 配置
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=20
spring.datasource.hikari.connection-timeout=1000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000

这里有个坑:连接池大小不是越大越好。数据库的CPU和IO是有限的,连接数太多反而会导致上下文切换频繁,性能下降。我曾经在一个项目里,把连接池从50降到20,吞吐量反而提升了30%。

4.4 读写分离与分库分表:从架构层面解决问题

当单库扛不住的时候,就该上读写分离和分库分表了。说白了,就是「把压力分散开」。

读写分离

风控系统的特点是:写少读多。大部分操作是查询历史记录、计算风险分,只有最终决策时才写入。所以读写分离非常合适。

我一般用ShardingSphere或MyCat来做。配置一个主库(写),两个从库(读)。查询走从库,写入走主库。注意一点:主从延迟。风控场景里,刚写入的数据可能马上要读,这时候如果从库还没同步,就会读到旧数据。

解决方案:对于强一致性的查询,强制走主库。比如「查询刚创建的订单」,加个注解或参数,指定路由到主库。

分库分表

分库分表是最后的大招。我建议不要过早分库分表,先试试索引优化、缓存、读写分离。如果单表数据超过5000万,或者QPS超过5000,再考虑分库分表。

分片策略,我常用的是:

  • 按用户ID分片:风控系统天然适合,同一个用户的数据都在一个库,查询快
  • 按时间分片:比如按月分表,历史数据可以归档
  • 一致性哈希:避免数据倾斜,但扩容时迁移数据麻烦

举个例子,按用户ID分库:

-- 分片规则:user_id % 16,分成16个库
-- 查询时,根据user_id计算目标库
SELECT * FROM risk_order_${db_index} 
WHERE user_id = 12345 
ORDER BY create_time DESC 
LIMIT 10;

这里要注意:跨分片的查询要避免。比如「查所有用户的异常订单」,这种查询得遍历所有分片,性能很差。我一般会设计一个「全局表」来存这种汇总数据,或者用Elasticsearch来做。

避坑指南:我曾经在一个项目里,分库分表后忘了改auto_increment主键,结果不同库生成了相同的主键ID,导致数据冲突。后来改用雪花算法(Snowflake)生成全局唯一ID,才解决这个问题。

知识体系总览

下面这张图,是我对数据库性能调优的整体理解。你可以把它当作一个检查清单:

数据库性能调优核心路径 数据库性能调优 SQL查询优化 索引设计与调优 连接池配置 读写分离/分库分表 只查必要字段 避免函数操作 合理使用LIMIT 联合索引顺序 覆盖索引 EXPLAIN分析 合理设置大小 超时时间控制 HikariCP推荐 主从分离 分片策略 全局ID生成 核心思路:从SQL到架构,层层递进,避免过早优化 先优化查询 → 再设计索引 → 配置连接池 → 最后考虑分库分表

数据库调优这件事,没有银弹。你得根据业务场景,一步步排查。我个人习惯是:先看慢查询日志,再看执行计划,最后才动架构。很多时候,一条索引就能解决90%的问题,没必要上来就分库分表。

嗯,这一章的内容就到这里。记住,数据库是风控系统的基石,把它调好了,后面的工作才能事半功倍。