怎么做网站企业介绍兰州启点网站建设
2026/4/7 13:47:47 网站建设 项目流程
怎么做网站企业介绍,兰州启点网站建设,推广网站加盟,视频高清线hdmi双重判定锁详解#xff1a;缓存击穿的终极解决方案 前言 这篇是微服务全家桶系列的学习笔记#xff0c;这次整理的是分布式场景下的双重判定锁#xff08;Double-Checked Locking#xff0c;简称 DCL#xff09;。 最近在做短链接跳转这块业务#xff0c;遇到了一个挺…双重判定锁详解缓存击穿的终极解决方案前言这篇是微服务全家桶系列的学习笔记这次整理的是分布式场景下的双重判定锁Double-Checked Locking简称DCL。最近在做短链接跳转这块业务遇到了一个挺有意思的问题缓存里没数据的时候一堆请求同时涌进来全都去查数据库数据库直接被打趴了。你想想一个热点短链接每秒几万次访问缓存一过期这几万个请求全部打到MySQL这谁顶得住后来引入了分布式锁但又发现一个问题锁是加上了可第一个请求把数据写回缓存后后面排队的请求拿到锁还是傻乎乎地去查数据库。这不是多此一举吗这就是双重判定锁要解决的问题——锁外检查一次锁内再检查一次既保证了并发安全又避免了无谓的数据库查询。个人主页山沐与山文章目录一、缓存的三大经典问题二、什么是双重判定锁三、实战代码解析四、完整流程图解五、最佳实践与踩坑记录六、常见问题七、总结一、缓存的三大经典问题在聊双重判定锁之前得先搞清楚为什么需要它。缓存在分布式系统里基本是标配了但用不好就会出问题。1.1 缓存穿透什么情况用户老是查一个根本不存在的数据每次都打到数据库。比如有人恶意请求id-1的数据数据库里根本没有缓存自然也存不了每次请求都穿透到数据库。怎么解决布隆过滤器先判断数据可能不可能存在空值缓存查不到也缓存个占位符下次直接返回1.2 缓存击穿什么情况热点Key突然过期大量请求同时打到数据库。这是本文的重点。假设有个爆款短链接每秒10万次访问缓存过期的那一瞬间10万个请求全部去查数据库。这不是击穿是什么怎么解决分布式锁只让一个请求去查库其他人等着双重判定锁拿到锁后再检查一次避免重复查库1.3 缓存雪崩什么情况大量Key同时过期数据库压力骤增。怎么解决随机过期时间别让大家同时过期永不过期策略后台异步更新1.4 三者对比问题触发条件危害解决方案缓存穿透查询不存在的数据数据库被无效请求打满布隆过滤器空值缓存缓存击穿热点Key过期瞬时高并发打到数据库分布式锁双重判定缓存雪崩大量Key同时过期数据库持续高压随机过期时间二、什么是双重判定锁2.1 核心思想双重判定锁的核心就三步第一次检查锁外先看缓存有没有有就直接返回不用加锁获取锁缓存没有才去抢锁第二次检查锁内拿到锁后再看一眼缓存因为等锁的时候别人可能已经把数据放进去了为什么要检查两次举个例子就明白了。2.2 一个生动的例子假设食堂打饭窗口只有一个阿姨数据库学生们排队请求。没有双重判定学生A看到菜没了 → 叫阿姨去厨房拿 学生B看到菜没了 → 也叫阿姨去厨房拿 学生C看到菜没了 → 也叫阿姨去厨房拿 ... (阿姨被叫烦了)阿姨跑了一趟拿回来菜结果后面几个学生还在叫她去拿因为他们不知道已经有人拿回来了。有双重判定学生A看到菜没了 → 举手说我去找阿姨 学生B看到菜没了 → 发现有人举手了等着 学生C看到菜没了 → 发现有人举手了等着 学生A叫完阿姨菜回来了 学生B看了一眼哦有菜了直接打 学生C看了一眼哦有菜了直接打关键点学生B和C等到可以行动的时候先看一眼有没有菜而不是直接去叫阿姨。这就是双重判定——拿到行动权后再确认一次。2.3 伪代码表示publicStringgetData(Stringkey){// [第一次检查] 锁外检查缓存Stringvaluecache.get(key);if(value!null){returnvalue;// 缓存命中直接返回}// 缓存未命中获取分布式锁RLocklockredissonClient.getLock(lock:key);lock.lock();try{// [第二次检查] 锁内再检查一次valuecache.get(key);if(value!null){returnvalue;// 其他线程已经加载了直接用}// 确实没有去查数据库valuedb.query(key);cache.set(key,value);returnvalue;}finally{lock.unlock();}}看到没lock.lock()之后的第一件事不是查数据库而是再检查一次缓存。因为在你等锁的这段时间里拿到锁的那个线程可能已经把数据放到缓存里了。三、实战代码解析来看一段真实项目中的代码这是短链接跳转服务的核心逻辑。3.1 Redis Key 设计publicclassRedisKeyConstant{// 短链接跳转缓存fullShortUrl - originUrlpublicstaticfinalStringGOTO_SHORT_LINK_KEYshort-link:goto:%s;// 空值缓存标记不存在的短链接publicstaticfinalStringGOTO_IS_NULL_SHORT_LINK_KEYshort-link:is-null:goto_%s;// 分布式锁防止缓存击穿publicstaticfinalStringLOCK_GOTO_SHORT_LINK_KEYshort-link:lock:goto:%s;}这里设计了三个KeyGOTO_SHORT_LINK_KEY正常的跳转缓存GOTO_IS_NULL_SHORT_LINK_KEY空值缓存防止缓存穿透LOCK_GOTO_SHORT_LINK_KEY分布式锁的Key3.2 核心跳转逻辑SneakyThrowsOverridepublicvoidrestoreUrl(StringshortUri,ServletRequestrequest,ServletResponseresponse){// 构建完整短链接StringserverNamerequest.getServerName();StringserverPortOptional.of(request.getServerPort()).filter(each-!Objects.equals(each,80)).map(String::valueOf).map(each-:each).orElse();StringfullShortUrlserverNameserverPort/shortUri;// 第一次判断锁外// [检查点1] 查缓存StringoriginLinkstringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl));if(StrUtil.isNotBlank(originLink)){// 缓存命中记录统计后直接跳转shortLinkStats(fullShortUrl,null,buildStatsRecord(fullShortUrl,request,response));((HttpServletResponse)response).sendRedirect(originLink);return;}// [检查点2] 布隆过滤器判断booleancontainsshortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);if(!contains){// 布隆过滤器说不存在那就一定不存在((HttpServletResponse)response).sendRedirect(/page/notfound);return;}// [检查点3] 检查空值缓存StringgotoIsNullShortLinkstringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY,fullShortUrl));if(StrUtil.isNotBlank(gotoIsNullShortLink)){// 已确认不存在的短链接((HttpServletResponse)response).sendRedirect(/page/notfound);return;}// 获取分布式锁 RLocklockredissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY,fullShortUrl));lock.lock();try{// 第二次判断锁内// [双重检查1] 再查一次缓存originLinkstringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl));if(StrUtil.isNotBlank(originLink)){// 其他线程已加载缓存直接使用shortLinkStats(fullShortUrl,null,buildStatsRecord(fullShortUrl,request,response));((HttpServletResponse)response).sendRedirect(originLink);return;}// [双重检查2] 再查一次空值缓存gotoIsNullShortLinkstringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY,fullShortUrl));if(StrUtil.isNotBlank(gotoIsNullShortLink)){((HttpServletResponse)response).sendRedirect(/page/notfound);return;}// 查询数据库 // 先查路由表拿 gid因为主表是按 gid 分表的LambdaQueryWrapperShortLinkGotoDOlinkGotoQueryWrapperWrappers.lambdaQuery(ShortLinkGotoDO.class).eq(ShortLinkGotoDO::getFullShortUrl,fullShortUrl);ShortLinkGotoDOshortLinkGotoDOshortLinkGotoMapper.selectOne(linkGotoQueryWrapper);if(shortLinkGotoDOnull){// 路由表没有设置空值缓存stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY,fullShortUrl),-,30,TimeUnit.MINUTES);((HttpServletResponse)response).sendRedirect(/page/notfound);return;}// 查短链接详情LambdaQueryWrapperShortLinkDOqueryWrapperWrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getGid,shortLinkGotoDO.getGid()).eq(ShortLinkDO::getFullShortUrl,fullShortUrl).eq(ShortLinkDO::getEnableStatus,0).eq(ShortLinkDO::getDelFlag,0);ShortLinkDOshortLinkDObaseMapper.selectOne(queryWrapper);// 检查是否存在或过期if(shortLinkDOnull||(shortLinkDO.getValidDate()!nullshortLinkDO.getValidDate().before(newDate()))){stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY,fullShortUrl),-,30,TimeUnit.MINUTES);((HttpServletResponse)response).sendRedirect(/page/notfound);return;}// 写入缓存并跳转 stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl),shortLinkDO.getOriginUrl(),LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()),TimeUnit.MILLISECONDS);shortLinkStats(fullShortUrl,shortLinkDO.getGid(),buildStatsRecord(fullShortUrl,request,response));((HttpServletResponse)response).sendRedirect(shortLinkDO.getOriginUrl());}finally{lock.unlock();}}3.3 代码分层解读这段代码分成四层层层递进层级位置检查内容作用第一层锁外缓存命中直接返回不加锁第二层锁外布隆过滤器快速拒绝不存在的请求第三层锁外空值缓存拦截已确认不存在的短链接第四层锁内双重判定避免等锁期间的重复查库为什么要这么多层因为越早返回越好。能在锁外解决的事情就不要进锁能在缓存解决的事情就不要查数据库。四、完整流程图解4.1 请求处理流程用户请求 │ ▼ ┌───────────────────────┐ │ 构建 fullShortUrl │ └───────────────────────┘ │ ┌─────────────────────────┴─────────────────────────┐ │ 无锁区域 │ │ ┌─────────────┐ 命中 ┌──────────────┐ │ │ │ 查缓存 │────────────▶│ 直接跳转 │ │ │ └─────────────┘ └──────────────┘ │ │ │ 未命中 │ │ ▼ │ │ ┌─────────────┐ 不存在 ┌──────────────┐ │ │ │ 布隆过滤器 │────────────▶│ 返回 404 │ │ │ └─────────────┘ └──────────────┘ │ │ │ 可能存在 │ │ ▼ │ │ ┌─────────────┐ 存在 ┌──────────────┐ │ │ │ 空值缓存 │────────────▶│ 返回 404 │ │ │ └─────────────┘ └──────────────┘ │ │ │ 不存在 │ └────────┴──────────────────────────────────────────┘ │ ▼ ┌───────────────────────┐ │ 获取分布式锁 │ │ lock.lock() │ └───────────────────────┘ │ ┌────────┴──────────────────────────────────────────┐ │ 有锁区域 │ │ ┌─────────────┐ 命中 ┌──────────────┐ │ │ │ 再查缓存 │────────────▶│ 直接跳转 │ │ │ │ (双重判定) │ │ (别人加载的) │ │ │ └─────────────┘ └──────────────┘ │ │ │ 仍未命中 │ │ ▼ │ │ ┌─────────────┐ 存在 ┌──────────────┐ │ │ │ 再查空值 │────────────▶│ 返回 404 │ │ │ │ (双重判定) │ └──────────────┘ │ │ └─────────────┘ │ │ │ 仍不存在 │ │ ▼ │ │ ┌─────────────────────────────────────────┐ │ │ │ 查询数据库 │ │ │ │ 路由表 → 短链接表 → 写入缓存 → 跳转 │ │ │ └─────────────────────────────────────────┘ │ └───────────────────────────────────────────────────┘ │ ▼ ┌───────────────────────┐ │ 释放锁 │ │ lock.unlock() │ └───────────────────────┘4.2 并发场景时序图假设三个请求几乎同时到来缓存为空时间轴 ──────────────────────────────────────────────────────▶ 请求A ─────┬────────────────────────────────────────────────── │ 查缓存 → 未命中 │ 查布隆 → 可能存在 │ 查空值 → 不存在 │ 获取锁 ✓ │ 双重判定 → 仍未命中 │ 查数据库... │ 写入缓存 ◀──────────────── 这时候缓存有值了 │ 释放锁 └──▶ 跳转成功 请求B ───────────┬──────────────────────────────────────────── │ 查缓存 → 未命中 │ 查布隆 → 可能存在 │ 查空值 → 不存在 │ 等待锁... ⏳ │ │ │ ▼ (A释放锁后) │ 获取锁 ✓ │ 双重判定 → 命中(A已写入) │ 释放锁 └──▶ 直接跳转没查库 请求C ───────────────────────────────────────────────────┬──── │ 查缓存 → 命中 └──▶ 直接跳转没加锁看到效果了吧请求A第一个到老老实实查库请求B等到锁后发现缓存已有值直接用不查库请求C来得晚连锁都不用加缓存里直接拿五、最佳实践与踩坑记录5.1 锁粒度要细// ✅ 正确每个短链接一把锁RLocklockredissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY,fullShortUrl));// ❌ 错误全局一把锁RLocklockredissonClient.getLock(short-link:global-lock);全局锁会导致所有请求串行化性能急剧下降。正确的做法是按资源粒度加锁每个短链接有自己的锁互不影响。5.2 先检查正常缓存再检查空值缓存有人可能会问为什么拿到锁后先查正常缓存而不是先查空值缓存lock.lock();try{// 先查正常缓存originLinkcache.get(GOTO_KEY);if(StrUtil.isNotBlank(originLink)){returnoriginLink;}// 再查空值缓存gotoIsNullcache.get(IS_NULL_KEY);if(StrUtil.isNotBlank(gotoIsNull)){return404;}// ...}原因是我们假设大部分请求都是正常的。如果把空值缓存检查放前面意味着假设系统经常被攻击。而实际情况是正常请求远多于恶意请求所以优先检查正常缓存能减少一次无谓的Redis查询。5.3 空值缓存要设过期时间// 设置 30 分钟过期stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY,fullShortUrl),-,30,TimeUnit.MINUTES);为什么假设短链接被误删后又恢复了如果空值缓存永不过期用户就永远访问不了。30分钟是个平衡点——既能防止短期内的穿透攻击又不会影响数据恢复后的正常访问。5.4 用 lock() 而不是 tryLock()// 当前实现阻塞等待lock.lock();// 为什么不用这个// if (!lock.tryLock()) {// throw new ServiceException(系统繁忙请稍后再试);// }因为短链接跳转是用户的核心操作不应该因为锁竞争就直接失败。用lock()让请求排队最终都能得到正确结果。用tryLock()虽然快但用户体验差——凭什么我点一下就失败了5.5 缓存更新时的清理策略当数据变更时记得清理相关缓存// 移入回收站删除跳转缓存publicvoidsaveRecycleBin(RecycleBinSaveReqDTOrequestParam){// ... 更新数据库stringRedisTemplate.delete(String.format(GOTO_SHORT_LINK_KEY,requestParam.getFullShortUrl()));}// 从回收站恢复删除空值缓存publicvoidrecoverRecycleBin(RecycleBinRecoverReqDTOrequestParam){// ... 更新数据库stringRedisTemplate.delete(String.format(GOTO_IS_NULL_SHORT_LINK_KEY,requestParam.getFullShortUrl()));}这点容易被忽略。短链接禁用时要删跳转缓存恢复时要删空值缓存否则会出现缓存和数据库不一致的问题。六、常见问题6.1 布隆过滤器说存在就一定存在吗不是。布隆过滤器的特性是说不存在→ 一定不存在可信说存在→ 可能存在有误判率所以即使布隆过滤器判断存在也还需要后续的检查。项目里配置的误判率是0.001千分之一基本上影响不大。// 预估 1000 万条数据误判率 0.001cachePenetrationBloomFilter.tryInit(10000000,0.001);6.2 为什么不用读写锁其实项目里在另一个场景用了读写锁——修改短链接分组gid的时候// 修改 gid 时加写锁RReadWriteLockreadWriteLockredissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY,fullShortUrl));RLockwLockreadWriteLock.writeLock();wLock.lock();// 统计访问时加读锁RLockrLockreadWriteLock.readLock();rLock.lock();但在跳转这个场景不适合用读写锁。因为跳转时大部分时间是读缓存不需要加锁只有缓存未命中时才需要写缓存这时候用普通锁就够了。6.3 双重判定锁是不是万能的不是。它主要解决缓存击穿问题对于缓存雪崩大量Key同时过期效果有限。雪崩问题需要其他手段问题解决方案缓存击穿分布式锁 双重判定 ✓缓存雪崩随机过期时间、热点数据永不过期缓存穿透布隆过滤器 空值缓存6.4 锁的粒度多细合适一般按业务Key来加锁。比如短链接跳转场景就按fullShortUrl加锁// 锁的粒度 单个短链接StringlockKeyString.format(LOCK_GOTO_SHORT_LINK_KEY,fullShortUrl);粒度太粗全局锁会导致串行化粒度太细比如按用户IP没有意义。原则是不同的业务资源之间不应该互相阻塞。七、总结本文介绍了分布式场景下双重判定锁的设计与实现重点包括缓存三大问题穿透、击穿、雪崩的区别与解决方案双重判定锁原理锁外检查一次锁内再检查一次实战代码短链接跳转服务的完整实现最佳实践锁粒度、检查顺序、缓存过期时间核心要点总结设计点推荐做法原因锁粒度按业务Key加锁避免全局串行化检查顺序先正常缓存后空值缓存假设大部分请求是正常的空值缓存过期30分钟平衡防护效果和数据恢复锁类型lock()阻塞等待保证最终一致性双重判定锁本质上是一种减少锁竞争的优化模式。第一次检查让大部分请求快速返回第二次检查避免重复查库。理解了这个核心思想在其他场景也能灵活运用。热门专栏推荐Agent小册服务器部署Java基础合集Python基础合集Go基础合集大数据合集前端小册数据库合集Redis 合集Spring 全家桶微服务全家桶数据结构与算法合集设计模式小册消息队列合集等等等还有许多优秀的合集在主页等着大家的光顾感谢大家的支持文章到这里就结束了如果有什么疑问的地方请指出诸佬们一起来评论区一起讨论希望能和诸佬们一起努力今后我们一起观看感谢您的阅读如果帮助到您不妨3连支持一下创造不易您们的支持是我的动力

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询