file_lock_windows.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. //go:build windows
  2. package core
  3. // Windows 跨进程文件锁: 用 kernel32 的 LockFileEx / UnlockFileEx 对文件末尾之后
  4. // 的"虚拟字节区"加锁 (不覆盖任何有效页, 见 tryLock 注释)。纯标准库。
  5. import (
  6. "syscall"
  7. "unsafe"
  8. )
  9. // 锁定区域长度: 锁 [fileSize, fileSize+1) —— 跨进程互斥, 不碰头页/数据页/WAL 页。
  10. // 历史坑: 原设计锁偏移=0 直接锁头页 (页 0), Windows NTFS 页粒度锁 (4KB) 会让
  11. // 任何 Pread 页 0 返回零数据, 连锁持有方自己 ReadAt 也中招 —— 引擎 ReadHeader
  12. // 拿到 FirstWALPage=0, RecoverWAL 整个空转。
  13. // 实际值见 const.go FileLockByteCount。
  14. const (
  15. winLockFileExclusive = 0x00000002 // LOCKFILE_EXCLUSIVE_LOCK
  16. winLockFileFailImmediately = 0x00000001 // LOCKFILE_FAIL_IMMEDIATELY (非阻塞)
  17. winErrLockViolation = 33 // ERROR_LOCK_VIOLATION
  18. )
  19. var (
  20. kernel32 = syscall.NewLazyDLL("kernel32.dll")
  21. procLockFileEx = kernel32.NewProc("LockFileEx")
  22. procUnlockFileEx = kernel32.NewProc("UnlockFileEx")
  23. )
  24. // overlappedFromOffset 根据 64 位文件偏移构造 LockFileEx 用的 OVERLAPPED。
  25. func overlappedFromOffset(offset uint64) *syscall.Overlapped {
  26. var ol syscall.Overlapped
  27. ol.Offset = uint32(offset & 0xFFFFFFFF)
  28. ol.OffsetHigh = uint32(offset >> 32)
  29. return &ol
  30. }
  31. func (ff *File) winHandle() (syscall.Handle, error) {
  32. sc, err := ff.f.SyscallConn()
  33. if err != nil {
  34. return 0, err
  35. }
  36. var h syscall.Handle
  37. if err := sc.Control(func(fd uintptr) {
  38. h = syscall.Handle(fd)
  39. }); err != nil {
  40. return 0, err
  41. }
  42. return h, nil
  43. }
  44. // fileSize 返回文件当前大小 (用于计算锁偏移)。
  45. func (ff *File) fileSize() (int64, error) {
  46. st, err := ff.f.Stat()
  47. if err != nil {
  48. return 0, err
  49. }
  50. return st.Size(), nil
  51. }
  52. // tryLock 非阻塞地对文件末尾之后的虚拟字节加排他锁; 已被其它进程/句柄锁定时返回 ErrDatabaseLocked。
  53. func (ff *File) tryLock() error {
  54. if ff.fileLocked {
  55. return nil
  56. }
  57. size, err := ff.fileSize()
  58. if err != nil {
  59. return err
  60. }
  61. // 锁 [EOF, EOF+1) —— 所有有效页都在 <EOF, 所以锁区间永远不重叠
  62. lockStart := uint64(size)
  63. h, err := ff.winHandle()
  64. if err != nil {
  65. return err
  66. }
  67. ol := overlappedFromOffset(lockStart)
  68. r1, _, e1 := syscall.Syscall6(procLockFileEx.Addr(), 6,
  69. uintptr(h),
  70. uintptr(winLockFileExclusive|winLockFileFailImmediately),
  71. 0,
  72. uintptr(FileLockByteCount),
  73. 0,
  74. uintptr(unsafe.Pointer(ol)))
  75. if r1 == 0 {
  76. if e1 == syscall.Errno(winErrLockViolation) {
  77. return ErrDatabaseLocked
  78. }
  79. return e1
  80. }
  81. ff.fileLocked = true
  82. ff.lockOffset = lockStart // 保存偏移 —— unlock 必须用完全相同的区间
  83. return nil
  84. }
  85. // tryRLock 非阻塞地对文件加【共享锁】(只读打开): 不加 EXCLUSIVE 标志。
  86. // 共享锁之间可共存, 与排他锁互斥; 冲突返回 ErrDatabaseLocked。
  87. func (ff *File) tryRLock() error {
  88. if ff.fileLocked {
  89. return nil
  90. }
  91. size, err := ff.fileSize()
  92. if err != nil {
  93. return err
  94. }
  95. lockStart := uint64(size)
  96. h, err := ff.winHandle()
  97. if err != nil {
  98. return err
  99. }
  100. ol := overlappedFromOffset(lockStart)
  101. r1, _, e1 := syscall.Syscall6(procLockFileEx.Addr(), 6,
  102. uintptr(h),
  103. uintptr(winLockFileFailImmediately), // 共享锁 (无 LOCKFILE_EXCLUSIVE_LOCK)
  104. 0,
  105. uintptr(FileLockByteCount),
  106. 0,
  107. uintptr(unsafe.Pointer(ol)))
  108. if r1 == 0 {
  109. if e1 == syscall.Errno(winErrLockViolation) {
  110. return ErrDatabaseLocked
  111. }
  112. return e1
  113. }
  114. ff.fileLocked = true
  115. ff.lockOffset = lockStart
  116. return nil
  117. }
  118. // unlock 释放文件锁 (Close 路径调用; 错误忽略 —— 句柄关闭同样会释放)。
  119. // 必须用与 tryLock 时完全相同的偏移区间 (Windows LockFileEx 解锁不支持通配符)。
  120. func (ff *File) unlock() {
  121. if !ff.fileLocked {
  122. return
  123. }
  124. if h, err := ff.winHandle(); err == nil {
  125. ol := overlappedFromOffset(ff.lockOffset)
  126. _, _, _ = syscall.Syscall6(procUnlockFileEx.Addr(), 5,
  127. uintptr(h),
  128. 0,
  129. uintptr(FileLockByteCount),
  130. 0,
  131. uintptr(unsafe.Pointer(ol)),
  132. 0)
  133. }
  134. ff.fileLocked = false
  135. ff.lockOffset = 0
  136. }