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

peter 等级 470 0 0

分布式锁应用场景

很多应用场景是需要系统保证幂等性的(如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只有键存在才能设置成功)

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

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

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"
    //lock
    lockSuccess, 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 func
    myfunc()
    //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)
}

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

代码执行结果:

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

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

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

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

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

1.3方案缺陷

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

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

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

1.4方案优化

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

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

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

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

func lock(myfunc func()) {
    //lock
    uuid := 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 func
    myfunc()
    //unlock
    value, _ := 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
    //unlock
    var luaScript = redis.NewScript(`
        if redis.call("get", KEYS[1]) == ARGV[1]
            then
                return redis.call("del", KEYS[1])
            else
                return 0
        end
    `)
    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来保障这里删除锁的操作是原子的。

完整代码参见:链接

1.6redis锁适用场景

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

2.zookeeper锁

2.1实现原理

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

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

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)
}

完整代码参见:链接

执行结果如下:

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


推荐阅读

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

收藏
评论区

相关推荐

css问题
1、 在IOS中图片不显示(给图片加了圆角或者img没有父级) <div<img src""/</div div {width: 20px; height: 20px; borderradius: 20px; overflow: h
Redis实现分布式锁
一、redis分布式锁的简易实现 用redis实现分布式锁是一个老生常谈的问题了。因为redis单条命令执行的原子性和高性能,当多个客户端执行setnx(相同key)时,最多只有一个获得成功。因此在对可用性要求不是特别高的场景下,redis分布式锁方案不失为一个性价比高的实现。 1. 多个客户端执行setnx lock
python中的各种锁
一、全局解释器锁(GIL)   1、什么是全局解释器锁       在同一个进程中只要有一个线程获取了全局解释器(cpu)的使用权限,那么其他的线程就必须等待该线程的全局解释器(cpu)使    用权消失后才能使用全局解释器(cpu),即时多个线程直接不会相互影响在同一个进程下也只有一个线程使用cpu,这样的机制称为全局    解释器锁(GIL)。  
Go:分布式锁实现原理与最佳实践
分布式锁应用场景 很多应用场景是需要系统保证幂等性的(如api服务或消息消费者),并发情况下或消息重复很容易造成系统重入,那么分布式锁是保障幂等的一个重要手段。 另一方面,很多抢单场景或者叫交易撮合场景,如dd司机抢单或唯一商品抢拍等都需要用一把“全局锁”来解决并发造成的问题。在防止并发情况下造成库存超卖的场景,也常用分布式锁来解决。 实现
synchronized锁升级过程
1.前置知识:    1.1 JAVA对象的内存布局            hotspot虚拟机中,普通对象在堆中的存储可以划分成三部分:对象头(包含了MarkWord和类型指针)、实例例数据和padding。JAVA对象的内存布局MarkWord的长度为4byte/8byte,用于存储对象自身的运行时数据
Zookeeper分布式锁?
客户端A要获取分布式锁的时候首先到locker下创建一个临时顺序节点(node_n),然后立即获取locker下的所有(一级)子节点。此时因为会有多个客户端同一时间争取锁,因此locker下的子节点数量就会大于1。对于顺序节点,特点是节点名称后面自动有一个数字编号,先创建的节点数字编号小于后创建的,因此可以将子节点按照节点名称后缀的数字顺序从小到大排序,这样
notifyAll唤醒线程的范围?
今天看到开源中国上有这样一个问答:假设我有两个对象锁,对象A锁有5个线程在等待,对象B锁有3个线程在等待,对象A锁中的线程执行完,这时调用notifyAll,是唤醒了对象AB两个锁的全部的等待线程还是只唤醒了A锁的5个线程? 1. 方法文档解释通过看该方法文档的解释,可以得出下面结论: notifyAll()中All的含义是所有的线程,而不是所有的锁,只能唤
解决进程死锁——银行家算法透析
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。避免死锁算法中最有代表性的算法是Dijkstra E.W 于1968年提出的银行家算法: 下面我们将从例题中一点一点的分析: 解题: 第一步:
大厂首发!java哨兵模式的作用
引言做了5年开发的我,阿里一直是我心之所向,如今我如愿以偿进入了国内互联网巨头——Alibaba!其实,今年下半年我面试不少互联网企业,像涂鸦智能,百度,京东,腾讯,字节,滴滴,阿里等等都有三井的身影,之后总结出来的针对Java面试的知识点或真题,每个点或题目都是在面试中被问过的,满满干货,诚意分享! 由于整理成了文档,总结的内容比较多,希望大家都能领取一份
推荐程序员面试秘籍!2021年大厂Java岗面试必问
01 JAVA基础 1.1 java知识点 Hashmap 源码级掌握,扩容,红黑树,最小树化容量,hash冲突解决,有些面试官会提出发自灵魂的审问,比如为什么是红黑树,别的树不可以吗;为什么8的时候树化,4不可以吗,等等 concureentHashMap,段锁,如何分段,和hashmap在hash上的区别,性能,等等 HashTable ,同步锁,这块可
窗口只显示一次,窗口置顶
root Toplevel() root.grabset() 窗口锁定在top上 root.focusset() 焦点锁定在top上
MyBatis-Plus
一、MyBatisPlus本文转自 https://www.cnblogs.com/lyh/p/12859477.html,如有侵权,请联系删除。 1、简介  MyBatisPlus 是一个 Mybatis 增强版工具,在 MyBatis 上扩充了其他功能没有改变其基本功能,为了简化开发提交效率而存在。官网文档地址:   https://mp.baomid
面试百度和美团,竟然问我多线程安全问题,正好撞在我知识点上
解决多线程安全问题无非两个方法synchronized和lock 具体原理以及如何 获取锁AQS算法本篇文章主要讲了lock的原理 就是AQS算法,还有个姊妹篇 讲解synchronized的实现原理 也是阿里经常问的,一定要看后面的文章,先说结论:非公平锁tryAcquire的流程是:检查state字段,若为0,表示锁未被占用,那么尝试占用,若不为0,检查
面试字节跳动Java工程师该怎么准备?值得收藏!
性能调优影响MySQLServer 性能的相关因素1. 商业需求对性能的影响2. 系统架构及实现对性能的影响3. Query语句对系统性能的影响4. Schema设计对系统的性能影响5. 硬件环境对系统性能的影响MySQL 数据库锁定机制1. MySQL锁定机制简介2. 各种锁定机制分析3. 合理利用锁机制优化MySQLMySQL数据库Que
高级java面试题,附答案+考点
蚂蚁金服一面1. 两分钟的自我介绍2. 二叉搜索树和平衡二叉树有什么关系,强平衡二叉树(AVL 树)和弱平衡二叉树 (红黑树)有什么区别3. B 树和 B+树的区别,为什么 MySQL 要使用 B+树4. HashMap 如何解决 Hash 冲突5. epoll 和 poll 的区别,及其应用场景6. 简述线程池原理,FixedThreadPoo