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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: [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. 技术栈: 考虑已有技术栈中是否已经包含了适用的锁方案,避免引入新的技术栈增加复杂性。

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

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
17天前
|
网络协议 算法 网络性能优化
计算机网络常见面试题(一):TCP/IP五层模型、TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议
计算机网络常见面试题(一):TCP/IP五层模型、应用层常见的协议、TCP与UDP的区别,TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议、ARP协议
|
20天前
|
存储 缓存 监控
解决分布式系统演进过程中数据一致性问题的方法
【10月更文挑战第24天】解决分布式系统演进过程中数据一致性问题是一个复杂而又重要的任务。需要综合运用多种方法和技术,根据具体的系统需求和场景,选择合适的解决方案。同时,不断地进行优化和改进,以适应不断变化的分布式系统环境。
37 4
|
1月前
|
NoSQL Java API
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试一线互联网企业时遇到了关于Redis分布式锁过期及自动续期的问题。尼恩对此进行了系统化的梳理,介绍了两种核心解决方案:一是通过增加版本号实现乐观锁,二是利用watch dog自动续期机制。后者通过后台线程定期检查锁的状态并在必要时延长锁的过期时间,确保锁不会因超时而意外释放。尼恩还分享了详细的代码实现和原理分析,帮助读者深入理解并掌握这些技术点,以便在面试中自信应对相关问题。更多技术细节和面试准备资料可在尼恩的技术文章和《尼恩Java面试宝典》中获取。
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
|
2月前
|
缓存 NoSQL Java
谷粒商城笔记+踩坑(12)——缓存与分布式锁,Redisson+缓存数据一致性
缓存与分布式锁、Redisson分布式锁、缓存数据一致性【必须满足最终一致性】
120 14
谷粒商城笔记+踩坑(12)——缓存与分布式锁,Redisson+缓存数据一致性
|
2月前
|
消息中间件 安全 前端开发
面试官:单核服务器可以不加锁吗?
面试官:单核服务器可以不加锁吗?
49 4
面试官:单核服务器可以不加锁吗?
|
2月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
1月前
|
架构师 Java 数据中心
二阶段提交:确保分布式系统中数据一致性的关键协议
【10月更文挑战第16天】在分布式系统中,数据一致性的维护是一个至关重要的挑战。为了应对这一挑战,二阶段提交(Two-Phase Commit,简称2PC)协议应运而生。作为一种经典的分布式事务协议,2PC旨在确保在分布式系统中的所有节点在进行事务提交时保持一致性。
37 0
|
1月前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。
|
2月前
|
消息中间件 Java 对象存储
数据一致性挑战:Spring Cloud与Netflix OSS下的分布式事务管理
数据一致性挑战:Spring Cloud与Netflix OSS下的分布式事务管理
51 2
|
3月前
|
监控 Go API
带你十天轻松搞定 Go 微服务之大结局(分布式事务)
带你十天轻松搞定 Go 微服务之大结局(分布式事务)

热门文章

最新文章