分布式锁概念及实现方式

分布式锁概念

什么是锁?

  • 在单进程的系统中,当存在多个线程能够同时改变某个变量(可变共享变量)时,就须要对变量或代码块作同步,使其在修改这种变量时可以线性执行,以防止并发修改变量带来不可控的结果。
  • 同步的本质是经过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么须要在某个地方作个标记,这个标记必须每一个线程都能看到,当标记不存在时能够设置该标记,其他后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。这个标记能够理解为锁。
  • 不一样地方实现锁的方式也不同,只要能知足全部线程都能看获得标记便可。如 Java 中 synchronize 是在对象头设置标记,Lock 接口的实现类基本上都只是某一个 volitile 修饰的 int 型变量其保证每一个线程都能拥有对该 int 的可见性和原子修改,linux 内核中也是利用互斥量或信号量等内存数据作标记。
  • 除了利用内存数据作锁其实任何互斥的都能作锁(只考虑互斥状况),如流水表中流水号与时间结合作幂等校验能够看做是一个不会释放的锁,或者使用某个文件是否存在做为锁等。只须要知足在对标记进行修改能保证原子性和内存可见性便可。

什么是分布式锁?

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

分布式锁应该有什么特性?

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

分布式锁的几种实现方式

目前分布式锁的实现方式主要采用如下三种:linux

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

尽管有这三种方案,可是不一样的业务也要根据本身的状况进行选型,他们之间没有最好只有更适合!git

基于数据库实现分布式锁:

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

1.建立一个表:redis

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作了惟一性约束,这里若是有多个请求同时提交到数据库的话,数据库会保证只有一个操做能够成功,那么咱们就能够认为操做成功的那个线程得到了该方法的锁,能够执行方法体内容。sql

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

delete from method_lock where method_name ='methodName';
复制代码

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

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

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

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

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

四、不具有阻塞锁特性,获取不到锁直接返回失败,因此须要优化获取逻辑,循环屡次去获取。

五、在实施的过程当中会遇到各类不一样的问题,为了解决这些问题,实现方式将会愈来愈复杂;依赖数据库须要必定的资源开销,性能问题须要考虑。

基于redis实现分布式锁:

一、选用Redis实现分布式锁缘由:

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

二、实现思想:

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

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

(3)释放锁的时候,经过UUID判断是否是该锁,如果该锁,则执行delete进行锁释放。

三、使用命令介绍:

SETNX:

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

EXPIRE:

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

DELETE:

delete key:#删除key
复制代码

若是在 setnx 和 expire 之间服务器进程忽然挂掉了,多是由于机器掉电或者是被人为杀掉的,就会致使 expire 得不到执行,也会形成死锁。因此可使用如下指令使得setnx和expire在同一条指令中执行:

set lock:codehole value ex 5 nx
复制代码

四、实现代码:

//可重入锁
public class RedisWithReentrantLock {

  private ThreadLocal<Map<String, Integer>> lockers = new ThreadLocal<>();

  private Jedis jedis;

  public RedisWithReentrantLock(Jedis jedis) {
    this.jedis = jedis;
  }

  private boolean _lock(String key) {
    return jedis.set(key, "", "nx", "ex", 5L) != null;
  }

  private void _unlock(String key) {
    jedis.del(key);
  }

  private Map<String, Integer> currentLockers() {
    Map<String, Integer> refs = lockers.get();
    if (refs != null) {
      return refs;
    }
    lockers.set(new HashMap<>());
    return lockers.get();
  }

  public boolean lock(String key) {
    Map<String, Integer> refs = currentLockers();
    Integer refCnt = refs.get(key);
    if (refCnt != null) {
      refs.put(key, refCnt + 1);
      return true;
    }
    boolean ok = this._lock(key);
    if (!ok) {
      return false;
    }
    refs.put(key, 1);
    return true;
  }

  public boolean unlock(String key) {
    Map<String, Integer> refs = currentLockers();
    Integer refCnt = refs.get(key);
    if (refCnt == null) {
      return false;
    }
    refCnt -= 1;
    if (refCnt > 0) {
      refs.put(key, refCnt);
    } else {
      refs.remove(key);
      this._unlock(key);
    }
    return true;
  }

  public static void main(String[] args) {
    Jedis jedis = new Jedis();
    RedisWithReentrantLock redis = new RedisWithReentrantLock(jedis);
    System.out.println(redis.lock("codehole"));
    System.out.println(redis.lock("codehole"));
    System.out.println(redis.unlock("codehole"));
    System.out.println(redis.unlock("codehole"));
  }

}

/** * 分布式锁的简单实现代码 * Created by liuyang on 2017/4/20. */
public class DistributedLock {

    private final JedisPool jedisPool;

    public DistributedLock(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    /** * 加锁 * @param lockName 锁的key * @param acquireTimeout 获取超时时间 * @param timeout 锁的超时时间 * @return 锁标识 */
    public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
        Jedis conn = null;
        String retIdentifier = null;
        try {
            // 获取链接
            conn = jedisPool.getResource();
            // 随机生成一个value
            String identifier = UUID.randomUUID().toString();
            // 锁名,即key值
            String lockKey = "lock:" + lockName;
            // 超时时间,上锁后超过此时间则自动释放锁
            int lockExpire = (int) (timeout / 1000);

            // 获取锁的超时时间,超过这个时间则放弃获取锁
            long end = System.currentTimeMillis() + acquireTimeout;
            while (System.currentTimeMillis() < end) {
                if (jedis.set(key, "", "nx", "ex", 5L) != null) {
                    retIdentifier = identifier;
                    return retIdentifier;
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retIdentifier;
    }

    /** * 释放锁 * @param lockName 锁的key * @param identifier 释放锁的标识 * @return */
    public boolean releaseLock(String lockName, String identifier) {
        Jedis conn = null;
        String lockKey = "lock:" + lockName;
        boolean retFlag = false;
        try {
            conn = jedisPool.getResource();
            while (true) {
                // 监视lock,准备开始事务
                conn.watch(lockKey);
                // 经过前面返回的value值判断是否是该锁,如果该锁,则删除,释放锁
                if (identifier.equals(conn.get(lockKey))) {
                    Transaction transaction = conn.multi();
                    transaction.del(lockKey);
                    List<Object> results = transaction.exec();
                    if (results == null) {
                        continue;
                    }
                    retFlag = true;
                }
                conn.unwatch();
                break;
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retFlag;
    }
}
复制代码

使用这种方式实现分布式锁在集群模式下会有必定的问题,好比在 Sentinel 集群中,主节点挂掉时,从节点会取而代之,客户端上却并无明显感知。原先第一个客户端在主节点中申请成功了一把锁,可是这把锁尚未来得及同步到从节点,主节点忽然挂掉了。而后从节点变成了主节点,这个新的节点内部没有这个锁,因此当另外一个客户端过来请求加锁时,当即就批准了。这样就会致使系统中一样一把锁被两个客户端同时持有,不安全性由此产生。

为了解决这个问题,Antirez 发明了 Redlock 算法,加锁时,它会向过半节点发送 set(key, value, nx=True, ex=xxx) 指令,只要过半节点 set 成功,那就认为加锁成功。释放锁时,须要向全部节点发送 del 指令。不过 Redlock 算法还须要考虑出错重试、时钟漂移等不少细节问题,同时由于 Redlock 须要向多个节点进行读写,意味着相比单实例 Redis 性能会降低一些。

基于zookeeper实现的分布式锁:

在使用zookeeper实现分布式锁的以前,须要先了解zookeeper的两个特性,第一个是zookeeper的节点类型,第二就是zookeeper的watch机制:

zookeeper的节点类型:

PERSISTENT 持久化节点

PERSISTENT_SEQUENTIAL 顺序自动编号持久化节点,这种节点会根据当前已存在的节点数自动加 1

EPHEMERAL 临时节点, 客户端session超时这类节点就会被自动删除

EPHEMERAL_SEQUENTIAL 临时自动编号节点

zookeeper的watch机制:

Znode发生变化(Znode自己的增长,删除,修改,以及子Znode的变化)能够经过Watch机制通知到客户端。那么要实现Watch,就必须实现org.apache.zookeeper.Watcher接口,而且将实现类的对象传入到能够Watch的方法中。Zookeeper中全部读操做(getData(),getChildren(),exists())均可以设置Watch选项。Watch事件具备one-time trigger(一次性触发)的特性,若是Watch监视的Znode有变化,那么就会通知设置该Watch的客户端。

zookeeper实现排他锁:

定义锁:

在一般的java并发编程中,有两种常见的方式能够用来定义锁,分别是synchronized机制和JDK5提供的ReetrantLock。然而,在zookeeper中,没有相似于这样的API能够直接使用,而是经过Zookeeper上的数据节点来表示一个锁,例如/exclusive_lock/lock节点就能够定义为一个锁。

获取锁:

在须要获取排他锁的时候,全部的客户端都会试图经过调用create()接口,在/exclusive_lock节点下建立临时子节点/exclusive_lock/lock。zookeeper会保证在全部客户端中,最终只有一个客户端可以建立成功,那么就能够认为该客户端得到了锁。同时,全部没有得到锁的客户端就须要到/exclusive_lock节点上注册一个子节点变动的Watcher监听,以便实时监听到lock节点的变动状况。

释放锁:

在定义锁部分,咱们已经提到,/exclusive_lock/lock是一个临时节点,所以在如下两种状况下,都有可能释放锁。

1.当前获取锁的客户端发生宕机,那么Zookeeper上的这个临时节点就会被移除。

2.正常执行完业务逻辑以后,客户端就会主动将本身建立的临时节点删除

不管在什么状况下移除了lock节点,Zookeeper都会通知全部在/exclusive_lock节点上注册了子节点变动Watcher监听的客户端。这些客户端在接收到通知后,再次从新发起分布式锁获取,即重复“获取锁”的过程:

zookeeper实现共享锁:

定义锁:

和排他锁同样,一样是经过zookeeper上的数据节点来表示一个锁,是一个相似于"/shared_lock/[hostname]-请求类型-序号"的临时顺序节点,例如/shared_lock/192.168.0.1-R-000000001,那么这个节点就表明了一个共享锁。

获取锁:

1.客户端调用create()方法建立一个相似于"/shared_lock/[hostname]-请求类型-序号"的临时顺序节点。

2.客户端调用getChildren()接口来获取全部已经建立的子节点列表。

3.肯定本身的节点序号在全部子节点中的顺序

​ 对于读请求:

​ 若是没有比本身序号小的子节点,或是全部比本身序号小的 子节点都是读请求,那么代表已经成功获取到了共享锁,同时开始执行读取逻辑。

​ 若是比本身序号小的子节点中有写请求,那么就须要进入等待。向比本身序号小的最后一个写请求节点注册Watcher监听。

​ 对于写请求:

​ 若是本身不是序号最小的节点,那么就须要进入等待。向比本身序号小的最后一个节点注册Watcher监听

4.等待Watcher通知,继续进入步骤2

释放锁:

在定义锁部分,咱们已经提到,/exclusive_lock/lock是一个临时节点,所以在如下两种状况下,都有可能释放锁。

1.当前获取锁的客户端发生宕机,那么Zookeeper上的这个临时节点就会被移除。

2.正常执行完业务逻辑以后,客户端就会主动将本身建立的临时节点删除

经常使用的分布式锁组件:

mykit-lock

mykit架构中独立出来的mykit-lock组件,旨在提供高并发架构下分布式系统的分布式锁架构。

GitHub地址:github.com/sunshinelyz…

参考资料:

从PAXOS到ZOOKEEPER分布式一致性原理和实践

blog.csdn.net/xlgen157387…

掘金小册:Redis深度探险:核心原理与应用实践

blog.csdn.net/tzs_1041218…

相关文章
相关标签/搜索