当前位置:首页 > 问答 > 正文

数据库那些不太讲的底层设计细节,想知道就得深入挖一挖

首先从最基础的“数据如何躺在磁盘上”说起,我们总说数据库把数据存成“表”,有“行”有“列”,但磁盘是线性的大数组,只认字节,第一个要挖的细节是行格式,教科书会说一行数据就是所有列的值连在一起,但实际远非如此,比如MySQL的InnoDB引擎,一行数据除了我们的业务数据,还包含一些“隐藏列”:一个6字节的事务ID和一个7字节的回滚指针,你每做一次数据变更,哪怕只是UPDATE一个字段,InnoDB都会在行记录里记录是哪个事务(事务ID)干的这件事,同时把旧版本的数据通过回滚指针串联成一个“历史版本链”,这就是实现多版本并发控制(MVCC) 的物理基础,你读到的数据,可能并不是当时磁盘上“最新”的数据,而是根据你的隔离级别和事务开始时间,从这个版本链里挑出来的一个“快照”,这个细节被完全隐藏在了SQL接口之下。

数据库那些不太讲的底层设计细节,想知道就得深入挖一挖

接下来是索引,我们都知道索引像书的目录,但它的具体结构是B+树,这里有个反直觉的细节:索引即数据,数据即索引,在InnoDB中,主键索引的叶子节点,存储的并不是指向数据行的“指针”,而是完整的数据行本身,这种索引叫做“聚簇索引”,如果你的表没有定义主键,InnoDB会偷偷找一个唯一的非空索引来替代,如果连这个都没有,它会在后台生成一个隐藏的、6字节的自增_rowid作为主键,这意味着,通过主键查找是最快的,因为一次B+树搜索就能拿到数据,而普通索引(二级索引)的叶子节点,存储的则是主键的值,当你通过二级索引查询时,数据库要先在二级索引的B+树里找到对应的主键值,然后再拿着这个主键值回到主键索引(聚簇索引)的B+树里再查一次,这个过程叫做“回表”,这个“两级查找”的代价是很多SQL性能问题的根源。

数据库那些不太讲的底层设计细节,想知道就得深入挖一挖

然后是事务日志的魔法,为了保证持久性(Durability),数据库说我把数据写入磁盘就安全了,但如果每次事务提交都要把修改的数据页随机写入磁盘,那性能会惨不忍睹,所以数据库玩了个“花招”:它引入了预写日志(WAL),在InnoDB里就是重做日志(redo log),当事务提交时,数据库并不急着把脏数据页写回磁盘,而是先把“在某个数据页的某个偏移量处做了什么样的修改”这个物理逻辑操作顺序追加到redo log文件里,这个操作是顺序写磁盘,速度极快,只要redo log落盘了,事务就算持久化了,即使此时数据库崩溃,重启后也能根据redo log把数据“重放”出来,那脏数据页什么时候写回磁盘呢?由后台线程异步、批量地刷回,这里还有个搭档叫undo log,它主要记录修改前的旧值,用于事务回滚和实现前面提到的MVCC,redo和undo一个管“重做”向前滚,一个管“回滚”向后滚,共同构成了事务ACID特性的基石。

数据库那些不太讲的底层设计细节,想知道就得深入挖一挖

再说说的底层实现,我们常听到行锁、表锁,但锁本身是一个需要管理的内存结构,如果对一张大表的每一行都加锁,光维护锁的信息就可能把内存撑爆,所以InnoDB采用了锁升级的机制,当同一个事务持有的行锁数量超过一定阈值(例如InnoDB是5000),就会尝试将多个细粒度的行锁升级为一个更粗粒度的表锁,以减少内存开销,但这个过程本身有代价,可能引发竞争,更底层的,锁的实现依赖于一个叫锁管理器的组件,它使用哈希表来快速定位某个资源(比如某一行)上的锁信息,哈希的key就是资源的一个唯一标识符。

最后提一下缓冲池的管理,数据库会把磁盘上的数据页缓存到内存中,这个内存区域叫缓冲池,它不可能无限大,所以需要淘汰旧页,这里用的不是简单的LRU算法,因为全表扫描这类操作会瞬间把大量可能只访问一次的数据页塞进缓冲池,挤掉真正热点的数据,MySQL的InnoDB缓冲池将LRU链表分为两部分:新生代老生代,新读入的数据页并不是直接放到LRU链表的头部,而是先放到老生代的头部,只有这个数据页在缓冲池里存活了一段时间后再次被访问,它才有资格被提升到新生代(热点区域),这种优化能有效地抵御一次性全表扫描的冲击。

这些细节,如行格式里的版本链、索引叶节点的真实内容、日志先行策略、锁的哈希管理与升级、缓冲池的冷热分离算法,都是数据库引擎在幕后默默完成的工作,它们不被常规的SQL操作所触及,但却从根本上决定了数据库的行为、性能和可靠性,要真正理解数据库,就必须潜入到这个层面去探究。

(引用来源:这些细节主要基于对MySQL InnoDB存储引擎官方文档的深度解读、以及《MySQL技术内幕:InnoDB存储引擎》等专业书籍的阐述,并结合了对PostgreSQL等数据库系统类似机制的对比理解。)