【Redis】双重判定锁详解:缓存击穿的终极解决方案

双重判定锁详解:缓存击穿的终极解决方案

前言

这篇是微服务全家桶系列的学习笔记,这次整理的是分布式场景下的双重判定锁(Double-Checked Locking,简称DCL)。

最近在做短链接跳转这块业务,遇到了一个挺有意思的问题:缓存里没数据的时候,一堆请求同时涌进来,全都去查数据库,数据库直接被打趴了。你想想,一个热点短链接每秒几万次访问,缓存一过期,这几万个请求全部打到MySQL,这谁顶得住?

后来引入了分布式锁,但又发现一个问题:锁是加上了,可第一个请求把数据写回缓存后,后面排队的请求拿到锁还是傻乎乎地去查数据库。这不是多此一举吗?

这就是双重判定锁要解决的问题——锁外检查一次,锁内再检查一次,既保证了并发安全,又避免了无谓的数据库查询。

🏠个人主页:山沐与山


文章目录

  • 一、缓存的三大经典问题
  • 二、什么是双重判定锁
  • 三、实战代码解析
  • 四、完整流程图解
  • 五、最佳实践与踩坑记录
  • 六、常见问题
  • 七、总结

一、缓存的三大经典问题

在聊双重判定锁之前,得先搞清楚为什么需要它。缓存在分布式系统里基本是标配了,但用不好就会出问题。

1.1 缓存穿透

什么情况?用户老是查一个根本不存在的数据,每次都打到数据库。

比如有人恶意请求id=-1的数据,数据库里根本没有,缓存自然也存不了,每次请求都穿透到数据库。

怎么解决?

  • 布隆过滤器:先判断数据可能不可能存在
  • 空值缓存:查不到也缓存个占位符,下次直接返回

1.2 缓存击穿

什么情况?热点Key突然过期,大量请求同时打到数据库。

这是本文的重点。假设有个爆款短链接,每秒10万次访问,缓存过期的那一瞬间,10万个请求全部去查数据库。这不是击穿是什么?

怎么解决?

  • 分布式锁:只让一个请求去查库,其他人等着
  • 双重判定锁:拿到锁后再检查一次,避免重复查库

1.3 缓存雪崩

什么情况?大量Key同时过期,数据库压力骤增。

怎么解决?

  • 随机过期时间:别让大家同时过期
  • 永不过期策略:后台异步更新

1.4 三者对比

问题触发条件危害解决方案
缓存穿透查询不存在的数据数据库被无效请求打满布隆过滤器+空值缓存
缓存击穿热点Key过期瞬时高并发打到数据库分布式锁+双重判定
缓存雪崩大量Key同时过期数据库持续高压随机过期时间

二、什么是双重判定锁

2.1 核心思想

双重判定锁的核心就三步:

  1. 第一次检查(锁外):先看缓存有没有,有就直接返回,不用加锁
  2. 获取锁:缓存没有才去抢锁
  3. 第二次检查(锁内):拿到锁后再看一眼缓存,因为等锁的时候别人可能已经把数据放进去了

为什么要检查两次?举个例子就明白了。

2.2 一个生动的例子

假设食堂打饭,窗口只有一个阿姨(数据库),学生们排队(请求)。

没有双重判定

学生A看到菜没了 → 叫阿姨去厨房拿 学生B看到菜没了 → 也叫阿姨去厨房拿 学生C看到菜没了 → 也叫阿姨去厨房拿 ... (阿姨被叫烦了)

阿姨跑了一趟拿回来菜,结果后面几个学生还在叫她去拿,因为他们不知道已经有人拿回来了。

有双重判定

学生A看到菜没了 → 举手说"我去找阿姨" 学生B看到菜没了 → 发现有人举手了,等着 学生C看到菜没了 → 发现有人举手了,等着 学生A叫完阿姨,菜回来了 学生B看了一眼,哦有菜了,直接打 学生C看了一眼,哦有菜了,直接打

关键点:学生BC等到可以行动的时候,先看一眼有没有菜,而不是直接去叫阿姨。这就是双重判定——拿到行动权后再确认一次

2.3 伪代码表示

publicStringgetData(Stringkey){// [第一次检查] 锁外检查缓存Stringvalue=cache.get(key);if(value!=null){returnvalue;// 缓存命中,直接返回}// 缓存未命中,获取分布式锁RLocklock=redissonClient.getLock("lock:"+key);lock.lock();try{// [第二次检查] 锁内再检查一次!value=cache.get(key);if(value!=null){returnvalue;// 其他线程已经加载了,直接用}// 确实没有,去查数据库value=db.query(key);cache.set(key,value);returnvalue;}finally{lock.unlock();}}

看到没?lock.lock()之后的第一件事不是查数据库,而是再检查一次缓存。因为在你等锁的这段时间里,拿到锁的那个线程可能已经把数据放到缓存里了。


三、实战代码解析

来看一段真实项目中的代码,这是短链接跳转服务的核心逻辑。

3.1 Redis Key 设计

publicclassRedisKeyConstant{// 短链接跳转缓存:fullShortUrl -> originUrlpublicstaticfinalStringGOTO_SHORT_LINK_KEY="short-link:goto:%s";// 空值缓存:标记不存在的短链接publicstaticfinalStringGOTO_IS_NULL_SHORT_LINK_KEY="short-link:is-null:goto_%s";// 分布式锁:防止缓存击穿publicstaticfinalStringLOCK_GOTO_SHORT_LINK_KEY="short-link:lock:goto:%s";}

这里设计了三个Key

  • GOTO_SHORT_LINK_KEY:正常的跳转缓存
  • GOTO_IS_NULL_SHORT_LINK_KEY:空值缓存,防止缓存穿透
  • LOCK_GOTO_SHORT_LINK_KEY:分布式锁的Key

3.2 核心跳转逻辑

@SneakyThrows@OverridepublicvoidrestoreUrl(StringshortUri,ServletRequestrequest,ServletResponseresponse){// 构建完整短链接StringserverName=request.getServerName();StringserverPort=Optional.of(request.getServerPort()).filter(each->!Objects.equals(each,80)).map(String::valueOf).map(each->":"+each).orElse("");StringfullShortUrl=serverName+serverPort+"/"+shortUri;// ==================== 第一次判断(锁外)====================// [检查点1] 查缓存StringoriginLink=stringRedisTemplate.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] 布隆过滤器判断booleancontains=shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);if(!contains){// 布隆过滤器说不存在,那就一定不存在((HttpServletResponse)response).sendRedirect("/page/notfound");return;}// [检查点3] 检查空值缓存StringgotoIsNullShortLink=stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY,fullShortUrl));if(StrUtil.isNotBlank(gotoIsNullShortLink)){// 已确认不存在的短链接((HttpServletResponse)response).sendRedirect("/page/notfound");return;}// ==================== 获取分布式锁 ====================RLocklock=redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY,fullShortUrl));lock.lock();try{// ==================== 第二次判断(锁内)====================// [双重检查1] 再查一次缓存originLink=stringRedisTemplate.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] 再查一次空值缓存gotoIsNullShortLink=stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY,fullShortUrl));if(StrUtil.isNotBlank(gotoIsNullShortLink)){((HttpServletResponse)response).sendRedirect("/page/notfound");return;}// ==================== 查询数据库 ====================// 先查路由表拿 gid(因为主表是按 gid 分表的)LambdaQueryWrapper<ShortLinkGotoDO>linkGotoQueryWrapper=Wrappers.lambdaQuery(ShortLinkGotoDO.class).eq(ShortLinkGotoDO::getFullShortUrl,fullShortUrl);ShortLinkGotoDOshortLinkGotoDO=shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);if(shortLinkGotoDO==null){// 路由表没有,设置空值缓存stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY,fullShortUrl),"-",30,TimeUnit.MINUTES);((HttpServletResponse)response).sendRedirect("/page/notfound");return;}// 查短链接详情LambdaQueryWrapper<ShortLinkDO>queryWrapper=Wrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getGid,shortLinkGotoDO.getGid()).eq(ShortLinkDO::getFullShortUrl,fullShortUrl).eq(ShortLinkDO::getEnableStatus,0).eq(ShortLinkDO::getDelFlag,0);ShortLinkDOshortLinkDO=baseMapper.selectOne(queryWrapper);// 检查是否存在或过期if(shortLinkDO==null||(shortLinkDO.getValidDate()!=null&&shortLinkDO.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 锁粒度要细

// ✅ 正确:每个短链接一把锁RLocklock=redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY,fullShortUrl));// ❌ 错误:全局一把锁RLocklock=redissonClient.getLock("short-link:global-lock");

全局锁会导致所有请求串行化,性能急剧下降。正确的做法是按资源粒度加锁,每个短链接有自己的锁,互不影响。

5.2 先检查正常缓存,再检查空值缓存

有人可能会问:为什么拿到锁后先查正常缓存,而不是先查空值缓存?

lock.lock();try{// 先查正常缓存originLink=cache.get(GOTO_KEY);if(StrUtil.isNotBlank(originLink)){returnoriginLink;}// 再查空值缓存gotoIsNull=cache.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 时加写锁RReadWriteLockreadWriteLock=redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY,fullShortUrl));RLockwLock=readWriteLock.writeLock();wLock.lock();// 统计访问时加读锁RLockrLock=readWriteLock.readLock();rLock.lock();

但在跳转这个场景不适合用读写锁。因为跳转时大部分时间是"读缓存",不需要加锁;只有缓存未命中时才需要"写缓存",这时候用普通锁就够了。

6.3 双重判定锁是不是万能的?

不是。它主要解决缓存击穿问题,对于缓存雪崩(大量Key同时过期)效果有限。雪崩问题需要其他手段:

问题解决方案
缓存击穿分布式锁 + 双重判定 ✓
缓存雪崩随机过期时间、热点数据永不过期
缓存穿透布隆过滤器 + 空值缓存

6.4 锁的粒度多细合适?

一般按业务Key来加锁。比如短链接跳转场景,就按fullShortUrl加锁:

// 锁的粒度 = 单个短链接StringlockKey=String.format(LOCK_GOTO_SHORT_LINK_KEY,fullShortUrl);

粒度太粗(全局锁)会导致串行化,粒度太细(比如按用户IP)没有意义。原则是:不同的业务资源之间不应该互相阻塞


七、总结

本文介绍了分布式场景下双重判定锁的设计与实现,重点包括:

  1. 缓存三大问题:穿透、击穿、雪崩的区别与解决方案
  2. 双重判定锁原理:锁外检查一次,锁内再检查一次
  3. 实战代码:短链接跳转服务的完整实现
  4. 最佳实践:锁粒度、检查顺序、缓存过期时间

核心要点总结

设计点推荐做法原因
锁粒度按业务Key加锁避免全局串行化
检查顺序先正常缓存,后空值缓存假设大部分请求是正常的
空值缓存过期30分钟平衡防护效果和数据恢复
锁类型lock()阻塞等待保证最终一致性

双重判定锁本质上是一种减少锁竞争的优化模式。第一次检查让大部分请求快速返回,第二次检查避免重复查库。理解了这个核心思想,在其他场景也能灵活运用。


热门专栏推荐

  • Agent小册
  • 服务器部署
  • Java基础合集
  • Python基础合集
  • Go基础合集
  • 大数据合集
  • 前端小册
  • 数据库合集
  • Redis 合集
  • Spring 全家桶
  • 微服务全家桶
  • 数据结构与算法合集
  • 设计模式小册
  • 消息队列合集

等等等还有许多优秀的合集在主页等着大家的光顾,感谢大家的支持

文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😊
希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🙏
如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🌟

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1119713.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

垃圾回收算法

垃圾回收的概念垃圾回收&#xff08;Garbage Collection&#xff0c;简称GC&#xff09;,顾名思义就是释放垃圾占用的空间&#xff0c;防止内存爆掉。有效的使用可以使用的内存&#xff0c;对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。垃圾判断算法既然JVM要…

一张图理清网络安全核心框架:体系、模型与标准体系的演进之路

网络安全体系概述 4.1.1 网络安全体系概述 一般面言&#xff0c;网络安全体系是网络安全保障系统的最高层概念抽象&#xff0c;是由各种网络安全单元按照一定的规则组成的&#xff0c;共同实现网络安全的目标。网络安全体系包括法律法规政策文件、安全策略、组织管理、技术措…

网络安全防护实战指南:关键技术演进与现代企业级解决方案

1&#xff1a;网络基础知识 Internet通过TCP/IP协议将遍布在全世界各地的计算机互联&#xff0c;从而形成超大的计算机网络。 2&#xff1a; 3&#xff1a;网络协议层模型 4&#xff1a;通信网络地址的发出点为源地址&#xff0c;接收点为目的地址&#xff1b; 在通信网络中&…

慢思考,深搜索:MiroThinker 1.5 如何重塑 AI 研究智能体范式

前言过去两年&#xff0c;AI 的主流叙事始终围绕“更快、更强、更聪明”展开。大模型竞相堆叠参数&#xff0c;响应速度被压缩到毫秒级&#xff0c;对话流畅度几乎以假乱真。这种进化路径在日常问答、内容生成等场景中确实带来了显著体验提升。但当我们面对需要深度调研、逻辑推…

一文读懂探针卡的概念、组成、分类以及应用

探针卡&#xff08;Probe Card&#xff09;在集成电路测试中起着至关重要的作用&#xff0c;尤其在晶圆测试&#xff08;wafer test&#xff09;环节&#xff0c;探针卡作为连接ATE测试机台和半导体晶圆之间的接口&#xff0c;确保了在芯片封装前对其电学性能进行初步测量和筛选…

从入门到精通:网络安全核心技术栈详解与实践路线图

网络安全技术虽然非常复杂&#xff0c;但是归纳起来&#xff0c;主要就是为了解决以下三方面问题&#xff1a; 1.数据的机密性&#xff1a;即如何令人们发送数据&#xff0c;即使被其他无关人员截取&#xff0c;他们也无法获知数据的含义。 2.数据的有效性&#xff1a;指数据不…

探讨 ‘Memory-augmented Retrieval’:利用历史对话的 Checkpoint 作为查询权重,提升检索的相关性

尊敬的各位同仁&#xff0c;欢迎来到本次关于“Memory-augmented Retrieval”的讲座。今天我们将深入探讨如何利用历史对话的“Checkpoint”作为查询权重&#xff0c;显著提升检索系统的相关性&#xff0c;尤其是在多轮对话和复杂交互场景中。在当今的AI时代&#xff0c;检索增…

论文降aigc保姆级教程:手把手教你免费降ai率,告别高ai焦虑。

最近太多人私信我&#xff1a;“论文AI率太高怎么办&#xff1f;连人工改的都不过检测&#xff01;” 这事儿我太懂了——前段时间我自己也被AI检测折磨得快崩溃。 市面上一堆打着“降低AI率”旗号的网站&#xff0c;不是乱扣格式&#xff0c;就是改完反而更像AI写的。 所以我…

腾讯云VOD AIGC视频生成工具 回调实现

腾讯云VOD AIGC视频生成工具 一个功能完整的腾讯云VOD AIGC视频生成工具库&#xff0c;支持轮询模式和回调模式两种获取结果方式。 目录结构 test/vod/ ├── tencent_aigc_video.py # 核心库&#xff1a;API封装、任务管理 ├── config.py # 配置文件&a…

基于yolov11实现车辆速度估计+距离测量+轨迹跟踪+区域进出统计python源码实现

这个是网上目前可能唯一一个使用不足一百行代码实现了复杂车辆速度估计距离测量轨迹跟踪区域进出统计系统。之所以这么简单是因为ultralytics模块现在已经成熟而且强大&#xff0c;不需要从头开始写车辆速度估计、距离测量、轨迹跟踪、区域进出统计系统代码&#xff0c;因为里面…

PCB阻焊层与助焊层的本质区别

清晰理解PCB的“化妆术”&#xff1a;阻焊层与助焊层的本质区别在Altium Designer&#xff08;AD&#xff09;中设计PCB时&#xff0c;我们经常在层叠管理器里看到 Solder Mask 和 Paste Mask 这两层。它们到底是什么&#xff1f;为什么总是成对出现&#xff1f;简单来说&#…

架构师视角:网络安全体系深度解析——核心模型、数据标准与落地实践

网络安全体系概述 4.1.1 网络安全体系概述 一般面言&#xff0c;网络安全体系是网络安全保障系统的最高层概念抽象&#xff0c;是由各种网络安全单元按照一定的规则组成的&#xff0c;共同实现网络安全的目标。网络安全体系包括法律法规政策文件、安全策略、组织管理、技术措…

本章节我们将讨论如何在 React 中使用表单。

React 表单与事件 本章节我们将讨论如何在 React 中使用表单。HTML 表单元素与 React 中的其他 DOM 元素有所不同,因为表单元素生来就保留一些内部状态。在 HTML 当中&#xff0c;像 <input>, <textarea>, 和 <select> 这类表单元素会维持自身状态&#xff0…

专利解析:涂液器凸轮槽与导向突起的滑动配合机制

在追求健康头皮与浓密秀发的道路上&#xff0c;一款得心应手的头皮护理液涂抹工具至关重要。今天我们要探讨的这款液体化学药剂涂抹器&#xff0c;专为涂抹头皮护理液而设计&#xff0c;旨在促进头皮健康、预防脱发。有了它&#xff0c;用户只需轻轻倾斜&#xff0c;就能将护理…

Escrcpy(安卓手机投屏软件)

Escrcpy 是一款强大的工具&#xff0c;它允许用户通过图形化的 Scrcpy 界面来显示和控制他们的 Android 设备。这款应用程序由 Electron 作为其底层框架驱动。Escrcpy 无需任何账户就可以使用&#xff0c;无需担心隐私或安全问题。Escrcpy没有广告&#xff0c;完全免费开源。 软…

显微观察:Bamtone K系列盲孔显微镜性能优势深度评测

随着电子产品向着高密度、小型化的方向持续演进&#xff0c;印刷电路板&#xff08;PCB&#xff09;的制造工艺复杂度也随之攀升。高密度互连&#xff08;HDI&#xff09;技术中&#xff0c;盲孔&#xff08;Blind Via&#xff09;作为连接不同层电路的关键结构&#xff0c;其质…

Photo Editor安卓版(照片编辑器安卓版)

Photo Editor是一款功能强大的图像编辑工具&#xff0c;适用于安卓设备。它提供了丰富的编辑功能&#xff0c;可以帮助您对照片进行各种调整、修饰和美化。不论您是想增强照片的颜色、裁剪图像的尺寸、添加滤镜效果&#xff0c;还是修复照片中的缺陷&#xff0c;这款软件都能满…

利用多智能体AI实现动态竞争格局评估

利用多智能体AI实现动态竞争格局评估关键词&#xff1a;多智能体AI、动态竞争格局评估、智能体交互、机器学习、博弈论摘要&#xff1a;本文聚焦于利用多智能体AI实现动态竞争格局评估这一重要课题。首先介绍了该研究的背景、目的、预期读者等内容。接着详细阐述了多智能体AI和…

【高斯泼溅】当3DGS遇上传统模型:从“画在一起”到“画得对”的全攻略​

在真实场景重建、数字孪生与新一代三维表达体系中&#xff0c;3DGS正迅速成为不可忽视的技术方向。凭借在细节保真度、重建效率和真实感上的优势&#xff0c;它让传统基于三维精模、倾斜摄影和网格建模的表达方式&#xff0c;首次在“真实还原”层面显得力不从心。 城市场景-3D…

leetcode 856. Score of Parentheses 括号的分数-耗时100

Problem: 856. Score of Parentheses 括号的分数 解题过程 耗时100%&#xff0c;两种方案的&#xff0c;1、递归调用即可&#xff0c;2、或者使用栈的&#xff0c; 1、使用栈&#xff0c;耗时100% int scoreOfParentheses(string s) {if(s"()") return 1;int n s.si…