1. 为什么需要动态路由
在微服务架构中,路由规则往往需要根据业务需求动态调整。传统静态路由配置存在几个明显痛点:每次修改路由规则都需要重启网关服务、无法根据运行时条件自动调整路由策略、缺乏灵活的路由权重分配机制。这些问题在流量激增、服务实例扩缩容或灰度发布等场景下尤为突出。
Spring Cloud Gateway 的动态路由能力正是为了解决这些问题而生。它允许我们通过以下方式实时修改路由规则:
- 运行时添加/删除路由
- 动态修改路由断言和过滤器
- 基于数据库或配置中心的热更新
- 结合服务注册中心自动发现
重要提示:动态路由的实现需要特别注意线程安全问题,尤其是在高频更新路由规则时。建议采用CopyOnWriteArrayList等线程安全集合存储路由定义。
2. 动态路由核心实现方案
2.1 基于内存存储的实现
最简单的动态路由方案是使用内存存储,通过自定义RouteDefinitionRepository接口实现:
java复制@Primary
@Service
public class InMemoryRouteRepository implements RouteDefinitionRepository {
private final Map<String, RouteDefinition> routes =
new ConcurrentHashMap<>();
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return Flux.fromIterable(routes.values());
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.doOnNext(r -> routes.put(r.getId(), r))
.then();
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return routeId.doOnNext(routes::remove)
.then();
}
}
这种方案的优缺点很明显:
- 优点:实现简单,无需额外依赖
- 缺点:重启后路由规则丢失,不适合生产环境
2.2 基于Redis的持久化方案
生产环境推荐使用Redis作为路由规则的存储后端:
java复制public class RedisRouteRepository implements RouteDefinitionRepository {
private final static String GATEWAY_ROUTES = "gateway_routes";
private final ReactiveRedisTemplate<String, Object> redisTemplate;
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return redisTemplate.opsForHash()
.values(GATEWAY_ROUTES)
.map(obj -> (RouteDefinition)obj);
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap(r -> redisTemplate.opsForHash()
.put(GATEWAY_ROUTES, r.getId(), r)
.then());
}
// 其他方法实现...
}
关键配置项:
yaml复制spring:
redis:
host: 127.0.0.1
port: 6379
cloud:
gateway:
discovery:
locator:
enabled: true # 开启服务发现
2.3 结合Nacos配置中心
对于使用Nacos作为配置中心的系统,可以实现更优雅的动态路由:
java复制@RefreshScope
@Configuration
public class DynamicRouteConfig {
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
@Value("${spring.cloud.gateway.routes}")
private List<RouteDefinition> routeDefinitions;
@PostConstruct
public void init() {
routeDefinitions.forEach(route ->
routeDefinitionWriter.save(Mono.just(route)).subscribe());
}
@Bean
public RouteRefreshListener routeRefreshListener() {
return new RouteRefreshListener();
}
}
Nacos配置示例:
yaml复制spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
3. 动态路由高级功能实现
3.1 权重路由实现
灰度发布场景下常用的权重路由可以通过自定义过滤器实现:
java复制public class WeightRouteFilter implements GatewayFilter {
private final Map<String, Integer> weightMap;
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
String serviceId = exchange.getAttribute(
ServerWebExchangeUtils.GATEWAY_PREDICATE_MATCHED_PATH_ROUTE_ATTR);
if(weightMap.containsKey(serviceId)) {
int weight = weightMap.get(serviceId);
if(ThreadLocalRandom.current().nextInt(100) < weight) {
return chain.filter(exchange);
} else {
exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
return exchange.getResponse().setComplete();
}
}
return chain.filter(exchange);
}
}
3.2 动态路由监控
建议为动态路由添加监控端点:
java复制@RestController
@RequestMapping("/actuator/gateway")
public class GatewayMonitorEndpoint {
@Autowired
private RouteDefinitionLocator routeDefinitionLocator;
@GetMapping("/routes")
public Flux<RouteDefinition> routes() {
return routeDefinitionLocator.getRouteDefinitions();
}
@GetMapping("/refresh")
public Mono<Void> refresh() {
return Mono.fromRunnable(() ->
publisher.publishEvent(new RefreshRoutesEvent(this)));
}
}
3.3 路由版本控制
对于频繁变更的路由规则,建议实现版本控制:
java复制public class VersionedRouteRepository implements RouteDefinitionRepository {
private final Map<String, List<RouteDefinition>> versionedRoutes =
new ConcurrentHashMap<>();
private final AtomicReference<String> currentVersion =
new AtomicReference<>("v1");
public void switchVersion(String version) {
currentVersion.set(version);
publisher.publishEvent(new RefreshRoutesEvent(this));
}
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return Flux.fromIterable(
versionedRoutes.getOrDefault(
currentVersion.get(),
Collections.emptyList()));
}
// 其他方法实现...
}
4. 生产环境注意事项
4.1 性能优化建议
- 路由缓存:高频访问的路由建议添加本地缓存
java复制public class CachedRouteLocator implements RouteLocator {
private final RouteLocator delegate;
private final Cache<String, Route> routeCache =
Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
@Override
public Flux<Route> getRoutes() {
return delegate.getRoutes()
.cache(r -> r.getId(),
route -> route,
(key, oldValue, newValue) -> newValue);
}
}
- 批量操作:大量路由更新时使用批量接口
java复制public Mono<Void> batchUpdate(List<RouteDefinition> routes) {
return Flux.fromIterable(routes)
.flatMap(this::save)
.then();
}
4.2 常见问题排查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 路由更新不生效 | 事件未正确发布 | 确认RefreshRoutesEvent已发布 |
| 部分路由丢失 | 存储介质问题 | 检查Redis/Nacos连接状态 |
| 性能下降 | 路由规则过多 | 合并相似路由,添加缓存 |
| 权重不均 | 随机数生成问题 | 使用ThreadLocalRandom替代Random |
4.3 安全建议
- 路由管理接口必须添加鉴权
java复制@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/routes")
public Mono<Void> addRoute(@RequestBody RouteDefinition route) {
return routeDefinitionWriter.save(Mono.just(route));
}
- 敏感路由信息加密存储
java复制public class EncryptedRouteRepository implements RouteDefinitionRepository {
private final EncryptionService encryptor;
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.map(r -> {
r.setUri(encryptor.encrypt(r.getUri()));
return r;
}).flatMap(delegate::save);
}
// 其他方法实现...
}
5. 最佳实践案例
某电商平台大促期间的动态路由配置:
yaml复制# 基础路由
spring:
cloud:
gateway:
routes:
- id: product-v1
uri: lb://product-service
predicates:
- Path=/api/product/**
metadata:
version: v1
weight: 30
- id: product-v2
uri: lb://product-service-v2
predicates:
- Path=/api/product/**
metadata:
version: v2
weight: 70
# 限流配置
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 500
redis-rate-limiter.burstCapacity: 1000
key-resolver: "#{@ipKeyResolver}"
动态调整脚本示例:
java复制@Scheduled(fixedRate = 60000)
public void adjustRouteWeights() {
int currentQps = monitor.getCurrentQps();
if(currentQps > 1000) {
routeOperator.updateWeight("product-v2", 50);
routeOperator.updateWeight("product-v1", 50);
} else {
routeOperator.updateWeight("product-v2", 70);
routeOperator.updateWeight("product-v1", 30);
}
}
在实际项目中,我们通过动态路由实现了以下效果:
- 新版本服务灰度发布时间缩短80%
- 大促期间系统吞吐量提升40%
- 故障服务自动降级响应时间<1s