[go 面试] 为并发加锁:保障数据一致性(分布式锁)

简介: [go 面试] 为并发加锁:保障数据一致性(分布式锁)

在单机程序中,当多个线程或协程同时修改全局变量时,为了保障数据一致性,我们需要引入锁机制,创建临界区。本文将通过一个简单的例子,说明在不加锁的情况下并发计数可能导致的问题,并介绍加锁的解决方案。


不加锁的并发计数



package main
import (
"sync"
)
// 全局变量
var counter int
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
  wg.Add(1)
  go func() {
   defer wg.Done()
   counter++
  }()
 }
 wg.Wait()
println(counter)
}


运行多次得到不同的结果:



❯❯❯ go run local_lock.go
945
❯❯❯ go run local_lock.go
937
❯❯❯ go run local_lock.go
959


这是因为多个 goroutine 同时对 counter 进行修改,由于不加锁,存在竞争条件,导致最终的结果不确定。

引入互斥锁解决竞争条件



package main
import (
"sync"
)
var counter int
var mu sync.Mutex // 互斥锁
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
  wg.Add(1)
  go func() {
   defer wg.Done()
   mu.Lock() // 加锁
   counter++
   mu.Unlock() // 解锁
  }()
 }
 wg.Wait()
println(counter)
}


通过引入互斥锁 sync.Mutex,在对 counter 进行修改前加锁,修改完成后解锁,确保了对 counter 操作的原子性。这样可以稳定地得到正确的计数结果。



❯❯❯ go run local_lock.go
1000

使用 Trylock 进行单一执行者控制


在某些场景,我们希望某个任务只有单一的执行者,后续的任务在抢锁失败后应放弃执行。这时候可以使用 Trylock。



package main
import (
"sync"
)
// Lock try lock
type Lock struct {
 c chan struct{}
}
// NewLock generate a try lock
func NewLock() Lock {
var l Lock
 l.c = make(chan struct{}, 1)
 l.c <- struct{}{}
return l
}
// Lock try lock, return lock result
func (l Lock) Lock() bool {
 lockResult := false
select {
case <-l.c:
  lockResult = true
default:
 }
return lockResult
}
// Unlock , Unlock the try lock
func (l Lock) Unlock() {
 l.c <- struct{}{}
}
var counter int
func main() {
var l = NewLock()
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
  wg.Add(1)
  go func() {
   defer wg.Done()
   if !l.Lock() {
    // log error
    println("lock failed")
    return
   }
   counter++
   println("current counter", counter)
   l.Unlock()
  }()
 }
 wg.Wait()
}



这里使用大小为 1 的 channel 模拟 Trylock 的效果。每个 goroutine 尝试加锁,如果成功则继续执行任务,否则放弃执行。

基于 Redis 的分布式锁


在分布式场景下,我们需要考虑多台机器之间的数据同步问题。这时候可以使用 Redis 提供的 setnx 命令来实现分布式锁。



package main
import (
"fmt"
"sync"
"time"
"github.com/go-redis/redis"
)
func incr() {
 client := redis.NewClient(&redis.Options{
  Addr:     "localhost:6379",
  Password: "", // no password set
  DB:       0,  // use default DB
 })
var lockKey = "counter_lock"
var counterKey = "counter"
// lock
 resp := client.SetNX(lockKey, 1, time.Second*5)
 lockSuccess, err := resp.Result()
if err != nil || !lockSuccess {
  fmt.Println(err, "lock result:", lockSuccess)
  return
 }
// counter ++
 getResp := client.Get(counterKey)
 cntValue, err := getResp.Int64()
if err == nil || err == redis.Nil {
  cntValue++
  resp := client.Set(counterKey, cntValue, 0)
  _, err := resp.Result()
  if err != nil {
   // log err
   println("set value error!")
  }
 }
println("current counter is", cntValue)
 delResp := client.Del(lockKey)
 unlockSuccess, err := delResp.Result()
if err == nil && unlockSuccess > 0 {
  println("unlock success!")
 } else {
  println("unlock failed", err)
 }
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
  wg.Add(1)
  go func() {
   defer wg.Done()
   incr()
  }()
 }
 wg.Wait()
}


通过 Redis 的 setnx 命令,我们可以实现一个简单的分布式锁。在获取锁成功后执行任务,任务执行完成后释放锁。


基于 ZooKeeper 的分布式锁


ZooKeeper 是另一个分布式系统协调服务,它提供了一套强一致性的 API,适用于一些需要高度可靠性的场景。以下是使用 ZooKeeper 实现的分布式锁示例。



package main
import (
"time"
"github.com/samuel/go-zookeeper/zk"
)
func main() {
 c, _, err := zk.Connect([]string{"127.0.0.1"}, time.Second) //*10)
if err != nil {
  panic(err)
 }
 l := zk.NewLock(c, "/lock", zk.WorldACL(zk.PermAll))
 err = l.Lock()
if err != nil {
  panic(err)
 }
println("lock succ, do your business logic")
 time.Sleep(time.Second * 10)
// do some thing
 l.Unlock()
println("unlock succ, finish business logic")
}



通过 ZooKeeper 提供的 Lock API,我们可以实


现分布式锁的获取和释放。ZooKeeper 的分布式锁机制通过临时有序节点和 Watch API 实现,保障了强一致性。


基于 etcd 的分布式锁


etcd 是近年来备受关注的分布式系统组件,类似于 ZooKeeper,但在某些场景下有更好的性能表现。以下是使用 etcd 实现分布式锁的示例。



package main
import (
"log"
"github.com/zieckey/etcdsync"
)
func main() {
 m, err := etcdsync.New("/lock", 10, []string{"<http://127.0.0.1:2379>"})
if m == nil || err != nil {
  log.Printf("etcdsync.New failed")
  return
 }
 err = m.Lock()
if err != nil {
  log.Printf("etcdsync.Lock failed")
  return
 }
log.Printf("etcdsync.Lock OK")
log.Printf("Get the lock. Do something here.")
 err = m.Unlock()
if err != nil {
  log.Printf("etcdsync.Unlock failed")
 } else {
  log.Printf("etcdsync.Unlock OK")
 }
}



通过 etcdsync 库,我们可以方便地使用 etcd 实现分布式锁。etcd 提供的分布式锁机制也是基于临时有序节点和 Watch API 实现的。

如何选择锁方案


在选择锁方案时,需要根据业务场景和性能需求进行权衡。以下是一些参考因素:

  1. 单机锁 vs 分布式锁: 如果业务在单机上,可以考虑使用单机锁。如果是分布式场景,需要使用分布式锁来保障多台机器之间的数据一致性。
  2. 锁的粒度: 锁的粒度是指锁定的资源范围,可以是整个应用、某个模块、某个数据表等。根据业务需求选择合适的锁粒度。
  3. 性能需求: 不同的锁方案在性能表现上有差异,例如,Redis 的 setnx 是一个简单的分布式锁方案,适用于低频次的锁操作。ZooKeeper 和 etcd 提供的分布式锁机制在一致性上更为强大,但性能相对较低。
  4. 可靠性需求: 如果对数据可靠性有极高要求,需要选择提供强一致性保障的分布式锁方案,如 ZooKeeper 或 etcd。
  5. 技术栈: 考虑已有技术栈中是否已经包含了适用的锁方案,避免引入新的技术栈增加复杂性。

最终的选择取决于业务需求和系统架构,需要仔细评估各种锁方案的优劣势。

相关文章
|
人工智能 安全 算法
Go入门实战:并发模式的使用
本文详细探讨了Go语言的并发模式,包括Goroutine、Channel、Mutex和WaitGroup等核心概念。通过具体代码实例与详细解释,介绍了这些模式的原理及应用。同时分析了未来发展趋势与挑战,如更高效的并发控制、更好的并发安全及性能优化。Go语言凭借其优秀的并发性能,在现代编程中备受青睐。
541 33
|
NoSQL 调度 Redis
分布式锁—3.Redisson的公平锁
Redisson公平锁(RedissonFairLock)是一种基于Redis实现的分布式锁,确保多个线程按申请顺序获取锁,从而实现公平性。其核心机制是通过队列和有序集合管理线程的排队顺序。加锁时,线程会进入队列并等待,锁释放后,队列中的第一个线程优先获取锁。RedissonFairLock支持可重入加锁,即同一线程多次加锁不会阻塞。新旧版本在排队机制上有所不同,新版本在5分钟后才会重排队列,而旧版本在5秒后就会重排。释放锁时,Redisson会移除队列中等待超时的线程,并通知下一个排队的线程获取锁。通过这种机制,RedissonFairLock确保了锁的公平性和顺序性。
|
12月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
923 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
存储 Go 开发者
Go 语言中如何处理并发错误
在 Go 语言中,并发编程中的错误处理尤为复杂。本文介绍了几种常见的并发错误处理方法,包括 panic 的作用范围、使用 channel 收集错误与结果,以及使用 errgroup 包统一管理错误和取消任务,帮助开发者编写更健壮的并发程序。
274 4
Go 语言中如何处理并发错误
|
NoSQL Redis
分布式锁设计吗,你是如何实现锁类型切换、锁策略切换基于限流的?
本方案基于自定义注解与AOP实现分布式锁,支持锁类型(如可重入锁、公平锁等)与加锁策略(如重试、抛异常等)的灵活切换,并结合Redisson实现可重入、自动续期等功能,通过LUA脚本保障原子性,兼顾扩展性与实用性。
309 0
|
NoSQL 调度 Redis
分布式锁—5.Redisson的读写锁
Redisson读写锁(RedissonReadWriteLock)是Redisson提供的一种分布式锁机制,支持读锁和写锁的互斥与并发控制。读锁允许多个线程同时获取,适用于读多写少的场景,而写锁则是独占锁,确保写操作的互斥性。Redisson通过Lua脚本实现锁的获取、释放和重入逻辑,并利用WatchDog机制自动续期锁的过期时间,防止锁因超时被误释放。 读锁的获取逻辑通过Lua脚本实现,支持读读不互斥,即多个线程可以同时获取读锁。写锁的获取逻辑则确保写写互斥和读写互斥,即同一时间只能有一个线程获取写锁,
648 17
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。
|
数据采集 消息中间件 编解码
Go语言实战案例:使用 Goroutine 并发打印
本文通过简单案例讲解 Go 语言核心并发模型 Goroutine,涵盖协程启动、输出控制、主程序退出机制,并结合 sync.WaitGroup 实现并发任务同步,帮助理解 Go 并发设计思想与实际应用。
|
数据采集 监控 Go
用 Go 实现一个轻量级并发任务调度器(支持限速)
本文介绍了如何用 Go 实现一个轻量级的并发任务调度器,解决日常开发中批量任务处理的需求。调度器支持最大并发数控制、速率限制、失败重试及结果收集等功能。通过示例代码展示了其使用方法,并分析了核心组件设计,包括任务(Task)和调度器(Scheduler)。该工具适用于网络爬虫、批量请求等场景。文章最后总结了 Go 并发模型的优势,并提出了扩展功能的方向,如失败回调、超时控制等,欢迎读者交流改进。
624 25
|
缓存 NoSQL Java
【📕分布式锁通关指南 11】源码剖析redisson之读写锁的实现
Redisson 的 `RedissonReadWriteLock` 提供了高效的分布式读写锁实现,适用于读多写少的场景。通过 Redis 与 Lua 脚本结合,确保读锁并行、写锁互斥,以及读写之间的互斥,保障了分布式环境下的数据一致性。它支持可重入、自动过期和锁释放机制,提升了系统并发性能与资源控制能力。
412 0