新闻详情

新闻详情

首页 / 资讯中心 / 详情

前端资源接入:Provider与Lease解决取消、缓存与迟到结果

发布时间:2026/9/19 16:24:59来源:尧图网络
前端资源接入:Provider与Lease解决取消、缓存与迟到结果
1. 从一次线上事故说起FUI 资源接入为什么需要 Provider 和 Lease去年年底我们团队负责的一个数据看板项目上线了新版本核心改动是把原来写死在页面里的几组业务数据源改成了通过统一的资源接入层动态获取。上线当天下午运营同学反馈说切换筛选条件时偶尔会看到上一次的旧数据闪一下然后才刷新成新数据更诡异的是有几次快速连续切换后页面直接卡在了加载态怎么点都没反应。排查过程不算复杂但暴露出来的问题很典型我们只做了“发起请求”和“拿到结果”这两件事完全没有处理取消、缓存和迟到结果这三种情况。快速切换筛选条件时前一个请求还没回来后一个请求已经发出去了两个请求的结果谁先到谁后到完全看网络心情。如果旧请求的结果后到它就会覆盖掉新请求已经渲染好的数据这就是“旧数据闪一下”的来源。而卡在加载态则是因为某个请求因为组件卸载被中断了但我们的状态管理没有收到任何“这个请求已经作废”的信号加载标志位永远停在 true。这三个问题——取消、缓存、迟到结果——几乎是所有做资源接入的人都会踩的坑。而解决它们的核心抽象就是Provider和Lease这两个概念。Provider 负责“资源从哪里来、怎么取”Lease 负责“这次取用什么时候开始、什么时候结束、结果还算不算数”。你可以把 Provider 理解成图书馆Lease 理解成你手里的借书证书是图书馆的但你能看多久、什么时候还、过期了还能不能续由借书证说了算。这篇内容适合正在做前端资源接入、数据层封装、或者任何需要处理异步请求生命周期的开发者。不管你用的是 React、Vue 还是原生 JS只要你在处理“请求可能被取消、结果可能迟到、数据需要缓存”这类问题Provider 和 Lease 这套思路都能直接套用。下面我会从设计动机讲起把取消、缓存、迟到结果这三块拆开揉碎再给出一套可以直接抄作业的实现方案最后分享几个我在实际项目中踩过的坑。2. Provider 与 Lease 的职责边界谁管什么为什么不能混2.1 Provider 的定位资源的唯一入口Provider 的核心职责只有一件事根据一个资源标识返回该资源的数据。它不关心谁在请求、请求了几次、请求完之后调用方还在不在。用一句话概括就是“你要数据我给数据其他的我不管”。这个定位非常重要因为它决定了 Provider 可以被多个调用方共享。比如一个用户信息 Provider页面头部要用、侧边栏要用、弹窗里也要用如果 Provider 本身持有“当前请求状态”这种和调用方绑定的东西那多个调用方之间就会互相干扰。所以 Provider 必须是无状态的或者说它的状态只和资源本身有关比如缓存和调用方无关。Provider 通常需要暴露的能力包括根据资源标识获取数据可能是同步的也可能是异步的管理资源级别的缓存同一个资源标识短时间内多次请求应该复用结果提供缓存失效和刷新机制注意这里说的是“资源级别的缓存”不是“请求级别的缓存”。资源标识相同不管谁请求、请求多少次都应该命中同一份缓存。这是 Provider 和 Lease 分工的第一个关键点。2.2 Lease 的定位一次取用行为的生命周期凭证Lease 这个词借用了资源管理领域的“租约”概念。一次资源取用行为从发起到结束就是一个 Lease 的生命周期。Lease 需要回答三个问题这次取用是否还有效如果调用方已经不需要这个结果了比如组件卸载、筛选条件变了Lease 应该被标记为失效后续即使数据回来了也不能用。这次取用的结果应该给谁一个 Provider 可能同时被多个 Lease 使用每个 Lease 对应一个调用方结果必须精确投递。这次取用什么时候结束正常拿到数据算结束被取消也算结束超时也算结束。结束之后必须释放相关资源不能留下悬空的状态。Lease 是有状态的而且这个状态是跟调用方绑定的。每个调用方持有自己的 Lease互不干扰。这就是为什么取消操作必须作用在 Lease 上而不是 Provider 上——取消的是“我这次取用”不是“这个资源以后都不能用了”。2.3 为什么不能把两者合并我见过不少实现把 Provider 和 Lease 揉成一个对象结果就是要么缓存没法共享因为每个调用方都新建了一个 Provider要么取消操作会误伤其他调用方因为取消的是共享的 Provider。举个具体的例子。假设页面 A 和页面 B 同时在使用“当前登录用户信息”这个资源。如果 Provider 和 Lease 合并了页面 A 卸载时调用取消这个取消会作用在共享的 Provider 上页面 B 的请求也会被一起取消页面 B 就莫名其妙拿不到数据了。反过来如果每个调用方都新建一个 Provider那缓存就没法共享页面 A 和页面 B 会各自发一次请求浪费带宽也浪费服务端资源。所以职责边界必须清晰Provider 管资源Lease 管取用。Provider 是长期存在的、共享的Lease 是短期的、每个调用方独有的。这个边界划清楚了取消、缓存、迟到结果这三个问题才有解。3. 取消机制的设计从“发了不管”到“精准撤回”3.1 取消的本质是状态作废不是中断请求很多人一提到取消第一反应就是“把请求中断掉”。但实际上取消的本质是把这次取用的状态标记为作废至于底层请求是否真的被中断那是优化手段不是必要条件。为什么这么说因为请求一旦发出去服务端可能已经开始处理了你中断的只是客户端等待结果的行为服务端该干的活一点没少。而且有些底层请求比如某些环境下的 fetch本身就不支持真正的中断。所以正确的做法是Lease 上维护一个cancelled标志取消时把它置为 true数据回来时先检查这个标志如果已经取消就直接丢弃结果不做任何状态更新。这样做的好处是取消逻辑和底层请求实现解耦。你可以在支持中断的环境里顺手把请求也中断掉省点资源在不支持的环境里就只做状态作废行为是一致的。3.2 Lease 上的取消标志怎么设计Lease 的取消标志需要满足几个条件可读数据回来时能检查可写取消操作能设置幂等多次取消不应该报错或产生副作用可通知取消时如果有正在等待的回调应该能通知到一个简单的实现是给 Lease 加一个status字段取值包括pending、fulfilled、rejected、cancelled。取消时把 status 从pending改成cancelled如果已经是终态fulfilled/rejected/cancelled就什么都不做保证幂等。class Lease { constructor(provider, key) { this.provider provider; this.key key; this.status pending; this.listeners []; } cancel() { if (this.status ! pending) return; this.status cancelled; this.notify(); } isActive() { return this.status pending; } notify() { this.listeners.forEach(fn fn(this.status)); this.listeners []; } }这段代码里cancel方法先检查状态只有pending才能取消保证了幂等。isActive方法供数据回来时检查如果返回 false 就丢弃结果。notify用来通知等待方比如让加载态结束。3.3 取消的触发时机组件卸载与依赖变更取消操作最常见的两个触发时机是组件卸载和依赖变更。组件卸载时取消是为了避免“组件已经没了数据才回来然后往一个不存在的组件上写状态”这种问题。在 React 里就是useEffect的清理函数在 Vue 里就是onUnmounted钩子。依赖变更时取消是为了避免“旧依赖的请求结果覆盖新依赖的数据”。比如筛选条件从 A 变成 BA 的请求应该被取消因为它的结果已经不再需要了。这里有个细节依赖变更时是先取消旧 Lease 再创建新 Lease还是先创建新 Lease 再取消旧 Lease我建议先取消旧的再创建新的因为如果先创建新的新请求可能很快返回然后旧请求才被取消中间会有一个短暂的状态混乱窗口。// React 中的典型用法 useEffect(() { const lease provider.acquire(key); lease.onStatusChange(status { if (status fulfilled) { setData(lease.getData()); } }); return () lease.cancel(); }, [key]);这段代码里key变化时清理函数会先执行取消旧 Lease然后 effect 重新执行创建新 Lease。顺序是对的。3.4 取消后的资源清理取消之后Lease 上可能还挂着一些资源比如事件监听器、定时器、回调引用。这些必须在取消时一并清理否则会造成内存泄漏。具体来说取消时需要做三件事清空listeners数组断开所有回调引用如果 Lease 上挂了定时器比如超时控制清除定时器如果 Lease 上保存了数据引用视情况释放如果数据很大且不再需要这里有个容易忽略的点取消之后Lease 对象本身可能还被外部引用着比如闭包里所以不能只清空内部字段就完事最好提供一个dispose方法把 Lease 和 Provider 之间的关联也断开让 Lease 可以被垃圾回收。4. 缓存策略Provider 层的资源复用与失效4.1 缓存应该放在 Provider 还是 Lease这个问题我在团队里争论过好几次。放在 Lease 上每个调用方都有自己的缓存简单直接但没法共享放在 Provider 上可以共享但需要处理并发请求的合并问题。我的结论是缓存必须放在 Provider 上。原因很简单缓存的目的是“同一个资源不要重复请求”而“同一个资源”这个概念只有 Provider 层面才知道。如果放在 Lease 上两个调用方请求同一个资源各自缓存各自的那缓存就失去了意义。放在 Provider 上之后Provider 需要维护一个key - cacheEntry的映射。cacheEntry 里至少要有数据、时间戳、以及一个“正在请求中”的标记用于并发合并。4.2 缓存命中与并发合并缓存命中分两种情况已有数据和正在请求中。已有数据的情况好处理直接返回缓存数据同时可以视策略决定是否后台刷新stale-while-revalidate。正在请求中的情况就需要并发合并第一个请求发出去之后在它返回之前如果有新的 Lease 请求同一个 key不应该再发一次请求而是把新的 Lease 挂到第一个请求上等第一个请求返回后把结果分发给所有挂着的 Lease。class Provider { constructor(fetcher) { this.fetcher fetcher; this.cache new Map(); // key - { data, timestamp, pending, leases } } acquire(key) { const lease new Lease(this, key); let entry this.cache.get(key); if (entry entry.data ! undefined !this.isExpired(entry)) { // 缓存命中直接返回 lease.status fulfilled; lease.data entry.data; return lease; } if (entry entry.pending) { // 已有请求在飞挂上去 entry.leases.push(lease); return lease; } // 新建请求 entry { data: undefined, timestamp: 0, pending: true, leases: [lease] }; this.cache.set(key, entry); this.fetcher(key).then( data this.resolve(key, data), err this.reject(key, err) ); return lease; } }这段代码里acquire方法先查缓存命中就直接返回没命中但已有请求在飞就挂到entry.leases上都没有才新建请求。这样并发合并就自然实现了。4.3 缓存失效的几种触发方式缓存不能永远有效必须有失效机制。常见的失效触发方式有时间过期设置 TTL超过时间就视为过期手动失效提供invalidate(key)方法外部主动清除依赖失效某个资源变更后依赖它的其他资源也要失效版本失效资源标识里带版本号版本变了自然就是新资源时间过期是最基础的但 TTL 设多少需要根据业务特点来定。用户信息这种变化不频繁的TTL 可以设长一点比如 5 分钟实时性要求高的数据TTL 就要短甚至不缓存。手动失效适合“写操作后刷新读缓存”的场景。比如用户改了昵称改完之后调用invalidate(user:123)下次请求就会重新拉取。依赖失效比较复杂需要维护资源之间的依赖关系图。我的建议是除非业务确实需要否则不要轻易引入依赖失效用版本号或者手动失效更简单可靠。4.4 缓存与取消的交互取消不应该清缓存这里有一个非常容易搞错的点取消一个 Lease不应该清除 Provider 的缓存。原因很简单取消的是“我这次取用”不是“这个资源作废了”。如果取消时把缓存也清了那其他正在使用这个缓存的调用方就会受影响而且下次请求还得重新拉一遍缓存的意义就没了。正确的做法是取消只影响 Lease 自己的状态Provider 的缓存该留留。只有当缓存本身过期或者被手动失效时才清除缓存。// Lease 的 cancel 方法只改自己的状态 cancel() { if (this.status ! pending) return; this.status cancelled; this.notify(); // 注意这里不碰 provider.cache }这个设计还有一个好处如果取消之后很快又有新的 Lease 请求同一个 key可以直接命中缓存如果请求已经返回并写入了缓存不用重新发请求。5. 迟到结果的识别与丢弃让过期数据无处可逃5.1 迟到结果的两种来源迟到结果有两种来源请求本身慢和取消不及时。请求本身慢很好理解网络抖动、服务端处理慢都会导致结果回来得晚。取消不及时则是说调用方已经不需要这个结果了但取消信号还没传到或者传到了但请求已经发出去了结果还是会回来。这两种情况都会导致同一个问题一个已经作废的 Lease它的结果回来了如果不加识别就使用就会污染当前状态。5.2 用 Lease 状态做第一道防线识别迟到结果的第一道防线就是 Lease 的status字段。数据回来时先检查lease.status如果不是pending说明这个 Lease 已经被取消或者已经完成了结果直接丢弃。resolve(key, data) { const entry this.cache.get(key); if (!entry) return; entry.data data; entry.timestamp Date.now(); entry.pending false; entry.leases.forEach(lease { if (lease.status ! pending) return; // 迟到结果丢弃 lease.status fulfilled; lease.data data; lease.notify(); }); entry.leases []; }这段代码里entry.leases.forEach里的if (lease.status ! pending) return;就是第一道防线。只要 Lease 不是 pending结果就不给它。5.3 用请求序号做第二道防线第一道防线能挡住大部分情况但有一种情况挡不住同一个 Lease 被复用了。比如某些实现里Lease 对象会被池化复用取消之后又被重新激活这时候 status 又变回 pending 了旧请求的结果回来时就会误判为有效。解决方法是给每个请求分配一个递增的序号Lease 上记录自己当前的序号数据回来时比对序号不一致就丢弃。let requestSeq 0; acquire(key) { const lease new Lease(this, key); lease.seq requestSeq; // ... } resolve(key, data, seq) { // ... entry.leases.forEach(lease { if (lease.seq ! seq) return; // 序号不匹配丢弃 // ... }); }序号机制比状态机制更严格因为它不依赖 Lease 的状态变化而是依赖请求本身的唯一性。即使 Lease 被复用序号也不会重复所以能准确识别迟到结果。5.4 迟到结果与缓存写入的关系这里有个细节需要想清楚迟到结果要不要写入缓存我的建议是如果请求已经完成结果应该写入缓存即使所有 Lease 都已经取消。因为缓存是资源级别的和 Lease 无关。请求已经发出去了服务端也处理了结果拿回来不写缓存就浪费了。下次有新的 Lease 请求同一个 key可以直接命中缓存不用重新请求。但要注意写入缓存和分发给 Lease 是两件事。写入缓存是无条件的只要请求成功分发给 Lease 是有条件的只给 pending 的 Lease。这两件事分开处理逻辑才清晰。resolve(key, data, seq) { const entry this.cache.get(key); if (!entry) return; // 无条件写入缓存 entry.data data; entry.timestamp Date.now(); entry.pending false; // 有条件分发给 Lease entry.leases.forEach(lease { if (lease.seq ! seq || lease.status ! pending) return; lease.status fulfilled; lease.data data; lease.notify(); }); entry.leases []; }这个设计还有一个额外好处如果取消之后马上又有新请求而旧请求刚好返回并写入了缓存新请求可以直接命中缓存响应速度会快很多。6. 一套可直接落地的实现方案6.1 核心类结构把前面的设计整合起来核心就是两个类Provider和Lease。Provider 负责资源获取和缓存Lease 负责取用生命周期。class Lease { constructor(provider, key) { this.provider provider; this.key key; this.status pending; this.data undefined; this.error undefined; this.seq 0; this.listeners []; } onStatusChange(fn) { if (this.status ! pending) { fn(this.status, this.data, this.error); return; } this.listeners.push(fn); } cancel() { if (this.status ! pending) return; this.status cancelled; this.notify(); } notify() { this.listeners.forEach(fn fn(this.status, this.data, this.error)); this.listeners []; } } class Provider { constructor(fetcher, options {}) { this.fetcher fetcher; this.ttl options.ttl || 60000; this.cache new Map(); this.seq 0; } acquire(key) { const lease new Lease(this, key); lease.seq this.seq; let entry this.cache.get(key); if (entry entry.data ! undefined !this.isExpired(entry)) { lease.status fulfilled; lease.data entry.data; return lease; } if (entry entry.pending) { entry.leases.push(lease); return lease; } entry { data: undefined, timestamp: 0, pending: true, leases: [lease] }; this.cache.set(key, entry); this.fetcher(key).then( data this.resolve(key, data, lease.seq), err this.reject(key, err, lease.seq) ); return lease; } isExpired(entry) { return Date.now() - entry.timestamp this.ttl; } resolve(key, data, seq) { const entry this.cache.get(key); if (!entry) return; entry.data data; entry.timestamp Date.now(); entry.pending false; entry.leases.forEach(lease { if (lease.seq ! seq || lease.status ! pending) return; lease.status fulfilled; lease.data data; lease.notify(); }); entry.leases []; } reject(key, error, seq) { const entry this.cache.get(key); if (!entry) return; entry.pending false; entry.leases.forEach(lease { if (lease.seq ! seq || lease.status ! pending) return; lease.status rejected; lease.error error; lease.notify(); }); entry.leases []; } invalidate(key) { this.cache.delete(key); } }6.2 在 React 中的接入方式在 React 里最自然的接入方式是封装一个自定义 Hook把 Lease 的生命周期和组件的生命周期对齐。function useResource(provider, key) { const [state, setState] useState({ status: pending, data: undefined, error: undefined }); useEffect(() { const lease provider.acquire(key); lease.onStatusChange((status, data, error) { setState({ status, data, error }); }); return () lease.cancel(); }, [provider, key]); return state; }这个 Hook 里useEffect的依赖是provider和key任何一个变化都会取消旧 Lease、创建新 Lease。清理函数调用lease.cancel()保证组件卸载或依赖变更时旧 Lease 被取消。6.3 在 Vue 中的接入方式Vue 3 的 Composition API 里接入方式和 React 类似只是生命周期钩子不同。import { ref, watchEffect, onUnmounted } from vue; function useResource(provider, keyRef) { const state ref({ status: pending, data: undefined, error: undefined }); let currentLease null; watchEffect(() { if (currentLease) currentLease.cancel(); const lease provider.acquire(keyRef.value); currentLease lease; lease.onStatusChange((status, data, error) { state.value { status, data, error }; }); }); onUnmounted(() { if (currentLease) currentLease.cancel(); }); return state; }watchEffect会在keyRef变化时重新执行执行前先取消旧 Lease。onUnmounted保证组件卸载时也取消。6.4 关键参数的选择建议TTL 设多少取决于业务对数据新鲜度的要求。我一般会分三档数据类型建议 TTL理由用户信息、配置项5 分钟变化不频繁长一点减少请求列表数据、统计数据30 秒有一定实时性要求但不需要秒级实时数据、状态数据不缓存或 5 秒实时性要求高缓存意义不大并发合并的窗口期其实就是请求在飞的时间不需要额外设置。只要entry.pending为 true新 Lease 就会挂上去不会重复请求。7. 实战中踩过的坑与排查思路7.1 坑一取消后加载态不结束现象快速切换筛选条件页面卡在加载态转圈不停。排查过程先看加载态是由什么控制的。我们的实现里加载态是status pending时显示的。切换筛选条件时旧 Lease 被取消status 变成cancelled但新 Lease 创建后 status 是pending所以加载态应该继续显示才对。问题出在旧 Lease 取消时它的onStatusChange回调没有被触发因为我们的cancel方法里只改了 status没有调用notify。而 UI 层监听的还是旧 Lease 的回调所以加载态一直停在旧 Lease 的 pending 状态上。修复cancel方法里必须调用notify让监听方知道状态变了。同时UI 层在依赖变更时应该切换到新 Lease 的回调上而不是继续监听旧 Lease。cancel() { if (this.status ! pending) return; this.status cancelled; this.notify(); // 这一行不能少 }经验取消操作必须通知监听方否则监听方会一直等一个永远不会来的结果。7.2 坑二缓存命中后仍然发请求现象同一个资源短时间内多次请求缓存明明有数据但还是发了新请求。排查过程查缓存命中的判断逻辑发现判断条件是entry entry.data。问题在于如果数据本身是0、false、空字符串这种 falsy 值entry.data就是 falsy判断会失败导致缓存被当成未命中。我们的场景里有一个资源的返回值是数字0正好踩中这个坑。修复判断缓存命中不能用 truthy 判断要用! undefined。if (entry entry.data ! undefined !this.isExpired(entry)) { // 缓存命中 }经验缓存判断一定要用严格的存在性检查不能用 truthy/falsy 判断否则 falsy 值会出问题。7.3 坑三并发合并时结果分发遗漏现象两个组件同时请求同一个资源第一个组件拿到了数据第二个组件一直没拿到。排查过程查并发合并的逻辑发现entry.leases数组在分发结果后被清空了但清空操作是在forEach之后。问题在于forEach过程中如果有新的 Lease 被 push 进来因为分发是同步的但新 Lease 可能在另一个微任务里被创建这些新 Lease 会被漏掉。更隐蔽的是如果forEach里的回调触发了状态更新状态更新又触发了新的请求新请求的 Lease 会被 push 到正在遍历的数组里导致遍历行为不确定。修复分发前先把entry.leases复制一份遍历副本分发完再清空原数组。同时分发过程中如果有新 Lease 进来应该让它们走缓存命中逻辑因为此时entry.data已经写入了。resolve(key, data, seq) { const entry this.cache.get(key); if (!entry) return; entry.data data; entry.timestamp Date.now(); entry.pending false; const leases entry.leases.slice(); // 复制一份 entry.leases []; leases.forEach(lease { if (lease.seq ! seq || lease.status ! pending) return; lease.status fulfilled; lease.data data; lease.notify(); }); }经验遍历一个可能被修改的数组时先复制一份再遍历避免遍历过程中的修改导致行为不确定。7.4 坑四迟到结果覆盖新数据现象快速切换筛选条件旧数据闪一下才变成新数据。排查过程这个就是典型的迟到结果问题。旧请求的结果回来得晚但它的 Lease 已经被取消了按理说应该被丢弃。查代码发现丢弃逻辑是有的但判断条件是lease.status ! pending。问题在于旧 Lease 被取消后我们又复用了同一个 Lease 对象为了减少对象创建复用时把 status 重置为pending了导致旧请求的结果回来时status 又是 pending判断失效。修复引入请求序号机制Lease 上记录seq数据回来时比对seq不一致就丢弃。序号是单调递增的不会重复所以能准确识别迟到结果。if (lease.seq ! seq) return; // 序号不匹配丢弃经验如果 Lease 对象会被复用光靠 status 判断不够必须加序号机制。7.5 坑五缓存失效后旧请求写入脏数据现象手动失效缓存后旧请求的结果回来把已经失效的缓存又写回去了。排查过程手动失效时我们调用了cache.delete(key)把缓存条目删了。但旧请求还在飞它回来时执行resolveresolve里先cache.get(key)发现是 undefined就直接 return 了。看起来没问题但实际上如果失效之后马上有新请求新请求创建了新的缓存条目旧请求回来时cache.get(key)拿到的是新条目就会把旧数据写进新条目里。修复resolve里不能只检查条目是否存在还要检查条目是否还是当初那个条目。给每个缓存条目加一个唯一 ID请求发出时记录条目 ID回来时比对。acquire(key) { // ... entry { id: this.entrySeq, data: undefined, timestamp: 0, pending: true, leases: [lease] }; this.cache.set(key, entry); const entryId entry.id; this.fetcher(key).then( data this.resolve(key, data, lease.seq, entryId), err this.reject(key, err, lease.seq, entryId) ); } resolve(key, data, seq, entryId) { const entry this.cache.get(key); if (!entry || entry.id ! entryId) return; // 条目已更换丢弃 // ... }经验缓存失效和请求在飞同时发生时必须用条目 ID 做二次校验否则旧请求会污染新缓存。8. 几个容易被忽略的边界情况8.1 同步 fetcher 的处理如果fetcher是同步的比如从 localStorage 读数据acquire方法里this.fetcher(key).then就会报错因为同步返回值没有then方法。处理方式是判断返回值是否是 Promise不是的话直接走 resolve 逻辑。const result this.fetcher(key); if (result typeof result.then function) { result.then(data this.resolve(key, data, lease.seq, entryId), err this.reject(key, err, lease.seq, entryId)); } else { this.resolve(key, result, lease.seq, entryId); }8.2 错误重试与 Lease 的关系错误重试应该作用在 Provider 层面而不是 Lease 层面。因为重试是“这个资源再取一次”和具体哪个调用方无关。重试时Provider 重新调用 fetcher把结果分发给当前挂着的所有 Lease。如果某个 Lease 在重试期间被取消了它就不应该收到重试结果。8.3 多个 Provider 的协作有时候一个资源依赖另一个资源比如“用户详情”依赖“用户 ID”。这种场景下可以让“用户详情”Provider 在 fetcher 里先 acquire “用户 ID”的 Lease拿到 ID 后再请求详情。但要注意这个内部 Lease 也要在外部 Lease 取消时一并取消否则会留下悬空请求。8.4 内存泄漏的排查Lease 和 Provider 都是长期存在的对象如果管理不当很容易内存泄漏。排查内存泄漏时重点看两个地方一是entry.leases数组是否在请求结束后被清空二是 Lease 的listeners数组是否在 notify 后被清空。这两个地方如果漏了Lease 和回调函数就会一直被引用无法回收。我在实际项目中遇到过一次内存泄漏原因是entry.leases在请求失败时没有被清空reject方法里漏了entry.leases []导致失败的 Lease 一直挂在缓存条目上。后来在reject里补上清空逻辑就好了。这个坑很隐蔽因为失败请求不像成功请求那么频繁泄漏速度慢不容易发现。9. 写在最后一点个人体会Provider 和 Lease 这套抽象我用了大概两年从最初的手写实现到后来团队内部封装成库中间改过好几版。最大的体会是取消、缓存、迟到结果这三个问题必须一起解决不能分开处理。只做取消不做缓存取消就没那么必要只做缓存不做取消缓存就会存一堆没人要的数据只做迟到结果识别不做取消识别出来的结果也没地方丢弃。另一个体会是序号机制比状态机制更可靠。状态机制依赖对象的状态变化一旦对象被复用或者状态被意外重置判断就会失效。序号机制依赖单调递增的数字只要数字不重复判断就永远准确。所以我现在写这类逻辑序号是标配状态只是辅助。最后分享一个小技巧如果你不确定自己的实现有没有问题可以写一个简单的压力测试模拟快速切换、并发请求、取消后重试这些场景观察状态变化是否符合预期。我一般会用一个计数器记录请求发起次数和结果分发次数正常情况下这两个数字应该满足一定的关系比如分发次数不超过发起次数取消的请求不分发结果。这个测试跑一遍大部分逻辑漏洞都能暴露出来。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

SpringBoot+Vue3构建美食推荐商城系统实战 2026/9/19 17:16:09

SpringBoot+Vue3构建美食推荐商城系统实战

1. 项目概述这个Java Web美食推荐商城系统采用了当前主流的技术栈组合:SpringBoot2Vue3MyBatis-PlusMySQL8.0。作为一个全栈项目,它完美展现了前后端分离架构在现代电商系统中的典型应用。我去年在开发类似项目时,这套技术组合的稳定性和开发…

阅读更多 →
FPGA硬件实战:LED极性适配、点阵扫描与蜂鸣器分频全链路解析 2026/9/19 17:16:09

FPGA硬件实战:LED极性适配、点阵扫描与蜂鸣器分频全链路解析

简介:本资源为北京航空航天大学宇航学院电气技术实践课程的FPGA实验报告,面向电子工程、自动化及计算机相关专业本科生,聚焦数字电路设计与硬件实现能力培养。报告完整覆盖四位二进制加法计数器、一位半加器及1616 LED点阵“高山仰止”四字循…

阅读更多 →
AI 生成的代码调试不了?TaoToken 这样配 Codex 的 config.toml 2026/9/19 17:16:09

AI 生成的代码调试不了?TaoToken 这样配 Codex 的 config.toml

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Spring AI实战生产落地:从架构设计到工具调用完整拆解 2026/9/19 17:16:09

Spring AI实战生产落地:从架构设计到工具调用完整拆解

最近不少朋友在问同一件事:Spring官方那个AI框架到底能不能用在生产项目里?我的答案是能,而且已经在这么干了。今天这篇就基于我一整个项目周期的实际体验,把Spring AI从架构设计、结构化输出、工具调用到记忆管理的完整链路拆开讲…

阅读更多 →
汽车产业链数字化融通转型:从订单流到质量流的数据工程实践 2026/9/19 17:16:09

汽车产业链数字化融通转型:从订单流到质量流的数据工程实践

简介:一份聚焦成都经开区以汽车产业为先导的制造业数字化融通转型案例文档,面向产业政策制定者、园区管理者及中小企业负责人,针对企业“不想转、不敢转、不会转”的共性难题。案例系统梳理了“以主促链、多维引导、分级支撑、协同发展”的推…

阅读更多 →
Keil MDK工程创建全流程解析:从芯片选型到调试烧录的避坑实战 2026/9/19 17:13:08

Keil MDK工程创建全流程解析:从芯片选型到调试烧录的避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞