[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. 技术栈: 考虑已有技术栈中是否已经包含了适用的锁方案,避免引入新的技术栈增加复杂性。 最终的选择取决于业务需求和系统架构,需要仔细评估各种锁方案的优劣势。
相关推荐
2401_882727571 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
追逐时光者2 小时前
.NET 在 Visual Studio 中的高效编程技巧集
后端·.net·visual studio
大梦百万秋3 小时前
Spring Boot实战:构建一个简单的RESTful API
spring boot·后端·restful
斌斌_____3 小时前
Spring Boot 配置文件的加载顺序
java·spring boot·后端
路在脚下@3 小时前
Spring如何处理循环依赖
java·后端·spring
海绵波波1074 小时前
flask后端开发(1):第一个Flask项目
后端·python·flask
小奏技术5 小时前
RocketMQ结合源码告诉你消息量大为啥不需要手动压缩消息
后端·消息队列
AI人H哥会Java7 小时前
【Spring】控制反转(IoC)与依赖注入(DI)—IoC容器在系统中的位置
java·开发语言·spring boot·后端·spring
凡人的AI工具箱7 小时前
每天40分玩转Django:Django表单集
开发语言·数据库·后端·python·缓存·django
奔跑草-7 小时前
【数据库】SQL应该如何针对数据倾斜问题进行优化
数据库·后端·sql·ubuntu