本文目录导读:

我来为您详细介绍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
选择合适的方案需要根据业务需求、系统规模和可用性要求来决定。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播