updateWithOptLock_concurrent_audit_test.go 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. package dept
  2. import (
  3. "context"
  4. "errors"
  5. "sync"
  6. "sync/atomic"
  7. "testing"
  8. "time"
  9. "perms-system-server/internal/testutil"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/require"
  12. )
  13. // TC-0759: M-5 修复实战回归 —— UpdateWithOptLock 在 10 个 goroutine 同时写同一行时,
  14. // 必须精确有 **1 个成功、9 个 ErrUpdateConflict**。
  15. // 旧实现依赖应用层 "先读后写", 高并发下两个 goroutine 都拿到 UpdateTime=t0 就都会写入成功
  16. // (最后一个覆盖前一个, 数据静默丢失). 新实现 WHERE updateTime=? 才更新, affected=0 即冲突。
  17. // 这是"真实并发"断言, 不是 mock, 踩在实际 MySQL 上, 为乐观锁修复提供最强的防退化护栏。
  18. func TestSysDeptModel_UpdateWithOptLock_ConcurrentSingleWinner(t *testing.T) {
  19. ctx := context.Background()
  20. conn := testutil.GetTestSqlConn()
  21. m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  22. base := time.Now().Unix()
  23. row := &SysDept{
  24. ParentId: 0,
  25. Name: "dept_optlock_" + testutil.UniqueId(),
  26. Path: "/optlock/" + testutil.UniqueId() + "/",
  27. Sort: 10,
  28. Remark: "orig",
  29. Status: 1,
  30. CreateTime: base,
  31. UpdateTime: base,
  32. }
  33. res, err := m.Insert(ctx, row)
  34. require.NoError(t, err)
  35. id, err := res.LastInsertId()
  36. require.NoError(t, err)
  37. tbl := m.TableName()
  38. t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, id) })
  39. const workers = 10
  40. var (
  41. wg sync.WaitGroup
  42. success int32
  43. conflicts int32
  44. other int32
  45. start = make(chan struct{})
  46. )
  47. for i := 0; i < workers; i++ {
  48. wg.Add(1)
  49. go func(idx int) {
  50. defer wg.Done()
  51. <-start
  52. err := m.UpdateWithOptLock(ctx, &SysDept{
  53. Id: id,
  54. ParentId: 0,
  55. Name: row.Name,
  56. Path: row.Path,
  57. Sort: int64(idx),
  58. Remark: "w" + testutil.UniqueId(),
  59. DeptType: "NORMAL",
  60. Status: 1,
  61. CreateTime: base,
  62. UpdateTime: base + int64(idx+1),
  63. }, base)
  64. switch {
  65. case err == nil:
  66. atomic.AddInt32(&success, 1)
  67. case errors.Is(err, ErrUpdateConflict):
  68. atomic.AddInt32(&conflicts, 1)
  69. default:
  70. atomic.AddInt32(&other, 1)
  71. t.Errorf("unexpected error: %v", err)
  72. }
  73. }(i)
  74. }
  75. close(start)
  76. wg.Wait()
  77. assert.Equal(t, int32(1), atomic.LoadInt32(&success),
  78. "M-5: 10 个并发写必须且仅有 1 个成功, 实际 %d", success)
  79. assert.Equal(t, int32(workers-1), atomic.LoadInt32(&conflicts),
  80. "M-5: 其余 goroutine 必须全部得到 ErrUpdateConflict (无声覆盖即 BUG)")
  81. assert.Equal(t, int32(0), atomic.LoadInt32(&other),
  82. "不应出现除成功/冲突外的其他错误")
  83. after, err := m.FindOne(ctx, id)
  84. require.NoError(t, err)
  85. assert.NotEqual(t, base, after.UpdateTime,
  86. "成功的那一个必须把 UpdateTime 推进, DB 里不允许停留在初值")
  87. assert.Equal(t, "orig", row.Remark)
  88. assert.NotEqual(t, "orig", after.Remark, "胜出者必须把 Remark 更新为 w*")
  89. }