分布式锁应用场景

很多应用场景是需要系统保证幂等性的(如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代码实现

import "github.com/go-redis/redis"  //redis package
//connect redis
var client = redis.NewClient(&redis.Options{Addr:     "localhost:6379",Password: "",DB:       0,
})
//lock
func lock(myfunc func()) {var lockKey = "mylockr"//locklockSuccess, err := client.SetNX(lockKey, 1, time.Second*5).Result()if err != nil || !lockSuccess {fmt.Println("get lock fail")return} else {fmt.Println("get lock")}//run funcmyfunc()//unlock_, err := client.Del(lockKey).Result()if err != nil {fmt.Println("unlock fail")} else {fmt.Println("unlock")}
}
//do action
var counter int64
func incr() {counter++fmt.Printf("after incr is %d\n", counter)
}
//5 goroutine compete lock
var wg sync.WaitGroup
func main() {for i := 0; i < 5; i++ {wg.Add(1)go func() {lock(incr)}()}wg.Wait()fmt.Printf("final counter is %d \n", counter)
}

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

https://github.com/skyhackvip/lock/blob/master/redislock.go

代码执行结果:

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

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

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

1.3方案缺陷

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

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

1.4方案优化

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

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

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

这里的1可以改为能识别该routine的唯一值(如uid,orderid等),也可以使用uuid随机生成一个。(关于如何生成uuid方案参见公众号上一篇文章)

func lock(myfunc func()) {//lockuuid := getUuid()lockSuccess, err := client.SetNX(lockKey, uuid, time.Second*5).Result()if err != nil || !lockSuccess {fmt.Println("get lock fail")return} else {fmt.Println("get lock")}   //run funcmyfunc()//unlockvalue, _ := client.Get(lockKey).Result()if value == uuid { //compare value,if equal then del_, err := client.Del(lockKey).Result()if err != nil {fmt.Println("unlock fail")}  else {fmt.Println("unlock")}}
}

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

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

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

1.5方案再优化

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

func lock(myfunc func()) {//...code//unlockvar luaScript = redis.NewScript(`if redis.call("get", KEYS[1]) == ARGV[1]thenreturn redis.call("del", KEYS[1])elsereturn 0end`)rs, _ := luaScript.Run(client, []string{lockKey}, uuid).Result()if rs == 0 {fmt.Println("unlock fail")} else {fmt.Println("unlock")}
}

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

完整代码参见:https://github.com/skyhackvip/lock/blob/master/redislualock.go

1.6redis锁适用场景

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

2.zookeeper锁

2.1实现原理

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

2.2代码实现

import "github.com/samuel/go-zookeeper/zk" //package
//connect zk
conn, _, err := zk.Connect([]string{"localhost:2181"}, time.Second)
//zklock
func zklock(conn *zk.Conn, myfunc func()) {lock := zk.NewLock(conn, "/mylock", zk.WorldACL(zk.PermAll))    err := lock.Lock()if err != nil {panic(err)}   fmt.Println("get lock")myfunc()lock.Unlock()fmt.Println("unlock")
}
//goroutine run
for i := 0; i < 5; i++ {go zklock(conn, incr)
}

完整代码参见:https://github.com/skyhackvip/lock/blob/master/zklock.go

执行结果如下:

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

2.3zookeeper锁适用场景

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

 3.方案对比选择

redis锁

zookeeper锁

描述

使用set nx实现

使用临时节点+watch实现

依赖

redis

zookeeper

适用场景

并发抢锁

锁占用时间长其他任务可等待。如消息幂等消费。

高可用性

redis发生故障主从切换等可能导致锁失效

利用paxos协议能保证分布式一致性,数据更可靠

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

更多参考资料

etcd实现锁:

https://github.com/zieckey/etcdsync

文章相关实现代码:

https://github.com/skyhackvip/lock

分布式锁实现原理与最佳实践相关推荐

  1. 《大数据系统构建:可扩展实时数据系统构建原理与最佳实践》一1.5 大数据系统应有的属性...

    本节书摘来自华章出版社<大数据系统构建:可扩展实时数据系统构建原理与最佳实践>一书中的第1章,第1.1节,南森·马茨(Nathan Marz) [美] 詹姆斯·沃伦(JamesWarren ...

  2. rocketmq存储结构_阿里专家分享内部绝密RocketMQ核心原理与最佳实践笔记

    本文源码以RocketMQ 4.2.0 和 RocketMQ 4.3.0 为 基 础 , 从RocketMQ的实际使用到RocketMQ的源码分析,再到RocketMQ企业落地实践方案,逐步讲解.使读 ...

  3. clickhouse原理解析与应用实践 pdf_阿里专家分享内部绝密RocketMQ核心原理与最佳实践PDF...

    前言 本文源码以RocketMQ 4.2.0 和 RocketMQ 4.3.0 为 基 础 , 从RocketMQ的实际使用到RocketMQ的源码分析,再到RocketMQ企业落地实践方案,逐步讲解 ...

  4. 技术分享| 基于 Etcd 的分布式锁实现原理及方案

    1. 为什么选择 Etcd 据官网介绍,Etcd 是一个分布式,可靠的 Key-Value 存储系统,主要用于存储分布式系统中的关键数据.初见之下,Etcd 与 NoSQL 数据库系统有几分相似,但作 ...

  5. 第09课:基于 Etcd 的分布式锁实现原理及方案

    Etcd 最新版本已经提供了支持分布式锁的基础接口(可见官网说明),但本文并不局限于此. 本文将介绍两条实现分布式锁的技术路线: 从分布式锁的原理出发,结合 Etcd 的特性,洞见分布式锁的实现细节: ...

  6. Redis进阶- Redisson分布式锁实现原理及源码解析

    文章目录 Pre 用法 Redisson分布式锁实现原理 Redisson分布式锁源码分析 redisson.getLock(lockKey) 的逻辑 redissonLock.lock()的逻辑 r ...

  7. 老夫带你深度剖析Redisson实现分布式锁的原理

    Redis实现分布式锁的原理 前面讲了Redis在实际业务场景中的应用,那么下面再来了解一下Redisson功能性场景的应用,也就是大家经常使用的分布式锁的实现场景. 引入redisson依赖 < ...

  8. SVN分支/合并原理及最佳实践

    #SVN分支/合并原理及最佳实践 SVN是一种常用的版本控制工具,一种典型的项目代码实践方式是: 存在一个代码基线(Base Line)或称主干,不同的模块使用各自的分支进行功能开发,在开发完毕后合并 ...

  9. redis实现分布式锁的原理

    redis实现分布式锁的原理 一.为什么使用分布式锁? ·>本地锁的局限性: ·>分布式锁的概念: 二.redis实现分布式锁的原理? 1.抢占分布式锁: 2.加锁的同时设置过期时间: 3 ...

最新文章

  1. analyzing problems
  2. 核心概念——节点分组 Group
  3. 剑灵系统推荐加点_剑灵重制修炼系统 无定式加点打造自我风格
  4. react leaflet_如何使用Leaflet在React中轻松构建地图应用
  5. leetcode1119. 删去字符串中的元音 小学难度
  6. 华为任正非推荐学习的博士PPT《认识5G,发展5G》
  7. log解析工具 px4_console.log(console.log) = ?
  8. [原创].NET 业务框架开发实战之六 DAL的重构
  9. 使用webpack打包ThinkPHP的资源文件
  10. 《Hadoop海量数据处理:技术详解与项目实战(第2版)》一第2章 环境准备
  11. java break(),Java BreakIterator last()用法及代码示例
  12. SqlServer2005基于已有表创建分区
  13. Java 设计模式 之 代理模式(Proxy)
  14. fluent设置uds c语言程序,Fluent中UDF和UDS二次开发高级实例专题
  15. 有源滤波器: 基于UAF42的50Hz陷波器设计
  16. matlab矩阵指定行求和,在matlab中对矩阵元素求和的有效(最快)方法
  17. 实验吧_网站综合渗透_Discuz!
  18. 测试人生 | 为了娃的奶粉钱,测试媛妈妈拿出考研的拼劲,半年终圆大厂梦
  19. 【UCIe】UCIe NOP 介绍
  20. Java常见面试题(181-200)

热门文章

  1. #react 之ant design Pro 学习研究#----启动项目
  2. 90国央行齐聚华盛顿研讨区块链:“这一切意味着什么”
  3. LeetCode:Rotate Image
  4. 使用 custom element 创建自定义元素
  5. 《1024伐木累》-程序员妹子与花木兰
  6. Symbian S60 签名工具
  7. python之路--内置函数03
  8. win10安装python
  9. 如何在 Java 中正确使用 wait, notify 和 notifyAll?
  10. 二维数组的遍历之查漏补缺