一、前文必看
Spring Cloud微服务网关Zuul动态路由配置。在前文中留了两个小坑。在本文将怕它给填了,所以前一篇文章建议看一下。
二、DynamicZuulRouteLocator小优化
在前文中提到,HeartbeatEvent
事件会频繁触发,每次都需要去查询数据库。而且ZuulRefreshListener
监听的其余四个刷新事件也不会经常触发。所以这里就可以做一下小优化,因为系统上线稳定后,路由一般是不会经常变动的。所以我们可以把路由规则配置给缓存起来,这样就不会每次的心跳都去查询数据库。
这里有分两种情况:
- 单机Zuul网关下的本地缓存
- Zuul网关集群下的分布式缓存
2.1 本地缓存
直接上代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
public class DynamicZuulRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
private final ZuulProperties zuulProperties;
private final RoutingRuleService routingRuleService;
private static final ConcurrentHashMap<String, ZuulRoute> routesCache = new ConcurrentHashMap<>();
public DynamicZuulRouteLocator(String servletPath, ZuulProperties properties, RoutingRuleService routingRuleService) {
super(servletPath, properties);
this.zuulProperties = properties;
this.routingRuleService = routingRuleService;
}
@Override
public void refresh() {
doRefresh();
}
@Override
protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
Map<String, ZuulProperties.ZuulRoute> allRoutes = null;
if (routesCache.isEmpty()) {
allRoutes = routingRuleService.findAllRoutes();
routesCache.putAll(allRoutes);
} else {
allRoutes = routesCache;
}
LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>();
// 父类的 SimpleRouteLocator的locateRoutes()读取的是配置文件中的路由规则配置
routesMap.putAll(super.locateRoutes());
routesMap.putAll(allRoutes);
LinkedHashMap<String, ZuulProperties.ZuulRoute> values = new LinkedHashMap<>();
routesMap.forEach((key, value) -> {
String path = key;
if (!path.startsWith("/")) {
path = "/" + path;
}
if (StringUtils.hasText(this.zuulProperties.getPrefix())) {
path = this.zuulProperties.getPrefix() + path;
if (!path.startsWith("/")) {
path = "/" + path;
}
}
values.put(path, value);
});
return values;
}
public static void clearCache() {
routesCache.clear();
}
}
|
这是简单地使用了ConcurrentHashMap
作为本地缓存。当然也可以使用caffeine
、Guava Cache
、Ehcache
等本地缓存框架。使用ConcurrentHashMap
主要是更加方便快捷,简单明了。例如像RocketMQ
也有使用了ConcurrentHashMap
来缓存路由信息。
这样在数据库面前就挡了层缓存,优化了查询性能,不需要每次的心跳都去查数据库。
2.2 Redis分布式缓存
一般情况下为了高可用,甚至是高并发的情况下,我们的微服务网关都是以集群的形式在线上部署的。上面的本地缓存并不适合在分布式的环境下使用。当然也已使用Redis作为一级缓存,然后本地缓存作为二级缓存。
这时候情况就复杂起来了。这里我画了一个简单的架构图设计图:
有两种设计方案:
- 方案1:Zuul集成路由规则的CRUD接口。对于路由管理或者说网关管理的接口集成在Zuul网关中,Zuul网关配置数据源。对路由CRUD的操作通过负载均衡到Zuul网关,然后更新Redis缓存。这样的话便不能使用本地缓存了,容易出现数据不一致的情况,当然如果能够维护好一级缓存和二级缓存的话,也是可以。
- 这种设计缺点较多,首先以来了Redis,提高了整个网关的复杂度,可用性也随之降低。
- 其次业务耦合度较高,依赖数据源,因为需要对外提供了系列接口。
- 方案2:单独一个管理后台应用,在这里对数据源进行操作。当路由信息发生改变的时候通过
WebSocket
、Zookeeper
、Nacos
或者Consul
等手段来通知刷新本地缓存的路由信息。
- 这样的话Zuul网关专注做路由转发,把业务解耦出来,设计更加的轻量级。Apache Shenyu网关就是这样做的。
三、实现手动触发路由刷新
看到这里应该知道,对于动态路由配置,我们是要提供一系列的接口来进行维护和操作的。当我们对路由进行了增删改后,是要去刷新Zuul中的路由信息的。按照这样手动修改路由信息触发的方式可以实现的有两种:全量和增量。
接下来的实现是以上面章节所说的方案1来做的,也没有使用分布式缓存,把源码中的本地缓存替换成Redis
就行了,主要是因为篇幅受限以及我们主要学习的是实现的原理。请勿直接用在生产上,仅供学习。
3.1 全量触发
OK,直接入正题。这里准备一些接口,从Controller到Service。
RoutingRuleController
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
@RestController
@RequestMapping("route")
public class RoutingRuleController {
@Autowired
private RoutingRuleService routingRuleService;
@PostMapping("add")
public Object addRoute(@RequestBody RoutingRule routingRule) {
routingRuleService.save(routingRule);
return "success";
}
@PostMapping("update")
public Object updateRoute(@RequestBody RoutingRule routingRule) {
routingRuleService.save(routingRule);
return "success";
}
@DeleteMapping("delete")
public Object updateRoute(Long id) {
routingRuleService.delete(id);
return "success";
}
}
|
接口:
1
2
3
4
5
6
7
8
|
public interface RoutingRuleService {
Map<String, ZuulProperties.ZuulRoute> findAllRoutes();
void save(RoutingRule routingRule);
void delete(Long id);
}
|
接口实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
@Service
public class RoutingRuleServiceImpl implements RoutingRuleService {
@Autowired
private RoutingRuleDao routingRuleDao;
@Autowired
private ApplicationEventPublisher publisher;
@Override
public Map<String, ZuulProperties.ZuulRoute> findAllRoutes() {
RoutingRule routingRule = new RoutingRule();
routingRule.setEnabled(1);
Example<RoutingRule> example = Example.of(routingRule);
List<RoutingRule> routingRuleList = routingRuleDao.findAll(example);
Map<String, ZuulProperties.ZuulRoute> zuulRouteMap = new LinkedHashMap<>();
routingRuleList.stream().filter(item -> StringUtils.isNotBlank(item.getPath()))
.forEach(item -> {
ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
BeanUtils.copyProperties(item, zuulRoute);
zuulRoute.setId(String.valueOf(item.getId()));
zuulRouteMap.put(item.getPath(), zuulRoute);
});
return zuulRouteMap;
}
@Override
public void save(RoutingRule routingRule) {
RoutingRule rule = routingRuleDao.saveAndFlush(routingRule);
publisher.publishEvent(new RefreshRouteEvent(rule));
}
@Override
public void delete(Long id) {
routingRuleDao.deleteById(id);
RoutingRule routingRule = new RoutingRule();
routingRule.setId(id);
publisher.publishEvent(new RefreshRouteEvent(routingRule));
}
}
|
从代码可以看到,在路由信息发生变更的时候,我们是通过Spring的事件发布,去通知Listener去完成剩下的操作的。这样可以实现业务上的解耦。
其实也不一定是要用Spring的事件发布,可以使用Disruptor
框架来实现高性能内存队列;当然也可以参考Apache Shenyu里面实现的disruptor
。我这里为了方便就使用了Spring的事件发布了。
定义一个事件RefreshRouteEvent
:
1
2
3
4
5
6
|
public class RefreshRouteEvent extends ApplicationEvent {
public RefreshRouteEvent(Object source) {
super(source);
}
}
|
还有监听器RefreshRouteListener
: 这里的监听器做法有很多
- 像下面代码里面那样直接清空
DynamicZuulRouteLocator
里的本地缓存,然后发布路由刷新事件,让ZuulRefreshListener
去刷新缓存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Component
public class RefreshRouteListener implements ApplicationListener<ApplicationEvent> {
@Autowired
private ApplicationEventPublisher publisher;
@Autowired
private RouteLocator routeLocator;
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof RefreshRouteEvent) {
DynamicZuulRouteLocator.clearCache();
publisher.publishEvent(new RoutesRefreshedEvent(routeLocator));
}
}
}
|
- 可以在
RefreshRouteListener
里面把数据库里面的路由查询出来,然后设置到DynamicZuulRouteLocator
的本地缓存中。
3.2 增量触发
每次修改单个路由配置就触发全量的路由加载,这很明显不符合我们程序开发的追求。但是SimpleRouteLotor
的方法里面并没有添加单个或删除单个路由配置的方法。我们再看一下之前的类图:
显然还有DiscoveryClientRouteLocator
和CompositeRouteLocator
,去这两个的源码会看到DiscoveryClientRouteLocator
就是我们想要找的那个类。至于CompositeRouteLocator
是整合Spring容器中所有的RouteLocator
,它的操作就是遍历所有的RouteLocator
去操作路由。
ZuulRefreshListener
监听器的刷新执行的方法是org.springframework.cloud.netflix.zuul.filters.CompositeRouteLocator#refresh
,可以看到是遍历所有的RouteLocator
执行refresh操作。
1
2
3
4
5
6
7
8
9
10
11
|
public class CompositeRouteLocator implements RefreshableRouteLocator {
...
@Override
public void refresh() {
for (RouteLocator locator : routeLocators) {
if (locator instanceof RefreshableRouteLocator) {
((RefreshableRouteLocator) locator).refresh();
}
}
}
}
|
DiscoveryClientRouteLocator
在ZuulProxyAutoConfiguration
被注入到了Spring容器。使用的时候直接@Autowired
引用就行。
所以有两种写法:
- 使用
DiscoveryClientRouteLocator
- 修改
DynamicZuulRouteLocator
这里使用第二种写法,接下来修改代码:
DynamicZuulRouteLocator
添加方法removeRoute(String id)
和addRoute(ZuulRoute zuulRoute)
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class DynamicZuulRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
...同上
public void removeRoute(String id) {
for (String path : routesCache.keySet()) {
ZuulRoute zuulRoute = routesCache.get(path);
if (org.apache.commons.lang3.StringUtils.equals(id, zuulRoute.getId())) {
routesCache.remove(path);
// 刷新 SimpleRouteLocator 里的路由信息
refresh();
}
}
}
public void addRoute(ZuulRoute zuulRoute) {
routesCache.put(zuulRoute.getPath(), zuulRoute);
// 刷新 SimpleRouteLocator 里的路由信息
refresh();
}
}
|
因为最终所有的路由信息是保存在SimpleRouteLocator
里的private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>();
,所以子类修改了路由信息需要刷新到SimpleRouteLocator
;
修改事件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class RefreshRouteEvent extends ApplicationEvent {
private boolean isDelete = false;
public RefreshRouteEvent(Object source) {
super(source);
}
public boolean isDelete() {
return isDelete;
}
public void setDelete(boolean delete) {
isDelete = delete;
}
}
|
修改service的delete方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Service
public class RoutingRuleServiceImpl implements RoutingRuleService {
...
@Override
public void delete(Long id) {
routingRuleDao.deleteById(id);
RoutingRule routingRule = new RoutingRule();
routingRule.setId(id);
RefreshRouteEvent refreshRouteEvent = new RefreshRouteEvent(routingRule);
refreshRouteEvent.setDelete(true);
publisher.publishEvent(refreshRouteEvent);
}
}
|
最后修改监听器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
@Component
public class RefreshRouteListener implements ApplicationListener<ApplicationEvent> {
@Autowired
private DynamicZuulRouteLocator dynamicZuulRouteLocator;
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof RefreshRouteEvent) {
// 全量加载路由信息
//DynamicZuulRouteLocator.clearCache();
//publisher.publishEvent(new RoutesRefreshedEvent(routeLocator));
// 增量修改路由信息
RefreshRouteEvent refreshRouteEvent = (RefreshRouteEvent) event;
RoutingRule routingRule = (RoutingRule) event.getSource();
if (refreshRouteEvent.isDelete()) {
dynamicZuulRouteLocator.removeRoute(String.valueOf(routingRule.getId()));
} else {
ZuulRoute zuulRoute = new ZuulRoute();
BeanUtils.copyProperties(routingRule, zuulRoute);
zuulRoute.setId(String.valueOf(routingRule.getId()));
dynamicZuulRouteLocator.addRoute(zuulRoute);
}
}
}
}
|
四、总结
通过两篇文章讲述了Zuul网关的动态路由配置的原理和实战,RouteLocator
接口的类图并不复杂,很多Zuul的组件在ZuulServerAutoConfiguration
和ZuulProxyAutoConfiguration
里面注入到了Spring容器,可以看一下里面都有什么组件,并分析一下其功能,能够很快地理解Zuul的一些功能原理。