在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

概述

在网络请求中,请求失败是不可避免的。网络不稳定、服务器暂时不可用、请求超时等情况都可能导致请求失败。实现请求重试机制可以自动重新发起失败的请求,提高整体成功率,提升用户体验。

本文将详细介绍请求重试机制的实现方法,包括简单重试、指数退避重试、带条件的重试等,并提供完整的代码示例。

为什么需要重试机制

网络请求失败的原因多种多样:

  • 网络不稳定:移动网络信号弱、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:是否使用指数退避
  • 提供了几个预设的配置:noneaggressiveconservative

带重试的 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:创建数据,多次请求可能创建多个相同的资源
  • 需要根据业务逻辑判断是否可重试

处理非幂等请求

对于非幂等请求,可以:

  1. 使用唯一标识符(如UUID)来防止重复创建
  2. 在服务端进行去重处理
  3. 不进行自动重试,让用户手动确认

总结

请求重试机制是构建健壮网络请求的重要组成部分。通过合理的重试策略,可以:

  • 提高请求成功率
  • 提升用户体验
  • 减少服务器压力
  • 增强系统稳定性

本文介绍了多种重试实现方式,从简单重试到带配置的高级重试,你可以根据实际需求选择合适的方案。

完整示例代码

以下是一个完整的带重试机制的 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 客户端,为你的网络请求提供可靠的重试保护。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐