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

关注公众号【爱发白日梦的后端】分享技术干货、读书笔记、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力!

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

不加锁的并发计数

text 复制代码
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)
}

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

text 复制代码
❯❯❯ go run local_lock.go
945
❯❯❯ go run local_lock.go
937
❯❯❯ go run local_lock.go
959

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

引入互斥锁解决竞争条件

text 复制代码
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 操作的原子性。这样可以稳定地得到正确的计数结果。

text 复制代码
❯❯❯ go run local_lock.go
1000

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

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

text 复制代码
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 命令来实现分布式锁。

text 复制代码
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 实现的分布式锁示例。

text 复制代码
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 实现分布式锁的示例。

text 复制代码
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. 技术栈: 考虑已有技术栈中是否已经包含了适用的锁方案,避免引入新的技术栈增加复杂性。 最终的选择取决于业务需求和系统架构,需要仔细评估各种锁方案的优劣势。
相关推荐
IT_陈寒8 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码9 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo9 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志10 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..10 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects10 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰10 小时前
一些基础和问题解决
后端
陈随易10 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN10 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp
Imchendiana10 小时前
《狂人日记NO.9》— 前后端一把梭,我的全栈实录
前端·后端