news 2026/9/28 23:06:44

缓存穿透、击穿、雪崩的实战排查与代码级解决方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
缓存穿透、击穿、雪崩的实战排查与代码级解决方案

凌晨两点十三分,我被电话叫醒。商城主页的品类楼层像被推倒的积木一样一片空白,监控大屏上数据库的QPS曲线从平时的800直线飙升到9000,Redis连接数打满,订单服务超时率冲上30%。后来排查结果让我哭笑不得:根本不是代码发布或者数据库故障,就是一次日常的定时任务批量重建缓存,撞上了活动的开场秒杀。三个词——缓存穿透、缓存击穿、缓存雪崩——一次全占,教科书都不敢这么演。

如果你也是后端开发,这几个词大概率在面试题里背过无数次,但真正遇到的时候,能立刻判断出“当前是哪种问题”,并把手里的方案选对、参数调好、代码落稳的人,并不多。这篇文章我不打算做概念堆砌,而是把这三种故障形态的成因、判断方法、代码级别的解决方案,以及我踩过的那些坑,一次性讲透。

1. 三者到底怎么区分,别再看图鉴式科普了

很多文章喜欢用“缓存雪崩是大量key同时过期、击穿是单个热点key过期、穿透是查了一个不存在的key”这种定义式描述,听起来很清晰,但落地的时候你会发现,它们经常混在一起出现,就像前面说的那次事故。所以我习惯用两个维度去区分:请求目标是不是真实存在,以及失效的范围是大还是小。

1.1 穿透:打到了一个不存在的key

缓存穿透,本质上是请求在“缓存层”和“存储层”都找不到答案。比如一个用户ID=-100的请求、一个商品ID=0的请求、或者是爬虫随机拼的ID,Redis查不到,数据库也没有这条记录。于是每次请求都穿过缓存,直接打到数据库上。

这里有个执行力很强的歪门邪道思路:既然缓存里没有,那就别让数据库也白挨打。在缓存和数据库之间加一道“校验层”,或者把不可能存在的结果也缓存起来,是穿透场景的核心解法。具体的代码和方案在第二章细讲。

1.2 击穿:单个热点key的“死而复生”瞬间

缓存击穿是三者里最“阴”的。它针对的是一个被高并发访问的热点key,比如首页的推荐位配置、秒杀商品的库存key。这个key平时能扛住99%的流量,但缓存刚好在失效的那一瞬间,同时来了几千个请求。它们发现缓存miss了,于是一起去数据库重建缓存,数据库瞬间被冲垮。

这个名字特别形象:一个key就像一面墙,墙倒的那一秒,所有压力瞬时穿透。它和穿透的根本区别在于——数据本身是真实存在的,只是缓存恰好过期了。

1.3 雪崩:大批key一起失效引发连锁反应

缓存雪崩,就是击穿的“群殴版”。大量的key在同一时间段集体失效,比如定时任务批量更新缓存,把同一批数据全部写进缓存并设置了相同的过期时间;或者Redis实例本身的物理故障导致缓存层全部不可用。

我遇到过一个非常经典的雪崩场景:运营人员把所有活动商品统一设置了“当日23:59:59过期”,结果零点刚过,所有商品缓存同时失效,系统直接被压垮。本质上,雪崩的核心在于失效时间的同步性,以及缓存层整体可用性的丧失。

1.4 一张表看懂三者的本质区别

场景请求的数据是否真实存在影响范围主要矛盾
缓存穿透不存在单次请求打穿缓存缓存与数据库都无法命中
缓存击穿存在单个热点key失效期间热点key重建的高并发冲突
缓存雪崩存在大量key同时失效/缓存层不可用失效时间的同步性与缓存整体可用性

这段描述帮助我在线上排查的第一时间建立判断框架:先看DB慢查询日志里有没有大量“查不到数据”的请求,有就是穿透;再看Redis的键空间统计里是否有某个key的访问频率异常高且正好miss,有就是击穿;如果观测到Redis连接异常或者大量key同时消失,那就是雪崩。

2. 缓存穿透实战解法:双保险比单单一个方案更可靠

穿透的解法市面上就两种主流:返回空值缓存和布隆过滤器拦截。很多资料把它们作为“二选一”的选项,但我的实际操作经验是:两者各守一道关卡,组合使用才最稳妥。

2.1 空值缓存:简单粗暴但要注意TTL

当一个查询在数据库里没查到数据,我不急着让它直接返回null,而是把这个“null”也写进Redis,设置一个较短的过期时间。这样后续同样的无效请求,就能命中缓存,不再打到数据库。

// 查询用户信息时处理穿透 public UserInfo getUserInfo(Long userId) { String cacheKey = "user:info:" + userId; // 1. 先查缓存 String value = redis.get(cacheKey); if (value != null) { // 2. 缓存中存了空值标记,直接返回null if (value.equals(EMPTY_MARK)) { return null; } return JSON.parseObject(value, UserInfo.class); } // 3. 缓存未命中,查数据库 UserInfo userInfo = userMapper.selectById(userId); if (userInfo == null) { // 4. 数据库查不到,缓存一个空值,TTL设置为3分钟 redis.set(cacheKey, EMPTY_MARK, 180); return null; } // 5. 正常数据缓存10分钟 redis.set(cacheKey, JSON.toJSONString(userInfo), 600); return userInfo; }

这里有几个实际操作中的关键细节:

  • 空值的TTL绝对不能长。空值缓存的意义是挡住短期内的恶意请求或数据缺失,但一旦数据真的被创建了,空值缓存还挡着就会造成数据不一致。我一般把空值TTL设置在60-180秒之间,最长不超过5分钟。
  • 要区分“真正的空”和“业务无关的默认值”。比如默认头像的URL、默认昵称,这些本身是有效数据,不应该走空值标记逻辑。
  • 空值缓存会消耗Redis内存,如果恶意请求构造的随机ID过多,比如几百万个不同的无效ID,空值缓存方案会撑爆内存。这时候就必须依靠第二道防线。

2.2 布隆过滤器:拦截非法ID的“准入名单”

布隆过滤器的原理,本质上是一个位数组配合多个哈希函数。它用极小的空间代价,判断一个元素是否“有可能存在于集合中”。注意,是“有可能”而不是“一定”。它会误判“可能存在”某个元素,但绝对不会漏判——如果一个ID判定为“不存在”,那它就一定不在数据库中。

我常用的方案,是项目启动后把数据库里所有合法的用户ID、商品ID,也就是会走查询逻辑的ID,全部加载进布隆过滤器。之后每次查询前先过过滤器,如果过滤器说“不存在”,直接返回null,根本不会去查Redis和DB。

// 使用Redisson的RBloomFilter实现 @Bean public RBloomFilter<Long> userIdBloomFilter(RedissonClient redissonClient) { RBloomFilter<Long> filter = redissonClient.getBloomFilter("bloom:user:ids"); filter.tryInit(1000000L, 0.01); // 预期元素数量100万,误判率1% return filter; } // 查询时的拦截逻辑 public UserInfo getUserInfoWithBloomFilter(Long userId) { if (!userBloomFilter.contains(userId)) { // 布隆过滤器判定不存在,直接返回,拦截穿透 return null; } // 命中过滤器,走正常缓存查询流程 return doGetUserInfo(userId); }

2.3 布隆过滤器参数设计:误判率和空间的计算

这里必须提一下参数设置的算法,很多人直接用默认值,用得不明不白的。

布隆过滤器的核心参数有三个:预期元素数量n、误判率p、位数组长度m和哈希函数个数k。它们之间存在这样的关系:

  • 位数组长度 m = - (n * ln p) / (ln 2)^2
  • 哈希函数个数 k = (m / n) * ln 2

以100万ID、1%误判率为例:

  • m = - (1000000 * ln 0.01) / (0.693)^2 ≈ 958万位,约1.14MB
  • k = (9580000 / 1000000) * 0.693 ≈ 7

也就是说,只要1.14MB的内存,就能容纳100万个ID,且误判率控制在1%以内。这里有个工程上的经验判断:误判率不要追求极致的0.01%,因为内存占用会指数级上升,1%-5%的误判率在绝大多数业务场景下完全够用——毕竟被误判放行的请求,后续还有空值缓存兜底。

如果你使用的是Guava的BloomFilter,它内部已经封装好了最优参数计算,只需要传入预期数量和误判率即可,但了解底层算法有助于你在面试或排查问题时快速判断瓶颈。

2.4 为什么不建议“只靠缓存空值”或“只靠布隆过滤器”

只靠布隆过滤器的问题是:它只能拦截数据库里“确定不存在”的数据,但数据是动态的。如果今天用户注册了一个新ID,它在布隆过滤器里是不存在的,如果不做实时更新,这个新用户会被拦截,无法查询到自己的数据。

只靠空值缓存的问题在前面也提过:恶意构造随机ID时,内存会爆炸。

所以我的实现方案是:布隆过滤器做第一道拦截,挡住绝大多数恶意无效请求;空值缓存做第二道拦截,兜住那些“数据库里确实没有但布隆过滤器误判放行”的请求。两层一起,才能把穿透率降到极致。

提示:布隆过滤器不支持删除操作。如果你有删除ID的需求,比如注销用户、下架商品,是需要重建过滤器或使用布谷鸟过滤器(支持删除)的。不要为了这个功能自己实现,调Redisson或者Guava的现成实现最稳。

3. 缓存击穿实战解法:互斥锁与逻辑过期怎么选

击穿的关键矛盾是“热点key失效瞬间的高并发重建”,思路也很直接:要么让同一时间只有一个线程去重建缓存,要么让旧数据在key失效后的短暂时间内继续对外服务。

市面上的成熟方案有四种,但我在生产环境实际用过、并且推荐的是下面两种:互斥锁(Mutex Lock)和逻辑过期(Logical Expiry)。

3.1 方案一:互斥锁重建缓存,只允许一个线程访问DB

核心思路是:发现缓存miss后,不直接查数据库,而是先尝试获取一把分布式锁。拿到锁的线程去查数据库并重建缓存,其他线程短暂自旋重试,等待第一个线程把缓存重建好。

public String getHotData(String cacheKey) { String value = redis.get(cacheKey); if (value != null) { return value; } // 缓存不存在,尝试获取互斥锁 String lockKey = "lock:" + cacheKey; // SET NX EX:只有key不存在时才能设置成功 if (redis.set(lockKey, "1", "NX", "EX", 5)) { try { // 拿到锁,查数据库并重建缓存 String dbValue = queryFromDb(); redis.set(cacheKey, dbValue, 600); return dbValue; } finally { // 释放锁 redis.delete(lockKey); } } else { // 没拿到锁的线程,短暂休眠后重试 Thread.sleep(50); return getHotData(cacheKey); // 递归重试,或使用循环重试 } }

这段代码的关键点,也是我实际踩过坑的地方:

  • 锁的过期时间要短,我一般设置5秒。如果DB查询时间较长,锁到期自动释放,可能导致并发问题。更稳妥的做法是使用Redisson的RLock实现“看门狗”自动续期,避免锁过期但线程还没执行完的情况。
  • 递归重试有栈溢出风险,高频场景下建议用for循环重试,最多重试3-5次,超过就返回兜底数据或直接抛异常。
  • 重建缓存时,一定要用DB的最新数据,不要基于旧缓存做修改——因为锁只保证DB查询这步不并发,但无法保证数据源自身的一致性。

3.2 方案二:逻辑过期,用旧数据撑住新流量

逻辑过期,是不设置Redis物理过期时间,而是在value里加一个字段记录“业务过期时间”。当读取时发现逻辑时间已过期,不是立刻删除key,而是返回旧值给用户,同时异步发起一个重建线程更新缓存。

public String getHotDataByLogicalExpire(String key) { String value = redis.get(key); if (value == null) { // 缓存不存在(可能是Redis重启),直接查DB并返回 return queryFromDbAndSetCache(key); } // 反序列化,读取逻辑过期时间 CacheObject cacheData = JSON.parseObject(value, CacheObject.class); if (cacheData.getExpireTime() > System.currentTimeMillis()) { // 未过期,直接返回 return cacheData.getData(); } // 已过期,尝试获取锁,异步重建 String lockKey = "lock:" + key; if (redis.set(lockKey, "1", "NX", "EX", 5)) { try { // 获取锁后,再检查一次逻辑时间 // 防止上一个重建线程刚更新完,本线程又重建一次 Thread thread = new Thread(() -> { String newValue = queryFromDb(); // 设置新的缓存值,逻辑过期时间为当前时间+600秒 redis.set(key, newCacheObject(newValue, 600)); redis.delete(lockKey); }); thread.start(); } finally { // 释放锁 redis.delete(lockKey); } } // 返回已过期的旧数据 return cacheData.getData(); }

这个方案的优点是:读取操作永远不阻塞,即使缓存过期了,用户拿到的还是旧数据,体验几乎无感知。我主推它来应对秒杀场景下的热点商品——因为秒杀场景对“返回旧库存数据”的容忍度比“超时无响应”高得多。

但它有一个代价:容易出现短期数据不一致,因为用户可能读取到过期的数据几秒甚至十几秒。所以,对数据一致性极高的业务要慎重,对读多写少且一致性容忍度较高的热点数据则非常合适。

3.3 两种方案选型表格对比

维度互斥锁重建逻辑过期
用户体验缓存重建期间,部分请求可能等待重试始终能拿到数据(旧值)
数据一致性高,读到的一定是重建后的新数据低,可能读到过期数据
对DB的压力只放行一个线程,压力最小高并发时DB可能短暂承压
实现复杂度中等,需要处理锁的获取、释放、重试相对复杂,需要维护逻辑过期时间和异步更新
适用场景数据一致性要求高,DB响应快读多写少,对短时旧数据容忍度高

3.4 一个容易被忽略的坑:锁的粒度

锁的粒度设计很重要。很多人直接对整个业务接口加锁,导致所有key的缓存重建都串行化,性能断崖式下降。正确的做法是按key粒度加锁,也就是每个热点数据的cacheKey对应一把独立的lockKey。这样某个商品重建缓存时,其他商品的查询完全不受影响。

我在生产环境踩过这个坑后总结出来的规则是:锁的粒度越细越好,锁的过期时间要远小于业务可接受的等待时间。

4. 缓存雪崩实战解法:时间随机化与高可用架构双管齐下

雪崩的成因可以分两类:一是大量key同时过期,二是Redis实例不可用。两种情况的解决思路完全不同。

4.1 针对“大量key同一时间点过期”

最朴素也最见效的做法,就是给过期时间加一个随机偏移量。

我记得当年为活动任务批量写缓存的时候,业务方原本要求“所有商品缓存统一在十分钟后过期”。我直接在代码里把统一过期时间改成了“基础过期时间 + 随机数”,这样就避免了一大波缓存同步失效。

// 设置缓存时,在原始过期时间基础上加上随机偏移 int baseExpire = 600; // 10分钟 int randomExpire = baseExpire + new Random().nextInt(300); // 加上0~5分钟随机数 redis.setex(cacheKey, randomExpire, value);

别看这个改动小,实际效果却出奇的好。如果一万个key都在同一秒失效,那么加了随机偏移后,它们会在5分钟的时间窗口内分散失效,DB压力从“瞬时峰值1万QPS”平摊成“持续平稳的几百QPS”。

另一个实践是避免设置相同的逻辑过期时间点。比如定时任务更新缓存时,很多工程师习惯为所有key设置“当天23:59:59过期”,这就人为制造了第二天的雪崩时刻。我一般会让运营数据的过期时间基于“创建时间+业务有效时长+随机偏移”的逻辑,而不是统一对齐到某个自然时间点。

4.2 针对“Redis整体不可用”

当Redis集群宕机、网络分区或连接池耗尽时,所有请求都会绕过缓存直接打向DB,这种雪崩更致命。我的方案是构建一个“缓存不可用时的降级链路”:

public String getDataWithFallback(String key) { try { String value = redis.get(key); if (value != null) { return value; } } catch (Exception e) { // Redis超时或者连接异常,记录告警,继续走降级逻辑 log.error("Redis异常,降级", e); } // 本地缓存兜底(Caffeine/Guava Cache) String localValue = localCache.get(key); if (localValue != null) { return localValue; } // 本地缓存也没有,查DB并更新本地缓存 String dbValue = queryFromDb(); localCache.put(key, dbValue); return dbValue; }

这个降级链路的精髓在于多级缓存兜底:Redis挂了,本地缓存顶上;本地缓存也没有,DB还能扛一阵。配合限流组件(比如Sentinel或Hystrix),把超出系统承载能力的请求直接快速失败,返回提示或降级数据,不让无休止的请求把DB打挂。

4.3 Redis高可用的基础设施配置

降级是兜底手段,高可用才是主动防御。我强烈建议在主生产环境使用Redis Sentinel(哨兵模式),在数据量超过单机内存承载能力时使用Redis Cluster(集群模式)。

哨兵模式至少部署一主两从三哨兵,主节点故障时自动完成故障转移,业务方无感知。Cluster模式则通过数据分片把key分散到多个主节点,任何一个分片挂掉,其他分片依然提供服务。另外由于Cluster模式下每个节点默认只负责部分slot,单节点宕机导致的缓存不可用范围会被限制在1/总节点数的规模内。

这里有个我实际遇到过的问题:配置了哨兵,但客户端没有配置多节点地址。有些框架的Redis客户端只配了一个主节点地址,主节点故障时它依然连老的地址,导致连接失败。正确做法是配置多个哨兵地址,让客户端通过哨兵动态感知当前主节点。

4.4 预防雪崩的几道闸门

生产环境中,我的雪崩防御体系是分层的:

  • 网络层:对可疑IP进行限流,拦截恶意并发请求。
  • 网关层:接入层配置总并发控制,防止瞬时流量冲击。
  • 缓存层:过期时间加随机偏移;核心数据用逻辑过期;Redis高可用架构。
  • 应用层:互斥锁重建热点key;多级本地缓存兜底;限流熔断器。

这套闸门体系在多次大促场景中验证过,即使Redis真的出现问题,数据库也不会直接被压垮。

5. 线上故障排查:一夜之间怎么从监控入手定位问题

理论讲得再多,不会排查等于零。我复盘一下自己是怎么在十分钟内定位到那场“三问题齐爆”的事故的。

5.1 看监控指标找线索

排查的第一步永远是看监控大盘,重点看四个指标:

指标穿透的特征击穿的慢特征雪崩的特征
Redis命中率短期不变或缓慢下降某个key刷新瞬间短暂下降后恢复整体断崖式暴跌后持续低位
DB QPS超高且持续单个时间点尖峰高峰持续数秒甚至数分钟
接口响应时间未命中时响应缓慢高并发下部分请求超时大面积超时甚至服务熔断
Redis连接数正常正常或略高打满或异常

那天的监控是Redis命中率一秒内从95%降到60%,同时DB QPS暴增。这两个信号同时出现,我第一反应是雪崩——因为击穿一般是命中率跌下去又快速涨回来,穿透也不可能让命中率波动这么大。

5.2 快慢日志定位具体key

确认是雪崩方向之后,通过Redis慢查询日志和DB慢查询日志,很快锁定了命中率暴跌区间内访问量最大的几个key——全部集中在活动商品品类上。再结合Redis-keyspace-notifications检测键失效事件,可以看到这些key几乎在同一秒内触发了expire,时间和那次定时任务吻合。

定位到一批key同时过期是问题根源之后,马上把那条定时任务脚本里的“写缓存设置统一过期时间”逻辑揪了出来,现场修改成带随机偏移的版本。因为Redis本身没挂,所以降级链路没用上,但如果你观察到的是Redis连接抛异常,就要立刻激活降级代码,先把流量从Redis切到本地缓存或限流。

5.3 一次实战模拟:用JMeter复现击穿

建议你在测试环境提前演练一次。我之前用JMeter做过一次击穿压测:

  • 先把某个热点key删除,模拟缓存失效。
  • 然后开启200个并发线程,循环请求这个key。
  • 观察监控:DB QPS瞬间冲到200,而缓存重建完成后降回0。
  • 接着应用互斥锁方案,重复上述步骤,发现DB QPS会降到1-2,其他190多个请求都在等待重试。

做过一次这种对比压测,你就会对击穿的危害和方案效果有切身体感,不至于上线后才手忙脚乱。

5.4 线上排查的三大铁律

我总结了几条排查时的操作纪律:

  1. 绝对不要在线上直接删除生产key来测试击穿,除非你已经确认业务低峰期且方案已上。
  2. 通过Redis的monitor命令观察实时请求模式,注意只筛选关键的访问量大的key,否则日志量会大到崩溃。
  3. 先保命再定位,如果DB眼看要被打挂,优先执行限流熔断,先把流量挡住,再慢慢查根因。不要试图在系统挂掉时还保持“完整取证”。

6. 综合落地实践:一套方案同时防住三种问题

前面分析了各自的解法,但生产中这三种问题往往交替出现。我推荐一套组合拳,你可以直接参考落地。

6.1 整体架构分流图

  • 所有查询请求先经过布隆过滤器,不存在的数据直接返回null,这是第一道防线。
  • 通过过滤器的请求进入Redis查询,命中直接返回;未命中判断是否热点key。
  • 热点key走逻辑过期或互斥锁方案,普通key走空值缓存方案。
  • 如果Redis异常,启动本地缓存降级和DB限流兜底。

这套链路在代码层面是一个标准的多级缓存Query流程:

public String queryProductDetail(Long productId) { // --- 第一层:布隆过滤器拦截 --- if (!productBloomFilter.contains(productId)) { return null; } // --- 第二层:Redis查询 --- String cacheKey = "product:detail:" + productId; String value = redis.get(cacheKey); if (value != null) { return value; } // --- 第三层:本地缓存兜底 --- value = localCache.get(cacheKey); if (value != null) { return value; } // --- 第四层:互斥锁重建,DB查询 --- String ret = getHotDataByMutex(cacheKey); return ret; }

6.2 参数配置的心得总结

我把自己常用的参数整理成一个速查建议表,方便你copy设置时参考:

参数建议值说明
空值缓存TTL60-180秒太短挡不住穿透,太长容易数据不一致
布隆过滤器误判率1%-5%越低越耗内存,1%是均衡点
互斥锁过期时间3-5秒要小于DB平均查询时间+重建缓存时间
互斥锁重试间隔50-200ms太短会空转消耗CPU,太长影响请求响应时间
逻辑过期预留时间基础TTL的20%-30%保证异步重建有足够时间窗口
缓存随机偏移基础过期时间的10%-50%太大导致缓存不生效,太小无法错峰
本地缓存容量1000-5000个key按业务实际热点key数评估

6.3 定时任务批量写缓存,一个必改的坏习惯

最后单独提一个高频雷区:定时任务里用统一的setex批量写缓存。

比如一个跑批系统每天凌晨把全量商品信息刷进Redis,很多工程师图简单,直接for循环set key,所有key的过期时间一模一样。一旦某个定时任务延迟到业务高峰前才执行完,就相当于给业务埋了一颗定时炸弹——统一过期时间到的那一刻,雪崩就来了。

我的标准做法是:跑批写缓存时,按每批或每个key随机分散过期时间;如果业务确实需要统一在某个时间点失效,那就再加一层懒加载重建,即任务先把数据更新至DB,Cache由读取时逐步重建,而不是依靠定时任务统一刷入。

7. 写在最后:这不想再被半夜叫醒的话

那场凌晨三点的故障之后,我把缓存相关的所有代码做了一次全面审查,不仅修了跑批和过期时间,还补上了空值缓存和布隆过滤器两道防线。后来类似的并发场景又来过好几次,但监控上的DB曲线始终很平稳,再没有人半夜打电话叫醒我。

我的体会是,缓存这三个问题真正难的地方不在“理解概念”,而在于“把方案落到代码里时,能不能提前想到边界条件”。锁过期了怎么办、空值会不会撑爆内存、布隆过滤器误判了后续靠谁兜底、Redis挂了业务能不能降级——这些问题每一个都是实操里能碰到的事情。

如果你现在正准备重构自己系统的缓存层,我建议顺序是:先给Redis加高可用,再给热点key做好互斥锁或逻辑过期,然后在入口加上布隆过滤器和空值缓存,最后定期做一次压测,模拟一种或多种故障同时发生的情况。不要等到监控报警才去思考方案,那会儿你和我一样,只能手忙脚乱看日志了。

这套组合拳运行稳定之后,你会发现“缓存雪崩、缓存击穿、缓存穿透”这几个词,在咨询记录里会渐渐消失。它们最终沉淀为你对系统容错能力的一种直觉,而不是面试答题时的三个名词。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 23:00:37

基于9100张安防异常行为数据集的YOLO目标检测实战指南

1. 拿到9100张安防异常行为数据集&#xff0c;先搞清楚它到底能做什么安防监控这个方向&#xff0c;做算法的人都有一个共识&#xff1a;模型结构可以复现&#xff0c;训练脚本可以照抄&#xff0c;唯独数据是卡脖子的那一环。你可以在开源社区找到几百个YOLO改进方案&#xff…

作者头像 李华
网站建设 2026/9/28 22:56:11

Claude Code实测指南:终端AI编程助手的命令速查与权限配置

作为一个常年泡在终端里改代码的人&#xff0c;我第一次听说 Claude Code 的时候其实是有点不以为然的——一个命令行工具而已&#xff0c;能比把代码复制粘贴进网页聊天窗口强到哪去&#xff1f;但当你真正在项目目录里跑起来claude&#xff0c;对着一个几千行代码的仓库问一句…

作者头像 李华
网站建设 2026/9/28 22:55:20

Token与JWT实战:从登录鉴权到续签与安全排坑

做后端开发这几年&#xff0c;我几乎在每个项目里都会被同一个问题绊倒几次&#xff1a;接口明明写好了&#xff0c;前端也按文档传了参数&#xff0c;可对方就是报401或者403。大多数时候翻一翻调用链&#xff0c;问题都出在Token上——Token过期、Token失效、Token续签失败&a…

作者头像 李华
网站建设 2026/9/28 22:52:20

航班订票系统实战:并发控制、订单状态机与数据库设计

1. 项目启动&#xff1a;m241这个编号背后的真实需求接手"m241航班订票管理系统"这个项目的时候&#xff0c;其实挺有意思的。编号m241是实训基地的项目标识&#xff0c;但落到实际开发上&#xff0c;需求一点都不抽象——就是一个能查航班、能订票、能管订单的Web系…

作者头像 李华
网站建设 2026/9/28 22:51:57

Android 13多路录音实战:AudioRecord六通道配置与避坑指南

1. 多路录音到底难在哪&#xff1a;从AudioRecord的底层逻辑说起Android录音这件事&#xff0c;表面上看就是拿个AudioRecord往缓冲区里读PCM数据&#xff0c;简单得不能再简单。但一旦把需求改成“同时录6个麦克风”&#xff0c;事情就完全不一样了。我在第一次接到这个需求的…

作者头像 李华
网站建设 2026/9/28 22:50:05

模型部署提速实战:基于ONNX与量化的自动优化工具解析

上个季度我们有个线上服务经常被客户投诉&#xff1a;不是精度不行&#xff0c;而是响应太慢。模型在 A100 上训得很欢&#xff0c;单卡精度也漂亮&#xff0c;可真要部署到公司那批旧 GPU 甚至部分纯 CPU 环境时&#xff0c;单次推理直接飙到一秒往上&#xff0c;超时率拉满。…

作者头像 李华