网关路由改完要重启?我写了个动态路由组件,配置秒生效,再也不怕半夜发版了
一、深夜发版的痛,你们懂的
兄弟们,你们有没有经历过这种场景:产品经理跑过来说“这个接口需要加个路由转发,帮我在网关里配一下”——你打开
application.yml,加上一段路由配置,然后执行mvn clean package,再docker build,再kubectl rollout restart,整个过程下来少说20分钟。更惨的是,如果配置写错了,又要再来一遍。我当时就想:能不能像Nacos改配置一样,改完路由立马生效,不用重启?
后来我翻遍了Spring Cloud Gateway的源码,发现它本身支持动态路由,只是需要自己实现路由数据源。于是我们封装了一个
mate-starter-gateway组件,用Redis做路由数据源,配合发布订阅实现运行时热更新。今天就把这套方案从零到一复盘出来,同时把组件封装的设计思路也讲清楚。
二、痛点分析:为什么需要动态路由?
Spring Cloud Gateway原生支持从配置文件(application.yml)加载路由,但这种方式有几个硬伤:
| 痛点 | 影响 |
|---|---|
| 改配置要重启 | 发版窗口受限,紧急配置无法快速生效 |
| 配置分散 | 多个网关实例的配置文件需要同步,容易不一致 |
| 无法运行时查看 | 当前生效的路由规则不透明,排查问题困难 |
| 回滚麻烦 | 配置错了要重新打包发布 |
我们需要的是一套配置集中管理、修改实时生效、无需重启网关的动态路由方案。
核心诉求:管理端(mate-admin)改路由 → 网关端(mate-gateway)秒级生效,中间不用重启、不用打包、不用发版。
三、整体架构:管理端 + Redis + 网关端
3.1 三个角色各司其职
这套动态路由方案由三个角

三个角色的职责划分:
| 层 | 是什么 | 谁读写 |
|---|---|---|
DB sys_route_conf |
路由配置的权威源(含 isDel、排序、完整字段) | 管理端 CRUD、列表展示 |
Redis Hash gateway:route:key |
网关的运行时路由数据源 | 管理端写、网关读 |
DynamicRouteInitRunner |
DB → Redis 的同步桥(预热/恢复/重载) | 启动、订阅重载 topic 时执行 |
Redis 这份数据是写穿式数据桥(write-through),而不是读加速缓存(cache-aside)。核心作用有三:
-
运行时热更新:管理端变更路由 → 写 Redis + publish,网关订阅后清内存缓存、重读 Redis,无需重启网关
-
容灾/重建:网关重启或 Redis 被 flush 后,由
DynamicRouteInitRunner从 DB 全量把路由灌回 Redis,路由不丢 -
解耦:网关只认 Redis 这一份运行时视图,DB 只是配置持久化层,两个进程互不直接依赖
四、核心数据流:一条路由的“旅行”
以新增一条路由为例,完整的数据流如下:

关键设计点:
-
管理端不依赖网关运行时:管理端只负责写 Redis + 发消息,不需要知道网关是否存活
-
网关端被动刷新:网关通过 Redis 订阅收到消息后主动拉取最新路由,而不是被动接收
-
内存缓存 + Redis 双层:网关内存中维护路由缓存,收到刷新消息后先清缓存,再重新从 Redis 加载
五、表结构设计
路由配置的核心是 sys_route_conf 表:
-- ----------------------------
-- Table structure for sys_route_conf
-- ----------------------------
DROP TABLE IF EXISTS `sys_route_conf`;
CREATE TABLE `sys_route_conf`
(
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
`route_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '路由id',
`route_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '服务名称',
`predicates` json NULL COMMENT '断言',
`filters` json NULL COMMENT '过滤器',
`uri` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`sort` int NULL DEFAULT 0 COMMENT '排序',
`metadata` json NULL COMMENT '元数据',
`create_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人',
`update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_del` int NULL DEFAULT 0 COMMENT '是否已删除',
`tenant_id` int NULL DEFAULT NULL COMMENT '租户ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB
AUTO_INCREMENT = 2
CHARACTER SET = utf8mb4
COLLATE = utf8mb4_general_ci COMMENT = '系统路由'
ROW_FORMAT = DYNAMIC;
关键字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
predicates |
JSON | 断言列表,如 [{"name":"Path","args":{"pattern":"/api/**"}}] |
filters |
JSON | 过滤器列表,如 [{"name":"StripPrefix","args":{"parts":"1"}}] |
uri |
VARCHAR | 转发地址,支持 lb://service-name 负载均衡 |
metadata |
JSON | 元数据,可存放自定义扩展信息 |
六、核心代码实现
6.1 管理端:Service 层
SysRouteConfServiceImpl 中的 refreshRoute 方法是整个动态路由的核心入口:
package com.mate.cloud.admin.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mate.cloud.admin.api.dto.SysRouteConfDTO;
import com.mate.cloud.admin.api.entity.SysRouteConf;
import com.mate.cloud.admin.mapper.SysRouteConfMapper;
import com.mate.cloud.admin.service.SysRouteConfService;
import com.mate.cloud.common.core.constant.CommonConstants;
import com.mate.cloud.gateway.constant.RouteCacheConstants;
import com.mate.cloud.gateway.vo.RouteDefinitionVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.net.URI;
import java.util.List;
/**
* 系统路由
* <p>
* 管理端职责:把路由定义写入 Redis(网关 {@code RedisRouteDefinitionWriter} 读取),
* 并通过 Redis 发布订阅通知网关重载,本模块不依赖 Spring Cloud Gateway 运行时。
*
* @author mate
*/
@Slf4j
@RequiredArgsConstructor
@Service("sysRouteConfService")
public class SysRouteConfServiceImpl extends ServiceImpl<SysRouteConfMapper, SysRouteConf> implements SysRouteConfService {
private final StringRedisTemplate stringRedisTemplate;
/**
* 获取全部路由
*
* @return 未删除的路由列表
*/
@Override
public List<SysRouteConf> routes() {
SysRouteConf condition = new SysRouteConf();
condition.setIsDel(CommonConstants.STATUS_NORMAL);
return baseMapper.selectList(new QueryWrapper<>(condition));
}
/**
* 通过路由 id 删除路由:数据库 + Redis + 通知网关。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean removeByRouteId(String routeId) {
// 1. 删除数据库记录(按业务路由 id)
boolean removed = this.remove(Wrappers.<SysRouteConf>lambdaQuery().eq(SysRouteConf::getRouteId, routeId));
if (removed) {
// 2. 删除 Redis 中的路由定义
stringRedisTemplate.opsForHash().delete(RouteCacheConstants.GATEWAY_ROUTE_KEY, routeId);
// 3. 通知网关重载路由
stringRedisTemplate.convertAndSend(RouteCacheConstants.ROUTE_JVM_RELOAD_TOPIC, "route-refresh");
}
log.info("删除路由 {} 结果:{}", routeId, removed);
return removed;
}
/**
* 新增/修改单条路由:写 Redis + 落库 + 通知网关(独立事务)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean refreshRoute(SysRouteConfDTO dto) {
log.info("更新路由 ->{}", dto);
// DTO -> 实体
SysRouteConf routeConf = toEntity(dto);
// 落库:存在则更新,不存在则新增(只更新非 null 字段,避免覆盖历史数据)
String routeId = routeConf.getRouteId();
long existCount = this.count(Wrappers.<SysRouteConf>lambdaQuery().eq(SysRouteConf::getRouteId, routeId));
boolean saved;
if (existCount > 0) {
LambdaUpdateWrapper<SysRouteConf> updateWrapper = Wrappers.lambdaUpdate();
updateWrapper.eq(SysRouteConf::getRouteId, routeId);
saved = this.update(routeConf, updateWrapper);
} else {
saved = this.save(routeConf);
}
if (saved) {
// 组装网关路由定义 JSON(字段与 Spring Cloud Gateway RouteDefinition 对齐),
// 序列化后写入 Redis,网关 RedisRouteDefinitionWriter 直接反序列化使用。
//2.数据库成功之后,组装VO写入Redis
RouteDefinitionVO vo = buildRouteDefinitionVO(routeConf);
String routeJson = JSONUtil.toJsonStr(vo);
// 写 Redis:field=routeId,value=RouteDefinition JSON(供网关读取)
stringRedisTemplate.opsForHash()
.put(RouteCacheConstants.GATEWAY_ROUTE_KEY, routeConf.getRouteId(), routeJson);
}
// 通知网关重载路由(跨进程:Redis 发布订阅)
stringRedisTemplate.convertAndSend(RouteCacheConstants.ROUTE_JVM_RELOAD_TOPIC, "route-refresh");
log.debug("更新网关路由结束,已通知网关重载");
return saved;
}
/**
* DTO -> 实体(断言/过滤器/元数据 由结构化对象序列化为 JSON 字符串)
*/
private SysRouteConf toEntity(SysRouteConfDTO dto) {
SysRouteConf routeConf = new SysRouteConf();
routeConf.setRouteId(dto.getRouteId());
routeConf.setRouteName(dto.getRouteName());
routeConf.setPredicates(JSONUtil.toJsonStr(dto.getPredicates()));
routeConf.setFilters(JSONUtil.toJsonStr(dto.getFilters()));
routeConf.setMetadata(JSONUtil.toJsonStr(dto.getMetadata()));
routeConf.setUri(dto.getUri());
routeConf.setSort(dto.getOrder());
// 新路由默认未删除
routeConf.setIsDel(CommonConstants.STATUS_NORMAL);
return routeConf;
}
/**
* 实体 -> 路由定义 VO(断言 / 过滤器 / 元数据 由 JSON 字符串反序列化为网关原生对象)
* <p>
* 供 {@link #refreshRoute(SysRouteConfDTO)} 与 {@code DynamicRouteInitRunner#loadAllRouteToRedis()} 复用,
* 组装结果序列化后写入 Redis,与网关 {@code RedisRouteDefinitionWriter} 反序列化保持一致。
*
* @param routeConf 数据库路由实体
* @return 网关路由定义 VO
*/
@Override
public RouteDefinitionVO buildRouteDefinitionVO(SysRouteConf routeConf) {
RouteDefinitionVO vo = new RouteDefinitionVO();
vo.setId(routeConf.getRouteId());
vo.setRouteName(routeConf.getRouteName());
vo.setOrder(routeConf.getSort());
if (routeConf.getUri() != null) {
vo.setUri(URI.create(routeConf.getUri()));
}
if (StrUtil.isNotBlank(routeConf.getFilters())) {
JSONArray filterObj = JSONUtil.parseArray(routeConf.getFilters());
vo.setFilters(filterObj.toList(FilterDefinition.class));
}
if (StrUtil.isNotBlank(routeConf.getPredicates())) {
JSONArray predicateObj = JSONUtil.parseArray(routeConf.getPredicates());
vo.setPredicates(predicateObj.toList(PredicateDefinition.class));
}
String metadataJson = routeConf.getMetadata();
if (StrUtil.isNotBlank(metadataJson) && JSONUtil.isTypeJSONObject(metadataJson)) {
vo.setMetadata(JSONUtil.parseObj(metadataJson));
}
return vo;
}
}
三个操作的顺序很重要:先落库 → 再写 Redis → 最后发消息。如果先发消息再写 Redis,网关可能读到旧数据或空数据。
6.2 管理端:Controller 接口
package com.mate.cloud.admin.controller;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.mate.cloud.admin.api.dto.SysRouteConfDTO;
import com.mate.cloud.admin.api.entity.SysRouteConf;
import com.mate.cloud.admin.api.vo.SysRouteConfVO;
import com.mate.cloud.admin.service.SysRouteConfService;
import com.mate.cloud.common.core.util.BeanMapper;
import com.mate.cloud.common.core.util.R;
import com.mate.cloud.log.annotation.SysLog;
import com.mate.cloud.security.annotation.HasPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 系统路由
*
* @author mate
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/route")
@Tag(description = "route", name = "系统路由管理")
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class SysRouteConfController {
private final SysRouteConfService sysRouteService;
/**
* 路由列表
*
* @param sysRoute 系统路由
* @return
*/
@Operation(summary = "路由列表", description = "路由列表")
@GetMapping("/list")
public R<List<SysRouteConfVO>> list() {
List<SysRouteConf> sysRouteConfs = sysRouteService.routes();
List<SysRouteConfVO> routeConfVOS = BeanMapper.convert(sysRouteConfs, SysRouteConfVO.class);
return R.ok(routeConfVOS);
}
/**
* 根据路由 id 查询路由配置
*
* @param routeId 路由 id
* @return R 路由配置
*/
@GetMapping
@HasPermission("sys_route_view")
public R<SysRouteConfVO> getByRouteId(@RequestParam String routeId) {
SysRouteConf sysRouteConf = sysRouteService.getOne(
Wrappers.<SysRouteConf>lambdaQuery().eq(SysRouteConf::getRouteId, routeId), false);
SysRouteConfVO sysRouteConfVO = BeanMapper.convert(sysRouteConf, SysRouteConfVO.class);
return R.ok(sysRouteConfVO);
}
/**
* 通过条件查询系统路由
*
* @param sysRoute 查询条件
* @return R 对象列表
*/
@GetMapping("/details")
@HasPermission("sys_route_view")
public R getDetails(@ParameterObject SysRouteConfVO sysRouteConfVO) {
SysRouteConf sysRoute = BeanMapper.convert(sysRouteConfVO, SysRouteConf.class);
return R.ok(sysRouteService.list(Wrappers.query(sysRoute)));
}
/**
* 新增系统路由
*
* @param sysRoute 系统路由
* @return R
*/
@SysLog("新增系统路由")
@PostMapping
@HasPermission("sys_route_add")
public R save(@RequestBody SysRouteConfDTO sysRouteConfDTO) {
return R.ok(sysRouteService.refreshRoute(sysRouteConfDTO));
}
/**
* 修改系统路由
*
* @param sysRoute 系统路由
* @return R
*/
@SysLog("修改系统路由")
@PutMapping
@HasPermission("sys_route_edit")
public R<Boolean> updateById(@RequestBody SysRouteConfDTO dto) {
return R.ok(sysRouteService.refreshRoute(dto));
}
/**
* 通过路由 id 删除系统路由
*
* @param routeId 路由业务 id
* @return R
*/
@SysLog("通过路由id删除系统路由")
@DeleteMapping("/{routeId}")
@HasPermission("sys_route_del")
public R<Boolean> removeById(@PathVariable String routeId) {
return R.ok(sysRouteService.removeByRouteId(routeId));
}
}
七、网关端组件封装(mate-starter-gateway)
以上是管理端的代码,但真正让这套方案变得“开箱即用”的,是 mate-starter-gateway 组件。它把“从Redis读路由、监听刷新消息、管理路由缓存、启动时恢复路由”这些能力封装成了一个可插拔的JAR包。
7.1 组件职责
mate-starter-gateway 做了四件事:
-
路由数据源切换:把路由数据源从
application.yml切换到了 Redis Hash,网关启动或运行时都从这里读路由配置 -
热更新机制:通过 Redis Pub/Sub 监听路由变更消息,收到消息后自动刷新内存路由缓存
-
容灾恢复:提供
DynamicRouteInitRunner,网关重启后自动从 DB 全量恢复路由到 Redis -
配置开关:一行配置控制整个动态路由功能启用/关闭
7.2 组件核心类
| 类名 | 职责 |
|---|---|
RedisRouteDefinitionWriter |
实现 Spring Cloud Gateway 的 RouteDefinitionRepository,从 Redis Hash 读取路由定义 |
RedisRouteRefreshListener |
订阅 Redis 刷新消息,收到消息后清空内存缓存并触发路由刷新事件 |
RouteCacheHolder |
管理网关内存中的路由缓存,提供清空方法供刷新时调用 |
DynamicRouteInitRunner |
网关启动时从 DB 全量加载路由到 Redis,保证路由数据不丢失 |
DynamicRouteAutoConfiguration |
条件装配所有动态路由 Bean,通过配置项控制开关 |
7.3 路由数据源:RedisRouteDefinitionWriter
作用:实现Spring Cloud Gateway的RouteDefinitionRepository接口,让网关从Redis Hash读取路由定义。
package com.mate.cloud.gateway.support;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.fastjson2.JSON;
import com.google.common.collect.Lists;
import com.mate.cloud.gateway.vo.RouteDefinitionVO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import static com.mate.cloud.gateway.constant.RouteCacheConstants.GATEWAY_ROUTE_KEY;
import static com.mate.cloud.gateway.constant.RouteCacheConstants.ROUTE_JVM_RELOAD_TOPIC;
/**
* @description: 保存路由信息
* <p>
* https://blog.51cto.com/abcd/4541618
* </p>
* <p>
* redis 保存路由信息,优先级比配置文件高
* </p>
* @author: MI
* @createDate: 2026/5/18 9:10
* @updateUser: MI
* @updateDate: 2026/5/18 9:10
* @updateRemark: 修改内容
* @version: v1.0
*/
@Slf4j
@Component
public class RedisRouteDefinitionWriter implements RouteDefinitionRepository {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 动态路由入口
* <p>
* 1. 先从内存中获取 2. 为空加载Redis中数据 3. 更新内存
* <p/>
* @return
*/
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
List<RouteDefinitionVO> routeList = RouteCacheHolder.getRouteList();
if (CollUtil.isNotEmpty(routeList)) {
log.debug("内存 中路由定义条数: {}, {}", routeList.size(), routeList);
return Flux.fromIterable(routeList);
}
// 管理端使用 StringRedisTemplate 以 JSON 字符串写入,此处 value 即为 JSON 字符串,直接解析即可。
List<RouteDefinitionVO> routeDefinitions = Lists.newArrayList();
stringRedisTemplate.opsForHash()
.values(GATEWAY_ROUTE_KEY)
.stream()
.forEach(route -> routeDefinitions.add(JSON.parseObject(route.toString(), RouteDefinitionVO.class)));
log.debug("redis 中路由定义条数: {}, {}", routeDefinitions.size(), routeDefinitions);
RouteCacheHolder.refreshRouteList(routeDefinitions);
// 返回路由
return Flux.fromIterable(routeDefinitions);
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap(routeDefinition -> {
RouteDefinitionVO vo = new RouteDefinitionVO();
BeanUtils.copyProperties(routeDefinition, vo);
log.info("保存路由信息{}", routeDefinition);
stringRedisTemplate.opsForHash().put(GATEWAY_ROUTE_KEY, routeDefinition.getId(), JSON.toJSONString(vo));
stringRedisTemplate.convertAndSend(ROUTE_JVM_RELOAD_TOPIC, "新增路由信息,网关缓存更新");
return Mono.empty();
});
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap(id -> {
if (Boolean.TRUE.equals(stringRedisTemplate.opsForHash().hasKey(GATEWAY_ROUTE_KEY, id))) {
log.info("删除路由信息{}", id);
stringRedisTemplate.opsForHash().delete(GATEWAY_ROUTE_KEY, id);
RouteCacheHolder.removeRouteList();
return Mono.empty();
}
return Mono.defer(() -> Mono.error(new RuntimeException("RouteDefinition not found, " + id + "")));
});
}
}
7.4 刷新监听器:RedisRouteRefreshMessageListener
作用:订阅Redis的gateway:jvm:route:reload:topic频道,收到消息后清空内存缓存并触发路由刷新。
package com.mate.cloud.gateway.listener;
import com.mate.cloud.gateway.event.RouteEventPublisher;
import com.mate.cloud.gateway.support.RouteCacheHolder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;
/**
* Redis路由变更消息监听器
* <p>
* 监听Redis发布订阅消息,当后台管理系统修改网关路由配置后, 通过Redis channel推送变更通知,网关收到消息后清空内存路由缓存并发布路由刷新事件,
* 触发网关重新加载全部路由定义,实现多网关实例路由动态刷新。
* </p>
*
* @author: MI
* @createTime: 2026/8/22 19:43
* @updateUser: MI
* @updateTime: 2026/8/22 19:43
* @updateRemark: 修改内容
* @version: 1.0
*/
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public class RedisRouteRefreshMessageListener implements MessageListener {
private final RouteEventPublisher routeEventPublisher;
/**
* 接收Redis发布‑订阅通道消息
* @param message redis消息体
* @param pattern 匹配的channel模式
*/
@Override
public void onMessage(Message message, byte[] pattern) {
String routeId = new String(message.getBody());
log.info("收到路由变更通知,routeId: {}", routeId);
// 先清空内存缓存,再发布刷新事件,避免命中旧路由
RouteCacheHolder.removeRouteList();
routeEventPublisher.refreshRoutes();
}
}
关键点:
-
使用
@ConditionalOnWebApplication(type = REACTIVE),只在 Reactive 网关应用(非Servlet管理端)生效 -
管理端即使引入依赖也不会被装配
7.5 RouteCacheHolder —— 路由缓存管理
作用:管理网关内存中的路由缓存,提供清空方法供刷新时调用。
package com.mate.cloud.gateway.support;
import cn.hutool.cache.Cache;
import cn.hutool.cache.CacheUtil;
import com.google.common.collect.Lists;
import com.mate.cloud.gateway.vo.RouteDefinitionVO;
import lombok.experimental.UtilityClass;
import java.util.List;
/**
* @description: 路由缓存工具类
* @createDate: 2026/8/18 9:43
* @updateDate: 2026/8/18 9:43
* @updateRemark: 修改内容
* @version: v1.0
*/
@UtilityClass
public class RouteCacheHolder {
private Cache<String, RouteDefinitionVO> cache = CacheUtil.newLFUCache(50);
/**
* 获取缓存的全部对象
* @return routeList
*/
public List<RouteDefinitionVO> getRouteList() {
List<RouteDefinitionVO> routes = Lists.newArrayList();
cache.forEach(route -> routes.add(route));
return routes;
}
/**
* 更新路由缓存
* @param routeDefinitionVoList
*/
public void refreshRouteList(List<RouteDefinitionVO> routeDefinitionVoList) {
routeDefinitionVoList.forEach(route -> cache.put(route.getId(), route));
}
/**
* 清空路由缓存
*/
public void removeRouteList() {
cache.clear();
}
}
7.6 网关路由事件发布器
package com.mate.cloud.gateway.event;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
/**
* 网关路由事件发布器
* <p>
* 对外提供方法,手动发布{@link RefreshRoutesEvent}事件, 触发Spring Cloud Gateway重新加载路由定义,用于本地直接触发路由刷新。
* </p>
*
* @author: MI
* @createTime: 2026/8/22 19:47
* @updateUser: MI
* @updateTime: 2026/8/22 19:47
* @updateRemark: 修改内容
* @version: 1.0
*/
@Component
@RequiredArgsConstructor
public class RouteEventPublisher {
private final ApplicationEventPublisher publisher;
/**
* 发布路由刷新事件,网关重新加载全部路由
*/
public void refreshRoutes() {
// 发布刷新事件,网关会自动监听并重新加载路由
publisher.publishEvent(new RefreshRoutesEvent(this));
}
}
7.7 动态路由配置类
package com.mate.cloud.gateway.configuration;
import com.mate.cloud.gateway.constant.RouteCacheConstants;
import com.mate.cloud.gateway.listener.RedisRouteRefreshMessageListener;
import io.lettuce.core.ReadFrom;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.config.PropertiesRouteDefinitionLocator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import java.time.Duration;
/**
* 动态路由配置类
*
* @link <a href=
* "https://github.com/spring-cloud/spring-cloud-gateway/blob/main/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java">spring-cloud-gateway</a>
* @author: MI
* @createDate: 2026/5/18 8:39
* @updateUser: MI
* @updateDate: 2026/5/18 8:39
* @updateRemark: 修改内容
* @version: v1.0
*/
@Configuration
@ComponentScan({ "com.mate.cloud.gateway" })
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnProperty(name = "gateway.dynamic-route.enabled", havingValue = "true", matchIfMissing = true)
public class DynamicRouteAutoConfiguration {
/**
* 配置文件设置为空 redis 加载为准
* <p>
* 加 {@link ConditionalOnMissingBean} 避免与 Spring Cloud Gateway 内置的同名 Bean 冲突:
* 网关自动装配已提供时,使用内置实现。
* @return
*/
@Bean
@ConditionalOnMissingBean(PropertiesRouteDefinitionLocator.class)
public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator() {
return new PropertiesRouteDefinitionLocator(new GatewayProperties());
}
/**
* redis监听器配置
* <p>
* 监听管理端发布的路由刷新频道,收到通知后交由 {@link RedisRouteRefreshMessageListener} 处理: 清空内存缓存并发布路由刷新事件,由
* Spring Cloud Gateway 内置的 RouteRefreshListener 重置路由定位器,实现动态重载。
* @param redisConnectionFactory redis配置
* @param redisRouteMessageListener 路由变更消息监听器
* @return
*/
@Bean
public RedisMessageListenerContainer redisContainer(RedisConnectionFactory redisConnectionFactory,
RedisRouteRefreshMessageListener redisRouteMessageListener) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
container.addMessageListener(redisRouteMessageListener,
new ChannelTopic(RouteCacheConstants.ROUTE_JVM_RELOAD_TOPIC));
return container;
}
@Bean
@ConditionalOnProperty(value = "spring.redis.cluster.enable", havingValue = "true")
public LettuceConnectionFactory redisConnectionFactory(DataRedisProperties redisProperties) {
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(
redisProperties.getCluster().getNodes());
// https://github.com/lettuce-io/lettuce-core/wiki/Redis-Cluster#user-content-refreshing-the-cluster-topology-view
ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
.enablePeriodicRefresh()
.enableAllAdaptiveRefreshTriggers()
.refreshPeriod(Duration.ofSeconds(5))
.build();
ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
.topologyRefreshOptions(clusterTopologyRefreshOptions)
.build();
// https://github.com/lettuce-io/lettuce-core/wiki/ReadFrom-Settings
LettuceClientConfiguration lettuceClientConfiguration = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED)
.clientOptions(clusterClientOptions)
.build();
return new LettuceConnectionFactory(redisClusterConfiguration, lettuceClientConfiguration);
}
}
7.8 启动初始化器:DynamicRouteInitRunner
作用:管理端启动时,从DB全量加载路由配置并写入Redis,保证路由数据不丢失。
package com.mate.cloud.admin.config;
import cn.hutool.json.JSONUtil;
import com.mate.cloud.admin.api.entity.SysRouteConf;
import com.mate.cloud.admin.service.SysRouteConfService;
import com.mate.cloud.gateway.constant.RouteCacheConstants;
import com.mate.cloud.gateway.vo.RouteDefinitionVO;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 容器启动后保存配置文件里面的路由信息到Redis
* @author: MI
* @email: 448341911@qq.com
* @createTime: 2026/8/24 22:36
* @updateUser: MI
* @updateTime: 2026/8/24 22:36
* @updateRemark: 修改内容
* @version: 1.0
*/
@Slf4j
@Configuration
@AllArgsConstructor
public class DynamicRouteInitRunner implements ApplicationRunner {
private final StringRedisTemplate stringRedisTemplate;
private final SysRouteConfService sysRouteService;
/**
* 项目启动完成回调:加载数据库路由写入Redis
*/
@Override
public void run(@NotNull ApplicationArguments args) {
log.info("项目启动,同步加载路由到redis,保证就绪后再对外服务");
loadAllRouteToRedis();
}
/**
* 全量从数据库加载路由写入Redis Hash(Pipeline流水线 + 差异更新,不删除整个hash)
* 启动调用 / Redis订阅消息触发调用
*/
public void loadAllRouteToRedis() {
try {
List<SysRouteConf> routeList = sysRouteService.routes();
final byte[] hashKeyBytes = RouteCacheConstants.GATEWAY_ROUTE_KEY.getBytes(StandardCharsets.UTF_8);
Set<String> dbRouteIdSet = new HashSet<>();
for (SysRouteConf route : routeList) {
dbRouteIdSet.add(route.getRouteId());
}
// 获取Redis中全部field,计算需要删除的残留路由
Set<Object> redisRouteIdObjs = stringRedisTemplate.opsForHash().keys(RouteCacheConstants.GATEWAY_ROUTE_KEY);
Set<byte[]> needDeleteFields = new HashSet<>();
if (redisRouteIdObjs != null) {
for (Object obj : redisRouteIdObjs) {
String routeId = (String) obj;
if (!dbRouteIdSet.contains(routeId)) {
needDeleteFields.add(routeId.getBytes(StandardCharsets.UTF_8));
}
}
}
// pipeline批量hset写入 + hdel删除残留field
stringRedisTemplate.executePipelined((RedisCallback<Void>) connection -> {
for (SysRouteConf route : routeList) {
String routeId = route.getRouteId();
RouteDefinitionVO vo = sysRouteService.buildRouteDefinitionVO(route);
String jsonStr = JSONUtil.toJsonStr(vo);
byte[] fieldBytes = routeId.getBytes(StandardCharsets.UTF_8);
byte[] valueBytes = jsonStr.getBytes(StandardCharsets.UTF_8);
connection.hSet(hashKeyBytes, fieldBytes, valueBytes);
log.info("加载路由ID:{}", routeId);
}
if (!needDeleteFields.isEmpty()) {
connection.hDel(hashKeyBytes, needDeleteFields.toArray(new byte[0][]));
log.info("pipeline清除Redis残留路由field数量:{}", needDeleteFields.size());
}
return null;
});
log.debug("初始化网关路由结束,本次加载路由数量:{}", routeList.size());
} catch (Exception e) {
log.error("全量加载网关路由到Redis失败", e);
}
}
/**
* redis 监听配置,监听 gateway_redis_route_reload_topic,重新加载Redis
*
* @param redisConnectionFactory redis 配置
* @return RedisMessageListenerContainer
*/
@Bean
public RedisMessageListenerContainer redisContainer(RedisConnectionFactory redisConnectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
container.addMessageListener((message, bytes) -> {
log.warn("接收到Redis重新加载路由事件");
loadAllRouteToRedis();
}, new ChannelTopic(RouteCacheConstants.ROUTE_REDIS_RELOAD_TOPIC));
return container;
}
}
7.8 配置开关
gateway:
dynamic-route:
enabled: true # true=开启动态路由(默认),false=关闭
@Configuration
@ConditionalOnProperty(
prefix = "gateway.dynamic-route",
name = "enabled",
havingValue = "true",
matchIfMissing = true // 缺省开启
)
public class DynamicRouteAutoConfiguration {
// 自动装配所有动态路由相关Bean
}
引入依赖后,无需在启动类加任何注解,组件通过配置项开关控制装配(缺省即开启)。
八、管理端如何使用组件
管理端(mate-admin-biz)引入 mate-starter-gateway 依赖后,通过 SysRouteConfService 操作路由:
<!-- 管理端 pom.xml -->
<dependency>
<groupId>com.mate</groupId>
<artifactId>mate-starter-gateway</artifactId>
</dependency>
管理端的 refreshRoute 方法调用组件提供的 RedisTemplate 完成写入和发布,字段常量也统一从组件中获取:
redisTemplate.opsForHash().put(routeKey, routeId, routeValue); redisTemplate.convertAndSend(topic, routeId);
九、列表查询为什么直查 DB?
这是一个容易被误解的设计点。GET /route/list 直查 DB 而非 Redis,是刻意设计:
| 原因 | 说明 |
|---|---|
| Redis 存的是运行时视图 | Redis 里存的是 RouteDefinitionVO,天然只有「未删除」路由,缺少 isDel 字段 |
| 字段映射有缺口 | Redis 存的字段与配置视图有映射缺口(routeId ↔ id、sortOrder ↔ order) |
| 管理端需要展示配置视图 | 列表要展示的是 SysRouteConf(删除态、排序、原始 JSON 字段) |
| 低频操作,无需缓存 | 列表是低频配置管理操作,直查 DB 没有性能压力,无需为此引入缓存一致性复杂度 |
三层职责清晰划分:
DB sys_route_conf → 配置权威源(管理端 CRUD、列表展示) Redis Hash gateway:route:key → 网关运行时数据源(管理端写、网关读) DynamicRouteInitRunner → DB → Redis 的同步桥(启动/重载时执行)
十、配置方式
10.1 开启 Gateway Endpoint(查看当前路由)
management:
endpoints:
web:
exposure:
include: gateway
endpoint:
gateway:
enabled: true
开启后可通过 /actuator/gateway/routes 查看当前生效的所有路由,方便排查问题。
10.2 动态路由开关
gateway:
dynamic-route:
enabled: true # true=开启动态路由(默认),false=关闭
十一、最佳实践与避坑指南
11.1 先落库再写 Redis
顺序不能乱:先落库 → 再写 Redis → 最后发消息。如果网关收到消息时 Redis 还没写入,会读到旧数据或空数据。
11.2 管理端不依赖网关存活
管理端只负责写 Redis + 发消息,不需要知道网关是否存活。网关重启后,会通过 DynamicRouteInitRunner 从 DB 全量恢复路由。
11.3 网关端内存缓存 + Redis 双层
网关收到刷新消息后,先清内存缓存,再重新从 Redis 加载。这样可以保证路由更新的原子性。
11.4 配置项缺省开启
gateway.dynamic-route.enabled 缺省为 true,无需额外配置即可使用。
11.5 列表查询不走 Redis
管理端的路由列表查询直查 DB,不查 Redis。这是为了避免缓存一致性问题,且列表是低频操作。
十二、总结
| 核心要点 | 说明 |
|---|---|
| 数据源 | Redis Hash 作为运行时路由数据源,DB 作为配置权威源 |
| 热更新机制 | 管理端写 Redis + PUBLISH,网关端 SUBSCRIBE 后清缓存、重载 |
| 容灾能力 | 网关重启后通过 DynamicRouteInitRunner 从 DB 全量恢复 |
| 解耦设计 | 管理端不依赖网关存活,网关只认 Redis |
| 组件封装 | mate-common-gateway 将路由数据源、刷新监听、缓存管理、启动恢复全部封装成可插拔 JAR |
| 配置开关 | gateway.dynamic-route.enabled 一行配置控制开关 |
| 适用场景 | 需要频繁调整路由规则、多环境路由统一管理、金丝雀发布 |
一句话总结:mate-starter-gateway 把“从Redis读路由、监听刷新消息、管理路由缓存、启动时恢复路由”这些能力封装成了一个可插拔组件,管理端只需要引入依赖、调用 refreshRoute 方法即可。网关端的路由热更新逻辑对业务开发完全透明,从此告别改路由要重启的噩梦。
兄弟们,你们生产环境的路由是怎么管理的?还在用配置文件硬编码吗?
浙公网安备 33010602011771号