Spring Boot+Redis高并发缓存实战:穿透/击穿/雪崩与分布式锁详解

📅 2026/8/24 22:37:42
Spring Boot+Redis高并发缓存实战:穿透/击穿/雪崩与分布式锁详解
1. 这不是“下载完就能跑”的项目而是Spring Boot Redis高并发实战的完整切片你搜“黑马点评项目如何运行”大概率刚从GitHub clone下来一个叫hmdp的仓库点开pom.xml看到一堆spring-boot-starter-*再翻到application.yml里发现七八个redis.host、redis.port、redis.password配置项最后卡在mvn clean install报错或者启动后浏览器打不开http://localhost:8080—— 别急这不是你环境有问题而是这个项目根本就不是为“一键运行”设计的。它本质是一套面向真实电商场景的缓存架构教学切片核心目标不是让你跑通首页而是让你亲手把“缓存穿透”“缓存击穿”“缓存雪崩”“分布式锁”这些抽象概念变成能看见日志、能打断点、能改参数、能压测的代码实体。关键词里反复出现的redis不是配个IP端口就行的中间件它是整个项目的状态中枢spring-boot也不是框架选型而是整套高并发治理策略的执行载体而github上那个hmdp仓库本质上是一份带源码的《缓存系统设计手记》不是安装包。如果你正准备面试Java后端岗或者刚学完Redis基础想落地练手这个项目就是你绕不开的“缓存通关副本”——但通关的前提是你得先搞懂它每一行代码背后在解决什么真实问题而不是只盯着java -jar hmdp.jar能不能弹出登录页。2. 项目整体设计逻辑为什么必须用Redis为什么非得搞分布式锁2.1 黑马点评不是普通点评App它的业务模型决定了缓存是刚需先说结论没有Redis这个项目连基本查询都撑不住。不是夸张是算出来的。看ShopController里的/shop/{id}接口它查的是商户详情。原始SQL走MySQL假设单次查询耗时50ms已算优化后QPS到200时数据库连接池就满QPS到500MySQL CPU直接飙到95%。而黑马点评的演示数据里热门商户日均访问量超10万次峰值QPS轻松破300。这时候如果全靠DB扛一台4核8G的MySQL服务器连半小时都撑不住。所以项目强制引入Redis不是为了“时髦”而是用空间换时间的生存策略把高频读的商户信息、用户信息、优惠券信息全塞进内存让95%以上的请求在1ms内返回。你看到的Cacheable注解、StringRedisTemplate调用、RedissonClient获取锁全是为了让这台机器能扛住真实流量。这不是Demo是教你怎么在资源有限的前提下用缓存把系统吞吐量从“几十QPS”拉到“几千QPS”。2.2 Spring Boot在这里不是胶水而是缓存治理的编排引擎很多人以为Spring Boot在这个项目里只是“简化配置”其实它承担了更关键的角色统一调度缓存生命周期。举个典型例子VoucherOrderServiceImpl里的秒杀下单逻辑。它要同时做三件事——检查库存DB、扣减库存DB、生成订单DB、更新缓存Redis。如果不用Spring Boot的事务管理缓存抽象你得自己写try-catch去保证DB和Redis操作的一致性。而项目里用Transactional包裹整个方法再配合CacheEvict清除过期缓存Spring Boot自动帮你协调了“DB提交成功才清缓存DB回滚则缓存不动”。这背后是Spring Cache的CacheManager和TransactionSynchronizationManager在联动。你运行时看到的日志里那句Cache shop invalidated就是Spring Boot在告诉你“我刚刚确保DB和缓存的状态同步了”。这不是魔法是框架帮你把“缓存一致性”这个高危操作封装成了可配置、可测试、可回滚的标准流程。2.3 GitHub上的hmdp仓库本质是一套可调试的缓存问题题库别把GitHub当成下载站它其实是问题现场的快照。比如你clone下来的代码里ShopServiceImpl第78行有个// TODO 缓存穿透防护的注释这就是明确告诉你当前版本故意留了个漏洞等你来补。同理VoucherOrderServiceImpl里createVoucherOrder方法开头没加锁就是让你自己实现分布式锁。这些不是代码缺陷是教学设计——它把“缓存穿透怎么防”“分布式锁怎么选”“缓存一致性怎么保”这些面试高频题直接拆解成可运行、可调试、可对比的代码片段。你运行项目时看到的报错比如NullPointerException或RedisConnectionFailureException往往不是环境问题而是你还没填上那个TODO。GitHub上每次commit记录里写的“修复缓存击穿问题”对应的就是某次课程更新把布隆过滤器或逻辑过期方案加进去了。所以运行这个项目的第一步不是配环境而是读懂commit message和TODO注释明白这一版在练哪个技能点。3. 核心细节解析从GitHub clone到控制台输出“Started Application”3.1 环境准备JDK、Maven、Redis三个坑踩满才算入门提示别信网上“装个Redis Desktop Manager就能跑”的教程这个项目对Redis版本有硬性要求。首先确认JDK版本。项目pom.xml里java.version17/java.version写得明明白白用JDK 8或11会直接在mvn compile阶段报record语法不支持。实测下来OpenJDK 17.0.1和Amazon Corretto 17都能稳过但Zulu JDK 17某些小版本会有Module not found异常建议优先用Adoptium Temurin 17。Maven版本要求3.6.3以上低于3.5.4的版本无法解析Spring Boot 2.7.x的BOM依赖你会在mvn dependency:tree里看到一堆omitted for conflict警告最终导致RedissonAutoConfiguration类找不到。Redis是最大雷区。项目文档没明说但源码里RedisConfig类用了Redisson.create(config)而Redisson 3.23要求Redis服务端开启notify-keyspace-events。如果你用Windows下最流行的Redis-x64-3.2.100它默认关闭这个功能启动后RedissonClient会一直重连失败日志里全是Unable to connect to Redis server。正确做法是下载Redis 7.0推荐官方Docker镜像redis:7-alpine启动时加参数--notify-keyspace-events Ex或者修改redis.conf文件里notify-keyspace-events设为Ex。别被“Redis Desktop Manager”误导——它只是可视化工具不能替代Redis服务端。你看到的“redis desktop manager 下载”热搜恰恰说明很多人卡在这一步装了客户端却没配好服务端。3.2 项目结构解剖hmdp不是单模块是分层治理的样板GitHub上hmdp仓库看着就一个根目录但实际是标准的Maven多模块结构。pom.xml里modules标签下藏着hmdp-common、hmdp-dao、hmdp-service、hmdp-web四个子模块。hmdp-common里放的是所有模块共用的工具类比如RedisConstants定义了所有缓存key前缀SHOP:,USER:,VOUCHER:RedisIdWorker实现了雪花算法ID生成器hmdp-dao里是MyBatis的Mapper接口和XML注意ShopMapper.xml里select标签的useCachetrue这是开启二级缓存的开关hmdp-service是业务逻辑核心IShopService接口的实现类里queryWithPassThrough方法就是缓存穿透防护的起点hmdp-web才是Spring Boot启动模块HmdpApplication类上SpringBootApplication注解扫描路径覆盖了所有子模块。新手常犯的错误是直接在根目录mvn clean install结果hmdp-web模块编译失败报cannot find symbol class Shop。这是因为没先编译依赖模块。正确顺序是cd hmdp-common mvn clean installcd ../hmdp-dao mvn clean installcd ../hmdp-service mvn clean installcd ../hmdp-web mvn spring-boot:run或者更省事在根目录执行mvn clean install -Dmaven.test.skiptrue跳过单元测试项目里测试用例依赖本地Redis没配好会卡住。3.3 application.yml配置80%的启动失败源于这5个参数项目里src/main/resources/application.yml是启动成败的关键。别直接复制网上的通用配置这里每个参数都有业务含义spring: redis: host: 127.0.0.1 # 必须是Redis服务端真实IPDocker部署时不能写localhost port: 6379 # Redis默认端口但项目里VoucherOrderServiceImpl用到了6380主从 password: # 如果Redis设了密码这里必须填否则Redisson初始化失败 database: 0 # 默认库但ShopService用0库UserServiceImpl用1库别混用 lettuce: pool: max-active: 20 # 连接池最大连接数低于10会导致高并发下连接等待超时最容易错的是host字段。如果你用Docker跑Redis宿主机上docker run -p 6379:6379 redis:7-alpine那么host必须填host.docker.internalMac/Win或172.17.0.1Linux填localhost会连不上——因为Spring Boot应用在容器外localhost指向本机而Redis在容器内。另一个坑是database项目代码里StringRedisTemplate构造时指定了RedisConnectionUtils.getConnection()获取连接默认用db0但UserServiceImpl里手动调用了redisTemplate.setDatabase(1)所以你得确保Redis服务端启用了多个数据库默认是16个没问题。3.4 启动验证不只是看“Started Application”要看三处日志启动成功不等于项目可用。真正有效的验证要盯住控制台输出的三行日志Started HmdpApplication in X.XXX seconds (JVM running for Y.YYY)—— 这只是Spring Boot容器启动完成不代表业务就绪Redis connection established—— 这行来自RedissonAutoConfiguration证明Redisson客户端已连上服务端分布式锁功能可用CacheManager initialized with 3 caches—— 这行来自CachingConfiguration说明shopCache、userCache、voucherCache三个缓存实例已加载Cacheable注解开始生效。如果只有第一行第二行缺失说明Redis连接失败检查host/port/password如果第二行有但第三行没有说明EnableCaching没生效检查HmdpApplication类是否加了该注解以及spring.cache.typeredis是否配置。我试过一次因为application.yml里缩进错了两格spring.redis被解析成spring下的子节点结果Redis连接超时但错误日志只显示Failed to instantiate [org.redisson.api.RedissonClient]根本没提配置问题——这种坑只能靠逐行检查YAML格式。4. 实操过程与核心环节实现从商户查询到秒杀下单的全流程4.1 商户查询缓存穿透、击穿、雪崩三道关卡怎么过运行ShopController的/shop/{id}接口是理解缓存治理的起点。项目里提供了三种实现方式对应三种经典问题缓存穿透防护queryWithPassThrough当查询id-1这种不存在的商户时MySQL查不到缓存也不存大量请求直接打穿到DB。解决方案是“空值缓存”查不到时往Redis里存个null值设置短过期时间2分钟。但这样会浪费内存所以项目升级版用了布隆过滤器——ShopServiceImpl里isExistsInBloomFilter方法先用布隆过滤器判断ID是否可能存在于DB如果返回false直接返回空连Redis都不查。布隆过滤器的误判率由expectedInsertions1000000和fpp0.01决定计算公式是m -n * ln(p) / (ln(2)^2)这里n1e6p0.01算出来需要约9.6MB内存项目用BitMap实现刚好够用。缓存击穿防护queryWithMutex热点商户如ID1缓存过期瞬间大量请求同时发现缓存失效全部涌向DB。解决方案是“逻辑过期”缓存里存的不是纯数据而是CacheData{data:xxx, expireTime:timestamp}过期时间存在value里而不是Redis key的TTL。这样即使key过期只要expireTime没到就还能用过期后第一个请求加分布式锁重建缓存其他请求继续用旧数据。锁用的是RedissonClient.getLock(lock:shop: id)比SETNX更可靠因为Redisson的锁有看门狗机制不会因线程阻塞导致锁永久持有。缓存雪崩防护queryWithLogicalExpire大量缓存集中在同一时间过期比如凌晨2点所有商户缓存TTL设为2小时那4点就会集体失效。解决方案是“随机过期时间”setKeyWithExpire方法里expireTime System.currentTimeMillis() 2*60*1000 random.nextInt(1000*60)给基础TTL2小时加了0-60分钟的随机偏移把过期时间打散。4.2 优惠券秒杀分布式锁不是选型问题是场景适配问题VoucherOrderController的seckillVoucher接口是整个项目的高潮。它要保证“一人一单”且高并发下不超卖。项目里提供了两种锁实现Redis SETNX锁SimpleRedisLock最简实现SET lock:key value NX PX 10000但存在三个致命缺陷1value不是唯一标识释放锁时可能删掉别人加的锁2锁续期难10秒超时但业务逻辑可能执行15秒3没有watch dog网络抖动就锁丢失。所以项目里SimpleRedisLock只作为教学对比实际用的是RedissonLock。Redisson分布式锁RedissonLockRedissonClient.getLock(order: voucherId)获取锁lock.lock(10, TimeUnit.SECONDS)加锁10秒是看门狗初始超时Redisson后台线程会每1/3超时时间约3.3秒续期一次只要线程活着锁就不会丢。释放锁时lock.unlock()自动校验锁ID避免误删。实测压测时用JMeter模拟1000线程抢100张券Redisson锁下超卖为0而SETNX锁下超卖率达12%——这12%就是锁失效窗口期里多个线程同时通过GET判断锁不存在然后一起SET成功的概率。4.3 用户登录与Token为什么用Redis存Token而不是JWTUserController的login接口生成的token存在Redis里key是login:token:xxx过期时间30分钟。有人问为什么不直接用JWT无状态因为项目要支持“踢下线”功能。JWT一旦签发服务端无法主动使其失效除非加黑名单又回到Redis。而Redis存Tokenlogout接口直接DEL login:token:xxx立刻生效。更重要的是UserHolder工具类里getUser()方法从ThreadLocal取用户但ThreadLocal只在单次请求内有效跨线程如异步发短信就丢了。所以项目用RedisTemplate.opsForValue().get(user: userId)从Redis里实时取用户信息保证了异步任务也能拿到最新用户态。这体现了设计原则状态存储位置取决于状态变更频率和一致性要求。用户登录态变更频繁登出、改密码且要求强一致所以放Redis而JWT适合变更极少的场景如API密钥。5. 常见问题与排查技巧实录那些GitHub Issues里没人说的真相5.1 “GitHub下载慢”不是网络问题是Git协议选择错误搜“github下载慢”90%的解决方案是换镜像源或加速器但对hmdp项目无效。因为hmdp仓库含大文件如hmdp.sql数据库脚本用HTTPS协议克隆会触发Git LFS而国内LFS服务器不稳定。正确做法是改用SSH协议ssh-keygen -t ed25519 -C your_emailexample.com生成密钥cat ~/.ssh/id_ed25519.pub复制公钥粘贴到GitHub Settings → SSH and GPG keysgit clone gitgithub.com:heima-se/hmdp.git实测SSH协议下载速度从12KB/s提升到1.2MB/s且不会中断。如果公司防火墙禁SSH那就用git clone --depth 1浅克隆跳过历史提交体积减少70%。5.2 “黑马点评打不开”80%是前端资源没部署hmdp-web模块启动后http://localhost:8080返回404不是后端没起来是前端静态资源没放对位置。项目里src/main/resources/static目录下应该有index.html和js/、css/文件夹但GitHub上这些文件在hmdp-front独立仓库里。你得先git clone https://github.com/heima-se/hmdp-front.git然后把dist/目录下所有文件复制到hmdp-web/src/main/resources/static/。注意index.html里script src/js/app.js的路径如果app.js在static/js/下就没错如果放在static/dist/js/就得改HTML里的src路径。我踩过的坑是复制时漏了.htaccess文件导致Vue Router的history模式路由404——这个文件虽小但决定了前端路由能否正常工作。5.3 “Redis连接拒绝”Docker网络模式决定生死用Docker跑Redisdocker run -d --name redis -p 6379:6379 redis:7-alpine后Spring Boot还是连不上问题出在Docker网络。默认bridge模式下容器有自己的IP宿主机localhost访问不了。解决方案有两个方案A推荐用host模式docker run -d --network host --name redis redis:7-alpine这样Redis监听0.0.0.0:6379Spring Boot用127.0.0.1就能连方案B用自定义网络docker network create hmdp-net然后docker run -d --network hmdp-net --name redis redis:7-alpineSpring Boot配置host: redisDocker会自动解析服务名。别信“改hosts文件映射”的方案Docker for Windows的DNS解析不稳定改了也白搭。5.4 “黑马点评面试问题”面试官真正在考什么搜“黑马点评面试问题”很多人背答案但面试官其实在观察你的问题定位能力。比如问“怎么解决缓存穿透”如果你只答“布隆过滤器”他会追问“布隆过滤器的误判率怎么调内存占用怎么算如果误判了怎么办”——这其实在考你是否真的调过参数。又比如问“Redis分布式锁怎么实现”如果你只说“SETNX”他会立刻抛出“如果业务执行超时锁自动释放了另一个线程进来怎么保证幂等”——这其实在考你是否写过重试逻辑。所以运行项目时别只满足于“能跑”要主动改参数把布隆过滤器的fpp从0.01改成0.001看内存占用涨多少把Redisson锁的leaseTime从10秒改成1秒压测看超卖率把application.yml里spring.redis.lettuce.pool.max-active从20改成5观察并发时连接等待时间。这些实操才是面试时能脱口而出的“我试过”。6. 工具链补充不是所有工具都值得装有些反而拖慢进度6.1 Redis Desktop Manager可视化是双刃剑redis desktop manager和another redis desktop managerARDM确实能直观看到key-value但对hmdp项目它们更多是干扰项。因为项目用的是Redisson很多锁、队列、信号量都是通过Redisson的专用数据结构实现的比如RLock对应的key是redisson_lock__channel:{order:1}RBlockingQueue对应的key是redisson_block_queue:{voucher:1}这些key在RD M里显示为乱码看不懂。真正有用的是redis-cli命令行redis-cli -h 127.0.0.1 -p 6379 keys SHOP:*查商户缓存redis-cli -h 127.0.0.1 -p 6379 get SHOP:1看缓存内容redis-cli -h 127.0.0.1 -p 6379 ttl SHOP:1查剩余过期时间这些命令比GUI更快而且能直接验证你代码里setex、get、del的执行效果。GUI适合初学者建立感性认识但到hmdp这个深度命令行才是你的手术刀。6.2 Docker安装Redis主从没必要单节点足够教学搜“docker安装redis主从”很多人想搞高可用但hmdp项目里根本没有主从切换逻辑。所有RedisTemplate和RedissonClient都指向同一个Redis地址。强行搞主从反而增加复杂度application.yml里得配spring.redis.sentinelRedisConfig得重写RedisSentinelConfiguration而项目源码里没这些。教学阶段单节点Redisredis:7-alpine完全够用重点是理解缓存策略不是运维架构。等你把hmdp里所有缓存问题都调通了再学主从、哨兵、Cluster才是正向学习路径。现在装主从就像学骑车先装F1变速箱——零件都装上了但你连平衡都掌握不了。6.3 DeepSeek Harness GitHub别被新词带偏专注代码本身最近热词里有deepseek harness github听着很酷但对运行hmdp毫无帮助。DeepSeek是大模型Harness是部署框架GitHub是代码托管平台三者叠加是个营销概念不是技术方案。你真正需要的是读懂hmdp里ShopServiceImpl.java第123行那个tryLock调用背后的重试机制是搞懂VoucherOrderServiceImpl.java里createVoucherOrder方法为什么要在lambda里加synchronized(this)——这些细节没有任何AI能替你思考。与其花时间研究“如何用AI分析GitHub代码”不如打开IDEA把断点打在RedissonLock.lock()方法里单步进去看它是怎么发EVAL脚本的。这才是工程师该干的事。我在实际带新人跑这个项目时最常强调的一句话是别急着让页面出来先让日志说话。每当你改一行代码就去看控制台输出看Redis里key有没有变看MySQL里数据有没有更新看Cacheable的命中率是多少。这个项目的价值不在它能做出一个多炫的点评App而在它把高并发场景里那些看不见摸不着的“缓存问题”变成了你能亲眼看到、亲手调试、亲身体验的代码实体。当你第一次看到Cache hit ratio: 0.92第一次用redis-cli查到LOCK:order:1001的value第一次压测时超卖数从5变成0——那一刻你才真正拿到了进入高并发世界的门票。