拓冰建站拓冰建站
首页 / 资讯中心 / 正文

后端性能优化:先从数据库索引和缓存策略入手

后端性能优化很多人一开口就是微服务、消息队列、异步化、读写分离仿佛不把这些词挂嘴边就不够高级。但等你真正把问题查到底十有八九会发现数据库慢了缓存没用好。索引和缓存才是后端性能优化的基本功也是绝大多数性能事故的根源所在。跳过它们去谈架构改造等于没学会走就想跑马拉松。先说数据库索引。我见过太多团队一遇到慢查询就加索引加完之后慢查询依旧于是骂数据库垃圾骂ORM垃圾就是不肯骂自己。索引不是越多越好更不是随便加个字段就能起飞。索引的本质是数据结构是拿额外的存储空间和维护成本去换取查询时的快速定位。你用错了它它就会反噬你。索引设计的核心原则只有一条让你的查询尽量走覆盖索引减少回表。回表意味着随机IO随机IO是机械硬盘时代的噩梦到了SSD时代虽然好了一些但仍然是数据库性能的头号杀手。如果一条SQL语句查出来的字段在索引树里全部存在那连数据页都不用碰速度能快一个数量级。反之如果你查了五十个字段而索引只包含两个字段MySQL就得去主键B树里反复随机读取再快的SSD也救不了你。很多人喜欢在where条件上用函数比如WHERE DATE(create_time) 2025-01-01。你猜猜这会发生什么只要对索引列使用函数索引就会失效数据库只能全表扫描。这是最基础的知识点但我在无数生产环境里见过这种写法。正确的做法是写成范围查询WHERE create_time 2025-01-01 AND create_time 2025-01-02。这一改可能就让一条原本要扫一亿行的SQL变成只扫几千行。再讲讲联合索引的字段顺序。联合索引有一个最左前缀原则很多人背得滚瓜烂熟但一用就错。最左前缀不是说你查了最左边的字段就行而是要求查询条件必须从索引最左字段开始并且不能跳过中间的字段。比如你有索引(a, b, c)查询WHERE b 1 AND c 2是走不了这个索引的但查询WHERE a 1 AND c 2可以走a部分c部分则用不了。很多人以为加了联合索引就万事大吉结果查询条件根本不按套路出牌索引形同虚设。索引的选择性决定了它的价值。选择性是指不重复的索引值与表记录总数的比值越接近1越好。比如性别字段只有男女两个值选择性就是2/总行数极小。你给性别字段加索引查询结果要过滤掉一半的数据数据库还不如直接全表扫。相反用户ID、订单号这类字段选择性极高索引起效快。实践中很多人喜欢给状态字段加索引比如订单状态、支付状态这些字段基数极小再加上筛选条件往往还带着时间范围索引就很难发挥应有的威力。那是不是索引加得越多越保险绝对不是。索引是拿空间换时间但空间本身也是时间。插入一条记录数据库要同时维护主键索引、唯一索引、普通索引、联合索引……每一个索引都是一棵额外的B树写一条数据就要往N棵树里插节点写入性能必然下降。更可怕的是索引还会占用大量内存缓存当你把缓冲池塞满无用的索引真正热的数据反而被挤出了内存。我见过一个系统表里只有两百万行数据却建了二十多个索引结果每次插入耗时超过200毫秒。删掉冗余索引后插入响应直接降到5毫秒查询反而更快了。慢查询优化有些人靠猜有些人靠看执行计划。请记住永远不要相信直觉要相信explain。EXPLAIN SELECT ...会让你看到访问类型type、可能的索引possible_keys、实际使用的索引key、扫描行数rows、额外信息Extra。如果你看到typeALL那就是全表扫描危险看到Extra里有Using filesort那说明排序没有走索引也危险。有一次我在线上排查一个慢SQL开发者自信满满地说肯定走了索引我用explain一看索引明明就在那但key列是NULL原因是隐式类型转换——字符串字段和数字比较MySQL把列转换成数字索引直接失效。这种问题靠肉眼是看不出来的只有explain能让你看到真相。索引聊完了再聊缓存。缓存的本质是用额外的存储设施去承载那些反复被读取的热数据。但很多团队对缓存的理解停留在把数据塞进Redis就行完全没有考虑数据从哪来、怎么更新、失效了怎么办。缓存最经典的问题是缓存穿透。穿透是查一个根本不存在的数据缓存没有数据库也没有每次请求都直接打到数据库。攻击者只需要伪造一堆不存在的ID就能轻易击穿数据库。这就好比你家门口有个门卫缓存有人来敲门你说没这个人结果每个访客你都让进去找一遍门卫形同虚设。解决办法很简单对空值也进行缓存哪怕缓存一个查无此人几分钟或者用布隆过滤器在缓存层之前快速过滤掉根本不可能存在的key。这两种方法都有效但布隆过滤器更优雅只是它有误判率需要权衡。然后是缓存击穿。击穿是一个热点key在过期的瞬间大量并发请求同时打到底层数据库。想象一下双十一零点某个爆款商品的库存key正好在那一刻过期瞬间涌入的百万请求全都冲到了数据库数据库直接宕机。解决办法有几个思路第一把热点key的过期时间设置成一个随机值避免同一时刻大量key集体失效第二用互斥锁当某个key失效时只允许一个线程去重建缓存其他线程等待结果第三逻辑过期不设置物理过期时间而是把过期时间作为字段存在value里异步去刷新。三种方案各有优劣但最简单粗暴的往往是给热点key加互斥锁代价小见效快。再有是缓存雪崩。雪崩是大量key同时过期或者Redis节点整体宕机导致请求全部落到数据库。如果击穿是狙击枪打一个点雪崩就是地毯式轰炸。应对雪崩的第一原则不要让Redis成为单点。主从复制、哨兵、集群这些基础的高可用手段必须上。其次过期时间要加随机抖动不要用固定的TTL。很多工程师习惯把缓存过期时间设置成统一的30分钟这很危险无数个相同TTL的key会在同一时间段集体失效形成周期性的数据库压力尖峰。但缓存策略最难的不是穿透、击穿、雪崩而是数据一致性。你更新了数据库缓存怎么办先删缓存再更新数据库先更新数据库再删缓存还是写一个复杂的双写方案每种做法都有坑。先删缓存再更新数据库如果删完缓存还没更新数据库之时来了一个读请求发现缓存没命中就会去读旧数据并重新写进缓存之后你再更新数据库缓存里永远都是旧数据。先更新数据库再删缓存也有滑动窗口期但相对安全因为只要保证删缓存这个操作一定成功就行。现实中很多团队喜欢用延迟双删先删缓存更新数据库睡眠几百毫秒再删一次缓存。这个方法的本质是赌一个时间窗口把第一次删除之后、更新数据库之前可能写入的旧缓存再次干掉。但这并不可靠因为睡眠几百毫秒是拍脑袋定的在分布式环境下网络延迟、线程调度都可能导致第二次删除发生在新的旧数据之后。更可靠的做法是监听数据库的binlog用消息队列异步重建缓存。把binlog变更解析成消息消费端负责把最新的数据写入缓存这样即使并发下有问题通过消息重试也能最终一致。别用那些花里胡哨的双写方案一份binlog一个消费者看似简单却最稳。说完了缓存的各种坑我再泼一点冷水缓存不是万能的它能解决读多写少的性能瓶颈但掩盖不了架构设计上的缺陷。如果一个系统天天被暴力请求打爆你用缓存挡住了但底层数据库的慢查询依旧存在索引设计依旧稀烂。等到缓存集群也撑不住了你怎么办加缓存实例加吧加到内存比数据库还大然后发现缓存本身的网络成了瓶颈。后端性能优化的正确路径永远是先优化数据库消除慢查询再考虑加缓存把真正的热数据放在离用户最近的地方。缓存不是银弹如果你用缓存去掩盖一个低级查询那只是把问题推后而问题迟早会以更丑的方式爆发出来。举个实战案例。之前帮一个客户排查线上系统订单列表接口平均响应1.8秒高峰期直接超时。一开始团队想加Redis缓存打算把订单列表整个存进去。我没有急着加缓存而是先看数据库。索引结构订单表有三个联合索引但列表查询用的是WHERE user_id ? AND status ? ORDER BY id DESC LIMIT 20。我explain一跑发现status字段的基数值极低只有两三个不同值。在联合索引里把低基数的status字段放在前面等于一开始就排除了大部分数据但用户ID才是真正的过滤主力。于是把索引改成(user_id, status)再加一个覆盖索引(id, user_id, status, order_no, amount)结果查询时间从0.8秒降到了0.02秒。这还没加缓存接口响应已经快了40倍。后面再针对热点用户加缓存那才是锦上添花。一个设计良好的索引能解决80%的慢查询问题。剩下的20%才是缓存、分库分表、读写分离这些重武器。但你如果反着来一上来就搞缓存把查询结果一股脑塞进去等缓存失效的那一刻数据库照样被打垮。缓存的价值在扛住瞬时高峰而索引的价值在让每次查询都轻松——它们是防御的不同层面缺一不可。回到最开始的问题后端性能优化为什么从索引和缓存入手因为这是成本最低、见效最快的两条路。改一行SQL加一个合理的索引可能比引入十台缓存服务器更有效。我做后端这么久没见过哪个系统是因为索引太优而被拖垮的倒是见过不少因为索引混乱、缓存滥用而把自己埋进坑里的。别追求那些玄乎的架构名词先低下头看看你的建表语句看看每一条SQL的执行计划再看看Redis里到底存了什么、过期策略是什么、更新策略是什么。性能优化的本质是把每一分CPU、每一块磁盘、每一笔网络开销都用在刀刃上。而索引和缓存恰好是最锋利的刀。最后的忠告定期审视你的索引和缓存策略就像定期体检一样。表数据量增长了一个数量级原来的索引可能就不再适用业务访问模式变了原来的缓存TTL可能就成了定时炸弹。没有一劳永逸的优化只有持续演进的设计。把这两件事做好你的后端性能就已经跑赢了大多数系统。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门