30年Postgres锁的六次关键进化


最近我闲着没事儿,把Postgres数据库30年来怎么管理内存缓冲区的"锁"设计翻了个底朝天。这事儿估计只有骨灰级Postgres技术宅才会感兴趣。不过既然研究了,咱就唠唠吧!

先说说Postgres缓冲区管理器是干啥的?简单来说,Postgres把硬盘数据分成8KB大小的"数据块"。为了提速,它会在内存里搞个"缓存区"存放最近用过的数据块。但内存有限啊,不可能存所有数据块,所以需要个"管理员"来决定哪些数据块留在内存里。

这个管理员主要靠两个小本本:

  1. 一个"哈希表"(像字典一样):通过数据块编号查找到内存位置
  2. 每个内存块都有个"户口本"(BufferDescriptor):记录这个块的使用情况

用数据时先查哈希表:

  • 找到了就直接用内存里的数据
  • 找不到就要从硬盘读,如果内存满了还得先踢掉一个旧数据块

踢人算法挺有意思,叫"钟表算法":把内存块想象成钟表盘,有个指针转圈找可以踢的数据块。每个块有个"使用次数"计数器,用一次就+1。指针扫到某个块时:

  • 如果计数>0就-1,继续找下一个
  • 如果计数=0就踢它

真正的技术难点在于:怎么让多个程序同时用内存还不打架?怎么让它们和谐共处还能跑得快?

进化史第一章:远古时代(1996年)
最早版本特别简单粗暴——所有操作共用一把大锁(BufMgrLock)!想查数据?先抢锁!想换数据?先抢锁!简单是简单,但性能差到爆,就是个临时方案。

进化史第二章:1998年(Postgres 6.5)
Vadim大神给每个内存块加了小锁(cntx_lock):

  • 读数据要拿共享锁(多人可同时读)
  • 写数据要拿独占锁(一次只能一人写)这样不同程序可以同时操作不同内存块了!大锁只用来管些简单操作。
进化史第三章:2005年(Postgres 8.1)
Tom Lane大神把大锁拆成三部分:
  1. BufMappingLock:管哈希表操作
  2. BufFreelistLock:管踢人算法的指针
  3. 每个内存块的小锁(buf_hdr_lock)这下不同操作可以真正并行处理了!
进化史第四章:2006年(Postgres 8.2)
哈希表大锁还是太忙,Tom Lane又把它分成多个小锁(像把大菜刀换成瑞士军刀),每个哈希桶配独立锁,冲突更少了。

进化史第五章:2015年(Postgres 9.5)
Andres Freund等大神开始用"原子操作"(CPU保证的不可打断操作)替代锁:

  • 先把踢人算法的指针改成原子的
  • 然后去掉BufFreelistLock大锁性能又提升一大截!

进化史第六章:2016年(Postgres 9.6)
Andres继续发力,把内存块"户口本"里的计数器全改成原子操作,彻底不要小锁了!

现在只剩一个无关紧要的小锁还在用,这套设计已经稳定运行11年啦!

总结几个编程真理:

  1. 先搞简单能用的
  2. 只优化真正的瓶颈(别瞎优化)
  3. 用实际数据说话
  4. 小步快跑慢慢改进
另外,算法选得好,并发没烦恼。就像这个"钟表算法",既简单又方便改造成无锁设计。要是用复杂算法可能就不好改了。

最后感叹下:细粒度锁和原子操作真是神器!虽然开始实现麻烦,但做好后维护特别轻松。反倒是大锁设计,改一行代码都担心影响性能,长期看更费劲。这告诉我们:好设计要经得起时间考验!