redis分布式锁的流程图
获取锁
释放锁
代码实现
分布式锁接口
接口中主要有 3 个方法
- lock:获取锁,不论成功还是失败,会立即返回结果
- tryLock:尝试在指定的时间内获取锁,直到超时
- unLock:释放锁
import java.util.concurrent.TimeUnit;
/**
* 分布式锁接口,内部定义了3个方法
*/
public interface DcsLock {
/**
* 获取锁,立即返回结果
*
* @param resources 锁资源
* @return 上锁结果
*/
LockResult lock(String resources);
/**
* 在指定的时间内尝试获取锁,直到超时
*
* @param resources 资源
* @param timeout 超时时间
* @param unit 超时时间单位
* @return 上锁结果
*/
LockResult tryLock(String resources, long timeout, TimeUnit unit);
/**
* 释放锁
*
* @param resources
*/
void unLock(String resources);
/**
* 上锁结果
*/
class LockResult {
public static LockResult fail(String resources) {
return new LockResult(resources, false);
}
public static LockResult success(String resources) {
return new LockResult(resources, false);
}
/**
* 上锁的资源信息
*/
private String resources;
/**
* 上锁是否成功
*/
private boolean success;
public LockResult() {
}
public LockResult(String resources, boolean success) {
this.resources = resources;
this.success = success;
}
public String getResources() {
return resources;
}
public void setResources(String resources) {
this.resources = resources;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
@Override
public String toString() {
return "LockResult{" +
"resources='" + resources + '\'' +
", success=" + success +
'}';
}
}
}
redis 实现分布式锁
下面这个类实现了上面的接口,内部采用 redis 实现了分布式锁的功能,注释比较详细,大家主要看对应的三个方法的代码。
代码中有 5 个比较重要的点:
- redis 中用到了 2 个关键方法:setIfAbsent 和 getAndSet,这 2 个方法都是原子操作,第一个方法用来设置一个值,当这个 key 不存在的时候才会设置成功;而第二个方法用来返回当前值的同时并设置一个新的值,注意这 2 个方法都是原子性的,也就是说,在并发的情况下,能够确保其正确性。
- 锁支持可重入:同一个线程支持多次获取一个分布式锁,所以需要判断锁的持有者是不是当前线程
- 锁的释放:需要放置锁被其他线程是否,所以释放的时候需要判断当前的操作者是不是锁的持有者线程
- 防止死锁:锁被占用之后,如果没有释放,比如获取锁之后系统重启了,这种情况会导致死锁,代码中我们在设置锁的值的时候,设置了一个超时时间,当超时时间过了,还未释放的,其他线程将可以尝试获取锁
- 锁续命:redis 实现的分布式锁是有有效期的,比如下面我们设置的是 100 秒,可能系统在跑批,耗时比较长,此时 100 秒可能不够,那么就需要一个程序来检测这种情况,发现程序还在使用锁,需要对锁进行续命操作,下面代码中当获取锁成功之后,我们会添加一个延迟续命的任务到延迟队列,不断的触发续命操作,直到锁被释放。
import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
/**
* redis实现分布式锁(实现了锁重入、自动续命、防止锁被非持有者删除)
*/
@Component
public class RedisLock implements DcsLock, InitializingBean, DisposableBean {
private Logger logger = LoggerFactory.getLogger(RedisLock.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 等待时间间隔:100ms
*/
private Long waitTimeInterval = 100L;
/**
* 默认时间单位(ms)
*/
private TimeUnit defaultTimeUnit = TimeUnit.MILLISECONDS;
/**
* 锁持有时间,默认100s
*/
private Long defaultHolderTime = this.defaultTimeUnit.convert(100L, TimeUnit.SECONDS);
/**
* 锁到期前多久开始续命(默认为持有时间过半的时候开始续命)
*/
private Long beforeExpireTime = defaultHolderTime / 2;
/**
* 续命队列(使用延迟队列)
*/
private DelayQueue lockDelayedTaskDelayQueue = new DelayQueue<>();
/**
* resources->锁持有者线程
*/
private Map lockOwnerThreadMap = new ConcurrentHashMap<>();
/**
* resources->锁持有次数
*/
private Map lockCounterMap = new ConcurrentHashMap<>();
/**
* resources->锁结果
*/
private Map lockResultMap = new ConcurrentHashMap<>();
/**
* 释放锁 &&入队列,这俩会操作本地缓存,需要互斥
*/
private ReentrantLock lock = new ReentrantLock();
/**
* 是否已停止
*/
private volatile boolean stop = false;
@Override
public LockResult lock(String resources) {
LockResult lockResult = LockResult.fail(resources);
//1、判断当前线程是否持有锁,如果有则,将持有次数+1
Thread thread = this.lockOwnerThreadMap.get(resources);
if (Thread.currentThread() == thread) {
this.lockCounterMap.get(resources).incrementAndGet();
lockResult = this.lockResultMap.get(resources);
} else {
//2、当前线程未持有锁,则从redis中获取锁信息
LockInfo lockInfo = this.getLockInfoFromRedis(resources);
String key = this.getKey(resources);
//未上锁
if (lockInfo == null) {
Long expireTimeMs = this.getExpireTimeMs(this.defaultTimeUnit, this.defaultHolderTime);
Boolean result = this.stringRedisTemplate.opsForValue().setIfAbsent(
key,
JSON.toJSONString(this.buildLockInfo(expireTimeMs)));
//setNX成功,上锁成功
if (result) {
this.stringRedisTemplate.expire(key, this.defaultHolderTime, this.defaultTimeUnit);
lockResult = DcsLock.LockResult.success(resources);
this.lockSuccessAfter(resources, lockResult, expireTimeMs);
}
} else {
//被上锁了,则判断锁是否已过期(为了避免死锁的情况【上锁了,但是没有释放】),持有者还未释放,则当前获取者尝试调用getAndSet(原子操作)尝试获取锁
if (lockInfo.getExpireTimeMs() < System.currentTimeMillis()) {
Long expireTimeMs = this.getExpireTimeMs(this.defaultTimeUnit, this.defaultHolderTime);
String oldValue = this.stringRedisTemplate.opsForValue().getAndSet(
key,
JSON.toJSONString(this.buildLockInfo(expireTimeMs)));
//getAndSet可能被并发执行,这个判断是为了判断并发的情况下,getAndSet被当前这个线程执行成功了
if (oldValue.equals(JSON.toJSONString(lockInfo))) {
lockResult = DcsLock.LockResult.success(resources);
this.lockSuccessAfter(resources, lockResult, expireTimeMs);
}
}
}
}
return lockResult;
}
@Override
public LockResult tryLock(String resources, long timeout, TimeUnit unit) {
Long expireTimeMs = this.getExpireTimeMs(unit, timeout);
while (true) {
LockResult lockResult = this.lock(resources);
long currentTimeMillis = System.currentTimeMillis();
if (lockResult.isSuccess() || expireTimeMs < currentTimeMillis) {
return lockResult;
} else {
try {
long waitTime = Math.min(this.waitTimeInterval, expireTimeMs - currentTimeMillis);
this.defaultTimeUnit.sleep(waitTime);
} catch (InterruptedException e) {
logger.error(e.getMessage(), e);
return lockResult;
}
}
}
}
@Override
public void unLock(String resources) {
//当前线程次有锁,则次有次数-1,次有次数为0的时候,将其从redis中和本地缓存中干掉
Thread thread = this.lockOwnerThreadMap.get(resources);
if (Thread.currentThread() == thread) {
int count = this.lockCounterMap.get(resources).decrementAndGet();
if (count == 0) {
//从redis中干掉
this.stringRedisTemplate.delete(this.getKey(resources));
//清理本地数据
this.unLockAfter(resources);
}
}
}
/**
* 将锁信息放入本地缓存 & 加入续命队列
*
* @param resources
* @param lockResult
* @param expireTimeMs
*/
private void lockSuccessAfter(String resources, LockResult lockResult, Long expireTimeMs) {
//1、将数据放入到本地缓存
this.lockOwnerThreadMap.put(resources, Thread.currentThread());
this.lockCounterMap.put(resources, new AtomicInteger(1));
this.lockResultMap.put(resources, lockResult);
//2、加入续命队列
this.addExtendingLifeQueue(resources, expireTimeMs);
}
/**
* 加入续命队列,续命队列会在任务过期前进行续命
*/
private void addExtendingLifeQueue(String resource, Long expireTimeMs) {
//释放锁 && 入续命队列互斥
this.lock.lock();
try {
if (this.lockResultMap.containsKey(resource)) {
LockDelayedTask lockDelayedTask = new LockDelayedTask(
resource,
expireTimeMs,
this.defaultTimeUnit.toMillis(this.beforeExpireTime));
this.lockDelayedTaskDelayQueue.put(lockDelayedTask);
}
} finally {
this.lock.unlock();
}
}
/**
* 从redis中获取LockInfo信息
*
* @param resource
* @return
*/
private LockInfo getLockInfoFromRedis(String resource) {
String value = this.stringRedisTemplate.opsForValue().get(this.getKey(resource));
if (value != null) {
return JSON.parseObject(value, LockInfo.class);
} else {
return null;
}
}
/**
* 创建一个 LockInfo
*
* @param expireTimeMs 过期时间
* @return
*/
private LockInfo buildLockInfo(Long expireTimeMs) {
return new LockInfo(expireTimeMs, UUID.randomUUID().toString());
}
private String getKey(String resources) {
return String.format("%s:%s", RedisDcsLock.class.getName(), resources);
}
@Override
public void afterPropertiesSet() {
// 启动续命线程
Thread extendingLifeThread = new Thread(this::executeExtendingLife);
extendingLifeThread.setDaemon(true);
extendingLifeThread.start();
}
@Override
public void destroy() throws Exception {
this.stop = true;
}
/**
* 根据过期时间计算过期截止时间
*
* @param unit
* @param expireTime
* @return
*/
private Long getExpireTimeMs(TimeUnit unit, Long expireTime) {
return System.currentTimeMillis() + unit.toMillis(expireTime);
}
/**
* 清理本地数据
*
* @param resources
*/
private void unLockAfter(String resources) {
//释放锁 && 入续命队列互斥
this.lock.lock();
try {
this.lockOwnerThreadMap.remove(resources);
this.lockResultMap.remove(resources);
this.lockCounterMap.remove(resources);
} finally {
this.lock.unlock();
}
}
/**
* 执行续命
*/
private void executeExtendingLife() {
while (!this.stop) {
LockDelayedTask lockDelayedTask = null;
//从续命队列中拉取续命任务
try {
lockDelayedTask = this.lockDelayedTaskDelayQueue.poll(1, TimeUnit.SECONDS);
//续命和释放锁互斥
if (lockDelayedTask != null && this.lockResultMap.containsKey(lockDelayedTask.getResources())) {
//续命过程:更新redis中锁信息、过期时间
logger.info("续命start:[{}]", lockDelayedTask);
String resources = lockDelayedTask.getResources();
String key = this.getKey(resources);
Long expireTimeMs = this.getExpireTimeMs(this.defaultTimeUnit, this.defaultHolderTime);
LockInfo lockInfo = this.buildLockInfo(expireTimeMs);
this.stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(lockInfo));
this.stringRedisTemplate.expire(key, this.defaultHolderTime, this.defaultTimeUnit);
//继续将任务丢到续命队列
this.addExtendingLifeQueue(resources, expireTimeMs);
logger.info("续命end:[{}]", lockDelayedTask);
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
/**
* 锁信息
*/
static class LockInfo {
/**
* 过期时间(ms)
*/
private Long expireTimeMs;
/**
* 一个id,防止重复
*/
private String id;
public LockInfo() {
}
public LockInfo(Long expireTimeMs, String id) {
this.expireTimeMs = expireTimeMs;
this.id = id;
}
public Long getExpireTimeMs() {
return expireTimeMs;
}
public void setExpireTimeMs(Long expireTimeMs) {
this.expireTimeMs = expireTimeMs;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public String toString() {
return "LockInfo{" +
"expireTimeMs=" + expireTimeMs +
", id='" + id + '\'' +
'}';
}
}
/**
* 锁延迟任务
*/
public static class LockDelayedTask implements Delayed {
//锁id
private String resources;
//锁有效期(ms)
private long expireTimeMs;
//多久续命一次(ms)
private long beforeExpireTime;
/**
* @param resources 资源
* @param expireTimeMs 锁有效期(ms)
* @param beforeExpireTime 锁到期前多久开始续命
*/
public LockDelayedTask(String resources, long expireTimeMs, long beforeExpireTime) {
this.resources = resources;
this.expireTimeMs = expireTimeMs;
this.beforeExpireTime = beforeExpireTime;
}
@Override
public long getDelay(@NotNull TimeUnit unit) {
return unit.convert(this.expireTimeMs - this.beforeExpireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(@NotNull Delayed o) {
LockDelayedTask o2 = (LockDelayedTask) o;
return Long.compare(this.getExpireTimeMs(), o2.getExpireTimeMs());
}
public String getResources() {
return resources;
}
public void setResources(String resources) {
this.resources = resources;
}
public long getExpireTimeMs() {
return expireTimeMs;
}
public void setExpireTimeMs(long expireTimeMs) {
this.expireTimeMs = expireTimeMs;
}
public long getBeforeExpireTime() {
return beforeExpireTime;
}
public void setBeforeExpireTime(long beforeExpireTime) {
this.beforeExpireTime = beforeExpireTime;
}
@Override
public String toString() {
return "DelayedLockResult{" +
"resources='" + resources + '\'' +
", expireTimeMs=" + expireTimeMs +
", beforeExpireTime=" + beforeExpireTime +
'}';
}
}
}