buffer_pool.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. package pool
  2. import (
  3. "sync"
  4. )
  5. // pageEntry 缓冲池中的页条目
  6. type pageEntry struct {
  7. data []byte // 页数据副本
  8. dirty bool // 是否被修改(需要写回磁盘)
  9. pageLen int // 页字节长度(用于计算文件偏移)
  10. ver uint64 // 内容版本号: 每次内容被写入都换一个 (见 BufferPool.stampLocked)
  11. }
  12. // DirtyPage 脏页快照
  13. //
  14. // ver 是快照当时该页的版本号, 只在 pool 内部使用。提交成功后 MarkClean 靠它判断
  15. // 「这页在提交窗口里有没有被再改过」: 改过就必须继续留脏, 不能被顺手标成干净页。
  16. type DirtyPage struct {
  17. PageID uint32
  18. Data []byte
  19. PageLen int
  20. ver uint64
  21. }
  22. // BufferPool 页面缓冲池
  23. // 拦截 ReadPage/WritePage,将页缓存在内存中,减少磁盘 I/O
  24. // 写回策略:
  25. // - Commit() → 通过 CollectDirty() 收集脏页,走 WAL 流程写回
  26. // - 脏页超过阈值时自动触发 onThreshold 回调 (由 DB 层执行 Commit)
  27. type BufferPool struct {
  28. pages map[uint32]*pageEntry // pageID → 页条目
  29. mu sync.Mutex
  30. capacity int // 最大缓存页数
  31. dirtyThreshold int // 脏页阈值 (0=不限制)
  32. onThreshold func() // 脏页超阈值回调
  33. dirtyCount int // 当前脏页数 (实时追踪)
  34. verSeq uint64 // 版本号发放器 (见 stampLocked)
  35. diskRead func(pageID uint32, buf []byte) error // 底层磁盘读
  36. diskWrite func(pageID uint32, buf []byte) error // 底层磁盘写
  37. // 锁表机制
  38. writeLocked bool // 写锁标志
  39. writeChan chan struct{} // 用于通知解锁的 channel
  40. // BufferPool 容量控制
  41. capacityWait chan struct{} // 容量满时等待的 channel
  42. // 事务支持
  43. onPageWrite func(pageID uint32) // 页写入时的回调(用于事务缓存原始数据)
  44. }
  45. // NewBufferPool 创建页面缓冲池
  46. // - capacity: 最大缓存页数
  47. // - diskRead/diskWrite: 底层磁盘读写函数
  48. func NewBufferPool(capacity int,
  49. diskRead func(uint32, []byte) error,
  50. diskWrite func(uint32, []byte) error,
  51. ) *BufferPool {
  52. bp := &BufferPool{
  53. pages: make(map[uint32]*pageEntry, capacity),
  54. capacity: capacity,
  55. diskRead: diskRead,
  56. diskWrite: diskWrite,
  57. }
  58. return bp
  59. }
  60. // SetDirtyThreshold 设置脏页阈值和回调
  61. // 当脏页数超过 threshold 时调用 onThreshold (由 DB 层触发 Commit)
  62. func (bp *BufferPool) SetDirtyThreshold(threshold int, onThreshold func()) {
  63. bp.mu.Lock()
  64. defer bp.mu.Unlock()
  65. bp.dirtyThreshold = threshold
  66. bp.onThreshold = onThreshold
  67. }
  68. // LockWrites 锁定写入(Commit/Flush 期间调用)
  69. func (bp *BufferPool) LockWrites() {
  70. bp.mu.Lock()
  71. bp.writeLocked = true
  72. bp.writeChan = make(chan struct{}) // 创建新的 channel
  73. bp.mu.Unlock()
  74. }
  75. // UnlockWrites 解锁写入(Commit/Flush 完成后调用)
  76. func (bp *BufferPool) UnlockWrites() {
  77. bp.mu.Lock()
  78. bp.writeLocked = false
  79. if bp.writeChan != nil {
  80. close(bp.writeChan) // 通知所有等待的 goroutine
  81. bp.writeChan = nil
  82. }
  83. bp.mu.Unlock()
  84. }
  85. // waitForWriteLock 等待写锁释放(在 WritePage 中调用)
  86. func (bp *BufferPool) waitForWriteLock() {
  87. bp.mu.Lock()
  88. if !bp.writeLocked {
  89. bp.mu.Unlock()
  90. return // 未锁定,直接返回
  91. }
  92. // 保存 channel 引用
  93. ch := bp.writeChan
  94. bp.mu.Unlock()
  95. // 阻塞等待解锁(channel 关闭时会返回)
  96. <-ch
  97. }
  98. // CollectDirty 取出所有脏页快照 (由 DB.Commit() 调用,配合 WAL 流程统一写回)。
  99. //
  100. // 快照之后【不把页移出缓存】。移出会造出一个「既不在缓存、也还没落盘」的窗口: 从
  101. // 收集完到 wal.Commit() 逐页回写完成之间要经过两次 fsync, 这期间任何缓存未命中的读
  102. // 都只能回源磁盘, 于是拿到的是旧内容 —— 或者, 对一页本次会话刚分配、盘上从来没写过
  103. // 的页, 拿到全零 (文件还没长到那个偏移时直接读出错)。btree 把全零页当成
  104. // EntryLen==0 的节点, 随后在 btPID 里切片越界 panic; 读出错则表现为
  105. // "insert: load failed"。页留在缓存里, 并发读就一定命中最新内容, 不存在两头都不在的时刻。
  106. //
  107. // 也不再清空 dirtyCount: 快照里的页此刻【尚未落盘】, 计数必须继续按"未持久化"算。
  108. // 页的脏标记由提交方在 wal.Commit() 成功之后用 MarkClean 收掉 —— 只有快照后没被再改过
  109. // (ver 未变) 的页才会转成干净页, 改过的继续留脏, 等下一次提交。
  110. // 干净页照旧保留在池中继续命中 (旧实现无论脏净一律 delete, 等于每次提交都清空缓存,
  111. // 提交后的读取全部回落到磁盘); 容量仍由 evictCleanForRoomLocked 约束。
  112. func (bp *BufferPool) CollectDirty() []DirtyPage {
  113. // 锁定写入,防止新的写操作
  114. bp.LockWrites()
  115. defer bp.UnlockWrites()
  116. bp.mu.Lock()
  117. var dirty []DirtyPage
  118. for pid, entry := range bp.pages {
  119. if !entry.dirty {
  120. continue // 干净页: 与磁盘一致, 留在池中继续服务后续读
  121. }
  122. data := make([]byte, len(entry.data))
  123. copy(data, entry.data)
  124. dirty = append(dirty, DirtyPage{PageID: pid, Data: data, PageLen: entry.pageLen, ver: entry.ver})
  125. }
  126. // 这里没有释放任何页, 所以不唤醒等待容量的 goroutine (快照只是复制)。
  127. bp.mu.Unlock()
  128. return dirty
  129. }
  130. // MarkClean 把「确实已经落盘」的快照页转成干净页 (提交成功后由提交方调用)。
  131. //
  132. // 判据是版本号而不是"提交成功了": 提交窗口里写入线程可能已经把这一页又改了一遍
  133. // (缓存里是更新的内容, 而本次 WAL 记的是快照那一份)。这种页必须继续留脏 —— 否则它
  134. // 的新内容既不会被写盘, 脏标记还向引擎报告"已经持久化了", 该页一旦被淘汰就永久丢失。
  135. //
  136. // 快照里的页若在窗口里被别的路径丢弃 (回收 / 直写盘), 这里跳过即可: 那些路径自己
  137. // 负责缓存一致性。
  138. func (bp *BufferPool) MarkClean(pages []DirtyPage) {
  139. if len(pages) == 0 {
  140. return
  141. }
  142. bp.mu.Lock()
  143. defer bp.mu.Unlock()
  144. for _, p := range pages {
  145. entry, ok := bp.pages[p.PageID]
  146. if !ok || !entry.dirty || entry.ver != p.ver {
  147. continue
  148. }
  149. entry.dirty = false
  150. bp.dirtyCount--
  151. }
  152. if bp.dirtyCount < 0 {
  153. bp.dirtyCount = 0
  154. }
  155. }
  156. // stampLocked 给页条目盖一个新的版本号。调用方必须持有 bp.mu。
  157. //
  158. // 版本号只用于回答一个问题: "从我拍快照到现在, 这一页被改过吗"。
  159. func (bp *BufferPool) stampLocked(e *pageEntry) {
  160. bp.verSeq++
  161. e.ver = bp.verSeq
  162. }
  163. // RestoreDirty 提交失败时兜底: 保证这批页在缓存里仍然是脏的。
  164. //
  165. // CollectDirty 现在不再把页移出缓存, 所以正常路径上这里无事可做 —— 留着是因为错误
  166. // 处理期间别的动作 (页回收 / 直写盘) 也可能碰过这些页。只补【缺失】的条目: 页已经在
  167. // 缓存里就原样不动, 免得用一份旧快照盖掉提交窗口里写入线程刚改出来的新内容。
  168. //
  169. // 这里刻意不触发阈值回调, 避免在错误处理路径上递归触发 Commit。
  170. func (bp *BufferPool) RestoreDirty(pages []DirtyPage) {
  171. if len(pages) == 0 {
  172. return
  173. }
  174. bp.mu.Lock()
  175. defer bp.mu.Unlock()
  176. for _, p := range pages {
  177. if _, ok := bp.pages[p.PageID]; ok {
  178. continue // 已在缓存 (很可能还带着更新的内容), 不要覆盖
  179. }
  180. e := &pageEntry{data: p.Data, dirty: true, pageLen: p.PageLen}
  181. bp.stampLocked(e)
  182. bp.pages[p.PageID] = e
  183. bp.dirtyCount++
  184. }
  185. }
  186. // DiscardPages 丢弃指定页的缓存 (不落盘), 并释放其占用容量。
  187. //
  188. // 事务 Rollback 使用: 事务期间 autoCommit 被 currentTxn 拦住, 任何脏页都不会刷到磁盘,
  189. // 所以「撤销」= 直接把本事务碰过的页从缓存里丢掉, 后续读取自然回到磁盘旧值。
  190. // 只丢弃传入的页 —— 事务开始前遗留的脏页不属于本事务, 必须原样保留。
  191. func (bp *BufferPool) DiscardPages(pageIDs map[uint32]struct{}) {
  192. if len(pageIDs) == 0 {
  193. return
  194. }
  195. bp.mu.Lock()
  196. defer bp.mu.Unlock()
  197. removedDirty := 0
  198. for pid := range pageIDs {
  199. entry, ok := bp.pages[pid]
  200. if !ok {
  201. continue
  202. }
  203. if entry.dirty {
  204. removedDirty++
  205. }
  206. delete(bp.pages, pid)
  207. }
  208. bp.dirtyCount -= removedDirty
  209. if bp.dirtyCount < 0 {
  210. bp.dirtyCount = 0
  211. }
  212. // 释放容量后唤醒等待空间的 goroutine
  213. if bp.capacityWait != nil {
  214. close(bp.capacityWait)
  215. bp.capacityWait = nil
  216. }
  217. }
  218. // DiscardPage 丢弃单页缓存 (不落盘), 并释放其占用容量。
  219. //
  220. // 用于「绕过缓冲池直写磁盘」(WritePageDirect) 之后, 让缓存中该页的旧副本立即失效,
  221. // 避免随后 ReadPage 命中与磁盘不一致的陈旧内容。
  222. func (bp *BufferPool) DiscardPage(pageID uint32) {
  223. bp.mu.Lock()
  224. entry, ok := bp.pages[pageID]
  225. if !ok {
  226. bp.mu.Unlock()
  227. return
  228. }
  229. if entry.dirty {
  230. bp.dirtyCount--
  231. if bp.dirtyCount < 0 {
  232. bp.dirtyCount = 0
  233. }
  234. }
  235. delete(bp.pages, pageID)
  236. if bp.capacityWait != nil {
  237. close(bp.capacityWait)
  238. bp.capacityWait = nil
  239. }
  240. bp.mu.Unlock()
  241. }
  242. // Flush 将所有脏页写入磁盘 (Close / 维护操作时使用), 并把落盘成功的页转成干净页。
  243. //
  244. // 收尾必须走 MarkClean: CollectDirty 不再移出缓存, 少了这一步, 刚写完的页会一直带着
  245. // 脏标记, 下一次 Commit 会把同一批内容再写一遍 (并且写进 WAL)。
  246. // 写盘错误在这里被忽略 (与调用方既有约定一致), 因此按"已落盘"收尾; ver 比对仍会放过
  247. // 提交窗口里被改过的页, 它们继续留脏, 交给下一次 Flush / Commit。
  248. func (bp *BufferPool) Flush() {
  249. dirty := bp.CollectDirty()
  250. for _, item := range dirty {
  251. _ = bp.diskWrite(item.PageID, item.Data)
  252. }
  253. bp.MarkClean(dirty)
  254. }
  255. // Close 关闭缓冲池 (刷盘后停止)
  256. func (bp *BufferPool) Close() {
  257. bp.Flush()
  258. }
  259. // UpdatePage 更新缓存页 (WAL 提交后调用, 保持缓存与磁盘一致)
  260. func (bp *BufferPool) UpdatePage(pageID uint32, data []byte) {
  261. bp.mu.Lock()
  262. defer bp.mu.Unlock()
  263. cached := make([]byte, len(data))
  264. copy(cached, data)
  265. e := &pageEntry{data: cached, dirty: false, pageLen: len(data)}
  266. bp.stampLocked(e) // 内容换过了, 版本号必须往前走, 否则旧快照会误认为这页没变
  267. bp.pages[pageID] = e
  268. // 回填的页是干净页, 不受 dirtyCount 约束; 不加这一步, 大量回收页会把缓存
  269. // 无限撑大 (回收 N 页 → 缓存增加 N 页), 并让 WritePage 的容量判断形同虚设。
  270. bp.evictCleanForRoomLocked()
  271. }
  272. // evictCleanForRoomLocked 容量已满时淘汰干净页 (dirty=false), 直到降到容量以下。
  273. //
  274. // 干净页内容与磁盘一致, 丢弃后下次读取会重新从磁盘载入, 语义完全不受影响;
  275. // 脏页绝不能被丢弃 (否则会静默丢掉尚未落盘的修改), 因此若缓存里全是脏页,
  276. // 这里不做任何事 —— 允许缓存临时超出容量, 由脏页阈值触发的 Commit 尽快回落。
  277. //
  278. // 调用方必须持有 bp.mu。
  279. func (bp *BufferPool) evictCleanForRoomLocked() {
  280. if bp.capacity <= 0 || len(bp.pages) < bp.capacity {
  281. return
  282. }
  283. for pid, entry := range bp.pages {
  284. if entry.dirty {
  285. continue
  286. }
  287. delete(bp.pages, pid)
  288. if len(bp.pages) < bp.capacity {
  289. return
  290. }
  291. }
  292. }
  293. // ClearAll 清除所有脏标记, 保留页在缓存中。
  294. //
  295. // ⚠️ 这是"无差别"操作, 只适用于确定没有并发写入的维护场景 (关闭 / 建表删表等)。
  296. // 提交路径上【不能】用它: 提交窗口里被写入线程重新改脏的页并不在本笔 WAL 里, 一并
  297. // 标干净就等于宣布"已持久化", 那份新内容以后既不会被写盘, 也会被淘汰逻辑当作可以
  298. // 直接丢弃的干净页。提交成功要收脏标记请用 MarkClean (按版本号逐页判定)。
  299. func (bp *BufferPool) ClearAll() {
  300. bp.mu.Lock()
  301. defer bp.mu.Unlock()
  302. for _, entry := range bp.pages {
  303. entry.dirty = false
  304. }
  305. bp.dirtyCount = 0
  306. }
  307. // ReadPage 读页 — 先查缓存,未命中则读磁盘 (不缓存, 避免 BufferPool 膨胀)
  308. func (bp *BufferPool) ReadPage(pageID uint32, buf []byte) error {
  309. bp.mu.Lock()
  310. if entry, ok := bp.pages[pageID]; ok {
  311. copy(buf, entry.data)
  312. bp.mu.Unlock()
  313. return nil
  314. }
  315. bp.mu.Unlock()
  316. // 缓存未命中 → 直接从磁盘读取, 不写入缓存
  317. return bp.diskRead(pageID, buf)
  318. }
  319. // WritePage 写页 — 写入缓存并标记脏,不立即落盘
  320. func (bp *BufferPool) WritePage(pageID uint32, buf []byte) error {
  321. // 1. 等待写锁释放(Commit/Flush 期间阻塞)
  322. bp.waitForWriteLock()
  323. // 事务支持:通知 Transaction 缓存原始数据
  324. if bp.onPageWrite != nil {
  325. bp.onPageWrite(pageID)
  326. }
  327. cached := make([]byte, len(buf))
  328. copy(cached, buf)
  329. var triggerThreshold bool
  330. bp.mu.Lock()
  331. // 2. 容量控制: 满了先淘汰干净页, 不阻塞等待。
  332. // 旧实现阻塞在 capacityWait 上等 Commit/Discard 释放空间, 但这两者都不可能
  333. // 在"当前这个正在阻塞的 goroutine"内被调用 —— 单写入者模型下必然死锁
  334. // (fatal error: all goroutines are asleep)。批量删除回收旧页时, realFreePage
  335. // 会把每个回收页经 SyncCache→UpdatePage 以干净页回填缓存(不计 dirtyCount),
  336. // 缓存被干净页迅速撑满, 紧接其后的写入即触发该死锁。
  337. bp.evictCleanForRoomLocked()
  338. existing, exists := bp.pages[pageID]
  339. if exists && existing.dirty {
  340. // 已是脏页, 直接覆盖, 不增加脏页计数
  341. existing.data = cached
  342. bp.stampLocked(existing) // 内容变了: 让提交窗口里的旧快照认不出这一页
  343. } else {
  344. // 新页或干净页, 增加脏页计数
  345. e := &pageEntry{data: cached, dirty: true, pageLen: len(buf)}
  346. bp.stampLocked(e)
  347. bp.pages[pageID] = e
  348. bp.dirtyCount++
  349. }
  350. // 3. 检查是否超过阈值 —— 只按「脏页数」判定。
  351. //
  352. // 不能再用 len(bp.pages): CollectDirty 现在会保留干净页, 缓存里长期驻留一批
  353. // 与磁盘一致的干净页, 总页数会持续偏高。若仍以总页数触发, 阈值会被这些干净页
  354. // 反复顶爆, 提交频率凭空放大数倍。脏页数才是真正"尚未落盘"的数据量, 也正是
  355. // 崩溃时可能丢失的上限; 干净页的容量由 evictCleanForRoomLocked 按 capacity 约束。
  356. if bp.dirtyThreshold > 0 && bp.onThreshold != nil && bp.dirtyCount >= bp.dirtyThreshold {
  357. triggerThreshold = true
  358. }
  359. bp.mu.Unlock()
  360. // 4. 在锁外触发回调, 避免死锁
  361. if triggerThreshold {
  362. bp.onThreshold()
  363. }
  364. return nil
  365. }
  366. // Len 当前缓存页数 (调试用)
  367. func (bp *BufferPool) Len() int {
  368. bp.mu.Lock()
  369. defer bp.mu.Unlock()
  370. return len(bp.pages)
  371. }
  372. // DirtyCount 脏页数 (调试用)
  373. func (bp *BufferPool) DirtyCount() int {
  374. bp.mu.Lock()
  375. defer bp.mu.Unlock()
  376. return bp.dirtyCount
  377. }
  378. // SetCapacity 设置 BufferPool 容量(页数)
  379. func (bp *BufferPool) SetCapacity(capacity int) {
  380. bp.mu.Lock()
  381. bp.capacity = capacity
  382. bp.mu.Unlock()
  383. }
  384. // SetOnPageWriteCallback 设置页写入回调(用于事务缓存原始数据)
  385. func (bp *BufferPool) SetOnPageWriteCallback(callback func(pageID uint32)) {
  386. bp.mu.Lock()
  387. bp.onPageWrite = callback
  388. bp.mu.Unlock()
  389. }