Redis实现分布式锁
1、什么是分布式锁
提到锁,我们最熟悉的就是线程锁和进程锁
线程锁主要是给方法、代码块加锁。在同一时刻只有一个线程能够获取锁。线程锁只在同一个JVM内起作用,线程锁的实现在根本上是依靠线程之间共享内存来实现的,比如Synchronized、Lock等
进程锁是控制同一操作系统中多个进程访问某个共享资源
在单机环境下,针对多线程并发问题可以通过线程锁来进行互斥控制,但是分布式环境下,系统分别部署在不同的机器上,不是同一个JVM,因此线程锁不起作用,为了解决这种问题,因此需要一种跨JVM的互斥机制来控制资源的访问,因此分布式锁出现了
2、分布式锁的实现方式
①数据库乐观锁
②基于Zookeeper实现分布式锁
③基于Redis实现分时锁(主要介绍)
3、Redis如何实现分布式锁
主要是基于Redis命令:SET [key] [value] NX EX [time]
下面是一个模拟分布式锁(模拟售票问题)
1、代码
//加锁
public void test(){
try {
//setIfAbsent就是 SETNX在Java里面的封装
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lock-key", "lock-value");
if(result == null || !result){
//加锁失败,返回错误信息
System.out.println("加锁失败");
return;
}
//获取库存
int ticket = Integer.parseInt(stringRedisTemplate.opsForValue().get("ticket"));
if(ticket > 0){
int nowTicket = ticket - 1;
stringRedisTemplate.opsForValue().set("ticket",nowTicket+"");
System.out.println("扣减成功,当前剩余票数"+nowTicket);
}else {
System.out.println("票数不足!");
}
} finally {
//如果中间发生异常,最终都会释放锁
stringRedisTemplate.delete(key);
}
}
可能存在问题:
1、当中途突然宕机或者运维kill -9 直接杀死进程,finally无法执行,锁最终无法释放,造成堵塞问题 (解决方式:加锁时增加过期时间)
2、当A、B两个线程同时进来后,A线程加锁成功,然后B线程加锁失败,此时B线程结束前执行finally方法,将A线程加的锁删除了,此时C线程进来,又获得了锁,AC两个线程仍然存在并发问题 (解决方式:给线程增加唯一标识,只能由加锁的线程来解锁)
优化后的代码:
public void test(){
String key = "lock-key";
//给予当前线程的唯一标识
String value = UUID.randomUUID().toString();
try {
//setIfAbsent就是 SETNX在Java里面的封装,优化:在此处设定30s的过期时间,就算是中途进程被杀掉,30S后锁会自动过期删除
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, value,30,TimeUnit.SECONDS);
if(result == null || !result){
//加锁失败,返回错误信息
System.out.println("加锁失败");
return;
}
//获取库存
int ticket = Integer.parseInt(stringRedisTemplate.opsForValue().get("ticket"));
if(ticket > 0){
int nowTicket = ticket - 1;
stringRedisTemplate.opsForValue().set("ticket",nowTicket+"");
System.out.println("扣减成功,当前剩余票数"+nowTicket);
}else {
System.out.println("票数不足!");
}
} finally {
//当前线程的唯一标识和锁存储的唯一标识匹配时才删除锁
if(value.equals(stringRedisTemplate.opsForValue().get(key))){
stringRedisTemplate.delete(key);
}
}
}
此时就完成了一个分布式锁,对于一般并发量不高的场景是足够的,但这就没有问题了吗?
仍可能存在问题:
1、当中间业务逻辑执行时间较长,超过30s,锁仍然会自动释放,仍然会造成当前线程未完成,然后另一线程又获得了锁的情况
解决方式:可以另外开启一个线程,定时扫描,如每次定时10s扫描一次分布式锁是否超时,如果分布式锁超时,而且当前线程没有完成,那就增加分布式锁的时间(续命)
redission框架集成了这种操作:
//引入redission依赖后
public void test(){
String key = "lock-key";
//初始化
RLock redissionLock = redission.getLock(key);
try {
//加锁
redissionLock.lock();
//获取库存
int ticket = Integer.parseInt(stringRedisTemplate.opsForValue().get("ticket"));
if(ticket > 0){
int nowTicket = ticket - 1;
stringRedisTemplate.opsForValue().set("ticket",nowTicket+"");
System.out.println("扣减成功,当前剩余票数"+nowTicket);
}else {
System.out.println("票数不足!");
}
} finally {
//解锁
redissionLock.unlock();
}
}