-
什么是分布式锁
为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁。
-
如何利用redis实现分布式锁
我们先看一段秒杀业务的逻辑代码:
int stock = Integer.parseInt(template.opsForValue().get("stock"));
if (stock > 0){
stock -= 1;
template.opsForValue().set("stock",stock+"");
System.out.println("秒杀成功!" + stock);
}else {
System.out.println("秒杀失败!");
}
简单分析一下这段代码的功能:
1.在redis中设置String类型的键值对,键为"stock",值为库存数量,均为字符串形式存储在redis中。
2.如果库存大于0,则秒杀成功,库存减一,否则秒杀失败。
再来分析一下这段代码中存在的问题:
多线程的情况下会出现线程安全问题,线程不同步导致超卖问题。
解决办法:给这段代码加锁,也就是加上synchronized关键字,同步代码块或者同步方法均可。
上面我们只考虑的是在单机模式下,下面我们来看一下分布式的情况下,假设有两台机器都部署了这段代码,架构图如下所所示:
我们可以看到,在分布式系统中,请求被转发到不同的服务器进行处理,虽然对于独立的server1和server2来说,加了同步处理之后都是线程安全的,但是对于整个系统来说并不是,也就是说进程server1和server2并不是同步的。
这时候就需要分布式锁来同步这两个线程了,我们看一下下面的代码,是对之前代码的补充:
String Lock_Key = "lock";
//1.所有线程进来先尝试获取锁,redis中的setnx命令
Boolean id = template.opsForValue().setIfAbsent(Lock_Key, clientId);
//未拿到锁则返回错误,加锁失败
if (!id)
return "error";
//2.成功拿到锁之后执行秒杀的业务逻辑
int stock = Integer.parseInt(template.opsForValue().get("stock"));
if (stock > 0){
stock -= 1;
template.opsForValue().set("stock",stock+"");
System.out.println("秒杀成功!" + stock);
}else {
System.out.println("秒杀失败!");
}
//3.释放锁
template.delete(Lock_Key);
简单分析一下上面这段代码:
1.在执行秒杀逻辑之前,利用redis的setnx命令向redis中获取锁(如果设置成功代表成功拿到了锁,否则失败返回失败信息)
2.成功获取锁之后执行秒杀逻辑
3.秒杀完成后释放锁
其实,就是增加了redis这么一个第三方变量来控制两台服务器的同步,redis其实具有一个信息传递的作用,相当于控制中心,控制两台服务器的同步工作。
存在的问题:
如果在释放锁之前出现了异常导致锁无法被正常释放,则会产生死锁问题,也就是key为"stock"的键一直存在,后面的所有线程都无法拿到锁。
可以进一步优化如下:
public String stock(){
String Lock_Key = "lock";
try {
//1.所有线程进来先尝试获取锁,redis中的setnx命令
Boolean id = template.opsForValue().setIfAbsent(Lock_Key, clientId);
template.expire(Lock_Key,30L, TimeUnit.SECONDS); //设置锁的过期时间
//未拿到锁则返回错误,加锁失败
if (!id)
return "error";
//2.成功拿到锁之后执行秒杀的业务逻辑
int stock = Integer.parseInt(template.opsForValue().get("stock"));
if (stock > 0){
stock -= 1;
template.opsForValue().set("stock",stock+"");
System.out.println("秒杀成功!" + stock);
}else {
System.out.println("秒杀失败!");
}
}finally {
//3.释放锁,无论秒杀是否成功都要释放锁
template.delete(Lock_Key);
}
return "end";
优化到这一步,这段秒杀业务可以说是比较完善了,但是在高并发环境下还会有很多的问题。
存在的问题:
在高并发环境下,会发生上一个线程把下一个线程刚设置的锁给删除的情况,这是因为我们之前为了能够保证释放锁,给锁设置了超时时间。
1.假设线程一执行到第三步释放锁之前,这时锁过期了(达到了过期时间)
2.这时候线程二进来了,执行完获取锁的操作
3.然后线程一此时还没有结束,它会继续执行,但是对于线程一而言它的锁已经删除了,但是线程一还会执行删除锁的步骤,而这时候线程二刚刚拿到锁,就被线程一给删除了。
4.最终造成的结果就是分布式锁永久失效。
解决办法:
在最后一步释放锁时添加一个判断条件,判断该锁是不是自己加的,如果是则删除,不是则不删除。
代码如下:
public String stock(){
String Lock_Key = "lock";
String clientId = UUID.randomUUID().toString();
try {
//1.所有线程进来先尝试获取锁,redis中的setnx命令
Boolean id = template.opsForValue().setIfAbsent(Lock_Key, clientId);
template.expire(Lock_Key,30L, TimeUnit.SECONDS);
//未拿到锁则返回错误,加锁失败
if (!id)
return "error";
//2.成功拿到锁之后执行秒杀的业务逻辑
int stock = Integer.parseInt(template.opsForValue().get("stock"));
if (stock > 0){
stock -= 1;
template.opsForValue().set("stock",stock+"");
System.out.println("秒杀成功!" + stock);
}else {
System.out.println("秒杀失败!");
}
}finally {
//3.释放锁,无论秒杀是否成功都要释放锁
if (template.opsForValue().get(Lock_Key).equals(clientId))
template.delete(Lock_Key);
}
return "end";
通过为每一个线程加锁时设置自己的UUID防止了锁永久失效的问题。
终极版(Redisson框架),在Redisson中已经为我们封装好了这样一套逻辑来解决锁永久失效的问题,Redisson实现分布式锁的原理如图所示:
从上图我们可以看到,Redisson的解决方案是定期检测当前线程时候持有锁,如果持有则会延长锁的过期时间,而其他线程则会一致循环尝试加锁,直到加锁成功,这样就解决了当前线程“意外”释放了其他线程的锁的问题。
所以最终的优化代码如下:
public String stock(){
String Lock_Key = "lock";
//获取锁的实例
RLock lock = redisson.getLock(Lock_Key);
try {
//1.所有线程进来先尝试获取锁,redis中的setnx命令
Boolean id = template.opsForValue().setIfAbsent(Lock_Key, clientId);
template.expire(Lock_Key,30L, TimeUnit.SECONDS);
//未拿到锁则返回错误,加锁失败
if (!id)
return "error";
//Redisson中加锁逻辑
lock.lock(30, TimeUnit.SECONDS);
//2.成功拿到锁之后执行秒杀的业务逻辑
int stock = Integer.parseInt(template.opsForValue().get("stock"));
if (stock > 0){
stock -= 1;
template.opsForValue().set("stock",stock+"");
System.out.println("秒杀成功!" + stock);
}else {
System.out.println("秒杀失败!");
}
}finally {
//3.释放锁,无论秒杀是否成功都要释放锁
lock.unlock();
}
return "end";
}
如有不足,欢迎指正,感谢浏览!