在前文我们已经用 Mutex 和 RWMutex 解决了竞态问题。但是在实际生产中,锁并不是唯一解,甚至在高并发场景下可能不是最佳解。这里我们来探索更多可能性:
Go 的哲学是:
“不要通过共享内存来通信,而要通过通信来共享内存。”
与其多个 goroutine 同时修改一个共享变量,不如把这个变量封装在一个 goroutine 内,通过 channel 接收消息来修改。这样能彻底避免数据竞争。
在下面的例子里,每个钱包就是一个“Actor”,只处理发给它的消息。
type Wallet struct {
balance int
ops chan func(*Wallet)
}
func NewWallet(balance int) *Wallet {
w := &Wallet{
balance: balance,
ops: make(chan func(*Wallet)),
}
go w.loop()
return w
}
func (w *Wallet) loop() {
for op := range w.ops {
op(w)
}
}
func (w *Wallet) Transfer(amount int, target *Wallet) {
done := make(chan struct{})
w.ops <- func(ws *Wallet) {
if ws.balance >= amount {
ws.balance -= amount
target.ops <- func(ts *Wallet) {
ts.balance += amount
close(done)
}
} else {
close(done)
}
}
<-done
}优点:
缺点:
如果逻辑只是简单的数值加减,可以用 Go 的 sync/atomic 包直接实现,避免锁的开销。
import "sync/atomic"
type Wallet struct {
Balance int64
}
func (w *Wallet) Transfer(amount int64, target *Wallet) {
if atomic.LoadInt64(&w.Balance) >= amount {
atomic.AddInt64(&w.Balance, -amount)
atomic.AddInt64(&target.Balance, amount)
}
}优点:
缺点:
实际的“钱包”业务,往往会持久化到数据库(MySQL、Postgres)。
这时,应用层锁已经不够,需要数据库层事务保证:
version 字段,更新时带条件:UPDATE wallet SET balance=balance-100, version=version+1
WHERE id=1 AND version=10;如果返回 0 行,说明有冲突,需要重试。INCRBY、DECRBY,数据库的 UPDATE ... SET balance=balance+? 都是原子操作,适合计数场景。优点:
缺点:
在多实例部署的钱包服务中,单机锁无法跨节点工作,这时可以使用分布式锁。
常见实现:
应用场景:
比如两个微服务实例同时尝试修改同一用户余额,需要在分布式层面保证互斥。
如果转账请求非常频繁,可以引入异步队列 + 批处理机制,而不是一笔笔实时处理。
这种方式在 交易所撮合引擎、支付网关 中很常见,能显著提高吞吐。
实际生产环境往往不是单一技术,而是组合拳:
解决 Go 并发下的共享资源问题,常见几类方案对比如下:
方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
Mutex / RWMutex | 简单直观,易用 | 可能阻塞,扩展性差 | 小规模并发,快速实现 |
Channel (Actor) | Go 风格,无锁化,天然避免竞态 | 内存/调度开销,逻辑复杂 | 钱包/账户模型,高并发逻辑 |
Atomic | 极致性能,适合简单加减 | 无法处理复杂逻辑 | 简单计数、统计场景 |
数据库事务/乐观锁 | 跨服务安全,结合持久化 | 重试成本高,DB 可能瓶颈 | 钱包、资金业务核心场景 |
分布式锁 | 多节点互斥,支持微服务架构 | 复杂度高,需考虑可靠性 | 集群/微服务共享资源 |
批处理/队列化 | 提升吞吐,削峰填谷 | 延迟增加,架构复杂 | 高频交易、支付、结算 |
在 Go 并发编程中,没有“万能方案”。锁是最直观的起点,但更优的解决方案往往要结Channel、事务、分布式锁、批处理等技术,根据业务特点灵活选型。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。