SpringBoot整合Redis缓存实战:从设计思路到避坑指南
1. 为什么要在SpringBoot里动缓存这块蛋糕做后端开发的朋友大概率都经历过这样的场景项目初期跑得飞快接口响应基本在几十毫秒以内可一旦用户量上来、查询逻辑变复杂数据库就开始喘粗气。尤其是那些“读多写少”的数据——比如商品分类、配置项、用户基础信息——每次请求都去数据库捞一遍纯属浪费资源。这时候缓存就该上场了。SpringBoot本身提供了一套缓存抽象机制通过几个注解就能把缓存逻辑织入业务代码而Redis作为分布式缓存的事实标准自然成了最顺手的搭档。这套组合能解决的核心问题有三个第一降低数据库压力把高频读取的数据放到内存里第二提升接口响应速度Redis的读写性能在十万级QPS以上第三在分布式环境下保证缓存的一致性多个服务实例共享同一份缓存数据。这篇文章适合已经写过SpringBoot基础项目、对Redis有初步了解、但还没把两者真正整合起来的开发者。我会从设计思路讲到代码落地把参数配置、序列化选型、缓存穿透处理这些容易踩坑的地方全部拆开揉碎。你不需要事先精通Redis但至少要能跑起来一个SpringBoot工程并且本地或远程有一台Redis服务可用。我自己的习惯是在动手写代码之前先把“为什么要这么设计”想清楚否则后面遇到问题只能靠猜。缓存集成这件事表面上是加几个注解实际上涉及序列化协议、过期策略、键名规范、异常降级等一系列决策。下面我就按实际项目中的推进顺序一步步展开。2. 整体设计思路与方案选型拆解2.1 为什么选Redis而不是其他缓存方案SpringBoot的缓存抽象支持多种实现包括ConcurrentHashMap、EhCache、Caffeine、Redis等。本地缓存如Caffeine速度最快但存在两个致命问题一是多实例部署时数据不一致二是容量受限于单机内存。Redis作为集中式缓存天然支持分布式场景虽然多了一次网络开销但换来的是数据一致性和可扩展性。在实际选型时我通常会这样判断如果数据量小、变更频率低、且服务是单实例部署用Caffeine就够了但只要涉及多实例或者数据量可能增长直接上Redis省得后期迁移。Redis还支持丰富的数据结构、持久化、过期策略这些特性在缓存场景里非常实用。2.2 SpringBoot缓存抽象的工作机制SpringBoot的缓存抽象核心是CacheManager和Cache两个接口。CacheManager负责管理缓存区域Cache则定义了具体的存取操作。当我们使用Cacheable、CachePut、CacheEvict这些注解时Spring会通过AOP拦截方法调用根据注解参数决定是查缓存、更新缓存还是清除缓存。默认情况下SpringBoot会自动配置一个ConcurrentMapCacheManager也就是用ConcurrentHashMap做缓存。要换成Redis只需要引入spring-boot-starter-data-redis依赖并在配置类中声明一个RedisCacheManager的Bean。SpringBoot的自动配置机制会检测到Redis相关的类存在从而优先使用Redis作为缓存实现。这里有个细节值得注意SpringBoot 2.x之后Redis的客户端从Jedis换成了Lettuce原因是Lettuce基于Netty支持异步和响应式编程且在多线程环境下性能更好。如果你习惯用Jedis也可以手动排除Lettuce再引入Jedis但除非有特殊需求否则建议直接用Lettuce。2.3 序列化方案的选择逻辑缓存数据最终要存到Redis里而Java对象不能直接存储必须经过序列化。SpringBoot默认使用JDK序列化也就是JdkSerializationRedisSerializer。这种方式的问题很明显序列化后的字节数组体积大、可读性差而且要求被序列化的类必须实现Serializable接口。更麻烦的是不同版本的类序列化后可能不兼容。我在实际项目中一般会换成JSON序列化用GenericJackson2JsonRedisSerializer或者Jackson2JsonRedisSerializer。JSON的好处是跨语言、可读、体积小。但要注意JSON序列化默认不保存类型信息反序列化时可能得到LinkedHashMap而不是原始对象。解决办法是在序列化器中开启类型信息或者指定具体的类型。还有一种选择是StringRedisSerializer适合键和值都是字符串的场景。通常我会把键用StringRedisSerializer值用JSON序列化器这样在Redis客户端里能直接看懂键名排查问题方便很多。2.4 缓存注解的语义与适用场景SpringBoot提供了四个核心缓存注解理解它们的语义是正确使用的前提Cacheable先查缓存命中则直接返回未命中则执行方法并将结果写入缓存。适用于查询操作。CachePut无论缓存是否存在都执行方法并将结果写入缓存。适用于更新操作保证缓存与数据库同步。CacheEvict执行方法后清除指定缓存。适用于删除操作或者更新时直接让旧缓存失效。Caching组合多个缓存操作比如同时清除多个缓存区域。这些注解都支持key、condition、unless等属性可以精细控制缓存行为。比如condition在方法执行前判断unless在方法执行后判断两者结合可以避免缓存空值或异常结果。注意Cacheable标注的方法在同一个类内部被调用时缓存注解不会生效。因为Spring的缓存是基于代理的内部调用绕过了代理对象。解决办法是把缓存方法抽到另一个Bean中或者注入自身代理。3. 核心配置与实操要点详解3.1 依赖引入与版本对齐第一步是在pom.xml中引入必要的依赖。核心依赖是spring-boot-starter-data-redis它包含了Lettuce客户端和Spring Data Redis。如果要用连接池还需要commons-pool2。另外缓存抽象本身在spring-boot-starter-cache中不过spring-boot-starter-data-redis已经传递依赖了它所以通常不需要单独引入。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependency版本方面SpringBoot 2.7.x和3.x在配置上有一些差异。3.x要求Java 17以上且部分配置项名称有调整。我建议新项目直接用3.x老项目如果升级成本高2.7.x也足够稳定。关键是保持SpringBoot版本和Spring Data Redis版本一致避免出现兼容性问题。3.2 application.yml中的关键参数配置文件里的参数直接决定了连接行为和性能表现。下面是我常用的配置模板spring: redis: host: 127.0.0.1 port: 6379 password: your_password database: 0 timeout: 3000ms lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2 max-wait: 2000ms cache: type: redis redis: time-to-live: 3600000 cache-null-values: false key-prefix: myapp:cache: use-key-prefix: true这里有几个参数需要重点解释。time-to-live是缓存的默认过期时间单位毫秒上面配置的是1小时。cache-null-values设为false表示不缓存空值这能防止缓存穿透但也意味着每次查询不存在的数据都会打到数据库。key-prefix给所有缓存键加统一前缀方便在Redis里区分不同应用的键。连接池参数中max-active是最大连接数max-idle是最大空闲连接数min-idle是最小空闲连接数。这些值需要根据实际并发量调整。我一般会先设一个保守值然后通过监控观察连接使用情况再优化。max-wait是获取连接的最大等待时间设为-1表示无限等待但生产环境不建议这么做容易导致线程堆积。3.3 RedisTemplate与CacheManager的配置虽然SpringBoot会自动配置RedisTemplate和RedisCacheManager但默认的序列化方式往往不满足需求。我通常会写一个配置类自定义序列化器和缓存管理器。Configuration EnableCaching public class RedisCacheConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); StringRedisSerializer stringSerializer new StringRedisSerializer(); GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofHours(1)) .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())) .disableCachingNullValues() .prefixCacheNameWith(myapp:cache:); return RedisCacheManager.builder(factory) .cacheDefaults(config) .transactionAware() .build(); } }EnableCaching注解必须加上否则缓存注解不会生效。transactionAware()方法让缓存操作与Spring事务同步在事务提交后才真正写入缓存避免事务回滚导致缓存脏数据。GenericJackson2JsonRedisSerializer会在JSON中保存类的全限定名反序列化时能还原成原始类型。但它要求类有无参构造函数且对泛型集合的支持需要额外处理。如果遇到反序列化问题可以改用Jackson2JsonRedisSerializer并指定具体类型。3.4 键名生成策略与SpEL表达式缓存键的生成直接影响可维护性。默认的键生成器是SimpleKeyGenerator它根据方法参数生成键。比如方法getUserById(Long id)默认键就是id的值。但这样容易冲突不同方法的相同参数值会生成相同的键。我通常会用SpEL表达式显式指定键比如Cacheable(value user, key user: #id) public User getUserById(Long id) { return userMapper.selectById(id); }这样生成的键就是myapp:cache:user:user:123前缀来自配置user是缓存区域名后面的user:123是SpEL表达式的结果。虽然看起来有点冗余但结构清晰排查问题时一眼就能看出是哪个方法的缓存。SpEL表达式还支持条件判断比如key #id 0 ? #id : default。不过键名里尽量不要包含特殊字符避免在Redis客户端里操作时出现转义问题。提示如果方法有多个参数且不想手动指定键可以实现KeyGenerator接口自定义生成逻辑。但更推荐显式指定因为隐式规则在代码重构时容易出问题。4. 完整实操流程与核心环节实现4.1 项目结构与基础准备假设我们有一个用户服务需要缓存用户信息和用户列表。项目结构大致如下src/main/java/com/example/demo/ ├── config/ │ └── RedisCacheConfig.java ├── controller/ │ └── UserController.java ├── service/ │ ├── UserService.java │ └── impl/ │ └── UserServiceImpl.java ├── mapper/ │ └── UserMapper.java └── entity/ └── User.javaUser实体需要实现Serializable接口虽然用JSON序列化时不强制要求但养成习惯没坏处。实体类中不要有循环引用否则JSON序列化会栈溢出。public class User implements Serializable { private static final long serialVersionUID 1L; private Long id; private String name; private String email; private Integer age; // getter、setter、构造方法省略 }4.2 缓存注解在Service层的落地Service层是缓存注解的主要战场。下面是一个典型的用户服务实现Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Override Cacheable(value user, key user: #id, unless #result null) public User getUserById(Long id) { return userMapper.selectById(id); } Override CachePut(value user, key user: #user.id) public User updateUser(User user) { userMapper.updateById(user); return user; } Override CacheEvict(value user, key user: #id) public void deleteUser(Long id) { userMapper.deleteById(id); } Override Cacheable(value userList, key page: #pageNum : #pageSize) public ListUser listUsers(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); return userMapper.selectList(null); } }getUserById方法上unless #result null表示如果查询结果为null就不写入缓存。这比全局的disableCachingNullValues()更灵活因为有些方法可能允许缓存空值。updateUser用CachePut方法执行后会把返回的User对象写入缓存。注意键的表达式用的是#user.id因为参数是User对象。如果id为null键会变成user:null这显然不对所以实际项目中要加校验。deleteUser用CacheEvict方法执行后清除对应键的缓存。默认是在方法执行后清除如果方法抛异常缓存不会清除。可以用beforeInvocation true改为方法执行前清除但这样即使方法失败缓存也没了需要根据业务权衡。listUsers缓存分页结果键包含页码和每页大小。这种缓存要注意数据变更时的失效问题通常会在新增或删除用户时清除整个userList区域。4.3 缓存更新策略的取舍缓存更新是分布式系统里最头疼的问题之一。常见的策略有三种策略操作方式优点缺点先更新数据库再更新缓存写库后CachePut缓存实时性好并发写时可能覆盖且更新缓存开销大先更新数据库再删除缓存写库后CacheEvict简单可靠避免脏数据下次读取时有缓存未命中开销先删除缓存再更新数据库写库前CacheEvict减少脏数据窗口并发读可能把旧数据写回缓存我一般选择“先更新数据库再删除缓存”也就是CacheEvict。理由是删除操作比更新操作简单不容易出错而且下次读取时会从数据库加载最新数据。虽然有一次缓存未命中的开销但对于大多数场景是可以接受的。如果业务对缓存命中率要求极高可以考虑延迟双删更新数据库后删除缓存隔几百毫秒再删一次。但这会引入额外的复杂度和不确定性除非有明确需求否则不建议。4.4 手动操作RedisTemplate的场景注解虽然方便但有些场景需要更细粒度的控制比如缓存预热、批量操作、设置不同的过期时间。这时候就需要直接使用RedisTemplate。Service public class CacheWarmUpService { Autowired private RedisTemplateString, Object redisTemplate; Autowired private UserMapper userMapper; public void warmUpUserCache() { ListUser users userMapper.selectList(null); for (User user : users) { String key myapp:cache:user:user: user.getId(); redisTemplate.opsForValue().set(key, user, Duration.ofHours(2)); } } public void batchEvictUserCache(ListLong userIds) { ListString keys userIds.stream() .map(id - myapp:cache:user:user: id) .collect(Collectors.toList()); redisTemplate.delete(keys); } }缓存预热通常在系统启动后或定时任务中执行把热点数据提前加载到Redis。批量清除用delete(CollectionString keys)方法比循环单个删除效率高得多。设置过期时间时Duration.ofHours(2)表示2小时。如果不设置过期时间缓存会一直存在直到内存满了被淘汰。生产环境一定要设置合理的过期时间避免内存无限增长。4.5 缓存穿透、击穿、雪崩的应对这三个问题是缓存领域的经典难题必须提前防范。缓存穿透是指查询一个不存在的数据缓存和数据库都没有导致每次请求都打到数据库。解决办法有两种一是缓存空值但设置较短的过期时间二是用布隆过滤器提前拦截。我一般用缓存空值简单有效。Cacheable(value user, key user: #id, unless #result null) public User getUserById(Long id) { User user userMapper.selectById(id); if (user null) { // 缓存空值过期时间设短一些 redisTemplate.opsForValue().set(myapp:cache:user:user: id, new NullValue(), Duration.ofMinutes(5)); } return user; }缓存击穿是指某个热点键过期瞬间大量请求同时打到数据库。解决办法是加互斥锁只让一个线程去加载数据其他线程等待。public User getUserWithLock(Long id) { String key myapp:cache:user:user: id; User user (User) redisTemplate.opsForValue().get(key); if (user ! null) { return user; } String lockKey lock:user: id; Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofSeconds(10)); if (Boolean.TRUE.equals(locked)) { try { user userMapper.selectById(id); if (user ! null) { redisTemplate.opsForValue().set(key, user, Duration.ofHours(1)); } return user; } finally { redisTemplate.delete(lockKey); } } else { // 等待后重试 try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return getUserWithLock(id); } }缓存雪崩是指大量缓存同时过期导致数据库压力骤增。解决办法是给过期时间加随机值避免同时失效。long ttl 3600 new Random().nextInt(600); redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(ttl));这样过期时间在1小时到1小时10分钟之间随机分布大大降低了同时失效的概率。5. 常见问题排查与避坑经验实录5.1 缓存注解不生效的几种原因这是新手最容易遇到的问题。我总结了几种常见情况第一忘记加EnableCaching注解。这个注解可以加在启动类上也可以加在配置类上但必须要有。第二方法不是public的。Spring的AOP代理只能拦截public方法private、protected、包级私有方法上的缓存注解会被忽略。第三同类内部调用。前面提过this.method()调用不会经过代理缓存注解失效。解决办法是注入自身或者把方法抽到另一个Bean。第四缓存管理器配置错误。如果自定义了CacheManager但Bean名称不对或者序列化器配置有问题可能导致缓存无法正常工作。可以通过日志观察是否有异常抛出。第五返回值类型不匹配。比如缓存的是ListUser但反序列化时变成了ListLinkedHashMap导致类型转换异常。这时候需要检查序列化器是否保存了类型信息。5.2 序列化异常与类型转换问题JSON序列化最常见的问题是反序列化后类型丢失。比如缓存一个User对象取出来变成LinkedHashMap强转时报ClassCastException。原因是GenericJackson2JsonRedisSerializer虽然会保存类型信息但如果类路径发生变化或者类没有无参构造函数反序列化就会失败。解决办法有三个一是确保类有无参构造函数二是用Jackson2JsonRedisSerializer并指定具体类型三是自定义ObjectMapper开启activateDefaultTyping。ObjectMapper mapper new ObjectMapper(); mapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY ); GenericJackson2JsonRedisSerializer serializer new GenericJackson2JsonRedisSerializer(mapper);另外LocalDateTime等Java 8时间类型的序列化需要额外注册JavaTimeModule否则会报InvalidDefinitionException。5.3 缓存与数据库数据不一致的排查数据不一致通常有几个来源一是更新数据库后缓存删除失败二是并发读写导致旧数据写回缓存三是缓存过期时间设置不合理。排查时可以先在Redis客户端里直接查看键的值对比数据库里的数据。如果发现缓存是旧数据检查更新逻辑是否执行了CacheEvict以及键的表达式是否匹配。如果键不匹配删除操作就清了个寂寞。还有一种情况是事务未提交时缓存已更新。比如在事务方法里调用CachePut缓存写入可能早于事务提交如果事务回滚缓存就成了脏数据。解决办法是用transactionAware()让缓存操作与事务同步。注意CacheEvict的beforeInvocation属性默认是false即方法执行后清除。如果方法抛异常缓存不会清除。在某些场景下这反而是好事因为数据没更新成功缓存也不该清除。5.4 连接池耗尽与超时问题生产环境偶尔会遇到RedisConnectionFailureException或者PoolExhaustedException通常是连接池配置不合理导致的。排查步骤检查max-active是否太小。如果并发请求数超过最大连接数后来的请求会等待等待超时后就抛异常。检查是否有连接泄漏。比如手动获取连接后没有归还或者使用了RedisTemplate但没有正确关闭。检查Redis服务端是否正常。用redis-cli ping测试连通性用info clients查看当前连接数。检查网络延迟。如果Redis和应用不在同一机房网络抖动可能导致超时。我一般会把max-active设为预估并发量的1.5倍max-wait设为2000ms左右。同时开启连接池监控观察活跃连接数和等待时间。5.5 常见问题速查表问题现象可能原因排查方法解决方案缓存注解不生效缺少EnableCaching检查启动类添加注解缓存注解不生效同类内部调用检查调用链抽到另一个Bean反序列化类型错误序列化器未保存类型查看缓存值配置ObjectMapper数据不一致缓存未清除检查键表达式修正SpEL表达式连接超时连接池耗尽查看连接池监控调大max-active内存增长过快未设置过期时间查看Redis内存设置TTL缓存穿透查询不存在的数据查看数据库压力缓存空值或布隆过滤器缓存雪崩大量键同时过期查看过期时间分布加随机值5.6 几个我踩过的坑第一个坑是键前缀重复。我在配置里设了key-prefix: myapp:cache:然后在SpEL里又写了user: #id结果键变成了myapp:cache:user:user:123。虽然不影响功能但看起来很奇怪。后来我把SpEL里的user:去掉了直接用#id键就变成了myapp:cache:user:123清爽多了。第二个坑是Cacheable和CachePut用在同一个方法上。这两个注解的语义是冲突的Cacheable会先查缓存CachePut总是执行方法。如果同时用行为不可预测。正确做法是分开方法查询用Cacheable更新用CachePut。第三个坑是缓存大对象。有一次我把一个包含几万条记录的列表缓存到Redis结果序列化花了2秒网络传输花了3秒整体比直接查数据库还慢。后来改成只缓存ID列表具体数据按需查询性能才恢复正常。缓存的对象不宜过大一般建议单个值不超过100KB。第四个坑是忘记处理缓存异常。Redis服务偶尔会不可用如果代码里没有降级逻辑整个接口就会报错。我后来加了一层try-catch缓存操作失败时直接查数据库保证核心功能可用。public User getUserWithFallback(Long id) { try { User user (User) redisTemplate.opsForValue() .get(myapp:cache:user: id); if (user ! null) { return user; } } catch (Exception e) { log.warn(缓存读取失败降级查数据库, e); } return userMapper.selectById(id); }这个降级逻辑虽然简单但在关键时刻能保住系统的可用性。缓存是提升性能的手段不是系统的核心依赖任何时候都不能让缓存故障拖垮整个服务。6. 缓存监控与性能调优的实战建议6.1 关键监控指标缓存上线后不能不管必须持续监控几个核心指标。Redis层面可以用info命令查看keyspace_hits、keyspace_misses、used_memory、connected_clients等。命中率低于80%就说明缓存策略有问题要么是键设计不合理要么是过期时间太短。应用层面可以监控缓存方法的调用次数、执行时间、异常率。SpringBoot Actuator提供了/actuator/caches端点可以查看缓存管理器信息。结合Micrometer还能把缓存指标上报到监控系统。我通常会在日志里记录缓存命中情况格式如下Around(annotation(cacheable)) public Object logCacheHit(ProceedingJoinPoint joinPoint, Cacheable cacheable) throws Throwable { long start System.currentTimeMillis(); Object result joinPoint.proceed(); long cost System.currentTimeMillis() - start; log.info(缓存方法: {}, 耗时: {}ms, joinPoint.getSignature().getName(), cost); return result; }这样能直观看到哪些方法耗时较长是否需要调整缓存策略。6.2 过期时间的设置艺术过期时间太短缓存频繁失效数据库压力大太长数据陈旧用户看到旧数据。我的经验是根据数据变更频率来定几乎不变的数据如配置项24小时或更长偶尔变更的数据如用户信息1到2小时频繁变更的数据如库存5到10分钟或者干脆不用缓存另外过期时间要加随机抖动避免雪崩。对于特别热的数据可以设置永不过期但通过后台任务定期更新。6.3 键名规范与内存优化键名要统一规范建议用应用名:模块名:业务名:标识的格式。比如myapp:user:info:123。这样在Redis客户端里用keys myapp:user:*就能批量查看排查问题很方便。内存优化方面尽量用短键名但不要短到看不懂。值的大小要控制大对象拆分成多个小键。如果Redis内存紧张可以配置maxmemory-policy为allkeys-lru让Redis自动淘汰最近最少使用的键。6.4 集群环境下的注意事项Redis集群模式下键的分布由哈希槽决定。如果用了CacheEvict清除多个键这些键可能分布在不同的节点上删除操作会涉及网络往返。批量操作时尽量用pipeline或者RedisTemplate的批量方法。另外集群模式下不支持跨槽的多键操作比如mget。如果业务需要批量获取可以在键名里加{tag}让相关键落到同一个槽。比如myapp:user:{123}:info和myapp:user:{123}:orders大括号里的内容相同会分配到同一个节点。这些细节在单机环境下不会遇到但一旦上集群就必须考虑。我的建议是如果数据量不大优先用主从哨兵模式避免集群的复杂性。只有数据量确实很大时才上集群。6.5 压测与容量规划上线前一定要做压测观察缓存命中率、响应时间、Redis内存增长趋势。用JMeter或wrk模拟并发请求逐步增加压力找到系统的瓶颈点。容量规划方面估算缓存数据总量单条数据平均大小乘以数据条数再乘以1.5的冗余系数。比如10万条用户数据每条1KB那就是100MB加上冗余约150MB。Redis单实例内存建议不超过10GB超过就考虑分片。压测时还要关注Redis的CPU使用率如果接近100%说明单实例扛不住了需要升级配置或分片。网络带宽也是瓶颈特别是缓存大对象时千兆网卡可能成为限制。6.6 我个人的调优心得调优这件事没有银弹必须结合具体业务。我的一般流程是先保证功能正确再优化性能最后考虑扩展性。缓存集成初期不要过度设计先把基本的注解用起来观察效果。遇到问题再针对性解决比如命中率低就调整键和过期时间内存增长快就加淘汰策略。还有一点很重要缓存不是万能的。有些场景用缓存反而增加复杂度比如数据一致性要求极高的金融交易。这时候应该考虑其他方案比如读写分离、CQRS或者直接优化SQL。技术选型要服务于业务而不是为了用而用。最后分享一个小技巧在开发环境把Redis的maxmemory设小一点比如100MB这样能提前暴露内存问题。生产环境再根据实际情况调整。这个做法帮我避免了好几次上线后内存暴涨的事故。

相关新闻

AC自动机+DP+高精度:字符串匹配计数问题全解析

AC自动机+DP+高精度:字符串匹配计数问题全解析

1. 从一道题看字符串匹配与计数的交叉地带如果你刷过一段时间的算法题,看到"Censored!"这个标题大概会心一笑——这是一道把AC自动机、动态规划和大数运算揉在一起的经典题。它的核心场景很直白:给定若干个"禁忌单词",问…

2026/10/13 19:42:46 阅读更多 →
朴素贝叶斯实现豆瓣影评情感分类:多项式分布为何更适合中文文本

朴素贝叶斯实现豆瓣影评情感分类:多项式分布为何更适合中文文本

简介:面向自然语言处理入门学习者与课程设计学生,这份基于朴素贝叶斯的豆瓣影评情感分类实战项目,完整演示了从数据清洗、中文分词、停用词过滤到TF-IDF/词袋特征构建、模型训练与评估的情感分析全流程。压缩包内共10个文件,以Pyt…

2026/10/13 15:55:20 阅读更多 →
光线追踪从数学原理到代码实现:递归求交、反射折射与性能优化实战

光线追踪从数学原理到代码实现:递归求交、反射折射与性能优化实战

光线追踪这四个字,很多刚接触图形学的朋友第一次听到会觉得挺玄乎,其实说白了就一句话:从你的眼睛(或者摄像机)出发,往屏幕上的每个像素方向射一条光线,看它撞到什么、弹到哪里、最后带什么颜色…

2026/10/13 16:51:56 阅读更多 →

最新新闻

三强版 Codex 一键部署包,本地桌面 AI 自动化全流程实操

三强版 Codex 一键部署包,本地桌面 AI 自动化全流程实操

🔍前言 本文介绍 Codex Harness 一键安装包的部署方案,无需解压,也无需繁琐的环境配置,新手也能快速上手。该方案同时支持 Windows 与 macOS 双平台,跟随向导几步即可完成安装、登录与模型密钥配置。文中完整演示了技…

2026/10/13 20:11:08 阅读更多 →
SpringBoot多环境日志配置:用logback-spring.xml+Profile实现环境隔离

SpringBoot多环境日志配置:用logback-spring.xml+Profile实现环境隔离

1. 为什么SpringBoot项目要把日志配置拆成多环境不管你信不信,我见过太多SpringBoot项目,日志配置就一个logback.xml躺在resources目录里,从入职第一周用到项目上线,开发环境一堆DEBUG刷屏没人管,生产环境出了问题想查…

2026/10/13 20:11:07 阅读更多 →
MATLAB中AES-256加解密实战:Java桥接与文件安全

MATLAB中AES-256加解密实战:Java桥接与文件安全

简介:这份资源面向学习密码学与MATLAB编程的读者,提供AES-256位加密算法在MATLAB 2012B环境下的完整实现代码,帮助理解对称加密的内部运作机制。压缩包共16个文件,约448KB,以13个m脚本为核心,涵盖密钥扩展、…

2026/10/13 20:11:07 阅读更多 →
C++结构体与结构体数组:从定义、内存布局到排序实践

C++结构体与结构体数组:从定义、内存布局到排序实践

如果你翻过C教材的目录,多半会在“结构体”这一章停几秒钟,然后想:这不就是把几个变量捆在一起吗?有什么好讲的。我工作之后才意识到,结构体恰恰是C里最容易被低估的基础设施,链表、树、图形控件的属性表、…

2026/10/13 20:11:07 阅读更多 →
Spring Boot JPA 实战指南:从实体映射到查询优化的避坑经验

Spring Boot JPA 实战指南:从实体映射到查询优化的避坑经验

1. 为什么我要认真聊聊 Spring Boot 里的 JPA做后端开发这些年,我接触过的持久层方案不算少。从最早手写 JDBC 拼 SQL,到后来用 MyBatis 写 XML 映射文件,再到 Spring Boot 项目里大量使用 JPA,每一种方案都有它适合的场景。但要说…

2026/10/13 20:11:07 阅读更多 →
Sufer 8.0英文版等值线图实战:从散点插值到批量出图的完整避坑指南

Sufer 8.0英文版等值线图实战:从散点插值到批量出图的完整避坑指南

简介:Surfer 8.0英文版是Golden Software公司推出的专业地形图绘制与三维建模软件,面向地质、环境科学、工程及地球科学领域的从业者与学习者,用于将离散数据转化为连续格网表面并生成专业图件。压缩包共234个文件,约12.41MB&…

2026/10/13 20:10:07 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/12 18:11:17 阅读更多 →