You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring微服务中Sticky Session负载均衡实现求助

实现Spring Cloud Gateway + Loadbalancer的Sticky Session方案

以下是适配你当前Eureka服务发现环境的Sticky Session实现方案,基于Spring Cloud Loadbalancer扩展轮询规则实现:

一、基于Cookie的粘性会话配置

这种方式通过在客户端Cookie中记录已绑定的服务实例ID,后续请求优先路由到该实例。

1. 自定义负载均衡规则类

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;

import java.util.List;
import java.util.Optional;

@Configuration
public class StickySessionLoadBalancerConfig {

    // 自定义粘性会话Cookie名称
    public static final String STICKY_SESSION_COOKIE = "STICKY_INSTANCE_ID";

    @Bean
    public RoundRobinLoadBalancer reactorServiceInstanceLoadBalancer(
            LoadBalancerClientFactory loadBalancerClientFactory) {
        return new StickySessionRoundRobinLoadBalancer(
                loadBalancerClientFactory.getLazyProvider("service-provider", ServiceInstanceListSupplier.class));
    }

    static class StickySessionRoundRobinLoadBalancer extends RoundRobinLoadBalancer {

        private final ServiceInstanceListSupplier serviceInstanceListSupplier;

        public StickySessionRoundRobinLoadBalancer(ServiceInstanceListSupplier serviceInstanceListSupplier) {
            super(serviceInstanceListSupplier);
            this.serviceInstanceListSupplier = serviceInstanceListSupplier;
        }

        @Override
        protected Optional<ServiceInstance> chooseInstance(List<ServiceInstance> instances) {
            // 从请求上下文获取当前请求的Exchange
            ServerWebExchange exchange = (ServerWebExchange) org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();
            if (exchange == null) {
                // 无上下文时用默认轮询
                return super.chooseInstance(instances);
            }

            ServerHttpRequest request = exchange.getRequest();
            // 读取Cookie中的实例ID
            String instanceId = request.getCookies().getFirst(STICKY_SESSION_COOKIE);
            if (instanceId != null) {
                // 查找匹配的存活实例
                Optional<ServiceInstance> stickyInstance = instances.stream()
                        .filter(instance -> instance.getInstanceId().equals(instanceId))
                        .findFirst();
                if (stickyInstance.isPresent()) {
                    return stickyInstance;
                }
            }

            // 无匹配实例时,用轮询选择新实例并设置Cookie到响应
            Optional<ServiceInstance> chosenInstance = super.chooseInstance(instances);
            chosenInstance.ifPresent(instance -> {
                exchange.getResponse().addCookie(
                        org.springframework.http.HttpCookie.from(STICKY_SESSION_COOKIE, instance.getInstanceId())
                                .path("/")
                                .build()
                );
            });
            return chosenInstance;
        }
    }
}

2. 绑定配置到目标服务

在application.yml中指定目标服务使用上述自定义负载均衡规则:

spring:
  cloud:
    loadbalancer:
      clients:
        service-provider: # 替换为你的Eureka服务ID
          configuration: com.yourpackage.StickySessionLoadBalancerConfig # 替换为你的配置类全路径

二、基于请求头的粘性会话配置

若不想使用Cookie,可改用自定义请求头(如X-Sticky-Instance)实现,只需修改上述规则类的实例选择逻辑:

@Override
protected Optional<ServiceInstance> chooseInstance(List<ServiceInstance> instances) {
    ServerWebExchange exchange = (ServerWebExchange) org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();
    if (exchange == null) {
        return super.chooseInstance(instances);
    }

    ServerHttpRequest request = exchange.getRequest();
    // 读取请求头中的实例ID
    String instanceId = request.getHeaders().getFirst("X-Sticky-Instance");
    if (instanceId != null) {
        Optional<ServiceInstance> stickyInstance = instances.stream()
                .filter(instance -> instance.getInstanceId().equals(instanceId))
                .findFirst();
        if (stickyInstance.isPresent()) {
            return stickyInstance;
        }
    }

    // 选择新实例后,在响应头中返回实例ID
    Optional<ServiceInstance> chosenInstance = super.chooseInstance(instances);
    chosenInstance.ifPresent(instance -> {
        exchange.getResponse().getHeaders().add("X-Sticky-Instance", instance.getInstanceId());
    });
    return chosenInstance;
}

三、关键注意事项

  • 确保Eureka中服务实例的instanceId唯一(默认生成规则为主机名:服务ID:端口,可通过eureka.instance.instance-id自定义)
  • 当绑定的实例下线时,负载均衡会自动切换到其他存活实例,后续请求会重新绑定新实例
  • Gateway路由配置需保留lb://前缀,确保请求通过Loadbalancer转发:
spring:
  cloud:
    gateway:
      routes:
        - id: service-provider-route
          uri: lb://service-provider
          predicates:
            - Path=/api/**

内容的提问来源于stack exchange,提问作者TheCoder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 22:25:17