Java案例如何实现服务随人?

wen python案例 1

本文目录导读:

Java案例如何实现服务随人?

  1. 核心概念
  2. 实现方案
  3. 完整示例:基于Spring Cloud Gateway
  4. 服务端处理示例
  5. 注意事项

我来为您详细介绍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实现分布式会话

实现“服务随人”的核心是在网关层根据用户标识进行一致性路由,推荐使用:

  1. 小规模系统:Nginx ip_hash + Cookie方案
  2. 微服务架构:Spring Cloud Gateway + 一致性哈希
  3. 高可用场景:Redis记录路由映射 + 分布式Session

选择合适的方案需要根据业务需求、系统规模和可用性要求来决定。

抱歉,评论功能暂时关闭!