1.什么是锁?html
多线程的锁、数据库的锁、分布式的锁,三种锁机制。java
在单进程的系统中,当存在多个线程能够同时改变某个变量(可变共享变量)时,就须要对变量或代码块作同步,使其在修改这种变量时可以线性执行消除并发修改变量。redis
而同步的本质是经过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么须要在某个地方作个标记,这个标记必须每一个线程都能看到,当标记不存在时能够设置该标记,其他后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。这个标记能够理解为锁。spring
除了利用内存数据作锁其实任何互斥的都能作锁(只考虑互斥状况),如流水表中流水号与时间结合作幂等校验能够看做是一个不会释放的锁,或者使用某个文件是否存在做为锁等。只须要知足在对标记进行修改能保证原子性和内存可见性便可。数据库
总结:一是在多线程中,维护线程的安全和维护线程的可见性;二是在在数据库中,保存数据的一致性。设计模式
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的值为valueString 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 变量存储当前持有锁的计数。不过这个实现起来有点麻烦,不推荐使用。
2.基于ZK分布式锁
基于zookeeper临时有序节点能够实现的分布式锁,大体思想即为:每一个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个惟一的瞬时有序节点。 判断是否获取锁的方式很简单,只须要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除便可。同时,其能够避免服务宕机致使的锁没法释放,而产生的死锁问题。
看下Zookeeper能不能解决前面提到的问题:
锁没法释放?
使用Zookeeper能够有效的解决锁没法释放的问题,由于在建立锁的时候,客户端会在ZK中建立一个临时节点,一旦客户端获取到锁以后忽然挂掉(Session链接断开),那么这个临时节点就会自动删除掉。其余客户端就能够再次得到锁。
非阻塞锁?
使用Zookeeper能够实现阻塞的锁,客户端能够经过在ZK中建立顺序节点,而且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端能够检查本身建立的节点是否是当前全部节点中序号最小的,若是是,那么本身就获取到锁,即可以执行业务逻辑了。
不可重入?
使用Zookeeper也能够有效的解决不可重入的问题,客户端在建立节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就能够了。若是和本身的信息同样,那么本身直接获取到锁,若是不同就再建立一个临时的顺序节点,参与排队。
单点问题?
使用Zookeeper能够有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就能够对外提供服务。
具体实现,参看以前zk相关的文章:
zookeeper知识点讲解(三)--API使用、分布式锁和简单的服务注册与发现;( https://mp.weixin.qq.com/s/W1wM5UNUFRAJZA02RLvIvw )。
参考资料:
MySQL中的链接、实例、会话、数据库、线程之间的关系,雅思敏.
2.Redis 命令参考(http://doc.redisfans.com/index.html).
3.zookeeper知识点讲解(三)--API使用、分布式锁和简单的服务注册与发现.