分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
为什么我们要用到分布式锁?
在使用sychronized会出现锁失效 ,因为我们又新开了一个JVM。
每个JVM都有一个独立的锁监视器,使用无法保障多集群状态下只有一个线程访问一个代码块。所以我们发明了分布式锁,实现在集群状态下,一个线程只访问一个代码块的功能。


分布式锁的特点:
- 多线程可见:实现效果同上
- 互斥:分布式锁必须能够确保在任何时刻只有一个节点能够获得锁,其他节点需要等待。
- 高可用性:当持有锁节点发生故障或宕机时,系统需要能够自动释放该锁,以确保其他节点能够继续获取锁。
- 高性能:减少对共享资源的访问等待时间,以及减少锁竞争带来的开销。
- 可重入性:如果一个节点已经获得了锁,那么它可以继续请求获取该锁而不会造成死锁。(锁超时机制)为了避免某个节点因故障或其他原因无限期持有锁而影响系统正常运行,分布式锁通常应该设置超时机制,确保锁的自动释放。

分布式锁解决超卖问题
流程图:

创建分布式锁:
java
public class SimpleRedisLock implements ILock {
/**
* RedisTemplate
*/
private StringRedisTemplate stringRedisTemplate;
/**
* 锁的名称
*/
private String name;
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
/**
* 获取锁
*
* @param timeoutSec 超时时间
* @return
*/
@Override
public boolean tryLock(long timeoutSec) {
String id = Thread.currentThread().getId() + "";
// SET lock:name id EX timeoutSec NX
Boolean result = stringRedisTemplate.opsForValue()
.setIfAbsent("lock:" + name, id, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
/**
* 释放锁
*/
@Override
public void unlock() {
stringRedisTemplate.delete("lock:" + name);
}
}
分布式锁的优化
优化1
实现了解决用分布式锁可能出现的超卖问题
问题发生原因解释:
当锁超时释放后,其他线程可能获取锁, 此时原线程执行完毕后误删新线程的锁, 导致锁失效,引发并发问题。

解决方式:我们为分布式锁提供一个线程标识,在释放锁时判断当前锁是否是自己的锁

java
@Override
public void unlock() {
// 判断锁的线程标识是否与当前线程一致
String currentThreadFlag = ID_PREFIX + Thread.currentThread().getId();
String redisThreadFlag = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if (currentThreadFlag != null || currentThreadFlag.equals(redisThreadFlag)) {
// 一致,说明当前的锁就是当前线程的锁,可以直接释放
stringRedisTemplate.delete(KEY_PREFIX + name);
}
// 不一致,不能释放
}
}
优化2
解决了锁超时释放的问题
问题发生原因解释:在上述优化中,我们给锁加了一个线程标识,在释放锁是加了一个线程判断,防止了锁超时释放的超卖问题。这其中有一个bug,当锁在判断完线程正确之后,发生了阻塞,但是锁已经超时释放了。这时线程2就会立马把锁抢过来,当线程1完成任务后,又因为刚刚判断过一致,直接释放了锁。

Redis中的lua脚本,解决超卖问题
lua是一种编写语言,我们小时候常玩的愤怒的小鸟正是用这种语言编写的。
用lua脚本解决的问题:
确保多条命令执行时的原子性,解决超卖问题。上述问题的又一新策略。


如何在java中调用lua脚本

java
package com.hmdp.utils.lock.impl;
import cn.hutool.core.lang.UUID;
import com.hmdp.utils.lock.Lock;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements Lock {
/**
* RedisTemplate
*/
private StringRedisTemplate stringRedisTemplate;
/**
* 锁的名称
*/
private String name;
/**
* key前缀
*/
private static final String KEY_PREFIX = "lock:";
/**
* ID前缀
*/
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
/**
* 获取锁
*
* @param timeoutSec 超时时间
* @return
*/
@Override
public boolean tryLock(long timeoutSec) {
String threadId = ID_PREFIX + Thread.currentThread().getId() + "";
// SET lock:name id EX timeoutSec NX
Boolean result = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
/**
* 加载Lua脚本
*/
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("lua/unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
/**
* 释放锁
*/
@Override
public void unlock() {
// 执行lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId()
);
}
Redisson
Redssion是一个成熟的Redis框架,包含功能如:分布式锁和同步器、分布式对象、分布式集合、分布式服务,各种Redis实现分布式的解决方案。
经过上述优化,我们的业务功能实现已经达到生产可用级别,但是还有如下几个问题:
- 分布式锁不可重入:同一个线程在未释放锁之前,不能再次获取同一把锁。
- 分布式锁不可重试:获取锁失败后,不能自动再次尝试获取锁,需要手动或额外逻辑控制重试。
- 分布式锁超时释放 :分布式锁通过设置过期时间避免死锁,但可能导致业务未完成时锁被提前失效。
而Redisson的存在就是为了解决这个问题。