锁和分布式锁

22 篇文章 0 订阅
9 篇文章 0 订阅

 

 

目录

一、线程安全

二、JVM锁

三、分布式锁

3.1 分布式锁的概念

3.2 分布式锁的条件

3.3 分布式锁的实现方式

四、Redisson实现分布式锁原理

4.1 基于redis的分布式锁

4.1.1 加锁操作

4.1.2 解锁操作

4.1.3. 死锁

4.1.4 Redis操作的原子性

4.1.5 Redis 分布式锁演变过程

4.2 Redisson原理图

4.2.1、加锁入口

4.2.2、异步加锁方法

4.2.3、加锁lua脚本

4.2.4、存入的数据结构

4.2.5、解锁操作


一、线程安全

如果多个线程同时对共享变量进行读改写的操作,那么共享变量会被多个线程同时进行操作,这样的读改写操作就不是原子的,操作完成之后共享变量的值会和期望的不一致,就会发生线程安全问题。

举一个最简单的例子:经典的读写改操作 i++

public class WrongResult {

   volatile static int i;

   public static void main(String[] args) throws InterruptedException {

       Runnable r = new Runnable() {

           @Override

           public void run() {

               for (int j = 0; j < 10000; j++) {

                   i++;

               }

           }

       };

       Thread thread1 = new Thread(r);

       thread1.start();

       Thread thread2 = new Thread(r);

       thread2.start();

       thread1.join();

       thread2.join();

       System.out.println(i);

    }

}

在这段代码中,我们首先定义了一个int类型的静态变量 i ,然后启动两个线程,分别对i进行了10000次的 i ++ 操作,理论上得到的结果应该是20000,单实际上的结果可能是12996,远远小于预期值,且每次运行之后的结果都不一样,这是为什么呢?

是因为在多线程下,CPU的调度是以时间片为单位分配的,每个线程都可以得到一定量的时间片,如果线程拥有的时间片耗尽,它将暂停执行并让出CPU给其他的线程,这样就可能造成线程安全问题。比如这个 i++的操作,它不是一个原子操作,它的执行步骤分为三步:第一步读取,第二步增加,第三步保存。每个线程都有自己的工作缓存,如图所示,在线程1执行 i + 1 的操作后,线程1被切走了,没有把结果保存到主内存,虽然两个线程最终都完成了 +1 的操作,最终结果保存了 i = 2 ,并不是预期的 i = 3 ,导致了结果错误,这也是最典型的线程安全问题。  

二、JVM锁

多线程环境中,经常遇到多个线程访问同一个共享资源,这时候作为开发者必须考虑如何维护数据的一致性,这个时候就需要某种机制来保证只有满足某个条件(获取锁成功)的线程才能访问资源,而不满足条件(获取锁失败)的线程只能等待,在下一轮竞争中来获取锁,从而访问资源。

根据分类标准,我们通常把锁分为以下 7大类别,分别是:

  • 偏向锁/轻量级锁/重量级锁
  • 可重入锁/非可重入锁
  • 共享锁/独占锁
  • 公平锁/非公平锁
  • 悲观锁/乐观锁
  • 自旋锁/非自旋锁
  • 可中断锁/不可中断锁

JVM提供的锁,主要有两种synchronized和lock,相信大家都不陌生了。下面介绍一下两个锁的异同点:

1、Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

2、synchronized除了在流程走完释放锁,还在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock 时需要在finally块中释放锁;

3、Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。

4、通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

5、Lock可以提高多个线程进行读操作的效率。ReadWriteLock可是实现并发读。

6、ReentrantLock和synchronized都是可重入锁。

JVM锁解决了在JVM内部的同一进程的线程互斥,但是当我们将应用程序部署成分布式架构,synchronized和lock就不能保证分布式架构和多JVM进程下的应用程序的互斥性了。

三、分布式锁

3.1 分布式锁的概念

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

3.2 分布式锁的条件

a. 在分布式系统环境下,一个方法在同一时间只能被一个机器的线程执行

b. 高可用的获取锁和释放锁

c. 高性能的获取锁和释放锁

d. 具备可重入的特性

e. 具备锁失效机制,防止死锁

f. 具备非阻塞锁特性,即没有获取到锁直接返回获取锁失败

3.3 分布式锁的实现方式

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

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

c. 基于Zookeeper实现分布式锁;

 

四、Redisson实现分布式锁原理

4.1 基于redis的分布式锁

4.1.1 加锁操作

加锁操作,其实是运用了setnx思想。SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

只在键 key 不存在的情况下, 将键 key 的值设置为 value 。若键 key 已经存在, 则 SETNX 命令不做任何动作。

setnx key value

该命令在设置成功时返回1,设置失败时返回0。

4.1.2 解锁操作

当线程执行完任务后,就需要释放锁,从而让其他线程可以获取到锁,最简单的释放锁的操作就是运用了我们的del操作。

del key

完成锁的释放之后,下一个线程就可以进行获取到锁了。

4.1.3. 死锁

如果一个线程获取到锁后,在执行过程中意外中断了,会导致我们的锁没有释放,其他线程获取不到锁,从而造成死锁。避免死锁的一种方式就是设置一个超时时间。

expire key 30

setnx指定本身是不支持传入超时时间的,如果我们setnx 和 expire 两个命令来实现加锁并设置超时时间,而这两个操作是非原子的,在极端情况下也会造成死锁的情况,set指定增加可选参数来替代setnx。

set(key,value,30,NX)

4.1.4 Redis操作的原子性

说到这里大家可能会有一个疑问,如果我并发进行setnx会不会有问题?为什么setnx可以保证原子性?

答案是:redis是单线程的,对于redis来说,执行get、set等API,都是一个一个的任务,这些任务都会由Redis的线程去负责执行,任务要么成功,要么失败,这就是Redis的命令是原子性的原因。

4.1.5 Redis 分布式锁演变过程

由于演变过程的内容较多,不在此wiki中进行录入,请大家移步以下链接:

https://www.cnblogs.com/lovellll/p/10245966.html

4.2 Redisson原理图

4.2.1、加锁入口

/**

 * leaseTime:租赁时间,当前锁最久可使用时间,默认30S,可通过看门狗进行自动续期

 * unit:时间单位

 * interruptibly:是否可被中断,默认不可中断

 */

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {

    // 当前线程Id,用于组成锁的唯一键

    long threadId = Thread.currentThread().getId();

    /*

     * 加锁核心逻辑.

     * 当其他线程使用同一加锁关键字加锁时,返回的ttl为关键字锁的剩余租赁时间

     */

    Long ttl = tryAcquire(leaseTime, unit, threadId);

    // ttl为null时,证明当前线程加锁成功

    if (ttl == null) {

        return;

    }

    /*

     * 当锁被占用的情况下,线程通过Redis订阅解锁事件,及时感知解锁操作,并再次竞争锁.

     * 同时通过分布式信号量,完成竞争限流

     */

    RFuture<RedissonLockEntry> future = subscribe(threadId);

    if (interruptibly) {

        commandExecutor.syncSubscriptionInterrupted(future);

    } else {

        commandExecutor.syncSubscription(future);

    }

    try {

        // 循环竞争锁

        while (true) {

            ttl = tryAcquire(leaseTime, unit, threadId);

            // 锁获取成功

            if (ttl == null) {
               break;
            }

            // 等待解锁消息发布
            if (ttl >= 0) {
                try {
                    future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    if (interruptibly) {
                        throw e;
                    }
                    future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                }
            } else {
                if (interruptibly) {
                    future.getNow().getLatch().acquire();
                } else {
                    future.getNow().getLatch().acquireUninterruptibly();
                }

            }

        }

    } finally {

        // 获取成功或失败后,取消订阅
        unsubscribe(future, threadId);
    }

}/**

 * leaseTime:租赁时间,当前锁最久可使用时间,默认30S,可通过看门狗进行自动续期

 * unit:时间单位

 * interruptibly:是否可被中断,默认不可中断

 */

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {

    // 当前线程Id,用于组成锁的唯一键

    long threadId = Thread.currentThread().getId();

    /*

     * 加锁核心逻辑.

     * 当其他线程使用同一加锁关键字加锁时,返回的ttl为关键字锁的剩余租赁时间

     */

    Long ttl = tryAcquire(leaseTime, unit, threadId);

    // ttl为null时,证明当前线程加锁成功

    if (ttl == null) {

        return;

    }

    /*

     * 当锁被占用的情况下,线程通过Redis订阅解锁事件,及时感知解锁操作,并再次竞争锁.

     * 同时通过分布式信号量,完成竞争限流

     */

    RFuture<RedissonLockEntry> future = subscribe(threadId);

    if (interruptibly) {

        commandExecutor.syncSubscriptionInterrupted(future);

    } else {

        commandExecutor.syncSubscription(future);

    }

    try {
        // 循环竞争锁
        while (true) {

            ttl = tryAcquire(leaseTime, unit, threadId);

            // 锁获取成功

            if (ttl == null) {

                break;

            }

            // 等待解锁消息发布

            if (ttl >= 0) {

                try {

                    future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);

                } catch (InterruptedException e) {

                    if (interruptibly) {

                        throw e;
                    }

                    future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);

                }

            } else {
                if (interruptibly) {

                    future.getNow().getLatch().acquire();

                } else {

                    future.getNow().getLatch().acquireUninterruptibly();

                }

            }

        }

    } finally {

        // 获取成功或失败后,取消订阅
        unsubscribe(future, threadId);
    }

}

4.2.2、异步加锁方法

/**

 * 异步的加锁方法,由tryAcquire调用,实现加锁的核心逻辑

 */

private RFuture<Boolean> tryAcquireOnceAsync(long leaseTime, TimeUnit unit, long threadId) {

    /*
     * leaseTime=-1为内置默认的锁租赁时间标志,默认为看门狗的超时时间30S
     */

    if (leaseTime != -1) {

        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);

    }
    // 异步执行Lua脚本

    RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);

    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {

        // 针对异常进行处理

        if (e != null) {

            return;

        }
        // 加锁成功,开启看门狗,进行锁续期操作

        if (ttlRemaining) {

            scheduleExpirationRenewal(threadId);

        }

    });

    return ttlRemainingFuture;

}

4.2.3、加锁lua脚本

/**

 * 定义异步类,执行加锁Lua脚本,保证执行的原子性

 */

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {

    internalLockLeaseTime = unit.toMillis(leaseTime);

    /*

     * Lua脚本解释:

     * 如果当前锁关键字不存在,新增Redisson锁,并设置过期时间,返回null,结束;

     * 如果Redisson锁存在,且加锁线程为当前线程,Redisson锁重入次数加1,重置过期时间,返回null,结束;

     * 否则,当前锁关键字存在,返回其剩余租赁时间

     */

    return evalWriteAsync(getName(), LongCodec.INSTANCE, command,

            "if (redis.call('exists', KEYS[1]) == 0) then " +

                    "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +

                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +

                    "return nil; " +

                    "end; " +

                    "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +

                    "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +

                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +

                    "return nil; " +

                    "end; " +

                    "return redis.call('pttl', KEYS[1]);",

            Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));

}

4.2.4、存入的数据结构

Hash:{"myLock":{"key":"761e9b81-5e36-494c-a94d-24ae725b3747:28","value":1}}

 

myLock:加锁的关键字,按照业务自定义

key:锁的唯一键,由Redisson连接池中连接Id及当前线程Id组合而成,使用:分割

value:当前锁重入的次数

4.2.5、解锁操作

/**

 * 通过线程Id进行解锁操作

 */

public RFuture<Void> unlockAsync(long threadId) {

    RPromise<Void> result = new RedissonPromise<Void>();

    // 解锁核心操作,异步执行Lua脚本

    RFuture<Boolean> future = unlockInnerAsync(threadId);

    future.onComplete((opStatus, e) -> {

        // 取消当前线程注册的看门狗

        cancelExpirationRenewal(threadId);

        // 异常处理

        if (e != null) {

            result.tryFailure(e);

            return;

        }

        // 当前线程未持有锁,进行解锁时的异常处理

        if (opStatus == null) {

            IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "

                    + id + " thread-id: " + threadId);

            result.tryFailure(cause);

            return;

        }

        // 解锁成功

        result.trySuccess(null);

    });
    return result;

}

/**

 * 定义异步类,执行解锁Lua脚本,保证执行的原子性

 */

protected RFuture<Boolean> unlockInnerAsync(long threadId) {

    /*

     * 如果锁关键字对应的Redisson锁下线程不为当前线程,返回null

     * 如果锁关键字对应的Redisson锁下线程为当前线程,重入次数减1:当重入次数大于0,重设锁失效时间,返回0;当重入次数小于等于0,删除Redisson锁,并发布解锁事件,返回1

     * 均不符合,返回null

     */

    return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,

            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +

                    "return nil;" +

                    "end; " +

                    "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +

                    "if (counter > 0) then " +

                    "redis.call('pexpire', KEYS[1], ARGV[2]); " +

                    "return 0; " +

                    "else " +

                    "redis.call('del', KEYS[1]); " +

                    "redis.call('publish', KEYS[2], ARGV[1]); " +

                    "return 1; " +

                    "end; " +

                    "return nil;",

            Arrays.asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));

}

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值