news 2026/10/1 23:18:18

SpringBoot整合Ehcache本地缓存:从配置到缓存一致性避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot整合Ehcache本地缓存:从配置到缓存一致性避坑指南

本地缓存这事,说小也小,说大也大。小到加个注解就完事,大到命中率上不去、缓存雪崩、数据不一致,哪一个都能把你折腾到加班。今天要聊的SpringBoot整合Ehcache本地缓存,就是我在多个项目里反复用、反复踩坑之后,沉淀下来的一套相对可靠的做法。这篇不端着讲理论,直接按项目落地的顺序走一遍,从依赖引入、配置文件写法、注解实操,到多环境下的取舍和排障,把你可能碰到的问题尽量都摊开来讲。

如果你是刚接触SpringBoot缓存的新手,这篇文章能让你少走很多弯路;如果你已经在项目里用过Redis或者Caffeine,想换一套真正符合JSR-107规范、支持堆外和磁盘持久化的本地缓存方案,那Ehcache正好合适。废话不多说,咱们从为什么需要本地缓存聊起。

1. 为什么本地缓存是绕不开的一环

1.1 先从一次接口性能优化说起

之前接手过一个订单查询服务,单表两三千万数据,查询条件乱七八糟,最夸张的时候一个列表接口要跑两秒多。刚开始我以为是SQL太烂,结果看执行计划,索引其实都用上了,瓶颈不在数据库,而在高频次的重复查询上。同一个用户一小时内反复查看同一个订单详情,请求参数一模一样,数据库却要实打实响应几十次。这种情况,靠优化SQL收效甚微,最直接的办法就是加本地缓存。

说白了,本地缓存就是进程内缓存,数据直接躺在JVM内存里,一次查询连网络IO都省了。对比一下:查一次数据库可能耗时50ms到200ms,查一次Redis也要走网络回路,大约1ms到5ms,而查一次本地缓存,就是一次内存读取,纳秒级,连0.01ms都用不到。性能差距不是一倍两倍,是几个数量级。所以不管你的项目用了多牛的数据库、多快的Redis,只要接口存在重复查询,本地缓存就是性价比最高的第一道防线。

1.2 本地缓存、分布式缓存与进程内缓存的边界感

很多同学分不清Ehcache、Caffeine和Redis之间的关系,其实一句话就能说清楚:Ehcache和Caffeine都是进程内缓存,活在应用自己的JVM里;Redis是分布式缓存,独立部署,多个应用节点共享同一份数据。分布式缓存的优势是数据全局一致,所有机器读到的是同一份,但劣势也很明显——再多一层网络开销,还多一个组件要运维。本地缓存没有网络开销,但每个机器节点各存各的,天然存在数据不一致的窗口期。

所以边界感很重要:读多写少、允许短时间不一致、单机数据量可控的数据,适合放本地缓存;强一致、跨节点共享、需要全局失效的数据,才应该放Redis或者直接查库。Ehcache在这个定位里的优势在于,它是JCache(JSR-107)标准实现,Spring Boot对它有一等公民的支持,同时还支持堆内、堆外、磁盘三级存储,这是Caffeine不具备的。Caffeine胜在极致性能和简单的API,但遇到需要几十GB缓存数据、超过堆内存容量的场景,Caffeine就无能为力了,而Ehcache可以把数据挪到堆外,甚至持久化到磁盘。

我还整理过一张对比表,放在团队内部做技术选型用,这里直接分享出来:

维度EhcacheCaffeineRedis
存储位置堆内/堆外/磁盘堆内独立进程/网络
访问延迟纳秒级纳秒级毫秒级
容量可扩至磁盘受JVM堆限制受机器内存限制
JSR-107支持原生需适配不支持
持久化支持磁盘不支持RDB/AOF
多实例共享不支持不支持支持
适合场景单机大数据量缓存单机小数据量高性能缓存多节点共享缓存

2. 环境准备:依赖引入与版本搭配

2.1 spring-boot-starter-cache 的引入

SpringBoot整合Ehcache的第一步,不是直接引Ehcache坐标,而是先引入Spring的缓存抽象层。Spring官方提供了一个叫做spring-boot-starter-cache的起步依赖,这个依赖本身不实现任何缓存逻辑,而是统一管理缓存管理器(CacheManager)的生命周期,顺便带上spring-context-support中关于缓存注解的能力。有了这层抽象,你在代码里写@Cacheable、@CacheEvict,底下的具体缓存实现是Ehcache还是Caffeine,随时可以切换。

Maven坐标很简单:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>

注意,这个starter不会自动帮你带出Ehcache。Spring Boot默认会优先在classpath里找缓存实现,如果啥都没找到,它就启用一个简单的ConcurrentMapCacheManager,也是能跑,但功能有限。想要用上Ehcache,你得单独再引Ehcache的依赖。

我见过不少同事在这里踩坑:只加了starter,没加Ehcache,然后在yml里写spring.cache.type=ehcache,结果启动报错,因为classpath里压根没有Ehcache的CacheManager实现类。所以第一步必须把Ehcache的坐标也加上。

2.2 版本选择:2.x还是3.x,别再搭错了车

Ehcache目前主流的版本是2.x和3.x,两者差距很大。2.x时代使用net.sf.ehcache包名,配置走ehcache.xml,里面有defaultCache、maxElementsInMemory这些老面孔;3.x时代改用org.ehcache包名,配置模型升级为CacheManagerBuilder,存储模型引入了堆外和磁盘资源池的概念。

Spring Boot 2.x之后,官方推荐使用的是Ehcache 3.x,因为Spring Boot自动配置类EhCacheCacheConfiguration默认适配的是org.ehcache包下的实现。如果非要用2.x,你得手动排除自动配置,再自己创建net.sf.ehcache.CacheManager,操作繁琐,也没有必要。我见过一个老项目卡在Spring Boot 1.5时代,强行用了Ehcache 2.10,后面升级Boot版本差点把缓存层重写。

在这里我建议直接锁定3.x。我实测比较稳的搭配是:Spring Boot 2.7.x对应Ehcache 3.9.7以上,Spring Boot 3.x对应Ehcache 3.10.8以上。3.10.x对javax.cache与jakarta.cache的兼容性都做了适配,跨Boot大版本时不用太纠结。

2.3 一个最小可用配置长什么样

引入依赖和基本配置的顺序我放在一起说。完整依赖如下:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> <version>3.10.8</version> </dependency>

然后在application.yml里声明缓存类型和配置文件位置:

spring: cache: type: ehcache ehcache: config: classpath:ehcache.xml

接着在启动类上加上@EnableCaching,这是激活Spring缓存注解的开关,很多人漏掉这一步,结果注解写了一大堆,程序跑起来毫无反应。加上之后,Spring Boot会自动检测到org.ehcache的CacheManager,并用你指定的ehcache.xml初始化。到这里,本地缓存的主干已经立起来了,接下来就是把配置写明白。

3. 核心配置拆解:heap、offheap与disk三级存储

3.1 三级存储模型详解

既然用Ehcache 3.x,就必须理解它的存储模型。Ehcache把存储划分为三层:堆内(heap)、堆外(offheap)和磁盘(disk)。写入缓存时,数据默认先进堆内,堆内满了之后按淘汰策略往堆外挪,堆外也满了再往磁盘挪。读取时则是反过来,优先从最快的堆内找,找不到再查堆外,最后才是磁盘。

这三层的性能和容量关系可以用一句话概括:越靠近CPU,速度越快,但容量越小。堆内就是你的JVM堆内存,受-Xmx限制,而且频繁存储大对象会加剧GC压力;堆外是DirectByteBuffer管理的内存,不受堆大小限制,不参与GC扫描,容量可以做得很大,但读写速度比堆内慢一个档次;磁盘就是普通文件存储,适合数据量特别大、但访问频率不高的场景。

一个典型的ehcache.xml配置是这样的:

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.ehcache.org/v3" xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.10.xsd"> <cache alias="userCache"> <key-type>java.lang.String</key-type> <value-type>com.example.User</value-type> <expiry> <ttl unit="seconds">300</ttl> </expiry> <resources> <heap unit="entries">1000</heap> <offheap unit="MB">64</offheap> <disk unit="MB">256</disk> </resources> </cache> </config>

这里heap的单位是entries,意思是最多保存1000个条目;offheap和disk的单位是MB,意思是可用空间上限。key-type和value-type建议显式声明,既方便Ehcache做序列化,也能在类型不匹配时提前报错,而不是等到运行时莫名其妙抛异常。

3.2 怎么定heap和offheap的大小

配置缓存容量的时候,最常被问的问题是:堆内到底开多大合适?这个问题没有标准答案,但有一个经验法则:堆内大小取决于你最热的数据量,而不是全部数据量。假设用户信息表有一千万行,但一个小时内活跃用户可能只有一万人,那堆内缓存撑死存一万个对象就够了。真正的大头冷数据交给堆外去扛。

我一般这么估算:先统计缓存对象的平均字节大小,然后计算QPS中最热的那个key集合的条目数,堆内条目数设置为热点条目的两倍左右,留出余量。堆外内存则参考JVM最大堆内存的三分之一以内,因为堆外内存不像堆内那样受GC控制,如果设置过大,反而会和堆内内存抢占物理内存,造成整体性能下降。磁盘容量一般比堆外再大几倍,当作冷数据的最终兜底。

另外要注意,<heap unit="entries">和<heap unit="MB">写哪个好?如果缓存对象体积不稳定,建议用MB做人维度控制;如果对象体积相对均匀,用entries更直观,因为Ehcache的淘汰是用LFU近似算法做条目淘汰,你控制条目数就等于控制了淘汰粒度。

3.3 TTL与TTI:两种生命周期的本质区别

Ehcache配置里有两个和时间相关的参数,很多人搞混:timeToLiveSeconds(TTL)和timeToIdleSeconds(TTI)。TTL的意思是缓存条目从创建时间开始计时,到了设定秒数就强制过期,不管这段时间内有没有被访问过;TTI的意思是从最后一次访问开始计时,如果在设定时间内没有被访问,就让它过期。

用生活场景来类比:TTL就像面包上的保质期,到点就过期,哪怕今天没人买也是这个点;TTI更像会员卡,你老不来消费,过段时间就被注销,但只要你不断续费(访问),就能一直用下去。

在ehcache.xml里的写法是:

<expiry> <ttl unit="seconds">300</ttl> </expiry>

或:

<expiry> <tti unit="seconds">600</tti> </expiry>

两者可以同时配置吗?Ehcache控制台会检测冲突,实际上同一条缓存里TTL和TTI只能二选一,因为它们的计算起点不同。我个人的选择是:对数据实时性要求高的配置类数据用TTL,比如系统参数十分钟刷新一次;对访问习惯性强的业务数据用TTI,比如用户会话信息,一小时没访问就失效,但用户不停访问就一直续期。另外,eternal这个古老配置项表示永久有效,建议不要轻易使用,数据只进不出是会出大问题的。

4. 注解实操:@Cacheable 的正确使用姿势

4.1 开启缓存与验证是否生效

配置再完美,注解不启用也白搭。启动类上加上@EnableCaching之后,Spring就会在运行时为目标方法创建代理,在方法执行前先查缓存,命中就直接返回缓存结果,不再进入方法体。这一层本质上是AOP,理解了AOP你才能理解后面很多坑。

验证缓存是否生效的最快办法:在方法里加一行打印日志,然后连续调用两次接口。第一次执行方法体,打印日志;第二次不再打印,说明缓存已经命中。如果你两次都看到日志,别急着怀疑配置,先检查启动类有没有@EnableCaching,再看application.yml里有没有把spring.cache.type写对。

4.2 三个关键参数:cacheNames、key、condition/unless

常用的@Cacheable有三个关键参数。第一个是cacheNames,对应你ehcache.xml里配置的缓存区域名称。比如ehcache.xml里定义了一个userCache区域,代码里就得写@Cacheable(cacheNames = "userCache"),两边名字对不上,Spring会直接创建默认容器,无法享受你预设的内存和过期策略。

第二个是key,负责生成缓存的索引。Spring默认的key生成策略是使用方法参数,如果方法有多个参数,就用SimpleKey组合起来。但多参数模型的key可读性差,而且容易出奇奇怪怪的拼接,我建议你总是显式声明:

@Cacheable(cacheNames = "userCache", key = "#userId") public User getUserById(Long userId) { return userMapper.selectById(userId); }

如果你要根据多个字段生成key,用SpEL表达式拼起来就行:

@Cacheable(cacheNames = "userCache", key = "#tenantId + ':' + #userId") public User getUser(Long tenantId, Long userId) { return userMapper.select(tenantId, userId); }

第三个组合是condition和unless。condition在方法执行前判断,满足条件的请求才走缓存;unless在方法执行后根据返回值判断,满足条件的返回值不缓存。比如订单状态只有已支付才允许缓存:

@Cacheable(cacheNames = "orderCache", key = "#orderId", unless = "#result == null") public Order getOrderById(Long orderId) { return orderMapper.selectById(orderId); }

注意unless里用#result引用返回值,这个选项特别适合过滤空值或者异常占位对象。对于查不到数据的情况,我建议主动做空值缓存,防止缓存穿透——也就是把null也存进去,设置较短的TTL。这种做法对高并发下防止无效请求穿透到数据库非常有效。

4.3 缓存更新与删除:@CachePut 和 @CacheEvict

读操作有了@Cacheable,写操作就要配合@CachePut和@CacheEvict。@CachePut的作用是执行方法体,然后把返回值强制写到缓存里,适用于更新数据时同步刷新缓存:

@CachePut(cacheNames = "userCache", key = "#user.userId") public User updateUser(User user) { userMapper.updateById(user); return user; }

@CacheEvict则是执行完方法后删除缓存条目,适用于删除数据时清掉旧缓存。它有两个常用属性:allEntries表示清空整个cacheNames区域的所有条目,beforeInvocation表示在方法执行前就删缓存,防止方法中途抛异常导致缓存与数据库不一致。

有一个细节值得注意:@CachePut方法运行时如果缓存里已经有旧值,新值会覆盖旧值,但如果此时有其他线程正在读旧值,就可能出现短时间的脏读。想要彻底安全,你就得引入分布式锁或者版本号机制,但那样复杂度会上升不少。对一个允许短时不一致的业务系统来说,@CachePut已经够用了。

4.4 同类内部调用为何失效

使用注解缓存的过程中,最普遍的坑就是同类内部调用导致缓存失效。比如UserService里有一个公开方法getUserById,它在内部又调用了本类的另一个方法getUserDetail,你给getUserDetail加了@Cacheable,结果完全没有缓存效果。

原因很简单:Spring缓存注解是基于动态代理的,代理类拦得到外部调用,但内部this调用直接调到了原始对象上,根本没经过代理。解决办法有三个:把需要缓存的方法拆到另一个Service类里,通过注入调用;或者自己注入CacheManager,手动getCache("xxx").put();再或者用@Resource注入自身的代理对象(配合@Lazy防止循环依赖)。

我个人最推荐第一个方案,因为拆类自然、可读性好,而且代理链路清晰。手动操作缓存的方案适合在不方便拆类的场景下做兜底。

5. 缓存一致性与多实例部署下的取舍

5.1 与数据库的一致性同步问题

本地缓存与数据库的一致性,是很多人掉进去的坑。你的业务数据更新了,数据库已经写入,但缓存里还是旧值,用户刷新页面看到的依然是老数据。解决思路主要有三种:先更新数据库,再主动@CacheEvict删除缓存;先更新数据库,再用@CachePut刷新缓存;或者设置一个很短TTL,让数据自然过期。

这三种方案里,主动删除缓存其实是更安全的做法,为什么呢?因为更新操作对缓存的命中率影响很大,如果你用@CachePut强制更新,更新的数据可能不是热点数据,白白浪费一次写入;而@CacheEvict只是删掉缓存,下次读请求到来时再回源加载,逻辑最简单,也最能保证最终一致。真正比较高频、同时读写比例悬殊的场景下,我才会用@CachePut去主动刷新。

还有一个极容易翻车的情况:缓存更新在事务里。假设你的事务很复杂,有一个使用方法:

@Transactional public void updateUserAndOrder(User user) { userService.update(user); orderService.update(user); }

方法里某个@CacheEvict在你还没提交事务时就已经执行了。此时如果事务因为后面的操作异常回滚,数据库并没有真正更新,但缓存已经被删掉。下一个查询线程就会回源数据库,而数据库数据没变,反而把旧值重新加载进缓存,最终数据与缓存还是一致的。这里真正危险的是@CachePut:事务回滚了,但缓存却被写入了新值(已经回滚的假数据),数据库还是旧值,二者就不一致了。

所以,在事务方法里执行缓存操作时,我建议把缓存更新动作放到事务提交后,利用TransactionSynchronizationManager.registerSynchronization:

TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCommit() { cacheManager.getCache("userCache").evict(userId); } });

这种写法虽然啰嗦,但能从根本上避免事务回滚导致缓存更新的问题。

5.2 多实例部署下的数据一致性

本地缓存永远绕不开一个问题:多实例部署时,每个节点各自为政。你有两台应用服务器,用户请求打到A节点时,A的本地缓存里有数据;请求转移到B节点,B的缓存里是空的,还得查库。更糟的是,A更新了数据库并清掉自己缓存,B节点并不知道,如果之后请求落到了B,B可能继续返回旧缓存。

很多团队用本地缓存踩坑之后,又推倒重来全面转向Redis,就是被这个问题搞怕了。其实对一致性要求不高的业务,本地缓存加上一个极短的TTL,比如30到60秒,就能在不引入额外组件的情况下解决大部分问题:即使某节点缓存未失效,最多多等几十秒也就过期了。对一致性有硬性要求的场景,建议直接放弃本地缓存,改用Redis。

如果你一定要在多实例下用本地缓存,但又想尽量更新及时,可以走发布订阅的思路:数据库更新成功后,向Redis的某个Channel发一条通知,所有应用节点订阅该Channel,收到通知后各自清掉对应的本地缓存。这个方案比直接改用Redis做缓存要轻量很多,但实现复杂度确实不低。

6. 监控、命中率与常见坑位排查实录

6.1 如何拿到命中率和缓存统计

本地缓存上线后,绝对不能不管。我一直跟团队说:没有监控的缓存就是盲盒,你根本不知道命中率是高是低。Ehcache 3内置了Statistics接口,可以通过CacheManager拿到:

CacheManager cacheManager = ...; Cache<String, User> cache = cacheManager.getCache("userCache", String.class, User.class); Statistics stats = cache.getRuntimeStatistics(); System.out.println("missCount = " + stats.getCacheMissCount()); System.out.println("hitCount = " + stats.getCacheHitCount());

从这些指标可以算出命中率。命中率低于50%的缓存配置意义就不大了,你需要审视是不是key设计不合理、TTL太短、每次缓存的都是冷数据。我之前有一个缓存区域命中率长期在30%徘徊,一查发现是key生成器默认拼接了几个无关参数,导致同一个业务key分裂成几十个缓存条目,命中率自然上不去。

6.2 排查实录:四个高频异常与解决方案

整理一下我在实际项目里碰到过的几类问题,一条条来说。

第一类:缓存疑似不生效,日志里看不到任何缓存提示。优先排查顺序是:启动类有没有@EnableCaching,Spring容器里现在到底用的是哪个CacheManager,是不是有自己定义的Bean把默认的覆盖了。项目中如果有多个CacheManager,Spring会根据@Primary或者名字来决定注解用谁,一旦配错,注解就会静默失效。

第二类:堆内内存溢出或频繁Full GC。这种问题往往是因为把整个大表都缓存到了堆内,上千万元素,对象体积还不小。我经历过一次服务频繁GC,排查到最后就是缓存条目过多。解决方式:把大区域改为堆外存储,设置合理的offheap大小,堆内只留热点数据。这样GC压力会明显下降。

第三类:序列化异常或者磁盘缓存恢复乱码。一旦使用磁盘持久化,所有缓存对象必须可序列化。如果你往磁盘缓存里存放了一个没实现Serializable接口的对象,运行时会直接抛ClassNotFoundException或SerializationException。排查思路:把所有value-type指定的对象都检查一遍,确保继承Serializable;另外,磁盘持久化的文件如果被手动删除,重启时也可能报Diskstore corruption,解决方式是临时在配置里禁用disk,让Ehcache重建。

第四类:CacheManager的Bean冲突。Spring Boot里如果你自定义了CacheManager,且没有加@Primary,多个CacheManager存在时会报No CacheResolver或NoUniqueBeanDefinitionException。标准做法是给默认的缓存管理器加@Primary,然后其他场景通过@Cacheable(cacheManager = "secondaryCacheManager")指定。

6.3 避免缓存击穿、穿透和雪崩

本地缓存一样逃不开三大经典问题:缓存穿透、击穿和雪崩。

穿透是查一个肯定不存在的数据,缓存里没有,数据库里也没有,每次请求都会穿透到数据库。处理方式前面提过,做空值缓存,把null也放进缓存,设置一个比正常数据短一些的TTL,比如30秒。

击穿是某个热点key过期的一瞬间,大量请求同时发现缓存没有,全部打到数据库。处理方式是在@Cacheable上加上sync = true,让并发请求只有一个线程去回源加载,其他线程阻塞等待。这个属性特别重要,热点数据一定要开。

雪崩是大量key在同一时间段集体过期,导致数据库瞬时压力暴增。处理方式有两种思路:TTL后面加一个随机数,让过期时间分散;或者检查业务特性,把某些缓存区域的TTL从固定改为动态。我在配置用户详情缓存时,会给TTL加上一个基于userId哈希的偏移量,这样就自动错开了过期高峰。

7. 基于实战的几点体会

写了这么多,最后分享一点个人感悟。本地缓存用得好不好,往往能看出一个开发者对业务读写模型的理解深不深。我见过很多人不管什么数据都往缓存里塞,最后内存吃紧、命中率低下,又反过来骂缓存没用。真正的做法是先判断数据特征:是否高频读、是否写少、是否允许最终一致、单机容量是否可控。四个条件都满足,才值得上本地缓存。

Ehcache在本地缓存里的位置,我觉得正好是“介于Caffeine和Redis之间的中间派”:它比Caffeine成熟,有完整的持久化能力,又比Redis更贴身、更快。用在单机应用、内部管理系统、垂直拆分后的某个服务节点上,它都能发挥很大作用。但也要清醒地知道,它的数据一致性和跨节点共享能力天然偏弱,该用Redis的时候别硬扛。

最后再给一个建议:新项目里可以直接基于Spring Boot Cache抽象层写代码,配置上先用Ehcache,以后再按需切Redis。缓存类型只改一个配置项,代码里的注解不用大动,这是Spring缓存抽象层最大的价值。希望这篇实操笔记能让你少踩几个坑,也欢迎你有更刁钻的本地缓存问题,咱们评论区继续聊。

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

昇思MindSpore大模型训练评估体系与性能优化实战指南

1. 大模型训练为什么需要一套靠谱的评估体系 1.1 从“跑起来”到“跑得稳”的认知转变 很多人第一次接触昇思 MindSpore 做模型训练&#xff0c;关注点几乎都落在“能不能跑通”上&#xff1a;环境装好、脚本拉起、loss 开始往下掉&#xff0c;就觉得大功告成。但真正把模型推…

作者头像 李华
网站建设 2026/10/1 23:17:50

Wine兼容层在国产系统中的适配与乱码问题解析

我无法根据当前输入生成符合要求的博文。原因如下&#xff1a;项目标题 "Madeira"是一个地理名称&#xff08;葡萄牙马德拉群岛&#xff09;&#xff0c;也常指代该地区著名的加强型葡萄酒&#xff08;Madeira Wine&#xff09;&#xff0c;但在提供的输入中&#xf…

作者头像 李华
网站建设 2026/10/1 23:16:54

音乐厅订票系统全链路实战:Spring Boot+Vue+MyBatis+MySQL架构解析

做一个项目之前&#xff0c;我习惯先把技术栈的风险清单拉出来过一遍&#xff0c;越早暴露越好。今天要聊的这个音乐厅订票系统&#xff0c;在技术上其实没有特别新鲜的亮点&#xff0c;它的价值在于"全链路"——从用户注册、场次查询、选座、下单支付、出票到后台管…

作者头像 李华
网站建设 2026/10/1 23:16:32

牛客每日一题+Tracker刷题复盘:乐团派对贪心思路全解析

前阵子刷牛客的每日一题&#xff0c;正好碰到一道叫“乐团派对”的题&#xff0c;加上我一直用自己搭的一套 tracker 在做刷题记录&#xff0c;那次就顺手把整个过程完整复盘了一遍&#xff1a;从最初读题时想当然&#xff0c;到后面把解法、证明、边界条件都理清楚&#xff0c…

作者头像 李华
网站建设 2026/10/1 23:16:07

VulnHub靶机Bulldog完整渗透实战:从信息收集到Root提权

靶机渗透这个圈子&#xff0c;玩到一定阶段都会有个感觉&#xff1a;光是看writeup、刷题库&#xff0c;不如老老实实拿一个靶机从信息收集打到提权&#xff0c;整个链路走一遍&#xff0c;比什么都长记性。最近我重新把VulnHub上的Bulldog拖出来打了一遍&#xff0c;这靶机难度…

作者头像 李华
网站建设 2026/10/1 23:11:28

生产者-消费者模式与并行任务调度:从BlockingQueue到虚拟线程的工程实践

我想先把这次做的东西说清楚——这个项目围绕的是生产者-消费者模式、并行任务调度&#xff0c;以及一个经常被忽略的细节&#xff1a;更简洁的注释和每项改进的详细解释。我自己维护过一套高吞吐的通知推送组件&#xff0c;早期代码就是“能跑就行”的水平&#xff0c;队列选型…

作者头像 李华