Redis(十七)分布式锁

news2025/1/19 11:28:07

文章目录

  • 面试题
  • 分布式锁
    • 锁的种类
    • 分布式锁需要具备的条件和刚需
    • 分布式锁
  • 案例
    • 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 then
  redis.call('hincrby','key','uuid:threadid',1)
  redis.call('expire','key',30)
  return 1
else
  return 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 
else
  return 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;
    }

    @Override
    public void lock()
    {
        tryLock();
    }
    @Override
    public boolean tryLock()
    {
        try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
        return false;
    }

    @Override
    public 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;
    }

    @Override
    public 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 = false
        Long 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()
        {
            @Override
            public void run()
            {
                if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)))
                {
                    renewExpire();
                }
            }
        },(this.expireTime * 1000)/3);
    }

    //暂时用不到
    @Override
    public void lockInterruptibly() throws InterruptedException
    {

    }
    @Override
    public Condition newCondition()
    {
        return null;
    }
}

分布式锁工厂

@Component
public class DistributedLockFactory
{
    @Autowired
    private 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.coloradmin.cn/o/1496261.html

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

相关文章

Jmeter压测分配业务比例

在进行综合场景压测时&#xff0c;由于不同的请求&#xff0c;要求所占比例不同&#xff0c;如何实现呢&#xff1f; 不同的请求&#xff0c;服务器对其处理能力不同&#xff0c;有的处理快&#xff0c;有的处理慢。 真实模拟按比例进行并发&#xff1a; 在使用LR进行过类似…

在winform中如何嵌入第三方软件窗体✨

相关win32api的学习✨ SetParent [DllImport("user32.dll ", EntryPoint "SetParent")] private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent); //将外部窗体嵌入程序语法&#xff1a; HWND SetParent([in] H…

windows关闭copilot预览版

如果用户不想在windows系统当中启用Copilot&#xff0c;可以通过以下三种方式禁用。 第一种&#xff1a;隐藏Copilot 按钮 右键点击任务栏&#xff0c;取消勾选“显示 Copilot&#xff08;预览版&#xff09;按钮”&#xff0c;任务栏则不再显示&#xff0c;用户可以通过快捷键…

2024 年 AI 辅助研发趋势:从研发数字化到 AI + 开发工具 2.0,不止于 Copilot

在上一年里&#xff0c;已经有不少的企业在工具链上落地了生成式 AI&#xff0c;结合我们对于这些企业的分析&#xff0c;以及最近在国内的一些 “新技术” 趋势&#xff0c;诸如于鸿蒙原生应用的初步兴起。从这些案例与趋势中&#xff0c;我们也看到了一些新的可能方向。 结合…

【C++】蓝桥杯必备 算法竞赛常用STL万字总结

传送门⏬⏬⏬[方便查表] &#x1f31f;一、什么是STL&#xff1f;&#x1f31f;二、为什么STL重要&#xff1f;✨1、原因✨2、STL的作用 &#x1f31f;三、STL知识点总结✨0.使用说明书✨1、vector 【可变数组】✨2、pair [ x,y ]✨3、string【字符串】✨4、queue【队列】 和pr…

2024年【道路运输企业安全生产管理人员】复审考试及道路运输企业安全生产管理人员模拟考试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年道路运输企业安全生产管理人员复审考试为正在备考道路运输企业安全生产管理人员操作证的学员准备的理论考试专题&#xff0c;每个月更新的道路运输企业安全生产管理人员模拟考试题祝您顺利通过道路运输企业安全…

第三讲 汇编初步 课程随手记

一、寄存器 32位CPU通用寄存器如下图所示&#xff1a; 因为教材依照的是32位CPU寄存器&#xff0c;而我安装的是64位寄存器&#xff0c;所以找了一下64位的寄存器的资料 PS&#xff1a;一般来说&#xff0c;Intel处理器字节存储顺序为小端法存储&#xff0c;是指数据的高字节保…

JavaScript极速入门(1)

初识JavaScript JavaScript是什么 JavaScript(简称JS),是一个脚本语言,解释型或者即时编译型语言.虽然它是作为开发Web页面的脚本语言而著名,但是也应用到了很多非浏览器的环境中. 看似这门语言叫JavaScript,其实在最初发明之初,这门语言的名字其实是在蹭Java的热度,实际上和…

Vue2+ElementUI下拉、Select组件的封装

Vue2ElementUI下拉、Select组件的封装&#xff1a;引言 在 Vue2 项目中&#xff0c;ElementUI 的 el-select 组件是常用的下拉选择框组件。它提供了丰富的功能和样式&#xff0c;可以满足各种需求。但是&#xff0c;在实际开发中&#xff0c;我们经常会遇到一些重复性的需求&a…

男人的玩具系统wordpress外贸网站主题模板

垂钓用品wordpress外贸模板 鱼饵、鱼竿、支架、钓箱、渔线轮、鱼竿等垂钓用品wordpress外贸模板。 https://www.jianzhanpress.com/?p3973 身体清洁wordpress外贸网站模板 浴盐、防蚊液、足部护理、沐浴液、洗手液、泡澡用品wordpress外贸网站模板。 https://www.jianzhan…

【CSP试题回顾】201612-1-中间数

CSP-201612-1-中间数 解题思路 输入和初始化&#xff1a;首先&#xff0c;程序读入一个整数n&#xff0c;表示序列中数的个数。接着&#xff0c;读入n个正整数并存储在numList向量中&#xff0c;这些数依次表示a1, a2, …, an。 排序&#xff1a;使用sort函数对numList进行升…

前端运算符比较与计算中的类型转换,运算规则

题目&#xff1a; 下面表达式的值分别都是什么&#xff08;类型转换&#xff09; 0 0 0 2 true 2 false false false false 0 false undefined false null null undefined\t\r\n 0JS中的原始类型有哪些 原始值类型就是 存储的都是值&#xff0c;没有函数可以调用的。…

【Python时序预测系列】基于LSTM+Attention实现单变量时间序列预测(源码)

这是我的第232篇原创文章。 一、引言 长短期记忆网络&#xff08;LSTM&#xff09;结合注意力机制是一种常用的深度学习模型结构&#xff0c;用于处理序列数据。LSTM是一种循环神经网络&#xff08;RNN&#xff09;的变体&#xff0c;专门设计用来解决长序列数据的梯度消失和…

不知道去哪里找拍抖音的短视频素材?分享几个抖音短视频素材资源网站

嘿嘿&#xff0c;小伙伴们&#xff0c;是不是在抖音创作的路上遇到了素材荒&#xff1f;别担心&#xff0c;我这里有几个超给力的短视频素材网站推荐给大家&#xff0c;保证让你的创作不再为素材发愁 1&#xff0c;蛙学府资源 这个网站简直是短视频素材的大宝库&#xff0c;无…

CSS盒子模型笔记

尚硅谷学习视频链接&#xff1a;117_CSS_盒子模型的组成部分_哔哩哔哩_bilibili 1、盒子组成 盒子组成 content内容 padding border &#xff08;margin不包含在盒子内&#xff09; 2、div样式width、height 当css3属性box-sizingcontent-box&#xff08;默认&#xff0…

基于Java的超市自助付款系统(Vue.js+SpringBoot)

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容2.1 商品类型模块2.2 商品模块2.3 超市账单模块 三、界面展示3.1 登录注册模块3.2 超市商品类型模块3.3 超市商品模块3.4 商品购买模块3.5 超市账单模块 四、部分源码展示4.1 实体类定义4.2 控制器接口 五、配套文档展示六、…

乐优商城(八)商品详情

1. 搭建商品详情微服务 当用户搜索到商品后&#xff0c;如果想要了解商品的更多信息&#xff0c;就需要进入商品详情页。 由于商品详情浏览量比较大&#xff0c;所以我们会创建一个微服务&#xff0c;用来展示商品详情。我们的商品详情页会采用 Thymeleaf 模板引擎渲染后&…

Linux高级编程:网络

回顾&#xff1a; 进程间的通信&#xff1a; 同一主机内通信&#xff1a; 传统的进程间通信方式&#xff08;管道、信号&#xff09;&#xff1b; IPC对象&#xff08;共享内存&#xff0c;消息队列&#xff0c;信号量集&#xff09;&#xff1b; 不同主机间进程的通信&#…

Linux之线程概念

目录 一、细粒度划分 1、堆区细粒度划分 2、物理内存和可执行程序细粒度划分 3、虚拟地址到物理地址的转化 二、线程的概念 1、基本概念 2、线程的优点 3、线程的缺点 4、线程异常 5、线程用途 三、Linux下的进程和线程 一、细粒度划分 1、堆区细粒度划分 在语言…

【MySQL】索引优化与关联查询优化

数据库调优的几个维度&#xff1a; 索引失效&#xff0c;没有充分用到索引——索引建立关联查询太多JOIN——SQL优化服务器调优以及各个参数设置——调整my.cnf数据过多——分库分表 SQL查询优化的几种方式&#xff1a; 物理查询优化&#xff1a;通过索引以及表连接方式进行…