尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

为什么架构设计提倡无状态化

为什么架构设计提倡无状态化 一、先分清概念什么是有状态 / 无状态有状态服务 (Stateful)实例本地存储用户会话、临时数据、连接信息同一个用户后续请求必须落到同一台服务器。 例服务器内存存登录 session、本地内存缓存购物车、本机文件保存临时任务。无状态服务 (Stateless)每一台服务实例完全对等任何一台实例都可以处理任意客户端请求实例重启、销毁不会丢失业务数据。 请求需要的全部上下文通过请求头、Cookie‑Token、Redis、数据库携带。核心对比对比维度有状态服务Stateful无状态服务Stateless核心定义实例本地保存会话、临时上下文请求依赖上一次请求服务实例不存储业务状态上下文全部外置所有实例完全对等请求调度需要会话黏连、IP‑Hash同一用户固定访问同一节点轮询、最小连接、随机、权重任意实例均可处理请求水平扩容困难新增节点不能承接已有用户状态简单随时新增实例实现弹性扩容实例宕机风险绑定该节点的用户会话丢失、业务中断节点故障直接摘除流量转发其他实例用户无感知滚动 / 灰度发布复杂需要迁移本地数据、谨慎停机极其简单容器随时销毁重建 Pod 即可更新负载均衡缺陷容易出现负载倾斜、热点节点流量均匀分配调度策略不受约束多机房容灾难度高本地存储数据需要同步迁移计算逻辑可任意部署机房流量一键切换并发一致性多实例本地缓存极易出现数据不一致状态统一存放 Redis、数据库集中管控并发故障排查偶现问题不同机器环境不一样所有实例环境一致问题容易复现排查典型组件MySQL、Redis、ES、本地会话、本地文件缓存网关、Web 接口、API 服务、微服务业务逻辑二、无状态架构的全部核心优势1. 支持水平扩容最主要原因有状态每台机器有专属数据新增实例无法立刻承接流量扩容非常麻烦。无状态所有实例一模一样流量高了直接加机器负载均衡随便分发请求Nginx / 网关随机、轮询即可不需要会话黏连。软考架构原话无状态服务易于横向扩展是集群伸缩的先决条件。2. 高可用故障容错简单某一台实例宕机、卡死、重启因为本地没有状态负载均衡直接把流量切向其他节点业务不受影响。有状态机器宕机则绑定这台机器的用户会话全部丢失用户掉线。3. 负载均衡策略自由不需要会话粘连有状态必须开启ip‑hash、cookie 黏连强制用户一直访问同一实例容易造成流量分配不均部分服务器负载爆满、其余空闲黏连的节点故障用户直接断线。 无状态不需要粘性会话轮询、最小连接数、权重调度全都可以随便用负载更加均衡。4. 方便灰度发布、滚动更新、容器编排K8s、Docker、云原生最偏好无状态 Pod旧版本实例逐个销毁新版本实例拉起 因为实例不带状态随时销毁重建滚动升级零感知。 有状态应用升级要谨慎迁移本地数据升级流程复杂很多。5. 降低锁、并发、数据一致性难题多台机器本地存状态就会出现多实例修改同一份本地缓存出现数据不一致。 无状态把全部状态外置到 Redis、MySQL、MQ统一中心化管理只需要解决中间件的并发控制。6. 可迁移、多可用区部署、异地容灾无状态服务是纯粹的计算逻辑可以部署在任意机房、任意服务器 故障时流量一键切到备用机房。 有状态服务受限于本地存储跨机房迁移成本极高。7. 易于测试、调试、故障排查每一个实例环境一致请求在哪台机器执行结果都相同。 不会出现 “在 A 服务器正常、B 服务器报错” 这种本地状态引发的诡异问题。无状态缺点每次请求需要携带上下文信息请求报文偏大高频会话数据依赖 Redis 等中间件增加中间件压力需要额外维护分布式会话架构组件变多三、通俗举例理解有状态例子网吧单人专属座位你上网之后座位保存你的浏览记录你中途走开就必须回到原来座位座位坏了你就无法上网加新座位也不能分担老座位客人。无状态例子自助取票机所有取票机一模一样你的身份信息状态存在云端数据库任意一台机器都可取票一台机器坏了直接换一台节假日高峰期直接新增取票机即可扩容。四、那业务状态放去哪里无状态落地方案服务本身不带状态状态外置常见存放位置客户端Token、JWT、Cookie、请求 Header登录凭证放在请求里分布式缓存 Redis会话 session、购物车、临时任务、热点缓存持久化数据库 MySQL/PG永久业务数据消息队列 MQ异步任务、流程状态分布式配置中心配置状态五、什么时候允许使用有状态服务无状态是业务服务原则并不是所有组件都要无状态下面天生就是有状态组件数据库主从集群Redis 集群Elasticsearch文件存储、本地临时队列 这类存储型组件本身就要保存数据需要专门做有状态集群方案StatefulSet。具体示例【有状态服务】完整示例本机内存存储 Session控制器代码import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; import java.util.UUID; RestController public class StatefulController { // 致命问题会话存储在实例本地内存当中 private final MapString, String localSessionMap new HashMap(); /** * 用户登录返回sessionId */ GetMapping(/stateful/login) public String login(RequestParam String username) { String sessionId UUID.randomUUID().toString(); // 将用户会话保存到当前进程内存 localSessionMap.put(sessionId, username); return sessionId; } /** * 获取当前登录用户 */ GetMapping(/stateful/getUser) public String getUser(RequestParam String sessionId) { String username localSessionMap.get(sessionId); if (username null) { return 未登录; } return 当前登录用户 username; } /** * 退出登录清除本地内存状态 */ GetMapping(/stateful/logout) public String logout(RequestParam String sessionId) { localSessionMap.remove(sessionId); return 退出成功; } }复现问题重点考试常考场景调用登录Nginx 轮询分配至8080 (实例 A)GET http://127.0.0.1:8080/stateful/login?username张三 返回uuid‑001session 存入实例 A 的 HashMap。接下来调用获取用户轮询命中8081 (实例 B)GET http://127.0.0.1:8081/stateful/getUser?sessionIduuid‑001 返回未登录原因实例 A、实例 B 拥有两块独立内存HashMap 互不共享。唯一妥协方案开启会话黏连 ip‑hashNginx 根据客户端 IP 固定转发到同一台实例。 随之带来一堆缺点服务器宕机所有绑定该实例用户直接掉线流量分配不均出现热点机器扩容新增节点无法分担已有用户滚动更新时关闭实例需要迁移会话。无状态服务方案 1状态存放在客户端 — JWT 完全无状态最经典服务端内存完全不保存会话用户信息加密放在 Token每次请求携带。Jwt 工具类简易版import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; public class JwtUtil { // 加密密钥 private static final String SECRET_KEY my‑secret‑key‑2026; // 有效期 2小时 private static final long EXPIRATION 2 * 60 * 60 * 1000; // 创建token存放用户名 public static String generateToken(String username){ return Jwts.builder() .setSubject(username) .setExpiration(new Date(System.currentTimeMillis()EXPIRATION)) .signWith(SignatureAlgorithm.HS256,SECRET_KEY) .compact(); } // 解析token拿到用户名 public static String parseUser(String token){ return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody() .getSubject(); } }无状态控制器‑JWTimport org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; RestController public class StatelessJwtController { GetMapping(/stateless/jwt/login) public String login(RequestParam String username){ // 只生成令牌服务本地内存不存储任何会话 String token JwtUtil.generateToken(username); return token; } GetMapping(/stateless/jwt/getUser) public String getUser(RequestHeader(token) String token){ String username JwtUtil.parseUser(token); return 当前登录用户 username; } }集群测试现象登录请求落到 8080拿到 token查询用户请求落到 8081 8081 只需要解析 token 即可不需要共享内存。JWT 优缺点优点服务彻底无状态、不需要缓存中间件适合网关、集群、多机房缺点token 无法服务端主动作废、token 体积大、不能存放大量会话信息方案 2状态外置到共享中间件 Redis企业最常用分布式 Session所有服务实例读取同一个 Redis状态脱离服务进程。Controller 代码import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.UUID; import java.util.concurrent.TimeUnit; RestController public class StatelessRedisController { Autowired private StringRedisTemplate redisTemplate; // 登录session存放Redis而不是本机HashMap GetMapping(/stateless/redis/login) public String login(RequestParam String username){ String sessionId UUID.randomUUID().toString(); // 设置过期时间 2小时 redisTemplate.opsForValue().set(sessionId, username,2, TimeUnit.HOURS); return sessionId; } GetMapping(/stateless/redis/getUser) public String getUser(RequestParam String sessionId){ String username redisTemplate.opsForValue().get(sessionId); if(username null){ return 未登录; } return 当前登录用户username; } GetMapping(/stateless/redis/logout) public String logout(RequestParam String sessionId){ redisTemplate.delete(sessionId); return 退出登录成功; } }集群现象8080、8081 都访问公共 Redis无论负载均衡转发到哪一台实例都可以读取会话。 服务实例本身不存在状态 →标准无状态服务。
返回列表