黑马点评笔记 分布式锁

文章目录

    • 分布式锁
      • 基本原理和实现方式对比
      • Redis分布式锁的实现核心思路
      • 实现分布式锁版本一
      • Redis分布式锁误删情况说明
      • 解决Redis分布式锁误删问题
      • 分布式锁的原子性问题
      • 分布式锁-Redission
      • 分布式锁-redission可重入锁原理
      • 分布式锁-redission锁重试和WatchDog机制
      • 分布式锁-redission锁的MutiLock原理

分布式锁

基本原理和实现方式对比

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

分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路

在这里插入图片描述

那么分布式锁他应该满足的条件呢?

可见性:多个线程都能看到相同的结果,注意:这个地方说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化的意思

互斥:互斥是分布式锁的最基本的条件,使得程序串行执行

高可用:程序不易崩溃,时时刻刻都保证较高的可用性

高性能:由于加锁本身就让性能降低,所有对于分布式锁本身需要他就较高的加锁性能和释放锁性能

安全性:安全也是程序中必不可少的一环

常见的分布式锁有三种

  • Mysql:mysql本身就带有锁机制,但是由于mysql性能本身一般,所以采用分布式锁的情况下,其实使用mysql作为分布式锁比较少见

  • Redis:redis作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都使用redis或者zookeeper作为分布式锁,利用setnx这个方法,如果插入key成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁

  • Zookeeper:zookeeper也是企业级开发中较好的一个实现分布式锁的方案

Redis分布式锁的实现核心思路

实现分布式锁时需要实现的两个基本方法:

  • 获取锁:

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

    • 手动释放
    • 超时释放:获取锁时添加一个超时时间

核心思路:

我们利用redis 的setNx 方法,当有多个线程进入时,我们就利用该方法,第一个线程进入时,redis 中就有这个key 了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可

实现分布式锁版本一

  • 加锁逻辑

锁的基本接口

在这里插入图片描述

SimpleRedisLock

利用setnx方法进行加锁,同时增加过期时间,防止死锁,此方法可以保证加锁和增加过期时间具有原子性
我们的方法,是把存在线程中的用户的id作为redis中的中的键,这样我们就可以作为为每一个用户设置单独的锁,而且我们也会为每个锁设置单的过期时间从而防止死锁,具体代码,可以看下面:

private static final String KEY_PREFIX="lock:"
@Override
public boolean tryLock(long timeoutSec) {// 获取线程标示String threadId = Thread.currentThread().getId()// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);
}

Redis分布式锁误删情况说明

逻辑说明:

持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明

解决方案:解决方案就是在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果属于自己,则不进行锁的删除,假设还是上边的情况,线程1卡顿,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1反应过来,然后删除锁,但是线程1,一看当前这把锁不是属于自己,于是不进行删除锁逻辑,当线程2走到删除锁逻辑时,如果没有卡过自动释放锁的时间点,则判断当前这把锁是属于自己的,于是删除这把锁。

在这里插入图片描述

解决Redis分布式锁误删问题

需求:修改之前的分布式锁实现,满足:在获取锁时存入线程标示(可以用UUID表示)
在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致

  • 如果一致则释放锁
  • 如果不一致则不释放锁

核心逻辑:在存入锁时,放入自己线程的标识,在删除锁时,判断当前这把锁的标识是不是自己存入的,如果是,则进行删除,如果不是,则不进行删除。

在这里插入图片描述
具体代码如下:加锁

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {// 获取线程标示String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);
}

释放锁

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

分布式锁的原子性问题

更为极端的误删逻辑说明:

线程1现在持有锁之后,在执行业务逻辑过程中,他正准备删除锁,而且已经走到了条件判断的过程中,比如他已经拿到了当前这把锁确实是属于他自己的,正准备删除锁,但是此时他的锁到期了,那么此时线程2进来,但是线程1他会接着往后执行,当他卡顿结束后,他直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题,之所以有这个问题,是因为线程1的拿锁,比锁,删锁,实际上并不是原子性的,我们要防止刚才的情况发生,

在这里插入图片描述
这个问题可以使用lua脚本实现,但是在java中我们一般会用redission这个第三方库。

分布式锁-Redission

引入依赖:

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

配置Redisson客户端:

@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient(){// 配置Config config = new Config();config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassword("123321");// 创建RedissonClient对象return Redisson.create(config);}
}

使用Redission的分布式锁

@Resource
private RedissionClient redissonClient;@Test
void testRedisson() throws Exception{//获取锁(可重入),指定锁的名称RLock lock = redissonClient.getLock("anyLock");//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);//判断获取锁成功if(isLock){try{System.out.println("执行业务");          }finally{//释放锁lock.unlock();}}}

业务代码更改
在 VoucherOrderServiceImpl

注入RedissonClient

@Resource
private RedissonClient redissonClient;@Override
public Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀尚未开始!");}// 3.判断秒杀是否已经结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀已经结束!");}// 4.判断库存是否充足if (voucher.getStock() < 1) {// 库存不足return Result.fail("库存不足!");}Long userId = UserHolder.getUser().getId();//创建锁对象 这个代码不用了,因为我们现在要使用分布式锁//SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);RLock lock = redissonClient.getLock("lock:order:" + userId);//获取锁对象boolean isLock = lock.tryLock();//加锁失败if (!isLock) {return Result.fail("不允许重复下单");}try {//获取代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {//释放锁lock.unlock();}}

分布式锁-redission可重入锁原理

在Lock锁中,他是借助于底层的一个voaltile的一个state变量来记录重入的状态的,比如当前没有人持有这把锁,那么state=0,假如有人持有这把锁,那么state=1,如果持有这把锁的人再次持有这把锁,那么state就会+1 ,如果是对于synchronized而言,他在c语言代码中会有一个count,原理和state类似,也是重入一次就加一,释放一次就-1 ,直到减少成0 时,表示当前这把锁没有被人持有。

在redission中,我们的也支持支持可重入锁

在分布式锁中,他采用hash结构用来存储锁,其中大key表示表示这把锁是否存在,用小key表示当前这把锁被哪个线程持有,所以接下来我们一起分析一下当前的这个lua表达式

这个地方一共有3个参数

KEYS[1] : 锁名称

ARGV[1]: 锁失效时间

ARGV[2]: id + “:” + threadId; 锁的小key

exists: 判断数据是否存在 name:是lock是否存在,如果==0,就表示当前这把锁不存在

redis.call(‘hset’, KEYS[1], ARGV[2], 1);此时他就开始往redis里边去写数据 ,写成一个hash结构

Lock{

​ id + “:” + threadId : 1

}

如果当前这把锁存在,则第一个条件不满足,再判断

redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1

此时需要通过大key+小key判断当前这把锁是否是属于自己的,如果是自己的,则进行

redis.call(‘hincrby’, KEYS[1], ARGV[2], 1)

将当前这个锁的value进行+1 ,redis.call(‘pexpire’, KEYS[1], ARGV[1]); 然后再对其设置过期时间,如果以上两个条件都不满足,则表示当前这把锁抢锁失败,最后返回pttl,即为当前这把锁的失效时间

分布式锁-redission锁重试和WatchDog机制

抢锁过程中,获得当前线程,通过tryAcquire进行抢锁,该抢锁逻辑和之前逻辑相同

1、先判断当前这把锁是否存在,如果不存在,插入一把锁,返回null

2、判断当前这把锁是否是属于当前线程,如果是,则返回null

所以如果返回是null,则代表着当前已经抢锁完毕,或者可重入完毕,但是如果以上两个条件都不满足,则进入到第三个条件,返回的是锁的失效时间,同学们可以自行往下翻一点点,你能发现有个while( true) 再次进行tryAcquire进行抢锁

long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {return;
}

接下来会有一个条件分支,因为lock方法有重载方法,一个是带参数,一个是不带参数,如果带带参数传入的值是-1,如果传入参数,则leaseTime是他本身,所以如果传入了参数,此时leaseTime != -1 则会进去抢锁,抢锁的逻辑就是之前说的那三个逻辑

if (leaseTime != -1) {return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}

如果是没有传入时间,则此时也会进行抢锁, 而且抢锁时间是默认看门狗时间 commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()

ttlRemainingFuture.onComplete((ttlRemaining, e) 这句话相当于对以上抢锁进行了监听,也就是说当上边抢锁完毕后,此方法会被调用,具体调用的逻辑就是去后台开启一个线程,进行续约逻辑,也就是看门狗线程

RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {if (e != null) {return;}// lock acquiredif (ttlRemaining == null) {scheduleExpirationRenewal(threadId);}
});
return ttlRemainingFuture;

此逻辑就是续约逻辑,注意看commandExecutor.getConnectionManager().newTimeout() 此方法

Method( new TimerTask() {},参数2 ,参数3 )

指的是:通过参数2,参数3 去描述什么时候去做参数1的事情,现在的情况是:10s之后去做参数一的事情

因为锁的失效时间是30s,当10s之后,此时这个timeTask 就触发了,他就去进行续约,把当前这把锁续约成30s,如果操作成功,那么此时就会递归调用自己,再重新设置一个timeTask(),于是再过10s后又再设置一个timerTask,完成不停的续约

那么大家可以想一想,假设我们的线程出现了宕机他还会续约吗?当然不会,因为没有人再去调用renewExpiration这个方法,所以等到时间之后自然就释放了。

private void renewExpiration() {ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;}Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ent == null) {return;}Long threadId = ent.getFirstThreadId();if (threadId == null) {return;}RFuture<Boolean> future = renewExpirationAsync(threadId);future.onComplete((res, e) -> {if (e != null) {log.error("Can't update lock " + getName() + " expiration", e);return;}if (res) {// reschedule itselfrenewExpiration();}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);
}

分布式锁-redission锁的MutiLock原理

为了提高redis的可用性,我们会搭建集群或者主从,现在以主从为例

此时我们去写命令,写在主机上, 主机会将数据同步给从机,但是假设在主机还没有来得及把数据写入到从机去的时候,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,此时锁信息就已经丢掉了。

在这里插入图片描述

为了解决这个问题,redission提出来了MutiLock锁,每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么他去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性。

在这里插入图片描述

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

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

相关文章

01、Tensorflow实现二元手写数字识别

01、Tensorflow实现二元手写数字识别&#xff08;二分类问题&#xff09; 开始学习机器学习啦&#xff0c;已经把吴恩达的课全部刷完了&#xff0c;现在开始熟悉一下复现代码。对这个手写数字实部比较感兴趣&#xff0c;作为入门的素材非常合适。 基于Tensorflow 2.10.0 1、…

pandas获取年月第一天、最后一天,加一秒、加一天、午夜时间

Timestamp对象 # ts = pandas.Timestamp(year=2023, month=10, day=15, # hour=15, minute=5, second=50, tz="Asia/Shanghai") ts = pandas.Timestamp("2023-10-15 15:05:50", tz="Asia/Shanghai") # 2023-10-15 15:05…

数据丢失预防措施包括什么

数据丢失预防措施是保护企业或个人重要数据的重要手段。以下是一些有效的预防措施&#xff1a; 可以通过域之盾软件来实现数据防丢失&#xff0c;具体的功能包括&#xff1a; https://www.yuzhidun.cn/https://www.yuzhidun.cn/ 1、备份数据 定期备份所有重要数据&#xff0…

unittest指南——不拼花哨,只拼实用

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

centos7 docker开启认证的远程端口2376配置

docker开启2375会存在安全漏洞 暴露了2375端口的Docker主机。因为没有任何加密和认证过程&#xff0c;知道了主机IP以后&#xff0c;&#xff0c;任何人都可以管理这台主机上的容器和镜像&#xff0c;以前贪图方便&#xff0c;只开启了没有认证的docker2375端口&#xff0c;后…

代码随想录算法训练营第五十三天|1143.最长公共子序列 1035.不相交的线 53. 最大子序和

文档讲解&#xff1a;代码随想录 视频讲解&#xff1a;代码随想录B站账号 状态&#xff1a;看了视频题解和文章解析后做出来了 1143.最长公共子序列 class Solution:def longestCommonSubsequence(self, text1: str, text2: str) -> int:dp [[0] * (len(text2) 1) for _ i…

机器学习入门

简介 https://huggingface.co/是一个AI社区&#xff0c;类似于github的地位。它开源了许多机器学习需要的基础组件如&#xff1a;Transformers, Tokenizers等。 许多公司也在不断地往上面提交新的模型和数据集&#xff0c;利用它你可以获取以下内容&#xff1a; Datasets : 数…

hikariCP 数据库连接池配置

springBoot 项目默认自动使用 HikariCP &#xff0c;HikariCP 的性能比 alibaba/druid快。 一、背景 系统中多少个线程在进行与数据库有关的工作&#xff1f;其中&#xff0c;而多少个线程正在执行 SQL 语句&#xff1f;这可以让我们评估数据库是不是系统瓶颈。 多少个线程在…

基于法医调查算法优化概率神经网络PNN的分类预测 - 附代码

基于法医调查算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于法医调查算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于法医调查优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神…

【学生成绩管理】数据库示例数据(MySQL代码)

【学生成绩管理】数据库示例数据&#xff08;MySQL代码&#xff09; 目录 【学生成绩管理】数据库示例数据&#xff08;MySQL代码&#xff09;一、创建数据库二、创建dept&#xff08;学院&#xff09;表1、创建表结构2、添加示例数据3、查看表中数据 三、创建stu&#xff08;学…

35.逻辑运算符

目录 一.什么是逻辑运算符 二.C语言中的逻辑运算符 三.逻辑表达式 三.视频教程 一.什么是逻辑运算符 同时对俩个或者俩个以上的表达式进行判断的运算符叫做逻辑运算符。 举例&#xff1a;比如去网吧上网&#xff0c;只有年满十八周岁并且带身份证才可以上网。在C语言中如果…

为什么 Flink 抛弃了 Scala

曾经红遍一时的Scala 想当初Spark横空出世之后&#xff0c;Scala简直就是语言界的一颗璀璨新星&#xff0c;惹得大家纷纷侧目&#xff0c;连Kafka这类技术框架也选择用Scala语言进行开发重构。 可如今&#xff0c;Flink竟然公开宣布弃用Scala 在Flink1.18的官方文档里&#x…

国家开放大学的学子们 练习题 走起!

试卷代号&#xff1a;1356 高级英语听说(2) 参考 试题 Section One (20 points, 2 points each) Directions: Listen to the conversation and fill in the blanks with the words you hear. Write the words on the Answer Sheet The conversation will be read TWICE. M…

windows11上安装WSL

Windows电脑上要配置linux&#xff08;这里指ubuntu&#xff09;开发环境&#xff0c;主要有三种方式&#xff1a; 1&#xff09;在windows上装个虚拟机&#xff08;比如vmware&#xff09;。缺点是vmware加载ubuntu后系统会变慢很多&#xff0c;而且需要通过samba来实现window…

使用Java连接Hbase

我在网上试 了很多代码&#xff0c;但是大部分都不能实现&#xff0c;Java连接Hbase&#xff0c;一直报一个错 java.util.concurrent.ExecutionException: org.apache.zookeeper.KeeperException$NoNodeException: KeeperErrorCode NoNode for /hbase/hbaseid一直也不清楚为什…

计算机组成原理。3-408

1.动态存储和静态存储 2.双端口RAM 注意&#xff1a;cpu通过地址线和数据线读写数据时&#xff0c;不能同时写&#xff0c;但可以同时读&#xff0c;也不能一边读一边写。 3.多体并行存储器 分为高位存储和低位存储 小结 4.磁盘存储器的组成 5.磁盘的性能指标 磁盘读写寻道…

如何对网站进行渗透测试

信息搜集 信息搜集拿到域名后获取真实IP&#xff0c;如果存在CDN想办法绕过端口扫描&#xff0c;针对开放的端口在获取客户同意的前提下进行爆破查找网站子域名&#xff0c;后台目录判断网站的CMS 可以使用 Wappalyzer插件 whatcms 是一个可以用来确定特定网站正在使用的什么…

Vue中Slot的使用指南

目录 前言 什么是slot&#xff1f; 单个slot的使用 具名slot的使用 作用域插槽 总结 前言 在Vue中&#xff0c;slot是一种非常强大和灵活的功能&#xff0c;它允许你在组件模板中预留出一个或多个"插槽"&#xff0c;然后在使用这个组件的时候动态地填充内容。这…

TSINGSEE青犀智能分析网关道路积水识别AI算法方案

在各处的街道、路口等区域&#xff0c;及时发现道路积水问题&#xff0c;可以大大减少城市管理部门压力&#xff0c;及时处理&#xff0c;减少交通事故与人员摔倒事故。通过道路积水AI算法&#xff0c;能有效提高城市管理部门效率&#xff0c;优化城市管理方式。 那么&#xff…

【Web】PhpBypassTrick相关例题wp

目录 ①[NSSCTF 2022 Spring Recruit]babyphp ②[鹤城杯 2021]Middle magic ③[WUSTCTF 2020]朴实无华 ④[SWPUCTF 2022 新生赛]funny_php 明天中期考&#xff0c;先整理些小知识点冷静一下 ①[NSSCTF 2022 Spring Recruit]babyphp payload: a[]1&b1[]1&b2[]2&…