Redis分布式锁 | 黑马点评

news2024/11/15 9:00:24

目录

一、分布式锁概述

二、基于Redis的分布式锁

1、思路分析

2、初级版本

3、误删问题

4、改进分布式锁

5、原子性问题

6、使用Lua脚本解决原子性问题

7、setnx实现分布式锁存在问题

三、Redisson

1、Redisson快速入门

2、Redisson可重入锁原理

3、Redisson可重试原理

4、Redisson解决超时问题

5、Redission主从一致性问题

四、总结


一、分布式锁概述

在集群模式下,synchronize根本锁不住。因为每个都是不同tomcat,不同jvm的存在,每个jvm的每个锁都可以有一个线程来获取,就会出现并行安全问题。

要想解决这种问题,必须得想办法让多个jvm只能用同一个锁。分布式锁

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。让多个jvm进程都可以看到锁监视器,而且只有一个进程可以拿到锁。

特点:多进程可见、互斥、高可用、高性能、安全性

二、基于Redis的分布式锁

1、思路分析

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

我们获取锁的方法可能会出现问题,当我们添加完锁之后,还没来得及设置时间突然宕机,这个时候就死锁了。所以我们必须保证添加锁和设置时间的时候要原子性,一起完成。

我们可以把获取锁的两步修改为:(NX代表互斥,EX是设置超时时间)

#添加锁,NX是互斥,EX是设置超时时间
SET lock thread1 NX EX 10

2、初级版本

需求:定义一个类,实现下面接口,利用Redis分布式锁实现一个用户只能下一单的业务

public interface ILock {

    /**
     * 尝试获取锁
     * @param timeoutSec 锁持有的超时时间,过期后自动释放
     * @return true代表获取锁成功;false代表获取锁失败
     */
    boolean tryLock(long timeoutSec);


    /**
     * 释放锁
     */
    void unlock();
}

实现ILock接口:

name属性:我们希望不同的业务有不同的锁,name是业务的名称

锁的参数:key是lock:业务名称,value是锁的线程的id

public class SimpleRedisLock implements ILock{

    private StringRedisTemplate stringRedisTemplate;

    //锁的名称
    private String name;

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

    //锁的前缀
    private static final String KEY_PREFIX ="lock:";


    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程表示
        long threadId = Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+name,threadId+"", timeoutSec, TimeUnit.SECONDS);
        //防止自动插箱的时候空指针带来的危险
        return Boolean.TRUE.equals(success);
    }

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

业务层调用,实现一人只能下一单

我们这里构造参数传入的时候name不仅仅传入业务名称了,还要加上用户id,因为只是锁一个用户下一单,同一个用户才加锁。所以传入“order:”+userId

  @Autowired
    private ISeckillVoucherService iSeckillVoucherService;

    @Autowired
    private RedisIdWorker redisIdWorker;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result seckillVoucher(Long voucherId) {
        //1.查询优惠劵
        SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
        //2.判断秒杀是否开始
        LocalDateTime beginTime = voucher.getBeginTime();
        if (beginTime.isAfter(LocalDateTime.now())) {
            //尚未开始
            return Result.fail("活动尚未开始");
        }
        //3.判断秒杀是否已经结束
        LocalDateTime endTime = voucher.getEndTime();
        if (LocalDateTime.now().isAfter(endTime)) {
            //已结束
            return Result.fail("活动已经结束");
        }
        //4判断库存是否充足
        if (voucher.getStock() < 1) {
            //库存不足
            return Result.fail("库存不足!");
        }
        Long userId = UserHolder.getUser().getId();
//        synchronized (userId.toString().intern()){
        //创建锁对象
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        //获取锁
        boolean tryLock = lock.tryLock(1200);
        //判断获取锁成功
        if (!tryLock){
            //获取锁失败,返回错误或重试
            return Result.fail("一个人允许下一单");
        }
        //这里可能会异常我们要try一下,异常的话,finally释放锁
        try {
            //获取spring事务代理对象
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } catch (IllegalStateException e) {
            e.printStackTrace();
        }finally {
            //释放锁
            lock.unlock();
        }
//    }
        return Result.fail("抢购失败");
    }

3、误删问题

当线程1正确获取redis锁执行业务,业务某种原因阻塞,超过时间就会超时释放,一旦1提前释放,2能获取成功,当2正在拿锁做业务时,1突然醒了然后执行完毕释放锁,这个时候1就把2的锁给释放掉了,3或者其他线程就能够进来。

这种情况产生主要是因为线程1把线程2的锁给释放了导致,我们可以在释放锁之前加上判断是否是自己的锁,我们可以把redis存入的value来当这个线程的标识,删除的时候取出来判断一下

4、改进分布式锁

修改之前的分布式锁,在获取锁的时候存入线程的标识(可以是UUID标识)

为什么要用UUID来标识呢,我们之前用线程id为什么不行?

因为线程id是递增的,每个jvm都是这样递增,所以不同的tomcat最终线程id是可能相同的

在释放的时候先判断线程是否一致,一致释放,不一致不释放。

获取锁和释放锁的方法改进:

我们这里采用UUID+线程id当做redis的value

public class SimpleRedisLock implements ILock{

    private StringRedisTemplate stringRedisTemplate;

    //锁的名称
    private String name;

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

    //锁的前缀
    private static final String KEY_PREFIX ="lock:";
    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);
    }

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

UUID是常量,同一个线程生成的UUID是一样的,所以可以这样写,我们的value还加上的线程id

因为单UUID还是可能会重复的,只是概率特别小,再加上线程id,就不太可能重复了。

5、原子性问题

这种情况:当线程1获取锁执行业务,完成业务释放锁判断标识一致可以释放,这个时候判断完成堵塞了(可能是jvm垃圾回收会阻塞所有的代码),就没有释放成功,如果足够长,就触发了超时释放锁。一旦超时释放,其他线程2就能获取锁然后执行业务,这个时候线程1恢复了,但是他已经判断完标识了,这个时候直接又把2的锁给释放了,然后线程3又能进来执行

 所以我们必须保证判断锁释放锁原子性

6、使用Lua脚本解决原子性问题

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性

lua脚本是用Lua是一种编程语言,我们只要会用基础的操作就可以了

再次改进Redis的分布式锁:

需求:基于Lua脚本实现分布式锁的释放锁逻辑

释放锁的逻辑改变

private static final DefaultRedisScript<Long> UNLOCK_SCRIPT ;

    static {
        UNLOCK_SCRIPT =new DefaultRedisScript<>();
        //设置脚本的路径,就在resource目录下直接名字就可以
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        //设置返回值类型
        UNLOCK_SCRIPT.setResultType(Long.class);
    }  
    @Override
    public void unlock() {
            //调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                //要传入集合所以要转一下
                Collections.singletonList(KEY_PREFIX+name),
                ID_PREFIX+Thread.currentThread().getId());
    }

lua脚本

-- 比较线程标示与锁中的标示是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then
    --释放锁 del key
    return redis.call('del',KEYS[1])
end
return 0

7、setnx实现分布式锁存在问题

不可重入:线程1先调用A获得锁,又要调用B方法,需要获取同一把锁,如果锁是不可重入的,方法A又没有释放锁,这个时候就会出现死锁问题。

不可重试:我们之前的是没有拿掉锁立刻失败返回false,我们有时候需要这个锁被别人获取拿不到,我们就等等,如果最后成功了我们再执行业务。

超时释放:太短的话可能没执行完业务就放,太长可能出问题了等待时间较长

主从一致性:redis主从之间同步是存在延迟的,线程1在主节点获得了锁,尚未同步给从节点的时候,突然主节点宕机,但是替换的从节点没有锁的,这个时候其他线程都可以拿到锁了。但是这种情况概率比较低,主从的延迟是极低的。

要解决上面这些问题就非常麻烦了,所以我们要借助成熟的框架Redisson

三、Redisson

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

1、Redisson快速入门

(1)引入Redisson依赖

(2)配置Redisson客户端

(3)使用Redisson的分布式锁

2、Redisson可重入锁原理

我们自己实现的分布所锁不能可重入,为什么redisson的可以呢,底层怎么实现呢?

如图,他底层锁的是用的redis的hash结构,因为还要存个进入锁的次数,key是之前业务名称,value分别是线程的唯一标识进入锁的次数

当每次进入就判断线程是不是之前的线程并让统计数+1,执行完业务就让统计次数-1,如果统计数为0,说明要释放锁了。

因为操作很多,所有我们要保证原子性必须用lua脚本来写

获得锁的lua脚本

 释放锁的lua脚本

3、Redisson可重试原理

他底层在拿不到锁的时候并没有直接结束,而是订阅别人释放锁的信号。

在源码lua中每当有锁释放的时候都会发出异步消息告诉别人已经释放锁了。

所有有人真的释放锁应该会发消息过来,我们收到消息再重试。

这个等待多久时间是我们设置的最大等待时间,等到最大剩余时间结束了还没拿到锁,那就取消订阅返回false。这种是等待释放了再尝试,不是一直尝试减少了cpu的消耗

4、Redisson解决超时问题

利用看门狗机制,就是在获取成功之后,开启一个看门狗的定时任务,每隔一段时间就会重置锁的超时时间。

5、Redission主从一致性问题

主从分离,主节点来写操作,从节点来读操作,为了保证数据一致性,所有要进行主从同步

但是主从同步会有一定的延迟,可能就会产生问题

如果主节点修改完之后还没来得及同步瞬间挂了,这就是产生了主从一致性问题

redission的解决策略就很简单,直接开三台redis同时来写和读,把3台redis的锁合在一起做连锁,也可以3台机器都做主从分离也可以不建立。

四、总结

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

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

相关文章

从某一点出发沿任意一方向旋转矩阵计算思考与实现

欢迎关注更多精彩 关注我&#xff0c;学习常用算法与数据结构&#xff0c;一题多解&#xff0c;降维打击。 上期讲到 绕任一向量旋转矩阵计算思考与实现 点击前往 点击前往 问题提出 之前讲到绕任一向量旋转矩阵实现&#xff0c;原来的向量都是从原点出发&#xff0c;现在把…

Ajax面试题目

更多Ajax面试题目可以查看专栏内容 1.AJAX应用和传统Web应用有什么不同&#xff1f; 传统的web前端与后端的交互中&#xff0c;浏览器直接访问Tomcat的Servlet来获取数据。Servlet通过转发把数据发送给浏览器。当我们使用AJAX之后&#xff0c;浏览器是先把请求发送到XMLHttpR…

Swift之struct二进制大小分析

随着Swift的日渐成熟和给开发过程带来的便利性及安全性&#xff0c;京喜App中的原生业务模块和基础模块使用Swift开发占比逐渐增高。本次讨论的是struct对比Class的一些优劣势&#xff0c;重点分析对包体积带来的影响及规避措施。 一、基础知识 1、类型对比 引用类型&#xff…

独立看门狗与窗口看门狗

定义 看门狗的本质是一个定时器&#xff0c;在启动后&#xff0c;需要在一定时间内再给它一个信号&#xff0c;俗称“喂狗”&#xff0c;如果没有按时“喂狗”&#xff0c;说明MCU可能处于非正常状态&#xff0c;这时看门狗就向MCU发送个复位信号&#xff0c;使整个系统重启&a…

51单片机数码管显示

文章目录前言一、数码管简介二、数码管原理图三、数码管显示原理四、静态数码管代表编写五、动态数码管总结前言 这篇文章将介绍数码管的显示其中包含了动态数码管和静态数码管两种。 一、数码管简介 数码管其实就是由多个发光二极管封装在一起组成“8”字型的器件当分别点亮…

【数据结构】超详细——堆的实现

一、堆的概念及性质 1.1 什么是堆&#xff1f; 堆是一种完全二叉树&#xff08;具体在下一章讲述&#xff09;&#xff0c;若二叉树的深度h&#xff0c;除了第h层外其余各层节点数满了&#xff0c;只有第h层缺额且该层结点靠左&#xff1b;任何一个数组可以看作完全二叉树&…

【14】C语言_函数简介

目录 1、C语言中函数的分类: 2、库函数 3、自定义函数 1、C语言中函数的分类: 1.库函数 2.自定义函数 2、库函数 为什么会有库函数? 1.我们知道在我们学习C语言编程的时候&#xff0c;总是在一个代码编写完成之后迫不及待的想知道结果&#xff0c;想把这个结果打印到我们的屏…

ESP32设备驱动-LX1972可见光传感器驱动

LX1972可见光传感器驱动 1、LX1972介绍 LX1972 是一款低成本硅光传感器,其光谱响应非常接近人眼。专利电路在 520nm 处产生峰值光谱响应,IR 响应小于峰值响应的 5%,高于 900nm。 光电传感器是一个 PIN 二极管阵列,具有线性、准确和非常可重复的电流传递函数。 芯片上的…

扫盲-从零开始搭建阿里云流媒体服务器/音视频编解码/

1.基础概念 2.简单模式-HTTP文件服务器 存储采用NAS&#xff0c;服务器配置 采用FASTDFS (192条消息) Linux新手入门系列&#xff1a;FastDFS单机部署一键安装脚本_IT小胖豆的博客-CSDN博客 有几个坑&#xff1a; 常用命令&#xff1a; tail -20f /usr/local/nginx/logs/er…

3小时精通opencv(三)图片裁剪与形状绘制

3小时精通opencv(三)图片裁剪与形状绘制 参考视频资源:3h精通Opencv-Python 文章目录3小时精通opencv(三)图片裁剪与形状绘制图片裁剪绘制形状绘制直线绘制矩形绘制圆形绘制文字整体代码图片裁剪 图片裁剪不需要使用opencv中特有的函数, 对于opencv中读取到的图像, 直接当做矩…

15. python数据类型转换

1. 隐式类型转换 - 自动完成 在隐式类型转换中&#xff0c;Python 会自动将一种数据类型转换为另一种数据类型&#xff0c;不需要我们去干预。 (1) 以下实例中&#xff0c;我们对两种不同类型的数据进行运算&#xff0c;较低数据类型&#xff08;整数&#xff09;就会转换为较…

java 探花交友项目day4 MongoDB

数据库表 接口定义 其他都比较简单 我们讲黑名单查询页面的设计 DubboService public class BlackListApiImpl extends ServiceImpl<BlackListMapper,BlackList> implements BlackListApi{Autowiredprivate BlackListMapper blackListMapper;Autowiredprivate UserInf…

Allegro如何输出坐标文件操作指导

Allegro如何输出坐标文件操作指导 PCB在SMT的时候会需要用坐标文件,Allegro支持输出坐标文件,如下图 具体操作如下 选择Tools选择report出现repor

LINUX学习之正则表达式(十二)

普通正则 元字符 元字符匹配描述.匹配除了换行符以外的任意单个字符*前导字符出现0次或连续多次.*任意长度字符^行首(以…开头)$行尾(以…结尾)^$空行[]匹配括号里任意单个字符或一组单个字符[^]匹配不包含括号里任一单个字符或一组单个字符^[]匹配以括号里任意单个字符或一组…

Python技能树-推导式

Python 列表推导式(1) Python 独步天下的推导式表达式&#xff0c;使用列表推导式过滤出偶数列表 # -*- coding: UTF-8 -*- if __name__ __main__:list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]print()print("# 使用列表推导式过滤出偶数")# TODO(you): 请在此实现过滤代…

Allegro如何添加平衡铜操作指导

Allegro如何添加平衡铜操作指导 PCB在加工的时候,工厂会添加平衡铜,Allegro支持自动加上平衡铜,如下图 具体操作如下 选择Manufacture点击Thieving

比较器: Comparable 与 Comparator 区别

比较器&#xff1a; Comparable 与 Comparator 区别 每博一文案 师父说: 人不能精得过火&#xff0c;太精明的人往往让人生厌&#xff0c;人也别傻的可怜&#xff0c;一腔热血付出却白忙一场。 太精明的人&#xff0c;凡事都想要争个明明白白&#xff0c;每一分钱都要和人计较…

macOS Ventura 13.1 系统问题:掉电快 充电慢

今年一月份升级了 MBA 的系统&#xff0c;之后的笔记本&#xff1a; 使用过程&#xff1a;电量不禁用&#xff0c;掉电很快。 充电过程&#xff1a;很慢。而且存在一定几率&#xff1a;电量充到某个值&#xff08;如30%&#xff09;之后不管再充多久还是这个电量值。 系统信息…

HDFS高可用单NameNode从standby恢复为active(二)

1、背景 有一个hdfs高可用集群&#xff0c;因为某些操作&#xff0c;导致其中一个namenode的信息全部丢失了。最后只剩下一个完整的namenode信息和datanode信息。于是在在启动hdfs后发现独有的namenode始终处于standby状态。即使通过hdfs haadmin -transitionToActive命令也不能…

java面向接口编程2023027

那就再进一步&#xff1a;面向接口编程 面向接口编程前面已经提到&#xff0c;接口体现的是一种规范和实现分离的设计哲学&#xff0c;充分利用接口可以极好地降低程序各模块之间的耦合&#xff0c;从而提高系统的可扩展性和可维护性。 基于这种原则&#xff0c;很多软件架构设…