transaction.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. package core
  2. import (
  3. "TsDB2/DB/core/page"
  4. "encoding/binary"
  5. "fmt"
  6. )
  7. // Transaction 事务管理器
  8. //
  9. // 核心思想:
  10. // - 事务内所有修改只落在 BufferPool —— autoCommit 被 currentTxn 拦住, 中途不会刷盘,
  11. // 所以磁盘始终是事务开始前的状态, 不需要任何「原始数据快照」
  12. // - Commit 时把脏页 + Header 整体写入 WAL, 保证原子
  13. // - Rollback 只需丢弃本事务碰过的页缓存(并从磁盘重载 TableInfo), 后续读取自然回到旧值
  14. type Transaction struct {
  15. db *DB
  16. touched map[uint32]struct{} // 本事务修改过的页 ID(Rollback 时丢弃其缓存)
  17. rowChanges map[uint32]struct{ original, new []byte } // pageID → {原始行, 新行}(用于 WAL)
  18. // pendingFree 本事务登记待回收的页 (堆页摘链 / 批量删除后作废的旧 DataPage)。
  19. // 不在事务中途直写盘: 提交成功后才真正还链, 回滚时整体丢弃
  20. // (回滚后链指针回到事务前, 这些页仍被引用, 绝不能还链)。
  21. pendingFree []uint32
  22. // pendingFreeChain 本事务登记待回收的整链 (如哈希索引整体重建后的旧桶链)。
  23. // 与 pendingFree 同一时序约定, 但一条链只登记一次, 提交后一次 FreeChain 还链。
  24. pendingFreeChain []pendingChain
  25. // allocated 本事务新分配的页 ID (从空闲链摘出, 或空闲链耗尽时由 NextFreePage 递增)。
  26. // 分配在 newPageWithReader 里就地推进了 header 的游离指针并直写盘, 事务中途无法回退;
  27. // 因此 Rollback 必须把这些页还给空闲链 —— 否则它们既不被任何表/索引引用、也不在
  28. // 空闲链上, 就成了永久泄漏的孤儿页 (文件只涨不缩)。
  29. // Commit 成功时这些页已被数据引用, 直接丢弃本表即可。
  30. allocated []uint32
  31. committed bool
  32. rolledBack bool
  33. }
  34. // 事务内被直接 FreePage(立即还链) 的页, 从 allocated 中撤销登记, 避免 Rollback 二次归还。
  35. func (txn *Transaction) undoAllocated(pageID uint32) {
  36. for i, id := range txn.allocated {
  37. if id == pageID {
  38. txn.allocated = append(txn.allocated[:i], txn.allocated[i+1:]...)
  39. return
  40. }
  41. }
  42. }
  43. // BeginTransaction 开启新事务
  44. func (db *DB) BeginTransaction() *Transaction {
  45. txn := &Transaction{
  46. db: db,
  47. touched: make(map[uint32]struct{}),
  48. rowChanges: make(map[uint32]struct{ original, new []byte }),
  49. committed: false,
  50. rolledBack: false,
  51. }
  52. // 设置当前活跃事务
  53. db.currentTxn = txn
  54. return txn
  55. }
  56. // markTouched 登记本事务修改过的页 (由 BufferPool 的 onPageWrite 回调触发)。
  57. // 只记页 ID(4B/页), 不存页数据 —— 回滚靠「丢弃缓存 + 从磁盘重读」实现。
  58. func (txn *Transaction) markTouched(pageID uint32) {
  59. txn.touched[pageID] = struct{}{}
  60. }
  61. // RecordRowChange 记录行级变化(用于 WAL)
  62. // originalRow: 原始行数据
  63. // newRow: 新行数据
  64. func (txn *Transaction) RecordRowChange(pageID uint32, originalRow []byte, newRow []byte) {
  65. txn.rowChanges[pageID] = struct{ original, new []byte }{
  66. original: originalRow,
  67. new: newRow,
  68. }
  69. }
  70. // Commit 提交事务
  71. // 只写「整页镜像」条目: 恢复所有被修改页的结构与数据
  72. // (B+树节点, IndexMeta, HeaderPage, DataPage 一次到位)。
  73. //
  74. // 不再额外写行级条目: 整页镜像已完整包含行数据, 再叠加行级记录会在恢复时被
  75. // 重复应用 (INSERT 变成两行), 见 wal_recovery 的 Phase 1 / Phase 2。
  76. func (txn *Transaction) Commit() error {
  77. if txn.committed {
  78. return fmt.Errorf("transaction already committed")
  79. }
  80. if txn.rolledBack {
  81. return fmt.Errorf("transaction already rolled back")
  82. }
  83. // 与 DB.Commit 共用同一把提交锁: 事务提交和自动提交(定时器 / 脏页阈值)也可能撞
  84. // 在一起, 两笔提交同时在飞会把页的归属搞乱。详见 DB.Commit 的说明。
  85. txn.db.commitMu.Lock()
  86. defer txn.db.commitMu.Unlock()
  87. // 1. 收集 BufferPool 脏页
  88. dirty := txn.db.file.CollectDirty()
  89. if len(dirty) == 0 {
  90. // 无脏页 = 修改已持久, 本事务登记的待回收页 + 全局积压都可以安全还链
  91. txn.db.freeNow(txn.pendingFree)
  92. txn.db.freeChainNow(txn.pendingFreeChain)
  93. txn.db.drainPendingFree()
  94. txn.committed = true
  95. txn.db.currentTxn = nil
  96. txn.touched = nil
  97. txn.pendingFree = nil
  98. txn.pendingFreeChain = nil
  99. txn.allocated = nil
  100. txn.rowChanges = nil
  101. return nil
  102. }
  103. // 2. 创建 WAL 事务
  104. wal := txn.db.BeginTxn()
  105. // 3. 写整页条目: 所有被修改的页 (保证页结构完整恢复)
  106. // 数据源就是 BufferPool 脏页本身(最新数据), 事务期间没有中途刷盘,
  107. // 因此不存在"写回事务前旧快照覆盖新数据"的风险。
  108. for _, d := range dirty {
  109. wal.RecordPage(d.PageID, d.Data)
  110. }
  111. // 4. HeaderPage 也纳入
  112. // marshalHeader 读整张头页, 含提交路径正在改的 WAL 链指针, 必须在 headerMu 内拍快照。
  113. txn.db.file.withHeader(func() {
  114. wal.RecordPage(0, marshalHeader(txn.db.header))
  115. })
  116. // 5. ⭐ 关键:Commit 会触发 fsync,确保落盘 ⭐
  117. if err := wal.Commit(); err != nil {
  118. // 提交失败: 这批页继续留在缓存里当脏页(CollectDirty 不再移出缓存),
  119. // RestoreDirty 只做兜底, 见 DB.Commit 里的说明
  120. txn.db.file.RestoreDirty(dirty)
  121. return fmt.Errorf("WAL commit failed: %v", err)
  122. }
  123. // 6. 提交成功 → 把这一批页收成干净页 (按快照版本号, 提交窗口里被改过的页继续留脏)
  124. txn.db.file.MarkClean(dirty)
  125. // 7. 提交成功 → 堆链/索引指针已持久化, 本事务登记摘链的页现在可以真正还链。
  126. // 必须在 wal.Commit() 之后: 它内部已完成 fsync + 回写真实页。
  127. txn.db.freeNow(txn.pendingFree)
  128. txn.db.freeChainNow(txn.pendingFreeChain)
  129. txn.db.drainPendingFree()
  130. // 8. 清理事务状态
  131. txn.committed = true
  132. txn.db.currentTxn = nil
  133. txn.touched = nil
  134. txn.pendingFree = nil
  135. txn.pendingFreeChain = nil
  136. txn.allocated = nil
  137. txn.rowChanges = nil
  138. return nil
  139. }
  140. // =============================================================================
  141. // diffDataPages 比较两个 DataPage 缓冲区, 提取行级变更
  142. // 返回 INSERT/DELETE/UPDATE 的 RowEntry 列表
  143. // =============================================================================
  144. func diffDataPages(origBuf, curBuf []byte) []RowEntry {
  145. const delFlag = uint32(0x80000000)
  146. type rowInfo struct {
  147. storeRow []byte
  148. deleted bool
  149. }
  150. // 解析 DataPage 中所有行
  151. // 布局: [PageHdr(20B)][Count(4B)][Min(8B)][Max(8B)] = 40B, 行区从 40 开始
  152. parseRows := func(buf []byte) map[uint64]rowInfo {
  153. rows := make(map[uint64]rowInfo)
  154. if len(buf) < int(page.DataPageHdrSize) {
  155. return rows
  156. }
  157. dh := page.DataHdrOf(buf)
  158. count := dh.Count
  159. dataEnd := dh.UseSize
  160. if dataEnd < page.DataPageHdrSize || dataEnd > uint32(len(buf)) {
  161. return rows
  162. }
  163. off := page.DataPageHdrSize
  164. for i := uint32(0); i < count; i++ {
  165. if off+4 > dataEnd || off+4 > uint32(len(buf)) {
  166. break
  167. }
  168. rawLen := binary.LittleEndian.Uint32(buf[off:])
  169. deleted := rawLen&delFlag != 0
  170. rowLen := rawLen & ^delFlag
  171. rowStart := off + 4
  172. if rowStart+rowLen > dataEnd || rowStart+rowLen > uint32(len(buf)) {
  173. break
  174. }
  175. if rowLen >= 8 {
  176. pkID := binary.BigEndian.Uint64(buf[rowStart : rowStart+8])
  177. row := make([]byte, rowLen)
  178. copy(row, buf[rowStart:rowStart+rowLen])
  179. rows[pkID] = rowInfo{storeRow: row, deleted: deleted}
  180. }
  181. off = rowStart + rowLen
  182. }
  183. return rows
  184. }
  185. origRows := parseRows(origBuf)
  186. curRows := parseRows(curBuf)
  187. var result []RowEntry
  188. // 检测 DELETE 和 UPDATE (存在于原始页中的行)
  189. for pkID, orig := range origRows {
  190. if orig.deleted {
  191. continue // 原始页中已删除的行不处理
  192. }
  193. cur, exists := curRows[pkID]
  194. if !exists || cur.deleted {
  195. // 行被删除
  196. pkBuf := make([]byte, 8)
  197. binary.BigEndian.PutUint64(pkBuf, pkID)
  198. result = append(result, RowEntry{OpType: WALOpDelete, StoreRow: pkBuf})
  199. } else {
  200. // 检查行数据是否变化
  201. if !bytesEqual(orig.storeRow, cur.storeRow) {
  202. result = append(result, RowEntry{OpType: WALOpUpdate, StoreRow: cur.storeRow})
  203. }
  204. }
  205. }
  206. // 检测 INSERT (存在于当前页但不在原始页中的行)
  207. for pkID, cur := range curRows {
  208. if cur.deleted {
  209. continue
  210. }
  211. if _, exists := origRows[pkID]; !exists {
  212. result = append(result, RowEntry{OpType: WALOpInsert, StoreRow: cur.storeRow})
  213. }
  214. }
  215. return result
  216. }
  217. func bytesEqual(a, b []byte) bool {
  218. if len(a) != len(b) {
  219. return false
  220. }
  221. for i := range a {
  222. if a[i] != b[i] {
  223. return false
  224. }
  225. }
  226. return true
  227. }
  228. // Rollback 回滚事务: 丢弃本事务修改过的页缓存, 从磁盘重新加载。
  229. //
  230. // 事务期间 autoCommit 被 currentTxn 拦住, 没有任何脏页被刷到磁盘, 磁盘上仍是事务
  231. // 开始前的状态 —— 所以"丢掉缓存"就等价于"撤销内存修改", 不需要任何页级快照。
  232. // 只丢 txn.touched 里的页: 事务开始前遗留的脏页不属于本事务, 必须原样保留。
  233. func (txn *Transaction) Rollback() error {
  234. if txn.committed {
  235. return fmt.Errorf("transaction already committed, cannot rollback")
  236. }
  237. if txn.rolledBack {
  238. return fmt.Errorf("transaction already rolled back")
  239. }
  240. // 1. 丢弃本事务碰过的页缓存 (不落盘), 后续读取自然回落到磁盘旧值
  241. txn.db.file.DiscardPages(txn.touched)
  242. // 2. Table.info 是独立于页缓存的内存副本, 必须从磁盘重新读回,
  243. // 否则 TotalRows / FirstData 等仍停留在事务中的值
  244. txn.db.tables.ReloadInfos(txn.touched)
  245. // 3. 本事务登记待回收的页/链必须整体丢弃: 回滚后堆链指针回到事务前,
  246. // 这些页重新被引用, 绝不能还链 (全局 pendingFree 不属于本事务, 保持不动)
  247. txn.pendingFree = nil
  248. txn.pendingFreeChain = nil
  249. // 4. 归还本事务新分配的页 (见 allocated 字段注释): 分配时 header 的游离指针
  250. // 已被就地推进并直写盘, 回滚不补回这些页就会产生孤儿页。
  251. // 放在 DiscardPages 之后: 池里那些未落盘的脏内容已丢弃, 这里用干净的空闲页头覆盖。
  252. txn.db.freeNow(txn.allocated)
  253. txn.allocated = nil
  254. // 清理事务状态
  255. txn.rolledBack = true
  256. txn.db.currentTxn = nil
  257. txn.touched = nil
  258. txn.rowChanges = nil
  259. return nil
  260. }
  261. // IsCommitted 检查事务是否已提交
  262. func (txn *Transaction) IsCommitted() bool {
  263. return txn.committed
  264. }
  265. // IsRolledBack 检查事务是否已回滚
  266. func (txn *Transaction) IsRolledBack() bool {
  267. return txn.rolledBack
  268. }