三种使用分布式锁方案

1、背景单体架构中使用同步访问解决多线程并发问题,分布式中须要有其余方案。redis

2、分布式锁的考量算法

  1.能够保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器-上的一个线程执行。
  2.这把锁要是一把可重入锁(避免死锁)
  3.这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
  4.这把锁最好是一把公平锁(根据业务需求考虑要不要这条)
  5.有高可用的获取锁和释放锁功能
      保证了只有锁的持有者才能来解锁,不然任何竞争者都能解锁
  6.获取锁和释放锁的性能要好
    7.若是作的好一点,须要有监控的平台。数据库

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

  1.基于数据库实现排他锁:利用version字段和for update操做获取锁。安全


    优势:易于理解
    问题:
      (1)锁没有失效时间,解锁失败时(宕机等缘由),其余线程获取不到锁。
      解决:作一个定时任务实现自动释放锁。多线程

      (2)锁属于非阻塞,由于获取锁的是insert操做,一旦获取失败就报错,没有得到锁的线程并不会进入排队队列,要想再次得到锁就要再次触发得到锁操做。
      解决:搞一个while循环,直到insert成功再返回成功。架构

      (3)不是可重入锁。
      解决:加入锁的机器字段,实现同一机器可重复加锁。
      另外在解锁时,必须是锁的持有者来解锁,其余竞争者没法解锁并发

      (4)因为是数据库,对性能要求高的应用不合适用此实现。
      解决:数据库自己特性决定。
      (5)在 MySQL 数据库中采用主键冲突防重,在大并发状况下有可能会形成锁表现象。dom

      解决:比较好的办法是在程序中生产主键进行防重分布式

      (6)这把锁是非公平锁,全部等待锁的线程凭运气去争夺锁性能

      解决:再建一张中间表,将等待锁的线程全记录下来,并根据建立时间排序,只有最早建立的容许获取锁。

      (7)考虑到数据库单点故障,须要实现数据库的高可用。

      注意:InnoDB 引擎在加锁的时候,只有经过索引进行检索的时候才会使用行级锁,不然会使用表级锁

    另外存在问题
    (1)行级锁并不必定靠谱:虽然咱们对方法字段名使用了惟一索引,而且显示使用 for update 来使用行级锁。
            可是,MySQL 会对查询进行优化,即使在条件中使用了索引字段,可是否使用索引来检索数据是由 MySQL 经过判断不一样执行计划的代价来决定的,
            若是MySQL 认为全表扫效率更高,好比对一些很小的表,它就不会使用索引,这种状况下 InnoDB 将使用表锁,而不是行锁。这种状况是致命的。

    (2)咱们要使用排他锁来进行分布式锁的 lock,那么一个排他锁长时间不提交,就会占用数据库链接。
      一旦相似的链接变得多了,就可能把数据库链接池撑爆


  2.基于redis实现(单机版):须要本身实现 必定要用 SET key value NX PX milliseconds 命令,而不要使用setnx 加expire


    优势: 性能高、超时失效比数据库简单。
    开源实现:Redis官方提出一种算法,叫Redlock,认为这种实现比普通的单实例实现更安全。
                      RedLock有多种语言的实现包,其中Java版本:Redisson。
    缺点:
    (1)失效时间没法把控。可能设置太短或者过长的状况.若是设置太短,其余线程可能会获取到锁,没法保证状况。过长时其余线程获取不到锁。
         解决:Redisson的思路:客户端起一个后台线程,快到期时自动续期,若是宕机了,后台线程也没有了。

    (2)若是采用 Master-Slave 模式,若是 Master 节点故障了,发生主从切换,主从切换的一瞬间,可能出现锁丢失的问题。
         解决:Redisson ,但存在争议的,不过应该问题不大。

 

  3.基于zookeeper实现(推荐):可靠性好,使用最普遍。实现:Curator

  4.基于etcd的实现:优于zookeeper实现,若是项目中应用了etcd,那么使用etcd。

    5.Spring Integration 实现了分布式锁:
    Gemfire
    JDBC
    Redis
    Zookeeper

 

基于数据库实现排他锁


方案1


获取锁

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

对method_name作了惟一性约束,这里若是有多个请求同时提交到数据库的话,数据库会保证只有一个操做能够成功。

方案2

 1 DROP TABLE IF EXISTS `method_lock`;  2 CREATE TABLE `method_lock` (  3   `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',  4   `method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',  5   `state` tinyint NOT NULL COMMENT '1:未分配;2:已分配',  6   `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,  7   `version` int NOT NULL COMMENT '版本号',  8  `PRIMARY KEY (`id`),  9  UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE 10 ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

 

先获取锁的信息
     select id, method_name, state,version from method_lock where state=1 and method_name='methodName';

占有锁
       update t_resoure set state=2, version=2, update_time=now() where method_name='methodName' and state=1 and version=2;

若是没有更新影响到一行数据,则说明这个资源已经被别人占位了。

缺点:

    一、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会致使业务系统不可用。
    二、这把锁没有失效时间,一旦解锁操做失败,就会致使锁记录一直在数据库中,其余线程没法再得到到锁。
    三、这把锁只能是非阻塞的,由于数据的insert操做,一旦插入失败就会直接报错。没有得到锁的线程并不会进入排队队列,要想再次得到锁就要再次触发得到锁操做。
    四、这把锁是非重入的,同一个线程在没有释放锁以前没法再次得到该锁。由于数据中数据已经存在了。

解决方案:
     一、数据库是单点?搞两个数据库,数据以前双向同步。一旦挂掉快速切换到备库上。
     二、没有失效时间?只要作一个定时任务,每隔必定时间把数据库中的超时数据清理一遍。
     三、非阻塞的?搞一个while循环,直到insert成功再返回成功。
     四、非重入的?在数据库表中加个字段,记录当前得到锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,若是当前机器的主机信息和线程信息在数据库能够查到的话,直接把锁分配给他就能够了。

基于redis实现

   获取锁:
        SET resource_name my_random_value NX PX 30000

      解锁方式一:不可用
    if( (GET user_id) == "XXX" ){ //获取到本身锁后,进行取值判断且判断为真。此时,这把锁刚好失效。
    DEL user_id
    }
    因为GET取值判断和DEL删除并不是原子操做,当程序判经过该锁的值判断发现这把锁是本身加上的,准备DEL。
    此时该锁刚好失效,而另一个请求刚好得到key值为user_id的锁。
    此时程序执行了了DEL user_id,删除了别人加的锁,尴尬!

  解锁方式二(推荐):为了保证查询和删除的原子性操做,须要引入lua脚本支持。

    

    if redis.call('get',KEYS[1]) == ARGV[1] then 
           return redis.call('del',KEYS[1]) 
    else
      return 0 
    end

 



使用zookeeper实现分布式锁

zookeeper分布式锁应用了临时顺序节点

获取锁

首先,在Zookeeper当中建立一个持久节点ParentLock。当第一个客户端想要得到锁时,须要在ParentLock这个节点下面建立一个临时顺序节点 Lock1。

 

以后,Client1查找ParentLock下面全部的临时顺序节点并排序,判断本身所建立的节点Lock1是否是顺序最靠前的一个。若是是第一个节点,则成功得到锁。

这时候,若是再有一个客户端 Client2 前来获取锁,则在ParentLock下载再建立一个临时顺序节点Lock2。 

Client2查找ParentLock下面全部的临时顺序节点并排序,判断本身所建立的节点Lock2是否是顺序最靠前的一个,结果发现节点Lock2并非最小的。

因而,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。

这时候,若是又有一个客户端Client3前来获取锁,则在ParentLock下载再建立一个临时顺序节点Lock3。 

 

Client3查找ParentLock下面全部的临时顺序节点并排序,判断本身所建立的节点Lock3是否是顺序最靠前的一个,结果一样发现节点Lock3并非最小的。

因而,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3一样抢锁失败,进入了等待状态。

 

这样一来,Client1获得了锁,Client2监听了Lock1,Client3监听了Lock2。这偏偏造成了一个等待队列,很像是Java当中ReentrantLock所依赖的AQS(AbstractQueuedSynchronizer)。

得到锁的过程大体就是这样,那么Zookeeper如何释放锁呢?

释放锁的过程很简单,只须要释放对应的子节点就好。

释放锁

释放锁分为两种状况:

1.任务完成,客户端显示释放

当任务完成时,Client1会显示调用删除节点Lock1的指令。

 

2.任务执行过程当中,客户端崩溃

得到锁的Client1在任务执行过程当中,若是Duang的一声崩溃,则会断开与Zookeeper服务端的连接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。 

 

因为Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会马上收到通知。这时候Client2会再次查询ParentLock下面的全部节点,确认本身建立的节点Lock2是否是目前最小的节点。若是是最小,则Client2瓜熟蒂落得到了锁。 

 

同理,若是Client2也由于任务完成或者节点崩溃而删除了节点Lock2,那么Client3就会接到通知。 

 

最终,Client3成功获得了锁。 

相关文章
相关标签/搜索