Spring Cloud Gateway是目前最流行的网关实现之一,通常与Spring Cloud中其它框架一起用于构建微服务项目,Spring Cloud Gateway默认是按照负载均衡的方式将请求路由到后端服务实例的,但在某些特殊场景下则需要有状态路由,例如服务有两个接口其中一个用于生成文件存储在本地,下一个接口用于下载生成的文件。本文介绍有状态路由的实现方法

1. 项目准备

新建一个网关项目 gateway 和后端服务项目 backend,并在 backend 中定义两个有状态的接口

@RestController
public class MyController {

    private final Map<String, String> map = new HashMap<>();

    @RequestMapping("/get")
    public String get() {
        return map.get("key");
    }

    @RequestMapping("/set/{value}")
    public void set(@PathVariable String value) {
        map.put("key", value);
    }
}

backend 启动两个实例并在 gateway 中配置两个实例的地址,此处为 localhost:8082localhost:8083,对应的实例Id是 ins2ins3

server:
  port: 8081
spring:
  cloud:
    discovery:
      client:
        simple:
          instances:
            backend:
              - uri: http://localhost:8082
                instance-id: ins2
              - uri: http://localhost:8083
                instance-id: ins3
    gateway:
      routes:
        - id: backend
          uri: lb://backend
          predicates:
            - Path=/**
完整项目源码参考 GitHub

此时通过网关先调用 /set/vv 接口再调用 /get 接口是显然无法获取到上一步保存的 vv 的,因为按照默认的负载均衡算法,两次请求被路由到了不同的 backend 实例上

C:\Users\pxzxj1>curl "http://localhost:8081/set/vv"

C:\Users\pxzxj1>curl "http://localhost:8081/get"

C:\Users\pxzxj1>

2. 配置验证

Spring Cloud LoadBalancer提供了 Request-based Sticky Session配置可以解决有状态路由问题,Spring Cloud Cloud也使用了Spring Cloud LoadBalancer实现路由选择

首先在网关的配置文件中添加如下内容

spring:
  cloud:
    loadbalancer:
      configurations: request-based-sticky-session               (1)
      sticky-session:
        add-service-instance-cookie: true                        (2)
  1. 基于请求中的Cookie路由,默认Cookie名称是 sc-lb-instance-id

  2. 请求转发到后端时将选中的后端实例信息添加到请求的Cookie中

为了将此次选中的实例信息返回客户端,后端接口也需要修改,将网关添加到请求中的Cookie添加到响应中

@RestController
public class MyController {

    private final Map<String, String> map = new HashMap<>();

    @RequestMapping("/get")
    public String get() {
        return map.get("key");
    }

    @RequestMapping("/set/{value}")
    public void set(@PathVariable String value, HttpServletRequest request, HttpServletResponse response) {
        Cookie[] cookies = request.getCookies();
        if(cookies != null) {
            for(Cookie cookie : cookies) {
                if("sc-lb-instance-id".equals(cookie.getName())) {
                    response.addCookie(cookie);
                    break;
                }
            }
        }
        map.put("key", value);
    }
}

此时再次调用 /set/vv 接口可以在它的响应的Cookie中看到本次网关选中的后端实例为 ins2,那么调用 /get 时也附带上此Cookie就可以使网关把请求也路由到 ins2,从而获取到 vv

C:\Users\pxzxj1>curl "http://localhost:8081/set/vv" -v
*   Trying 127.0.0.1:8081...
* Connected to localhost (127.0.0.1) port 8081 (#0)
> GET /set/vv HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.83.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Set-Cookie: sc-lb-instance-id=ins2
< Content-Length: 0
< Date: Wed, 08 Mar 2023 08:44:57 GMT
<
* Connection #0 to host localhost left intact

C:\Users\pxzxj1>curl -H "Cookie: sc-lb-instance-id=ins2" "http://localhost:8081/get"
vv
C:\Users\pxzxj1>