分布式系统测试缓存、注册中心与链路追踪验证上篇咱们搞定了消息队列测试今天继续深入分布式系统的其他组件——Redis缓存、服务注册中心、分布式链路追踪。这些基础设施的测试往往被忽略但出了问题定位起来最头疼。一、Redis 缓存测试缓存测试的核心问题缓存命中、缓存穿透、缓存雪崩、数据一致性。场景订单详情缓存ServicepublicclassOrderQueryService{AutowiredprivateOrderRepositoryorderRepository;AutowiredprivateStringRedisTemplateredisTemplate;privatestaticfinalStringORDER_CACHE_KEYorder:%s;privatestaticfinallongCACHE_TTL30;// 30分钟publicOrdergetOrder(LongorderId){StringkeyString.format(ORDER_CACHE_KEY,orderId);// 1. 查缓存StringcachedredisTemplate.opsForValue().get(key);if(cached!null){returnJSON.parseObject(cached,Order.class);}// 2. 查数据库OrderorderorderRepository.findById(orderId).orElseThrow(()-newOrderNotFoundException(orderId));// 3. 写缓存redisTemplate.opsForValue().set(key,JSON.toJSONString(order),CACHE_TTL,TimeUnit.MINUTES);returnorder;}CacheEvict(keyorder: #orderId)publicvoidupdateOrder(LongorderId,OrderUpdateRequestrequest){// 更新数据库...// 缓存由CacheEvict自动删除}}测试方案Testcontainers RedisdependencygroupIdcom.redis.testcontainers/groupIdartifactIdtestcontainers-redis-junit/artifactIdversion2.2.0/versionscopetest/scope/dependencySpringBootTestTestcontainersclassOrderCacheTest{ContainerstaticRedisContainerredisnewRedisContainer(DockerImageName.parse(redis:7-alpine));DynamicPropertySourcestaticvoidconfigureRedis(DynamicPropertyRegistryregistry){registry.add(spring.data.redis.host,redis::getHost);registry.add(spring.data.redis.port,redis::getMappedPort(6379));}AutowiredOrderQueryServicequeryService;AutowiredOrderRepositoryorderRepository;AutowiredStringRedisTemplateredisTemplate;BeforeEachvoidsetUp(){// 清空缓存redisTemplate.getConnectionFactory().getConnection().flushAll();}TestDisplayName(首次查询缓存未命中查数据库并写入缓存)voidshouldQueryDBAndCacheOnFirstAccess(){// Given: 数据库有数据OrderorderorderRepository.save(newOrder(1L,ITEM-001,newBigDecimal(99.99)));// When: 第一次查询OrderresultqueryService.getOrder(order.getId());// Then: 返回正确数据assertThat(result.getId()).isEqualTo(order.getId());// Then: 缓存已写入StringcachedredisTemplate.opsForValue().get(order:order.getId());assertThat(cached).isNotNull();assertThat(cached).contains(ITEM-001);}TestDisplayName(二次查询缓存命中不查数据库)voidshouldHitCacheOnSecondAccess(){// Given: 数据已在缓存OrderorderorderRepository.save(newOrder(1L,ITEM-001,newBigDecimal(99.99)));queryService.getOrder(order.getId());// 预热缓存// When: 再次查询OrderresultqueryService.getOrder(order.getId());// Then: 结果正确虽然没有直接验证没查DB但可以通过监控验证assertThat(result.getSku()).isEqualTo(ITEM-001);}TestDisplayName(更新订单后缓存失效)voidshouldInvalidateCacheOnUpdate(){// Given: 缓存已有数据OrderorderorderRepository.save(newOrder(1L,ITEM-001,newBigDecimal(99.99)));queryService.getOrder(order.getId());// 写缓存// When: 更新订单queryService.updateOrder(order.getId(),newOrderUpdateRequest(ITEM-002));// Then: 缓存已删除StringcachedredisTemplate.opsForValue().get(order:order.getId());assertThat(cached).isNull();}TestDisplayName(缓存过期后重新查数据库)voidshouldQueryDBAfterCacheExpire()throwsInterruptedException{// Given: 写入缓存TTL设短一点方便测试OrderorderorderRepository.save(newOrder(1L,ITEM-001,newBigDecimal(99.99)));queryService.getOrder(order.getId());// When: 等待缓存过期测试中可以把TTL设为1秒Thread.sleep(2000);// Then: 缓存已过期StringcachedredisTemplate.opsForValue().get(order:order.getId());assertThat(cached).isNull();// 再次查询应该重新查DBOrderresultqueryService.getOrder(order.getId());assertThat(result).isNotNull();}}缓存穿透测试TestDisplayName(查询不存在的订单不缓存空值防穿透)voidshouldNotCacheNullResult(){// When: 查询不存在的订单assertThatThrownBy(()-queryService.getOrder(99999L)).isInstanceOf(OrderNotFoundException.class);// Then: 不应该缓存空值否则恶意请求会压垮DBStringcachedredisTemplate.opsForValue().get(order:99999);assertThat(cached).isNull();}TestDisplayName(查询不存在的订单缓存空值布隆过滤器方案)voidshouldCacheNullWithShortTTL(){// 另一种方案缓存空值但TTL很短比如1分钟// 验证空值缓存的TTL}二、服务注册中心测试微服务通过注册中心Nacos/Eureka/Consul互相发现。测试中需要验证服务是否正确注册、是否能被发现、故障时是否剔除。Nacos 测试SpringBootTest(webEnvironmentSpringBootTest.WebEnvironment.RANDOM_PORT)TestcontainersclassServiceDiscoveryTest{ContainerstaticGenericContainer?nacosnewGenericContainer(DockerImageName.parse(nacos/nacos-server:v2.2.3)).withEnv(MODE,standalone).withExposedPorts(8848).waitingFor(Wait.forHttp(/nacos).forStatusCode(200));DynamicPropertySourcestaticvoidconfigureNacos(DynamicPropertyRegistryregistry){StringnacosUrlString.format(http://%s:%d,nacos.getHost(),nacos.getMappedPort(8848));registry.add(spring.cloud.nacos.discovery.server-addr,()-nacosUrl);registry.add(spring.cloud.nacos.config.server-addr,()-nacosUrl);}AutowiredDiscoveryClientdiscoveryClient;TestDisplayName(服务启动后自动注册到Nacos)voidshouldRegisterToNacos(){// 等待注册完成await().atMost(Duration.ofSeconds(30)).pollInterval(Duration.ofSeconds(1)).untilAsserted(()-{ListServiceInstanceinstancesdiscoveryClient.getInstances(order-service);assertThat(instances).isNotEmpty();});}TestDisplayName(能从Nacos发现用户服务)voidshouldDiscoverUserService(){// 先手动注册一个用户服务实例模拟registerMockService(user-service,localhost,8081);// 验证能发现ListServiceInstanceinstancesdiscoveryClient.getInstances(user-service);assertThat(instances).hasSize(1);assertThat(instances.get(0).getHost()).isEqualTo(localhost);}}三、分布式链路追踪测试链路追踪Sleuth Zipkin/Jaeger能帮你追踪请求在多个服务间的流转。测试中需要验证TraceId是否正确传递、Span是否完整、链路数据是否正确上报。场景验证TraceId传递SpringBootTest(webEnvironmentSpringBootTest.WebEnvironment.RANDOM_PORT)classTracingTest{AutowiredTestRestTemplaterestTemplate;AutowiredTracertracer;// Micrometer TracingTestDisplayName(HTTP请求携带TraceId并在服务间传递)voidshouldPropagateTraceId(){// When: 发送请求带自定义TraceIdStringcustomTraceIdabc123;ResponseEntityStringresponserestTemplate.exchange(/api/orders/1,HttpMethod.GET,newHttpEntity(Map.of(X-B3-TraceId,customTraceId)),String.class);// Then: 响应中应该包含Trace信息assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);// 验证日志中包含了TraceId// 可以通过Appender捕获日志验证}TestDisplayName(异步任务继承父Span的TraceId)voidshouldInheritTraceInAsyncTask()throwsException{// Given: 当前有活跃的SpanSpanparentSpantracer.nextSpan().name(parent-operation).start();try(Tracer.SpanInScopewstracer.withSpanInScope(parentSpan)){// When: 提交异步任务CompletableFutureStringfutureCompletableFuture.supplyAsync(()-{// Then: 异步线程中应该能获取到相同的TraceIdSpancurrentSpantracer.currentSpan();assertThat(currentSpan).isNotNull();assertThat(currentSpan.context().traceId()).isEqualTo(parentSpan.context().traceId());returndone;});future.get(5,TimeUnit.SECONDS);}finally{parentSpan.end();}}}Zipkin 验证TestcontainersclassZipkinIntegrationTest{ContainerstaticGenericContainer?zipkinnewGenericContainer(DockerImageName.parse(openzipkin/zipkin:2.24)).withExposedPorts(9411);TestDisplayName(链路数据正确上报到Zipkin)voidshouldReportTracesToZipkin(){// 触发一个跨服务请求orderService.createOrder(request);// 等待数据上报await().atMost(Duration.ofSeconds(10)).untilAsserted(()-{// 查询Zipkin API验证Trace存在StringzipkinUrlString.format(http://%s:%d,zipkin.getHost(),zipkin.getMappedPort(9411));ResponseEntityStringresponserestTemplate.getForEntity(zipkinUrl/api/v2/traces?serviceNameorder-service,String.class);assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);assertThat(response.getBody()).contains(order-service);});}}四、混沌测试入门分布式系统测试的终极形态——故意搞破坏看系统能不能扛住。简单实现随机杀容器TestDisplayName(Redis故障时服务应该降级查数据库)voidshouldFallbackWhenRedisDown(){// Given: 缓存已有数据OrderorderqueryService.getOrder(1L);assertThat(order).isNotNull();// When: 杀掉Redis容器redis.stop();// Then: 服务应该降级查数据库不抛异常OrderfallbackOrderqueryService.getOrder(1L);assertThat(fallbackOrder).isNotNull();assertThat(fallbackOrder.getId()).isEqualTo(1L);// 恢复Redisredis.start();}专业工具Chaos Monkey for Spring BootdependencygroupIdde.codecentric/groupIdartifactIdchaos-monkey-spring-boot/artifactIdversion3.0.2/versionscopetest/scope/dependency# application-chaos.ymlchaos:monkey:enabled:trueassaults:level:3# 攻击强度 1-10latency-active:truelatency-range-start:1000latency-range-end:3000exceptions-active:trueexception:type:java.io.IOExceptionargument:模拟IO异常SpringBootTestActiveProfiles(chaos)classChaosTest{AutowiredOrderServiceorderService;TestDisplayName(在混沌攻击下核心流程仍然可用)voidshouldSurviveChaos(){// 即使服务被注入延迟和异常核心功能应该仍然可用// 验证降级、熔断、重试机制是否生效for(inti0;i10;i){try{OrderResultresultorderService.createOrder(request);// 记录成功/失败}catch(Exceptione){// 验证是预期的异常类型assertThat(e).isInstanceOfAny(ServiceUnavailableException.class,TimeoutException.class);}}}}五、小结今天咱们聊了分布式系统的测试组件测试重点工具Redis缓存命中/穿透/雪崩/一致性Testcontainers Redis注册中心服务注册/发现/剔除Testcontainers Nacos链路追踪TraceId传递/Span完整性Micrometer Tracing Zipkin混沌测试故障降级/熔断/恢复Chaos Monkey一句话总结分布式系统的测试不能只验证正常情况缓存穿透、服务故障、网络延迟这些异常场景才是价值所在。Testcontainers让你能在测试中真实模拟这些场景。