Go:分布式锁实现原理与最佳实践

分布式锁应用场景

很多应用场景是需要系统保证幂等性的(如api服务或消息消费者),并发情况下或消息重复很容易造成系统重入,那么分布式锁是保障幂等的一个重要手段。

另一方面,很多抢单场景或者叫交易撮合场景,如dd司机抢单或唯一商品抢拍等都需要用一把“全局锁”来解决并发造成的问题。在防止并发情况下造成库存超卖的场景,也常用分布式锁来解决。

实现分布式锁方案

这里介绍常见两种:redis锁、zookeeper锁

1.Redis实现方案

1.1实现原理

redis分布式锁基本都知道setnx命令(if not exists),其实现原理即:如果进入redis添加某个键不存在可以设置成功,如果已存在则会设置失败。

说明:setnx命令已过时,这里推荐使用 set +nx 参数来实现。

set命令:set key value ex seconds nx

  • ex 表示过期时间,精确到秒 (对应另一个参数px过期时间精确到毫秒)
  • nx 表示if not exists,只有键不存在才能设置成功(对应另一个参数xx只有键存在才能设置成功)

设置过期时间的作用,如果某个并行任务(进程/线程/协程)持有锁,但不能正常释放,将导致所有任务都无法获取锁,获取执行权限。而引入了过期时间解决此问题的同时,也会引入新的问题,具体后面分析。

1.2代码实现

1import "github.com/go-redis/redis" //redis package 2//connect redis 3var client = redis.NewClient(&redis.Options{ 4 Addr: "localhost:6379", 5 Password: "", 6 DB: 0, 7}) 8//lock 9func lock(myfunc func()) { 10 var lockKey = "mylockr" 11 //lock 12 lockSuccess, err := client.SetNX(lockKey, 1, time.Second*5).Result() 13 if err != nil || !lockSuccess { 14 fmt.Println("get lock fail") 15 return 16 } else { 17 fmt.Println("get lock") 18 } 19 //run func 20 myfunc() 21 //unlock 22 _, err := client.Del(lockKey).Result() 23 if err != nil { 24 fmt.Println("unlock fail") 25 } else { 26 fmt.Println("unlock") 27 } 28} 29//do action 30var counter int64 31func incr() { 32 counter++ 33 fmt.Printf("after incr is %d\n", counter) 34} 35//5 goroutine compete lock 36var wg sync.WaitGroup 37func main() { 38 for i := 0; i < 5; i++ { 39 wg.Add(1) 40 go func() { 41 lock(incr) 42 }() 43 } 44 wg.Wait() 45 fmt.Printf("final counter is %d \n", counter) 46}

以上代码截取关键部分,完整代码参见:链接

代码执行结果:

根据执行结果可以看到,每次执行最后的计数不一样,多个协程间互相抢锁,只有拿到锁才会计数加1,抢锁失败则不执行。

这里说明下:由于routine执行时间太短,执行完把锁释放了所以才有其他routine可以拿到锁。如果incr代码中增加sleep时间,那么结果都是1了。

用一张图来更直观解释具体执行情况:

1.3方案缺陷

刚才提到使用了过期时间,虽然解决了“死锁”问题,但会引来新的问题,具体问题分析如下:

可以看到routine1拿到锁,但由于执行时间过长(比锁失效时间长),导致锁提前失效释放,routine3可以正常拿到锁,而之后routine1进行锁释放,当routine3进行锁释放时就会失败,如果此时有其他并发来的时候锁也会有问题。

1.4方案优化

那么有什么有效解决方案呢?

简单来说就是利用lock的value,还记得之前代码设置lock的时候随便使用了一个值1就打发了。

1resp := client.SetNX(lockKey, 1, time.Second\*5)

这里的1可以改为能识别该routine的唯一值(如uid,orderid等),也可以使用uuid随机生成一个。

1func lock(myfunc func()) { 2 //lock 3 uuid := getUuid() 4 lockSuccess, err := client.SetNX(lockKey, uuid, time.Second*5).Result() 5 if err != nil || !lockSuccess { 6 fmt.Println("get lock fail") 7 return 8 } else { 9 fmt.Println("get lock") 10 } 11 //run func 12 myfunc() 13 //unlock 14 value, _ := client.Get(lockKey).Result() 15 if value == uuid { //compare value,if equal then del 16 _, err := client.Del(lockKey).Result() 17 if err != nil { 18 fmt.Println("unlock fail") 19 } else { 20 fmt.Println("unlock") 21 } 22 } 23}

这里增加了value的比较,确认了是当前routine,才会进行删除。至此问题解决了吗?

1value, \_ := client.Get(lockKey).Result() 和 value==uuid

这个操作本身不具有“原子性”,可能当获取到value并且对比一致了,但此时lock过期失效了,而同时另一个routine拿到了结果,那么这里又会把别人的锁误删除了。

1.5方案再优化

那么有没有办法保障操作的原子性呢,这里可以使用lua彻底解决,lua是嵌入式语言,redis本身支持。使用golang操作redis运行lua命令,保障问题解决。上代码如下:

1func lock(myfunc func()) { 2 //...code 3 //unlock 4 var luaScript = redis.NewScript(` 5 if redis.call("get", KEYS[1]) == ARGV[1] 6 then 7 return redis.call("del", KEYS[1]) 8 else 9 return 0 10 end 11 `) 12 rs, _ := luaScript.Run(client, []string{lockKey}, uuid).Result() 13 if rs == 0 { 14 fmt.Println("unlock fail") 15 } else { 16 fmt.Println("unlock") 17 } 18}

lua脚本中KEYS[1]代表lock的key,ARGV[1]代表lock的value,也就是生成的uuid。通过执行lua来保障这里删除锁的操作是原子的。

完整代码参见:链接

1.6redis锁适用场景

由redis设置的锁,多个并发任务进行争抢占用,因此非常适合高并发情况下,用来进行抢锁。

2.zookeeper锁

2.1实现原理

使用zk的临时节点插入值,如果插入成功后watch会通知所有监听节点,此时其他并行任务不可再进行插入。具体图示如下:

2.2代码实现

1import "github.com/samuel/go-zookeeper/zk" //package 2//connect zk 3conn, _, err := zk.Connect([]string{"localhost:2181"}, time.Second) 4//zklock 5func zklock(conn *zk.Conn, myfunc func()) { 6 lock := zk.NewLock(conn, "/mylock", zk.WorldACL(zk.PermAll)) 7 err := lock.Lock() 8 if err != nil { 9 panic(err) 10 } 11 fmt.Println("get lock") 12 myfunc() 13 lock.Unlock() 14 fmt.Println("unlock") 15} 16//goroutine run 17for i := 0; i < 5; i++ { 18 go zklock(conn, incr) 19}

完整代码参见:链接

执行结果如下:

每次执行,执行结果都是5。

2.3zookeeper锁适用场景

相比于redis抢锁导致其他routine抢锁失败退出,使用zk实现的锁会让其他routine处于“等锁”状态。

3.方案对比选择

<table width="489"><tbody style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;"><tr style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;"><td width="127" style="word-break: break-all;border-color: initial;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;"><br></td><td width="154.33333333333334" style="word-break: break-all;border-color: initial;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;"><p style="max-width: 100%;min-height: 1em;box-sizing: border-box !important;overflow-wrap: break-word !important;"><span style="max-width: 100%;font-size: 14px;box-sizing: border-box !important;overflow-wrap: break-word !important;">redis锁</span></p></td><td width="173.33333333333334" style="word-break: break-all;border-color: initial;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;"><p style="max-width: 100%;min-height: 1em;box-sizing: border-box !important;overflow-wrap: break-word !important;"><span style="max-width: 100%;font-size: 14px;box-sizing: border-box !important;overflow-wrap: break-word !important;">zookeeper锁</span></p></td></tr><tr style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;"><td width="38.33333333333333" style="word-break: break-all;border-color: initial;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;"><p style="max-width: 100%;min-height: 1em;box-sizing: border-box !important;overflow-wrap: break-word !important;"><span style="max-width: 100%;font-size: 14px;box-sizing: border-box !important;overflow-wrap: break-word !important;">描述</span></p></td><td width="127" style="word-break: break-all;border-color: initial;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;"><p style="max-width: 100%;min-height: 1em;box-sizing: border-box !important;overflow-wrap: break-word !important;"><span style="max-width: 100%;font-size: 14px;box-sizing: border-box !important;overflow-wrap: break-word !important;">使用set nx实现</span></p></td><td width="154.33333333333334" style="word-break: break-all;border-color: initial;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;"><p style="max-width: 100%;min-height: 1em;box-sizing: border-box !important;overflow-wrap: break-word !important;"><span style="max-width: 100%;font-size: 14px;box-sizing: border-box !important;overflow-wrap: break-word !important;">使用临时节点+watch实现</span></p></td></tr><tr style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;"><td width="7.9999999999999964" style="word-break: break-all;border-color: initial;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;"><p style="max-width: 100%;min-height: 1em;box-sizing: border-box !important;overflow-wrap: break-word !important;"><span style="max-width: 100%;font-size: 14px;box-sizing: border-box !important;overflow-wrap: break-word !important;">依赖</span></p></td><td width="127" style="word-break: break-all;border-color: initial;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;"><p style="max-width: 100%;min-height: 1em;box-sizing: border-box !important;overflow-wrap: break-word !important;"><span style="max-width: 100%;font-size: 14px;box-sizing: border-box !important;overflow-wrap: break-word !important;">redis</span></p></td><td width="154.33333333333334" style="word-break: break-all;border-color: initial;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;"><span style="max-width: 100%;font-size: 14px;box-sizing: border-box !important;overflow-wrap: break-word !important;">zookeeper</span></td></tr><tr style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;"><td width="7.9999999999999964" style="word-break: break-all;border-color: initial;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;"><p style="max-width: 100%;min-height: 1em;box-sizing: border-box !important;overflow-wrap: break-word !important;"><span style="max-width: 100%;font-size: 14px;box-sizing: border-box !important;overflow-wrap: break-word !important;">适用场景</span></p></td><td width="127" style="word-break: break-all;border-color: initial;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;"><p style="max-width: 100%;min-height: 1em;box-sizing: border-box !important;overflow-wrap: break-word !important;"><span style="max-width: 100%;font-size: 14px;box-sizing: border-box !important;overflow-wrap: break-word !important;">并发抢锁</span></p></td><td width="153.33333333333334" style="word-break: break-all;border-color: initial;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;"><p style="max-width: 100%;min-height: 1em;box-sizing: border-box !important;overflow-wrap: break-word !important;"><span style="font-size: 14px;">锁占用时间长其他任务可等待。如消息幂等消费。<br></span></p></td></tr><tr style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;"><td width="7.9999999999999964" style="word-break: break-all;border-color: initial;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;"><p style="max-width: 100%;min-height: 1em;box-sizing: border-box !important;overflow-wrap: break-word !important;"><span style="max-width: 100%;font-size: 14px;box-sizing: border-box !important;overflow-wrap: break-word !important;">高可用性</span></p></td><td width="127" style="word-break: break-all;border-color: initial;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;"><p style="max-width: 100%;min-height: 1em;box-sizing: border-box !important;overflow-wrap: break-word !important;"><span style="max-width: 100%;font-size: 14px;box-sizing: border-box !important;overflow-wrap: break-word !important;">redis发生故障主从切换等可能导致锁失效</span></p></td><td width="153.33333333333334" style="word-break: break-all;border-color: initial;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;"><span style="font-size: 14px;">利用paxos协议能保证分布式一致性,数据更可靠<br></span></td></tr></tbody></table>

如果不是对锁有特别高的要求,一般情况下使用redis锁就够了。除提到的这两种外使用etcd也可以完成锁需求,具体可以参考下方资料。

更多参考资料

etcd实现锁:

https://github.com/zieckey/etcdsync

文章相关实现代码:

https://github.com/skyhackvip/lock


推荐阅读

本文转自 https://mp.weixin.qq.com/s/lrSQBK-Kihkj6994kQFpUQ,如有侵权,请联系删除。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

Twitter的分布式自增ID算法snowflake (Java版)

概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移