详解锁,分布式锁的几种实现方式

1.什么是锁?

       多线程的锁、数据库的锁、分布式的锁,三种锁机制。

      在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量。

       而同步的本质是通过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。这个标记可以理解为锁。

       除了利用内存数据做锁其实任何互斥的都能做锁(只考虑互斥情况),如流水表中流水号与时间结合做幂等校验可以看作是一个不会释放的锁,或者使用某个文件是否存在作为锁等。只需要满足在对标记进行修改能保证原子性和内存可见性即可。

        总结:一是在多线程中,维护线程的安全和维护线程的可见性;二是在在数据库中,保存数据的一致性。

2.什么是分布式锁?

        当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。

3.多线程锁

在单机环境中,为了保持在使用多线程的时候,确保线程的安全性,我们一般会使用Java API 中java.util.concurrent.* 包下面的API进行代码的编写。

       如果在不同的场景使用不同的API呢?对于Java来说,是面向对象的,无非就是在class(类),变量,method(方法)三个地方(一是类上(class);二是方法上;三是全局变量。)

       对于Class来说,最著名的是Spring依赖注入Bean实例,使用的是单例设计模式,确保了在多线程创建class的时候,线程是安全的。      

 

      而对于method来说,有两个关键字,一是synchronized,另一个是lock。对于synchronized来说,一般是加在函数名称之前加上synchronized,或者在方法体上加上。

synchronized(Object o){        //do something        }

        而对于lock来说,lock是重入锁,只能在方法体内部加上,格式如下:       

 Lock reentrantLock = new ReentrantLock();        reentrantLock.lock();        //处理事务        reentrantLock.unlock();

        其实对于上面的三条语句(代码)来说,并不是最好的。我们还可以设置锁(是否公平,锁的时间)相关属性,同时把reentrantLock.unlock();在finally里面,也就是说,在捕获异常的时候,不管执行如何,最终都会释放这个锁的,代码如下:       

 Lock reentrantLock = new ReentrantLock();        try {        //设置时间        reentrantLock.tryLock(2, TimeUnit.SECONDS);        //do something      
        }catch (Exception e){        e.printStackTrace();        }finally {        reentrantLock.unlock();        }

补充知识点:

什么是重入锁?

重入锁

(1)重进入:

1.定义:重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞。关联一个线程持有者+计数器,重入意味着锁操作的颗粒度为“线程”。

2.需要解决两个问题:

线程再次获取锁:锁需要识别获取锁的现场是否为当前占据锁的线程,如果是,则再次成功获取;

锁的最终释放:线程重复n次获取锁,随后在第n次释放该锁后,其他线程能够获取该锁。要求对锁对于获取进行次数的自增,计数器对当前锁被重复获取的次数进行统计,当锁被释放的时候,计数器自减,当计数器值为0时,表示锁成功释放。

3.重入锁实现重入性:每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁

       最后对于全局变量来说,需要使用关键字volatile。相信看过spring源码的都知道,缓存。不知道有没有发现,使用了volatile关键字进行修饰。

       总结:

      主要是synchronized和lock的区别,两个都可以用在方法体上。两个都是重入锁,synchronized可重入性,指的是同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入。而lock最大的区别是:可中断响应、锁申请等待限时、公平锁等功能。另外可以结合Condition来使用。也就是说lock功能更强大。从Jdk 1.5 开始之后,由于对synchronized进行了优化,synchronized和lock的效率是差不多的。

      注意:上面的锁,在单机服务器是很好使用的,假如获取订单号(唯一),是可以确保的。但是如果把该项目部署到多台服务器上去,该锁只能在该服务器上锁住,也就是对于分布式是没有的,获取订单号是确保不了唯一的。

4.分布式锁

在单机环境中,Java中其实提供了很多并发处理相关的API,但是这些API在分布式场景中就无能为力了。也就是说单纯的Java Api并不能提供分布式锁的能力。所以针对分布式锁的实现目前有多种方案,一是基于缓存,而缓存我们以Redis为例;第二个是基于ZK。相对于数据库来说,上面两个效率高,速度快一点。

        1.基于Redis分布式锁

        网上大部分资料,一般都是以setnx() 和 expire() 两个命令为基础的,进行分布式锁控制。其实这个方式是存在问题的,因为这两个命令不是原子操作,可能存在的情况如下:

       setnx()命令设置key和value成功,但是执行到expire()命令的时候,发生了服务器进程之间突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire() 得不到执行,进而造成死锁。

        正确的打开方式:

        第一种命令的方式:

        需要在Redis 2.6.12 版本之后,还是利用set()命令,该命令有设置时间的参数,也就是说该命令直接结合了expire()命令。

set aaron:test:lock true ex 10 nx

        参数解析:

EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。

PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。 

NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。 

XX :只在键已经存在时,才对键进行设置操作。

 

        释放锁很简单,直接删除 key: 

del aaron:test:lock

        第二种Java代码方式:

        获取锁:

        调用:

Long time = System.currentTimeMillis() + RedisConstant.LOCK_TIME_OUT_TEN;
if (!redisLockUtil.tryLock(key, time)) {
throw new Exception(...);
}

        具体工具类:

public boolean tryLock(String key, long value) {
   while (value > System.currentTimeMillis()) {       // 第一步:如果设置成功,代表加锁成功       if (stringRedisTemplate.opsForValue().setIfAbsent(key, String.valueOf(value))) {           return true;       }
       // 第二步:获取上一个线程A的锁的值   BC的值为value       String currentValue = stringRedisTemplate.opsForValue().get(key);       // 第三步:如果锁过期       if (!StringUtils.isEmpty(currentValue) && Long.valueOf(currentValue) < System.currentTimeMillis()) {
           // 第四步:获取上一个锁的值并设置新的值, BC有先后关系。           String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, String.valueOf(value));           // 第五步:判断           if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {               return true;           }       }
       try {           Thread.sleep(300);       } catch (InterruptedException e) {           e.printStackTrace();       }   }   return false;}

    释放锁:

public Boolean unlock(String key, long value) {   // 执行lua脚本,确保原子性   String script = "if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del',KEYS[1]) return true else return false end";   DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(script, Boolean.class);   return stringRedisTemplate.execute(redisScript, Collections.singletonList(key), String.valueOf(value));}

        总结:

       1.超时问题。Redis的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候第一个线程持有的锁过期了,临界区的逻辑还没有执行完,这个时候第二个线程就提前重新持有了这把锁,导致临界区代码不能得到严格的串行执行。为了避免这个问题,Redis 分布式锁不要用于较长时间的任务。如果真的偶尔出现了,数据出现的小波错乱可能需要人工介入解决。

       我的建议最好是测试一下该需要执行的程序最大运行时间,然后把这个运行时间设置到超时时间里面去。

        2.可重入性

上面的Java代码是不支持可重入性的,Redis 分布式锁如果要支持可重入,需要对客户端的 set 方法进行包装,使用线程的 Threadlocal 变量存储当前持有锁的计数。不过这个实现起来有点麻烦,不推荐使用。

640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1

        2.基于ZK分布式锁

       基于zookeeper临时有序节点可以实现的分布式锁,大致思想即为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

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

锁无法释放?

      使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。

非阻塞锁?

    使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。

不可重入?

    使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。

单点问题?

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

具体实现,参看之前zk相关的文章:

zookeeper知识点讲解(三)--API使用、分布式锁和简单的服务注册与发现;( https://mp.weixin.qq.com/s/W1wM5UNUFRAJZA02RLvIvw )。

参考资料:

  1. MySQL中的连接、实例、会话、数据库、线程之间的关系,雅思敏.

  2. 2.Redis 命令参考(http://doc.redisfans.com/index.html).

  3. 3.zookeeper知识点讲解(三)--API使用、分布式锁和简单的服务注册与发现.

 

转载于:https://my.oschina.net/u/2380961/blog/3025382

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值