C产品如何满足特定用户需求?
- 内容介绍
- 文章标签
- 相关推荐
本文共计905个文字,预计阅读时间需要4分钟。
直接说结论:
为什么不能用 MemoryCache 实现 LRU
MemoryCache 是线程安全、带过期和内存压力回收的通用缓存,但它内部不记录单个 key 的访问时序,也不允许你 hook 淘汰前的判断逻辑。调用 Get 不会自动“提升热度”,Set 也不会把旧项移到头部——它只按时间或内存阈值批量清理,无法保证“最久未用”那个被精准踢出。
- 你无法在容量满时强制淘汰链表尾部节点,只能等 GC 或后台线程触发回收
- 没有 API 能获取“上次访问时间戳”或“访问频次”,做不了热力分析
- 高频小对象场景下,
MemoryCache的后台扫描和弱引用管理反而增加 GC 压力
LinkedList<T> 的 remove 操作必须基于 node,不是 value
这是最容易踩的坑:如果写 _list.Remove((key, value)),底层会遍历整个链表找匹配项,时间复杂度退化成 O(N),彻底废掉 LRU 的设计前提。
- 必须用
_map[key]拿到LinkedListNode<(K, V)>,再传给_list.Remove(node) - 字典的 value 类型必须是
LinkedListNode<(K, V)>,不是(K, V)或自定义类实例——否则无法 O(1) 定位节点 - 不要在节点里存 key 或 value 的副本,那会导致字典和链表数据不一致;节点只负责串联,数据由链表本身承载
多线程下必须锁住「查字典 + 移动节点」这一整段逻辑
ConcurrentDictionary 看起来合适,但它无法原子地完成“读取 node → 从链表移除 → 插入头部”三步。并发 Get 和 Put 可能导致节点被重复移动、NullReferenceException 或链表断裂。
- 用
lock (_syncRoot)最稳妥,粒度控制在方法入口即可 - 别用
ReaderWriterLockSlim做读写分离——LRU 的Get本质是写操作(要移动节点),读写锁收益极低 - 避免在 lock 块里做任何可能阻塞的事,比如 IO、外部 API 调用
容量超限时删尾节点,但要注意 Last 可能为 null
初始化空链表时 _list.Last 是 null,直接调用 _list.RemoveLast() 没问题,但若手动取 _list.Last!.Value 就会崩。更危险的是,在 Put 中判断 _map.Count > _capacity 时,实际链表长度可能已超——因为 Get 不改变计数,但会移动节点。
- 删尾前先判
if (_list.Count > _capacity && _list.Last != null) - 删除后立即
_map.Remove(_list.Last!.Value.Item1),别依赖node.Value之外的任何中间变量 - 测试时务必覆盖“反复
Get同一个 key 导致链表长度不变但字典计数超标”的边界 case
真正难的不是写对逻辑,而是让所有操作都落在 O(1) 路径上——任何一个地方用了 Find、漏了锁、误用了 value 查链表,整个实现就退化成玩具级。线程安全和节点定位,这两处不盯死,性能优势归零。
本文共计905个文字,预计阅读时间需要4分钟。
直接说结论:
为什么不能用 MemoryCache 实现 LRU
MemoryCache 是线程安全、带过期和内存压力回收的通用缓存,但它内部不记录单个 key 的访问时序,也不允许你 hook 淘汰前的判断逻辑。调用 Get 不会自动“提升热度”,Set 也不会把旧项移到头部——它只按时间或内存阈值批量清理,无法保证“最久未用”那个被精准踢出。
- 你无法在容量满时强制淘汰链表尾部节点,只能等 GC 或后台线程触发回收
- 没有 API 能获取“上次访问时间戳”或“访问频次”,做不了热力分析
- 高频小对象场景下,
MemoryCache的后台扫描和弱引用管理反而增加 GC 压力
LinkedList<T> 的 remove 操作必须基于 node,不是 value
这是最容易踩的坑:如果写 _list.Remove((key, value)),底层会遍历整个链表找匹配项,时间复杂度退化成 O(N),彻底废掉 LRU 的设计前提。
- 必须用
_map[key]拿到LinkedListNode<(K, V)>,再传给_list.Remove(node) - 字典的 value 类型必须是
LinkedListNode<(K, V)>,不是(K, V)或自定义类实例——否则无法 O(1) 定位节点 - 不要在节点里存 key 或 value 的副本,那会导致字典和链表数据不一致;节点只负责串联,数据由链表本身承载
多线程下必须锁住「查字典 + 移动节点」这一整段逻辑
ConcurrentDictionary 看起来合适,但它无法原子地完成“读取 node → 从链表移除 → 插入头部”三步。并发 Get 和 Put 可能导致节点被重复移动、NullReferenceException 或链表断裂。
- 用
lock (_syncRoot)最稳妥,粒度控制在方法入口即可 - 别用
ReaderWriterLockSlim做读写分离——LRU 的Get本质是写操作(要移动节点),读写锁收益极低 - 避免在 lock 块里做任何可能阻塞的事,比如 IO、外部 API 调用
容量超限时删尾节点,但要注意 Last 可能为 null
初始化空链表时 _list.Last 是 null,直接调用 _list.RemoveLast() 没问题,但若手动取 _list.Last!.Value 就会崩。更危险的是,在 Put 中判断 _map.Count > _capacity 时,实际链表长度可能已超——因为 Get 不改变计数,但会移动节点。
- 删尾前先判
if (_list.Count > _capacity && _list.Last != null) - 删除后立即
_map.Remove(_list.Last!.Value.Item1),别依赖node.Value之外的任何中间变量 - 测试时务必覆盖“反复
Get同一个 key 导致链表长度不变但字典计数超标”的边界 case
真正难的不是写对逻辑,而是让所有操作都落在 O(1) 路径上——任何一个地方用了 Find、漏了锁、误用了 value 查链表,整个实现就退化成玩具级。线程安全和节点定位,这两处不盯死,性能优势归零。

