main.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. package main
  2. // =============================================================================
  3. // rw_concurrency — 读写并发实测
  4. //
  5. // 目的: 用真实运行回答两个问题
  6. // 1. 读和写是不是真并发? (读者能否在写事务进行中完成扫描, 而不是干等)
  7. // 2. 并发读者读到的是什么? 是事务前的一致快照(固定 20000 行), 还是随写者
  8. // 进度不断变化、未提交的"中间态"(行数在 20000~60000 之间跳), 甚至 panic?
  9. //
  10. // 布局:
  11. // - 先在一个事务里插 20000 行并提交 (基线);
  12. // - 写者开一个长事务, 再插 40000 行 (pk 20001..60000), 中途不提交;
  13. // - 4 个读者在写事务期间反复全表扫描 + 随机点查, 记录行数 / 错误 / panic。
  14. // =============================================================================
  15. import (
  16. "fmt"
  17. "os"
  18. "path/filepath"
  19. "runtime"
  20. "sync"
  21. "sync/atomic"
  22. "time"
  23. "git.ycl.pub/yl32940/TsDB2/DB/core"
  24. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  25. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  26. )
  27. const (
  28. baseRows = 20000 // 基线已提交行数
  29. writeRows = 40000 // 长事务里新插行数 (未提交)
  30. readers = 4
  31. )
  32. func main() {
  33. dir, err := os.MkdirTemp("", "rwconc-")
  34. if err != nil {
  35. panic(err)
  36. }
  37. defer os.RemoveAll(dir)
  38. path := filepath.Join(dir, "t.db")
  39. db, err := core.Open(path)
  40. if err != nil {
  41. panic(err)
  42. }
  43. tbl, err := db.CreateTable("emp", []types.FieldInfo{
  44. {FieldType: types.FieldType_Uint64, FieldName: []byte("No")},
  45. {FieldType: types.FieldType_Float64, FieldName: []byte("Score")},
  46. {FieldType: types.FieldType_String, Flag: types.WidthCodeFor(24), FieldName: []byte("Name")},
  47. })
  48. if err != nil {
  49. panic(err)
  50. }
  51. // 主键 B+ 树(键=行号 pkID): 建了它 Query 全表扫描 / FindByPK 才能定位行。
  52. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  53. panic(err)
  54. }
  55. // ---- 基线: 一个事务插 baseRows 行后提交 ----
  56. tx := db.BeginTransaction()
  57. for i := 1; i <= baseRows; i++ {
  58. if _, err := tbl.InsertRowWithID(uint64(i), []types.FieldValue{
  59. {FieldName: []byte("No"), Value: uint64(i)},
  60. {FieldName: []byte("Score"), Value: float64(i)},
  61. {FieldName: []byte("Name"), Value: fmt.Sprintf("name-%d", i)},
  62. }); err != nil {
  63. panic(err)
  64. }
  65. }
  66. if err := tx.Commit(); err != nil {
  67. panic(err)
  68. }
  69. // ---- 统计量 ----
  70. var (
  71. scans atomic.Int64
  72. overlap atomic.Int64 // 与写事务窗口重叠的扫描次数
  73. snapshot atomic.Int64 // 恰好读到基线 baseRows 行 (事务前快照)
  74. dirty atomic.Int64 // 读到 baseRows < n < 总数 (未提交中间态)
  75. readErrors atomic.Int64
  76. panics atomic.Int64
  77. pkErrors atomic.Int64
  78. )
  79. var mu sync.Mutex
  80. minN, maxN := int(1<<30), 0
  81. // 写事务窗口 (unixnano, 0=尚未开始)
  82. var wStart, wEnd atomic.Int64
  83. stop := make(chan struct{})
  84. var readersWG sync.WaitGroup
  85. oneScan := func() (n int, panicked bool, err error) {
  86. defer func() {
  87. if r := recover(); r != nil {
  88. panicked = true
  89. err = fmt.Errorf("panic: %v", r)
  90. }
  91. }()
  92. err = tbl.Query().ScanWithPKID(func(_ uint64, _ []types.FieldValue) bool {
  93. n++
  94. return true
  95. })
  96. return
  97. }
  98. for r := 0; r < readers; r++ {
  99. readersWG.Add(1)
  100. go func(id int) {
  101. defer readersWG.Done()
  102. for {
  103. select {
  104. case <-stop:
  105. return
  106. default:
  107. }
  108. t0 := time.Now().UnixNano()
  109. n, panicked, err := oneScan()
  110. if panicked {
  111. panics.Add(1)
  112. }
  113. if err != nil {
  114. readErrors.Add(1)
  115. }
  116. ws, we := wStart.Load(), wEnd.Load()
  117. inWindow := ws != 0 && ws <= t0 && (we == 0 || t0 <= we)
  118. // 随机点查一个基线行, 验证结构是否可解析
  119. if _, gerr := tbl.FindByPK(uint64((id*7919 + int(scans.Load()))%baseRows + 1)); gerr != nil {
  120. pkErrors.Add(1)
  121. }
  122. if n > 0 || err == nil {
  123. scans.Add(1)
  124. if inWindow {
  125. overlap.Add(1)
  126. }
  127. switch {
  128. case n == baseRows:
  129. snapshot.Add(1)
  130. case n > baseRows && n < baseRows+writeRows:
  131. dirty.Add(1)
  132. }
  133. mu.Lock()
  134. if n < minN {
  135. minN = n
  136. }
  137. if n > maxN {
  138. maxN = n
  139. }
  140. mu.Unlock()
  141. }
  142. }
  143. }(r)
  144. }
  145. // ---- 写者: 长事务插入 writeRows 行 ----
  146. time.Sleep(300 * time.Millisecond) // 让读者先跑起来
  147. wStart.Store(time.Now().UnixNano())
  148. wtx := db.BeginTransaction()
  149. for i := baseRows + 1; i <= baseRows+writeRows; i++ {
  150. if _, err := tbl.InsertRowWithID(uint64(i), []types.FieldValue{
  151. {FieldName: []byte("No"), Value: uint64(i)},
  152. {FieldName: []byte("Score"), Value: float64(i)},
  153. {FieldName: []byte("Name"), Value: fmt.Sprintf("name-%d", i)},
  154. }); err != nil {
  155. panic(err)
  156. }
  157. if (i-baseRows)%4000 == 0 {
  158. time.Sleep(5 * time.Millisecond) // 拉长中间态窗口, 便于读者撞上
  159. }
  160. }
  161. if err := wtx.Commit(); err != nil {
  162. panic(err)
  163. }
  164. wEnd.Store(time.Now().UnixNano())
  165. time.Sleep(300 * time.Millisecond) // 提交后再让读者跑一会
  166. close(stop)
  167. readersWG.Wait()
  168. finalN, _, _ := oneScan()
  169. _ = db.Close()
  170. fmt.Println("================ 读写并发实测结果 ================")
  171. fmt.Printf("CPU 核数 GOMAXPROCS = %d\n", runtime.GOMAXPROCS(0))
  172. fmt.Printf("写事务窗口约 %d ms (插入 %d 行, 未提交期间读者并发扫描)\n",
  173. (wEnd.Load()-wStart.Load())/1_000_000, writeRows)
  174. fmt.Println("------------------------------------------------")
  175. fmt.Printf("读者完成全表扫描总次数 : %d\n", scans.Load())
  176. fmt.Printf(" 其中与写事务窗口【重叠】 : %d <- 生命周期在墙上时钟重叠(实际在锁上排队, 见判定)\n", overlap.Load())
  177. fmt.Printf(" 读到事务前快照(%d行) : %d\n", baseRows, snapshot.Load())
  178. fmt.Printf(" 读到未提交中间态(行数跳) : %d <- 行数在 %d~%d 之间, 无快照隔离\n",
  179. dirty.Load(), baseRows, baseRows+writeRows)
  180. fmt.Printf("扫描行数范围 : min=%d max=%d\n", minN, maxN)
  181. fmt.Printf("扫描返回错误次数 : %d\n", readErrors.Load())
  182. fmt.Printf("扫描 panic 次数 : %d (结构解析越界等硬损坏)\n", panics.Load())
  183. fmt.Printf("点查 FindByPK 错误次数 : %d\n", pkErrors.Load())
  184. fmt.Println("------------------------------------------------")
  185. fmt.Printf("写事务提交后最终行数 : %d (应为 %d)\n", finalN, baseRows+writeRows)
  186. // 判定
  187. fmt.Println("\n【判定】")
  188. if dirty.Load() == 0 && minN == baseRows && maxN == baseRows+writeRows {
  189. fmt.Println(" - 无脏读: 写事务未提交期间, 读者在全局串行锁(accMu)上排队, 读不到",
  190. fmt.Sprintf("%d~%d 的中间态;", baseRows, baseRows+writeRows))
  191. fmt.Printf(" 扫描结果只有两种 —— 写事务拿锁前读到事务前状态(%d), 提交后读到完整结果(%d)。\n",
  192. baseRows, baseRows+writeRows)
  193. }
  194. if overlap.Load() > 0 {
  195. fmt.Printf(" - 注: %d 次扫描的生命周期与写窗口在墙上时钟重叠, 但它们在锁上排队, 并未读到中间态。\n", overlap.Load())
  196. }
  197. if dirty.Load() > 0 || panics.Load() > 0 || readErrors.Load() > 0 {
  198. fmt.Println(" - 仍存在脏读 / panic / 错误: 串行锁未生效, 属缺陷。")
  199. os.Exit(1)
  200. }
  201. }