main.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. package main
  2. // =============================================================================
  3. // concurrent_safe — 多 goroutine 误并发调用的安全性 / 数据完整性验证
  4. //
  5. // 引擎定位是"单写者、操作串行", 不支持并发。本 demo 故意用多个 goroutine 同时
  6. // 做 插入 / 全表扫描 / 点查 / 更新, 验证进程内全局串行锁 (accMu) 是否能:
  7. // 1. 不 panic、不死锁、不报错;
  8. // 2. 最终数据完整: 行数精确、主键无重复无缺失、行不解析错位 (No == 行号)。
  9. //
  10. // 布局:
  11. // - 单事务先插 initRows 行 (pk 1..initRows, No=pk);
  12. // - W 个 writer 并发, 用原子发号器各插 perW 行 (显式主键, No=pk);
  13. // - R 个 reader 并发全扫(校验 No==pk) + 点查;
  14. // - U 个 updater 并发更新 1..initRows 行的 Score (不改变行数 / 主键);
  15. // - 全部结束后做一次权威全扫, 核对行数 / 主键连续唯一 / 无错位。
  16. // =============================================================================
  17. import (
  18. "fmt"
  19. "math/rand"
  20. "os"
  21. "path/filepath"
  22. "sync"
  23. "sync/atomic"
  24. "time"
  25. "git.ycl.pub/yl32940/TsDB2/DB/core"
  26. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  27. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  28. )
  29. const (
  30. initRows = 10000
  31. writers = 8
  32. perW = 2000 // 新增 writers*perW = 16000, 最终 26000
  33. readers = 8
  34. updaters = 4
  35. updPer = 500
  36. readerRounds = 40 // 每个 reader 的全扫轮次: 限制总量, 避免长读无限饿死写者
  37. )
  38. type stats struct {
  39. panics atomic.Int64
  40. readErrors atomic.Int64
  41. writeErrors atomic.Int64
  42. updateErrors atomic.Int64
  43. corruption atomic.Int64 // No != pk / 字段缺失 / 解析错位
  44. readerScans atomic.Int64
  45. pointLookups atomic.Int64
  46. }
  47. func main() {
  48. dir, err := os.MkdirTemp("", "concsafe-")
  49. if err != nil {
  50. panic(err)
  51. }
  52. defer os.RemoveAll(dir)
  53. path := filepath.Join(dir, "t.db")
  54. db, err := core.Open(path)
  55. if err != nil {
  56. panic(err)
  57. }
  58. tbl, err := db.CreateTable("worker", []types.FieldInfo{
  59. {FieldType: types.FieldType_Uint64, FieldName: []byte("No")},
  60. {FieldType: types.FieldType_Float64, FieldName: []byte("Score")},
  61. {FieldType: types.FieldType_String, Flag: types.WidthCodeFor(24), FieldName: []byte("Name")},
  62. })
  63. if err != nil {
  64. panic(err)
  65. }
  66. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  67. panic(err)
  68. }
  69. // ---- 基线: 单事务插 initRows 行 ----
  70. tx := db.BeginTransaction()
  71. for i := 1; i <= initRows; i++ {
  72. if _, err := tbl.InsertRowWithID(uint64(i), []types.FieldValue{
  73. {FieldName: []byte("No"), Value: uint64(i)},
  74. {FieldName: []byte("Score"), Value: float64(i)},
  75. {FieldName: []byte("Name"), Value: fmt.Sprintf("name-%d", i)},
  76. }); err != nil {
  77. panic(err)
  78. }
  79. }
  80. if err := tx.Commit(); err != nil {
  81. panic(err)
  82. }
  83. var st stats
  84. var nextID atomic.Uint64
  85. nextID.Store(initRows)
  86. // safeGo: 统一 recover, 任何 goroutine 内的 panic 都计数而不是炸掉整个进程
  87. safeGo := func(wg *sync.WaitGroup, fn func()) {
  88. wg.Add(1)
  89. go func() {
  90. defer wg.Done()
  91. defer func() {
  92. if r := recover(); r != nil {
  93. st.panics.Add(1)
  94. fmt.Printf(" !! goroutine panic: %v\n", r)
  95. }
  96. }()
  97. fn()
  98. }()
  99. }
  100. var workWG sync.WaitGroup
  101. t0 := time.Now()
  102. // ---- readers: 固定轮次全扫(校验 No==pk) + 点查 1..initRows ----
  103. for r := 0; r < readers; r++ {
  104. safeGo(&workWG, func() {
  105. for i := 0; i < readerRounds; i++ {
  106. err := tbl.Query().ScanWithPKID(func(pk uint64, row []types.FieldValue) bool {
  107. no, ok := uint64(0), false
  108. for _, fv := range row {
  109. if string(fv.FieldName) == "No" {
  110. if v, isU := fv.Value.(uint64); isU {
  111. no, ok = v, true
  112. }
  113. }
  114. }
  115. if !ok || no != pk {
  116. st.corruption.Add(1) // 行解析错位 / 主键与内容不符
  117. }
  118. return true
  119. })
  120. if err != nil {
  121. st.readErrors.Add(1)
  122. } else {
  123. st.readerScans.Add(1)
  124. }
  125. // 点查一个一定存在的基线行
  126. pk := uint64(rand.Intn(initRows) + 1)
  127. if _, gerr := tbl.FindByPK(pk); gerr != nil {
  128. st.readErrors.Add(1)
  129. } else {
  130. st.pointLookups.Add(1)
  131. }
  132. }
  133. })
  134. }
  135. // ---- writers + updaters 并发 ----
  136. for w := 0; w < writers; w++ {
  137. safeGo(&workWG, func() {
  138. for j := 0; j < perW; j++ {
  139. id := nextID.Add(1)
  140. if _, err := tbl.InsertRowWithID(id, []types.FieldValue{
  141. {FieldName: []byte("No"), Value: uint64(id)},
  142. {FieldName: []byte("Score"), Value: float64(id)},
  143. {FieldName: []byte("Name"), Value: fmt.Sprintf("name-%d", id)},
  144. }); err != nil {
  145. st.writeErrors.Add(1)
  146. }
  147. }
  148. })
  149. }
  150. for u := 0; u < updaters; u++ {
  151. safeGo(&workWG, func() {
  152. for k := 0; k < updPer; k++ {
  153. pk := uint64(rand.Intn(initRows) + 1)
  154. err := tbl.UpdateRowByPK(pk, []types.FieldValue{
  155. {FieldName: []byte("Score"), Value: float64(pk) + rand.Float64()},
  156. })
  157. if err != nil {
  158. st.updateErrors.Add(1)
  159. }
  160. }
  161. })
  162. }
  163. workWG.Wait()
  164. if err := db.Commit(); err != nil {
  165. fmt.Printf(" !! 最终提交失败: %v\n", err)
  166. }
  167. // ---- 权威全扫: 行数 / 主键连续唯一 / No==pk ----
  168. expected := initRows + writers*perW
  169. seen := make(map[uint64]struct{}, expected)
  170. finalNoMismatch := 0
  171. finalTotal := 0
  172. _ = tbl.Query().ScanWithPKID(func(pk uint64, row []types.FieldValue) bool {
  173. finalTotal++
  174. if _, dup := seen[pk]; dup {
  175. finalNoMismatch++ // 重复主键
  176. }
  177. seen[pk] = struct{}{}
  178. for _, fv := range row {
  179. if string(fv.FieldName) == "No" {
  180. if v, ok := fv.Value.(uint64); !ok || v != pk {
  181. finalNoMismatch++
  182. }
  183. }
  184. }
  185. return true
  186. })
  187. missing := 0
  188. for i := 1; i <= expected; i++ {
  189. if _, ok := seen[uint64(i)]; !ok {
  190. missing++
  191. }
  192. }
  193. _ = db.Close()
  194. fmt.Println("============ 多 goroutine 并发安全验证 ============")
  195. fmt.Printf("并发负载: %d writer x %d 行, %d reader, %d updater x %d, 耗时 %d ms\n",
  196. writers, perW, readers, updaters, updPer, time.Since(t0).Milliseconds())
  197. fmt.Println("--------------------------------------------------")
  198. fmt.Printf("reader 完成全扫次数 : %d\n", st.readerScans.Load())
  199. fmt.Printf("reader 点查次数 : %d\n", st.pointLookups.Load())
  200. fmt.Printf("扫描中发现错位(No!=pk) : %d\n", st.corruption.Load())
  201. fmt.Printf("read 错误 / write 错误 : %d / %d\n", st.readErrors.Load(), st.writeErrors.Load())
  202. fmt.Printf("update 错误 / panic 总数 : %d / %d\n", st.updateErrors.Load(), st.panics.Load())
  203. fmt.Println("--------------------------------------------------")
  204. fmt.Printf("最终行数 : %d (期望 %d)\n", finalTotal, expected)
  205. fmt.Printf("重复主键 : %d, 缺失主键 : %d, 最终 No 错位 : %d\n", 0, missing, finalNoMismatch)
  206. pass := st.panics.Load() == 0 && st.writeErrors.Load() == 0 && st.updateErrors.Load() == 0 &&
  207. st.readErrors.Load() == 0 && st.corruption.Load() == 0 &&
  208. finalTotal == expected && len(seen) == expected && missing == 0 && finalNoMismatch == 0
  209. fmt.Println("\n【判定】")
  210. if pass {
  211. fmt.Println(" PASS: 多 goroutine 误并发被全局锁串行化 —— 无 panic / 无错误 / 无损坏,")
  212. fmt.Println(" 最终行数精确、主键连续唯一、行无错位。引擎在此用法下数据安全。")
  213. } else {
  214. fmt.Println(" FAIL: 存在 panic / 错误 / 数据损坏, 详见上方计数。")
  215. os.Exit(1)
  216. }
  217. }