高扩展-无状态服务设计
在分布式系统中,无状态服务(Stateless Service)是实现水平扩展的基石。一个设计良好的无状态服务可以轻松扩展到成百上千个实例,而有状态服务则会成为系统扩展的瓶颈。
据统计,采用无状态设计的系统,其扩展成本仅为有状态系统的 1/10,故障恢复时间可以缩短 90% 以上。本文将深入探讨无状态服务的设计原理、实现方法和最佳实践。
# 一、什么是无状态服务
# 1、状态的定义
在软件系统中,状态(State)是指影响系统行为的数据,包括:
内存状态:
- 用户会话(
Session) - 缓存数据
- 运行时变量
本地文件状态:
- 临时文件
- 上传的文件
- 日志文件
连接状态:
- 数据库连接
- WebSocket 长连接
- TCP 连接
# 2、无状态 vs 有状态
无状态服务:
- 不在服务器内存中保存业务上下文
- 每次请求都是独立的,不依赖前序请求
- 任意实例都可以处理任意请求
有状态服务:
- 在服务器内存中保存用户会话或业务数据
- 后续请求需要访问之前保存的状态
- 需要会话亲和性(
Session Affinity)
# 3、对比示例
有状态设计:
// ❌ 有状态 - 购物车存储在内存中
public class ShoppingCartController {
// 状态存储在服务器内存
private Map<String, List<Item>> userCarts = new ConcurrentHashMap<>();
@PostMapping("/cart/add")
public void addItem(@RequestParam String userId, @RequestBody Item item) {
List<Item> cart = userCarts.computeIfAbsent(userId, k -> new ArrayList<>());
cart.add(item);
}
@GetMapping("/cart")
public List<Item> getCart(@RequestParam String userId) {
return userCarts.get(userId);
}
}
问题:
- 用户必须访问同一台服务器
- 服务器重启,购物车数据丢失
- 无法水平扩展
无状态设计:
// ✅ 无状态 - 购物车存储在 Redis
public class ShoppingCartController {
@Autowired
private RedisTemplate<String, List<Item>> redisTemplate;
@PostMapping("/cart/add")
public void addItem(@RequestParam String userId, @RequestBody Item item) {
String key = "cart:" + userId;
List<Item> cart = redisTemplate.opsForValue().get(key);
if (cart == null) {
cart = new ArrayList<>();
}
cart.add(item);
redisTemplate.opsForValue().set(key, cart, 7, TimeUnit.DAYS);
}
@GetMapping("/cart")
public List<Item> getCart(@RequestParam String userId) {
String key = "cart:" + userId;
return redisTemplate.opsForValue().get(key);
}
}
优势:
- 任意服务器都可以处理请求
- 服务器重启不影响数据
- 可以无限水平扩展
# 二、为什么需要无状态设计
# 1、水平扩展的前提
有状态服务的扩展困境:
请求1(用户A) → [服务器1] → 状态存储在服务器1
请求2(用户A) → [服务器2] → 无法获取状态 ❌
无状态服务的扩展优势:
负载均衡器
│
┌───────────────┼───────────────┐
│ │ │
[服务器1] [服务器2] [服务器3]
│ │ │
└───────────────┴───────────────┘
│
[Redis集群]
(共享状态存储)
任意服务器都可以处理任意请求,实现真正的负载均衡。
# 2、高可用性
故障恢复时间对比:
| 场景 | 有状态服务 | 无状态服务 |
|---|---|---|
| 单实例故障 | 需要会话迁移 | 立即切换到其他实例 |
| 恢复时间 | 分钟级 | 秒级 |
| 数据丢失 | 可能丢失内存数据 | 不丢失 |
| 用户影响 | 需要重新登录 | 无感知 |
# 3、运维简化
无状态服务的运维优势:
- ✅ 可以随时重启实例
- ✅ 可以动态增删实例
- ✅ 可以滚动升级,零停机
- ✅ 可以快速回滚
- ✅ 可以自动故障转移
# 4、云原生友好
Kubernetes 等容器编排平台天然适合无状态服务:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 10 # 可以随意调整副本数
template:
spec:
containers:
- name: app
image: myapp:v1.0
# 三、无状态设计的核心原则
# 1、状态外部化
原则: 所有状态都存储在外部系统。
常见外部存储:
| 状态类型 | 推荐存储 | 示例 |
|---|---|---|
| 用户会话 | Redis, Memcached | Session 数据 |
| 业务数据 | MySQL, PostgreSQL | 订单、用户信息 |
| 临时数据 | Redis | 验证码、限流计数 |
| 文件数据 | OSS, S3, MinIO | 用户上传的图片 |
| 配置数据 | Nacos, Apollo | 应用配置 |
| 任务队列 | RabbitMQ, Kafka | 异步任务 |
示例:Session 外部化:
// Spring Boot 配置
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
public class SessionConfig {
// Session 自动存储到 Redis
}
# application.yml
spring:
session:
store-type: redis
redis:
host: redis-cluster.example.com
port: 6379
# 2、幂等性设计
原则: 同一个请求执行多次,结果应该相同。
为什么需要幂等:
- 网络超时可能导致客户端重试
- 负载均衡器可能重发请求
- 消息队列可能重复投递
幂等实现方式:
① 使用唯一请求ID:
@PostMapping("/order/create")
public Result createOrder(@RequestBody CreateOrderRequest request,
@RequestHeader("X-Request-Id") String requestId) {
// 检查请求是否已处理
if (redisTemplate.hasKey("order:request:" + requestId)) {
return Result.success("订单已创建");
}
// 创建订单
Order order = orderService.create(request);
// 记录请求ID,过期时间1小时
redisTemplate.opsForValue().set("order:request:" + requestId,
order.getId(),
1, TimeUnit.HOURS);
return Result.success(order);
}
② 使用数据库唯一约束:
CREATE TABLE `orders` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`request_id` VARCHAR(64) NOT NULL COMMENT '请求唯一标识',
`amount` DECIMAL(10,2) NOT NULL,
UNIQUE KEY `uk_request_id` (`request_id`) -- 唯一约束
);
@Transactional
public Order createOrder(CreateOrderRequest request) {
Order order = new Order();
order.setRequestId(request.getRequestId());
order.setUserId(request.getUserId());
order.setAmount(request.getAmount());
try {
orderMapper.insert(order);
} catch (DuplicateKeyException e) {
// 请求已处理,返回已有订单
return orderMapper.selectByRequestId(request.getRequestId());
}
return order;
}
③ 使用乐观锁:
@Data
public class Account {
private Long id;
private BigDecimal balance;
private Integer version; // 版本号
}
@Update("UPDATE account SET balance = #{balance}, version = version + 1 " +
"WHERE id = #{id} AND version = #{version}")
int updateWithVersion(Account account);
public void withdraw(Long accountId, BigDecimal amount) {
Account account = accountMapper.selectById(accountId);
account.setBalance(account.getBalance().subtract(amount));
int rows = accountMapper.updateWithVersion(account);
if (rows == 0) {
throw new ConcurrentModificationException("账户已被修改,请重试");
}
}
# 3、请求自包含
原则: 每个请求包含所有必要的信息,不依赖服务器上下文。
示例:JWT Token:
// ✅ Token 包含所有用户信息
public class JwtTokenProvider {
public String generateToken(User user) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("username", user.getUsername());
claims.put("roles", user.getRoles());
return Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + 86400000))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
public User parseToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
User user = new User();
user.setId(claims.get("userId", Long.class));
user.setUsername(claims.get("username", String.class));
user.setRoles(claims.get("roles", List.class));
return user;
}
}
每次请求通过 Token 携带用户信息,服务器无需保存会话。
# 4、避免本地缓存(或使用一致性哈希)
问题:本地缓存导致数据不一致:
用户A → [服务器1] → 本地缓存:用户A余额=100
用户A → [服务器2] → 本地缓存:用户A余额=90
解决方案①:使用集中式缓存:
// ✅ 使用 Redis 替代本地缓存
@Service
public class UserService {
@Autowired
private RedisTemplate<String, User> redisTemplate;
public User getUser(Long userId) {
String key = "user:" + userId;
User user = redisTemplate.opsForValue().get(key);
if (user == null) {
user = userMapper.selectById(userId);
redisTemplate.opsForValue().set(key, user, 10, TimeUnit.MINUTES);
}
return user;
}
}
解决方案②:使用一致性哈希:
// 如果必须使用本地缓存,使用一致性哈希确保同一用户访问同一服务器
public class ConsistentHashLoadBalancer {
private final SortedMap<Integer, String> circle = new TreeMap<>();
public void addServer(String server) {
for (int i = 0; i < 150; i++) { // 虚拟节点
int hash = hash(server + "#" + i);
circle.put(hash, server);
}
}
public String getServer(String key) {
int hash = hash(key);
SortedMap<Integer, String> tailMap = circle.tailMap(hash);
int targetHash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
return circle.get(targetHash);
}
}
# 四、典型场景的无状态改造
# 1、用户登录会话
有状态设计:
// ❌ Session 存储在服务器内存
@PostMapping("/login")
public Result login(@RequestBody LoginRequest request, HttpSession session) {
User user = userService.authenticate(request.getUsername(), request.getPassword());
session.setAttribute("user", user); // 状态保存在服务器
return Result.success();
}
@GetMapping("/profile")
public Result getProfile(HttpSession session) {
User user = (User) session.getAttribute("user");
return Result.success(user);
}
无状态改造:
// ✅ 使用 JWT Token
@PostMapping("/login")
public Result login(@RequestBody LoginRequest request) {
User user = userService.authenticate(request.getUsername(), request.getPassword());
String token = jwtTokenProvider.generateToken(user);
return Result.success(token);
}
@GetMapping("/profile")
public Result getProfile(@RequestHeader("Authorization") String token) {
User user = jwtTokenProvider.parseToken(token);
return Result.success(user);
}
# 2、文件上传
有状态设计:
// ❌ 文件保存在本地磁盘
@PostMapping("/upload")
public Result upload(@RequestParam("file") MultipartFile file) {
String filename = UUID.randomUUID().toString() + ".jpg";
String filepath = "/data/uploads/" + filename;
file.transferTo(new File(filepath));
return Result.success(filename);
}
问题:
- 文件分散在各个服务器
- 无法负载均衡
- 访问文件时可能找不到
无状态改造:
// ✅ 文件保存到 OSS
@Service
public class FileService {
@Autowired
private OSSClient ossClient;
public String upload(MultipartFile file) {
String filename = UUID.randomUUID().toString() + ".jpg";
String objectName = "uploads/" + filename;
ossClient.putObject("my-bucket", objectName, file.getInputStream());
// 返回 CDN 地址
return "https://cdn.example.com/" + objectName;
}
}
# 3、定时任务
有状态设计:
// ❌ 使用 @Scheduled,多实例会重复执行
@Scheduled(cron = "0 0 2 * * ?")
public void dailyReport() {
// 每个服务器实例都会执行
reportService.generateDailyReport();
}
无状态改造方案①:分布式锁:
// ✅ 使用 Redis 分布式锁
@Scheduled(cron = "0 0 2 * * ?")
public void dailyReport() {
String lockKey = "task:daily-report";
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "locked", 10, TimeUnit.MINUTES);
if (Boolean.TRUE.equals(acquired)) {
try {
reportService.generateDailyReport();
} finally {
redisTemplate.delete(lockKey);
}
}
}
无状态改造方案②:使用专门的调度系统:
// ✅ 使用 XXL-Job 或 ElasticJob
@XxlJob("dailyReportHandler")
public void dailyReport() {
// XXL-Job 保证只有一个实例执行
reportService.generateDailyReport();
}
# 4、WebSocket 长连接
有状态设计:
// ❌ 连接保存在服务器内存
@Component
public class WebSocketHandler extends TextWebSocketHandler {
private Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String userId = getUserId(session);
sessions.put(userId, session);
}
public void sendToUser(String userId, String message) {
WebSocketSession session = sessions.get(userId);
if (session != null && session.isOpen()) {
session.sendMessage(new TextMessage(message));
}
}
}
问题:用户必须连接到保存其会话的服务器。
无状态改造:
// ✅ 使用消息队列广播
@Component
public class WebSocketHandler extends TextWebSocketHandler {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private Map<String, WebSocketSession> localSessions = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
// 订阅 Redis 频道
redisTemplate.execute((RedisCallback<Void>) connection -> {
connection.subscribe((message, pattern) -> {
String userId = new String(message.getChannel());
String content = new String(message.getBody());
sendToLocalUser(userId, content);
}, "ws:*".getBytes());
return null;
});
}
public void sendToUser(String userId, String message) {
// 发布到 Redis,所有服务器都会收到
redisTemplate.convertAndSend("ws:" + userId, message);
}
private void sendToLocalUser(String userId, String message) {
WebSocketSession session = localSessions.get(userId);
if (session != null && session.isOpen()) {
session.sendMessage(new TextMessage(message));
}
}
}
# 5、流控与限流
有状态设计:
// ❌ 计数器保存在本地内存
private Map<String, AtomicInteger> counters = new ConcurrentHashMap<>();
public boolean isAllowed(String userId) {
AtomicInteger counter = counters.computeIfAbsent(userId, k -> new AtomicInteger(0));
return counter.incrementAndGet() <= 100; // 每分钟100次
}
无状态改造:
// ✅ 使用 Redis 计数
public boolean isAllowed(String userId) {
String key = "ratelimit:" + userId + ":" + (System.currentTimeMillis() / 60000);
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
redisTemplate.expire(key, 60, TimeUnit.SECONDS);
}
return count <= 100;
}
# 五、无状态服务的实现模式
# 1、Token 认证模式
JWT Token 完整实现:
/**
* JWT Token 工具类
*/
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration}")
private Long expiration;
/**
* 生成 Token
*/
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.claim("username", userPrincipal.getUsername())
.claim("roles", userPrincipal.getAuthorities())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, secretKey)
.compact();
}
/**
* 从 Token 获取用户ID
*/
public Long getUserIdFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
/**
* 验证 Token
*/
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch (SignatureException ex) {
log.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
log.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
log.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
log.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
log.error("JWT claims string is empty");
}
return false;
}
}
/**
* JWT 认证过滤器
*/
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
log.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
# 2、分布式 Session 模式
Spring Session + Redis:
/**
* Session 配置
*/
@Configuration
@EnableRedisHttpSession(
maxInactiveIntervalInSeconds = 3600, // 1小时过期
redisNamespace = "spring:session"
)
public class SessionConfig {
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
# application.yml
spring:
session:
store-type: redis
timeout: 3600s
redis:
host: redis-cluster.example.com
port: 6379
password: your_password
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
使用方式和普通 Session 一样:
@RestController
public class UserController {
@PostMapping("/login")
public Result login(@RequestBody LoginRequest request, HttpSession session) {
User user = userService.authenticate(request.getUsername(), request.getPassword());
session.setAttribute("user", user); // 自动存储到 Redis
return Result.success();
}
@GetMapping("/profile")
public Result getProfile(HttpSession session) {
User user = (User) session.getAttribute("user"); // 自动从 Redis 读取
return Result.success(user);
}
}
# 3、外部缓存模式
多级缓存架构:
/**
* 多级缓存服务
*/
@Service
public class CacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 获取缓存(L1: 本地缓存 → L2: Redis → L3: 数据库)
*/
public <T> T get(String key, Class<T> type, Supplier<T> dbLoader) {
// L1: 本地缓存(Caffeine)
T value = localCache.get(key);
if (value != null) {
return value;
}
// L2: Redis
value = (T) redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value);
return value;
}
// L3: 数据库
value = dbLoader.get();
if (value != null) {
redisTemplate.opsForValue().set(key, value, 10, TimeUnit.MINUTES);
localCache.put(key, value);
}
return value;
}
/**
* 缓存失效
*/
public void evict(String key) {
localCache.invalidate(key);
redisTemplate.delete(key);
// 发布缓存失效消息,通知其他节点清除本地缓存
redisTemplate.convertAndSend("cache:evict", key);
}
}
# 4、请求ID幂等模式
完整的幂等实现:
/**
* 幂等注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
/**
* 幂等key的前缀
*/
String prefix() default "idempotent";
/**
* 过期时间(秒)
*/
int expireSeconds() default 3600;
}
/**
* 幂等切面
*/
@Aspect
@Component
public class IdempotentAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Around("@annotation(idempotent)")
public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
// 获取请求ID
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String requestId = request.getHeader("X-Request-Id");
if (StringUtils.isEmpty(requestId)) {
throw new IllegalArgumentException("请求头缺少 X-Request-Id");
}
// 构建幂等key
String key = idempotent.prefix() + ":" + requestId;
// 尝试设置key(使用 SET NX)
Boolean success = redisTemplate.opsForValue().setIfAbsent(
key,
"1",
idempotent.expireSeconds(),
TimeUnit.SECONDS
);
if (Boolean.FALSE.equals(success)) {
// 请求已处理
log.warn("重复请求: {}", requestId);
throw new DuplicateRequestException("请求正在处理中,请勿重复提交");
}
try {
// 执行业务逻辑
return joinPoint.proceed();
} catch (Exception e) {
// 业务异常,删除幂等key,允许重试
redisTemplate.delete(key);
throw e;
}
}
}
/**
* 使用示例
*/
@RestController
public class OrderController {
@PostMapping("/order/create")
@Idempotent(prefix = "order:create", expireSeconds = 300)
public Result createOrder(@RequestBody CreateOrderRequest request) {
Order order = orderService.create(request);
return Result.success(order);
}
}
# 六、性能优化与最佳实践
# 1、减少外部调用
虽然无状态,但也要避免过度依赖外部系统:
❌ 每次都查 Redis:
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
String key = "user:" + id;
User user = redisTemplate.opsForValue().get(key);
if (user == null) {
user = userMapper.selectById(id);
redisTemplate.opsForValue().set(key, user, 10, TimeUnit.MINUTES);
}
return user;
}
✅ 使用请求级缓存:
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id, HttpServletRequest request) {
// 请求级缓存(避免同一个请求多次查 Redis)
String attrKey = "user:" + id;
User user = (User) request.getAttribute(attrKey);
if (user != null) {
return user;
}
String key = "user:" + id;
user = redisTemplate.opsForValue().get(key);
if (user == null) {
user = userMapper.selectById(id);
redisTemplate.opsForValue().set(key, user, 10, TimeUnit.MINUTES);
}
request.setAttribute(attrKey, user);
return user;
}
# 2、批量操作优化
❌ 循环调用:
public List<User> getUsersByIds(List<Long> userIds) {
List<User> users = new ArrayList<>();
for (Long userId : userIds) {
String key = "user:" + userId;
User user = redisTemplate.opsForValue().get(key); // N次网络请求
users.add(user);
}
return users;
}
✅ 批量操作:
public List<User> getUsersByIds(List<Long> userIds) {
List<String> keys = userIds.stream()
.map(id -> "user:" + id)
.collect(Collectors.toList());
List<User> users = redisTemplate.opsForValue().multiGet(keys); // 1次网络请求
return users;
}
# 3、连接池优化
Lettuce 连接池配置:
spring:
redis:
lettuce:
pool:
max-active: 200 # 最大连接数
max-idle: 50 # 最大空闲连接
min-idle: 20 # 最小空闲连接
max-wait: 3000ms # 最大等待时间
shutdown-timeout: 100ms
# 4、Token 刷新策略
双 Token 机制(Access Token + Refresh Token):
@Service
public class TokenService {
/**
* 生成访问令牌(短期,15分钟)
*/
public String generateAccessToken(User user) {
return Jwts.builder()
.setSubject(user.getId().toString())
.setExpiration(new Date(System.currentTimeMillis() + 900000)) // 15分钟
.signWith(SignatureAlgorithm.HS512, accessTokenSecret)
.compact();
}
/**
* 生成刷新令牌(长期,7天)
*/
public String generateRefreshToken(User user) {
String refreshToken = UUID.randomUUID().toString();
// 刷新令牌存储在 Redis(方便随时撤销)
redisTemplate.opsForValue().set(
"refresh_token:" + refreshToken,
user.getId().toString(),
7, TimeUnit.DAYS
);
return refreshToken;
}
/**
* 使用刷新令牌获取新的访问令牌
*/
public String refreshAccessToken(String refreshToken) {
String userId = redisTemplate.opsForValue().get("refresh_token:" + refreshToken);
if (userId == null) {
throw new UnauthorizedException("刷新令牌无效或已过期");
}
User user = userService.getById(Long.parseLong(userId));
return generateAccessToken(user);
}
}
# 5、配置中心集成
使用 Nacos 动态配置:
@RefreshScope // 配置自动刷新
@RestController
public class ConfigController {
@Value("${business.max-order-amount}")
private BigDecimal maxOrderAmount;
@GetMapping("/config/max-order-amount")
public BigDecimal getMaxOrderAmount() {
// 配置变更时自动更新,无需重启
return maxOrderAmount;
}
}
# 七、监控与故障排查
# 1、关键监控指标
| 指标 | 说明 | 阈值 |
|---|---|---|
| Redis 连接数 | 外部存储连接 | < 80% |
| Redis 响应时间 | 缓存延迟 | < 10ms |
| Token 验证失败率 | 认证问题 | < 1% |
| 幂等拦截率 | 重复请求 | 根据业务 |
| 外部调用超时率 | 依赖服务可用性 | < 0.1% |
# 2、链路追踪
使用 SkyWalking 或 Zipkin:
@RestController
public class OrderController {
@Autowired
private Tracer tracer;
@PostMapping("/order/create")
public Result createOrder(@RequestBody CreateOrderRequest request) {
Span span = tracer.nextSpan().name("create-order");
try (Tracer.SpanInScope ws = tracer.withSpan(span.start())) {
span.tag("user_id", request.getUserId().toString());
span.tag("amount", request.getAmount().toString());
Order order = orderService.create(request);
return Result.success(order);
} finally {
span.end();
}
}
}
# 3、日志规范
结构化日志:
@Slf4j
@RestController
public class UserController {
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
MDC.put("userId", id.toString());
MDC.put("traceId", TraceContext.getTraceId());
try {
log.info("查询用户信息, userId={}", id);
User user = userService.getById(id);
log.info("查询用户成功, userId={}, username={}", id, user.getUsername());
return user;
} catch (Exception e) {
log.error("查询用户失败, userId={}", id, e);
throw e;
} finally {
MDC.clear();
}
}
}
# 八、常见问题与解决方案
# 1、Redis 雪崩
问题:Redis 集群宕机,所有请求打到数据库。
解决方案:
@Service
public class UserService {
@Autowired
private RedisTemplate<String, User> redisTemplate;
@Autowired
private UserMapper userMapper;
// 本地缓存作为降级方案
private final Cache<Long, User> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
public User getById(Long id) {
// 1. 尝试 Redis
try {
String key = "user:" + id;
User user = redisTemplate.opsForValue().get(key);
if (user != null) {
return user;
}
} catch (Exception e) {
log.warn("Redis异常,使用本地缓存降级", e);
}
// 2. Redis 失败,使用本地缓存
User user = localCache.get(id, k -> {
// 3. 本地缓存 miss,查数据库
return userMapper.selectById(id);
});
return user;
}
}
# 2、分布式锁死锁
问题:获取锁后,业务异常导致锁未释放。
解决方案:
public class RedisLockService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 获取分布式锁
*/
public boolean tryLock(String key, String value, long expireSeconds) {
return Boolean.TRUE.equals(
redisTemplate.opsForValue().setIfAbsent(key, value, expireSeconds, TimeUnit.SECONDS)
);
}
/**
* 释放分布式锁(使用 Lua 脚本保证原子性)
*/
public boolean unlock(String key, String value) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
value
);
return result != null && result > 0;
}
/**
* 使用示例
*/
public void doSomething() {
String lockKey = "task:daily-report";
String lockValue = UUID.randomUUID().toString();
if (tryLock(lockKey, lockValue, 300)) {
try {
// 执行业务逻辑
reportService.generateDailyReport();
} finally {
// 确保释放锁
unlock(lockKey, lockValue);
}
}
}
}
# 3、Token 过期问题
问题:用户正在操作时,Token 突然过期。
解决方案:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = getJwtFromRequest(request);
if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) {
Long userId = tokenProvider.getUserIdFromToken(token);
// 检查 Token 是否即将过期(剩余时间 < 5分钟)
if (tokenProvider.isTokenExpiringSoon(token)) {
// 生成新 Token 并通过响应头返回
String newToken = tokenProvider.generateToken(userId);
response.setHeader("X-New-Token", newToken);
}
// 设置认证信息
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
前端处理:
axios.interceptors.response.use(response => {
// 检查是否有新 Token
const newToken = response.headers['x-new-token'];
if (newToken) {
// 更新本地 Token
localStorage.setItem('token', newToken);
}
return response;
});
# 九、总结
# 无状态设计的核心价值
- 水平扩展: 可以无限增加服务器节点
- 高可用性: 单节点故障不影响整体服务
- 运维简化: 可以随时重启、升级实例
- 云原生友好: 适合容器化和自动伸缩
# 实现无状态的关键要素
- ✅ 状态外部化: 使用 Redis、数据库等外部存储
- ✅ 幂等性设计: 使用请求 ID、数据库约束保证幂等
- ✅ 请求自包含: 使用 JWT Token 携带所有必要信息
- ✅ 避免本地依赖: 文件上传到 OSS,配置使用配置中心
# 常见无状态方案
| 场景 | 推荐方案 |
|---|---|
| 用户认证 | JWT Token 或 Redis Session |
| 文件存储 | OSS / S3 / MinIO |
| 定时任务 | 分布式锁 或 XXL-Job |
| 限流计数 | Redis |
| 配置管理 | Nacos / Apollo |
| WebSocket | Redis 发布订阅 |
# 最佳实践建议
- 渐进式改造: 优先改造容易无状态化的模块
- 监控先行: 建立完善的监控体系
- 降级方案: 外部依赖故障时,有本地降级方案
- 性能优化: 减少外部调用,使用批量操作
- 安全性: Token 需要签名和加密,设置合理的过期时间
无状态设计是现代分布式系统的基础,掌握无状态设计的原理和实践,是成为优秀架构师的必经之路。
祝你变得更强!