wal.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. package core
  2. import (
  3. "TsDB2/DB/core/page"
  4. "encoding/binary"
  5. "hash/crc32"
  6. )
  7. // =============================================================================
  8. // WAL (Write-Ahead Log) 实现
  9. //
  10. // WAL 页布局 (16KB), 权威定义见 page.WALPageHdr:
  11. // [WALPageHdr(40B)] [WALEntry...]
  12. // 首页与续页头部等长 —— 续页的 TxnID/TotalEntries/State 填 0, 换来整条链能用
  13. // 同一个结构体解析, 读写都不再按 isFirst 分叉。
  14. //
  15. // ⭐ 页间链指针用 PageHdr.NextPage (与空闲链同一个字段), 不再自定义 NextWalPage ——
  16. // 这样整条 WAL 链本身就是一条合法空闲链, 回收时一次拼接即可, 不必逐页遍历 (见 recycleChain)。
  17. //
  18. // WALEntry (见 page.WALEntryMeta): [TargetPageID(4B)] [DataLen(4B)] [Checksum(4B)] [OpType(1B)] [Data(DataLen)]
  19. // OpType == WALOpFullPage(0) → Data 为整页镜像 (DataLen = 页有效长度)
  20. // OpType == Insert/Delete/Update → Data 为行级数据
  21. // Checksum = CRC32(OpType || Data)
  22. //
  23. // ⚠️ OpType 必须显式存储: 早期实现靠"Data 的首字节"猜类型, 但整页镜像的首字节
  24. // 就是 PageHdr.Type, 而 DataPage/TablePage/HeapPage 的 Type 恰好是 2/1/3,
  25. // 与行级 OpType 冲突, 整页记录会被误判成行级记录而丢失。
  26. //
  27. // WAL 状态机 (State 字段):
  28. // 0 = Mem (内存中,未写盘)
  29. // 1 = Written (已 write(),未 fsync())
  30. // 2 = Persisted (已 fsync(),但未标记提交)
  31. // 3 = Committed (已提交,可安全恢复)
  32. //
  33. // 打包: 一个 WAL 页可容纳**多条** Entry —— 写端按「当前页剩余空间 >= 13B + DataLen」
  34. // 紧凑追加, 放不下才开新页。整页镜像 (≈16KB) 仍会独占一页; 瘦页 (点更新/小表) 则能
  35. // 在一页里合并几十条, 页尾不再被零填充浪费。
  36. // 读端本就按 EntryCount 逐条解析 (见 wal_recovery.go), 所以打包只改写端。
  37. // =============================================================================
  38. // DataPage 磁盘布局见 page.DataPageHdr (头 40B), 行数据区从 page.DataPageHdrSize 开始。
  39. // WAL 状态常量 (对应 State 字段)
  40. const (
  41. WALStateMem = 0 // 内存中,未写盘
  42. WALStateWritten = 1 // 已 write(),未 fsync()
  43. WALStatePersisted = 2 // 已 fsync(),但未标记提交
  44. WALStateCommitted = 3 // 已提交,可安全恢复
  45. )
  46. // =============================================================================
  47. // WAL 条目操作类型 (每个条目都显式带 1B OpType)
  48. //
  49. // INSERT: Data = storeRow [pkID(8B BE) + rowData]
  50. // DELETE: Data = pkID(8B BE)
  51. // UPDATE: Data = storeRow [pkID(8B BE) + rowData] (新行数据)
  52. // =============================================================================
  53. const (
  54. WALOpFullPage = 0 // 整页镜像 (Data = 页内容)
  55. WALOpInsert = 1 // 插入行
  56. WALOpDelete = 2 // 删除行
  57. WALOpUpdate = 3 // 更新行
  58. )
  59. // RowEntry 行级 WAL 记录
  60. type RowEntry struct {
  61. PageID uint32 // 目标 DataPage ID
  62. OpType byte // WALOpInsert / WALOpDelete / WALOpUpdate
  63. StoreRow []byte // INSERT/UPDATE: [pkID(8B BE) + rowData]; DELETE: pkID(8B BE)
  64. }
  65. // WALWriter 事务 WAL 写入器
  66. type WALWriter struct {
  67. txnID uint64
  68. entries []walEntry
  69. file *File
  70. header *HeaderPage
  71. state byte // 当前 WAL 状态 (Mem/Written/Persisted/Committed)
  72. }
  73. type walEntry struct {
  74. pageID uint32
  75. data []byte // 完整页镜像 (isRowLevel=false 时使用)
  76. isRowLevel bool // 是否为行级记录(true=存行数据,false=存整页)
  77. opType byte // 行级操作类型 (WALOpInsert/WALOpDelete/WALOpUpdate)
  78. rowEntry *RowEntry // 行级记录
  79. }
  80. // BeginTxn 开始新事务
  81. func (db *DB) BeginTxn() *WALWriter {
  82. // NextTxnID 是头页里的全局游标, 提交 / 事务提交都可能并发取号, 必须在 headerMu 内自增。
  83. var txnID uint64
  84. db.file.withHeader(func() {
  85. txnID = db.header.NextTxnID
  86. db.header.NextTxnID++
  87. })
  88. return &WALWriter{
  89. txnID: txnID,
  90. entries: make([]walEntry, 0, 16),
  91. file: db.file,
  92. header: db.header,
  93. state: WALStateMem, // 初始状态: Mem (内存中)
  94. }
  95. }
  96. // RecordPage 记录一个被修改的页 (同一 pageID 只保留最新镜像)
  97. func (w *WALWriter) RecordPage(pageID uint32, data []byte) {
  98. d := make([]byte, len(data))
  99. copy(d, data)
  100. // 同一 pageID 只保留最新镜像
  101. for i := range w.entries {
  102. if w.entries[i].pageID == pageID {
  103. w.entries[i].data = d
  104. return
  105. }
  106. }
  107. w.entries = append(w.entries, walEntry{
  108. pageID: pageID,
  109. data: d,
  110. isRowLevel: false, // 原有逻辑:存整页
  111. })
  112. }
  113. // RecordRowChange 记录行级变更 (带 OpType, 用于崩溃恢复)
  114. // pageID: 目标 DataPage ID
  115. // opType: WALOpInsert / WALOpDelete / WALOpUpdate
  116. // storeRow: INSERT/UPDATE = [pkID(8B BE) + rowData], DELETE = pkID(8B BE)
  117. func (w *WALWriter) RecordRowChange(pageID uint32, opType byte, storeRow []byte) {
  118. data := make([]byte, len(storeRow))
  119. copy(data, storeRow)
  120. w.entries = append(w.entries, walEntry{
  121. pageID: pageID,
  122. isRowLevel: true,
  123. opType: opType,
  124. rowEntry: &RowEntry{
  125. PageID: pageID,
  126. OpType: opType,
  127. StoreRow: data,
  128. },
  129. })
  130. }
  131. // Rollback 回滚: 丢弃所有条目
  132. func (w *WALWriter) Rollback() {
  133. w.entries = nil
  134. w.state = WALStateMem // 重置状态
  135. }
  136. // Commit 原子提交流程:
  137. // 1. 将 entries 写入 WAL 页链 (State: Mem → Written)
  138. // 2. fsync WAL 页 (State: Written → Persisted)
  139. // 3. 标记 WAL 首页 Committed = 1 并 fsync (State: Persisted → Committed)
  140. // 4. 回写实际页 (redo; 整页镜像, 重复执行幂等)
  141. // 5. 回收所有 WAL 页 + 清空 Header 链指针 + fsync
  142. //
  143. // ⭐ 关键: 「提交标记」必须先于「回写真实页」落盘。
  144. // 若顺序反过来, 断电落在两者之间时真实页已被原地覆盖, 而提交标记仍是旧值,
  145. // 恢复会丢弃整个 WAL, 留下半应用(损坏)的页 —— 这里没有 undo, 无法自愈。
  146. //
  147. // 缓冲池的脏标记【不在这里】收尾: 提交方拿到 success 之后调用 File.MarkClean
  148. // (按版本号逐页判定), 因为只有提交方知道哪些页属于这次快照, 也才知道窗口里有没有
  149. // 人把这些页又改了一遍。
  150. func (w *WALWriter) Commit() error {
  151. if len(w.entries) == 0 {
  152. return nil
  153. }
  154. // === 步骤 1: 写入 WAL 页链 (State: Mem → Written) ===
  155. walPageIDs, err := w.writeWALPages()
  156. if err != nil {
  157. return err
  158. }
  159. // === 步骤 2: fsync WAL 页 (State: Written → Persisted) ===
  160. if err := w.file.sync(); err != nil {
  161. return err
  162. }
  163. // 状态转换: Written → Persisted (已落盘)
  164. w.state = WALStatePersisted
  165. // === 步骤 3: 标记 WAL 首页为已提交, 并 fsync (State: Persisted → Committed) ===
  166. // 这一步之后, 无论发生什么崩溃, 恢复都能重放整笔事务 (WAL 已完整且被标记)。
  167. if err := w.markCommitted(walPageIDs[0]); err != nil {
  168. return err
  169. }
  170. if err := w.file.sync(); err != nil {
  171. return err
  172. }
  173. // === 步骤 4: 回写实际页 (直接写盘, 绕过 BufferPool) ===
  174. // 放在提交标记之后: 中途断电可由恢复阶段重做 (整页镜像, 幂等)。
  175. //
  176. // 用 WritePageHome 而不是 WritePageDirect: 这里写出去的正是缓存快照本身, 缓存里
  177. // 那一份的内容只会等于它或新于它 (并发写入时), 无差别失效会丢掉更新的那一份。
  178. // 缓存状态由提交方在 Commit() 返回后用 File.MarkClean 按版本号收尾 —— 这也是
  179. // 这里不再 ClearCache 的原因: ClearAll 会把提交窗口里被重新改脏的页一并标干净,
  180. // 那些页并不在本笔 WAL 里, 标干净等于把它们的新内容永久丢掉。
  181. for _, e := range w.entries {
  182. if e.pageID == 0 {
  183. continue
  184. }
  185. if err := w.file.WritePageHome(e.pageID, e.data); err != nil {
  186. return err
  187. }
  188. }
  189. // === 步骤 5: 回收整条 WAL 链 (O(1) 拼接, 与链长无关) ===
  190. // 链内每页的 next 都写在 PageHdr.NextPage, 与空闲链同字段, 所以整条链
  191. // head…tail 可以直接挂到空闲链尾, 不必逐页遍历回收。
  192. // 链首/链尾/链长都已在 writeWALPages 中记录 (walPageIDs), 尾页 NextPage 必为 0。
  193. //
  194. // 还链 (改空闲链指针) 与清空 WAL 链指针是一次头页上的复合修改, 必须整体落在
  195. // headerMu 内, 否则并发写入线程可能看到"WAL 链已清空、空闲链却还没接好"的中间态。
  196. var recycleErr error
  197. w.file.withHeader(func() {
  198. recycleErr = recycleChainWithCache(w.file, w.header,
  199. walPageIDs[0], walPageIDs[len(walPageIDs)-1], uint32(len(walPageIDs)))
  200. if recycleErr != nil {
  201. return
  202. }
  203. w.header.FirstWALPage = 0
  204. w.header.LastWALPage = 0
  205. recycleErr = w.file.writeHeaderLocked(w.header)
  206. })
  207. if recycleErr != nil {
  208. return recycleErr
  209. }
  210. // 收尾必须 fsync: 否则断电后磁盘上的 Header 仍指向已提交的 WAL,
  211. // 下次打开会重复重放 (行级记录场景还会造成重复插入)
  212. if err := w.file.sync(); err != nil {
  213. return err
  214. }
  215. w.entries = nil
  216. return nil
  217. }
  218. // writeWALPages 将所有 entry 紧凑写入 WAL 页链, 返回分配的页 ID 列表
  219. // 状态转换: Mem → Written
  220. //
  221. // 打包策略: 一条 Entry 占 [13B 元数据 + DataLen]。当前页剩余空间够就继续追加,
  222. // 不够才定型落盘并开新页 —— 一条 200B 的记录原本独占 16KB, 现在一页可放约 76 条。
  223. //
  224. // 所有 WAL 页头部等长 (page.WALPageHdrSize); walEntryPayload 已把单条记录截断到
  225. // 「一定能放进空页」的长度, 因此开新页后必定写得下, 不会出现空页仍放不下的情况。
  226. func (w *WALWriter) writeWALPages() ([]uint32, error) {
  227. var pageIDs []uint32
  228. // 当前正在填充的 WAL 页
  229. var curBuf []byte
  230. var curPID uint32
  231. var curIsFirst bool
  232. var curOff uint32 // 已用字节数 (从页头之后开始)
  233. var curEntries uint16 // 已写入的条目数
  234. // flushCurrent 定型当前页 (回填 UseSize / EntryCount) 并落盘。
  235. // 链指针不在这里填 —— 只有"开新页"时才知道下一页是谁, 由调用处先回填再 flush。
  236. // ⭐ 全程用内存缓冲记录"是否首页", 不从磁盘读回上一页判定 —— 那种实现依赖
  237. // TxnID != 0, 而首个事务的 TxnID 恰好是 0, 会把链指针写到错误偏移。
  238. flushCurrent := func() error {
  239. if curBuf == nil {
  240. return nil
  241. }
  242. wh := page.WalHdrOf(curBuf)
  243. wh.UseSize = curOff
  244. wh.EntryCount = curEntries
  245. return w.file.WritePageDirect(curPID, curBuf)
  246. }
  247. for i := range w.entries {
  248. opType, targetPID, payload := w.walEntryPayload(w.entries[i])
  249. dataLen := uint32(len(payload))
  250. need := page.WALEntryMeta + dataLen
  251. // 当前页放不下 → 开新页
  252. if curBuf == nil || uint32(PageSize)-curOff < need {
  253. // 分配新页: headerMu 内摘空闲链 / 递增游标并推进头页。
  254. // erase=false —— 页紧接着就在内存里被整页重造 (curBuf 是全 0 的新切片,
  255. // flushCurrent 一次写满 16KB), 不需要擦页; 且此刻正处在 Commit 内
  256. // (持有 commitMu), 擦页会重入 autoCommit → Commit → 自死锁在 commitMu 上。
  257. pid := w.file.allocPage(w.header, false)
  258. // 先回填上一页的链指针, 再把上一页定型落盘。
  259. // 链指针统一写 PageHdr.NextPage (偏移 12) —— 与空闲链表同一个字段,
  260. // 所以整条 WAL 链本身就是一条合法空闲链, 回收时可一次拼接 (见 Commit 步骤 5)。
  261. if curBuf != nil {
  262. page.HdrOf(curBuf).NextPage = pid
  263. if err := flushCurrent(); err != nil {
  264. return nil, err
  265. }
  266. }
  267. pageIDs = append(pageIDs, pid)
  268. curBuf = make([]byte, PageSize)
  269. curPID = pid
  270. curIsFirst = len(pageIDs) == 1
  271. curEntries = 0
  272. var prevPID uint32
  273. if len(pageIDs) > 1 {
  274. prevPID = pageIDs[len(pageIDs)-2]
  275. }
  276. wh := page.WalHdrOf(curBuf)
  277. wh.Init(pid, prevPID)
  278. if curIsFirst {
  279. // === 首页: TxnID + TotalEntries + State ===
  280. wh.TxnID = w.txnID
  281. wh.TotalEntries = uint32(len(w.entries))
  282. wh.State = WALStateWritten // State = Written (已写盘,未 fsync)
  283. }
  284. // 首页与续页头部等长, 条目区起点一致
  285. curOff = page.WALPageHdrSize
  286. }
  287. // 追加当前 entry
  288. // 统一格式: [TargetPageID(4B)][DataLen(4B)][Checksum(4B)][OpType(1B)][Data]
  289. // CRC32 覆盖 OpType + Data, 整页/行级一致, 类型显式存储不再靠首字节猜测。
  290. csBuf := make([]byte, 1+dataLen)
  291. csBuf[0] = opType
  292. copy(csBuf[1:], payload)
  293. binary.LittleEndian.PutUint32(curBuf[curOff:], targetPID)
  294. binary.LittleEndian.PutUint32(curBuf[curOff+4:], dataLen)
  295. binary.LittleEndian.PutUint32(curBuf[curOff+8:], crc32.ChecksumIEEE(csBuf))
  296. curBuf[curOff+12] = opType
  297. curOff += page.WALEntryMeta
  298. copy(curBuf[curOff:], payload)
  299. curOff += dataLen
  300. curEntries++
  301. }
  302. // 定型最后一页
  303. if err := flushCurrent(); err != nil {
  304. return nil, err
  305. }
  306. // 更新 HeaderPage 的 WAL 链指针
  307. // 发布链指针是为了崩溃恢复能找回这笔事务, 必须在 headerMu 内对"首页是否已存在"
  308. // 与"尾页指向"做一次原子更新, 否则并发提交可能互相覆盖链尾。
  309. if len(pageIDs) > 0 {
  310. w.file.withHeader(func() {
  311. if w.header.FirstWALPage == 0 {
  312. w.header.FirstWALPage = pageIDs[0]
  313. }
  314. w.header.LastWALPage = pageIDs[len(pageIDs)-1]
  315. _ = w.file.writeHeaderLocked(w.header)
  316. })
  317. }
  318. // 状态转换: Mem → Written (已写盘,未 fsync)
  319. w.state = WALStateWritten
  320. return pageIDs, nil
  321. }
  322. // walEntryPayload 解析一条 entry 的目标页 / OpType / 载荷, 并把载荷截断到
  323. // 「一定能放进一张空 WAL 页」的长度 (首页/续页头等长, 只需一个限制)。
  324. //
  325. // 这个截断是 writeWALPages 打包逻辑的前提: 开新页后必定能写下当前这条记录。
  326. func (w *WALWriter) walEntryPayload(e walEntry) (byte, uint32, []byte) {
  327. opType := byte(WALOpFullPage)
  328. targetPID := e.pageID
  329. var payload []byte
  330. switch {
  331. case e.isRowLevel && e.rowEntry != nil:
  332. opType = e.rowEntry.OpType
  333. targetPID = e.rowEntry.PageID
  334. payload = e.rowEntry.StoreRow
  335. case e.isRowLevel:
  336. // 行级条目缺少 rowEntry: 视作空行级记录
  337. opType = e.opType
  338. payload = nil
  339. default:
  340. // 整页记录: 只存有效前缀 (整页 16KB 无法与条目元数据同放一页)
  341. storeLen := uint32(len(e.data))
  342. if e.pageID == 0 {
  343. // HeaderPage 只有前 4KB 有效; 必须与 recoverHeader 的解析长度一致
  344. storeLen = PageSize4K
  345. } else if len(e.data) >= int(page.PageHdrSize) {
  346. useSize := page.HdrOf(e.data).UseSize
  347. if useSize > 0 && useSize < uint32(len(e.data)) {
  348. storeLen = useSize
  349. }
  350. }
  351. if storeLen > uint32(len(e.data)) {
  352. storeLen = uint32(len(e.data))
  353. }
  354. payload = e.data[:storeLen]
  355. }
  356. // 防御: 单条记录不得超过空首页容量 (行级记录同样受此保护)
  357. if maxLen := uint32(PageSize) - page.WALPageHdrSize - page.WALEntryMeta; uint32(len(payload)) > maxLen {
  358. payload = payload[:maxLen]
  359. }
  360. return opType, targetPID, payload
  361. }
  362. // markCommitted 标记 WAL 首页为已提交 (State: Persisted → Committed)
  363. func (w *WALWriter) markCommitted(firstPageID uint32) error {
  364. buf := make([]byte, PageSize)
  365. // 必须直接读盘: WAL 页是用 WritePageDirect 写的, 走 BufferPool 可能读到陈旧缓存
  366. if err := w.file.readPageDisk(firstPageID, buf); err != nil {
  367. return err
  368. }
  369. page.WalHdrOf(buf).State = WALStateCommitted // State = Committed
  370. if err := w.file.WritePageDirect(firstPageID, buf); err != nil {
  371. return err
  372. }
  373. // 状态转换: Persisted → Committed
  374. w.state = WALStateCommitted
  375. return nil
  376. }
  377. // sync 刷盘 (fsync)
  378. func (ff *File) sync() error {
  379. return ff.f.Sync()
  380. }
  381. // =============================================================================
  382. // applyRowToPage 将行级 WAL 记录应用到页缓冲区 (用于崩溃恢复)
  383. //
  384. // 操作类型:
  385. //
  386. // WALOpInsert: 在 DataPage 中追加一行
  387. // WALOpDelete: 在 DataPage 中软删除指定 pkID 的行
  388. // WALOpUpdate: 在 DataPage 中按 pkID 原地更新行数据
  389. //
  390. // storeRow 格式: [pkID(8B BE) + rowData] (INSERT/UPDATE)
  391. // storeRow 格式: pkID(8B BE) (DELETE)
  392. // =============================================================================
  393. func applyRowToPage(buf []byte, opType byte, storeRow []byte) {
  394. const delFlag = uint32(0x80000000)
  395. if len(buf) < int(page.DataPageHdrSize) {
  396. return
  397. }
  398. // 读取 DataPage 元数据 (Count/Min/Max 紧随 PageHdr)
  399. dh := page.DataHdrOf(buf)
  400. count := dh.Count
  401. dataEnd := dh.UseSize
  402. if dataEnd < page.DataPageHdrSize || dataEnd > uint32(len(buf)) {
  403. return
  404. }
  405. switch opType {
  406. case WALOpInsert:
  407. // 追加行: [len(4B)] [storeRow]
  408. off := dataEnd
  409. if off+4+uint32(len(storeRow)) > uint32(len(buf)) {
  410. return // 页满, 放弃 (整页镜像已包含该行)
  411. }
  412. binary.LittleEndian.PutUint32(buf[off:], uint32(len(storeRow)))
  413. off += 4
  414. copy(buf[off:], storeRow)
  415. off += uint32(len(storeRow))
  416. // 更新 count 和 dataEnd (UseSize)
  417. dh.Count = count + 1
  418. dh.UseSize = off
  419. case WALOpDelete:
  420. // storeRow = pkID(8B BE)
  421. if len(storeRow) < 8 {
  422. return
  423. }
  424. keyPK := binary.BigEndian.Uint64(storeRow[:8])
  425. off := page.DataPageHdrSize
  426. for i := uint32(0); i < count; i++ {
  427. if off+4 > dataEnd {
  428. break
  429. }
  430. rawLen := binary.LittleEndian.Uint32(buf[off:])
  431. deleted := rawLen&delFlag != 0
  432. rowLen := rawLen & ^delFlag
  433. rowStart := off + 4
  434. if rowStart+rowLen > dataEnd {
  435. break
  436. }
  437. if !deleted && rowLen >= 8 {
  438. rowPK := binary.BigEndian.Uint64(buf[rowStart : rowStart+8])
  439. if rowPK == keyPK {
  440. // 设置软删除标志
  441. binary.LittleEndian.PutUint32(buf[off:], rowLen|delFlag)
  442. return
  443. }
  444. }
  445. off = rowStart + rowLen
  446. }
  447. case WALOpUpdate:
  448. // storeRow = [pkID(8B BE) + rowData]
  449. if len(storeRow) < 8 {
  450. return
  451. }
  452. keyPK := binary.BigEndian.Uint64(storeRow[:8])
  453. off := page.DataPageHdrSize
  454. for i := uint32(0); i < count; i++ {
  455. if off+4 > dataEnd {
  456. break
  457. }
  458. rawLen := binary.LittleEndian.Uint32(buf[off:])
  459. deleted := rawLen&delFlag != 0
  460. rowLen := rawLen & ^delFlag
  461. rowStart := off + 4
  462. if rowStart+rowLen > dataEnd {
  463. break
  464. }
  465. if !deleted && rowLen >= 8 {
  466. rowPK := binary.BigEndian.Uint64(buf[rowStart : rowStart+8])
  467. if rowPK == keyPK {
  468. // 原地覆盖行数据
  469. copy(buf[rowStart:], storeRow)
  470. // 剩余字节置零
  471. for j := uint32(len(storeRow)); j < rowLen; j++ {
  472. buf[rowStart+j] = 0
  473. }
  474. return
  475. }
  476. }
  477. off = rowStart + rowLen
  478. }
  479. }
  480. }