SpringBoot+Mybatis-Plus+Redis+Spring Cache四组件协同实战
发布时间:2026/10/20 5:07:15 锦皓数字建站

1. 项目概述为什么这个组合成了Java后端开发的“标准答案”最近在带几个刚转行的开发者做实战训练几乎每人都会问一句“老师现在写SpringBoot项目Mybatis-Plus是不是必须配Redis和Spring Cache”——这问题背后不是技术好奇而是真实业务场景倒逼出来的共识。我参与过的近20个中型业务系统从电商订单履约到内部工单平台只要涉及高频读、低频写、数据一致性要求适中的模块这套组合就不是“可选方案”而是“默认起点”。它解决的从来不是“能不能跑起来”而是“能不能扛住日均50万次商品详情页访问”“能不能让库存查询响应压在80ms以内”“能不能避免缓存击穿把数据库拖垮”这些具体到毛细血管的问题。核心关键词——SpringBoot、Mybatis-Plus、Spring Cache、Redis——这四个词串在一起本质是一条被反复验证的“性能优化流水线”SpringBoot提供开箱即用的容器与自动装配能力Mybatis-Plus在保留SQL可控性的前提下把CRUD模板代码砍掉70%Spring Cache作为抽象层把缓存逻辑从业务代码里“剥”出来实现关注点分离而Redis则是这条流水线上最可靠的“高速缓冲区”。它不负责数据持久化的终极保障但承担着90%以上热数据的毫秒级响应压力。你不需要记住所有注解参数但必须清楚Cacheable不是魔法它背后是CacheManager如何对接Redis连接池是keyGenerator怎么避免键冲突是unless表达式在什么条件下让缓存失效——这些细节直接决定你的接口是稳定在50ms还是某天突然抖动到2秒。适合谁来读如果你正在用SpringBoot写真实项目哪怕只是公司内部的一个审批系统只要用户量超过200人、单表数据过10万、页面加载明显有延迟感这篇就是为你写的。它不讲“Spring Cache原理图解”这种教科书内容只聚焦一件事怎么把这四个组件拧成一股绳让它们在你的项目里真正协同工作而不是各自为政、互相拖累。接下来我会拆解每一个环节的真实取舍、踩过的坑、以及那些文档里不会写但上线前必须确认的检查项。2. 整体架构设计与技术选型逻辑2.1 为什么不是MyBatis原生Guava Cache先说一个反例。去年帮某高校实验室重构一个科研设备预约系统原始方案用的是MyBatis原生Guava Cache本地缓存。初期测试很顺单机QPS轻松破300响应时间平均40ms。但上线第三天凌晨监控报警——数据库连接池耗尽CPU飙升至95%。排查发现Guava Cache的maximumSize设为10000但实际热点数据只有200个设备信息其余9800个缓存项全是冷数据不仅没提升性能反而因频繁的LRU淘汰触发了大量GC。更致命的是集群部署后每个节点都有自己的Guava Cache副本A节点更新了设备状态B节点的缓存还是旧的导致用户看到“已预约”却能重复提交。这就是选型的核心逻辑本地缓存解决不了分布式一致性而分布式缓存又必须解决穿透、击穿、雪崩三大陷阱。Redis天然支持主从复制、哨兵高可用、Cluster分片更重要的是它提供了SETNXset if not exists、EXPIRE、EVAL等原子操作为应对缓存异常提供了底层武器。而Spring Cache的价值在于它把Redis的这些能力封装成Cacheable(cacheNames product, key #id, unless #result null)这样一行声明式代码让你不用在Service里写redisTemplate.opsForValue().get(product: id)再手动判空、反序列化、类型转换。2.2 Mybatis-Plus为何不可替代有人问“既然都用Redis了SQL还重要吗”——非常重要。我见过太多项目把所有查询都塞进Cacheable结果缓存命中率不到30%。原因很简单Mybatis-Plus的QueryWrapper能动态拼接WHERE条件而Cacheable的key生成器却很难优雅处理多条件组合。比如一个商品搜索接口/api/products?categoryphonebrandapplepriceMin5000sortsales如果用Cacheable(key #p0)整个参数对象做key那priceMin5000和priceMin4999就是两个完全不同的key缓存利用率极低。Mybatis-Plus的解法是把复杂查询逻辑留在Mapper层用Select或QueryWrapper精准控制SQL只对最终返回的、结构稳定的实体对象做缓存。例如我们定义一个ProductCacheService里面只暴露getById(Long id)和listByCategory(String category)两个方法前者用Cacheable缓存单个商品后者用Cacheable缓存分类列表注意列表缓存需谨慎后文详述。这样缓存粒度可控key生成简单#id或#category命中率自然上去。Mybatis-Plus的LambdaQueryWrapper还能防止字段名写错编译期就能发现问题比纯XML写法少掉一半头发。2.3 SpringBoot版本与依赖兼容性红线这是最容易被忽略的“定时炸弹”。SpringBoot 2.7.x默认使用Spring Framework 5.3.x而Mybatis-Plus 3.5.x要求Spring Framework最低5.2.0Redis客户端则推荐LettuceSpringBoot 2.3默认而非Jedis。但如果你强行升级到Mybatis-Plus 4.0当前最新就会发现它要求Spring Boot 3.x基于Jakarta EE 9而Spring Boot 3.x又要求Java 17——这意味着你得同步升级JDK、重构所有javax.*包引用、调整Hibernate Validator配置。对于一个运行两年的老项目这种升级成本远超收益。我的实操建议是锁定SpringBoot 2.7.182023年最后一个2.x维护版 Mybatis-Plus 3.5.3.1 Spring Data Redis 2.7.12。这个组合经过大量生产环境验证Lettuce连接池稳定Redisson集成平滑且所有Starter依赖版本由Spring Boot Parent统一管理不会出现NoSuchMethodError。在pom.xml里务必删除所有显式声明的spring-boot-starter-data-redis版本号只留dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId !-- 不要写version交给parent管理 -- /dependency否则Maven可能拉取到不兼容的快照版导致RedisConnectionFactory初始化失败——这个错误在启动日志里藏得很深通常只显示Failed to instantiate [org.springframework.data.redis.core.RedisTemplate]你需要翻到Caused by那一行才能看到真实的NoClassDefFoundError: io/lettuce/core/RedisClient。3. 核心组件集成与关键配置详解3.1 Redis连接池深度调优不只是max-active很多教程只告诉你配置spring.redis.pool.max-active8但线上出问题时你得知道为什么是8而不是16或32。Lettuce连接池Spring Boot 2.3默认和Jedis有本质区别Jedis是直连每个线程独占一个连接Lettuce是基于Netty的异步连接一个连接实例可复用处理多个命令。所以max-active在Lettuce里实际对应的是max-inactive最大空闲连接数而真正影响并发能力的是io.lettuce.core.resource.ClientResources里的线程池。我在一个日均订单12万的系统里做过压测当spring.redis.lettuce.pool.max-active16时QPS卡在1800左右redis-cli --latency显示P99延迟跳到120ms将max-active降至8同时把spring.redis.lettuce.pool.min-idle2并增加spring.redis.lettuce.shutdown-timeout100优雅关闭超时QPS反而升到2100P99稳定在65ms。原因在于过多空闲连接会占用Netty EventLoop线程导致命令排队而少量连接配合合理的min-idle能让连接复用率更高减少TCP握手开销。以下是生产环境推荐的application.ymlRedis配置段spring: redis: host: 192.168.10.100 port: 6379 password: your_secure_password database: 0 timeout: 2000 # 命令执行超时单位毫秒 lettuce: pool: max-active: 8 # 最大连接数非越大越好 max-idle: 8 # 最大空闲连接数通常等于max-active min-idle: 2 # 最小空闲连接数保证低峰期有连接可用 max-wait: -1 # 获取连接最大等待时间-1表示无限等待生产环境慎用 shutdown-timeout: 100 # 关闭连接池时等待命令完成的超时提示max-wait: -1在测试环境可以接受但生产环境必须设为正数如1000否则某个慢查询阻塞连接池会导致整个应用线程池雪崩。我们曾因此触发过一次全站503。3.2 Spring Cache抽象层的三层拦截机制Spring Cache不是简单的“查缓存→查DB→回填缓存”三步走。它的执行链路是Cacheable注解 →CacheInterceptor→CacheManager→ 具体缓存实现如RedisCacheManager。理解这个链路才能精准定位问题。比如当你发现缓存没生效第一反应不该是“Redis挂了”而应检查CacheInterceptor是否被代理——如果目标方法是private或final或者类没有被Spring管理new出来的对象Cacheable根本不会触发。更关键的是CacheManager的配置。默认的SimpleCacheManager只存内存显然不行。我们需要自定义RedisCacheManager并重点配置三个参数cacheDefaults定义所有缓存的默认TTLTime To Live。不要在每个Cacheable里写cacheNames product, cacheManager redisCacheManager而是统一管理。initialCacheNames预热缓存名称避免首次访问时动态创建缓存实例的开销。usePrefix必须设为true否则所有缓存key会混在一起product:1和user:1可能冲突。以下是一个生产级的RedisCacheManager配置类Configuration EnableCaching public class CacheConfig { Bean public RedisCacheConfiguration redisCacheConfiguration() { // 使用Jackson2JsonRedisSerializer序列化支持泛型 RedisSerializationContext.SerializationPairObject pair RedisSerializationContext.SerializationPair.fromSerializer( new GenericJackson2JsonRedisSerializer()); return RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith(pair) .entryTtl(Duration.ofHours(2)) // 默认2小时过期 .disableCachingNullValues(); // 禁止缓存null避免缓存穿透 } Bean public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory, RedisCacheConfiguration redisCacheConfiguration) { RedisCacheManager.RedisCacheManagerBuilder builder RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory); builder.cacheDefaults(redisCacheConfiguration); // 针对不同业务场景设置差异化TTL builder.withInitialCacheConfigurations( Map.of( product, redisCacheConfiguration.entryTtl(Duration.ofMinutes(30)), category, redisCacheConfiguration.entryTtl(Duration.ofHours(6)), config, redisCacheConfiguration.entryTtl(Duration.ofDays(7)) ) ); return builder.build(); } }注意disableCachingNullValues()这一行。它意味着如果数据库查不到数据返回nullSpring Cache不会把这个null存入Redis。这是防御缓存穿透的第一道防线——否则攻击者用大量不存在的ID如/product/999999999刷接口缓存里全是null每次请求都穿透到DB。3.3 Mybatis-Plus的缓存联动策略二级缓存不是银弹Mybatis原生支持二级缓存cache/标签但Mybatis-Plus默认禁用它原因很现实二级缓存作用于Mapper级别无法感知跨Mapper的数据变更。比如ProductMapper查商品OrderMapper更新订单但订单更新可能影响商品库存而ProductMapper的二级缓存对此一无所知导致脏读。我们的方案是彻底关闭Mybatis二级缓存把缓存控制权完全交给Spring Cache。在application.yml中添加mybatis-plus: configuration: cache-enabled: false # 关闭Mybatis二级缓存然后在Service层用Cacheable精准控制。但这里有个陷阱Cacheable默认缓存整个方法返回值如果方法返回ListProduct那整个列表会被序列化成一个value存入Redis。当列表中某个商品价格更新你调用updateById()Spring Cache并不知道该刷新哪个缓存项——它只能清空整个listByCategory缓存导致其他未变更的商品也失效。解法是对集合查询改用CacheableCacheEvict组合并引入缓存预热机制。例如Service public class ProductCacheService { Autowired private ProductMapper productMapper; // 缓存单个商品key为id Cacheable(value product, key #id) public Product getById(Long id) { return productMapper.selectById(id); } // 缓存分类列表但key包含分类ID便于精准清除 Cacheable(value categoryProducts, key #category) public ListProduct listByCategory(String category) { return productMapper.selectList(new QueryWrapperProduct().eq(category, category)); } // 更新商品时同步清除相关缓存 CacheEvict(value {product, categoryProducts}, allEntries false, key #product.category) public void update(Product product) { productMapper.updateById(product); } }注意CacheEvict的key #product.category——当更新一个商品时它会清除categoryProducts缓存中所有该分类下的列表而不是整个缓存。虽然牺牲了一点性能需要重新查询列表但保证了数据强一致且避免了“缓存雪崩”。4. 实操全流程与关键环节实现4.1 从零开始的五步集成实录我以一个真实的“用户中心”模块为例演示如何在2小时内完成四组件集成。假设你已有SpringBoot 2.7.18基础项目数据库为MySQL。第一步添加Maven依赖精确到小数点后三位dependencies !-- SpringBoot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Mybatis-Plus Starter -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- Redis Starter -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- Lombok简化代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies注意不要添加spring-boot-starter-cacheSpring Boot 2.7已内置显式添加可能导致版本冲突。第二步配置Redis连接与缓存管理器在application.yml中填写Redis服务器地址本地开发可用Docker# 启动RedisDocker docker run -d --name my-redis -p 6379:6379 -d redis:7-alpine配置文件spring: redis: host: localhost port: 6379 database: 0 timeout: 2000 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 2第三步编写实体类与MapperMybatis-Plus核心Data TableName(t_user) public class User { TableId(type IdType.AUTO) private Long id; private String username; private String email; private Integer status; // 0-禁用, 1-启用 } Mapper public interface UserMapper extends BaseMapperUser { // Mybatis-Plus已提供selectById等方法无需额外SQL }第四步定义缓存ServiceSpring Cache落地Service RequiredArgsConstructor public class UserCacheService { private final UserMapper userMapper; /** * 缓存单个用户TTL 30分钟 * key生成规则user::123 */ Cacheable(value user, key #id, unless #result null) public User getById(Long id) { System.out.println(DB query for user: id); // 日志验证是否走缓存 return userMapper.selectById(id); } /** * 缓存启用用户列表TTL 2小时 * key生成规则user:enabled */ Cacheable(value userList, key enabled) public ListUser listEnabled() { System.out.println(DB query for enabled users); return userMapper.selectList(new QueryWrapperUser().eq(status, 1)); } /** * 更新用户时清除对应缓存 * 注意这里用key #user.id确保只清单个用户缓存 */ CacheEvict(value {user, userList}, key #user.id) public void update(User user) { userMapper.updateById(user); } }第五步编写Controller验证效果RestController RequestMapping(/api/user) RequiredArgsConstructor public class UserController { private final UserCacheService userCacheService; GetMapping(/{id}) public ResultUser getUser(PathVariable Long id) { User user userCacheService.getById(id); return Result.success(user); } GetMapping(/list/enabled) public ResultListUser getEnabledUsers() { ListUser users userCacheService.listEnabled(); return Result.success(users); } }启动应用第一次访问/api/user/1控制台打印DB query for user: 1第二次访问无日志输出说明命中缓存。用redis-cli连接执行KEYS user*能看到user::1这样的keyGET user::1可查看序列化后的JSON值。4.2 缓存穿透、击穿、雪崩的实战防御这三大问题不是理论而是每天都在发生的线上事故。下面是我总结的“防御三件套”全部可直接抄作业。缓存穿透防御布隆过滤器Bloom Filter当恶意请求用大量不存在的ID如/user/999999999刷接口disableCachingNullValues()只能防住一层但高频请求仍会打到DB。布隆过滤器是空间换时间的最优解——它用一个很长的bit数组和多个哈希函数判断“某个ID是否可能存在”。即使误判说存在其实不存在也只会多查一次DB但绝不会漏判说不存在其实存在。Lettuce原生支持RedisBloom模块但国内云Redis大多不开放。我们采用轻量级方案在应用层用guava:32.0.0-jre的BloomFilter定期从DB加载有效ID到内存。Component public class UserBloomFilter { private final UserMapper userMapper; private volatile BloomFilterLong bloomFilter; public UserBloomFilter(UserMapper userMapper) { this.userMapper userMapper; reloadFilter(); } public boolean mightContain(Long id) { return bloomFilter ! null bloomFilter.mightContain(id); } Scheduled(fixedRate 300000) // 每5分钟刷新一次 public void reloadFilter() { // 查询所有启用用户的ID构建布隆过滤器 ListLong userIds userMapper.selectObjs( new QueryWrapperUser().select(id).eq(status, 1) ).stream().map(obj - (Long) obj).collect(Collectors.toList()); BloomFilterLong newFilter BloomFilter.create( Funnels.longFunnel(), userIds.size(), 0.01 // 误判率1% ); userIds.forEach(newFilter::put); this.bloomFilter newFilter; System.out.println(BloomFilter reloaded, size userIds.size()); } }在UserCacheService.getById()中前置校验Cacheable(value user, key #id, unless #result null) public User getById(Long id) { // 先过布隆过滤器不存在的ID直接返回null不查DB if (!userBloomFilter.mightContain(id)) { return null; } return userMapper.selectById(id); }缓存击穿防御互斥锁Mutex Lock热点数据如首页Banner过期瞬间大量请求同时发现缓存失效全部涌向DB造成瞬时压力。解法是第一个请求去DB加载其他请求等待等缓存回填后再读。Spring Cache本身不提供锁但我们用Redis的SETNX命令实现Service public class BannerCacheService { Autowired private RedisTemplateString, Object redisTemplate; Cacheable(value banner, key #id) public Banner getById(Long id) { String lockKey lock:banner: id; String valueKey banner: id; // 尝试获取锁超时时间3秒 Boolean isLocked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofSeconds(3)); if (Boolean.TRUE.equals(isLocked)) { try { // 获取锁成功查DB并回填缓存 Banner banner bannerMapper.selectById(id); if (banner ! null) { redisTemplate.opsForValue() .set(valueKey, banner, Duration.ofMinutes(30)); } return banner; } finally { // 释放锁 redisTemplate.delete(lockKey); } } else { // 获取锁失败等待100ms后重试避免自旋 try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return getById(id); // 递归重试 } } }缓存雪崩防御随机TTL 多级缓存雪崩是大量缓存同一时间过期导致DB瞬间被打满。最简单有效的办法给TTL加一个随机偏移量。比如基础TTL是30分钟我们设置30 random(0, 10)分钟让过期时间分散开。在CacheConfig中修改Bean public RedisCacheConfiguration redisCacheConfiguration() { return RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith(pair) .entryTtl(Duration.ofMinutes(30 ThreadLocalRandom.current().nextInt(0, 10))) .disableCachingNullValues(); }更进一步可引入多级缓存本地Caffeine缓存TTL 1分钟 Redis缓存TTL 30分钟。Caffeine作为第一道屏障能扛住大部分请求Redis作为第二道降低整体穿透率。不过这会增加架构复杂度中小项目用随机TTL已足够。5. 常见问题与排查技巧实录5.1 缓存不生效的7种死因与诊断路径缓存不生效是最常被问的问题但原因千差万别。我整理了一份“症状-原因-解决方案”速查表按发生频率排序症状可能原因排查命令/步骤解决方案第一次访问有日志第二次还有日志方法未被Spring代理private/final/非Spring Bean在Controller里注入Service调用其方法检查类上是否有Service确保目标类被Spring管理方法为public且调用来自外部BeanRedis里有key但Cacheable没走CacheManager未正确配置或EnableCaching缺失查看启动日志搜索CacheInterceptor是否注册检查CacheConfig类是否被Configuration标记确认EnableCaching在主配置类上CacheManagerBean名是否为cacheManager默认缓存key是SimpleKey []Cacheable方法无参数或参数为数组/Mapkey生成器无法识别在Cacheable中显式指定key default显式写key或用SpEL表达式如key #root.methodName缓存值是乱码或class字段缺失序列化器不匹配如用了StringRedisTemplate但存的是对象redis-cli执行GET key看返回值是否为JSON统一使用RedisTemplateString, Object并配置GenericJackson2JsonRedisSerializer更新后缓存未清除CacheEvict的key与Cacheable不一致或allEntriestrue但未指定value检查两个注解的value属性是否相同key表达式是否等价用CacheEvict(value user, key #user.id)确保key生成逻辑一致应用重启后缓存全丢Redis未配置持久化或连接池未正确关闭redis-cli INFO persistence查看rdb_last_bgsave_status生产环境Redis必须开启RDBAOF混合持久化appendonly yes缓存命中率低于10%缓存粒度太粗如缓存整个List或key设计不合理用redis-cli --bigkeys分析大keyMONITOR命令抓取key生成逻辑改用单对象缓存或对List做分页缓存如user:list:page1提示诊断时永远先看Redis里有没有key再看Spring日志有没有CacheInterceptor执行记录。如果Redis有key但应用没读大概率是序列化问题如果Redis没key但日志显示CacheInterceptor执行了那就是CacheManager配置错了。5.2 Redis连接池耗尽的典型表现与根因现象应用启动正常但高峰期大量请求超时日志出现Cannot get Jedis connection或Unable to connect to Redisredis-cli INFO clients显示connected_clients接近maxclients上限。根因分析按概率排序Lettuce连接池未正确关闭Spring Boot应用停机时若RedisConnectionFactory未优雅关闭连接会滞留在Redis端直到超时默认timeout 0即永不过期。解决方案是在CacheConfig中配置shutdown-timeout并在PreDestroy中手动关闭Component public class RedisConnectionManager { Autowired private RedisConnectionFactory connectionFactory; PreDestroy public void destroy() { if (connectionFactory instanceof LettuceConnectionFactory) { ((LettuceConnectionFactory) connectionFactory).destroy(); } } }业务代码未释放连接使用RedisTemplate时若在try-catch中抛出异常finally块未执行connection.close()。但Lettuce是自动管理的此情况较少。更常见的是在Stream流式处理中对Redis操作未做异常兜底。例如// 危险如果getById()抛异常连接可能泄露 users.stream().map(user - userCacheService.getById(user.getId())).collect(...);应改为users.parallelStream() .map(id - { try { return userCacheService.getById(id); } catch (Exception e) { log.error(Cache get failed for id: {}, id, e); return null; // 或抛自定义业务异常 } }) .filter(Objects::nonNull) .collect(Collectors.toList());网络中间件问题公司内网有Redis代理如Twemproxy其max_connections配置过小。此时redis-cli能连但应用连不上。解决方案是联系运维检查代理层配置或绕过代理直连Redis。5.3 Mybatis-Plus与缓存协同的隐藏陷阱陷阱一LambdaQueryWrapper的序列化失败当你在Cacheable方法里传入LambdaQueryWrapper作为参数Spring Cache会尝试序列化它但LambdaQueryWrapper内部有SerializedLambda反序列化时可能找不到类。报错java.lang.ClassNotFoundException: com.xxx.User$$Lambda$xxx。解法永远不要把QueryWrapper作为Cacheable方法的参数。应该把查询条件拆成基本类型// 错误 Cacheable(key #wrapper) public ListUser list(LambdaQueryWrapperUser wrapper) { ... } // 正确 Cacheable(key #status _ #username) public ListUser list(Integer status, String username) { return userMapper.selectList(new QueryWrapperUser() .eq(status, status) .like(username, username)); }陷阱二分页插件Page与缓存的冲突Mybatis-Plus的Page对象包含current、size、records等字段若直接缓存PageUserrecords里的User对象会被序列化但Page本身的分页元数据total、pages可能过期。更糟的是Page实现了Serializable但某些字段如searchCount在反序列化后为null导致前端分页异常。解法缓存分页数据不缓存Page对象。定义DTOData public class PageResultT { private long total; private int pages; private ListT records; } Cacheable(key #page.current _ #page.size _ #status) public PageResultUser page(PageUser page, Integer status) { PageUser result userMapper.selectPage(page, new QueryWrapperUser().eq(status, status)); return new PageResult(result.getTotal(), result.getPages(), result.getRecords()); }这样缓存的是纯净的JSON无任何框架耦合。6. 性能压测与线上监控关键指标6.1 用JMeter模拟真实流量的5个必设参数很多团队压测只看TPS却忽略了缓存系统的健康度。以下是我在某电商平台压测时JMeter中必须配置的5个参数线程组Thread Group设置Number of Threads为预期峰值QPS的1.5倍如预估5000 QPS则设7500线程Ramp-Up Period设为300秒模拟流量缓慢爬升避免瞬间冲击。HTTP Header Manager添加Content-Type: application/json否则Spring Boot可能返回406错误。CSV Data Set Config准备10万个真实用户ID的CSV文件让每个线程随机读取ID模拟真实缓存key分布避免所有请求打到同一个key如user:1造成热点。View Results Tree仅在调试时开启正式压测必须关闭否则内存溢出。Backend Listener配置InfluxDBGrafana实时采集jmeter_metrics重点关注http_req_duration{applicationuser-service, uri/api/user/{id}}P95响应时间redis_commands_total{commandget, resultsuccess}Redis GET命令成功率jvm_memory_used_bytes{areaheap}堆内存使用率压测结果解读当QPS达到4000时若redis_commands_total的resulterror突增说明Redis连接池或网络有问题若http_req_durationP95从80ms跳到300ms但Redis指标正常则问题在应用层如GC频繁、线程池打满。6.2 线上Redis监控的3个黄金指标别再只看redis-cli INFO memory了。生产环境必须盯紧这三个指标instantaneous_ops_per_secRedis每秒处理命令数。正常值应在峰值QPS的1.2倍内如应用QPS 5000Redis OPS应在6000左右。若OPS远高于QPS说明有大量无效请求如缓存穿透或客户端重复发送。evicted_keys被驱逐的key数量。非零值说明内存不足Redis在LRU淘汰。理想值为0。若持续增长需扩容Redis内存或优化缓存策略如缩短TTL、清理冷数据。 3
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。