news 2026/10/7 4:47:52

Redis商户查询缓存实战:穿透、击穿、雪崩与一致性治理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis商户查询缓存实战:穿透、击穿、雪崩与一致性治理

“黑马点评”这个项目我前后刷了两遍,第二遍专门盯住了“商户查询缓存”这一块,才算是把 Redis 在企业级查询场景里到底怎么落地给嚼碎了。这个模块看起来就是“查一个商铺详情加个缓存”,但里面塞了一堆实际开发中必然踩坑的东西:缓存穿透、缓存击穿、缓存雪崩,还有缓存和数据库的一致性。我把自己反复调试的过程整理成笔记,这一篇重点讲商户查询缓存,适合那些跟着教程敲完却不太理解为什么这么写的同学,也适合准备面试前想搞懂 Redis 缓存治理细节的人。

1. 商户查询缓存:先搞清楚它到底在解决什么问题

1.1 为什么要专门给商户查询加一层缓存

黑马点评是一个模仿大众点评的实战项目,商户查询是最核心也最高频的接口之一。你想想,用户打开 App 第一件事就是看附近的店铺,点进去看详情,一个商户详情页可能在高峰期被请求成千上万次。如果每次请求都直接打到数据库,哪怕 MySQL 能扛住几千 QPS,也会被大量重复的读请求拖垮,尤其是冷门商户和热门商户混在一起时,数据库连接数很快就会被打满。

缓存的作用就是把这些重复的查询结果“就近”存起来。第一次有人查某个商户时,我们老老实实去数据库拿,然后写入 Redis;之后同样的查询直接走 Redis,不再碰数据库。对于读多写少的场景,这种“缓存数据库”的分层架构能扛住几十倍甚至上百倍的流量压力。商户查询恰好就是典型的读多写少业务——用户反复看,商户信息却很少变。

从数据特征上还能看到另一层意义。商户信息是结构化数据,规模可控,单条数据大小通常在几 KB 以内,非常适合用 Redis 的 String 结构存储序列化后的 JSON。相比缓存用户会话、缓存验证码这类临时数据,商户缓存的业务属性更强,它需要解决的是“数据源压力”问题,而不是单纯的“临时存储”问题。

1.2 缓存选型:为什么选了 Redis 而不是本地缓存

做缓存方案时,我第一反应是:项目里已经引了 Spring 全家桶,为什么不直接用本地缓存,比如 Caffeine 或者简单的 ConcurrentHashMap?当时我把两套方案都简单测了一下。

本地缓存的优势是无网络开销,速度更快,实现也简单,但目前提是缓存只为单个应用实例服务。黑马点评部署时通常不会单机跑,Nginx 后面挂多个后端实例时,用户请求会被负载均衡到任意一台机器。本地缓存会让每个实例各存一份数据,出现“每台机器数据不一致”的问题,而且当某个实例发生热点数据更新时,其他实例难以及时感知,只能等过期。另外本地缓存受堆内存限制,商户数据一旦量大,很容易挤占 JVM 空间。

Redis 作为分布式缓存,是多实例共享的,所有后端实例读写同一份缓存数据,不存在单机内存容量瓶颈,也天然支持过期、持久化、集群等能力。虽然多了一次网络 IO,但 Redis 本身处理速度极快,在局域网环境下,一次读取通常在毫秒级,完全能接受。最终我选了 Redis,更准确地说是 Spring Data Redis 中的 StringRedisTemplate,配合 JSON 字符串存储。

1.3 一条商户查询请求的完整流转路径

把整体流程画在脑子里,后面写代码才不会乱。标准的 Cache Aside 模式下的查询链路是这样的:

  • 请求到达/shop/{id}接口;
  • 先根据 key 查 Redis;
  • 如果 Redis 有数据,直接反序列化为 Shop 对象返回;
  • 如果 Redis 没有数据,查数据库;
  • 数据库也没有,返回错误;
  • 数据库有,则把数据写入 Redis,设置过期时间,再返回给前端。

这个流程看着简单,但它埋了很多细节:key 的规则是什么,用什么序列化方式,过期时间设多长,缓存没命中时数据库并发请求怎么控制。任何一个环节处理不好,都会在真实场景里引发“缓存穿透”“缓存击穿”或者“缓存雪崩”。后面的章节就把这些细节逐个拆开讲。

2. 设计与实操:从零搭建一个可用的商户缓存查询接口

2.1 准备环境:Redis 依赖与配置

黑马点评的初始工程里往往已经带了 Redis 依赖,但自己从头建项目的话,需要确认这几个东西。首先是 Maven 依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>

第二个依赖是连接池依赖,Spring Boot 2.x 使用 Lettuce 作为默认客户端,Lettuce 本身可以不用连接池,但在高并发下建议配置连接池,避免频繁创建连接。配置文件里基础项如下:

spring: redis: host: localhost port: 6379 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0

这里要注意,配置连接池后还需要引入 commons-pool2,否则启动时不会报错,但运行时拿不到连接池对象,我踩过一次这个坑。另外,生产环境一定要设置密码和指定 db,不过学习阶段本机默认配置就够了。

2.2 缓存 Key 的设计细节

商户查询缓存的 key 到底长什么样,教程里给的是cache:shop:1001这种形式。我第一次写的时候直接用了shop_1001,功能没错,后来才意识到命名空间的重要性。

规范的 key 由三部分组成:业务前缀、模块名、唯一标识。cache:shop:id这种冒号分隔的写法是 Redis 社区比较标准的做法,一方面方便在 Redis Manager 里按前缀模糊搜索和删除,另一方面也降低了多业务之间 key 冲突的概率。如果你还有其他模块,比如优惠券、博客,都可以沿用cache:coupon:、cache:blog:这样的前缀。

还有一个小细节:不同类型的查询要注意 key 的粒度。商户查询是按 id 查询,key 就是cache:shop:1001;但店铺列表查询往往附带地理位置、类型等条件,这种复杂查询的 key 就得拼接查询参数,比如cache:shop:type:1:region:xxx。设计 key 的时候尽量保持“越简单越好,但不要牺牲可管理性”。

2.3 商品(商户)信息序列化的坑

黑马点评里的商户实体是Shop,它对应的表叫tb_shop,字段有 id、name、typeId、images、area、address、avgPrice、sold、comments、score、openHours、createTime、updateTime 等。Redis 里存的不是对象序列化后的二进制,而是 JSON 字符串。

为什么不用 JDK 序列化?因为 JDK 序列化后的数据里有类路径信息和大量描述头,可读性差、体积大,而且 Redis 里如果存的是 Java 对象二进制格式,其他语言或者直接用命令行看数据时会完全看不懂。用 JSON 的话,数据清晰,也方便后续做数据分析、日志排查。

实际代码里,我一开始用的是Jackson的ObjectMapper,后来发现想省事的话,直接使用StringRedisTemplate配合手动 JSON 转换即可。Spring Data Redis 默认的RedisTemplate<Object,Object>用的是JdkSerializationRedisSerializer,存进去的数据是乱码,不要直接拿它做业务缓存。推荐换成:

@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // key 使用 String 序列化 template.setKeySerializer(RedisSerializer.string()); // value 使用 JSON 序列化 template.setValueSerializer(RedisSerializer.json()); // hash 结构也做同样配置 template.setHashKeySerializer(RedisSerializer.string()); template.setHashValueSerializer(RedisSerializer.json()); template.afterPropertiesSet(); return template; } }

但这里又有一个新坑:用RedisSerializer.json()反序列化时,如果对象里有 LocalDateTime 这类时间字段,Jackson 反序列化会报异常,因为Shop的createTime和updateTime是LocalDateTime类型。教程里在application.yaml中增加了一个配置:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

这个配置只对Date类型有效,对LocalDateTime未必管用。稳妥的做法是给Shop中时间字段加上@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解,或者用自定义ObjectMapper注册JavaTimeModule。StringRedisTemplate存取 JSON 字符串时,建议自己控制序列化,这样最稳:

private static final ObjectMapper MAPPER = new ObjectMapper() .findAndRegisterModules() .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

2.4 核心查询代码实现(附带缓存空值逻辑)

查询商户接口的核心代码,教程里其实给得挺简练,但里面包含了一个处理缓存穿透的思想:缓存空对象。先把基础版写出来,后面再讲为什么需要处理空值。

@Service public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private ObjectMapper objectMapper; @Override public Result queryShopById(Long id) { String key = "cache:shop:" + id; // 1. 从 Redis 查询 String shopJson = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(shopJson)) { Shop shop = objectMapper.readValue(shopJson, Shop.class); return Result.ok(shop); } // 2. 注意:如果缓存中存的是空字符串,说明之前查过数据库且不存在 if (shopJson != null) { // 说明是故意缓存的空值,直接返回不存在 return Result.fail("店铺不存在"); } // 3. 查询数据库 Shop shop = getById(id); if (shop == null) { // 4. 缓存空值,设置较短过期时间,防止穿透 stringRedisTemplate.opsForValue().set(key, "", 2L, TimeUnit.MINUTES); return Result.fail("店铺不存在"); } // 5. 写入 Redis,设置过期时间 stringRedisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(shop), 30L, TimeUnit.MINUTES); return Result.ok(shop); } }

这段代码里有几个关键点:

  • StrUtil.isNotBlank判断字符串不为空且非空串,但空字符串也是“非空”的反面,所以先用shopJson != null判断是否命中缓存空值;
  • 写入缓存时,空值要设置一个远小于正常值的过期时间,比如 2 分钟,防止恶意查询不存在的 id 时把 Redis 塞满;
  • 正常数据设置 30 分钟的 TTL,这个值不是随便定的,需要根据业务数据更新频率来调整。

上面的代码为了展示清晰忽略了异常处理,实操中一定要用 try-catch 包住 JSON 转换逻辑,避免缓存数据损坏时把业务接口也拖挂。

3. 缓存更新的姿势:一致性问题的处理和取舍

3.1 更新策略:Cache Aside 模式拆解

商户信息不是永远不变的,运营可能会改店名、调整价格、修改营业时间。这就有个很现实的问题:数据库更新后,缓存里的旧数据怎么处理?

业界最常用的就是 Cache Aside 模式,也叫旁路缓存,操作顺序是:读的时候先读缓存,读不到再读库,然后回填缓存;写的时候先更新数据库,再删除缓存。听起来很简单,但每一步都有它存在的理由,不能随意换顺序。

这个模式最大的好处是实现简单且足够可靠:只要数据库更新成功,缓存删除虽然可能失败,但最多只是让下一次查询拿到旧数据,等 TTL 过期后最终一致。相比完全同步更新缓存的做法,删除缓存而不是更新缓存更划算,因为更新的成本高,还要考虑并发写顺序问题,而删除操作只需要一个DEL命令,代价极低。

在黑马点评项目里,更新商户信息对应的是updateShop接口,要走的就是“先更新数据库,再删除缓存”这条路。我们业务中使用了 MyBatis Plus,可以直接调用updateById,然后执行delete操作删除 Redis key。

3.2 先更新数据库还是先删缓存?

我在学习时一直在想,为什么不先删缓存再更新数据库?或者先更新缓存再更新数据库?实际上这几种顺序都有问题。

先删缓存、再更新数据库:如果删完缓存之后、数据库还没更新完,突然来一个读请求,会查不到缓存然后去数据库读取旧数据,再回填到缓存里。等数据库更新完成后,缓存里还是旧数据,而且 TTL 可能很长,导致数据长期不一致。

先更新缓存、再更新数据库:如果数据库更新失败,缓存里就是新数据,但实际上数据库是旧数据,数据不一致问题更严重,而且很难排查。

先更新数据库、再更新缓存:数据库更新成功后,在写缓存时会经历一个时间窗口,此时并发读可能读到旧数据。虽然时间很短,但严格来说也存在不一致问题,不过它好过上面两种方案。

先更新数据库、再删缓存:是目前综合来看最稳妥的方式。即使删除缓存失败,也可以通过 TTL 兜底,而且配合下面的延迟双删,能最大程度减少不一致窗口。

3.3 延迟双删:一个很土但有效的补丁

既然“先更新数据库再删缓存”已经不错,为什么还要延迟双删?因为存在一个极端情况:请求 A 更新数据库后准备删除缓存,请求 B 在这之前读到了旧缓存,然后 B 把旧数据回填到缓存里,此时 A 再删缓存,旧数据被删;但如果 A 删除缓存在 B 回填之前,B 则会把旧数据重新写回缓存,造成缓存长期不一致。

延迟双删的思路是:第一次删除缓存后,等待几百毫秒,再次删除一次。目的是让那些在“删除后、回填前”的并发读请求完成回填,第二次删除就能把可能被写入的旧缓存清掉。代码思路如下:

updateById(shop); stringRedisTemplate.delete(key); // 延迟 500ms 后再删一次 executor.schedule(() -> stringRedisTemplate.delete(key), 500, TimeUnit.MILLISECONDS);

这里有两个注意点。延迟时间的选择不是越长越好,而是要大于“读数据库并回填缓存”的时间,一般取 500ms 到 1s 之间,压测时可以用实际耗时来调节。另一个是第二次删除必须保证执行成功,生产环境可以扔到消息队列里异步重试,或者使用可重试的脚本,否则又等于没删。延迟双删不是银弹,它只是把不一致窗口缩短到极低,真正的最终一致还是要靠 TTL 兜底。

3.4 过期时间与随机化:最廉价的雪崩预防

每次往 Redis 里写缓存时,都需要设置过期时间。为什么不设置永久有效?一来缓存数据必须与源数据最终一致,二来防止 Redis 内存被无限制占用。

我在写的时候一开始直接set(key, json, 30, TimeUnit.MINUTES),也没有多想。后来模拟大量缓存同时过期时才发现问题:如果一大批商户缓存同时失效,而这时正好有大量请求进来,数据库会被瞬间压垮,这就是缓存雪崩的其中一个诱因。

解决办法很朴素:给 TTL 加一个随机扰动。比如原本是 30 分钟,实际设置为 30 分钟加上一个 0 到 300 秒的随机值。这样即使大量 key 是同一个时间创建的,过期时间也会错开,避免出现“整点集体失效”的场面。代码实现:

long ttl = 30 * 60 + ThreadLocalRandom.current().nextInt(300); stringRedisTemplate.opsForValue().set(key, json, ttl, TimeUnit.SECONDS);

这个技巧非常廉价,但很有效。对于商户类冷数据,甚至可以把基础 TTL 提升到 1 小时甚至更长,降低数据库压力,同时靠业务更新时主动删缓存保证一致性。

4. 线上最常见的三大缓存问题:穿透、击穿、雪崩

4.1 缓存穿透:空值缓存与布隆过滤器

缓存穿透是指查询一个不存在的数据。因为缓存里没有,所以请求会一直打到数据库。如果攻击者或者恶意用户频繁用不存在的 id 调用接口,数据库会被大量无效查询打崩。

黑马点评里最传统的场景就是大量查询一个不存在的商户 id,比如-1、99999999这种。我在课程练习里主要使用了缓存空值的方式解决:当查询数据库发现结果为 null 时,仍然把 null 值包装成空字符串写入 Redis,并设置一个 2 分钟的过期时间。这样下次同样的查询直接命中缓存,虽然拿到的不是真实数据,但避免了对数据库的重复冲击。

缓存空值的优点是实现简单,完全不改变现有代码结构,缺点也很明显:如果恶意请求总是换不同的不存在 id,空值缓存也挡不住,而且会占用 Redis 空间。所以更严谨的方案是引入布隆过滤器,把所有可能存在的商户 id 提前存到布隆过滤器里,查询前先判断 id 是否存在,如果不存在就直接返回失败,连 Redis 都不需要查。

布隆过滤器的原理可以理解为用一个很长的二进制位数组加多个哈希函数来标记数据是否存在。它能够比较快速地判断一个元素“一定不存在”或者“可能存在”,代价是存在一定的误判率,不过对于防止穿透来说是够用的。在实际项目中,布隆过滤器适合用在数据量大、空查比例高的场景,而黑马点评这种单表数据量小的情况下,缓存空值已经够用了。

4.2 缓存击穿:互斥锁和逻辑过期方案对比

缓存击穿和缓存穿透名字接近,但意思完全不一样。击穿针对的是某个热点 key:当这个 key 突然过期时,正好有大量请求同时来查它,所有请求都发现缓存没有了,于是一起打到数据库。典型场景就是某个爆款店铺突然首页展示,查询量瞬间飙升,而它的缓存又刚好到期。

黑马点评教程里给了两种方案:互斥锁和逻辑过期。

互斥锁的思路是:第一个发现缓存未命中的请求,先获取一个分布式锁,然后去查数据库回填缓存;其他请求发现拿不到锁,就暂时等待,等第一个请求回填完成后再去读缓存。代码上可以用 Redis 的SETNX命令实现锁,注意设置过期时间防止死锁。这么说可能有点抽象,实际代码类似:

String lockKey = "lock:shop:" + id; boolean locked = tryLock(lockKey); if (!locked) { Thread.sleep(20); return queryShopById(id); // 递归重试 } try { // 再次检查缓存(double check) // 查询数据库,回填缓存 } finally { unlock(lockKey); }

互斥锁方案实现相对简单,逻辑清晰,而且能保证强一致性,但缺点是为等待锁的请求增加了一定的延迟,如果持有锁的线程执行较慢,后续请求会被阻塞。

逻辑过期方案正好相反。它不设置真实的 TTL 过期,而是把过期时间作为一个逻辑字段存进 value 里。查询时如果发现逻辑时间已过期,先尝试获取互斥锁,拿到锁的线程单独去重建缓存,其他线程则先返回旧数据。这个方案的特点是“短暂的数据不一致可以接受,但绝不阻塞用户请求”,更适合对性能要求极高、能容忍少许脏数据的场景。黑马点评明显推荐理解互斥锁,逻辑过期更多是面试和扩展思路用。

我自己实际模拟击穿场景时发现,如果热点 key 的过期时间固定,击穿的破坏力比想象中大:几十个并发请求就足以让数据库 CPU 飙高。加了互斥锁后数据库压力明显下降。但要注意,锁的粒度最好细到单个 key,而不是全业务共用一把锁,否则会出现一个商户慢查询拖慢所有商户查询的情况。

4.3 缓存雪崩:过期时间随机化与多级降级

缓存雪崩是指大量 key 在同一时间过期,或者 Redis 节点宕机,导致所有请求直接打到数据库,造成数据库不可用。

这里要区分一下:缓存雪崩和击穿的区别在于,击穿是单个热点 key 失效,雪崩是大面积缓存同时失效。在商户查询场景里,如果所有商户缓存都设置了相同的 30 分钟过期,那么每过一个 30 分钟周期,缓存里的数据就会像约定好一样集体失效一次,这时候压力峰值特别难看。我用 Jmeter 简单压过,整点前后的请求错误率明显升高。

缓解手段我在 3.4 节已经提过,就是 TTL 随机化。此外还可以做多级缓存:在 JVM 本地缓存一层 Caffeine,设置非常短的过期时间,比如 30 秒,这样即使 Redis 中的 key 集体失效,还有本地缓存挡一层。我后来在实际项目中加上了 Caffeine 做 L1 缓存,Redis 做 L2 缓存,效果相当明显,数据库 QPS 压力进一步降低。

另一个思路是限流和降级。当检测到数据库连接池使用率过高时,直接对一些非核心查询返回默认兜底数据,比如热门店铺的默认营业时间,而不是让请求继续打到数据库。黑马点评课程里没有实现这么重,但从雪崩应对的角度,理解这个方向就够了。

5. 实测踩坑记录:这些问题不真跑一遍根本发现不了

5.1 缓存穿透压测下数据库连接被打满

我用 Jmeter 模拟了 1000 个并发请求,随机传不存在的商户 id,数据库连接池瞬间被打满,控制台全是连接超时异常。后来加了空值缓存,情况明显好转,但还发现一个细节:空值缓存写入时没有设置过期时间,导致 Redis 里堆积了大量过期空值。这里要特别留意,所有缓存空值都一定要尽可能短地设置 TTL,否则 Redis 内存会涨得很快。

另外一个坑来自 MyBatis Plus 的getById方法。如果数据库中确实不存在该记录,它返回 null,但如果表主键类型是雪花 ID,而请求传入的 id 是普通数字,MyBatis Plus 会自动做类型转换,严重时还会抛异常。所以接口入参最好使用包装类型,比如Long,同时增加参数校验,过滤掉明显不合法 id。

5.2 并发查询时的缓存击穿现场

我模拟热点商户缓存过期,同时用 100 个线程并发请求同一个 id,结果数据库在那一瞬间出现了几十条一模一样的 SQL。原因很简单:所有线程都发现缓存不存在,然后一起执行了select * from tb_shop where id = ?。这说明缓存未命中后的回填操作必须加锁保护,否则热点 key 就是数据库的定时炸弹。

互斥锁实现中我也踩了坑:第一次用setIfAbsent实现加锁后,忘记设置过期时间。后来某个请求抛异常导致锁没有释放,整个接口就一直卡住。正确做法是加锁时设置超时时间,比如 3 秒,避免死锁。还有一个细节,释放锁时不能直接del key,最好用 Lua 脚本先比较 value,防止误删了别人的锁,这就是网上常说的“锁的持有者校验”。

5.3 序列化工具选择导致的兼容性坑

课程刚开始时,我用的RedisTemplate默认序列化器,存 Redis 时 key 带了乱码前缀,value 是二进制 JDK 序列化对象。后来从命令行看数据,完全无法阅读,重新启动项目后反序列化还报了类 cast 异常,就是因为序列化方式不一致。

后来换成了StringRedisTemplate加手动 JSON,世界清净了。但要注意,手动 JSON 时Shop对象里如果有LocalDateTime字段,用默认 Jackson 会报InvalidDefinitionException,需要在ObjectMapper上注册JavaTimeModule,或者给字段加@JsonFormat注解。时间字段序列化后的格式如果带T,还要注意前端解析是否兼容。

5.4 缓存清理工具 和后续扩展

实操中经常需要手动清缓存来测试效果,我用的是 RedisInsight,直接在界面里按前缀cache:shop:*搜索,比命令行方便很多。不过别在生产环境这么干,KEYS命令在大量 key 时会阻塞 Redis,应该用SCAN命令。项目里如果要开发缓存治理功能,最好封装一个统一的缓存管理接口,支持按前缀删除、按 key 删除,同时预留 TTL 修改能力。

如果你打算继续深挖这个项目,我建议接着做两件事:一是给商户查询加一个“附近商户”的 GEO 缓存,用 Redis GEO 命令处理坐标数据;二是把缓存代码抽成注解,比如自定义@Cacheable类似的逻辑,统一管理 key 和过期时间。这样黑马点评的缓存模块就算真正吃透了。

个人经验上,学缓存最容易犯的毛病是只背概念不看实现。你把穿透、击穿、雪崩背得再熟,不如真的开一个压测工具,把并发打上去,亲眼看看数据库发生了什么。黑马点评这个项目最大的好处就是给了你一个足够真实的业务场景,商户查询缓存这段代码值得反复敲、反复折腾,每次折腾都会对缓存一致性有更深的理解。

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

牛客网FED37数组反转:从双指针到前端数组操作的全面拆解

把牛客网FED37数组反转做明白之后&#xff0c;我才发现这道入门题背后串起了一长串前端基础知识点&#xff1a;数组的引用语义、原地修改和生成新数组的差别、时间复杂度与空间复杂度的取舍&#xff0c;以及怎么在在线评测环境下写出能被测试用例正确调用的函数。我把这道题交给…

作者头像 李华
网站建设 2026/10/7 4:46:16

深度强化学习求解无人机辅助旅行商问题:从建模到PyTorch实战

简介&#xff1a;这份PDF文献聚焦无人机与卡车协同配送的旅行商问题&#xff08;TSP-D&#xff09;&#xff0c;面向物流优化、最后一公里配送方案设计的研究人员与工程师。针对传统注意力模型难以协调异质车辆动作序列的痛点&#xff0c;作者提出融合注意力编码器与LSTM解码器…

作者头像 李华
网站建设 2026/10/7 4:46:04

VCS编译流程、许可证管理与Verdi联合调试实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 4:45:43

DeepSeek DSec解析:Agentic训练的弹性沙盒基础设施

最近大模型圈子里讨论最多的&#xff0c;除了各家开源模型的评测分数&#xff0c;就是“Agentic Training&#xff08;代理式训练&#xff09;”这个方向了。单纯靠静态的SFT数据堆出来的模型&#xff0c;在真实工具调用、代码执行这类场景里总差一口气。而DeepSeek这边放出的《…

作者头像 李华
网站建设 2026/10/7 4:45:36

CPO-BiTCN-BiGRU回归预测:MATLAB时序预测与超参数自动优化

做回归预测的朋友&#xff0c;对“优化算法深度学习模型”这种组合套路应该不陌生。这个项目标题是CPO-BiTCN-BiGRU回归预测&#xff0c;具体点说&#xff0c;就是用2024年提出的冠豪猪优化算法&#xff08;Crested Porcupine Optimizer&#xff0c;简称CPO&#xff09;去自动搜…

作者头像 李华
网站建设 2026/10/7 4:44:33

Python lambda 匿名函数:核心语法、实战场景与常见陷阱

1. 从一个排序需求说起&#xff1a;lambda 出现的理由用 Python 写代码写了这么多年&#xff0c;我越来越觉得 lambda 是个特别有意思的设计。很多刚入门的朋友看到lambda x: x[1]这种写法会觉得挺神秘&#xff0c;其实它就是一把小巧的折叠刀——平时用不着&#xff0c;但真到…

作者头像 李华