
后端性能优化最先该做的不是引入消息队列、拆微服务而是低头看看你的数据库查询和缓存设计。大多数后端服务的性能瓶颈都死在一条查了百万行然后丢弃九十九万九千行的SQL上以及一个每个请求都去穿透数据库的缓存方案上。用户反复点击刷新数据库反复全表扫描服务器CPU飙红而这一切的根源往往只是因为我们没有想清楚“数据到底从哪来”和“数据如何被复用”。查询的代价一次全表扫描到底有多贵数据库查询的优化本质是减少数据访问的量级而不是增加服务器的算力。你加十台应用服务器也不会让一条五分钟的慢查询变成一百毫秒。慢查询的花销往往浪费在三个地方全表扫描、随机I/O、排序和临时表。以MySQL为例没有索引的where条件会让存储引擎老老实实地把每个页都读一遍然后Buffer Pool被无关数据污染内存命中率骤降磁盘I/O飙升——这是一个连锁反应。很多团队把优化等同于“加索引”这当然没错但加错索引比不加索引更可怕。一个低选择性字段上的索引会让优化器选择全表扫描一个冗余索引会让写入变慢三倍。创建索引前你得用EXPLAIN看执行计划看type、rows、Extra。如果Extra里有“Using filesort”“Using temporary”说明你的排序和分组正在内存中制造昂贵的工作表。执行计划不会说谎说谎的是你自以为“应该有索引”的直觉。另外避免使用SELECT 。它会把不需要的列也读出来导致无法利用覆盖索引还会增加网络传输字节。覆盖索引是一种让索引本身包含查询所需全部字段的结构它能避免回表减少一次随机I/O。回表是数据库优化的隐形敌人覆盖索引是它最廉价的解药。慢查询日志先找到罪魁祸首没有慢查询日志就不存在性能优化。你对着一个线上系统凭感觉调参就像在黑夜里扔飞镖。开启慢查询日志设置阈值比如超过一秒钟就记录下来然后每天看一遍。你会发现真正拖垮系统的往往只有那几条查询它们也许出现了几百次每次都很慢加起来就是几十分钟的等待。优化一条被调用一百次的慢查询比优化一百条被调用一次的慢查询回报高一百倍。读性能优化如果只盯着SQL本身还是会被某些怪物绊倒。比如N1问题。它经常出现在ORM框架的“懒加载”功能里。你查询了十个订单然后遍历每个订单访问它的客户信息——ORM发现客户信息没有加载于是自动执行一次查询一次十次一百次。这就是N1。N1问题不是SQL写错了而是访问模式错了。它把一个本来一条JOIN就能解决的事变成了N1次往返而且每次往返都有网络开销、SQL解析、权限校验。解决方案也很直接用JOIN或子查询一次性把关联数据取出如果数据量太大就分页批量查询使用IN限定条件。或者在ORM里显式地“预加载”。批量是数据库查询优化最底层的哲学——一次多拿别反复问。真正的极端情况不是一百条数据而是一万条你要把往返次数从一万次压到十次。缓存设计不是所有数据都值得缓存查询优化到极限仍然扛不住高并发时缓存就登场了。缓存不是银弹而是放大镜。它会把热数据的性能放大但也会把一致性、内存泄漏、序列化开销等问题一起放大。先说缓存的对象它是第一个决策点只缓存那些被重复读取且变化频率较低的数据。比如商品详情、用户基本信息、配置参数。至于库存、积分这类每个请求都可能变的数据缓存要非常小心。缓存设计的第一原则是只缓存那些被重复读取且变化频率较低的数据。缓存粒度是另一个决策点。有人喜欢把整个接口返回结果缓存起来这很简单但风险在于任何字段更新都会导致整个缓存失效。更精细的做法是分字段缓存或者在服务层缓存可复用的“零件”。缓存粒度越粗失效成本越高粒度越细管理复杂度越大——这个平衡点需要用业务访问模式来定夺。缓存过期时间则需格外谨慎。太短命中率低数据库压力大太长数据陈旧。没有完美的过期时间只有“业务可接受的过期时间”。所谓最终一致性就是用户能接受的短暂偏差。比如商品名称晚一秒更新用户可以理解但支付状态晚一秒更新就会引发投诉。所以你得为不同数据设定不同的过期策略。缓存一致性双删只是开始缓存和数据库之间的一致性问题是后端工程师最头疼的事。经典的Cache Aside模式读的时候先读缓存没有则读数据库再写缓存写的时候先更新数据库然后删缓存。这个模式有一个小坑更新数据库成功后删除缓存失败怎么办下次读到的就是旧缓存。缓存失效是所有分布式系统最昂贵的bug。它会造成数据错乱却极难排查。于是有了“延迟双删”先删缓存再更新数据库等几百毫秒后再删一次缓存。但这只是概率上减少窗口期并不能保证绝对一致。更彻底的做法订阅数据库的Binlog监听到数据变化后异步删除缓存。放弃强一致才能获得高性能。如果你非要强一致那不如不缓存直接用数据库事务回到性能地狱。还有一种思路对不强制一致的读允许短暂脏读比如把过期时间设成10秒那10秒内读到旧版本也无妨。高性能的后端通常在“一致性”上做的不是加法而是减法。把不必要的强一致减掉你会发现缓存设计的空间大大拓宽。缓存三大灾难穿透、击穿、雪崩缓存穿透查询一个根本不存在的数据缓存和数据库都没有于是每次请求都打到数据库。攻击者可以构造不存在的ID让你的数据库瞬间崩溃。穿透的根本问题在于缓存不缓存不存在的东西。解法缓存空值设置短过期时间或者用布隆过滤器前置判断。缓存击穿一个热点Key在过期瞬间大量请求同时访问它全部穿透到数据库。这时要加互斥锁只放一个请求去查库其他等待。注意等待的线程要设置超时否则一个慢查询拖死所有线程。击穿的本质是热点数据失效时我们缺少一次性重建的约束力。缓存雪崩大量Key在同一时间过期或者Redis宕机导致流量全部涌向数据库。解法过期时间加随机扰动避免集体过期Redis高可用以及限流和降级。缓存雪崩不是缓存的问题是设计者没有敬畏时间。你是把系统对缓存的依赖当成了常态却没有考虑它消失时该怎么办。查询与缓存协同一套完整的性能优化闭环在实际项目中数据库查询优化和缓存设计必须协同。比如对热点列表数据先用条件组合成缓存key命中直接返回未命中则查数据库但查数据库的SQL首先要经过索引和覆盖索引优化这样即使缓存失效数据库也能扛住。还需要“本地缓存分布式缓存”多级结构应用服务器的本地内存提供纳秒级访问Redis提供毫秒级访问数据库提供几十毫秒级访问。每一级缓存都是在为下游减轻压力而不是为自己增加负担。要主动监测缓存命中率、慢查询数量、数据库QPS。当缓存命中率低于90%时就要查查是不是key设计不合理、过期时间太短或者被穿透攻击了。没有监控的优化都是盲人摸象。最好建立一套“性能账单”每天自动报告哪些查询慢哪些缓存失效频繁哪些接口耗时上涨。用数据驱动下一步优化而不是凭经验反复测试。写到这里给所有后端开发者的忠告是优化数据库和缓存永远先做“减法”再做“加法”。减法包括减少查询字段、减少返回行数、减少不必要的索引、减少缓存依赖。加法包括加合理的索引、加缓存层、加预加载、加保护机制。性能优化的本质是用最小的资源消耗完成数据最恰当的搬运。而你手里最重要的工具不是更快的服务器而是对数据访问路径的深刻洞察。