**
应用场景
Q:在多线程并发的情况下如何保证一个代码块在同一时间只能有一个线程访问?
A:在这种情况下,我们都是加锁来处理的。
Q:那什么是锁呢?
A:我们一般所说的锁,就是指单进程多线程的锁机制。在单进程中,如果有多个线程并发访问某个某个全局资源,存在并发修改的问题。如果要避免这个问题,我们需要对资源进行同步,同步其实就是可以加一个锁来保证同一时刻只有一个线程能操作这个资源。
比如java中的synchronized关键字实现,使用可重入锁ReentrantLock,有的是基于volatile来实现。但追究其根本,是要所有的线程能够知道这个锁,以及这加锁和释放锁的信息,不然这个锁是没有意义的,并且这个锁是存在于内存中的。
Q:但这些加锁的方式适用于分布式环境,适用于集群么?
A:以集群为例,就是多个实例,就是多个进程,也就是java中多个JVM,我们知道多线程是可以共享父进程的资源,包括内存,也就可以知道锁,但是多个进程之间是无法共享资源的,甚至都不在一台机器上,这时我们就需要引入分布式锁,来让所有的进程都知道这个锁,来控制对全局资源的并发修改。
分布式锁的分类
在了解分布式锁,我们先了解一下多线程的一些常识
-
一些名词解释
互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量 已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
临界区:每个进程中访问临界资源的那段程序称为临界区,每次只允许一个进程进入临界区,进入后不允许其他进程进入。
自旋锁:与互斥量类似,它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。用在以下情况:锁持有的时间短,而且线程并不希望在重新调度上花太多的成本。
自旋锁与互斥锁的区别:线程在申请自旋锁的时候,线程不会被挂起,而是处于忙等的状态。
信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
公平锁:加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得 。
非公平锁:加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。
注意:非公平锁性能比公平锁高5~10倍,因为公平锁需要在多核的情况下维护一个队列。 -
加锁会出现的常见问题
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用, 它们都将无法推进下去。解决方案:一定要能及时释放锁,设置超时时间。
活锁:指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。解决方案:调整重试机制,引入一些随机性,例如如果检测到冲突,那么就暂停随机的一定时间进行重试。这会大大减少碰撞的可能性。
饥饿锁:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。使用公平锁,避免使用同步块。 -
分布式锁的要求
(1) 获取锁和释放锁的性能要好
(2) 判断是否获得锁必须是原子性的,否则可能导致多个请求都获取到锁,解锁同样,也要保证原子性,不能解到别的请求的锁
(3) 涉及到锁不能有单点问题,如果有单点问题,那么都获取不到锁,导致业务异常。
(4)可重入一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁;
(5).阻塞锁和非阻塞锁,阻塞锁即没有获取到锁,则继续等待获取锁;非阻塞锁即没有获取到锁后,不继续等待,直接返回锁失败。
-
几种实现方式
(1)数据库做分布式锁
使用数据库的锁表或者采用乐观锁增加版本号事项
优点:简单,方便
缺点:没有过期时间,增加了数据库的负担
(2)缓存做分布式锁, Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系,基于setnx、expire两个命令来实现
优点:性能高,实现简单
缺点:锁的超时机制不稳定,处理时间超过有效期则会使锁失去作用,并且无法有序获取锁。需要使用轮询的机制去获取锁,对性能消耗大。(下面介绍的redis分布式锁有对这些问题进行了一些处理)
(3)zookeeper做分布式锁。zookeeper是一个为分布式应用提供一致性服务的软件,它内部是一个分层的文件系统目录 树结构,规定统一个目录下只能有一个唯一文件名。当创建一个临时顺序节点时,可以注册一个该节点的监视器,当节点状态发生改变时,watch被触发时,ZooKeeper将会向客户端发送且仅发送一条通知,因为watch只能被触发一次。
优点:可靠性高,不依赖于超时时间与轮询机制获取锁,
缺点:性能比不上缓存,需要频繁创建删除阶段。
redis实现分布式锁
加锁流程:
实现代码:
@Component
public class RedisReentrantLock {
@Autowired
private RedisTemplate redisTemplate;
private final ConcurrentMap<Thread, LockData> threadData = new ConcurrentHashMap<>();
//重试时间
private static final int retryAwait=300;
//默认锁过期时间
private static final int lockTimeout=5000;
//轮询获取锁过期时间
private static final int awaitTimeout = 20000;
//更新锁有效时间的间隔
private static final int guardTime = 2000;
private static final String LOCK_ ="LOCK_";
//当前锁的唯一标识
private String lockId;
//加锁时使用lua脚本
private final String luaScriptOn = ""
+ "\nlocal r = tonumber(redis.call('SETNX', KEYS[1],ARGV[1]));"
+ "\nredis.call('PEXPIRE',KEYS[1],ARGV[2]);"
+ "\nreturn r";
//解锁时使用的lua脚本
private final String luaScriptOff = ""
+"\nlocal v = redis.call('GET', KEYS[1]);"
+"\nlocal r= 0;"
+"\nif v == ARGV[1] then"
+"\nr =redis.call('DEL',KEYS[1]);"
+"\nend"
+"\nreturn r";
//申请延迟锁有效时间的lua脚本
private final String getLuaScriptDelay=""+
"\nif redis.call('get', KEYS[1]) == ARGV[1] " +
"\nthen return redis.call('expire',KEYS[1],ARGV[2]) " +
"\nelse return 0 end";
/**
* 记录当前线程重入次数与是否获取锁
*/
private static class LockData {
//当前线程
final Thread owningThread;
//获取锁对应的值
final String lockVal;
//重入次数
final AtomicInteger lockCount = new AtomicInteger(1);
private LockData(Thread owningThread, String lockVal) {
this.owningThread = owningThread;
this.lockVal = lockVal;
}
}
/**
* 加锁
* @param lockKey 加锁唯一标识入参
* @param timeout 分布式锁有效时间,出入零或负数按默认值
* @param unit 时间单位
* @return 是否加锁成功
* @throws InterruptedException
*/
public boolean tryLock (String lockKey, long timeout, TimeUnit unit) throws InterruptedException{
if (StringUtils.isEmpty(lockKey)){
return false;
}
//获取当前线程
Thread thread = Thread.currentThread();
//判断当前线程是否获取锁,以及重入计数
LockData lockData = threadData.get(thread);
if (!Objects.equals(null,lockData)){
lockData.lockCount.incrementAndGet();
return true;
}
//获取锁开始时间
final long startTime = System.currentTimeMillis();
//加锁有效时间
final long millisToLock = (Objects.equals(null, unit)||timeout <lockTimeout ) ? lockTimeout : unit.toMillis(timeout);
//redis分布式锁的value值,解锁时用于校验是否为当前用户的锁
String lockValue = lockKey+ UUID.randomUUID();
//轮询去获取锁,未获取锁则阻塞一段时间继续去获取,直到获取锁超时返回失败
for (;;){
//判断是否存在锁,存在则返回false,不存则新增key值并返回true
if(createLock(lockKey,lockValue,millisToLock,startTime) == 1){
LockData data = new LockData(thread,lockValue);
threadData.put(thread,data);
this.lockId = lockKey;
return true;
}
if (System.currentTimeMillis() - startTime - retryAwait>awaitTimeout){
return false;
}
//让线程阻塞一断时间
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(retryAwait));
}
}
/**
* 创建加锁命令
* @param lockKey 加锁唯一标识入参
* @param lockValue 锁对应的值
* @param millisToLock 锁的过期时间
* @return 是否加锁成功 1 为成功
*/
public Integer createLock(String lockKey ,String lockValue, Long millisToLock ,Long startTime){
//执行解锁的lua脚本,根据返回值判断是否成功
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScriptOn, Long.class);
ArrayList<String> keys = new ArrayList<>(1);
keys.add(LOCK_+lockKey);
String values [] = new String[2];
values[0] = lockValue;
values[1] = String.valueOf(millisToLock);
Object execute = redisTemplate.execute(redisScript, keys, values);
if (Objects.equals(null,execute)){
return 0;
}
//创建守护进程
Thread guard = new Thread(() -> {
System.out.println("进入守护线程");
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(millisToLock - 1000));
for (;;){
//阻塞一断时间
DefaultRedisScript<Long> redisScriptDelay = new DefaultRedisScript<>(getLuaScriptDelay, Long.class);
ArrayList<String> dKeys = new ArrayList<>(1);
dKeys.add(LOCK_ + lockKey);
String dValues[] = new String[2];
dValues[0] = lockValue;
dValues[1] = String.valueOf(millisToLock);
Object res = redisTemplate.execute(redisScriptDelay, keys, values);
System.out.println("执行守护线程lua脚本");
if (Long.parseLong(String.valueOf(res)) == 0) {
System.out.println("守护线程结束");
return;
}
if(System.currentTimeMillis() - startTime >= awaitTimeout){
System.out.println("持有锁超时,结束守护线程");
return;
}
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(guardTime));
}
});
guard.setDaemon(true);
guard.start();
return Integer.parseInt(String.valueOf(execute));
}
/**
* 解锁,使用lua命令,保证解锁过程的原子性,防止解到别人的锁
*/
public void unLock(){
//获取当前线程,获取其持有锁的信息
Thread thread = Thread.currentThread();
LockData lockData = threadData.get(thread);
try {
if (Objects.equals(null,lockData)){
throw new IllegalMonitorStateException("当前用户未持有此锁:"+lockId);
}
//减少重入次数
int i = lockData.lockCount.decrementAndGet();
if (i > 0){
return;
}
if (i < 0){
throw new IllegalMonitorStateException("当前用户重入此锁有误:"+lockId);
}
//执行解锁的lua脚本,根据返回值判断是否成功
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScriptOff, Long.class);
ArrayList<String> keys = new ArrayList<>(1);
keys.add(LOCK_+this.lockId);
String values [] = new String[1];
values[0] = lockData.lockVal;
Object result = this.redisTemplate.execute(redisScript,keys, values);
int res = Integer.parseInt(String.valueOf(result));
if (Objects.equals(null,result)|| res == 0){
throw new IllegalMonitorStateException("当前用户删除此锁有误:"+thread.getName());
}
} finally {
threadData.remove(thread);
}
}
}
代码已上传到github,此为示例代码,还有待优化
源代码地址
**