当前位置: 首页 > news >正文

基于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;
	}
}

 

 完毕。。。。

相关文章:

  • 学历证书查询 易语言代码
  • ssm小型物流信息系统毕业设计源码071146
  • Windows使用命令查看端口号占用情况并关闭进程
  • Windows与网络基础-5-安装eNSP软件环境
  • solidworks动画制作教程——装配体爆炸动画
  • 青菜学艺往事
  • Java14-线程、同步
  • Unity中的序列化和反序列化
  • RocketMQ的架构设计
  • C++ 小游戏 视频及资料集(5)
  • k8s主节点与子节点的错误解决
  • spring boot 自定redis缓存注解
  • K8s 高可用集群架构(二进制)部署及应用
  • Linux上 tomcat的虚拟主机IP映射配置
  • 使用 MAUI 进行数据可视化:与 图表控件LightningChart JS 的兼容性项目模板
  • 【跃迁之路】【733天】程序员高效学习方法论探索系列(实验阶段490-2019.2.23)...
  • 2018一半小结一波
  • Angular js 常用指令ng-if、ng-class、ng-option、ng-value、ng-click是如何使用的?
  • gf框架之分页模块(五) - 自定义分页
  • gitlab-ci配置详解(一)
  • Golang-长连接-状态推送
  • HTML5新特性总结
  • Java 内存分配及垃圾回收机制初探
  • mac修复ab及siege安装
  • Redash本地开发环境搭建
  • Redis学习笔记 - pipline(流水线、管道)
  • SpiderData 2019年2月25日 DApp数据排行榜
  • spring boot 整合mybatis 无法输出sql的问题
  • Transformer-XL: Unleashing the Potential of Attention Models
  • UMLCHINA 首席专家潘加宇鼎力推荐
  • vue.js框架原理浅析
  • 从伪并行的 Python 多线程说起
  • 前端性能优化--懒加载和预加载
  • 前嗅ForeSpider教程:创建模板
  • 让你的分享飞起来——极光推出社会化分享组件
  • 如何借助 NoSQL 提高 JPA 应用性能
  • 如何优雅的使用vue+Dcloud(Hbuild)开发混合app
  • 使用阿里云发布分布式网站,开发时候应该注意什么?
  • 小程序上传图片到七牛云(支持多张上传,预览,删除)
  • 一个6年java程序员的工作感悟,写给还在迷茫的你
  • 一个项目push到多个远程Git仓库
  • 追踪解析 FutureTask 源码
  • 好程序员web前端教程分享CSS不同元素margin的计算 ...
  • #调用传感器数据_Flink使用函数之监控传感器温度上升提醒
  • $.proxy和$.extend
  • (7)STL算法之交换赋值
  • (NSDate) 时间 (time )比较
  • (vue)el-checkbox 实现展示区分 label 和 value(展示值与选中获取值需不同)
  • (八)c52学习之旅-中断实验
  • (含react-draggable库以及相关BUG如何解决)固定在左上方某盒子内(如按钮)添加可拖动功能,使用react hook语法实现
  • (蓝桥杯每日一题)love
  • (图)IntelliTrace Tools 跟踪云端程序
  • (一)RocketMQ初步认识
  • (译)2019年前端性能优化清单 — 下篇
  • .NET Core 控制台程序读 appsettings.json 、注依赖、配日志、设 IOptions