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(全表扫描),赶紧优化。如果是ref或range,基本合格。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,才解决这个问题。
知识体系总览
下面这张图,是我对数据库性能调优的整体理解。你可以把它当作一个检查清单:
数据库调优这件事,没有银弹。你得根据业务场景,一步步排查。我个人习惯是:先看慢查询日志,再看执行计划,最后才动架构。很多时候,一条索引就能解决90%的问题,没必要上来就分库分表。
嗯,这一章的内容就到这里。记住,数据库是风控系统的基石,把它调好了,后面的工作才能事半功倍。