基于数据库实现微服务动态路由

一、概述

随着微服务在各个项目中使用越来越普遍,大多数公司选择使用Spring Cloud框架或者Dubbo来实现项目落地。

因此,在说动态路由之前,笔者先带大家来对比下这两个框架。

  • 从性能效率上来看:dubbo 使用了 netty 比 spring cloud 的 http 快很多。大概是 2 倍的差距。
  • 从开发效率上来看:公司同学一般都会 spring,入门 spring cloud 的门槛低且与 spring 融合度更高。dubbo 相对会花一些学习成本。
  • 从社区活跃度来看:spring cloud 的活跃度明显高于 dubbo。而后续 dubbo 看到了微服务的流行,也逐渐恢复了更新。
  • 从生态圈上来看:spring cloud 周边组件很多,通过 starter 自动装配即拿即用。而 dubbo 得自己集成其他的框架。

笔者所在的公司选择的Spring Cloud技术栈,主要有以下两个原因 :

  1. 对于性能没有极致的要求。
  2. 想尽快产出,减少学习成本,增加开发效率,减少企业成本。

网关分为Zuul,Kong,Nginx代理以及自研四种,一般中小公司使用Zuul即可。接着我们来了解下网关的作用,主要包括:动态路由,灰度发布,鉴权认证,性能监控,限流熔断等。

二、搭建基础环境

首先我们得去搭建三个服务,分别为注册中心:eureka-server,网关中心:zuul-gateway,还有个就是应用服务,就以库存服务为例:inventory-api。

在网关中配置路由,传统的做法如下:

zuul:
  prefix: /api # 添加路由前缀
  retryable: true
  sensitive-headers:
  add-host-header: true
  routes:
    inventory-api:
      path: /inventory/**
      service: inventory-api
      strip-prefix: true

试想,随着我们的应用可能越来越多,如果在生产环境通过暂停网关服务,修改网关的路由配置,接着上线,对于很多公司的业务来说,会有较大的影响。因此我们就得考虑改造Zuul网关,通过动态路由来将我们的服务实现路由配置。

我们先来在库存服务中,新增一个简单的API接口

package com.calvin.api;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Title DemoController
 * @Description Demo测试
 * @author calvin
 * @date: 2020/3/11 4:59 PM 
 */
@RestController
@RequestMapping("demo")
public class DemoController {

    @GetMapping("test")
    public String demoTest() {
        return "SUCCESS";
    }

}

先测试不通过路由访问:http://localhost:10020/demo/test
在这里插入图片描述
接着根据网关服务里面配置好的路由,来根据路由访问:http://localhost:10010/api/inventory/demo/test
在这里插入图片描述

三、改造网关服务

首先我们得先将刚才在网关服务里面配置的路由注释掉。
在这里插入图片描述
设计思路:网关服务定时去读取数据库中新定义的路由表,通过路由表中的数据,实现路由管理。

定义路由表:gateway_api_route,并插入一条数据,数据为inventory-api服务。

DROP TABLE IF EXISTS `gateway_api_route`;
CREATE TABLE `gateway_api_route` (
  `id` tinyint(10) NOT NULL AUTO_INCREMENT,
  `path` varchar(255) NOT NULL,
  `service_id` varchar(50) DEFAULT NULL,
  `url` varchar(255) DEFAULT NULL,
  `retryable` tinyint(1) DEFAULT NULL,
  `enabled` tinyint(1) NOT NULL,
  `strip_prefix` int(11) DEFAULT NULL,
  `api_name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
 
INSERT INTO `gateway_api_route`(`id`, `path`, `service_id`, `url`, `retryable`, `enabled`, `strip_prefix`, `api_name`) VALUES (1, '/inventory/**', 'inventory-api', '', 1, 1, 1, 'inventory-api');

接着就是代码实现部分了。(使用通用tk-mapper操作数据库,详见代码)

(1)定义实体类

package com.calvin.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Data;
import tk.mybatis.mapper.annotation.KeySql;

@Data
@Entity
@Table(name = "gateway_api_route")
public class GatewayApiRoute {

    @Id
    @KeySql(useGeneratedKeys = true)
    @GeneratedValue
    private String id;

    private String path;

    private String serviceId;

    private String url;

    private boolean stripPrefix = true;

    private Boolean retryable;

    private Boolean enabled;

}

(2)获取路由配置信息

package com.calvin.config;

import com.calvin.entity.GatewayApiRoute;
import com.calvin.mapper.GatewayApiRouteMapper;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.StringUtils;

/**
 * 获取路由配置信息
 */
@Slf4j
public class DynamicRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {

    private ZuulProperties properties;

    @Autowired
    private GatewayApiRouteMapper gatewayApiRouteMapper;

    public DynamicRouteLocator(String servletPath, ZuulProperties properties) {
        super(servletPath, properties);
        this.properties = properties;
    }

    @Override
    public void refresh() {
        doRefresh();
    }

    /**
     * 修改路由对象信息
     * @return
     */
    @Override
    protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
        LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>();
        // 加载application.yml中的路由表
        routesMap.putAll(super.locateRoutes());
        // 加载db中的路由表
        routesMap.putAll(locateRoutesFromDB());
        // 统一处理一下路由path的格式
        LinkedHashMap<String, ZuulProperties.ZuulRoute> values = new LinkedHashMap<>();
        for (Map.Entry<String, ZuulProperties.ZuulRoute> entry : routesMap.entrySet()) {
            String path = entry.getKey();
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            if (StringUtils.hasText(this.properties.getPrefix())) {
                path = this.properties.getPrefix() + path;
                if (!path.startsWith("/")) {
                    path = "/" + path;
                }
            }
            log.info("path:{},value:{}", path, entry.getValue());
            values.put(path, entry.getValue());
        }
        return values;
    }

    /**
     * 从数据库读取zuul路由规则
     * @return
     */
    private Map<String, ZuulProperties.ZuulRoute> locateRoutesFromDB() {
        Map<String, ZuulProperties.ZuulRoute> routes = new LinkedHashMap<>();
        GatewayApiRoute gatewayApiRoute = new GatewayApiRoute();
        gatewayApiRoute.setEnabled(Boolean.TRUE);
        // 查询所有生效的路由
        List<GatewayApiRoute> results = gatewayApiRouteMapper.select(gatewayApiRoute);
        for (GatewayApiRoute result : results) {
            if (StringUtils.isEmpty(result.getPath())) {
                continue;
            }
            if (StringUtils.isEmpty(result.getServiceId()) && StringUtils.isEmpty(result.getUrl())) {
                continue;
            }
            ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
            try {
                BeanUtils.copyProperties(result, zuulRoute);
            } catch (Exception e) {
                e.printStackTrace();
            }
            routes.put(zuulRoute.getPath(), zuulRoute);
        }
        return routes;
    }

}

(3)声明Bean

package com.calvin.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DynamicRouteConfiguration {

    @Autowired
    private ZuulProperties zuulProperties;

    @Autowired
    private ServerProperties server;

    @Bean
    public DynamicRouteLocator routeLocator() {
        return new DynamicRouteLocator(server.getServlet().getServletPrefix(), this.zuulProperties);
    }

}

(4)定时从数据库和配置文件中读取配置

package com.calvin.task;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.RoutesRefreshedEvent;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * 定时从数据库和配置文件中读取配置
 */
@Component
@Configuration
@EnableScheduling
public class RefreshRouteTask {

    @Autowired
    private ApplicationEventPublisher publisher;

    @Autowired
    private RouteLocator routeLocator;

    @Scheduled(fixedRate = 5000)
    private void refreshRoute() {
        RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
        publisher.publishEvent(routesRefreshedEvent);
    }

}

工程启动后,可以看到每隔5秒打印最新的路由信息(路由来源:application.yml已经配置和路由表中)在这里插入图片描述
(5)测试是否生效

浏览器依旧访问:http://localhost:10010/api/inventory/demo/test在这里插入图片描述从结果可以看到 ,已经成功实现了路由从数据库中动态读取。

代码托管:https://gitee.com/calvin1993/distributed-exercise

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值