分布式锁的实现

本文摘自网上,仅做学习使用!javascript

用一web应用集群,负载均衡部署实现:java

 

在上图能够看到,变量A在JVM一、JVM二、JVM3三个JVM内存中(这个变量A主要体现是在一个类中的一个成员变量,是一个有状态的对象),若是咱们不加任何控制的话,变量A同进都会在JVM分配一块内存,三个请求发过来同时对这个变量进行操做,显然结果不是咱们想要的。web

若是咱们业务中存在这样的场景的话,就须要找到一种方法来解决。redis

为了保证一个方法或属性在高并发的状况下同一时间只能被同一个线程执行,在传统单机部署的状况下,可使用Java并发处理相关的API(如ReentrantLockSynchronized)进行互斥控制。可是,随之业务发展的须要,原单机部署的系统演化成分布式集群系统后,因为分布式系统多线程、多进程而且分布在不一样的机器上,这将原来的单机部署状况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。
为了解决这个问题,就须要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!数据库

分布式锁应该具有哪些条件

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

分布式锁的实现方式

目前几乎全部大型网站及应用都是分布式部署,分布式场景中的数据一致性问题一直是一个比较重要的话题,分布式的CAP理论告诉咱们任何一个分布式系统都没法同时知足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时知足两项
通常状况下,都须要牺牲强一致性来换取系统的高可用性,系统每每只须要保证最终一致性,只要这个最终时间是在用户能够接受的范围内便可。
在不少时候,为了保证数据的最终一致性,须要不少的技术方案来支持,好比分布式事务、分布式锁等。有的时候,咱们须要保证一信方法在同一时间内只能被同一个线程执行。
而分布式锁的具体实现方案有以下三种:缓存

基于数据库实现;
基于缓存(Redis等)实现;
基于Zookeeper实现;服务器

以上尽管有三种方案,可是咱们须要根据不一样的业务进行选型。多线程

数据库锁

基于数据库表

要实现分布式锁,最简单的方式可能就是直接建立一张锁表,而后经过操做该表中的数据来实现了。并发

当咱们要锁住某个方法或资源时,咱们就在该表中增长一条记录,想要释放锁的时候就删除这条记录。负载均衡

CREATE TABLE `methodLock` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名', `desc` varchar(1024) NOT NULL DEFAULT '备注信息', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成', PRIMARY KEY (`id`), UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法'; 

当咱们想要锁住某个方法时,执行如下SQL:

insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)

由于咱们对method_name作了惟一性约束,这里若是有多个请求同时提交到数据库的话,数据库会保证只有一个操做能够成功,那么咱们就能够认为

操做成功的那个线程得到了该方法的锁,能够执行方法体内容。

当方法执行完毕以后,想要释放锁的话,须要执行如下Sql:

delete from methodLock where method_name ='method_name'

上面这种简单的实现有如下几个问题:

一、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会致使业务系统不可用。

二、这把锁没有失效时间,一旦解锁操做失败,就会致使锁记录一直在数据库中,其余线程没法再得到到锁。

三、这把锁只能是非阻塞的,由于数据的insert操做,一旦插入失败就会直接报错。没有得到锁的线程并不会进入排队队列,要想再次得到锁就要再次触发得到锁操做。

四、这把锁是非重入的,同一个线程在没有释放锁以前没法再次得到该锁。由于数据中数据已经存在了。

固然,咱们也能够有其余方式解决上面的问题。

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

基于数据库的排它锁

除了能够经过增删操做数据表中的记录之外,其实还能够借助数据库中自带的锁来实现分布式的锁。

咱们还用刚刚建立的那张数据库表。能够经过数据库的排他锁来实现分布式锁。

在查询语句后面增长for update,数据库会在查询过程当中给数据库表增长排他锁。当某条记录被加上排他锁以后,其余线程没法再在该行记录上增长排他锁。

咱们能够认为得到排它锁的线程便可得到分布式锁,当获取到锁以后,能够执行方法的业务逻辑,执行完方法以后,再经过如下方法解锁:

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

经过connection.commit()操做来释放锁。

这种方法能够有效的解决上面提到的没法释放锁和阻塞锁的问题。

  • 阻塞锁? for update语句会在执行成功后当即返回,在执行失败时一直处于阻塞状态,直到成功。
  • 锁定以后服务宕机,没法释放?使用这种方式,服务宕机以后数据库会本身把锁释放掉。

可是仍是没法直接解决数据库单点和可重入问题。

总结:

总结一下使用数据库来实现分布式锁的方式,这两种方式都是依赖数据库的一张表,一种是经过表中的记录的存在状况肯定当前是否有锁存在,另一种是经过数据库的排他锁来实现分布式锁。

**数据库实现分布式锁的优势: **直接借助数据库,容易理解。

**数据库实现分布式锁的缺点: **会有各类各样的问题,在解决问题的过程当中会使整个方案变得愈来愈复杂。

操做数据库须要必定的开销,性能问题须要考虑。

乐观锁

乐观锁假设认为数据通常状况下不会形成冲突,只有在进行数据的提交更新时,才会检测数据的冲突状况,若是发现冲突了,则返回错误信息

实现方式:

时间戳(timestamp)记录机制实现:给数据库表增长一个时间戳字段类型的字段,当读取数据时,将timestamp字段的值一同读出,数据每更新一次,timestamp也同步更新。当对数据作提交更新操做时,检查当前数据库中数据的时间戳和本身更新前取到的时间戳进行对比,若相等,则更新,不然认为是失效数据。

若出现更新冲突,则须要上层逻辑修改,启动重试机制

一样也可使用version的方式。

性能对比

(1) 悲观锁实现方式是独占数据,其它线程须要等待,不会出现修改的冲突,可以保证数据的一致性,可是依赖数据库的实现,且在线程较多时出现等待形成效率下降的问题。通常状况下,对于数据很敏感且读取频率较低的场景,能够采用悲观锁的方式

(2) 乐观锁能够多线程同时读取数据,若出现冲突,也能够依赖上层逻辑修改,可以保证高并发下的读取,适用于读取频率很高而修改频率较少的场景

(3) 因为库存回写数据属于敏感数据且读取频率适中,因此建议使用悲观锁优化

基于redis的分布式锁

相比较于基于数据库实现分布式锁的方案来讲,基于缓存来实如今性能方面会表现的更好一点。并且不少缓存是能够集群部署的,能够解决单点问题。

首先,为了确保分布式锁可用,咱们至少要确保锁的实现同时知足如下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即便有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其余客户端能加锁。
  3. 具备容错性。只要大部分的Redis节点正常运行,客户端就能够加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端本身不能把别人加的锁给解了。

基于缓存(Redis)的实现方式

使用Redis实现分布式锁的理由:

  1. Redis具备很高的性能;
  2. Redis的命令对此支持较好,实现起来很方便;

Redis命令介绍:

SETNX

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

 

expire

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

 

delete

// 删除key
delete key;

 

咱们经过Redis实现分布式锁时,主要经过上面的这三个命令。

经过Redis实现分布式的核心思想为:

  1. 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间自动释放锁,锁的value值为一个随机生成的UUID,经过这个value值,在释放锁的时候进行判断。
  2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
    3.释放锁的时候,经过UUID判断是否是当前持有的锁,若时该锁,则执行delete进行锁释放。

具体实现代码以下:

public class DistributedLock {

    private final JedisPool jedisPool;
    private final static String KEY_PREF = "lock:"; // 锁的前缀

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

    /**
     * 加锁
     *
     * @param lockName       String 锁的名称(key)
     * @param acquireTimeout long 获取超时时间
     * @param timeout        long 锁的超时时间
     * @return 锁标识
     */
    public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
        Jedis conn = null;

        try {
            // 获取链接
            conn = jedisPool.getResource();
            // 随机生成一个value
            String identifier = UUID.randomUUID().toString();
            // 锁名,即 key值
            String lockKey = KEY_PREF + lockName;
            // 超时时间, 上锁后超过此时间则自动释放锁
            int lockExpire = (int) (timeout / 1000);

            // 获取锁的超时时间,超过这个时间则放弃获取锁
            long end = System.currentTimeMillis() + acquireTimeout;
            while (System.currentTimeMillis() < end) {
                if (conn.setnx(lockKey, identifier) == 1) {
                    conn.expire(lockKey, lockExpire);
                    // 返回value值,用于释放锁时间确认
                    return identifier;
                }

                // 返回-1表明key没有设置超时时间,为key设置一个超时时间
                if (conn.ttl(lockKey) == -1) {
                    conn.expire(lockKey, lockExpire);
                }

                try {
                    Thread.sleep(10);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return null;
    }

    /**
     * 释放锁
     *
     * @param lockName   String 锁key
     * @param identifier String 释放锁的标识
     * @return boolean
     */
    public boolean releaseLock(String lockName, String identifier) {
        Jedis conn = null;
        String lockKey = KEY_PREF + 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 (Exception e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retFlag;
    }
}

 

基于Zookeeper实现分布式锁

基于zookeeper临时有序节点能够实现的分布式锁。大体思想即为:每一个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个惟一的

瞬时有序节点。 判断是否获取锁的方式很简单,只须要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除便可。同时,其能够避免服务宕机导

致的锁没法释放,而产生的死锁问题。

来看下Zookeeper能不能解决前面提到的问题。

  • 锁没法释放?使用Zookeeper能够有效的解决锁没法释放的问题,由于在建立锁的时候,客户端会在ZK中建立一个临时节点,一旦客户端获取到锁以后忽然挂掉(

Session链接断开),那么这个临时节点就会自动删除掉。其余客户端就能够再次得到锁。

  • 非阻塞锁?使用Zookeeper能够实现阻塞的锁,客户端能够经过在ZK中建立顺序节点,而且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户

端能够检查本身建立的节点是否是当前全部节点中序号最小的,若是是,那么本身就获取到锁,即可以执行业务逻辑了。

  • 不可重入?使用Zookeeper也能够有效的解决不可重入的问题,客户端在建立节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的

时候和当前最小的节点中的数据比对一下就能够了。若是和本身的信息同样,那么本身直接获取到锁,若是不同就再建立一个临时的顺序节点,参与排队。

  • 单点问题?使用Zookeeper能够有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就能够对外提供服务。

能够直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。

Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并无缓存服务那么高。

由于每次在建立锁和释放锁的过程当中,都要动态建立、销毁瞬时节点来实现锁功能。ZK中建立和删除节点只能经过Leader服务器来执行,而后将数据同不到全部的Follower机器上。

**使用Zookeeper实现分布式锁的优势: **有效的解决单点问题,不可重入问题,非阻塞问题以及锁没法释放的问题。实现起来较为简单。

**使用Zookeeper实现分布式锁的缺点 : **性能上不如使用缓存实现分布式锁。 须要对ZK的原理有所了解。

三种方案的比较

**从理解的难易程度角度(从低到高): **数据库 > 缓存 > Zookeeper

从实现的复杂性角度(从低到高)**: **Zookeeper >= 缓存 > 数据库

**从性能角度(从高到低): **缓存 > Zookeeper >= 数据库

**从可靠性角度(从高到低): **Zookeeper > 缓存 > 数据库

相关文章
相关标签/搜索