欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 培训 > 7天用Go从零实现分布式缓存GeeCache(学习)(4)上锁总结

7天用Go从零实现分布式缓存GeeCache(学习)(4)上锁总结

2025/2/24 12:37:04 来源:https://blog.csdn.net/weixin_51147313/article/details/143658944  浏览:    关键词:7天用Go从零实现分布式缓存GeeCache(学习)(4)上锁总结

Go 语言锁机制与代码中加锁的详细说明

Go 语言提供了多种锁机制来控制并发访问,以确保数据的一致性和安全性。本文将首先介绍 Go 中常用的几种锁机制,然后详细说明您的代码中加锁的地方、加锁的目的,以及锁是如何确保线程安全的。最后,将这两部分内容进行整理和合并,帮助您更全面地理解 Go 语言的并发控制和代码实现。


一、Go 语言中的锁机制

Go 语言的 sync 包提供了多种用于并发控制的锁机制,以下是常用的几种:

1. sync.Mutex(互斥锁)

  • 用途:控制对共享资源的独占访问,只允许一个 goroutine 持有锁,防止数据竞争(race condition)。
  • 使用方法:调用 Lock() 加锁,Unlock() 解锁。
  • 适用场景:适用于需要完全互斥的场景,例如对共享变量的写操作。

示例

var mu sync.Mutexfunc increment() {mu.Lock()counter++mu.Unlock()
}

2. sync.RWMutex(读写锁)

  • 用途:允许多个 goroutine 同时读取,但写操作是独占的,能提高读多写少场景的性能。
  • 使用方法
    • RLock() 获取读锁,RUnlock() 释放读锁;
    • Lock() 获取写锁,Unlock() 释放写锁。
  • 适用场景:适用于读多写少的场景,例如缓存、配置文件等。

示例

var mu sync.RWMutexfunc read() {mu.RLock()defer mu.RUnlock()fmt.Println(counter)
}func write() {mu.Lock()defer mu.Unlock()counter++
}

3. sync.Once(单次锁)

  • 用途:确保某些初始化操作只执行一次。
  • 使用方法:调用 Do(func),传入的函数只会执行一次,无论多少 goroutine 调用 Do
  • 适用场景:适用于单次初始化的场景,例如单例模式或仅初始化一次的资源。

示例

var once sync.Oncefunc initialize() {once.Do(func() {fmt.Println("Initializing...")})
}

4. sync.Cond(条件变量)

  • 用途:用于协调多个 goroutine 的等待和通知操作,通常配合 Mutex 使用。
  • 使用方法
    • Wait() 等待条件满足并自动释放锁;
    • Signal() 唤醒一个等待的 goroutine;
    • Broadcast() 唤醒所有等待的 goroutine。
  • 适用场景:适用于需要等待条件的场景,例如生产者-消费者模型。

示例

var mu sync.Mutex
var cond = sync.NewCond(&mu)func waitForCondition() {cond.L.Lock()cond.Wait()  // 等待通知fmt.Println("Condition met")cond.L.Unlock()
}func signalCondition() {cond.L.Lock()cond.Signal()  // 通知一个等待中的 goroutinecond.L.Unlock()
}

5. sync.Map(并发安全的 Map)

  • 用途:实现并发安全的键值对存储,适合高并发环境下的读写操作。
  • 使用方法:提供 StoreLoadDeleteRange 等方法来操作 map。
  • 适用场景:适用于大量的读写操作,例如缓存。

示例

var m sync.Mapfunc main() {m.Store("key", "value")if v, ok := m.Load("key"); ok {fmt.Println(v)}m.Delete("key")
}

6. sync.WaitGroup(等待组)

  • 用途:用于等待一组并发操作完成。
  • 使用方法
    • Add(n) 添加要等待的 goroutine 数量;
    • Done() 表示某个 goroutine 完成;
    • Wait() 阻塞直到所有 goroutine 完成。
  • 适用场景:适用于需要等待多个 goroutine 完成的场景,例如并发任务的汇总。

示例

var wg sync.WaitGroupfunc worker() {defer wg.Done()fmt.Println("Working...")
}func main() {wg.Add(2)go worker()go worker()wg.Wait()  // 等待所有 worker 完成
}

7. sync/atomic 包(原子操作)

  • 用途:提供底层的原子操作,避免使用锁,但依赖于硬件的原子指令。
  • 使用方法atomic.AddInt32atomic.LoadInt32atomic.StoreInt32 等。
  • 适用场景:适合简单计数、状态切换等轻量级并发场景,避免锁开销。

示例

import "sync/atomic"var counter int32func increment() {atomic.AddInt32(&counter, 1)
}

二、代码中加锁的地方详细说明

1. cache.go 中的加锁

1.1 cache 结构体中的互斥锁

文件geecache/cache.go

type cache struct {mu         sync.Mutex // 互斥锁,保护 lru 和 cacheBytes 的并发访问lru        *lru.Cache // LRU 缓存实例cacheBytes int64      // 缓存的最大字节数
}
  • 加锁目的:保护 lru 缓存和 cacheBytes 字段的并发访问,确保在多协程环境下对缓存的操作是安全的。
1.2 cache 的方法中使用锁
1.2.1 add 方法
func (c *cache) add(key string, value ByteView) {c.mu.Lock()         // 加锁,保护对 lru 的并发访问defer c.mu.Unlock() // 函数退出时解锁// 延迟初始化 lru 缓存if c.lru == nil {c.lru = lru.New(c.cacheBytes, nil)}c.lru.Add(key, value) // 向 lru 缓存中添加数据
}
  • 加锁位置:在方法开始时调用 c.mu.Lock(),在方法结束时使用 defer c.mu.Unlock() 解锁。
  • 加锁目的
    • 确保对 lru 缓存的访问是线程安全的。
    • 防止多个协程同时初始化 lru,导致竞态条件(race condition)。
    • 保护 lru.Add 的调用,因为 lru.Cache 本身不支持并发访问。
1.2.2 get 方法
func (c *cache) get(key string) (value ByteView, ok bool) {c.mu.Lock()         // 加锁,保护对 lru 的并发访问defer c.mu.Unlock() // 函数退出时解锁if c.lru == nil {return}if v, ok := c.lru.Get(key); ok {return v.(ByteView), ok}return
}
  • 加锁位置:同样在方法开始时加锁,结束时解锁。
  • 加锁目的
    • 保护对 lru 缓存的读取操作。
    • 防止并发情况下多个协程同时访问未初始化的 lru,导致空指针异常或其他竞态问题。
    • 确保 lru.Get 的操作是线程安全的。
1.3 lru.Cache 不支持并发访问
  • 原因lru.Cache 内部使用了 Go 标准库的 container/list,该数据结构不是并发安全的。
  • 解决方案:在 cache 结构体中使用互斥锁 mu 来保护对 lru 的所有访问,包括读和写操作。

2. singleflight.go 中的加锁

2.1 Group 结构体中的互斥锁

文件geecache/singleflight/singleflight.go

type Group struct {mu sync.Mutex       // 互斥锁,保护 m 的并发访问m  map[string]*call // 存储正在进行的请求
}
  • 加锁目的:保护 m 字典的并发访问,防止多个协程同时对其进行读写操作。
2.2 Group 的方法中使用锁
2.2.1 Do 方法
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {g.mu.Lock()if g.m == nil {g.m = make(map[string]*call)}if c, ok := g.m[key]; ok {g.mu.Unlock()c.wg.Wait()return c.val, c.err}c := new(call)c.wg.Add(1)g.m[key] = cg.mu.Unlock()c.val, c.err = fn()c.wg.Done()g.mu.Lock()delete(g.m, key)g.mu.Unlock()return c.val, c.err
}
  • 加锁位置

    • 第一次加锁:方法开始时,保护对 g.m 的访问,包括初始化和查找。
    • 第一次解锁:如果发现已有相同的请求在进行中,立即解锁,等待已有请求完成。
    • 第二次加锁:在请求完成后,再次加锁,删除 g.m 中对应的 key
    • 第二次解锁:删除后立即解锁。
  • 加锁目的

    • 第一次加锁
      • 确保对 g.m 的初始化和查找是线程安全的。
      • 防止多个协程同时对 g.m 进行修改,导致竞态条件。
    • 第二次加锁
      • 确保从 g.m 中删除已完成的请求时,不会与其他协程发生冲突。
  • 锁的粒度控制

    • 在持有锁期间,只执行必要的操作,尽快解锁,减少锁的持有时间,提高并发性能。
2.3 等待组 sync.WaitGroup
  • 用途:使用 c.wgsync.WaitGroup)让等待的协程阻塞,直到请求完成,避免重复请求。
  • 与锁的配合
    • 锁用于保护对共享资源 g.m 的访问。
    • WaitGroup 用于协调请求的执行和等待,避免协程忙等或重复执行。

3. http.go 中的加锁

3.1 HTTPPool 结构体中的互斥锁

文件geecache/http.go

type HTTPPool struct {self        string                 // 当前节点的地址basePath    string                 // HTTP 请求的基础路径mu          sync.Mutex             // 互斥锁,保护 peers 和 httpGetters 的并发访问peers       *consistenthash.Map    // 一致性哈希环httpGetters map[string]*httpGetter // 远程节点的 HTTP 客户端映射
}
  • 加锁目的:保护 peershttpGetters 的并发访问,确保节点列表的更新和读取是线程安全的。
3.2 HTTPPool 的方法中使用锁
3.2.1 Set 方法
func (p *HTTPPool) Set(peers ...string) {p.mu.Lock()defer p.mu.Unlock()p.peers = consistenthash.New(defaultReplicas, nil)p.peers.Add(peers...)p.httpGetters = make(map[string]*httpGetter, len(peers))for _, peer := range peers {p.httpGetters[peer] = &httpGetter{baseURL: peer + p.basePath}}
}
  • 加锁位置:方法开始时加锁,方法结束时通过 defer 解锁。
  • 加锁目的
    • 确保对 peershttpGetters 的更新是原子性的,防止在更新过程中其他协程读取到不完整的数据。
    • 防止多个协程同时调用 Set 方法,导致数据竞争。
3.2.2 PickPeer 方法
func (p *HTTPPool) PickPeer(key string) (PeerGetter, bool) {p.mu.Lock()defer p.mu.Unlock()if peer := p.peers.Get(key); peer != "" && peer != p.self {p.Log("Pick peer %s", peer)return p.httpGetters[peer], true}return nil, false
}
  • 加锁位置:方法开始时加锁,方法结束时通过 defer 解锁。
  • 加锁目的
    • 保护对 peershttpGetters 的并发访问,确保读取到的节点信息是一致的。
    • 防止在读取节点信息时,另一个协程正在修改节点列表,导致数据不一致或程序崩溃。
3.3 consistenthash.Map 不支持并发访问
  • 原因consistenthash.Map 没有内部的并发控制机制,直接对其进行并发访问可能导致竞态条件。
  • 解决方案:在 HTTPPool 中使用互斥锁 mu 来保护对 consistenthash.Map 的访问。

4. 其他需要注意的地方

4.1 Group 结构体中的并发控制

文件geecache/geecache.go

type Group struct {name      string              // 缓存组的名称getter    Getter              // 加载数据的回调接口mainCache cache               // 并发安全的本地缓存peers     PeerPicker          // 节点选择器loader    *singleflight.Group // 防止缓存击穿的请求合并
}
  • 并发控制方式
    • 对于本地缓存 mainCache,通过其内部的互斥锁 mu 进行保护。
    • 对于防止缓存击穿,使用了 singleflight.Group,避免并发情况下的重复请求。
    • Group 本身没有使用互斥锁,因为其内部的关键部分都已经由各自的组件进行了并发控制。
4.2 lru.Cache 的并发访问
  • 说明lru.Cache 本身不支持并发访问,需要由外部进行并发控制。
  • 解决方案:在 cache 结构体中使用互斥锁 mu 保护对 lru.Cache 的访问。
4.3 consistenthash.Map 的并发访问
  • 说明consistenthash.Map 没有内部的并发控制,需要在外部加锁。
  • 解决方案:在使用 consistenthash.Map 的地方,如 HTTPPool 中,使用互斥锁 mu 保护对其的访问。

5. 加锁机制如何确保线程安全

5.1 互斥锁 sync.Mutex
  • 工作原理:互斥锁是一种用于保护共享资源的锁机制。在同一时刻,只有一个 goroutine 能够获得互斥锁,从而独占地访问被保护的资源。
  • 在代码中的作用
    • 防止竞态条件:当多个协程同时读写共享资源时,可能会发生竞态条件,导致数据不一致或程序崩溃。通过互斥锁,可以确保共享资源的访问是互斥的,防止竞态发生。
    • 保护临界区:临界区是指对共享资源进行访问的代码片段。在进入临界区之前加锁,退出时解锁,确保临界区内的代码不会被多个协程同时执行。
5.2 加锁的粒度控制
  • 细粒度加锁:在代码中,尽量缩小锁的持有时间,只在需要保护的代码段内持有锁,其他时间尽快解锁,提高并发性能。
  • 避免死锁:在加锁的过程中,注意锁的顺序,避免在多个锁之间形成循环等待,导致死锁。

三、整理和合并

结合以上内容,我们可以总结如下:

  1. Go 语言提供了多种锁机制,包括 sync.Mutexsync.RWMutexsync.Oncesync.Condsync.Mapsync.WaitGroupsync/atomic 包等,用于不同的并发场景。

  2. 在您的代码中,主要使用了 sync.Mutexsync.WaitGroup

    • sync.Mutex 用于保护共享资源的访问,防止竞态条件。
    • sync.WaitGroup 用于等待并发操作完成,防止重复请求。
  3. 代码中加锁的地方

    • cache.go 中的 cache 结构体:使用 sync.Mutex 保护对 lru.Cache 的并发访问,因为 lru.Cache 本身不支持并发。
    • singleflight.go 中的 Group 结构体:使用 sync.Mutexsync.WaitGroup 防止并发情况下的重复请求。
    • http.go 中的 HTTPPool 结构体:使用 sync.Mutex 保护对节点列表和客户端映射的并发访问。
  4. 锁的使用方式和目的

    • 互斥锁 sync.Mutex:在需要对共享资源进行读写操作的地方加锁,确保线程安全。
    • 等待组 sync.WaitGroup:在需要等待多个协程完成操作的地方使用,避免重复请求或提前退出。
  5. 加锁机制确保线程安全的方式

    • 防止竞态条件:通过锁机制,确保同一时间只有一个协程访问共享资源,防止数据不一致。
    • 提高并发性能:通过细粒度加锁,减少锁的持有时间,避免不必要的阻塞。
  6. 整体策略

    • 在需要保护共享资源的地方使用锁,例如对缓存、节点列表等的访问。
    • 在持有锁期间,只执行必要的操作,尽快解锁,提高系统的并发性能。
    • 合理选择锁的类型,根据场景选择合适的锁机制,例如读多写少的场景可以考虑使用 sync.RWMutex

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词