Redis(十七)分布式锁

文章目录

  • 面试题
  • 分布式锁
    • 锁的种类
    • 分布式锁需要具备的条件和刚需
    • 分布式锁
  • 案例
    • nginx分布式微服务部署,单机锁问题
    • 分布式锁注意事项
    • lock/unlock+lua脚本自研版的redis分布式锁搞定
      • lua脚本
    • 可重入锁
      • 可重入锁种类
      • 可重入锁hset实现,对比setnx(重要)
      • 分布式锁需要具备的条件和刚需
      • lua脚本
    • 工厂模式分布式锁
    • 自动续期
      • CAP再提起
    • 总结
      • 引入分布式锁

面试题

基于Redis的什么用法?

  1. 数据共享,分布式Session
  2. 分布式锁
  3. 全局ID
  4. 计算器、点赞
  5. 位统计
  6. 购物车
  7. 轻量级消息队列
  8. 抽奖
  9. 回来的题目
  10. 点赞、签到、打卡
  11. 差集交集并集,用户关注、可能认识的人,推荐模型
  12. 热点新闻、热搜排行榜
  • Redis 做分布式锁的时候有需要注意的问题?
  • 你们公司自己实现的分布式锁是否用的setnx命令实现?

不可以

  • 这个是最合适的吗?你如何考虑分布式锁的可重入问题?
  • 如果是 Redis 是单点部署的,会带来什么问题?那你准备怎么解决单点问题呢?
  • Redis集群模式下,比如主从模式,CAP方面有没有什么问题呢?

CAP:

C:一致性:在分布式系统中的任意一个节点都会查询到相同的信息(拿到的都是最新的)
A:可用性:服务一直可用,而且是正常响应时间,好的可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。(只要我访问你就给我返回,如果要满足分布式(P),机器之间网络断掉的话,直接和C冲突)
P:分区容错性:当分布式系统中一部分节点崩溃的时候,当前系统仍旧能够正常对外提供服务(多台机器,分布式,不满足P就是单机么)

Redis集群:是AP,Redis单机是C,一致性
区别Zookeeper集群:是CP,全部节点收到后返回ack

  • 那你简单的介绍-下 Redlock吧?你简历上写redisson,你谈谈
  • Redis分布式锁如何续期?看门狗知道吗?

分布式锁

JUC中AQS锁的规范落地参考+可重入锁考虑+Lua脚本+Redis命令实现分布式锁

锁的种类

  1. 单机版同一个M虚拟机内,synchronized或者Lock接口
  2. 分布式多个不同M虚拟机,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。

分布式锁需要具备的条件和刚需

  1. 独占性:OnlyOne,任何时刻只能有且仅有一个线程持有
  2. 高可用:若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况;高并发请求下,依旧高性能
  3. 防死锁:杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
  4. 不乱抢:防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放,自己约的锁含着泪也要自己解
  5. 重入性:同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。

分布式锁

在这里插入图片描述

案例

nginx分布式微服务部署,单机锁问题

分布式部署后,单机锁还是出现超卖现象,需要分布式锁

分布式锁注意事项

  1. 重试:递归重试,容易导致stackoverflowerror
  2. 宕机-防止死锁:部署了微服务的Java程序机器挂了,代码层面根本没有走到finally这块
  3. 防止误删key:stringRedisTemplate.delete(key);只能自己删除自己的锁,不可以删除别人的,需要添加判断
  4. Lua保证原子性:存在问题就是最后的判断+del不是一行原子命令操作,需要用lua脚本进行修改
  5. 可重入锁+设计模式:不满足可重入性

lock/unlock+lua脚本自研版的redis分布式锁搞定

lua脚本

https://redis.io/docs/reference/patterns/distributed-locks/

在这里插入图片描述
使用示例
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可重入锁

可重入锁(递归锁):可以再次进入的同步锁
进入:进入同步域(即同步代码块/方法或显式锁定的代码)
一句话:一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入

可重入锁种类

  1. 隐式锁(即synchronized关键字使用的锁)默认是可重入锁
//同步块
public class ReEntryLockDemo
{public static void main(String[] args){final Object objectLockA = new Object();new Thread(() -> {synchronized (objectLockA){System.out.println("-----外层调用");synchronized (objectLockA){System.out.println("-----中层调用");synchronized (objectLockA){System.out.println("-----内层调用");}}}},"a").start();}
}
  1. Synchronized的重入的实现机理
//同步方法
public class ReEntryLockDemo
{public synchronized void m1(){System.out.println("-----m1");m2();}public synchronized void m2(){System.out.println("-----m2");m3();}public synchronized void m3(){System.out.println("-----m3");}public static void main(String[] args){ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();reEntryLockDemo.m1();}
}
  1. 显式锁(即Lock)也有ReentrantLock这样的可重入锁。
//显式锁
public class ReEntryLockDemo
{static Lock lock = new ReentrantLock();public static void main(String[] args){new Thread(() -> {lock.lock();try{System.out.println("----外层调用lock");lock.lock();try{System.out.println("----内层调用lock");}finally {// 这里故意注释,实现加锁次数和释放次数不一样// 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。lock.unlock(); // 正常情况,加锁几次就要解锁几次}}finally {lock.unlock();}},"a").start();new Thread(() -> {lock.lock();try{System.out.println("b thread----外层调用lock");}finally {lock.unlock();}},"b").start();}
}

切记,一般而言,你lock了几次就要unlock几次

public class ReEntryLockDemo
{Lock lock = new ReentrantLock();public void entry(){new Thread(() -> {lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t"+"外层调用lock");lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t"+"内层调用lock");}finally {//这里不解锁,已经加了两次锁//lock.unlock();}}finally {lock.unlock();}},"t1").start();//暂停毫秒try { TimeUnit.MILLISECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }new Thread(() -> {lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t"+"外层调用lock");}finally {lock.unlock();}},"t2").start();}public static void main(String[] args){ReEntryLockDemo demo = new ReEntryLockDemo();demo.entry();//在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的}
}

可重入锁hset实现,对比setnx(重要)

可重入锁模拟redis
hset redis锁名字(zzyyRedisLock) 某个请求线程的UUID+ThreadID 加锁的次数
在这里插入图片描述
setnx:只能解决有无的问题,但是不完美
hset:不但解决有无,还解决可重入问题

分布式锁需要具备的条件和刚需

  1. 独占性
  2. 高可用
  3. 防死锁
  4. 不乱抢

lua脚本

|-先判断redis分布式锁这个key是否存在EXISTS key|-key不存在:返回零说明不存在,hset新建当前线程属于自己的锁BY UUID:ThreadlD |-key存在:返回壹说明已经有锁,需进一步判断是不是当前线程自己的:HEXISTS key uuid:ThreadlD|-返回0说明不是自己的|-返回非0说明是自己的:自增1表示重入
  1. 显示参数版本
if redis.call('exists','key') == 0 or redis.call('hexists','key','uuid:threadid') == 1 thenredis.call('hincrby','key','uuid:threadid',1)redis.call('expire','key',30)return 1
elsereturn 0
end
  1. 参数替换版本
名称替换位置示例值
keyKEYS[1]testRedisLock
valueARGV[1]2f586ae740a94736894ab9d51880ed9d:1
过期时间值ARGV[2]30 秒
if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 
elsereturn 0
end

工厂模式分布式锁

封装锁,使用工厂模式

@Component
public class RedisDistributedLock implements Lock
{private StringRedisTemplate stringRedisTemplate;private String lockName;//KEYS[1]private String uuidValue;//ARGV[1]private long   expireTime;//ARGV[2]//注意这里public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName, String uuid){this.stringRedisTemplate = stringRedisTemplate;this.lockName = lockName;this.uuidValue = uuid+":"+Thread.currentThread().getId();this.expireTime = 30L;}@Overridepublic void lock(){tryLock();}@Overridepublic boolean tryLock(){try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}return false;}@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException{if(time == -1L){String script ="if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then    " +"redis.call('hincrby',KEYS[1],ARGV[1],1)    " +"redis.call('expire',KEYS[1],ARGV[2])    " +"return 1  " +"else   " +"return 0 " +"end";System.out.println("lockName:"+lockName+"\t"+"uuidValue:"+uuidValue);while(!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName), uuidValue,String.valueOf(expireTime))){//暂停60毫秒try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); }}//新建一个后台扫描程序,来坚持key目前的ttl,是否到我们规定的1/2 1/3来实现续期renewExpire();return true;}return false;}@Overridepublic void unlock(){System.out.println("unlock(): lockName:"+lockName+"\t"+"uuidValue:"+uuidValue);String script ="if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then    " +"return nil  " +"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then    " +"return redis.call('del',KEYS[1])  " +"else    " +"return 0 " +"end";// nil = false 1 = true 0 = falseLong flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));if(null == flag){throw new RuntimeException("this lock doesn't exists,o(╥﹏╥)o");}}private void renewExpire(){String script ="if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then     " +"return redis.call('expire',KEYS[1],ARGV[2]) " +"else     " +"return 0 " +"end";new Timer().schedule(new TimerTask(){@Overridepublic void run(){if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))){renewExpire();}}},(this.expireTime * 1000)/3);}//暂时用不到@Overridepublic void lockInterruptibly() throws InterruptedException{}@Overridepublic Condition newCondition(){return null;}
}

分布式锁工厂

@Component
public class DistributedLockFactory
{@Autowiredprivate StringRedisTemplate stringRedisTemplate;private String lockName;private String uuid;public DistributedLockFactory(){//uuid会变化,所以在类创建的时候就uuid放入内部,否则影响可重入性this.uuid = IdUtil.simpleUUID();}public Lock getDistributedLock(String lockType){if(lockType == null) {return null;}if(lockType.equalsIgnoreCase("REDIS")){this.lockName = "zzyyRedisLock";return new RedisDistributedLock(stringRedisTemplate,lockName,uuid);}else if(lockType.equalsIgnoreCase("ZOOKEEPER")){this.lockName = "zzyyZookeeperLockNode";//TODO zookeeper版本的分布式锁return null;}else if(lockType.equalsIgnoreCase("MYSQL")){//TODO MYSQL版本的分布式锁return null;}return null;}
}

使用工厂锁

public String sale7(){String retMessage = "";Lock redisLock = distributedLockFactory.getDistributedLock("redis");redisLock.lock();try{//1 查询库存信息String result = stringRedisTemplate.opsForValue().get("inventory001");//2 判断库存是否足够Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);//3 扣减库存,每次减少一个if(inventoryNumber > 0){stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;System.out.println(retMessage+"\t"+"服务端口号"+port);testReEntry();}else{retMessage = "商品卖完了,o(╥﹏╥)o";}}finally {redisLock.unlock();}return retMessage+"\t"+"服务端口号"+port;}

自动续期

CAP再提起

CAP即:
1、Consistency(一致性):对于客户端的每次读操作,要么读到的是最新的数据,要么读取失败。换句话说,一致性是站在分布式系统的角度,对访问本系统的客户端的一种承诺:要么我给您返回一个错误,要么我给你返回绝对一致的最新数据,不难看出,其强调的是数据正确。
2、Availability(可用性):任何客户端的请求都能得到响应数据,不会出现响应错误。换句话说,可用性是站在分布式系统的角度,对访问本系统的客户的另一种承诺:我一定会给您返回数据,不会给你返回错误,但不保证数据最新,强调的是不出错。
3、Partition tolerance(分区容忍性):由于分布式系统通过网络进行通信,网络是不可靠的。当任意数量的消息丢失或延迟到达时,系统仍会继续提供服务,不会挂掉。换句话说,分区容忍性是站在分布式系统的角度,对访问本系统的客户端的再一种承诺:我会一直运行,不管我的内部出现何种数据同步问题,强调的是不挂掉。

Redis集群是AP
Zookeeper集群是CP
Eureka集群是AP
Nacos集群是AP

总结

nginx微服务单机锁出现问题:只能锁本服务

引入分布式锁

  1. 只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面fnallv释放锁
  2. 宕机了,部署了微服务代码层面根本没有走到fnally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
  3. redis的分布式锁key,增加过期时间此外,还必须要setnx+过期时间必须同一行
    1. 必须规定只能自己删除自己的锁,不能把别人的锁删除了unlock变为Lua脚本保证
    2. 锁重入,hset替代setnx+lock变为Lua脚本保证
    3. 自动续期

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

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

相关文章

ArrayList的扩容机制

ArrayList 的底层操作机制源码分析 ArrayList中维护一个Object类型的数组elementData transient Object[] elementData; //transient表示瞬间 短暂的&#xff0c;表示该属性不会被序列化当创建ArrayList对象时&#xff0c;如果使用的是无参构造器&#xff0c;则初始elementDa…

【QT】自定义控件的示例

自定义控件&#xff08;很重要&#xff09; 什么是自定义控件&#xff1f; 顾名思义就是创建一个窗口&#xff0c;放入多个控件&#xff0c;拼接起来&#xff0c;一起使用。 为什么需要它&#xff1f; 需求&#xff0c;假设有100个窗口&#xff0c;那如果有两个控件同时被使…

浅研究下selinux

一、 selinux SELinux的状态&#xff1a; enforcing&#xff1a;强制&#xff0c;每个受限的进程都必然受限 permissive&#xff1a;允许&#xff0c;每个受限的进程违规操作不会被禁止&#xff0c;但会被记录于审计日志 disabled&#xff1a;禁用 相关命令&#xff1a; ge…

【AI视野·今日Robot 机器人论文速览 第八十二期】Tue, 5 Mar 2024

AI视野今日CS.Robotics 机器人学论文速览 Tue, 5 Mar 2024 Totally 63 papers &#x1f449;上期速览✈更多精彩请移步主页 Interesting: &#x1f4da;双臂机器人拧瓶盖, (from 伯克利) website: https://toruowo.github.io/bimanual-twist &#x1f4da;水下抓取器, (from …

Docker前后端项目部署

目录 一、搭建项目部署的局域网 二、redis安装 三、MySQL安装 四、若依后端项目搭建 4.1 使用Dockerfile自定义镜像 五、若依前端项目搭建 一、搭建项目部署的局域网 搭建net-ry局域网&#xff0c;用于部署若依项目 docker network create net-ry --subnet172.68.0.0/1…

如何使用ArcGIS Pro进行坡度分析

坡度分析是地理信息系统中一种常见的空间分析方法&#xff0c;用于计算地表或地形的坡度&#xff0c;这里为大家介绍一下如何使用ArcGIS Pro进行坡度分析&#xff0c;希望能对你有所帮助。 数据来源 教程所使用的数据是从水经微图中下载的DEM数据&#xff0c;除了DEM数据&…

【项目】图书管理系统

目录 前言&#xff1a; 项目要求&#xff1a; 知识储备&#xff1a; 代码实现&#xff1a; Main&#xff1a; Books包&#xff1a; Book&#xff1a; BookList&#xff1a; Operate包&#xff1a; Operate: addOperate: deleteOperate: exitOperate: findOperate:…

chatgpt-3的文章生成器有哪些?可以批量生成文章的生成器

GPT-3&#xff08;Generative Pre-trained Transformer 3&#xff09;作为人工智能领域的一项重大突破&#xff0c;开启了新一代的文本生成技术。同时市面上也涌现出了一些GPT-3文章生成器&#xff0c;为用户提供了快速、高效地生成各种类型文章的工具。本文将介绍一些中国的GP…

深入了解雪花算法ID生成过程

小伙伴们好&#xff0c;欢迎关注&#xff0c;一起学习&#xff0c;无限进步 以下内容为学习过程中的笔记 集群高并发情况下如何保证分布式唯一全局 Id 生成&#xff1f; 问题 为什么需要分布式全局唯一 ID 以及分布式 ID 的业务需求 在复杂分布式系统设计中&#xff0c;往往需要…

设计MySQL数据表的几个注意点

最近合作搞项目&#xff0c;发现了很多问题。特别的&#xff0c;数据库层面上的问题更为致命。记录一下&#xff0c;希望后面看到博客的同学们注意。 注意&#xff1a;以下观点只用于一般情况下的单体、微服务&#xff0c;不保证适用所有场景。 一、ID问题 ID名称问题 如下图…

Android SDK2 (实操三个小目标)

书接上回&#xff1a;Android SDK 1&#xff08;概览&#xff09;-CSDN博客 今天讲讲三个实际练手内容&#xff0c;用的是瑞星微的sdk。 1 实操编译Android.bp 首先还是感叹下&#xff0c;现在的系统真的越搞越复杂&#xff0c;最早只有gcc&#xff0c;后面多了make&#xf…

【ESP32 IDF快速入门】点亮第一个LED灯与流水灯

文章目录 前言一、有哪些工作模式&#xff1f;1.1 GPIO的详细介绍1.2 GPIO的内部框图输入模式输出部分 二、GPIO操作函数2.1 GPIO 汇总2.2 GPIO操作函数gpio_config配置引脚reset 引脚函数设置引脚电平选中对应引脚设置引脚的方向 2.3 点亮第一个灯 三、流水灯总结 前言 ESP32…