最近我闲着没事儿,把Postgres数据库30年来怎么管理内存缓冲区的"锁"设计翻了个底朝天。这事儿估计只有骨灰级Postgres技术宅才会感兴趣。不过既然研究了,咱就唠唠吧!
先说说Postgres缓冲区管理器是干啥的?简单来说,Postgres把硬盘数据分成8KB大小的"数据块"。为了提速,它会在内存里搞个"缓存区"存放最近用过的数据块。但内存有限啊,不可能存所有数据块,所以需要个"管理员"来决定哪些数据块留在内存里。
这个管理员主要靠两个小本本:
- 一个"哈希表"(像字典一样):通过数据块编号查找到内存位置
- 每个内存块都有个"户口本"(BufferDescriptor):记录这个块的使用情况
用数据时先查哈希表:
- 找到了就直接用内存里的数据
- 找不到就要从硬盘读,如果内存满了还得先踢掉一个旧数据块
踢人算法挺有意思,叫"钟表算法":把内存块想象成钟表盘,有个指针转圈找可以踢的数据块。每个块有个"使用次数"计数器,用一次就+1。指针扫到某个块时:
- 如果计数>0就-1,继续找下一个
- 如果计数=0就踢它
真正的技术难点在于:怎么让多个程序同时用内存还不打架?怎么让它们和谐共处还能跑得快?
进化史第一章:远古时代(1996年)
最早版本特别简单粗暴——所有操作共用一把大锁(BufMgrLock)!想查数据?先抢锁!想换数据?先抢锁!简单是简单,但性能差到爆,就是个临时方案。
进化史第二章:1998年(Postgres 6.5)
Vadim大神给每个内存块加了小锁(cntx_lock):
- 读数据要拿共享锁(多人可同时读)
- 写数据要拿独占锁(一次只能一人写)这样不同程序可以同时操作不同内存块了!大锁只用来管些简单操作。
Tom Lane大神把大锁拆成三部分:
- BufMappingLock:管哈希表操作
- BufFreelistLock:管踢人算法的指针
- 每个内存块的小锁(buf_hdr_lock)这下不同操作可以真正并行处理了!
哈希表大锁还是太忙,Tom Lane又把它分成多个小锁(像把大菜刀换成瑞士军刀),每个哈希桶配独立锁,冲突更少了。
进化史第五章:2015年(Postgres 9.5)
Andres Freund等大神开始用"原子操作"(CPU保证的不可打断操作)替代锁:
- 先把踢人算法的指针改成原子的
- 然后去掉BufFreelistLock大锁性能又提升一大截!
进化史第六章:2016年(Postgres 9.6)
Andres继续发力,把内存块"户口本"里的计数器全改成原子操作,彻底不要小锁了!
现在只剩一个无关紧要的小锁还在用,这套设计已经稳定运行11年啦!
总结几个编程真理:
- 先搞简单能用的
- 只优化真正的瓶颈(别瞎优化)
- 用实际数据说话
- 小步快跑慢慢改进
最后感叹下:细粒度锁和原子操作真是神器!虽然开始实现麻烦,但做好后维护特别轻松。反倒是大锁设计,改一行代码都担心影响性能,长期看更费劲。这告诉我们:好设计要经得起时间考验!