如何高效调用限频API并实现数据本地缓存?
问题背景
我开发的应用需要从分页API拉取数据,具体情况如下:
- API采用分页返回,每次请求最多获取1000条记录,总计需处理110000条(共11页)
- 每条主记录需额外发起4次请求获取关联数据
- API限流规则:每分钟最多允许60次请求
我用Kotlin的delay实现了一个限频函数,但运行速度过慢,现寻求两个问题的解决方案:
- 如何高效拉取所有API记录且不触发限流?
- 将所有记录缓存至本地数据库的可行思路?
当前实现代码
限频函数
suspend fun <T> observeRateLimit(delayInMillis: Long, block: suspend () -> T): Deferred<T> = withContext(Dispatchers.IO) { launch { delay(delayInMillis) } async { block() } }
数据拉取逻辑
suspend fun populate(nextPage: Int?) { getAllRecords(nextPage).map { result -> val records = result.data.map { // 等待1分钟 observeRateLimitAsync(60000) { val id = it.id val entity = it.mapToEntity() // 获取关联数据1 val related1 = fetchRelatedRecord1(id) // 获取关联数据2 val related2 = fetchRelatedRecord2(id) // 获取关联数据3 val related3 = fetchRelatedRecord3(id) // 获取关联数据4 val related4 = fetchRelatedRecord4(id) // 处理实体 entity }.await() } // 插入数据库 // 如果有下一页,继续拉取 if (result.pages.nextUrl != null) populate(records.size + 1000) }.flowOn(Dispatcher.IO).collect() }
补充:API接口与仓库实现
ApiService
interface ApiService { @GET("$path/{id}") suspend fun getSubject(@Path("id") id: Int): Result<Response> }
Repository
class Repository @Inject constructor(val apiService: ApiService) { suspend fun getSubject(id: Int) = apiService.getSubject(id) }
解决方案
1. 高效拉取数据且不触发限流
当前实现每条记录等待1分钟,完全浪费了限流配额,优化核心是精准控制请求频率并最大化并行处理:
(1)用令牌桶算法实现精准限频
自己实现轻量级令牌桶,控制每分钟发放60个请求令牌,所有API请求(主分页+关联数据)都需先获取令牌,既不超限流规则,又能把配额用满。
class RateLimiter(private val permitsPerMinute: Int) { private val tokenChannel = Channel<Unit>(Channel.UNLIMITED) private val scope = CoroutineScope(Dispatchers.IO) init { scope.launch { val intervalMs = (60_000 / permitsPerMinute).toLong() while (isActive) { tokenChannel.send(Unit) delay(intervalMs) } } } suspend fun acquire() { tokenChannel.receive() } } // 全局初始化限流器 val apiRateLimiter = RateLimiter(60)
(2)并行处理+批量执行
- 分页拉取主数据时,并行处理当前页的所有关联请求,但每个请求前都通过限流器获取令牌
- 不要等一页的关联请求全部处理完再拉下一页,可并行发起下一页的主数据请求(同样走限流器)
优化后的populate逻辑示例:
suspend fun populate(nextPage: Int?) { getAllRecords(nextPage).collect { result -> // 并行处理当前页所有记录的关联数据 val processedEntities = result.data.map { record -> async(Dispatchers.IO) { val id = record.id val entity = record.mapToEntity() // 每个关联请求都先获取令牌 apiRateLimiter.acquire() val related1 = fetchRelatedRecord1(id) apiRateLimiter.acquire() val related2 = fetchRelatedRecord2(id) apiRateLimiter.acquire() val related3 = fetchRelatedRecord3(id) apiRateLimiter.acquire() val related4 = fetchRelatedRecord4(id) // 将关联数据绑定到实体 entity.apply { this.related1 = related1 this.related2 = related2 this.related3 = related3 this.related4 = related4 } } }.awaitAll() // 批量插入数据库 batchInsertToDb(processedEntities) // 处理下一页(需正确解析nextPage参数) result.pages.nextUrl?.let { val nextPageNum = parseNextPageNumber(it) populate(nextPageNum) } } }
(3)优先处理分页请求
分页请求是后续所有操作的基础,可以给分页请求设置稍高的优先级,确保分页拉取不被关联请求阻塞,进一步提升整体效率。
2. 本地数据库缓存思路
(1)分表存储主数据与关联数据
- 创建主数据表,存储分页API返回的基础记录
- 为每个关联数据创建独立表,通过主记录ID建立外键关联(以SQLite/Room为例)
- 优势:关联数据可独立更新,数据结构更清晰,便于后续查询维护
(2)批量插入提升性能
避免单条数据插入,用Room的批量插入注解配合列表参数,大幅降低数据库IO开销:
@Dao interface MainEntityDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(entities: List<MainEntity>) } @Dao interface Related1Dao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(relatedList: List<Related1>) }
(3)事务保证数据一致性
插入主数据和关联数据时,用数据库事务确保操作原子性,避免出现主数据已插入但关联数据缺失的情况:
@Dao interface SyncDao { @Transaction suspend fun syncBatch(mainEntities: List<MainEntity>, related1List: List<Related1>, related2List: List<Related2>) { insertAllMain(mainEntities) insertAllRelated1(related1List) insertAllRelated2(related2List) // ... 其他关联数据插入 } }
(4)增量更新策略
- 记录每条数据的最后更新时间,后续同步时优先拉取本地没有或更新时间晚于本地的记录(需API支持按时间过滤)
- 若API不支持增量,可定期分批次全量同步,避免一次性占用过多系统资源
内容的提问来源于stack exchange,提问作者codejockie
相关产品推荐
相关产品推荐

