一、为什么要有锁的概念
1.假如现在我们有这么一个场景:
用户在淘宝app上购买商品,用户提交订单的时候提交了,多点击了几次。
不管用户点击几次,只要用户一直停留在一个页面,那么就必须生成一个订单。
1.1 如果我们的服务是单体服务的话
比如现在我们的并发量,单体服务不够我们来支撑了,那么我们得升级到微服务,这个时候我们的大致流程图就变成这样了。
1.2 微服务的话
二、分布式锁的实现方式
1.Redis 实现分布式锁
1.2 SETNX命令
使用Redis完成分布式锁之前,首先我们的Redis必须有 互斥 的能力,我们可以使用 SETNX 命令,这个命令表示 SET IF NOT EXISTS,
如果key不存在,我们就设置他的值。如果存在我们什么也不做。
我们使用Redis客户端来演示一下:
- 客户端1加锁:
加锁成功返回的是1
2.客户端2加锁:
返回的是0,代表我们加锁失败了
此时,加锁成功的客户端,就可以去操作我们的数据库了,可以提交订单添加数据了。操作完成后。我们执行Redis的 Del命令删除掉这个Key,也就是我们释放锁资源。
3.释放锁资源
其中这样会导致一些问题。不知道大家想到了没有
假如说我们的客户端1拿到锁资源后,如果发生了下面的场景,就会导致这个锁资源一直被它占用着
- 程序处理业务逻辑异常,没及时释放锁资源
- 进程挂了,没机会事放锁
这就导致 死锁 了
其实解决这个问题很简单,我们看下Redis里面的这个命令。
1.3 SETEX命令
在Redis种实现时,就是我们添加这个KEY的时候 我们给他加一个过期时间。
假设我们这里操作共享资源的时间不超过10s,那我们加锁时这么添加
这样一来,无论客户端是否异常,这个锁都可以在10s后被【自动释放】,其他客户端就可以拿到锁资源。
但是现在还是有问题:
现在的操作,加锁、设置过期时2条命令,有没有可能只执行了第一条命令,第二条却 来不及 执行呢?例如
- SETNX 执行成功,执行EXPIRE时由于忘了问题,执行失败
- SETNX 执行成功,Redis异常宕机,EXPIRE没有计汇执行
- SETNX 执行成功,客户端异常崩溃,EXPIRE也没有机会执行
总之,这两条命令不能保证时原子性操作(一起成功),就有潜在的风险导致过期时间设置失败,依旧发生 死锁 的问题
在Redis 2.6.12之后,Redis扩展了SET命令的参数,我们使用这个命令就可以了。
SET lock 1 EX 10 NX
但是这样的话,其实我们这还要一个问题。比如
我A加了锁,我在执行我的逻辑
然后B线程来了,给我把我的锁给我干掉了 这样不就乱套了么
因为我们上面写 删除锁的时候,我们并没有去判断这个锁,是不是我们线程A的锁,所以就会发生释放别人锁的风险,这样的解决流程,非常不 严谨,那么如何解决这个问题呢?
1.4 释放了别人的锁怎么办?
其实我们可以这样写,比如我们添加锁的时候设置
set 锁资源名称 当前线程的唯一标识 EX 过期时间 NX
SET lock $uuid EX 20 NX
然后我们释放锁资源的时候,判断当前key的value是不是我们当前线程所持有的,如果是就进行删除,那我们的命令是不是可以这么写
//如果get到的lock的值 == 我们的线程持有的值
if redis.get("lock") == $uuid:
//那么我们就进行删除操作
redis.del("lock")
这里释放锁使用的是 GET+DEL 两条命令,这时,又会遇到我们前面讲的原子性,那么怎么解决呢?
1.5 使用Lua脚本来保证原子性和解锁?
我们可以使用Lua脚结合get和del一起保证原子性
//安全释放锁的Lua脚本如下:
if redis.call("GET",KEYS[1]) == ARGV[1]
then
return redis.call("DEL",KEYS[1])
else
return 0
end
1.6 使用Jedis 实现分布式锁
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;
import java.util.Arrays;
@Component
public class RedisLock {
@Autowired
private JedisPool jedisPool;
private final static String RELEASE_LOCK_LUA =
"if redis.call('get',KEYS[1])==ARGV[1] then\n" +
" return redis.call('del', KEYS[1])\n" +
" else return 0 end";
/**
* 加锁代码
* @param key key值
* @param timeout 过期时间
* @param value value值
* @return true成功 false失败
*/
public boolean lock(String key, Long timeout, String value) {
Jedis jedis = null;
try {
SetParams params = new SetParams();
params.px(timeout);
params.nx();
if ("OK".equals(jedis.set(key, value, params)) {
return true;
}else{
return false;
}
} catch (Exception e) {
throw new RuntimeException("分布式锁尝试加锁失败!");
} finally {
jedis.close();
}
}
/**
* 释放所资源
* @param key
* @param value
*/
public void unlock(String key, String value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
Long result = (Long) jedis.eval(RELEASE_LOCK_LUA,
Arrays.asList(key),
Arrays.asList(value));
if (result.longValue() != 0L) {
System.out.println("Redis上的锁已释放!");
} else {
System.out.println("Redis上的锁释放失败!");
}
} catch (Exception e) {
throw new RuntimeException("释放锁失败!", e);
} finally {
if (jedis != null) jedis.close();
System.out.println("本地锁所有权已释放!");
}
}
}
1.7 使用Redission实现分布式锁
redission中两个加锁的方法
/**
* 该方法尝试在指定的等待时间内获取锁,并且设置锁的租期
* <p>
* 如果锁当前不可用,方法会等待一段时间(waitTime)直到获取到锁,或者在等待期间线程被中断抛出 InterruptedException
* <p>
* 如果在等待期间获取到锁,则返回 true
* <p>
* 如果在等待期间未能获取到锁(超过等待时间),则返回 false。
* <p>
* 获取到锁后,锁会自动在指定的租期时间(leaseTime)后释放。
*
* @param waitTime 参数指定了尝试获取锁的最大等待时间
* @param leaseTime 参数指定了锁的租期时间
* @param unit 参数指定了 waitTime 和 leaseTime 的时间单位
* @return
* @throws InterruptedException
*/
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
/**
* 该方法用于获取锁,并指定锁的租期时间
* <p>
* 调用该方法后,如果锁当前不可用,则当前线程将被阻塞,直到成功获取到锁。
* <p>
* 当调用 lock 方法时,如果锁当前不可用,则当前线程将一直等待,直到获取到锁。
* <p>
* 获取到锁后,锁将在指定的租期时间(leaseTime)后自动释放。
* <p>
* 如果将 leaseTime 参数设置为 -1,表示持有锁直到显式调用 unlock 方法来释放锁。
* <p>
* 使用 lock 方法可以确保当前线程获取到锁后,其他线程在此期间将被阻塞。该方法适用于需要确保获取到锁才能继续执行的场景,以及需要指定锁的租期时间的场景
*
* @param leaseTime 参数指定了锁的租期时间
* @param unit 参数指定了 leaseTime 的时间单位。
*/
void lock(long leaseTime, TimeUnit unit);
释放锁的方法都是
/**
* 该方法用于释放锁
* <p>
* 调用 unlock 方法将释放之前获取的锁,使其变为可用状态。
* <p>
* 只有持有锁的线程才能成功调用 unlock 方法释放锁。
* <p>
* unlock 方法没有返回值,它只是用于显式释放锁。
* <p>
* unlock 方法在 Redission 中用于释放之前获取的锁资源,以便其他线程能够获取到该锁并执行相应的任务。
*/
void unlock();
1.8 Redis集群模式
Redis集群模式(主从复制的架构下) 也可以使用我们上面的这种方式来用