分布式锁的实现

1 分布式锁的疑问

谈到分布式锁,有很多实现方式,如数据库、redis、ZooKeeper等。提个问题:

  • 实现分布式锁需要满足哪些条件呢?

2 数据库实现分布式锁

2.1 实现案例

如使用数据库事务中的锁如record lock来实现,如下所示

1 获取锁

public void lock(){
    connection.setAutoCommit(false)
    int count = 0;
    while(count < 4){
        try{
            select * from lock where lock_name=xxx for update;
            if(结果不为空){
                //代表获取到锁
                return;
            }
        }catch(Exception e){

        }
        //为空或者抛异常的话都表示没有获取到锁
        sleep(1000);
        count++;
    }
    throw new LockException();
}

2 释放锁

public void release(){
    connection.commit();
}

数据库的lock表,lock_name是主键,通过for update操作,数据库就会对该行记录加上record lock,从而阻塞其他人对该记录的操作。

一旦获取到了锁,就可以开始执行业务逻辑,最后通过connection.commit()操作来释放锁。

其他没有获取到锁的就会阻塞在上述select语句上,可能的结果有2种,在超时之前获取到了锁,在超时之前仍未获取到锁(这时候会抛出超时异常,然后进行重试)

数据库当然还有其他方式,如插入一个有唯一约束的数据。成功插入则表示获取到了锁,释放锁就是删除该记录。该方案也有很多问题要解决

2.2 存在的问题

首先性能不是特别高。

通过数据库的锁来实现多进程之间的互斥,但是这貌似也有一个问题:就是sql超时异常的问题

jdbc超时具体有3种超时,具体见深入理解JDBC的超时设置

  • 框架层的事务超时
  • jdbc的查询超时
  • Socket的读超时

这里只涉及到后2种的超时,jdbc的查询超时还好(mysql的jdbc驱动会向服务器发送kill query命令来取消查询),如果一旦出现Socket的读超时,对于如果是同步通信的Socket连接来说(底层实现Connection的可能是同步通信也可能是异步通信),该连接基本上不能使用了,需要关闭该连接,从新换用新的连接,因为会出现请求和响应错乱的情况,比如jedis出现的类型转换异常,详见Jedis的类型转换异常深究

3 redis实现分布式锁

而redis通常可以使用setnx来实现分布式锁

3.1 基本版

1 获取锁

public void lock(){
    for(){
        ret = setnx lock_ley (current_time + lock_timeout)
        if(ret){
            //获取到了锁
            break;
        }
        //没有获取到锁
        sleep(100);
    }
}

2 释放锁

public void release(){
    del lock_ley
}

setnx来创建一个key,如果key不存在则创建成功返回1,如果key已经存在则返回0。依照上述来判定是否获取到了锁

获取到锁的执行业务逻辑,完毕后删除lock_key,来实现释放锁

其他未获取到锁的则进行不断重试,直到自己获取到了锁

3.2 改进版

上述逻辑在正常情况下是OK的,但是一旦获取到锁的客户端挂了,没有执行上述释放锁的操作,则其他客户端就无法获取到锁了,所以在这种情况下有2种方式来解决:

  • 为lock_key设置一个过期时间
  • 对lock_key的value进行判断是否过期

以第一种为例,在set键值的时候带上过期时间,即使挂了,也会在过期时间之后,其他客户端能够重新竞争获取锁

public void lock(){
    while(true){
        ret = set lock_key identify_value nx ex lock_timeout
        if(ret){
            //获取到了锁
            return;
        }
        sleep(100);
    }
}

public void release(){
    value = get lock_key
    if(identify_value == value){
        del lock_key
    }
}

以第二种为例,一旦发现lock_key的值已经小于当前时间了,说明该key过期了,然后对该key进行getset设置,一旦getset返回值是原来的过期值,说明当前客户端是第一个来操作的,代表获取到了锁,一旦getset返回值不是原来过期时间则说明前面已经有人修改了,则代表没有获取到锁,详细见用Redis实现分布式锁,改正如下:

# get lock
lock = 0
while lock != 1:
    timestamp = current_unix_time + lock_timeout
    lock = SETNX lock.foo timestamp
    if lock == 1 or (now() > (GET lock.foo) and now() > (GETSET lock.foo timestamp)):
        break;
    else:
        sleep(10ms)

# do your job
do_job()

# release
if now() < GET lock.foo:
    DEL lock.foo

这里看来第二种其实没有第一种比较好。

3.3 问题依旧

问题1: lock timeout的存在也使得失去了锁的意义,即存在并发的现象。一旦出现锁的租约时间,就意味着获取到锁的客户端必须在租约之内执行完毕业务逻辑,一旦业务逻辑执行时间过长,租约到期,就会引发并发问题。所以有lock timeout的可靠性并不是那么的高。

问题2: 上述方式仅仅是redis单机情况下,还存在redis单点故障的问题。如果为了解决单点故障而使用redis的sentinel或者cluster方案,则更加复杂,引入的问题更多。

4 ZooKeeper实现分布式锁

4.1 案例

这也是ZooKeeper客户端curator的分布式锁实现。

1 获取锁

public void lock(){
    path = 在父节点下创建临时顺序节点
    while(true){
        children = 获取父节点的所有节点
        if(path是children中的最小的){
            代表获取了节点
            return;
        }else{
            添加监控前一个节点是否存在的watcher
            wait();
        }
    }
}

watcher中的内容{
    notifyAll();
}

2 释放锁

public void release(){
    删除上述创建的节点
}

4.2 总结

ZooKeeper版本的分布式锁问题相对比较来说少。

  • 锁的占用时间限制:redis就有占用时间限制,而ZooKeeper则没有,最主要的原因是redis目前没有办法知道已经获取锁的客户端的状态,是已经挂了呢还是正在执行耗时较长的业务逻辑。而ZooKeeper通过临时节点就能清晰知道,如果临时节点存在说明还在执行业务逻辑,如果临时节点不存在说明已经执行完毕释放锁或者是挂了。由此看来redis如果能像ZooKeeper一样添加一些与客户端绑定的临时键,也是一大好事。
  • 是否单点故障:redis本身有很多中玩法,如客户端一致性hash,服务器端sentinel方案或者cluster方案,很难做到一种分布式锁方式能应对所有这些方案。而ZooKeeper只有一种玩法,多台机器的节点数据是一致的,没有redis的那么多的麻烦因素要考虑。

总体上来说ZooKeeper实现分布式锁更加的简单,可靠性更高。

5 分布式锁实现原理总结

从上面我们经历了3种实现方式,可以从中总结下,该怎么去回答最初提出的问题。

5.1 分布式锁的实现

在我自己看来有如下3个方面:

  • 怎么获取锁
  • 怎么释放锁
  • 怎么得知锁被释放了

5.1.1 怎么获取锁

能够提供一种方式,多个客户端并发操作,只能有一个客户端能满足相应的要求

如数据库的for update的sql语句、或者插入一个含有唯一约束的数据等

如redis的setnx等

如ZooKeeper的求最小节点的方式

这些都可以保证只能有一个客户端获取到了锁

5.1.2 怎么释放锁

场景一般有2种情况:

  • 1 正常情况下的释放锁
  • 2 异常情况下如何释放锁(即释放锁的操作没有被执行,如挂掉、没执行成功等原因)

如redis正常情况下释放锁是删除lock_key,异常情况下,只能通过lock_key的超时时间了

如ZooKeeper正常情况下释放锁是删除临时节点,异常情况下,服务器也会主动删除临时节点(这种机制就简单多了)

5.1.3 怎么得知锁被释放了

实现方式一般有2种情况:

  • 1 没有获取到锁的客户端不断尝试获取锁
  • 2 服务器端通知客户端锁被释放了

当然第二种情况是最优的(客户端所做的无用功最少),如ZooKeeper通过注册watcher来得到锁释放的通知。而数据库、redis没有办法来通知客户端锁释放了,那客户端就只能傻傻的不断尝试获取锁了。

欢迎来拍砖,相互讨论,我相信会越辩越清晰。

时间: 2024-09-12 20:07:41

分布式锁的实现的相关文章

redis 如何实现分布式锁

问题描述 redis 如何实现分布式锁 redis 使用 GETSET命令实现的分布式锁 必须设置超时时间来防止死锁的发生,但是如果一个线程的执行时间超过了设定的超时时间,锁会被另外一个线程获取,但是当前线程还没执行结束,这样就造成了 多个线程同时可以访问一个资源的问题,请问有办法解决吗?

C#和.NET中的分布式锁服务

背景 分布式锁服务在大家的项目中或许用的不多,因为大家都把排他放在数据库那一层来挡.当大量的行锁.表锁.事务充斥着数据库的时候,不如换个角度思考问题.一般web应用很多的瓶颈都在数据库上,这里给大家介绍的是减轻数据库锁负担的一种方案. 简介 如果我们的需求很简单,例如对于用户的账户资金,要保证原子性操作.并且不同的客户端在同一时间内只能提交一个对象操作.lock.单例?!在单台上还可以,但是大型web项目上,负载均衡是常用的技术手段手段,同一意义的对象可能存在不同的副本,这时我们又如何保证排他操

基于redis实现的分布式锁

  基于redis实现的分布式锁 我们知道,在多线程环境中,锁是实现共享资源互斥访问的重要机制,以保证任何时刻只有一个线程在访问共享资源.锁的基本原理是:用一个状态值表示锁,对锁的占用和释放通过状态值来标识,因此基于redis实现的分布式锁主要依赖redis的SETNX命令和DEL命令,SETNX相当于上锁,DEL相当于释放锁,当然,在下面的具体实现中会更复杂些.之所以称为分布式锁,是因为客户端可以在redis集群环境中向集群中任一个可用Master节点请求上锁(即SETNX命令存储key到re

基于Redis实现分布式锁

背景在很多互联网产品应用中,有些场景需要加锁处理,比如:秒杀,全局递增ID,楼层生成等等.大部分的解决方案是基于DB实现的,Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系.其次Redis提供一些命令SETNX,GETSET,可以方便实现分布式锁机制. Redis命令介绍使用Redis实现分布式锁,有两个重要函数需要介绍 SETNX命令(SET if Not eXists)语法:SETNX key value功能:当且仅当 key 不

Tair分布式锁 实践经验(160805更新)

前言 在JVM中,怎么样防止因多线程并发造成的数据不一致或逻辑混乱?大家一定都能想到锁,能想到java.util.concurrent包下的各种各样的用法. 但在分布式环境下,不同的机器.不同的jvm,那些好用的并发锁工具,并不能满足需要. 所以我们需要有一个分布式锁,来解决分布式环境下的并发问题.而本文正是在这条道路上,走出的一些经验的总结. 我们按照待解决问题的场景,一步一步看下去. 问题一:如何实现一个分布式锁: 锁,大多是基于一个只能被1个线程占用.得到的资源来实现的. JVM的锁,是基

基于Zookeeper的分布式锁

这篇文章只需要你10分钟的时间. 实现分布式锁目前有三种流行方案,分别为基于数据库.Redis.Zookeeper的方案,其中前两种方案网络上有很多资料可以参考,本文不做展开.我们来看下使用Zookeeper如何实现分布式锁. 什么是Zookeeper? Zookeeper(业界简称zk)是一种提供配置管理.分布式协同以及命名的中心化服务,这些提供的功能都是分布式系统中非常底层且必不可少的基本功能,但是如果自己实现这些功能而且要达到高吞吐.低延迟同时还要保持一致性和可用性,实际上非常困难.因此z

分布式锁总结

0 前言 可以先看下之前写的实现分布式锁的方案 分布式锁的实现 然后再来看下下面的总结. 1 设置锁超时时间 redis.数据库等实现的分布式锁,需要设置锁超时时间的原因在于:其他客户端无法得知已经获取锁的客户端的状态 是挂了呢,还是正在执行.所以只能傻傻的设置一个超时,认为超时之后就简单的判定获取锁的客户端挂了. 一旦锁设定了超时时间,可能获取锁的客户端因各种原因执行业务操作的时候耗时较长,超出了锁的超时时间,这时其他客户端就可以再次获取锁了,所以就会带来并发问题. 2 消除锁超时时间 为了消

分布式锁的简单实现

分布式锁在分布式应用当中是要经常用到的,主要是解决分布式资源访问冲突的问题. 一开始考虑采用ReentrantLock来实现,但是实际上去实现的时候,是有问题的,ReentrantLock的lock和unlock要求必须是在同一线程进行,而分布式应用中,lock和unlock是两次不相关的请求,因此肯定不是同一线程,因此导致无法使用ReentrantLock. 接下来就考虑采用自己做个状态来进行锁状态的记录,结果发现总是死锁,仔细一看代码,能不锁死么. ? 1 2 3 4 5 6 7 8 9 1

redis分布式锁的运用和理解

问题描述 redis分布式锁的运用和理解 redis是单进程单线程模式 ,为什么还需要分布式锁?跨jvm是指多个服务器上在运行同一个服务吗? 网上看了很多说跨jvm会需要锁 ,但还是不怎么理解 解决方案 redis分布式锁redis分布式锁-SETNX实现Redis实现分布式锁 解决方案二: redis 3.0也有集群模式了,可以多个机器组成一个整体的cache 解决方案三: redis分布式锁和zookeeper分布式锁都在在企业服务中常用的知识.譬如:你有一个服务程序,需要部署在5台独立的机