Go 中的 Lock使用?

锁(Lock)是并发编程中用于保护共享资源、防止数据竞争(data race)的关键同步原语。 1.sync.Mutex (互斥锁) 特点 同一时间只允许一个 goroutine 持有锁。 不可重入(reentrant):同一个 goroutine 再次调用 Lock() 会导致死锁。 var mu sync.Mutex var count int func increment() { mu.Lock() defer mu.Unlock() count++ } 注意 必须配对使用:每个 Lock() 必须对应一个 Unlock()。 避免在持有锁时进行阻塞操作(如 I/O、channel 发送/接收),否则会降低并发性能。 不要复制已使用的 Mutex(因为其内部状态不可复制)。 2.sync.RWMutex 读写锁 特点 支持 多个 reader 或 一个 writer。 读锁(RLock):允许多个 goroutine 同时读。 写锁(Lock):独占,阻塞所有读和其他写。 适用于“读多写少”的场景。 var rwmu sync.RWMutex var cache map[string]string // 读操作 func get(key string) string { rwmu.RLock() defer rwmu.RUnlock() return cache[key] } // 写操作 func set(key, value string) { rwmu.Lock() defer rwmu.Unlock() cache[key] = value } 注意 写锁优先级问题:某些实现中,持续的读请求可能“饿死”写请求(Go 的 RWMutex 在写等待时会阻止新读者进入,缓解此问题)。 不要在读锁内修改共享数据!仅用于读取。 和 Mutex 一样,不可重入、不可复制。 3.sync.Map 并发安全的map 特点 内置并发安全的 map,内部使用分段锁或原子操作优化。 适用于读远多于写、key 集合不固定(频繁增删)的场景。 不是通用替代品,仅在特定场景比 map + RWMutex 更高效。 var m sync.Map m.Store("key", "value") if v, ok := m.Load("key"); ok { fmt.Println(v) } 注意 接口类型:key 和 value 都是 interface{},有类型转换开销。 不支持遍历快照:Range 回调期间 map 可能被修改。 不适合复杂操作(如“读-改-写”原子操作),此时仍需配合锁。 4.原子操作(sync/atomic)—— 无锁 特点 对简单类型(int32/int64/pointer 等)提供原子读写、CAS(Compare-And-Swap)。 性能极高,无 goroutine 阻塞。 适用于计数器、标志位等简单状态。 var counter int64 // 原子自增 atomic.AddInt64(&counter, 1) // 原子加载 val := atomic.LoadInt64(&counter) 注意 仅适用于对齐的、简单内存操作。 不能用于保护复杂数据结构(如 slice、struct 字段组合)。 5.使用注意 锁粒度控制 尽量缩小临界区,只保护真正需要同步的代码。 ...

2025年12月9日 · Mumu

了解分布式锁吗?

分布式锁是分布式系统从“松散运行”走向“协作、可靠运行”的关键桥梁。 1.什么是分布式锁? 分布式锁是用于在分布式系统中协调多个进程或线程访问共享资源的一种机制,确保在任何时刻只有一个客户端能够操作特定的资源,从而保证数据的一致性。 2.分布式锁的基本要素 无论是基于 Redis、ZooKeeper 还是数据库实现,一个可靠的分布式锁必须满足三个要素: *加锁(Lock):在资源上设置一个锁的标记。 *设置过期时间(TTL):防止客户端宕机导致锁无法释放,造成死锁。这是分布式锁与本地锁(如 Java 的 ReentrantLock)最核心的区别。 *释放锁(Unlock):客户端完成操作后,安全地移除锁。 3.分布式锁面临问题和解决 3.1 死锁 如果客户端在获取锁后,由于某种原因(例如,程序崩溃)未能正常释放锁,导致锁一直被占用,其他客户端无法获取锁,造成死锁。解决方案:在加锁的同时设置过期时间,即使客户端未能正常释放锁,锁也会在过期后自动释放。或者使用 Redlock 算法,提高锁的可靠性,防止死锁。 3.2 锁的误删 -如果客户端 A 获取锁后,由于执行时间过长,导致锁过期自动释放。此时,客户端 B 获取了锁。然后,客户端 A 执行完业务逻辑后,尝试释放锁,但实际上释放的是客户端 B 的锁,造成锁的误删除。解决方案:在加锁时,将锁的值设置为一个唯一标识(例如,UUID),在释放锁时,先判断锁的值是否与自己的唯一标识相等,如果相等,则释放锁;否则,不释放锁。此过程要保证原子性,可以使用 Lua 脚本实现。 -在网络分区的情况下,可能会导致多个进程同时认为自己持有锁。解决方案:在获取锁时生成一个唯一的 UUID,并将其存储在锁的 Key 中。在释放锁时,先检查当前存储的 UUID 是否与自己的 UUID 匹配,只有匹配时才释放锁。 3.3 锁的续期 如果客户端在加锁后,执行时间超过了锁的过期时间,导致锁被自动释放。此时,其他客户端可能会获取锁,造成并发问题。解决方案:客户端在获取锁后,启动一个后台线程,定期检查锁的剩余时间,如果剩余时间小于一定阈值,则使用 EXPIRE 命令续期锁的过期时间。此流程可以自己实现,也可以使用开源框架,例如Redisson 框架不仅提供了自动续期的功能,还可以简化分布式锁的实现。 3.4 锁的竞争 在高并发场景下,多个进程可能会同时竞争锁,导致锁的获取失败率较高。解决方案:可以使用随机退避重试策略,在获取锁失败后,随机等待一段时间后再次重试。 3.5 锁的重入性 如果同一个进程多次尝试获取锁,可能会导致锁的获取失败。解决方案:在锁的 Key 中存储一个计数器,表示当前进程获取锁的次数。每次获取锁时增加计数器,释放锁时减少计数器,只有计数器为 0 时才删除锁的 Key。 3.6 锁的公平性 多个进程同时请求锁时,可能会出现“饥饿”现象,某些进程长时间无法获取锁。解决方案:可以使用 Redis 的 List 数据结构实现排队机制,确保请求锁的进程按照顺序获取锁。或者使用成熟的分布式锁实现库,如 Redisson,它提供了公平锁和可重入锁等功能。

2025年10月14日 · Mumu

什么是“重锁” Heavy Lock?

1.什么是“重锁”(Heavy Lock) 在 Go 性能调优或并发编程中,我们常说的 “重锁”(heavy lock)不是官方术语,而是一个工程上的概念,指的是:锁竞争严重、临界区较大、持锁时间较长的互斥锁(sync.Mutex)。 1.多个 goroutine 同时频繁地去争夺同一把锁; 2.加锁的代码块中做了比较“重”的操作(比如 I/O、JSON 编码、数据库操作); 导致 goroutine 阻塞、上下文切换频繁,最终造成性能瓶颈。 2.为什么会出现“重锁”问题 1.临界区太大(锁保护的范围过广); 2.频繁写操作导致锁争用; 3.使用全局变量或共享状态; 4.没有分片(sharding)或局部化锁机制; 5.锁中包含耗时操作(例如网络请求、磁盘 I/O)。 var mu sync.Mutex var cache = make(map[string]string) func Set(k, v string) { mu.Lock() defer mu.Unlock() cache[k] = v } #当高并发调用 Set() 时,所有 goroutine 都在争抢同一把 mu,这就形成“重锁”。 3.优化思路与替代方案 3.1 使用 sync.Map 适用于读多写少的场景: var m sync.Map m.Store("a", 1) v, _ := m.Load("a") #sync.Map 内部采用分片和原子操作,避免了全局锁竞争。 3.2 使用原子操作(sync/atomic) 适用于简单的计数、标志位等操作: var count int64 atomic.AddInt64(&count, 1) #无锁化操作,性能更高,且不阻塞其他 goroutine。 3.3 优化锁粒度(细化锁) 将一把全局锁拆分成多把局部锁: ...

2025年10月14日 · Mumu