轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • JavaScript
  • TypeScript
  • Node.js
  • 前端框架
  • 前端工程化
  • 浏览器与Web API
  • 架构设计与模式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • JavaScript
  • TypeScript
  • Node.js
  • 前端框架
  • 前端工程化
  • 浏览器与Web API
  • 架构设计与模式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 架构设计与模式

    • 高可用-分布式基础之CAP理论
    • 高可用-服务容错与降级策略
    • 高可用-故障检测与自动恢复
    • 高可用-混沌工程实践
    • 高可用-分布式事务实战
    • 高可用-多活与容灾架构设计
    • 高性能-缓存架构设计
    • 高性能-性能优化方法论
    • 高性能-异步处理与消息队列
    • 高性能-数据库性能优化
    • 高性能-负载均衡策略详解
    • 高性能-CDN与静态资源优化
    • 高扩展-水平扩展vs垂直扩展
    • 高扩展-无状态服务设计
      • 一、什么是无状态服务
        • 1、状态的定义
        • 2、无状态 vs 有状态
        • 3、对比示例
      • 二、为什么需要无状态设计
        • 1、水平扩展的前提
        • 2、高可用性
        • 3、运维简化
        • 4、云原生友好
      • 三、无状态设计的核心原则
        • 1、状态外部化
        • 2、幂等性设计
        • 3、请求自包含
        • 4、避免本地缓存(或使用一致性哈希)
      • 四、典型场景的无状态改造
        • 1、用户登录会话
        • 2、文件上传
        • 3、定时任务
        • 4、WebSocket 长连接
        • 5、流控与限流
      • 五、无状态服务的实现模式
        • 1、Token 认证模式
        • 2、分布式 Session 模式
        • 3、外部缓存模式
        • 4、请求ID幂等模式
      • 六、性能优化与最佳实践
        • 1、减少外部调用
        • 2、批量操作优化
        • 3、连接池优化
        • 4、Token 刷新策略
        • 5、配置中心集成
      • 七、监控与故障排查
        • 1、关键监控指标
        • 2、链路追踪
        • 3、日志规范
      • 八、常见问题与解决方案
        • 1、Redis 雪崩
        • 2、分布式锁死锁
        • 3、Token 过期问题
      • 九、总结
        • 无状态设计的核心价值
        • 实现无状态的关键要素
        • 常见无状态方案
        • 最佳实践建议
    • 高扩展-微服务架构演进
    • 高扩展-弹性伸缩设计
  • 代码质量管理

  • 基础

  • 操作系统

  • 计算机网络

  • AI

  • 编程范式

  • 安全

  • 中间件

  • 心得

  • 架构
  • 架构设计与模式
轩辕李
2025-05-22
目录

高扩展-无状态服务设计

在分布式系统中,无状态服务(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;
});

# 九、总结

# 无状态设计的核心价值

  1. 水平扩展: 可以无限增加服务器节点
  2. 高可用性: 单节点故障不影响整体服务
  3. 运维简化: 可以随时重启、升级实例
  4. 云原生友好: 适合容器化和自动伸缩

# 实现无状态的关键要素

  1. ✅ 状态外部化: 使用 Redis、数据库等外部存储
  2. ✅ 幂等性设计: 使用请求 ID、数据库约束保证幂等
  3. ✅ 请求自包含: 使用 JWT Token 携带所有必要信息
  4. ✅ 避免本地依赖: 文件上传到 OSS,配置使用配置中心

# 常见无状态方案

场景 推荐方案
用户认证 JWT Token 或 Redis Session
文件存储 OSS / S3 / MinIO
定时任务 分布式锁 或 XXL-Job
限流计数 Redis
配置管理 Nacos / Apollo
WebSocket Redis 发布订阅

# 最佳实践建议

  1. 渐进式改造: 优先改造容易无状态化的模块
  2. 监控先行: 建立完善的监控体系
  3. 降级方案: 外部依赖故障时,有本地降级方案
  4. 性能优化: 减少外部调用,使用批量操作
  5. 安全性: Token 需要签名和加密,设置合理的过期时间

无状态设计是现代分布式系统的基础,掌握无状态设计的原理和实践,是成为优秀架构师的必经之路。

祝你变得更强!

编辑 (opens new window)
#无状态#服务设计#扩展性#微服务
上次更新: 2025/12/18
高扩展-水平扩展vs垂直扩展
高扩展-微服务架构演进

← 高扩展-水平扩展vs垂直扩展 高扩展-微服务架构演进→

最近更新
01
AI编程时代的一些心得
09-11
02
Claude Code 最佳实践(个人版)
08-01
03
高扩展-弹性伸缩设计
06-05
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式