You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何高效调用限频API并实现数据本地缓存?

问题背景

我开发的应用需要从分页API拉取数据,具体情况如下:

  • API采用分页返回,每次请求最多获取1000条记录,总计需处理110000条(共11页)
  • 每条主记录需额外发起4次请求获取关联数据
  • API限流规则:每分钟最多允许60次请求

我用Kotlin的delay实现了一个限频函数,但运行速度过慢,现寻求两个问题的解决方案:

  1. 如何高效拉取所有API记录且不触发限流?
  2. 将所有记录缓存至本地数据库的可行思路?

当前实现代码

限频函数

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 02:23:14