得直接开始正题。聊到互联网后端我估计前面两篇已经把HTTP协议、Web框架、接口设计这些基础盘得差不多了这篇咱们必须得换个频道聊聊在“学习记录”这个系列里最劝退、也最躲不开的硬骨头——数据库与缓存。你去看各大招聘网站的岗位要求后端工程师的JD里写的是“精通MySQL与Redis”但面试时候问的全是库表结构和缓存穿透。说白了后端这行当前半场比的是谁接口写得快后半场拼的是谁数据倒腾得稳。今天这篇我就把自己在这阶段踩过的坑、翻过的车、连夜修复的线上故障沉淀成一份能直接抄作业的学习实录重点说说关系型数据库到底怎么学才算入门以及缓存这尊大佛该怎么供。这篇笔记主要面向的是有Servlet或Spring基础、正在尝试独立做项目、或者已经在公司实习天天跟CRUD打交道的朋友。如果你还在纠结“数据库不就是写SQL吗”那这篇内容可能正好能帮你掰扯清楚设计层面的弯弯绕绕。1. 从“会写SQL”到“懂表结构”的认知跨越1.1 为什么学了增删改查还是做不好系统我见过太多同学包括当时的我自己以为数据库这关过了——CRUD写得飞起连表查询不在话下觉得自己已经是后端工程师了。结果一到自己做项目、或者接手公司的老系统看到几百行的存储过程、一堆冗余字段的订单表、乱七八糟的加索引习惯直接傻眼。其实这里有个核心认知误区SQL语法是语言能力表结构设计才是架构能力。你会说法语跟你能用法语写出一部小说是两码事。后端开发里前者决定你能否混过试用期后者决定你能不能接得住大流量的冲击。大多数新手写的系统之所以一上线就卡死就是因为表设计得一塌糊涂——该拆的表没拆该冗余的字段没冗余该建索引的地方没建不该建索引的地方糊了一堆。我当初第一个模拟项目X是个简单的博客系统用户表、文章表、评论表当时觉得设计得挺美三张表搞定一切。直到有一天地址栏被人拼上了诡异的条件几条慢查询直接把数据库干趴了我才意识到写SQL谁都会但你要能预判这个表未来三年会怎么变、会有哪些查询场景、哪些字段会被高频更新这才是功力。1.2 后端视角下数据库的三种角色等到后来跟着某导师做某跨平台系统我才慢慢理顺了一个理念数据库在系统里其实承担着三种完全不同的角色你必须分开去理解它们而不是笼统地觉得“数据库就是存数据的”。第一种是存储引擎。这是数据库最底层的物理机制InnoDB还是MyISAM决定了你的数据怎么落盘、怎么加锁、怎么崩溃恢复。对于绝大多数互联网应用闭眼选InnoDB就行什么外键约束、事务支持、行级锁全是它给的。你非要用MyISAM去省那点空间并发一上来数据错乱就够你喝一壶的。第二种是查询引擎。这是你跟数据库对话的界面SQL写得好不好、索引命中没命中全看这个层面。高阶一点的还有执行计划分析、优化器怎么选索引的问题你得能看懂EXPLAIN的输出才知道自己的SQL到底烂在哪里。第三种是分布式基础设施。等数据量过亿、读写比例失衡之后单机数据库再能打也扛不住这时候主从复制、读写分离、分库分表、中间件全得安排上。这个阶段其实已经超出绝大多数后端开发初期的学习范畴了但你心里得有这根弦知道数据库不是神它也有天花板。分清这三种角色之后你再去看那些数据库调优的文章会发现思路完全不一样了——你不是在琢磨一条SQL怎么写你是在权衡一个系统里各个组件的职能边界。2. 关系型数据库的事务与锁必须得掰碎了讲2.1 事务隔离级别别只背口诀面试的时候最容易被问的就是事务隔离级别很多同学张口就来“读未提交、读已提交、可重复读、串行化”但你再追一句“MySQL默认是哪个为什么选它”就哑火了。MySQL InnoDB 默认的是可重复读REPEATABLE READ这事儿得从历史包袱讲起。MySQL的二进制日志格式在早期版本里如果使用读已提交READ COMMITTED会导致主从复制时数据不一致。所以官方干脆把默认级别定成可重复读配合间隙锁Gap Lock来解决一部分幻读问题。你听起来觉得这很随意但实际做后端开发你就是要面对这种“默认值未必是最合理、但却是最兼容”的现实。实操的时候我给你的建议是不要死记隔离级别要去想你的业务场景里会不会出现脏读、不可重复读、幻读。比如一个转账接口你肯定要保证两个账户余额的一致性那可能得上SERIALIZABLE不对其实你用可重复读配合行锁就够了。比如一个统计报表的查询它只读一次快照用读已提交也完全没问题。你得把每个隔离级别想象成一个安全等级不同的玻璃房你能看到外面的数据变化程度不一样同时你对外界数据的变更封锁程度也不一样。2.2 锁的粒度与死锁复盘锁这块我一个过来人给你提个醒学锁不是在学数据库理论是在学并发编程。MySQL的行锁、表锁、间隙锁、意向锁本质上是操作系统里PV操作在数据库领域的映射。说个我自己真实踩过的大坑。某跨平台系统的订单模块当时为了给用户增加积分先查用户表再更新积分表结果同一时间两个请求互相持有对方的行锁位置直接死锁业务回调报错用户积分没到账还投诉。排查了半天才发现是代码里更新账户信息的顺序不一致——一个事务先更新A表再更新B表另一个事务先更新B表再更新A表。后来跟某导师复盘他一句话点醒我所有事务里涉及多行更新的必须按照固定的顺序来访问资源。这就跟你过狭窄的桥一样大家都靠右走谁也堵不住谁。所以后来我在代码规范里写了条死规矩——多表更新时按照表名的字典序来上锁。这招土是土了点但在绝大多数业务系统里它确实能帮你规避掉80%以上的死锁风险。2.3 索引的底层逻辑与失效陷阱B树索引的原理我不打算展开太多计算机组成原理的细节你可以把它理解成一个“有序的、带层次的多级目录”。MySQL每次查数据都是先翻目录走索引再翻正文回表查数据。你要想让查询快本质上是让这个“翻目录”的动作尽量少。实际操作中新人最容易犯的错就是建了一堆索引结果查询的时候压根用不上。比如你在where条件里对索引列做了函数运算、隐式类型转换、前导模糊查询好一点的引擎优化器还能帮你转换一下烂一点的就直接全表扫了。我整理一个索引失效自查清单这东西我觉得比任何理论都值钱违反最左前缀法则。假设你建了一个(a, b, c)联合索引你上来就查b字段索引直接废掉。在索引列上进行计算、函数、类型转换。使用不等于! 或 导致全表扫描。like以通配符开头也就是%关键词这种写法。字符串列与数字比较时隐式转换让索引失效。or连接的两个条件中如果只有一个条件走索引整个查询可能退化为全表扫描。每次写完一条复杂SQLEXPLAIN一下看type字段是不是到了const、ref、range级别如果还是ALL那就是在裸奔扫全表正常应该立刻回头重写SQL或者调整索引设计。3. 缓存引入的收益与三大经典难题3.1 为什么数据库很努力你还是要上缓存你可以这么理解数据库就像一个很认真但办事很慢的老会计每笔账都要核对半天准确率百分百但速度起不来。而缓存就像老会计桌上的便签纸记住最近谁查过什么、余额多少下次有人来问直接照本宣科念出来快得飞起。后端业务一旦上了量你会发现绝大部分请求都是读多写少比如商品详情、用户信息、配置数据算上本地缓存和分布式的Redis你可能能挡住95%的重复查询压力。我刚接触Redis的时候觉得这玩意就是set和get两个命令后来才意识到缓存设计的好坏直接决定了你系统的稳定性和资金的损耗。你缓存得当数据库能清清爽爽地只写核心数据缓存失当数据库就是缓存雪崩时替你挡枪的炮灰惨不忍睹。3.2 缓存穿透、击穿与雪崩每个都有话聊缓存穿透是指查一个根本不存在的数据缓存里没有、数据库里也没有每次请求都直接打穿缓存层直捣数据库。解决方案很成熟一是用布隆过滤器在缓存前置就拦截掉大部分非法key二是把空结果也缓存起来设置较短的过期时间比如60秒。我——个人更喜欢第二种因为布隆过滤器有误判率而且维护成本略高业务简单的话没必要为了造火箭去学烧锅炉。缓存击穿是指某个热点key在过期的瞬间有大量请求同时访问这个key。因为缓存里没有数据库一瞬间就被高并发流量锤爆了。业界最经典的做法是互斥锁也就是当缓存失效时只让一个线程去重建缓存其他线程要么等待、要么走降级逻辑。另外一种玩法是逻辑过期也就是缓存里没有设置实际的过期时间而是在value里塞一个过期标记后台异步去刷新。这样能保证任何时刻缓存里都有值但实现复杂度会高一些容易把你绕晕。缓存雪崩就更好理解了大量key集中在同一时间失效或者Redis节点直接宕机导致所有流量瞬间涌入数据库。处理思路无非就是过期时间加随机值、多级缓存本地Caffeine加Redis、以及Redis的高可用方案主从加哨兵、集群。别觉得这些方案是架构师才该考虑的你自己单机写个demo也要有意识地做过期时间抖动不然压测的时候第一个死的就是你。3.3 数据一致性问题不存在完美的银弹这块我必须实实在在给你泼盆冷水Cache Aside Pattern旁路缓存依然是现阶段最主流、最稳妥的方案别听人吹什么分布式事务缓存强一致那都是特定场景下的核弹不是常规武器。旁路缓存的逻辑很简单读的时候先读缓存缓存没有则读数据库然后回填缓存写的时候先更新数据库再删除缓存。为什么是删除缓存而不是更新缓存因为更新缓存是写操作很容易产生并发竞争两个线程同时写数据库可能最后写进缓存的是旧值删掉缓存则强迫下一次读取时重建缓存保证最终一致性。这一步有一个典型的坑更新数据库和删除缓存不是原子操作。如果删除缓存失败缓存里就是脏数据。常规做法是引入重试机制或者订阅数据库的变更日志异步删缓存。我用得比较顺手的套路是把“删除缓存”这个动作扔进MQ里失败了还可以重新消费。说实话这世界上没有一套方案能让缓存和数据库在任意时刻都强一致你要做的是结合业务场景评估容忍度然后选择成本和一致性之间的平衡点。4. 实操篇从零搭建一套“数据库缓存”的高可用骨架4.1 初始环境与基础组件选型我不会推荐你去手写一个数据库中间件那是另一个修炼维度的内容。但把环境搭起来是必须的。我用的是某云服务器2核4G作为主力测试机操作系统是CentOS数据库用MySQL 8.0缓存用Redis 6.x。安装过程不啰嗦但有几个版本上的注意事项得提一下。MySQL 8.0默认字符集是utf8mb4你不用像以前5.7那样建库时反复确认编码了。Redis 6.x之后默认开启了ACL用户的权限控制比以前严谨写脚本时需要注意认证方式别拿老密码命令往上怼。基础依赖就三样JDK、Maven、Git。JDK我用的是1.8版本很多公司老项目还在上面跑不过如果你是新起项目直接上17也行注意Spring Boot的版本匹配就好。技术框架就选择Spring Boot MyBatis-Plus RedisTemplate这套组合在中小型后端项目里就是万金油资料多、上手快、坑也基本都被前人踩平了。4.2 表结构设计与索引调整实践为了演示事务和缓存我设计了一个简化的“秒杀系统”模拟项目Y。核心表是库存表在秒杀场景下存在典型的读多写少、热点集中。建表的核心逻辑给大家看一眼你先注意字段类型和索引选择CREATE TABLE stock ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键ID, goods_id BIGINT NOT NULL COMMENT 商品ID, total_count INT NOT NULL DEFAULT 0 COMMENT 总库存, sold_count INT NOT NULL DEFAULT 0 COMMENT 已售数量, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_goods_id (goods_id), KEY idx_sold_count (sold_count) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4 COMMENT 库存表;这里有几个细节值得嚼一嚼。goods_id我加了唯一索引是因为一个商品只有一条库存记录从业务的唯一性上就该约束住这也是防止脏数据的兜底手段。sold_count加普通索引是为了应对后台的销量排序统计。注意我没有给create_time加索引如果销售的筛选场景更偏向时间范围那你就得再权衡索引的成本和收益别贪多。秒杀场景的更新语句不能用普通的update直接扣库存吗可以但需要加上version字段做乐观锁控制防止超卖。经典的原子更新长这样UPDATE stock SET sold_count sold_count 1, version version 1 WHERE goods_id #{goodsId} AND version #{oldVersion}这样一次原子更新配合数据库行锁基本能保证不会超卖。但更极端的秒杀场景里这种扣减库存的更新可能还是会在高并发下出现性能瓶颈具体解决就得靠后面的缓存冷热分离来缓解数据库压力了。4.3 缓存设计与热点Key的刷新机制我在这套模拟项目Y里商品详情是热点数据肯定要先塞到Redis里。缓存结构我选用了Hash类型比简单的String可读性更强适合存对象属性HSET goods:detail:1001 title 某品牌手机 stock 10000 price 4999普通商品直接设置缓存过期时间比如30分钟加一个随机抖动值5分钟内防止大量商品同时失效。热点key的处理就不一样了。比如活动大促的时候运营预估某几个爆款商品会被疯狂点击我就在后台配置了一份热点key白名单对这些key使用逻辑过期策略不设置Redis的TTL也就是EXPIRE命令不上在写入Hash结构时额外存一个字段expire_time为当前时间戳30分钟查询时判断expire_time是否小于当前时间如果过期了先返回旧数据同时异步触发一个线程去数据库加载最新数据并回填缓存这种设计的好处是缓存永远有值不会出现空窗期极端情况下最多就是返回的数据滞后几秒但对商品详情这种场景来说完全能接受。我后来把这种玩法扩展到了用户会话、配置中心实测下来稳得很。4.4 结合Spring Cache的框架封装直接用RedisTemplate当然灵活但每个方法都手写缓存逻辑重复代码能写到你想吐。我建议项目里直接集成Spring Cache用注解来声明缓存规则把精力留在业务上。依赖只需要一个Spring Boot的starter然后配置一下Redis连接信息。用起来是这样的Cacheable(cacheNames goods:detail, key #goodsId) public GoodsVO getGoodsDetail(Long goodsId) { // 难得写一次数据库查询 return goodsMapper.selectById(goodsId); }这个注解的效果是调用方法前先查缓存如果没有就执行方法体再把结果塞进缓存。下次同一个参数进来直接走缓存了。要注意的是cacheNames和key决定了缓存空间的划分如果你是分布式系统多个服务共享同一个Rediskey一定要带业务前缀避免互相覆盖。更新的时候用CachePut删除的时候用CacheEvict。但我要提醒一句注解缓存的粒度不要太细。如果你把每一行查询接口都加上Cacheable那你后面清理缓存绝对会疯——因为你根本不知道哪些key被业务改动了。我习惯的做法是只在读多写少、一致性要求不高的查询接口上加其他一律手写RedisTemplate做精细控制。5. 压测调优实录与数据指标的二次解读5.1 压测工具选型与脚本避坑后端性能到底行不行不能靠“感觉”得靠数据。业界常用的有Apache Bench、wrk、JMeter。个人做接口压测的话wrk上手快、性能强适合命令行选手如果你想看图形界面和聚合报告JMeter更合适。我这篇使用的实际压测是wrk因为简单直接。压测前有一步必做——先把日志级别调成WARN不然压测时应用疯狂打印INFO日志磁盘IO会被打满测出来的数据全是假象。我的压测命令长这样wrk -t8 -c100 -d30s --latency http://localhost:8080/api/goods/detail/1001解释一下参数8个线程100个并发连接持续压30秒。这个配置对一个2核4G的测试机来说已经属于中等强度压力了。5.2 从压测报告看系统瓶颈我第一次压这个秒杀详情接口的时候QPS大概在3200左右响应时间P99是210ms看着好像还行但打开数据库慢查询日志发现一堆耗时超过2秒的SQL。这说明了什么说明接口的总耗时全耗在数据库上了Redis缓存压根没把流量挡住。后来排查发现Redis里的key根本没存进去因为我压测的是商品ID为1001的数据但缓存预热脚本只做了1000以前的1001直接打得数据库。就这么一个低级失误暴露出“缓存Key管理和预热脚本不统一”的问题这在线上是能引发性能事故的。修复方案特别简单把预热脚本改成动态从配置中心拉取封面商品的ID列表然后手动执行一遍HSET仅此而已。再次压测QPS直接飙到11500响应时间P99降到25ms。这组数据对比足够说明缓存的命中率就是后端性能的第一生命线。5.3 内存分析Redis内存碎片化压测之后我顺手看了一下Redis的内存使用情况发现INFO memory里used_memory只有300MB但used_memory_rss已经快600MB了。这说明Redis的内部存在大量内存碎片尤其是我频繁进行HSET和DEL操作时jemalloc分配器会留下碎片空间。Redis 4.0以后支持自动内存碎片清理但默认是不开的。我调整了一下配置activedefrag yes active-defrag-ignore-bytes 100mb active-defrag-threshold-lower 10 active-defrag-threshold-upper 100意思就是当碎片超过100MB且碎片率超过10%时开始自动清理超过100%时加大清理力度。调整后观察两天内存碎片率从1.8降到了1.1左右效果非常明显。这波操作让我明白数据库调优不只是SQL层的事中间件的运行参数同样在你的一亩三分地范围里。6. 常见故障与排查技巧速查表长期跟“数据库缓存”这套组合拳打交道故障总是不期而至。我把这个学习阶段最常碰到的几个问题整理成了一张速查表方便你以后直接对着排查。这里面的每一项都是我或者身边同学真实踩过的坑不信你看完就知道很多事故其实是低级问题叠加出来的。故障现象可能原因快速排查命令/思路推荐解决动作缓存命中率低数据库压力大Key命名不统一、过期时间设置太短INFO stats看keyspace_hits/hitsmisses统一Key前缀拉长TTL并加抖动接口偶尔超时平均值不高存在慢查询或大Key热点打开MySQL慢查询日志Redis-cli --bigkeys索引优化、大Key拆分、热点数据前置缓存与数据库数据不一致更新缓存失败、删缓存失败对比业务日志与实际缓存引入消息队列异步重试删除Redis连接突然异常连接池耗尽或Redis宕机redis-cli -p 6379 ping扩大连接池、配置健康检查与自动重连数据库死锁多资源更新顺序不一致SHOW ENGINE INNODB STATUS规范事务访问顺序锁定粒度尽量小主从延迟导致刚写入查不到复制线程延迟SHOW SLAVE STATUS看Seconds_Behind_Master强制走主库查询、或者引入缓存标记这张表看着简单但每一行后面都藏着一整晚的排查经历。我希望你别走我老路遇到故障别急着重启服务或者清缓存先看指标、看日志、看监控大部分问题的线索其实一早就暴露出来了只是你没在意。7. 写在最后的个人体会学到这个阶段我对后端的理解已经脱离了“写接口”的视角开始真正摸到“数据”的脾气。数据库的表结构、索引、锁、事务以及缓存的一致性、高可用它们之间不是孤立的知识点而是一张互相牵连的大网。你在一个点上做的决策可能会在另一个点上百倍地还回来。该加缓存的地方加了系统飞起来不该加缓存的地方死磕反而引入了数据不一致的风险。如果有朋友正处于这个学习阶段我建议你多去折腾别只看书只背题。自己用Spring Boot写一套带商品列表、详情、秒杀的系统然后拿wrk压它一晚上把故障报错一条条搜出来解决掉。这个过程比任何付费课程都管用。数据库和缓存这块的水实在太深我能分享的也只是这一段路踩出来的脚印。下一篇学习记录我大概率会接着写分布式会话管理或者消息队列的实战选型到时候咱们再接着聊。最后再分享个小技巧每次上线大版本前把Redis的RDB快照打开把MySQL的binlog日志保留天数检查一遍这些看似不起眼的动作关键时刻能救你一条命。