分布式锁的实现
为什么要用分布式锁
- 解决同一资源被多个客户端并发写入,导致数据错乱
开源的解决方案
- Redisson
是一个高级的分布式协调Redis客户端;Redisson、Jedis、Lettuce 是三个不同的操作 Redis 的客户端,Jedis、Lettuce 的 API 增删改查,而 Redisson API 侧重于分布式开发
自己用redis去实现一个分布式锁功能
需要考虑的问题
- 同一个客户端同一个线程内部多次加锁的问题
支持可重入锁解决
- 加锁的规则
公平锁 或者 非公平锁; 如果采用公平锁实现,那么每个线程都先进入等待队列;如果采用非公平锁实现,那么每个线程先尝试获取锁,失败后在进入等待队列
- 程序异常导致redis锁未释放,如何处理?
采用key过期策略
- 业务处理时间大于key过期时间怎么办?
保障机制: 开启线程对key进行重置过期时间
具体实现代码
接口定义
/**
* 分布式锁
*
* @author 唐海斌
* @date 2022/4/14 14:45
*/
public interface DistributedLock {
/**
* 加锁
* @param monitor 锁监视器
*/
void lock(String monitor);
/**
* 加锁,在指定时间内无法获取到锁则返回
*
* @param monitor 锁监视器
* @param second 超时时间: 单位秒
*/
boolean tryLock(String monitor, long second);
/**
* 释放锁
* @param monitor 锁监视器
*/
void unlock(String monitor);
}
具体实现
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
/**
* 分布式锁-----使用redis实现
* <p>
* 需实现的功能:
* 1. 可重入锁
* 2. 锁规则: 公平or非公平
* 3. 锁过期时间刷新问题
*
*
* @author 唐海斌
* @date 2022/4/14 14:45
*/
@Slf4j
@Component
public class RedisDistributedLock implements DistributedLock {
/**
* 获取锁的线程排队
*/
private static final ConcurrentLinkedQueue<Thread> THREAD_QUEUE = new ConcurrentLinkedQueue<>();
/**
* 默认锁过期时间
*/
private static final int DEFAULT_EXPIRE_LOCK_TIME_MINUTE = 2;
/**
* 线程锁持有次数
* key: 监视器
* value: 具体线程信息
*/
private static final Map<String, LockDetail> THREAD_LOCK_COUNT = new HashMap<>();
@Autowired
private RedisTemplate redisTemplate;
@Override
public void lock(final String monitor) {
assert monitor != null && !Objects.equals(monitor.trim(), "");
final Thread currentThread = Thread.currentThread();
Boolean lockSuccess = execLock(currentThread, monitor);
if (Objects.equals(lockSuccess, Boolean.TRUE)) {
return;
}
//未获取到锁, 放入等待队列并且阻塞当前线程
THREAD_QUEUE.add(currentThread);
LockSupport.park();
//被唤醒则重试获取锁
lock(monitor);
}
@Override
public boolean tryLock(String monitor, long second) {
assert monitor != null && !Objects.equals(monitor.trim(), "");
assert second > 0;
final Thread currentThread = Thread.currentThread();
Boolean lockSuccess = execLock(currentThread, monitor);
if (Objects.equals(lockSuccess, Boolean.TRUE)) {
return true;
}
//未获取到锁, 有限时阻塞当前线程
LockSupport.parkNanos(second);
//到了限制时间后,再次获取锁
return execLock(currentThread, monitor);
}
@Override
public void unlock(String monitor) {
assert monitor != null && !Objects.equals(monitor.trim(), "");
final Thread currentThread = Thread.currentThread();
LockDetail lockDetail = THREAD_LOCK_COUNT.get(monitor);
//未被当前线程持有
if (lockDetail == null || lockDetail.getThread() != currentThread) {
return;
}
Long currentCount = redisTemplate.opsForValue().decrement(monitor);
if (currentCount < 1L) {
try {
THREAD_LOCK_COUNT.remove(monitor);
//当前线程持有次数为0则从redis移除锁
redisTemplate.delete(monitor);
} finally {
//唤醒队列中的第一个线程
Thread firstThread = THREAD_QUEUE.poll();
if (firstThread != null) {
LockSupport.unpark(firstThread);
}
}
} else {
lockDetail.setCount(currentCount);
}
}
/**
* 执行加锁操作
*
* @param monitor
* @return
*/
private boolean execLock(Thread thread, String monitor) {
LockDetail lockDetail = THREAD_LOCK_COUNT.get(monitor);
if (lockDetail != null) {
if (lockDetail.getThread() == thread) {
//当前线程是重入情况
lockDetail.setCount(redisTemplate.opsForValue().increment(monitor));
return true;
}
return false;
}
//尝试加锁
if (execLock2Redis(thread, monitor)) {
return true;
}
//加锁失败,保障机制: 锁释放为0后,key未移除,所以这里获取一下锁持有数量
Object lockCountForRedis = redisTemplate.opsForValue().get(monitor);
if (lockCountForRedis == null) {
return false;
}
//如果该锁持有数量小于1则移除
if (Long.parseLong(lockCountForRedis.toString()) < 1) {
redisTemplate.delete(monitor);
}
//再次尝试获取锁
return execLock2Redis(thread, monitor);
}
private boolean execLock2Redis(Thread thread, String monitor) {
Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent(monitor, 1, DEFAULT_EXPIRE_LOCK_TIME_MINUTE, TimeUnit.MINUTES);
if (Objects.equals(lockSuccess, Boolean.TRUE)) {
THREAD_LOCK_COUNT.put(monitor, new LockDetail(thread, 1L));
}
return lockSuccess;
}
/**
* 定时重置key过期时间
*/
@PostConstruct
public void init() {
Thread refreshMonitorExpireTimeThread = new Thread(() -> {
while (true) {
Set<String> monitors = THREAD_LOCK_COUNT.keySet();
if (monitors.size() > 0) {
monitors.forEach(monitor -> {
//重置过期时间
redisTemplate.expire(monitor, DEFAULT_EXPIRE_LOCK_TIME_MINUTE, TimeUnit.MINUTES);
});
}
//阻塞1分钟
LockSupport.parkNanos(TimeUnit.MINUTES.toNanos(1));
}
});
refreshMonitorExpireTimeThread.setName("refresh-monitor-thread");
refreshMonitorExpireTimeThread.start();
}
@Data
class LockDetail {
/**
* 持有线程
*/
private Thread thread;
/**
* 线程持有锁的次数
*/
private Long count;
public LockDetail(Thread thread, Long count) {
this.thread = thread;
this.count = count;
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)