4. 应用层无状态设计:水平扩展策略、会话管理、API网关高可用、限流与熔断

做市系统最怕什么?

怕节点挂了,交易中断。怕流量突增,系统雪崩。

我见过太多团队,一开始把状态全塞在应用内存里。结果呢?扩容时发现Session不同步,缩容时丢数据。说白了,这就是给自己挖坑。

这一章,咱们聊聊怎么把应用层做成「无状态」。这是水平扩展的前提,也是高可用的基石。

4.1 为什么必须无状态?

有状态的应用,就像个记性特别好的老头。你换个人跟他说话,他就不认得了。

做市系统里,每个节点都要处理订单、计算报价。如果节点A记住了用户的登录状态,节点B没记住,那负载均衡一转发,用户就得重新登录。这在交易场景下是致命的。

无状态设计,就是把所有「记忆」都扔到外部存储里。应用本身只负责计算和转发。

核心原则:

  • 应用实例不保存任何业务状态
  • 所有会话数据存放到Redis/数据库
  • 节点可以随时启停,不影响整体服务

我在项目中遇到过一件事。有一次做压力测试,一个节点内存飙到90%。我一看,好家伙,Session全堆在本地。扩容了4个节点,结果每个节点都得重新登录。用户骂声一片。

从那以后,我定了个规矩:应用层代码里,不准出现static变量存业务数据。

4.2 水平扩展策略

无状态设计做好了,水平扩展就是水到渠成的事。

你想想看,每个节点都一样,加机器就是加能力。这就是所谓的「线性扩展」。

4.2.1 扩展方式

扩展类型 做法 适用场景
垂直扩展 加CPU、加内存 单机性能瓶颈
水平扩展 加机器实例 流量持续增长
自动扩缩容 K8s HPA + 自定义指标 流量波动大

我个人习惯用K8s的HPA来做自动扩缩。指标用QPS和CPU混合策略。比如QPS超过5000就加一个Pod,低于1000就缩一个。

# 自动扩缩容配置示例
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: market-maker-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: market-maker
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Pods
    pods:
      metric:
        name: qps_per_pod
      target:
        type: AverageValue
        averageValue: 5000

避坑指南:

我曾经把扩缩容阈值设得太低。结果流量一波动,Pod频繁创建销毁。每次创建都要加载行情数据,反而拖慢了系统。后来我加了冷却时间,至少稳定30秒再判断。

4.3 会话管理

无状态不代表没有会话。只是会话数据不放在应用里。

做市系统的会话管理,我推荐用Token + Redis的方案。

4.3.1 会话存储方案对比

方案 优点 缺点
本地Session 实现简单 无法水平扩展
Redis集中存储 支持扩展,性能好 依赖Redis高可用
JWT无状态Token 完全无状态 无法主动失效

我个人倾向用Redis。JWT虽然无状态,但做市系统里经常需要主动踢掉异常用户。JWT做不到这一点。

// 会话管理核心逻辑
public class SessionManager {
    private final RedisTemplate<String, Object> redisTemplate;
    
    // 创建会话,过期时间30分钟
    public String createSession(UserSession session) {
        String token = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(
            "session:" + token, 
            session, 
            30, 
            TimeUnit.MINUTES
        );
        return token;
    }
    
    // 每次请求续期
    public void refreshSession(String token) {
        redisTemplate.expire("session:" + token, 30, TimeUnit.MINUTES);
    }
}

注意:

Redis挂了怎么办?我建议部署Redis Sentinel或Cluster集群。同时,应用层要有降级策略——Redis不可用时,可以短暂允许本地Session,但必须打日志告警。

4.4 API网关高可用

网关是流量的总入口。它挂了,整个系统就瘫痪了。

做市系统的网关,我要求做到99.99%可用。怎么实现?

4.4.1 网关集群部署

至少部署3个网关节点,前面挂负载均衡器。我用的是Nginx + Keepalived的方案。

# Nginx upstream配置
upstream gateway_cluster {
    server 10.0.1.10:8080 weight=5;
    server 10.0.1.11:8080 weight=5;
    server 10.0.1.12:8080 weight=5;
    
    # 健康检查
    check interval=3000 rise=2 fall=5 timeout=1000;
}

server {
    listen 443 ssl;
    location /api/ {
        proxy_pass http://gateway_cluster;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

嗯,这里要注意。网关本身也要做健康检查。我见过有人只配了负载均衡,没配健康检查。结果一个网关挂了,流量还往那边发,导致大量502错误。

4.4.2 网关无状态化

网关同样要无状态。所有的路由规则、限流配置,都从配置中心拉取。我用的是Nacos。

为什么?因为网关实例随时可能重启。如果配置写在本地文件里,重启就丢了。

我的经验:

网关的配置变更,一定要支持热加载。我曾经因为改了一个路由规则,需要重启网关。结果那1秒的断连,导致几百笔订单没被路由到后端。虽然最后补单了,但用户体验极差。

4.5 限流与熔断

做市系统最怕流量洪峰。比如某个币种突然暴涨,所有用户都来下单。如果不限流,后端数据库直接被打爆。

4.5.1 限流策略

我常用的限流算法有三种:

算法 特点 适用场景
令牌桶 允许突发流量 API接口限流
漏桶 平滑流量 数据库写入限流
滑动窗口 精确控制 用户级别限流

我个人习惯用令牌桶。做市系统需要处理突发订单,令牌桶正好允许一定程度的突发。

// 基于Redis的分布式限流
public class RateLimiter {
    private final RedisTemplate<String, Object> redisTemplate;
    
    public boolean tryAcquire(String key, int permits, int rate) {
        // Lua脚本保证原子性
        String luaScript = 
            "local key = KEYS[1]" +
            "local rate = tonumber(ARGV[1])" +
            "local permits = tonumber(ARGV[2])" +
            "local current = redis.call('get', key) or 0" +
            "if current + permits <= rate then" +
            "    redis.call('incrby', key, permits)" +
            "    redis.call('expire', key, 1)" +
            "    return 1" +
            "else" +
            "    return 0" +
            "end";
        
        return redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            List.of(key),
            String.valueOf(rate),
            String.valueOf(permits)
        ) == 1L;
    }
}

4.5.2 熔断机制

限流是保护自己。熔断是保护下游。

当下游服务响应变慢或报错时,熔断器会快速失败,防止雪崩。

我常用的是Hystrix的改良版——Resilience4j。它更轻量,适合做市系统这种低延迟场景。

// 熔断器配置
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)           // 50%失败率触发熔断
    .waitDurationInOpenState(Duration.ofSeconds(30))  // 熔断持续30秒
    .slidingWindowSize(100)             // 统计最近100次请求
    .permittedNumberOfCallsInHalfOpenState(10)  // 半开状态允许10次试探
    .build();

CircuitBreaker circuitBreaker = CircuitBreaker.of("orderService", config);

避坑指南:

我曾经把熔断阈值设得太敏感。有一次网络抖动,持续了3秒。结果熔断器打开了,30秒内所有订单都走降级逻辑。用户下了单,系统说「稍后处理」,结果用户以为没成功,又下了一单。最后重复订单一大堆。

我的建议是:熔断阈值要结合业务容忍度来设。做市系统可以容忍短暂延迟,但不能容忍大量重复订单。

4.6 本章小结

应用层无状态设计,说白了就是让每个节点都变成「工具人」。谁来都一样,挂了换一个就行。

核心就四点:

  • 无状态:状态扔到Redis,应用只做计算
  • 水平扩展:K8s HPA自动扩缩,但要加冷却时间
  • 会话管理:Token + Redis,支持主动失效
  • 限流熔断:令牌桶限流,Resilience4j熔断

你想想看,如果每个节点都无状态,扩缩容就像搭积木一样简单。这才是高可用的基础。


专注资料整理