随着互联网技术的不断发展,数据量的不断增加,业务逻辑日趋复杂,在这种背景下,传统的集中式系统已经无法满足我们的业务需求,分布式系统被应用在更多的场景,与单体应用不同的是,分布式系统中竞争共享资源的最小粒度从线程升级成了进程。而在分布式系统中访问共享资源就需要一种互斥机制,来解决分布式系统中控制共享资源访问的问题,以保证数据一致性,在这种情况下,我们就需要用到分布式锁。
总结:
1)应用场景:分布式系统。
2)作用:提供一种共享资源访问的互斥机制,保证数据一致性。
目前分布式锁常见的三种实现方式:
1、基于数据库实现分布式锁;
2、基于缓存(Redis等)实现分布式锁;
3、基于Zookeeper实现分布式锁。
本文就基于Redis手工实现分布式锁,当前现在主流的是基于Redisson工具包实现(不在本文范围内),但手工实现原理基本一致。
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。
接口比较简单,主要定义了加锁和释放锁两个方法
- 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);
- }
实现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;
- }
- }
完毕。。。。