MST
星途 面试题库

面试题:Flutter网络请求缓存策略在高并发场景下的优化

在Flutter应用面临高并发网络请求的情况下,已有的结合http/dio的网络请求数据缓存策略可能会出现性能问题,如缓存冲突、数据一致性等。请详细分析可能出现的问题,并提出针对性的优化方案,包括但不限于缓存机制调整、并发控制等方面。
38.2万 热度难度
前端开发Flutter

知识考点

AI 面试

面试题答案

一键面试

可能出现的问题分析

  1. 缓存冲突
    • 原因:当多个并发请求同时尝试访问和修改缓存时,就可能出现缓存冲突。例如,一个请求正在读取缓存数据,另一个请求同时在更新缓存,这可能导致读取到不完整或错误的数据。
    • 示例:假设缓存是一个简单的内存哈希表,两个并发请求A和B,A尝试读取缓存中某个键对应的值,而B同时删除了该键值对,那么A可能读取到一个不存在或已过期的数据。
  2. 数据一致性
    • 原因:在高并发环境下,网络请求返回数据的顺序可能与请求发出的顺序不一致。如果缓存机制没有妥善处理这种情况,可能会导致缓存中的数据与最新的网络数据不一致。
    • 示例:请求1先发起获取用户信息,请求2后发起更新用户信息。但由于网络波动,请求2先返回更新成功,请求1后返回旧的用户信息。如果缓存简单地根据请求返回顺序更新,就会导致缓存中的用户信息是旧的,与服务器上的实际数据不一致。
  3. 缓存失效策略问题
    • 原因:如果缓存失效策略设置不合理,在高并发时可能导致大量请求绕过缓存直接去请求网络,从而失去缓存的意义。例如,缓存过期时间设置过短,大量请求同时过期,瞬间对网络造成巨大压力。
    • 示例:假设所有缓存数据的过期时间都设置为1分钟,在某一分钟内有大量数据缓存过期,下一分钟内所有这些数据的请求都会直接去请求网络,可能导致网络拥堵。

优化方案

  1. 缓存机制调整
    • 采用读写锁
      • 原理:对于缓存的读取操作可以并发执行,而写入操作则需要独占锁。这样可以避免在读取缓存时被写入操作干扰,保证数据的一致性。
      • 代码示例(伪代码)
import 'dart:async';
import 'dart:io';

class Cache {
  final Map<String, dynamic> _cache = {};
  final ReadWriteLock _lock = ReadWriteLock();

  Future<dynamic> get(String key) async {
    await _lock.readLock();
    try {
      return _cache[key];
    } finally {
      _lock.readUnlock();
    }
  }

  Future<void> set(String key, dynamic value) async {
    await _lock.writeLock();
    try {
      _cache[key] = value;
    } finally {
      _lock.writeUnlock();
    }
  }
}
  • 多级缓存策略
    • 原理:结合内存缓存和磁盘缓存。内存缓存速度快,用于处理频繁访问的数据,但容量有限且断电易失;磁盘缓存容量大,用于存储不常访问但需要长期保存的数据。在高并发时,先从内存缓存读取数据,如果未命中再从磁盘缓存读取。
    • 实现方式:可以使用shared_preferences作为磁盘缓存,结合内存中的Map作为内存缓存。例如:
import 'package:shared_preferences/shared_preferences.dart';

class MultiLevelCache {
  final Map<String, dynamic> _memoryCache = {};
  SharedPreferences? _prefs;

  Future<dynamic> get(String key) async {
    if (_memoryCache.containsKey(key)) {
      return _memoryCache[key];
    }
    if (_prefs == null) {
      _prefs = await SharedPreferences.getInstance();
    }
    return _prefs!.get(key);
  }

  Future<void> set(String key, dynamic value) async {
    _memoryCache[key] = value;
    if (_prefs == null) {
      _prefs = await SharedPreferences.getInstance();
    }
    if (value is String) {
      await _prefs!.setString(key, value);
    } else if (value is int) {
      await _prefs!.setInt(key, value);
    } else if (value is bool) {
      await _prefs!.setBool(key, value);
    }
  }
}
  • 优化缓存失效策略
    • 随机过期时间:为缓存数据设置随机的过期时间,避免大量数据同时过期。例如,原本设置过期时间为1分钟,可以改为在30秒到90秒之间随机设置过期时间。
    • 基于时间戳和版本号的失效策略:在缓存数据时,同时记录数据的时间戳和版本号。每次请求网络更新数据时,更新版本号。如果缓存中的版本号与服务器返回的版本号不一致,即使缓存未过期也更新缓存。
  1. 并发控制
    • 使用队列控制并发请求数量
      • 原理:创建一个请求队列,设置最大并发请求数。当请求到达时,如果当前并发请求数小于最大并发数,则直接发起请求;否则,将请求加入队列,等待前面的请求完成后依次处理队列中的请求。
      • 代码示例(伪代码)
import 'dart:async';
import 'package:dio/dio.dart';

class RequestQueue {
  final int _maxConcurrentRequests;
  final Queue<Function> _requestQueue = Queue();
  int _activeRequests = 0;
  final Dio _dio = Dio();

  RequestQueue(this._maxConcurrentRequests);

  Future<Response> enqueueRequest(Function requestFunction) {
    Completer<Response> completer = Completer();
    _requestQueue.add(() async {
      try {
        Response response = await requestFunction();
        completer.complete(response);
      } catch (e) {
        completer.completeError(e);
      } finally {
        _activeRequests--;
        _processQueue();
      }
    });
    _processQueue();
    return completer.future;
  }

  void _processQueue() {
    while (_activeRequests < _maxConcurrentRequests && _requestQueue.isNotEmpty) {
      _activeRequests++;
      _requestQueue.removeFirst()();
    }
  }
}
  • 请求去重
    • 原理:在高并发时,可能会有多个相同的请求同时发起。可以使用一个Set来记录已经发起的请求,当有新请求时,先检查Set中是否已有相同请求,如果有则不再发起,而是等待已有请求的返回结果,并将结果共享。
    • 代码示例(伪代码)
import 'dart:async';
import 'package:dio/dio.dart';

class RequestDeduplication {
  final Map<String, Completer<Response>> _ongoingRequests = {};
  final Dio _dio = Dio();

  Future<Response> sendRequest(String url, {Map<String, dynamic>? data}) {
    String requestKey = '$url${data?.toString()?? ''}';
    if (_ongoingRequests.containsKey(requestKey)) {
      return _ongoingRequests[requestKey]!.future;
    }
    Completer<Response> completer = Completer();
    _ongoingRequests[requestKey] = completer;
    _dio.post(url, data: data).then((response) {
      completer.complete(response);
      _ongoingRequests.remove(requestKey);
    }).catchError((e) {
      completer.completeError(e);
      _ongoingRequests.remove(requestKey);
    });
    return completer.future;
  }
}