在日常开发中,我们经常会遇到高并发的业务场景,比如钱包系统的转账。如何保证并发情况下的数据一致性,是 Go 工程师必须掌握的技能之一。今天我用一个简单的钱包转账例子,带大家看看 Go 中数据竞争是怎么发生的,以及如何用 sync.Mutex
和 sync.RWMutex
来解决。
假设我们实现了一个简单的钱包结构体 Wallet
,并提供了转账方法:
type Wallet struct {
Balance int
}
func (w *Wallet) Transfer(amount int, target *Wallet) {
if w.Balance >= amount {
w.Balance -= amount
target.Balance += amount
}
}
在 main
函数里,我们让两个用户账户各自初始余额 1000,然后模拟 1000 个并发协程,每次从 userA
转账 1 元给 userB
:
func main() {
userA := &Wallet{Balance: 1000}
userB := &Wallet{Balance: 1000}
var wg sync.WaitGroup
wg.Add(1000)
for i := 0; i < 1000; i++ {
go func() {
defer wg.Done()
userA.Transfer(1, userB)
}()
}
wg.Wait()
fmt.Printf("UserA余额: %d\n", userA.Balance)
fmt.Printf("UserB余额: %d\n", userB.Balance)
}
预期结果:
但实际运行多次后,输出往往并不一致,比如:
UserA余额: 312
UserB余额: 1688
为什么会这样?
原因在于:
userA.Balance
和 userB.Balance
w.Balance -= amount
和 target.Balance += amount
并不是原子操作我们可以用 Go 内置的 -race
工具检测:
$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c000114038 by goroutine 14:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:13 +0x78
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Previous write at 0x00c000114038 by goroutine 8:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:14 +0x9c
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Goroutine 14 (running) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
Goroutine 8 (finished) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
==================
==================
WARNING: DATA RACE
Read at 0x00c000114038 by goroutine 14:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:14 +0x8c
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Previous write at 0x00c000114038 by goroutine 24:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:14 +0x9c
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Goroutine 14 (running) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
Goroutine 24 (finished) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
==================
==================
WARNING: DATA RACE
Read at 0x00c000114048 by goroutine 14:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:15 +0xb8
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Previous write at 0x00c000114048 by goroutine 24:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:15 +0xc8
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Goroutine 14 (running) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
Goroutine 24 (finished) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
==================
==================
WARNING: DATA RACE
Write at 0x00c000114038 by goroutine 18:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:14 +0x9c
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Previous write at 0x00c000114038 by goroutine 30:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:14 +0x9c
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Goroutine 18 (running) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
Goroutine 30 (running) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
==================
==================
WARNING: DATA RACE
Write at 0x00c000114048 by goroutine 32:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:15 +0xc8
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Previous write at 0x00c000114048 by goroutine 25:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:15 +0xc8
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Goroutine 32 (running) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
Goroutine 25 (finished) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
==================
UserA余额: 92
UserB余额: 1769
Found 5 data race(s)
exit status 66
输出会提示存在 DATA RACE,验证了我们的推断。
sync.Mutex
最直接的办法就是用 互斥锁 (sync.Mutex
) 保护共享数据,确保在同一时间只有一个 goroutine 能执行转账操作。
type Wallet struct {
Balance int
mu sync.Mutex
}
func (w *Wallet) Transfer(amount int, target *Wallet) {
w.mu.Lock()
defer w.mu.Unlock()
if w.Balance >= amount {
w.Balance -= amount
target.mu.Lock()
target.Balance += amount
target.mu.Unlock()
}
}
这样,每次转账都必须拿到锁,保证操作的完整性。
再次运行程序,结果稳定为:
UserA余额: 0
UserB余额: 2000
问题解决。
sync.RWMutex
但是,如果钱包的读操作(比如查询余额)非常频繁,而写操作相对较少,这时使用 sync.Mutex
会导致读操作也被阻塞,降低整体性能。
Go 提供了 读写锁 sync.RWMutex
:
RLock
/ RUnlock
)Lock
/ Unlock
)修改代码如下:
type Wallet struct {
Balance int
mu sync.RWMutex
}
func (w *Wallet) Transfer(amount int, target *Wallet) {
w.mu.Lock()
defer w.mu.Unlock()
if w.Balance >= amount {
w.Balance -= amount
target.mu.Lock()
target.Balance += amount
target.mu.Unlock()
}
}
func (w *Wallet) GetBalance() int {
w.mu.RLock()
defer w.mu.RUnlock()
return w.Balance
}
这样:
通过这个钱包转账的例子,我们可以总结 Go 并发下的几个关键点:
-race
检测工具来排查Go 并发编程的核心是对共享资源的正确管理,合理使用 Mutex 和 RWMutex,才能写出既安全又高效的代码。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。