[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 复制代码
❯❯❯ go run local_lock.go
945
❯❯❯ go run local_lock.go
937
❯❯❯ go run local_lock.go
959

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

引入互斥锁解决竞争条件

go 复制代码
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 复制代码
❯❯❯ go run local_lock.go
1000

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

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

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

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

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

go 复制代码
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. 技术栈: 考虑已有技术栈中是否已经包含了适用的锁方案,避免引入新的技术栈增加复杂性。
    最终的选择取决于业务需求和系统架构,需要仔细评估各种锁方案的优劣势。
相关推荐
穿越在未来5 分钟前
【RabbitMQ】Producer之TTL过期时间 - 基于AMQP 0-9-1
分布式·消息队列·rabbitmq·消息中间件·rabbitmq ttl
Huooya1 小时前
springboot的外部配置加载顺序
spring boot·面试·架构
钢板兽1 小时前
Java后端高频面经——Spring、SpringBoot、MyBatis
java·开发语言·spring boot·spring·面试·mybatis
钢板兽1 小时前
Java后端高频面经——JVM、Linux、Git、Docker
java·linux·jvm·git·后端·docker·面试
可乐cc呀2 小时前
kafka单机部署实战
分布式·zookeeper·kafka
A仔不会笑2 小时前
MySQL面试篇——性能优化
java·数据库·mysql·面试·性能优化
雷渊2 小时前
MySQL为什么有时候会抖动一下?
面试
StableAndCalm2 小时前
什么是zookeeper
分布式·zookeeper·云原生
uhakadotcom2 小时前
ClickHouse入门:快速掌握高性能数据分析
后端·面试·github
雷渊2 小时前
深入分析mysql中的binlog和redo log
java·后端·面试