本文目录导读:

我来为您详细介绍Java中实现“服务随人”(即请求路由到特定服务实例)的几种方案。
核心概念
“服务随人”指:同一用户的多次请求路由到同一服务实例,常用于:
- 用户会话状态维护
- 本地缓存利用
- 实时通信场景
实现方案
基于Sticky Session(粘性会话)
网关层通过Cookie或请求头实现固定路由:
// Spring Cloud Gateway + 自定义过滤器
@Component
public class StickySessionGatewayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 1. 从Cookie获取用户标识
String userId = extractUserIdFromCookie(request);
if (userId == null) {
// 生成新ID并设置到Cookie
userId = UUID.randomUUID().toString();
exchange.getResponse().addCookie(
ResponseCookie.from("userId", userId).build()
);
}
// 2. 计算目标服务实例(基于一致性哈希)
String targetInstance = calculateTargetInstance(userId);
// 3. 将实例标识放入请求头
exchange.getAttributes().put("target-instance", targetInstance);
return chain.filter(exchange);
}
private String extractUserIdFromCookie(ServerHttpRequest request) {
HttpCookie cookie = request.getCookies().getFirst("userId");
return cookie != null ? cookie.getValue() : null;
}
private String calculateTargetInstance(String userId) {
// 一致性哈希实现
return ConsistentHashRouter.getNode(userId);
}
}
负载均衡器配置(Nginx):
upstream backend {
ip_hash; # 基于IP的粘性会话
server 192.168.1.1:8080;
server 192.168.1.2:8080;
server 192.168.1.3:8080;
}
一致性哈希路由
public class ConsistentHashRouter {
private final TreeMap<Integer, String> ring = new TreeMap<>();
private final int virtualNodes = 100; // 虚拟节点数
public ConsistentHashRouter(List<String> instances) {
for (String instance : instances) {
addNode(instance);
}
}
private void addNode(String instance) {
for (int i = 0; i < virtualNodes; i++) {
String virtualNode = instance + "#" + i;
int hash = hash(virtualNode);
ring.put(hash, instance);
}
}
public String getNode(String key) {
if (ring.isEmpty()) return null;
int hash = hash(key);
Map.Entry<Integer, String> entry = ring.ceilingEntry(hash);
return (entry != null) ? entry.getValue() : ring.firstEntry().getValue();
}
private int hash(String key) {
// 使用FNV1_32_HASH算法
final int p = 16777619;
int hash = (int) 2166136261L;
for (byte b : key.getBytes(StandardCharsets.UTF_8)) {
hash = (hash ^ b) * p;
}
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
return hash & 0x7FFFFFFF;
}
}
// 使用示例
@RestController
public class RouterController {
@Autowired
private ConsistentHashRouter router;
@GetMapping("/route")
public String routeToUserInstance(@RequestParam String userId) {
String targetInstance = router.getNode(userId);
return "User " + userId + " routed to: " + targetInstance;
}
}
Spring Cloud Ribbon 自定义规则
public class UserAwareRule extends AbstractLoadBalancerRule {
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
List<Server> servers = lb.getAllServers();
// 从请求上下文获取用户标识
String userId = RequestContextHolder.currentRequestAttributes()
.getRequest().getHeader("X-User-Id");
if (userId == null) {
return RoundRobinRule.getInstance().choose(lb, key);
}
// 基于用户ID选择实例
int serverIndex = Math.abs(userId.hashCode()) % servers.size();
return servers.get(serverIndex);
}
}
// 配置使用自定义规则
@Configuration
public class RibbonConfig {
@Bean
public IRule ribbonRule() {
return new UserAwareRule();
}
}
基于Redis的会话路由
@Service
public class SessionRouterService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String USER_INSTANCE_KEY = "user:instance:%s";
/**
* 记录用户与实例的绑定关系
*/
public void bindUserToInstance(String userId, String instanceId) {
String key = String.format(USER_INSTANCE_KEY, userId);
redisTemplate.opsForValue().set(key, instanceId, 30, TimeUnit.MINUTES);
}
/**
* 获取用户绑定的实例
*/
public String getInstanceForUser(String userId) {
String key = String.format(USER_INSTANCE_KEY, userId);
return redisTemplate.opsForValue().get(key);
}
/**
* 用户登录时绑定
*/
public void onUserLogin(String userId, String instanceId) {
bindUserToInstance(userId, instanceId);
}
/**
* 用户登出时解绑
*/
public void onUserLogout(String userId) {
String key = String.format(USER_INSTANCE_KEY, userId);
redisTemplate.delete(key);
}
}
完整示例:基于Spring Cloud Gateway
# application.yml
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- name: StickySession
args:
cookieName: USER_SESSION
ttl: 1800
// 自定义Gateway过滤器
@Component
public class StickySessionGatewayFilterFactory
extends AbstractGatewayFilterFactory<StickySessionGatewayFilterFactory.Config> {
public StickySessionGatewayFilterFactory() {
super(Config.class);
}
@Override
public String name() {
return "StickySession";
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 获取或创建会话Cookie
String sessionId = getOrCreateSessionId(request, response, config);
// 根据sessionId路由到固定实例
String instanceId = routeBySession(sessionId);
exchange.getAttributes().put("instanceId", instanceId);
return chain.filter(exchange);
};
}
private String getOrCreateSessionId(ServerHttpRequest request,
ServerHttpResponse response,
Config config) {
HttpCookie cookie = request.getCookies().getFirst(config.cookieName);
if (cookie != null) {
return cookie.getValue();
}
String sessionId = UUID.randomUUID().toString();
response.addCookie(ResponseCookie.from(config.cookieName, sessionId)
.maxAge(config.ttl)
.path("/")
.build());
return sessionId;
}
private String routeBySession(String sessionId) {
// 一致性哈希或直接映射
return String.valueOf(Math.abs(sessionId.hashCode()) % 3 + 1);
}
public static class Config {
private String cookieName = "SESSION_ID";
private int ttl = 1800;
// getters and setters
}
}
服务端处理示例
@RestController
public class UserController {
@Autowired
private UserService userService;
@Autowired
private SessionRouterService sessionRouter;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
// 用户登录
User user = userService.login(request.getUsername(), request.getPassword());
// 绑定用户到当前实例
String instanceId = getCurrentInstanceId();
sessionRouter.bindUserToInstance(user.getId(), instanceId);
return ResponseEntity.ok(user);
}
@GetMapping("/user/{id}/data")
public ResponseEntity<?> getUserData(@PathVariable String id) {
// 检查当前请求是否路由到正确的实例
String expectedInstance = sessionRouter.getInstanceForUser(id);
String currentInstance = getCurrentInstanceId();
if (!expectedInstance.equals(currentInstance)) {
// 返回重定向或错误
return ResponseEntity.status(302)
.header("Location", "http://" + expectedInstance + "/api/user/" + id + "/data")
.build();
}
// 处理请求
return ResponseEntity.ok(userService.getUserData(id));
}
private String getCurrentInstanceId() {
InetAddress addr;
try {
addr = InetAddress.getLocalHost();
return addr.getHostAddress() + ":" + System.getProperty("server.port");
} catch (UnknownHostException e) {
return "unknown";
}
}
}
注意事项
| 问题 | 解决方案 |
|---|---|
| 实例宕机 | 用户会话丢失,需要重新绑定 |
| 节点扩缩容 | 影响较小,但可能导致路由变化 |
| 负载不均 | 使用虚拟节点和一致性哈希 |
| Session共享 | 配合Redis实现分布式会话 |
实现“服务随人”的核心是在网关层根据用户标识进行一致性路由,推荐使用:
- 小规模系统:Nginx ip_hash + Cookie方案
- 微服务架构:Spring Cloud Gateway + 一致性哈希
- 高可用场景:Redis记录路由映射 + 分布式Session
选择合适的方案需要根据业务需求、系统规模和可用性要求来决定。