news 2026/4/16 15:07:35

401 刷新 Token 的队列版(请求挂起排队 + 刷新后统一重放/统一失败)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
401 刷新 Token 的队列版(请求挂起排队 + 刷新后统一重放/统一失败)

——更工程化的“中间件”语义,适合中大型项目

共享 Future 方案已经够用;队列版适用于:

  • 想在 refresh 期间“挂起请求”,不立刻抛错

  • 想刷新失败时“一锅端”所有等待请求

  • 想控制重试节奏(顺序/限流/并发重放)

1. 队列版核心思想

当请求 401:

  1. 不立刻handler.next(err)

  2. 把这个请求的(RequestOptions + handler)存入队列

  3. 触发一次 refresh(并发锁保证只一次)

  4. refresh 成功:统一重放队列里所有请求(每个 resolve 回原来的 caller)

  5. refresh 失败:统一 reject(并触发全局登出)

2. 代码实现(可直接用)

2.1 事件总线(可选,但强烈推荐)

import 'dart:async'; enum AuthEvent { expired } class AuthEventBus { AuthEventBus._(); static final AuthEventBus I = AuthEventBus._(); final _c = StreamController<AuthEvent>.broadcast(); Stream<AuthEvent> get stream => _c.stream; void emit(AuthEvent e) => _c.add(e); }

2.2 队列元素

import 'package:dio/dio.dart'; class _QueuedReq { final RequestOptions options; final ErrorInterceptorHandler handler; _QueuedReq(this.options, this.handler); }

2.3 队列版 RefreshInterceptor

import 'package:dio/dio.dart'; class QueueRefreshInterceptor extends Interceptor { final Dio dio; final Dio cleanDio; final RefreshManager mgr; final List<_QueuedReq> _queue = []; bool _expiredEmitted = false; QueueRefreshInterceptor({ required this.dio, required this.cleanDio, required this.mgr, }); @override void onError(DioException err, ErrorInterceptorHandler handler) async { if (err.response?.statusCode != 401) { return handler.next(err); } final req = err.requestOptions; // 防死循环:同一请求只重放一次 if (req.extra["retried"] == true) { return handler.next(err); } final pair = TokenStore.get(); if (pair == null || pair.refreshToken.isEmpty) { _emitExpiredOnce(); return handler.next(err); } // ① 入队 + 挂起(此刻不 next,不 resolve) _queue.add(_QueuedReq(req, handler)); // ② 触发“只一次”的刷新 final ok = await mgr.getOrCreate(() async { try { final res = await cleanDio.post("/auth/refresh", data: { "refreshToken": pair.refreshToken, }); final data = res.data as Map<String, dynamic>; final access = data["accessToken"] as String; final refresh = data["refreshToken"] as String; await TokenStore.set(TokenPair(access, refresh)); return access; // 返回非空代表成功 } catch (_) { await TokenStore.clear(); return null; } }); // ③ 注意:多个 onError 都会走到这里。为了避免重复 drain: if (_queue.isEmpty) return; final pending = List<_QueuedReq>.from(_queue); _queue.clear(); // ④ 刷新失败:统一失败 + 全局登出事件 if (ok == null) { _emitExpiredOnce(); for (final q in pending) { q.handler.next(err); } return; } // ⑤ 刷新成功:统一重放队列请求(这里选择顺序重放,最稳) for (final q in pending) { try { final resp = await _replay(q.options); q.handler.resolve(resp); } catch (e) { q.handler.next(e is DioException ? e : err); } } } Future<Response<dynamic>> _replay(RequestOptions req) { final token = TokenStore.get()?.accessToken ?? ""; return dio.request( req.path, data: req.data, queryParameters: req.queryParameters, options: Options( method: req.method, headers: Map<String, dynamic>.from(req.headers) ..["Authorization"] = "Bearer $token", extra: Map<String, dynamic>.from(req.extra)..["retried"] = true, ), cancelToken: req.cancelToken, onSendProgress: req.onSendProgress, onReceiveProgress: req.onReceiveProgress, ); } void _emitExpiredOnce() { if (_expiredEmitted) return; _expiredEmitted = true; AuthEventBus.I.emit(AuthEvent.expired); } }

3. 队列版怎么接入 DioClient?

class DioClient { DioClient._(); static final DioClient instance = DioClient._(); late final Dio dio; late final Dio cleanDio; final RefreshManager mgr = RefreshManager(); void init() { dio = Dio(BaseOptions(baseUrl: "https://api.example.com")); cleanDio = Dio(BaseOptions(baseUrl: "https://api.example.com")); dio.interceptors.addAll([ AuthInterceptor(), QueueRefreshInterceptor(dio: dio, cleanDio: cleanDio, mgr: mgr), ]); } }

4. 共享 Future 版 vs 队列版怎么选?

  • 小中型项目:共享 Future 版足够(简单稳定)

  • 中大型项目:队列版更工程化(挂起/统一重放/统一失败)

结语:你真正学会的是“异步并发控制”

这套方案的本质不是 Dio,而是:

  • 共享 Future 作为异步锁

  • 临界区只执行一次

  • 失败请求恢复(retry / replay)

  • 全局状态一致(expired 事件)

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 12:02:10

anything-llm能否支持3D模型注释查询?工业设计场景设想

Anything-LLM能否支持3D模型注释查询&#xff1f;工业设计场景设想 在现代工业设计中&#xff0c;一个典型的挑战是&#xff1a;当工程师面对一个复杂的3D零件时&#xff0c;如何快速确认它的设计依据、材料规范或测试记录&#xff1f;比如&#xff0c;有人问&#xff1a;“这…

作者头像 李华
网站建设 2026/4/16 9:06:49

性能测试之全链路压测实战理论详解

前言 要说当下研发领域最热门的几个词&#xff0c;全链路压测 肯定跑不了。最近的几次大会上&#xff0c;也有不少关于全链路的议题。之前有朋友在面试过程中也有被问到了什么是全链路压测&#xff0c;如何有效的开展全链路压测。今天我们就来聊聊全链路压测&#xff0c;但本文…

作者头像 李华
网站建设 2026/4/16 11:02:29

基于数据挖掘的疾病数据可视化与预测系统任务书

山东中医药大学本科生毕业论文任务书毕业论文题目&#xff1a; 学生姓名 &#xff1a; 专业 &#xff1a; 学号 &#xff1a;主要研究内容&#xff08;方向&#xff09; :该系统是一个基于大数据和机器学习的医疗分析系统&#xff0c;利用Pandas、PySpark…

作者头像 李华
网站建设 2026/4/16 11:02:04

基于数据挖掘的线上教育平台用户行为价值分析系统文献综述

1. 概述(1) 研究背景在当今这个信息化时代&#xff0c;随着互联网的广泛覆盖与信息技术的飞速发展&#xff0c;线上教育平台已成为人们获取知识、进行自我提升的重要途径[1]。特别是在教育领域&#xff0c;线上教育平台不仅为学习者提供了丰富多样的学习资源&#xff0c;还创造…

作者头像 李华
网站建设 2026/4/14 17:09:50

Kafka生产环境踩坑实录:消息积压与性能调优

半夜被电话叫醒&#xff0c;消息积压了200万条&#xff0c;消费者根本追不上。 这种场景搞过Kafka的应该都经历过&#xff0c;整理一下踩过的坑和解决方案。 坑一&#xff1a;消息积压 现象 监控告警&#xff1a;topic-order的lag超过100万。 # 查看消费者lag kafka-consumer-g…

作者头像 李华
网站建设 2026/4/16 13:04:44

手把手教你制作Arduino寻迹小车(新手教程)

从零开始做一辆会“认路”的小车&#xff1a;Arduino寻迹实战全记录你有没有想过&#xff0c;让一个小车自己沿着黑线跑&#xff0c;不用遥控、也不靠人推&#xff1f;这听起来像是机器人比赛里的高科技项目&#xff0c;其实——用一块Arduino板子、几个红外传感器和电机驱动模…

作者头像 李华