Redis分布式锁的实现方式

news2024/10/7 12:21:20

目录

    • 一、分布式锁是什么
      • 1、获取锁
      • 2、释放锁
    • 二、代码实例
      • 上面代码存在锁误删问题:
    • 三、基于```SETNX```实现的分布式锁存在下面几个问题
      • 1、不可重入
      • 2、不可重试
      • 3、超时释放
      • 4、主从一致性
    • 四、Redisson实现分布式锁
      • 1、pom
      • 2、配置类
      • 3、测试类
    • 五、探索tryLock源码
      • 1、tryLock源码
        • 尝试获取锁
      • 2、重置锁的有效期
        • 更新有效期
      • 3、调用lua脚本
    • 六、释放锁unlock源码
      • 1、取消更新任务
      • 2、删除定时任务

一、分布式锁是什么

分布式锁是 满足分布式系统或集群模式下多进程可见并且互斥的锁。

基于Redis实现分布式锁:

1、获取锁

  • 互斥:确保只能有一个线程获取锁;
  • 非阻塞:尝试获取锁,成功返回true,失败返回false;

添加锁过期时间,避免服务宕机引起死锁。

SET lock thread1 NX EX 10

2、释放锁

  • 手动释放;DEL key1
  • 超时释放,获取锁时添加一个超时锁;

二、代码实例

package com.guor.utils;

import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

public class RedisLock implements ILock{

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public RedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";

    @Override
    public boolean tryLock(long timeout) {
        // 获取线程唯一标识
        long threadId = Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId+"", timeout, TimeUnit.SECONDS);
        // 防止拆箱的空指针异常
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

上面代码存在锁误删问题:

  1. 如果线程1获取锁,但线程1发生了阻塞,导致Redis超时释放锁;
  2. 此时,线程2尝试获取锁,成功,并执行业务;
  3. 此时,线程1重新开始执行任务,并执行完毕,执行释放锁(即删除锁);
  4. 但是,线程1删除的锁,和线程2的锁是同一把锁,这就是分布式锁误删问题

在释放锁时,释放线程自己的分布式锁,就可以解决这个问题。

package com.guor.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

public class RedisLock implements ILock{

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public RedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";
    private static final String UUID_PREFIX = UUID.randomUUID().toString(true) + "-";

    @Override
    public boolean tryLock(long timeout) {
        // 获取线程唯一标识
        String threadId = UUID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeout, TimeUnit.SECONDS);
        // 防止拆箱的空指针异常
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        // 获取线程唯一标识
        String threadId = UUID_PREFIX + Thread.currentThread().getId();
        // 获取锁中的标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        // 判断标示是否一致
        if(threadId.equals(id)) {
            // 释放锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
}

三、基于SETNX实现的分布式锁存在下面几个问题

1、不可重入

同一个线程无法多次获取同一把锁。

2、不可重试

获取锁只尝试一次就返回false,没有重试机制。

3、超时释放

锁的超时释放虽然可以避免死锁,但如果业务执行耗时较长,也会导致锁释放,存在安全隐患。

4、主从一致性

如果Redis是集群部署的,主从同步存在延迟,当主机宕机时,此时会选一个从作为主机,但是此时的从没有锁标识,此时,其它线程可能会获取到锁,导致安全问题。

四、Redisson实现分布式锁

Redisson是一个在Redis的基础上实现的Java驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中包含各种分布式锁的实现。

1、pom

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>

2、配置类

package com.guor.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        // 配置
        Config config = new Config();

        /**
         * 单点地址useSingleServer,集群地址useClusterServers
         */
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123456");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

3、测试类

package com.guor;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Slf4j
@SpringBootTest
class RedissonTest {

    @Resource
    private RedissonClient redissonClient;

    private RLock lock;

    @BeforeEach
    void setUp() {
    	// 获取指定名称的锁
        lock = redissonClient.getLock("nezha");
    }

    @Test
    void test() throws InterruptedException {
        // 尝试获取锁
        boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);
        if (!isLock) {
            log.error("获取锁失败");
            return;
        }
        try {
            log.info("哪吒最帅,哈哈哈");
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

在这里插入图片描述

五、探索tryLock源码

1、tryLock源码

在这里插入图片描述

尝试获取锁

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
	// 最大等待时间
	long time = unit.toMillis(waitTime);
	long current = System.currentTimeMillis();
	long threadId = Thread.currentThread().getId();
	Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
	if (ttl == null) {
		return true;
	} else {
		// 剩余等待时间 = 最大等待时间 - 获取锁失败消耗的时间
		time -= System.currentTimeMillis() - current;
		if (time <= 0L) {// 获取锁失败
			this.acquireFailed(waitTime, unit, threadId);
			return false;
		} else {
			// 再次尝试获取锁
			current = System.currentTimeMillis();
			// subscribe订阅其它释放锁的信号
			RFuture<RedissonLockEntry> subscribeFuture = this.subscribe(threadId);
			// 当Future在等待指定时间time内完成时,返回true
			if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
				if (!subscribeFuture.cancel(false)) {
					subscribeFuture.onComplete((res, e) -> {
						if (e == null) {
							// 取消订阅
							this.unsubscribe(subscribeFuture, threadId);
						}

					});
				}

				this.acquireFailed(waitTime, unit, threadId);
				return false;// 获取锁失败
			} else {
				try {
					// 剩余等待时间 = 剩余等待时间 - 获取锁失败消耗的时间
					time -= System.currentTimeMillis() - current;
					if (time <= 0L) {
						this.acquireFailed(waitTime, unit, threadId);
						boolean var20 = false;
						return var20;
					} else {
						boolean var16;
						do {
							long currentTime = System.currentTimeMillis();
							// 重试获取锁
							ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
							if (ttl == null) {
								var16 = true;
								return var16;
							}
							// 再次失败了,再看一下剩余时间
							time -= System.currentTimeMillis() - currentTime;
							if (time <= 0L) {
								this.acquireFailed(waitTime, unit, threadId);
								var16 = false;
								return var16;
							}
							// 再重试获取锁
							currentTime = System.currentTimeMillis();
							if (ttl >= 0L && ttl < time) {
								// 通过信号量的方式尝试获取信号,如果等待时间内,依然没有结果,会返回false
								((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
							} else {
								((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
							}
							time -= System.currentTimeMillis() - currentTime;
						} while(time > 0L);

						this.acquireFailed(waitTime, unit, threadId);
						var16 = false;
						return var16;
					}
				} finally {
					this.unsubscribe(subscribeFuture, threadId);
				}
			}
		}
	}
}

在这里插入图片描述

2、重置锁的有效期

private void scheduleExpirationRenewal(long threadId) {
	RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry();
	// this.getEntryName():锁的名字,一个锁对应一个entry
	// putIfAbsent:如果不存在,将锁和entry放到map里
	RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
	if (oldEntry != null) {
		// 同一个线程多次获取锁,相当于重入
		oldEntry.addThreadId(threadId);
	} else {
		// 如果是第一次
		entry.addThreadId(threadId);
		// 更新有效期
		this.renewExpiration();
	}
}

更新有效期,递归调用更新有效期,永不过期

private void renewExpiration() {
	// 从map中得到当前锁的entry
	RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
	if (ee != null) {
		// 开启延时任务
		Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
			public void run(Timeout timeout) throws Exception {
				RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
				if (ent != null) {
					// 取出线程id
					Long threadId = ent.getFirstThreadId();
					if (threadId != null) {
						// 刷新有效期
						RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
						future.onComplete((res, e) -> {
							if (e != null) {
								RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
							} else {
								if (res) {
									// 递归调用更新有效期,永不过期
									RedissonLock.this.renewExpiration();
								}
							}
						});
					}
				}
			}
		}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);// 10S
		ee.setTimeout(task);
	}
}

更新有效期

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
	return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, 
	// 判断当前线程的锁是否是当前线程
	"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
		// 更新有效期
		redis.call('pexpire', KEYS[1], ARGV[1]); 
		return 1; 
		end; 
		return 0;", 
		Collections.singletonList(this.getName()), this.internalLockLeaseTime, this.getLockName(threadId));
}

3、调用lua脚本

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
	// 锁释放时间
	this.internalLockLeaseTime = unit.toMillis(leaseTime);
	return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, 
		// 判断锁成功
		"if (redis.call('exists', KEYS[1]) == 0) then
			redis.call('hincrby', KEYS[1], ARGV[2], 1); // 如果不存在,记录锁标识,次数+1
			redis.call('pexpire', KEYS[1], ARGV[1]); // 设置锁有效期
			return nil; // 相当于Java的null
		end; 
		if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
			redis.call('hincrby', KEYS[1], ARGV[2], 1); // 如果存在,判断锁标识是否是自己的,次数+1
			redis.call('pexpire', KEYS[1], ARGV[1]); // 设置锁有效期
			return nil; 
		end; 
		// 判断锁失败,pttl:指定锁剩余有效期,单位毫秒,KEYS[1]:锁的名称
		return redis.call('pttl', KEYS[1]);", 
			Collections.singletonList(this.getName()), this.internalLockLeaseTime, this.getLockName(threadId));
}

六、释放锁unlock源码

1、取消更新任务

public RFuture<Void> unlockAsync(long threadId) {
	RPromise<Void> result = new RedissonPromise();
	RFuture<Boolean> future = this.unlockInnerAsync(threadId);
	future.onComplete((opStatus, e) -> {
		// 取消更新任务
		this.cancelExpirationRenewal(threadId);
		if (e != null) {
			result.tryFailure(e);
		} else if (opStatus == null) {
			IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + this.id + " thread-id: " + threadId);
			result.tryFailure(cause);
		} else {
			result.trySuccess((Object)null);
		}
	});
	return result;
}

2、删除定时任务

void cancelExpirationRenewal(Long threadId) {
	// 从map中取出当前锁的定时任务entry
	RedissonLock.ExpirationEntry task = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
	if (task != null) {
		if (threadId != null) {
			task.removeThreadId(threadId);
		}
		// 删除定时任务
		if (threadId == null || task.hasNoThreads()) {
			Timeout timeout = task.getTimeout();
			if (timeout != null) {
				timeout.cancel();
			}

			EXPIRATION_RENEWAL_MAP.remove(this.getEntryName());
		}
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/114566.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微软发布 Entity Framework EF Core 8 或 EF8

Entity Framework 现已被广泛使用&#xff0c;微软首席软件工程经理 Arthur Vickers 日前在一个在线社区会议上的发言。 Entity Framework Core 8.0&#xff08;也称为 EF Core 8 或 EF8&#xff09;的未来规划。EF Core 8 是 EF Core 7 之后的下一个版本&#xff0c;这将是一个…

链表的实现:无头单向非循环链表的实现

笔者在上篇博客书写了一个名为&#xff1a;链式存储之&#xff1a;链表的引出及其简介原文链接为&#xff1a;https://blog.csdn.net/weixin_64308540/article/details/128374876?spm1001.2014.3001.5501对于此篇博客&#xff0c;在一写出来&#xff0c;便引起了巨大反响&…

Golang 【basic_leaming】函数详解

阅读目录1、函数定义2、函数的调用3、函数参数4、函数返回值5、函数变量作用域全局变量局部变量6、函数类型与变量定义函数类型函数类型变量7、高阶函数函数作为参数函数作为返回值8、匿名函数和闭包匿名函数闭包闭包进阶示例1闭包进阶示例2闭包进阶示例39、defer 语句defer 执…

Windows-试用phpthink发现原来可这样快速搭建mysql、redis等环境、xdebug

一、前言 最近在简单学习 php 国人框架 phpthink&#xff0c;不得不说牛&#xff0c;我在 github 上既然搜不到此项目… 但是发现搭建依赖环境不会&#xff0c;于是百度一下&#xff0c;几乎都是各种集成工具什么宝塔、小皮面板等等。有固然是方便&#xff0c;但为什么其它语言…

DAY5 Recommended system cold startup problem

推荐系统的冷启动问题 推荐系统冷启动概念 ⽤户冷启动&#xff1a;如何为新⽤户做个性化推荐物品冷启动&#xff1a;如何将新物品推荐给⽤户&#xff08;协同过滤&#xff09;系统冷启动&#xff1a;⽤户冷启动物品冷启动本质是推荐系统依赖历史数据&#xff0c;没有历史数据⽆…

html+圣诞树

圣诞节 基督教纪念耶稣诞生的重要节日。亦称耶稣圣诞节、主降生节&#xff0c;天主教亦称耶稣圣诞瞻礼。耶稣诞生的日期&#xff0c;《圣经》并无记载。公元336年罗马教会开始在12月25日过此节。12月25日原是罗马帝国规定的太阳神诞辰。有人认为选择这天庆祝圣诞&#xff0c;是…

【学习打卡07】 可解释机器学习笔记之Shape+Lime代码实战

可解释机器学习笔记之ShapeLime代码实战 文章目录可解释机器学习笔记之ShapeLime代码实战基于Shapley值的可解释性分析使用Pytorch对MNIST分类可解释性分析使用shap的Deep Explainer进行可视化使用Pytorch对预训练ImageNet图像分类可解释性分析指定单个预测类别指定多个预测类别…

Elasticsearch 核心技术(一):Elasticsearch 安装、配置、运行(Windows 版)

❤️ 个人主页&#xff1a;水滴技术 &#x1f680; 支持水滴&#xff1a;点赞&#x1f44d; 收藏⭐ 留言&#x1f4ac; &#x1f338; 订阅专栏&#xff1a;大数据核心技术从入门到精通 文章目录一、Elasticsearch 版本的选择二、下载 **Elasticsearch**三、安装 Elasticsear…

Springboot+Netty实现基于天翼物联网平台CTWing(AIOT)终端TCP协议(透传模式)-云服务端(IOT平台)

之前有文章用java实现了设备端和应用订阅端&#xff0c;那么我根据AIOT的协议也可以实现一个demo物联网平台端&#xff0c;这种简易的平台是实现自己搭建物联网平台的基础。 直接用代码 新建Springboot的maven项目&#xff0c;pom.xml文件导入依赖包&#xff08;用到了swagge…

UDP协议在Windows上使用示例

UDP(User Datagram Protocol&#xff0c;用户数据报协议)是无连接的&#xff0c;因此在两个进程通信前没有握手过程。UDP协议提供一种不可靠数据传送服务&#xff0c;也就是说&#xff0c;当进程将一个报文发送进UDP套接字时&#xff0c;UDP协议并不保证该报文将到达接收进程。…

过孔基础常识

过孔&#xff0c;一个绝大多数硬件工程师都听说过&#xff0c;但又并非真正了解的名词。了解的都知道&#xff0c;其在PCB板中其着至关重要的的作用。没有过孔的存在&#xff0c;很难画出一块完美的PCB板。所以呢&#xff0c;小编今日就带大家了解了解什么是过孔。 什么是过孔…

FCN代码及效果展示

1. 代码获取 代码地址: https://github.com/Le0v1n/ml_code/tree/main/Segmentation/FCN 2. 从头开始训练 2.1 测试平台 GPU&#xff1a;NVIDIA RTX 3070CPU: Intel I5-10400FRAM: 16GBOS: Windows 11Dataset: VOC2012Class num: 21(201)Batch size: 4Learning Rate: 0.1Ep…

嘉兴经开区第四届创新创业大赛总决赛成功举办

12月21日至12月22日&#xff0c;嘉兴经济技术开发区第四届创新创业大赛总决赛成功举办&#xff0c;经过激烈角逐最后共有10家企业分别获得大赛初创组和成长组的一二三等奖。 总决赛现场 嘉兴经开区第四届中国创新创业大赛于6月正式启动&#xff0c;陆续在嘉兴、成都、北京、西…

【详细学习SpringBoot源码之内嵌Tomcat启动原理分析编译部署Tomcat源码过程解析-9】

一.知识回顾 【0.SpringBoot专栏的相关文章都在这里哟&#xff0c;后续更多的文章内容可以点击查看】 【1.SpringBoot初识之Spring注解发展流程以及常用的Spring和SpringBoot注解】 【2.SpringBoot自动装配之SPI机制&SPI案例实操学习&SPI机制核心源码学习】 【3.详细学…

12-RabbitMq概述与工作模式深度剖析

MQ概述 MQ全称 Message Queue&#xff08;消息队列&#xff09;&#xff0c;是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。 MQ 的优势 应用解耦&#xff1a;提高系统容错性和可维护性 异步提速&#xff1a;提升用户体验和系统吞吐量 削峰填谷&#xff1…

unity中使用代码接绘制三维模型

一 模型的构成 在三维世界中&#xff0c;绘制一个模型并不是什么很复杂的问题。只要知道了基本原理一切需求便迎刃而解。 如下图所示&#xff0c;任何模型都是由点线面构成的&#xff0c;而面的最小单位是三角形。 任何一个多边形的面&#xff0c;都是由多个三角形构成的。比…

Web前端105天-day64-HTML5_CORE

HTML5CORE04 目录 前言 一、复习 二、WebSocket 三、服务器搭建 四、聊天室 五、defineProperty 5.1.初识defineProperty 5.2.配置多个属性 5.3.可配置 5.4.赋值监听 5.5.练习 5.6.计算属性 总结 前言 HTML5CORE04学习开始 一、复习 SVG: 利用HTML的 DOM 来绘制图…

PCB贴片机如何送料?

1.常见的贴片机供料器四种形式 http://www.sz-bjzn.com/1547.html 2.模块化设计SMT贴片机送料器的操作方法 3.淘宝 https://item.taobao.com/item.htm?spma230r.1.14.98.33e41823OZ1zzn&id579043582781&ns1&abbucket20#detail 不错&#xff1a;https://item.tao…

distinct与group by 去重

distinct与group by 去重distinct 特点&#xff1a;group by 特点&#xff1a;总结&#xff1a;mysql中常用去重复数据的方法是使用 distinct 或者group by &#xff0c;以上2种均能实现&#xff0c;但也有不同的地方。distinct 特点&#xff1a; 1、distinct 只能放在查询字段…

重新更新anaconda

更新anaconda问题阐述问题分析打开Anaconda Nvaigator打开文件所在位置复制文件所在路径找到此电脑或者打开设置找到高级系统设置环境变量添加环境变量打开scripts文件修改成功再一次启动感谢观看今天手贱,不小心删掉的anaconda,我想一不做二不休,直接重新重装了,就找到了anaco…