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熔断
你想想看,如果每个节点都无状态,扩缩容就像搭积木一样简单。这才是高可用的基础。