什么是分布式锁?实现方式有哪些?

2021-03-29 16:34发布

7条回答
武新雨 - 一个很酷的人。
2楼 · 2021-03-29 18:57

在回答这个问题之前,我们先回答一下什么是锁。

普通的锁,即在单机多线程环境下,当多个线程需要访问同一个变量或代码片段时,被访问的变量或代码片段叫做临界区域,我们需要控制线程一个一个的顺序执行,否则会出现并发问题。

如何控制呢?就是设置一个各个线程都能看的见的标志。然后,每个线程想访问临界区域时,都要先查看标志,如果标志没有被占用,则说明目前没有线程在访问临界区域。如果标志被占用了,则说明目前有线程正在访问临界区域,则当前线程需要等待。

这个标志,就是锁。

在单机多线程的java程序中,我们可以使用堆内存中的变量作为标志,因为多线程是共享堆内存的,堆内存中的变量对于各个线程都是可见的。


讲明白了普通的锁,接下来,我们再看看分布式锁。


在分布式环境下,即多台计算机,每个计算机上会启动jvm执行程序的运行环境下,如果不同计算机上的线程想访问临界区域时,该怎么办呢?


前面普通锁的使用堆内存中的变量的方式肯定不适用了。因为在多机环境下,某台计算机上的堆内存中的变量对于其他计算机上的线程肯定是不可见的。那么,根据锁的本质和原理,我们就要找到另外的对于多机上的线程都可见的标志,以它来作为锁,就可以了。这样的锁,就是分布式锁。


当然,这里只是解释了什么是分布式锁,至于分布式锁该如何实现,其实有多重方式,关键在于要保证锁对多机上的程序是可见的即可。一些常用的实现方式是,使用redis,使用数据库等等。




实现:

  • 基于数据库实现分布式锁;

  • 基于缓存(Redis等)实现分布式锁;

  • 基于Zookeeper实现分布式锁;


有点好奇
3楼 · 2021-03-30 08:49

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。

在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。

实现方式有

1、基于数据库实现分布式锁;

2、基于缓存(Redis等)实现分布式锁;

3、基于Zookeeper实现分布式锁。

从性能角度(从高到低)来看:“缓存方式>Zookeeper方式>=数据库方式”。

三岁奶猫
4楼 · 2021-03-30 13:39

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。

灰机带翅膀
5楼 · 2021-04-02 13:43

分布式锁的三种实现方式

1. 基于数据库实现分布式锁
2. 基于缓存(例如Redis)实现分布式锁
3. 基于Zookeeper实现分布式锁


我的网名不再改
6楼 · 2021-04-04 14:05

在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。那具体什么是分布式锁,分布式锁应用在哪些业务场景、如何来实现分布式锁呢?

一 为什么要使用分布式锁

我们在开发应用的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的锁进行处理,并且可以完美的运行,毫无Bug!

注意这是单机应用,后来业务发展,需要做集群,一个应用需要部署到几台机器上然后做负载均衡,大致如下图:

上图可以看到,变量A存在三个服务器内存中(这个变量A主要体现是在一个类中的一个成员变量,是一个有状态的对象),如果不加任何控制的话,变量A同时都会在分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的!即使不是同时发过来,三个请求分别操作三个不同内存区域的数据,变量A之间不存在共享,也不具有可见性,处理的结果也是不对的!

如果我们业务中确实存在这个场景的话,我们就需要一种方法解决这个问题!

为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用并发处理相关的功能进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的应用并不能提供分布式锁的能力。为了解决这个问题就需要一种跨机器的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

二、分布式锁应该具备哪些条件

在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件:

1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

三、分布式锁的三种实现方式

目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。

在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。

基于数据库实现分布式锁;
基于缓存(Redis等)实现分布式锁;
基于Zookeeper实现分布式锁;

四、基于数据库的实现方式

基于数据库的实现方式的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。

(1)创建一个表:

复制代码

DROP TABLE IF EXISTS `method_lock`;CREATE TABLE `method_lock` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',
  `desc` varchar(255) NOT NULL COMMENT '备注信息',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,  PRIMARY KEY (`id`),  UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

复制代码

 

(2)想要执行某个方法,就使用这个方法名向表中插入数据:

INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '测试的methodName');

因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。

(3)成功插入则获取锁,执行完成后删除对应的行数据释放锁:

delete from method_lock where method_name ='methodName';

注意:这只是使用基于数据库的一种方法,使用数据库实现分布式锁还有很多其他的玩法!

使用基于数据库的这种实现方式很简单,但是对于分布式锁应该具备的条件来说,它有一些问题需要解决及优化:

1、因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换;

2、不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;

3、没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;

4、不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。

5、在实施的过程中会遇到各种不同的问题,为了解决这些问题,实现方式将会越来越复杂;依赖数据库需要一定的资源开销,性能问题需要考虑。

五、基于Redis的实现方式

1、选用Redis实现分布式锁原因:

(1)Redis有很高的性能;
(2)Redis命令对此支持较好,实现起来比较方便

2、使用命令介绍:

(1)SETNX

SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。

(2)expire

expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。

(3)delete

delete key:删除key

在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。

3、实现思想:

(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。

(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

4、 分布式锁的简单实现代码:

复制代码

#连接redis
redis_client = redis.Redis(host="localhost",
                           port=6379,
                           password=password,
                           db=10)

#获取一个锁
lock_name:锁定名称
acquire_time: 客户端等待获取锁的时间
time_out: 锁的超时时间
def acquire_lock(lock_name, acquire_time=10, time_out=10):
    """获取一个分布式锁"""
    identifier = str(uuid.uuid4())    end = time.time() + acquire_time
    lock = "string:lock:" + lock_name    while time.time() < end:        if redis_client.setnx(lock, identifier):
            # 给锁设置超时时间, 防止进程崩溃导致其他进程无法获取锁
            redis_client.expire(lock, time_out)            return identifier
        elif not redis_client.ttl(lock):
            redis_client.expire(lock, time_out)
        time.sleep(0.001)    return False

#释放一个锁
def release_lock(lock_name, identifier):
    """通用的锁释放函数"""
    lock = "string:lock:" + lock_name
    pip = redis_client.pipeline(True)    while True:
        try:
            pip.watch(lock)
            lock_value = redis_client.get(lock)            if not lock_value:                return True            if lock_value.decode() == identifier:
                pip.multi()
                pip.delete(lock)
                pip.execute()                return True
            pip.unwatch()            break
        except redis.excetions.WacthcError:
            pass    return False

复制代码

5、测试刚才实现的分布式锁

例子中使用50个线程模拟秒杀一个商品,使用–运算符来实现商品减少,从结果有序性就可以看出是否为加锁状态。

复制代码

def seckill():
    identifier=acquire_lock('resource')    print(Thread.getName(),"获得了锁")
    release_lock('resource',identifier)for i in range(50):
    t = Thread(target=seckill)
    t.start()

复制代码

六、基于ZooKeeper的实现方式

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:

(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。

优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。

缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。

七、总结


上面的三种实现方式,没有在所有场合都是完美的,所以,应根据不同的应用场景选择最适合的实现方式。

在分布式环境中,对资源进行上锁有时候是很重要的,比如抢购某一资源,这时候使用分布式锁就可以很好地控制资源。
当然,在具体使用中,还需要考虑很多因素,比如超时时间的选取,获取锁时间的选取对并发量都有很大的影响,上述实现的分布式锁也只是一种简单的实现,主要是一种思想


小小123
7楼 · 2021-04-19 10:19

在回答这个问题之前,我们先回答一下什么是锁。

普通的锁,即在单机多线程环境下,当多个线程需要访问同一个变量或代码片段时,被访问的变量或代码片段叫做临界区域,我们需要控制线程一个一个的顺序执行,否则会出现并发问题。

如何控制呢?就是设置一个各个线程都能看的见的标志。然后,每个线程想访问临界区域时,都要先查看标志,如果标志没有被占用,则说明目前没有线程在访问临界区域。如果标志被占用了,则说明目前有线程正在访问临界区域,则当前线程需要等待。

这个标志,就是锁。


小光光321
8楼 · 2022-05-17 10:52

常用的四种方案:

1. 基于数据库表做乐观锁,用于分布式锁。

2. 使用memcached的add()方法,用于分布式锁。

3. 使用redis的setnx()、expire()方法,用于分布式锁。

4. 使用redis的setnx()、get()、getset()方法,用于分布式锁。

不常用但是可以用于技术方案探讨的:

1. 使用memcached的cas()方法,用于分布式锁。

2. 使用redis的watch、multi、exec命令,用于分布式锁。

3. 使用zookeeper,用于分布式锁。


相关问题推荐

  • 回答 36

    看军事新闻的同学应该都知道,一艘航空母舰作战能力虽然很强,但是弱点太明显,就是防御能力太差,单艘的航空母舰很少单独行动,通常航空母舰战斗群才是主要军事力量,你可以把单艘航母理解为的单体应用(防御差,机动性不好),把航母战斗群(调度复杂,维护...

  • 回答 31

    初始化过程细节:首先进行的就是将服务装载到容器中,然后准备注册服务。和Spring中启动过程类似,Spring启动时,将bean装载进容器中的时候,首先要解析bean。所以dubbo也是先读配置文件解析服务。解析服务:基于dubbo.jar内的META-INF/spring.handlers配置,...

  • 回答 27

    分布式事务 指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上 。换成比较容易理解的话,就是多个事务之间再保持事务的特性,也就是多个事务之间保证结果的一致性。分布式事务解决方案1、基于XA协议的两阶...

  • 什么是接口幂等性?2021-02-24 18:21
    回答 25

    1. 接口调用存在的问题        现如今我们的系统大多拆分为分布式SOA,或者微服务,一套系统中包含了多个子系统服务,而一个子系统服务往往会去调用另一个服务,而服务调用服务无非就是使用RPC通信或者restful,既然是通信,那么就有可能在服务器处理...

  • 回答 21

    前提是另外一个bean在bean容器中能找到

  • 回答 22

    1.View——表示层1.1准备数据实现方式:struts,servlet等1.2显示数据实现方式:extjs,jsp,jquery,html等2.Service——业务层实现方式:drools等3.Dao——数据访问层实现方式:hibernate、mybatis等...

  • SpringCloud Netflix和Sprin2020-11-12 09:41
    回答 8
    已采纳

    SpringCloud Netflix和SpringCloud Alibaba的区别如下图:

  • 回答 16

    哨兵(Sentinel)是 redis 的高可用性解决方案,前面我们讲的主从复制它是高可用的基础,需要人工介入才能完成故障转移,哨兵可以解决这个问题,在主从复制情况下,当主节点发生故障时,哨兵可以自动的发现故障并且完成故障转移,实现真正的 redis 高可用。在...

  • 回答 8

    redis为什么会有高并发问题redis的出身决定Redis是一种单线程机制的nosql数据库,基于key-value,数据可持久化落盘。由于单线程所以redis本身并没有锁的概念,多个客户端连接并不存在竞争关系,但是利用jedis等客户端对redis进行并发访问时会出现问题。发生连...

  • 回答 8

    用dubbo是想利用分布式集群的形式来提高服务的并发量,适用与大型项目.如果不用它还想提高并发另一个解决方案是springCloud+微服务.适合大中小型项目.当前对并发要求的项目还是很多的,所以dubbo用的也相对较多.建议如果是初学者可以跳过dubbo直接学习第二个方...

  • 回答 4

    不需要。一般是service分出去。然后其它放在web层即一个jar为service业务处理,一个为web层war包

  • 回答 6

     springmvc位于表现层,主要与浏览器进行交互(接收和响应浏览器请求)。springmvc采用MVC设计模型,模型由model、view和controller组成。         model(模型):对应JavaBean         view(视图):对应JSP         controller(控制器)...

没有解决我的问题,去提问