基于Redis手工实现分布式锁
1.分布式锁概述
1.1什么是分布式锁
随着互联网技术的不断发展,数据量的不断增加,业务逻辑日趋复杂,在这种背景下,传统的集中式系统已经无法满足我们的业务需求,分布式系统被应用在更多的场景,与单体应用不同的是,分布式系统中竞争共享资源的最小粒度从线程升级成了进程。而在分布式系统中访问共享资源就需要一种互斥机制,来解决分布式系统中控制共享资源访问的问题,以保证数据一致性,在这种情况下,我们就需要用到分布式锁。
总结:
1)应用场景:分布式系统。
2)作用:提供一种共享资源访问的互斥机制,保证数据一致性。
1.2分布式锁的特性
- 互斥“”在分布式系统环境下,一个方法在同一时间只能被一个线程执行(即获取锁)
- 高可用:高可用的获取锁与释放锁
- 高性能:高性能的获取锁与释放锁
- 可重入:具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
- 防止死锁:具备锁失效机制,超时即自动解锁,
- 非阻塞:即如果线程没有获取到锁将,直接返回获取锁失败,而不会一直阻塞
2.分布式锁的实现
目前分布式锁常见的三种实现方式:
1、基于数据库实现分布式锁;
2、基于缓存(Redis等)实现分布式锁;
3、基于Zookeeper实现分布式锁。
本文就基于Redis手工实现分布式锁,当前现在主流的是基于Redisson工具包实现(不在本文范围内),但手工实现原理基本一致。
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。
2.1 锁接口定义
接口比较简单,主要定义了加锁和释放锁两个方法
package com.example.demo.redis.lock;
import java.util.concurrent.TimeUnit;
/**
*
* @ClassName: RedisDistributeLock
* @Description: Redis分布式锁
* @Author: liulianglin
* @DateTime 2022年8月30日 下午5:47:47
*/
public interface RedisDistributeLock {
/**
*
* @Description: 加锁
* @Author: liulianglin
* @Datetime: 2022年8月30日 下午5:49:43
* @param key 主键
* @param timeout 超时时间
* @param unit 超时时间单位
* @return boolean true:加锁成功;false:加锁失败
* @throws
*/
boolean tryLock(String key, long timeout, TimeUnit unit);
/**
*
* @Description: 释放锁。加锁和释放锁的线程必须保证是同一个。
* @Author: liulianglin
* @Datetime: 2022年8月30日 下午5:51:20
* @param key 主键
* @throws
*/
void releaseLock(String key);
}
2.2 接口实现
实现RedisDistributeLock接口,其中针对分布式锁的一些特性进行实现,如加锁解锁线程一致性保证、可重入、非阻塞等(参考注释)
package com.example.demo.redis.lock;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
public class RedisDistributeLockImpl implements RedisDistributeLock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
// 计数器
private ThreadLocal<Integer> counterThreadLocal = new ThreadLocal<Integer>();
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit) {
Boolean isLocked = false;
if (Objects.isNull(threadLocal.get())) {
String uuid = UUID.randomUUID().toString();
threadLocal.set(uuid);
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
// 如果获取锁失败,通过自旋尝试获取锁,
if (!isLocked) {
for (;;) {
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
if (isLocked) {
// 获取成功立即退出
break;
}
}
}
/*
* 启动一个线程扮演“看门狗”的角色,不断更新锁索过期时间
*
* 注意:这里将stringRedisTemplate对象传给看门狗
*/
new Thread(new WatchDogThread(uuid, stringRedisTemplate, key)).start();
} else {
isLocked = true;
}
// 加锁成功后
if (isLocked) {
Integer curCount = counterThreadLocal.get() == null ? 0 : counterThreadLocal.get();
counterThreadLocal.set(curCount++);
}
return isLocked;
}
@Override
public void releaseLock(String key) {
// 保证解锁和加锁线程是同一个,防止避免了一个线程对程序进行了加锁操作后,其他线程对这个锁进行了解锁操作的问题
String uuid = stringRedisTemplate.opsForValue().get(key);
if (!Objects.isNull(threadLocal.get()) &&
threadLocal.get().equals(uuid)) {
Integer curCount = counterThreadLocal.get();
if (Objects.isNull(curCount) || (--curCount)<=0) {
stringRedisTemplate.delete(key);
// 获取对应的看门狗线程的ID
String watchDogThreadIdStr = stringRedisTemplate.opsForValue().get(uuid);
// 获取看门狗
Thread watchDogThread = ThreadUtils.getThreadByThreadId(Long.valueOf(watchDogThreadIdStr));
if (!Objects.isNull(watchDogThread)) {
// 终端看门狗
watchDogThread.interrupt();
stringRedisTemplate.delete(uuid);
}
}
}
}
}
2.3 看门狗线程
看门狗线程,更新锁的超时时间,保证锁的释放一定是在加锁线程业务代码执行完毕之后,
防止在业务处理过程中锁超时失效,其他线程依旧能够获取到锁。
package com.example.demo.redis.lock;
import java.util.concurrent.TimeUnit;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
*
* @ClassName: WatchDogThread
* @Description: 看门狗线程,更新锁的超时时间,保证锁的释放一定是在加锁线程业务代码执行完毕之后,
* 防止在业务处理过程中锁超时失效,其他线程依旧能够获取到锁。
* @Author: liulianglin
* @DateTime 2022年8月31日 上午9:33:07
*/
public class WatchDogThread implements Runnable{
private String uuid;
private String key;
private StringRedisTemplate stringRedisTemplate;
public WatchDogThread(String uuid, StringRedisTemplate stringRedisTemplate, String key) {
this.uuid = uuid;
this.stringRedisTemplate = stringRedisTemplate;
this.key = key;
}
@Override
public void run() {
/*
* 以uuid为key,将当前线程的ID作为value保存到Redis中
在RedisDistributeLockImpl删除锁时需要通过uuid获取到当前线程ID,然后停止当前看门狗线程。
*/
stringRedisTemplate.opsForValue().set(uuid, String.valueOf(Thread.currentThread().getId()));
// 循环更新所的过期时间
while(true) {
stringRedisTemplate.expire(key, 10, TimeUnit.SECONDS);
try {
// 每秒执行1次
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.4 线程操作工具类
主要是通过线程ID获取线程,目前只有一个方法,比较简单
package com.example.demo.redis.lock;
public class ThreadUtils {
/**
*
* @Description: 通过ThreadID获取线程对象
* @Author: liulianglin
* @Datetime: 2022年8月31日 上午10:00:54
* @param threadId
* @return Thread
* @throws
*/
public static Thread getThreadByThreadId(long threadId) {
ThreadGroup group = Thread.currentThread().getThreadGroup();
while(group != null) {
Thread[] threads = new Thread[(int)(group.activeCount() * 1.2)];
int count = group.enumerate(threads, true);
for(int i = 0; i < count; i++) {
if(threadId == threads[i].getId()) {
return threads[i];
}
}
group = group.getParent();
}
return null;
}
}
完毕。。。。