transaction.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. package core
  2. import (
  3. "fmt"
  4. )
  5. // Transaction 事务管理器
  6. //
  7. // 核心思想:
  8. // - 事务内所有修改只落在 BufferPool —— autoCommit 被 currentTxn 拦住, 中途不会刷盘,
  9. // 所以磁盘始终是事务开始前的状态, 不需要任何「原始数据快照」
  10. // - Commit 时把脏页 + Header 整体写入 WAL, 保证原子
  11. // - Rollback 只需丢弃本事务碰过的页缓存(并从磁盘重载 TableInfo), 后续读取自然回到旧值
  12. type Transaction struct {
  13. db *DB
  14. touched map[uint32]struct{} // 本事务修改过的页 ID(Rollback 时丢弃其缓存)
  15. // pendingFree 本事务登记待回收的页 (堆页摘链 / 批量删除后作废的旧 DataPage)。
  16. // 不在事务中途直写盘: 提交成功后才真正还链, 回滚时整体丢弃
  17. // (回滚后链指针回到事务前, 这些页仍被引用, 绝不能还链)。
  18. pendingFree []uint32
  19. // pendingFreeChain 本事务登记待回收的整链 (如哈希索引整体重建后的旧桶链)。
  20. // 与 pendingFree 同一时序约定, 但一条链只登记一次, 提交后一次 FreeChain 还链。
  21. pendingFreeChain []pendingChain
  22. // allocated 本事务新分配的页 ID (从空闲链摘出, 或空闲链耗尽时由 NextFreePage 递增)。
  23. // 分配在 newPageWithReader 里就地推进了 header 的游离指针并直写盘, 事务中途无法回退;
  24. // 因此 Rollback 必须把这些页还给空闲链 —— 否则它们既不被任何表/索引引用、也不在
  25. // 空闲链上, 就成了永久泄漏的孤儿页 (文件只涨不缩)。
  26. // Commit 成功时这些页已被数据引用, 直接丢弃本表即可。
  27. allocated []uint32
  28. committed bool
  29. rolledBack bool
  30. }
  31. // 事务内被直接 FreePage(立即还链) 的页, 从 allocated 中撤销登记, 避免 Rollback 二次归还。
  32. func (txn *Transaction) undoAllocated(pageID uint32) {
  33. for i, id := range txn.allocated {
  34. if id == pageID {
  35. txn.allocated = append(txn.allocated[:i], txn.allocated[i+1:]...)
  36. return
  37. }
  38. }
  39. }
  40. // BeginTransaction 开启新事务。
  41. //
  42. // ⭐ 起始边界收敛: 先把自动提交尚未落盘的脏页提交掉, 再挂牌事务。否则事务回滚
  43. // 按 touched 丢弃页缓存时, 会连同这些页里【事务开始前】的未提交修改一起丢掉,
  44. // 而未被 touched 的元数据页 (IndexMeta 等) 仍指向被丢弃的树页 —— "meta 指树、
  45. // 树已没了" 的撕裂状态, 之后一切按索引的读写全部 load failed / 走空。
  46. // (驱动变异测试实锤: 自动提交挂起的插入 → BEGIN → 插入 → ROLLBACK → 再插入必
  47. // 炸; BEGIN 即"此前工作已提交"的边界, 与 SQL 的 BEGIN 语义一致。)
  48. func (db *DB) BeginTransaction() *Transaction {
  49. if db.errIfReadOnly() != nil {
  50. panic(ErrReadOnly) // 事务无 error 返回, 抢救模式下显式拒绝
  51. }
  52. if db.currentTxn.Load() != nil {
  53. // 直接覆盖会让前一笔事务的 Rollback 丢弃错误的页集合 (markTouched 已绑到
  54. // 新 txn, 旧事务碰过的页再也回滚不掉) —— 属不可恢复的静默损坏。该 API 无
  55. // error 返回, 与上方抢救模式的处理一致: 显式 panic 拒绝嵌套/并发事务。
  56. panic("BeginTransaction: 已有活跃事务, 禁止嵌套/并发事务 (请先 Commit 或 Rollback)")
  57. }
  58. // 边界收敛: 未提交的脏页先落盘 (有脏页才发生 fsync, 干净时是 no-op)
  59. if err := db.Commit(); err != nil {
  60. panic(fmt.Sprintf("BeginTransaction: 收敛未提交修改失败: %v", err))
  61. }
  62. // 操作级串行锁: 从这里持有, 直到 Commit/Rollback 成功释放。事务期间其他 goroutine
  63. // 的读写一律在 accMu 上排队, 保证事务不被并发打断 / 读者不撞中间态。
  64. db.accMu.Lock()
  65. txn := &Transaction{
  66. db: db,
  67. touched: make(map[uint32]struct{}),
  68. committed: false,
  69. rolledBack: false,
  70. }
  71. // 设置当前活跃事务
  72. db.currentTxn.Store(txn)
  73. return txn
  74. }
  75. // HasActiveTransaction 报告当前是否有活跃事务 (只读)。
  76. //
  77. // 供上层 (ORM) 在"自动开事务"之前查询: core.BeginTransaction 遇到已有活跃事务
  78. // 会直接 panic, 而不知情地新开事务还会把用户的事务顶掉、替用户全局提交 —
  79. // 用户随后的 Rollback 因此失去原子性。ORM 层必须先查这里, 活跃则复用、不提交。
  80. func (db *DB) HasActiveTransaction() bool {
  81. return db.currentTxn.Load() != nil
  82. }
  83. // markTouched 登记本事务修改过的页 (由 BufferPool 的 onPageWrite 回调触发)。
  84. // 只记页 ID(4B/页), 不存页数据 —— 回滚靠「丢弃缓存 + 从磁盘重读」实现。
  85. func (txn *Transaction) markTouched(pageID uint32) {
  86. txn.touched[pageID] = struct{}{}
  87. }
  88. // Commit 提交事务
  89. // 只写「整页镜像」条目: 恢复所有被修改页的结构与数据
  90. // (B+树节点, IndexMeta, HeaderPage, DataPage 一次到位)。
  91. //
  92. // 不再额外写行级条目: 整页镜像已完整包含行数据, 再叠加行级记录会在恢复时被
  93. // 重复应用 (INSERT 变成两行), 见 wal_recovery 的 Phase 1 / Phase 2。
  94. func (txn *Transaction) Commit() error {
  95. if txn.committed {
  96. return fmt.Errorf("transaction already committed")
  97. }
  98. if txn.rolledBack {
  99. return fmt.Errorf("transaction already rolled back")
  100. }
  101. // 与 DB.Commit 共用同一把提交锁: 事务提交和自动提交(定时器 / 脏页阈值)也可能撞
  102. // 在一起, 两笔提交同时在飞会把页的归属搞乱。详见 DB.Commit 的说明。
  103. txn.db.commitMu.Lock()
  104. defer txn.db.commitMu.Unlock()
  105. // 1. 收集 BufferPool 脏页
  106. dirty := txn.db.file.CollectDirty()
  107. if len(dirty) == 0 {
  108. // 无脏页 = 修改已持久, 本事务登记的待回收页 + 全局积压都可以安全还链
  109. txn.db.freeNow(txn.pendingFree)
  110. txn.db.freeChainNow(txn.pendingFreeChain)
  111. txn.db.drainPendingFree()
  112. txn.committed = true
  113. txn.db.currentTxn.Store(nil)
  114. txn.touched = nil
  115. txn.pendingFree = nil
  116. txn.pendingFreeChain = nil
  117. txn.allocated = nil
  118. txn.db.accMu.Unlock() // 事务成功终结, 释放 Begin 持有的串行锁
  119. return nil
  120. }
  121. // 2. 创建 WAL 事务
  122. wal := txn.db.BeginTxn()
  123. // 3. 写整页条目: 所有被修改的页 (保证页结构完整恢复)
  124. // 数据源就是 BufferPool 脏页本身(最新数据), 事务期间没有中途刷盘,
  125. // 因此不存在"写回事务前旧快照覆盖新数据"的风险。
  126. for _, d := range dirty {
  127. wal.RecordPage(d.PageID, d.Data)
  128. }
  129. // 4. HeaderPage 也纳入
  130. // marshalHeader 读整张头页, 含提交路径正在改的 WAL 链指针, 必须在 headerMu 内拍快照。
  131. txn.db.file.withHeader(func() {
  132. wal.RecordPage(0, marshalHeader(txn.db.header))
  133. })
  134. // 5. ⭐ 关键:Commit 会触发 fsync,确保落盘 ⭐
  135. if err := wal.Commit(); err != nil {
  136. // 提交失败: 这批页继续留在缓存里当脏页(CollectDirty 不再移出缓存),
  137. // RestoreDirty 只做兜底, 见 DB.Commit 里的说明
  138. txn.db.file.RestoreDirty(dirty)
  139. return fmt.Errorf("WAL commit failed: %v", err)
  140. }
  141. // 6. 提交成功 → 把这一批页收成干净页 (按快照版本号, 提交窗口里被改过的页继续留脏)
  142. txn.db.file.MarkClean(dirty)
  143. // 7. 提交成功 → 堆链/索引指针已持久化, 本事务登记摘链的页现在可以真正还链。
  144. // 必须在 wal.Commit() 之后: 它内部已完成 fsync + 回写真实页。
  145. txn.db.freeNow(txn.pendingFree)
  146. txn.db.freeChainNow(txn.pendingFreeChain)
  147. txn.db.drainPendingFree()
  148. // 8. 清理事务状态
  149. txn.committed = true
  150. txn.db.currentTxn.Store(nil)
  151. txn.touched = nil
  152. txn.pendingFree = nil
  153. txn.pendingFreeChain = nil
  154. txn.allocated = nil
  155. txn.db.accMu.Unlock() // 事务成功终结, 释放 Begin 持有的串行锁
  156. return nil
  157. }
  158. // Rollback 回滚事务: 丢弃本事务修改过的页缓存, 从磁盘重新加载。
  159. //
  160. // 事务期间 autoCommit 被 currentTxn 拦住, 没有任何脏页被刷到磁盘, 磁盘上仍是事务
  161. // 开始前的状态 —— 所以"丢掉缓存"就等价于"撤销内存修改", 不需要任何页级快照。
  162. // 只丢 txn.touched 里的页: 事务开始前遗留的脏页不属于本事务, 必须原样保留。
  163. func (txn *Transaction) Rollback() error {
  164. if txn.committed {
  165. return fmt.Errorf("transaction already committed, cannot rollback")
  166. }
  167. if txn.rolledBack {
  168. return fmt.Errorf("transaction already rolled back")
  169. }
  170. // 1. 丢弃本事务碰过的页缓存 (不落盘), 后续读取自然回落到磁盘旧值
  171. txn.db.file.DiscardPages(txn.touched)
  172. // 2. Table.info 是独立于页缓存的内存副本, 必须从磁盘重新读回,
  173. // 否则 TotalRows / FirstData 等仍停留在事务中的值
  174. txn.db.tables.ReloadInfos(txn.touched)
  175. // 3. 本事务登记待回收的页/链必须整体丢弃: 回滚后堆链指针回到事务前,
  176. // 这些页重新被引用, 绝不能还链 (全局 pendingFree 不属于本事务, 保持不动)
  177. txn.pendingFree = nil
  178. txn.pendingFreeChain = nil
  179. // 4. 归还本事务新分配的页 (见 allocated 字段注释): 分配时 header 的游离指针
  180. // 已被就地推进并直写盘, 回滚不补回这些页就会产生孤儿页。
  181. // 放在 DiscardPages 之后: 池里那些未落盘的脏内容已丢弃, 这里用干净的空闲页头覆盖。
  182. txn.db.freeNow(txn.allocated)
  183. txn.allocated = nil
  184. // 清理事务状态
  185. txn.rolledBack = true
  186. txn.db.currentTxn.Store(nil)
  187. txn.touched = nil
  188. txn.db.accMu.Unlock() // 回滚完成, 释放 Begin 持有的串行锁
  189. return nil
  190. }
  191. // IsCommitted 检查事务是否已提交
  192. func (txn *Transaction) IsCommitted() bool {
  193. return txn.committed
  194. }
  195. // IsRolledBack 检查事务是否已回滚
  196. func (txn *Transaction) IsRolledBack() bool {
  197. return txn.rolledBack
  198. }