请求重试机制 - 鸿蒙Flutter提升请求成功率



概述
在网络请求中,请求失败是不可避免的。网络不稳定、服务器暂时不可用、请求超时等情况都可能导致请求失败。实现请求重试机制可以自动重新发起失败的请求,提高整体成功率,提升用户体验。
本文将详细介绍请求重试机制的实现方法,包括简单重试、指数退避重试、带条件的重试等,并提供完整的代码示例。
为什么需要重试机制
网络请求失败的原因多种多样:
- 网络不稳定:移动网络信号弱、Wi-Fi连接中断等
- 服务器问题:服务器负载过高、临时维护、内部错误
- 超时:网络延迟过大导致请求超时
- 限流:请求频率过高被服务器限流
重试机制可以:
- 自动重新发起失败的请求
- 提高请求成功率
- 减少用户手动重试的操作
- 提升用户体验
简单重试实现
最简单的重试机制是在请求失败时立即重新发起请求。
基本代码
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<dynamic> fetchWithRetry(String url, {int maxRetries = 3}) async {
int retryCount = 0;
while (retryCount < maxRetries) {
try {
final response = await http.get(Uri.parse(url));
if (response.statusCode >= 200 && response.statusCode < 300) {
return jsonDecode(response.body);
}
} catch (e) {
// 忽略异常,继续重试
}
retryCount++;
await Future.delayed(Duration(seconds: retryCount));
}
throw Exception('请求失败,已重试$maxRetries次');
}
代码说明
maxRetries参数指定最大重试次数,默认为3次- 使用
while循环进行重试 - 每次重试后等待一定时间,等待时间随重试次数增加
- 如果达到最大重试次数仍未成功,则抛出异常
使用示例
try {
var data = await fetchWithRetry('https://api.example.com/users');
print('请求成功: $data');
} catch (e) {
print('请求失败: $e');
}
指数退避重试
简单重试有一个问题:如果服务器暂时不可用,频繁重试会增加服务器负担。指数退避策略可以解决这个问题。
什么是指数退避
指数退避是一种重试策略,每次重试的等待时间是前一次的两倍。例如:
- 第1次重试:等待1秒
- 第2次重试:等待2秒
- 第3次重试:等待4秒
- 第4次重试:等待8秒
这样可以避免在服务器繁忙时频繁请求,给服务器恢复的时间。
实现代码
Future<dynamic> fetchWithExponentialBackoff(String url, {
int maxRetries = 3,
Duration initialDelay = const Duration(seconds: 1),
}) async {
int retryCount = 0;
Duration delay = initialDelay;
while (retryCount < maxRetries) {
try {
final response = await http.get(Uri.parse(url));
if (response.statusCode >= 200 && response.statusCode < 300) {
return jsonDecode(response.body);
}
} catch (e) {
// 忽略异常
}
retryCount++;
if (retryCount < maxRetries) {
await Future.delayed(delay);
delay = delay * 2;
}
}
throw Exception('请求失败,已重试$maxRetries次');
}
代码说明
initialDelay参数指定初始延迟时间- 每次重试后,延迟时间翻倍
- 使用
Future.delayed()等待指定时间
使用示例
var data = await fetchWithExponentialBackoff(
'https://api.example.com/data',
maxRetries: 5,
initialDelay: const Duration(seconds: 2),
);
带条件的重试
不是所有的错误都适合重试。例如,客户端错误(4xx)通常不需要重试,因为这是客户端的问题。
可重试的状态码
| 状态码 | 含义 | 是否可重试 |
|---|---|---|
| 408 | 请求超时 | 是 |
| 429 | 请求过多 | 是 |
| 500 | 服务器内部错误 | 是 |
| 502 | 网关错误 | 是 |
| 503 | 服务不可用 | 是 |
| 504 | 网关超时 | 是 |
实现代码
bool shouldRetry(int statusCode) {
return statusCode >= 500 || statusCode == 408 || statusCode == 429;
}
Future<dynamic> fetchWithConditionalRetry(String url, {
int maxRetries = 3,
}) async {
int retryCount = 0;
while (retryCount < maxRetries) {
final response = await http.get(Uri.parse(url));
if (response.statusCode >= 200 && response.statusCode < 300) {
return jsonDecode(response.body);
}
if (shouldRetry(response.statusCode)) {
retryCount++;
await Future.delayed(Duration(seconds: retryCount * 2));
} else {
throw Exception('HTTP Error: ${response.statusCode}');
}
}
throw Exception('请求失败,已重试$maxRetries次');
}
代码说明
shouldRetry()函数判断状态码是否适合重试- 如果是可重试的状态码,则进行重试
- 如果是不可重试的状态码,直接抛出异常
重试配置类
将重试配置封装为一个类,可以方便地管理和传递重试参数。
实现代码
class RetryConfig {
final int maxRetries;
final Duration initialDelay;
final Duration maxDelay;
final bool useExponentialBackoff;
const RetryConfig({
this.maxRetries = 3,
this.initialDelay = const Duration(seconds: 1),
this.maxDelay = const Duration(seconds: 10),
this.useExponentialBackoff = true,
});
static const RetryConfig none = RetryConfig(maxRetries: 0);
static const RetryConfig aggressive = RetryConfig(
maxRetries: 5,
initialDelay: Duration(milliseconds: 500),
);
static const RetryConfig conservative = RetryConfig(
maxRetries: 2,
initialDelay: Duration(seconds: 3),
maxDelay: Duration(seconds: 15),
);
}
代码说明
maxRetries:最大重试次数initialDelay:初始延迟时间maxDelay:最大延迟时间(防止延迟时间过长)useExponentialBackoff:是否使用指数退避- 提供了几个预设的配置:
none、aggressive、conservative
带重试的 API 客户端
将重试机制集成到 API 客户端中,可以统一管理所有请求的重试逻辑。
实现代码
class RetryApiClient {
final String baseUrl;
final RetryConfig retryConfig;
final Duration timeout;
RetryApiClient({
required this.baseUrl,
this.retryConfig = const RetryConfig(),
this.timeout = const Duration(seconds: 10),
});
Map<String, String> _buildHeaders({bool requireAuth = true, String? token}) {
Map<String, String> headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
if (requireAuth && token != null) {
headers['Authorization'] = 'Bearer $token';
}
return headers;
}
bool _shouldRetry(int statusCode) {
return statusCode >= 500 || statusCode == 408 || statusCode == 429;
}
Future<dynamic> _retry(Future<http.Response> Function() request, RetryConfig config) async {
int retryCount = 0;
Duration delay = config.initialDelay;
while (retryCount <= config.maxRetries) {
try {
final response = await request().timeout(timeout);
if (response.statusCode >= 200 && response.statusCode < 300) {
return jsonDecode(response.body);
}
if (!_shouldRetry(response.statusCode)) {
throw Exception('HTTP Error: ${response.statusCode}');
}
} on TimeoutException {
// 超时也是可重试的错误
} catch (e) {
if (retryCount >= config.maxRetries) {
rethrow;
}
}
retryCount++;
await Future.delayed(delay);
if (config.useExponentialBackoff) {
delay = (delay * 2).clamp(config.initialDelay, config.maxDelay);
}
}
throw Exception('请求失败,已重试${config.maxRetries}次');
}
Future<dynamic> get(String path, {
Map<String, String>? query,
RetryConfig? config,
String? token,
}) async {
RetryConfig effectiveConfig = config ?? retryConfig;
Uri uri = Uri.parse('$baseUrl$path');
if (query != null) {
uri = uri.replace(queryParameters: query);
}
return _retry(
() => http.get(uri, headers: _buildHeaders(requireAuth: true, token: token)),
effectiveConfig,
);
}
Future<dynamic> post(String path, Map<String, dynamic> body, {
RetryConfig? config,
String? token,
}) async {
RetryConfig effectiveConfig = config ?? retryConfig;
return _retry(
() => http.post(
Uri.parse('$baseUrl$path'),
headers: _buildHeaders(requireAuth: true, token: token),
body: jsonEncode(body),
),
effectiveConfig,
);
}
Future<dynamic> put(String path, Map<String, dynamic> body, {
RetryConfig? config,
String? token,
}) async {
RetryConfig effectiveConfig = config ?? retryConfig;
return _retry(
() => http.put(
Uri.parse('$baseUrl$path'),
headers: _buildHeaders(requireAuth: true, token: token),
body: jsonEncode(body),
),
effectiveConfig,
);
}
Future<dynamic> delete(String path, {
RetryConfig? config,
String? token,
}) async {
RetryConfig effectiveConfig = config ?? retryConfig;
return _retry(
() => http.delete(
Uri.parse('$baseUrl$path'),
headers: _buildHeaders(requireAuth: true, token: token),
),
effectiveConfig,
);
}
}
使用示例
final apiClient = RetryApiClient(baseUrl: 'https://api.example.com');
// 使用默认重试配置
var user = await apiClient.get('/users/1');
// 使用自定义重试配置
var data = await apiClient.get(
'/large-data',
config: const RetryConfig(maxRetries: 5),
);
// 不重试
var result = await apiClient.post(
'/users',
{'name': 'John'},
config: RetryConfig.none,
);
重试机制的高级技巧
1. 带抖动的指数退避
在指数退避的基础上添加随机抖动,可以避免多个客户端同时重试导致的"风暴"。
Duration _calculateDelay(Duration baseDelay, int retryCount) {
final jitter = Random().nextDouble() * baseDelay.inMilliseconds;
final delay = baseDelay * (1 << retryCount);
return Duration(milliseconds: (delay.inMilliseconds + jitter).toInt());
}
2. 重试日志记录
记录重试日志可以帮助排查问题。
Future<dynamic> _retry(Future<http.Response> Function() request, RetryConfig config) async {
int retryCount = 0;
Duration delay = config.initialDelay;
while (retryCount <= config.maxRetries) {
try {
final response = await request();
if (response.statusCode >= 200 && response.statusCode < 300) {
if (retryCount > 0) {
print('请求成功,重试次数: $retryCount');
}
return jsonDecode(response.body);
}
} catch (e) {
print('请求失败(第$retryCount次): $e');
}
retryCount++;
if (retryCount <= config.maxRetries) {
print('等待${delay.inSeconds}秒后重试...');
await Future.delayed(delay);
delay = delay * 2;
}
}
throw Exception('请求失败');
}
3. 可取消的重试
提供取消机制,允许用户在重试过程中取消请求。
Future<dynamic> fetchWithCancelableRetry(
String url, {
int maxRetries = 3,
required CancelToken cancelToken,
}) async {
int retryCount = 0;
while (retryCount < maxRetries) {
if (cancelToken.isCancelled) {
throw Exception('请求已取消');
}
try {
final response = await http.get(Uri.parse(url));
if (response.statusCode >= 200 && response.statusCode < 300) {
return jsonDecode(response.body);
}
} catch (e) {
// 忽略异常
}
retryCount++;
await Future.delayed(Duration(seconds: retryCount));
}
throw Exception('请求失败');
}
UI 层的重试处理
在 UI 层需要为用户提供重试操作的界面。
示例代码
class RetryDemoPage extends StatefulWidget {
const RetryDemoPage({super.key});
State<RetryDemoPage> createState() => _RetryDemoPageState();
}
class _RetryDemoPageState extends State<RetryDemoPage> {
String _status = '准备就绪';
bool _loading = false;
int _retryCount = 0;
Future<void> _fetchData() async {
setState(() {
_loading = true;
_status = '请求中...';
_retryCount = 0;
});
try {
final client = RetryApiClient(
baseUrl: 'https://jsonplaceholder.typicode.com',
retryConfig: const RetryConfig(maxRetries: 3),
);
var user = await client.get('/users/1');
setState(() {
_status = '请求成功!\n用户名: ${user['name']}\n重试次数: $_retryCount';
});
} catch (e) {
setState(() {
_status = '请求失败: $e';
});
} finally {
setState(() => _loading = false);
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('重试机制示例')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _loading ? null : _fetchData,
child: _loading
? const CircularProgressIndicator()
: const Text('发起请求'),
),
const SizedBox(height: 20),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(_status),
),
),
],
),
),
);
}
}
重试机制最佳实践
1. 不要对所有请求都重试
只对可重试的错误进行重试,避免不必要的重试。
2. 使用指数退避策略
指数退避可以避免频繁重试给服务器带来压力。
3. 设置最大重试次数
设置最大重试次数,避免无限循环重试。
4. 设置最大延迟时间
设置最大延迟时间,避免等待时间过长。
5. 记录重试日志
记录重试日志,便于问题排查和监控。
6. 考虑幂等性
对幂等请求可以放心重试,非幂等请求需要谨慎处理。
7. 提供重试配置
提供灵活的重试配置,允许根据不同场景调整。
8. 与超时机制结合
将重试机制与超时机制结合,提供更健壮的网络请求。
幂等性说明
在实现重试机制时,需要考虑请求的幂等性。
幂等请求
幂等请求是指多次执行相同的请求,结果是相同的。这类请求可以安全地重试。
- GET:查询数据,多次查询结果相同
- PUT:更新数据(根据ID),多次更新结果相同
- DELETE:删除数据,多次删除结果相同
非幂等请求
非幂等请求是指多次执行相同的请求,结果可能不同。这类请求需要谨慎重试。
- POST:创建数据,多次请求可能创建多个相同的资源
- 需要根据业务逻辑判断是否可重试
处理非幂等请求
对于非幂等请求,可以:
- 使用唯一标识符(如UUID)来防止重复创建
- 在服务端进行去重处理
- 不进行自动重试,让用户手动确认
总结
请求重试机制是构建健壮网络请求的重要组成部分。通过合理的重试策略,可以:
- 提高请求成功率
- 提升用户体验
- 减少服务器压力
- 增强系统稳定性
本文介绍了多种重试实现方式,从简单重试到带配置的高级重试,你可以根据实际需求选择合适的方案。
完整示例代码
以下是一个完整的带重试机制的 API 客户端:
import 'package:http/http.dart' as http;
import 'dart:convert';
class RetryConfig {
final int maxRetries;
final Duration initialDelay;
final Duration maxDelay;
final bool useExponentialBackoff;
const RetryConfig({
this.maxRetries = 3,
this.initialDelay = const Duration(seconds: 1),
this.maxDelay = const Duration(seconds: 10),
this.useExponentialBackoff = true,
});
}
class RetryApiClient {
final String baseUrl;
final RetryConfig retryConfig;
final Duration timeout;
RetryApiClient({
required this.baseUrl,
this.retryConfig = const RetryConfig(),
this.timeout = const Duration(seconds: 10),
});
bool _shouldRetry(int statusCode) {
return statusCode >= 500 || statusCode == 408 || statusCode == 429;
}
Future<dynamic> _retry(Future<http.Response> Function() request, RetryConfig config) async {
int retryCount = 0;
Duration delay = config.initialDelay;
while (retryCount <= config.maxRetries) {
try {
final response = await request().timeout(timeout);
if (response.statusCode >= 200 && response.statusCode < 300) {
return jsonDecode(response.body);
}
if (!_shouldRetry(response.statusCode)) {
throw Exception('HTTP Error: ${response.statusCode}');
}
} on TimeoutException {
// 超时也是可重试的错误
} catch (e) {
if (retryCount >= config.maxRetries) {
rethrow;
}
}
retryCount++;
await Future.delayed(delay);
if (config.useExponentialBackoff) {
delay = (delay * 2).clamp(config.initialDelay, config.maxDelay);
}
}
throw Exception('请求失败');
}
Future<dynamic> get(String path, {RetryConfig? config}) async {
return _retry(
() => http.get(Uri.parse('$baseUrl$path')),
config ?? retryConfig,
);
}
Future<dynamic> post(String path, Map<String, dynamic> body, {RetryConfig? config}) async {
return _retry(
() => http.post(
Uri.parse('$baseUrl$path'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(body),
),
config ?? retryConfig,
);
}
}
通过这个示例,你可以快速实现一个带有重试机制的 API 客户端,为你的网络请求提供可靠的重试保护。
更多推荐



所有评论(0)