链表变量本身不是调优对象,真正需调优的是其在缓存淘汰算法中承担的访问序列表征与节点位置管理角色;必须配合哈希表实现O(1)查找,双向链表仅维护顺序;InnoDB midpoint机制是对LRU链表施加区域语义,LFU需频次→链表映射及min_freq维护;线程安全应细粒度分离哈希表锁与链表操作。
链表变量本身不是调优对象,真正需要调优的是它在缓存淘汰算法中承担的角色——访问序列表征与节点位置管理。关键不在“链表”这个结构,而在它如何与哈希表、时间阈值、频次标记协同工作,从而应对真实流量的突发性、扫描污染和冷热混杂。
双向链表必须配合哈希表,否则访问是O(n)
单靠链表遍历定位某个key,每次get或put都要从头扫到尾,复杂度O(n),完全不可用于高并发场景。必须用哈希表(如Go的map、Java的HashMap、C++的unordered_map)建立key→链表节点指针的映射,才能实现O(1)查找。链表只负责维护顺序,哈希表只负责快速定位,二者缺一不可。
- Go中用
*list.Element作value,哈希表存map[interface{}]*list.Element - Java中用
LinkedHashMap,启用accessOrder=true,底层自动维护双向链表+哈希桶 - C++需手写Node结构体,含
key、value、freq(LFU场景)、prev/next指针
InnoDB缓冲池的midpoint机制,本质是链表分区策略
MySQL的InnoDB没用简单LRU链表,而是把LRU链表逻辑切为Young区和Old区,分界点叫midpoint,默认在37%位置。这不是加了新结构,而是对同一链表施加“区域语义”——所有新加载页进Old区尾部,只有被二次访问且间隔超innodb_old_blocks_time(默认1s)才晋升到Young区头部。这直接让全表扫描无法冲垮热数据。
- 调优时可缩小Old区:如设
innodb_old_blocks_pct=25,适合读多写少、热数据集明确的OLTP业务 - 若业务含大量临时报表查询,可适当延长
innodb_old_blocks_time至2000ms,进一步过滤瞬时访问 - 注意:midpoint不是固定指针,而是随缓冲池使用动态浮动的逻辑位置
LFU中的频率链表,要求每个频次级都有独立双向链表
LFU不能只用一个链表+计数器,否则相同频次的节点无法按LRU顺序淘汰。正确做法是构建“频次→链表”的哈希映射:freq_to_list[2]指向所有访问2次的节点组成的双向链表,新访问时插入头部,淘汰时从尾部取。
- 必须维护
min_freq变量,但只在evict()前检查:while (freq_to_list[min_freq].empty()) min_freq++ - 新增key时
min_freq必须设为1;覆盖旧key时不能动min_freq,因频次未变 - Node结构里必须显式存
freq字段,仅靠链表位置无法反推频次
线程安全不是加锁就完事,要细粒度控制链表操作边界
LRU缓存在Web服务中必然并发读写。粗暴地给整个Add/Get加全局互斥锁会严重拖慢吞吐。更优解是分离关注点:
- 哈希表读写加锁(如Go用
sync.RWMutex,读多时用读锁) - 链表节点移动(如move-to-head)本身是无状态的指针操作,只要确保哈希表映射不乱,可不加锁
- 淘汰动作(pop-tail)需独占锁,但应尽量缩短临界区,比如只锁淘汰+回调执行,不包含后续插入逻辑


















