首页 > 其他分享 >分布式锁实现

分布式锁实现

时间:2024-11-05 21:23:00浏览次数:3  
标签:实现 lock 获取 num key 执行 stringRedisTemplate 分布式

分布式锁实现

分布式锁主流的实现方案:

  1. 基于数据库实现分布式锁
  2. 基于缓存( Redis等)
  3. 基于Zookeeper

每一种分布式锁解决方案都有各自的优缺点:

  1. 高性能:Redis最高
  2. 可靠性:zookeeper最高

分布式锁的关键是多进程共享的内存标记(锁),因此只要我们在Redis中放置一个这样的标记(数据)就可以了。不过在实现过程中,不要忘了我们需要实现下列目标:

  • 多进程可见:多进程可见,否则就无法实现分布式效果
  • 避免死锁:死锁的情况有很多,我们要思考各种异常导致死锁的情况,保证锁可以被释放
  • 排它:同一时刻,只能有一个进程获得锁
  • 高可用:避免锁服务宕机或处理好宕机的补救措施(redis集群架构:1.主从复制 2.哨兵 3.cluster集群)

分布式锁使用的逻辑如下:

尝试获取锁
	成功:执行业务代码    
		执行业务  
			try{
				获取锁
				业务代码-宕机
			} catch(){
			
			}finally{ 
				释放锁
			}
 	失败:等待;

使用Redis实现分布式锁

image-20241029210846880

  1. 多个客户端同时获取锁(setnx)
  2. 获取成功,执行业务逻辑:从db获取数据,放入缓存,执行完成释放锁(del)
  3. 其他客户端等待重试

1、分布式锁初版

/**
 * 采用SpringDataRedis实现分布式锁
 * 原理:执行业务方法前先尝试获取锁(setnx存入key val),如果获取锁成功再执行业务代码,业务执行完毕后将锁释放(del key)
 */
@Override
public void testLock() {

    //0.先尝试获取锁 setnx key val
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", "lock");
    if(flag){
        //获取锁成功,执行业务代码
        //1.先从redis中通过key num获取值  key提前手动设置 num 初始值:0
        String value = stringRedisTemplate.opsForValue().get("num");
        //2.如果值为空则非法直接返回即可
        if (StringUtils.isBlank(value)) {
            return;
        }
        //3.对num值进行自增加一
        int num = Integer.parseInt(value);
        stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

        //4.将锁释放
        stringRedisTemplate.delete("lock");

    }else{
        try {
            Thread.sleep(100);
            this.testLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

问题:setnx刚好获取到锁,业务逻辑出现异常,导致锁无法释放

解决:设置过期时间,自动释放锁。

2、优化之设置锁的过期时间

设置过期时间有两种方式:

  1. 首先想到通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)
  2. 在set时指定过期时间(推荐)

image-20241029211018144

设置过期时间:

//0.先尝试获取锁 setnx key val
Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock", "lock", 3, TimeUnit.SECONDS); 

问题1:设置过期时间可能会出现业务未执行完,key就过期了,其他线程就可以获取到锁,此时就出现了并发安全问题。

解决:自动续期

当前线程加锁成功后,创建了一个子线程,当锁的ttl时间还剩1/3时,自动将锁的ttl恢复到目标时间,这个操作就是锁的自动续期。

public void test(){
	new Thread(
    ()->{
        while(true){
            if(ttl==ttl/3){
                redisTemplate.opsForValue().set(key,value,time, TimeUnit.SECONDS);
            }
        }
    }
    ).start()
        
   //业务代码...
}

问题2:可能会释放其他服务器的锁。

场景:如果业务逻辑的执行时间是7s。执行流程如下

  1. index1业务逻辑没执行完,3秒后锁被自动释放。
  2. index2获取到锁,执行业务逻辑,3秒后锁被自动释放。
  3. index3获取到锁,执行业务逻辑,index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁, 导致index3的业务只执行1s就被别人释放。最终等于没锁的情况。

解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁

3、优化之UUID防误删

image-20241029211444085

@Override
public void testLock() {
    //0.先尝试获取锁 setnx key val
    String uuid = UUID.randomUUID().toString();
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
    if(flag){
        //获取锁成功,执行业务代码
        //1.先从redis中通过key num获取值  key提前手动设置 num 初始值:0
        String value = stringRedisTemplate.opsForValue().get("num");
        //2.如果值为空则非法直接返回即可
        if (StringUtils.isBlank(value)) {
            return;
        }
        //3.对num值进行自增加一
        int num = Integer.parseInt(value);
        stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

        //4.将锁释放,判断当前持有锁的对象=》是否《=跟当初获取锁的对象的值 一致
        if(uuid.equals((String)stringRedisTemplate.opsForValue().get("lock"))) {
            stringRedisTemplate.delete("lock");
        }
    }else{
        try {
            Thread.sleep(100);
            this.testLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
} 

问题:删除操作缺乏原子性。

场景:

​ 1、index1执行删除时,查询到的lock值确实和uuid相等

​ 2、index1执行删除前,lock刚好过期时间已到,被Redis自动释放,在Redis中没有了锁。

​ 3、index2获取了lock,index2线程获取到了cpu的资源,开始执行方法

​ 4、index1执行删除,此时会把index2的lock删除。

index1 因为已经在方法中了,所以不需要重新上锁。index1有执行的权限。index1已经比较完成了,这个时候,开始执行删除了index2的锁!

解决:使用lua脚本保证原子性

4、优化之LUA脚本保证删除的原子性

释放锁的LUA脚本:

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

redis java客户端使用LUA脚本:

//通过execute可以执行LUA脚本,参数1:脚本字符串,参数2:脚本返回值类型,参数3:keys列表,参数4:argv列表
stringRedisTemplate.execute(new DefaultRedisScript<>(script , Boolean.class),list,args...)

使用LUA脚本优化分布式锁

/**
 * 采用SpringDataRedis实现分布式锁
 * 原理:执行业务方法前先尝试获取锁(setnx存入key val),如果获取锁成功再执行业务代码,业务执行完毕后将锁释放(del key)
 */
@Override
public void testLock() {

    //0.先尝试获取锁 setnx key val
    //问题:锁可能存在线程间相互释放
    //Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", "lock", 10, TimeUnit.SECONDS);
    //解决:锁值设置为uuid
    String uuid = UUID.randomUUID().toString();
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);

    if(flag){
        //获取锁成功,执行业务代码
        //1.先从redis中通过key num获取值  key提前手动设置 num 初始值:0
        String value = stringRedisTemplate.opsForValue().get("num");
        //2.如果值为空则非法直接返回即可
        if (StringUtils.isBlank(value)) {
            return;
        }
        //3.对num值进行自增加一
        int num = Integer.parseInt(value);
        stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

        //4.将锁释放 判断uuid
        //问题:删除操作缺乏原子性。
        //if(uuid.equals(stringRedisTemplate.opsForValue().get("lock"))){ //线程一:判断是满足是当前线程锁的值
        //    //条件满足,此时锁正好到期,redis锁自动释放了线程2获取锁成功,线程1将线程2的锁删除
        //    stringRedisTemplate.delete("lock");
        //}
        //解决:redis执行lua脚本保证原子,lua脚本执行会作为一个整体执行

        //执行脚本参数 参数1:脚本对象封装lua脚本,参数二:lua脚本中需要key参数(KEYS[i])  参数三:lua脚本中需要参数值 ARGV[i]
        //4.1 先创建脚本对象 DefaultRedisScript泛型脚本语言返回值类型 Long 0:失败 1:成功
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        //4.2设置脚本文本
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                "then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
        redisScript.setScriptText(script);
        //4.3 设置响应类型
        redisScript.setResultType(Long.class);
        stringRedisTemplate.execute(redisScript, Arrays.asList("lock"), uuid);
    }else{
        try {
            //睡眠
            Thread.sleep(100);
            //自旋重试
            this.testLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

DefaultRedisScript

DefaultRedisScript 是 Spring Data Redis 提供的一个类,用于简化 Redis Lua 脚本的使用。

类构造方法

  • DefaultRedisScript():创建一个新的 DefaultRedisScript 实例。

主要方法

  • setScriptText(String script):设置要执行的脚本内容。
    • 参数:一个字符串,表示 Lua 脚本的内容。
  • setResultType(Class resultType):设置脚本执行后返回值的类型。
    • 参数:指定返回值的类型,通常是 String.classLong.classObject.class
  • getScriptText():获取当前设置的脚本文本。
  • getResultType():获取当前脚本的返回类型。

执行脚本

  • redisTemplate.execute(redisScript对象, 实参1,实参2);

分布式锁总结

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下几个条件:

  • 互斥性。在任意时刻,只有一个客户端能持有锁。
  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
  • 加锁和解锁必须具有原子性。

标签:实现,lock,获取,num,key,执行,stringRedisTemplate,分布式
From: https://www.cnblogs.com/21CHS/p/18528877

相关文章

  • ROS机器人编程<六>:了解ROS系统及使用VScode实现话题通信(C++)
    目录ROS中基本的通信机制:一、话题通信:1.话题通信定义与基本概念2.核心要素3.工作流程4.消息接口与数据类型二、vscode实现话题通信三、C++实现话题通信 要求:编写发布订阅实现,要求发布方以10HZ(每秒10次)的频率发布文本消息,订阅方订阅消息并将消息内容打印输出1.在......
  • 鸿蒙 next 实现应用内的暗黑模式切换
    鸿蒙next实现应用内的暗黑模式切换实现暗黑模式的大致思路是利用@Provider与@Consume共享一个lightMode变量,在页面创建时读取持久化的lightMode,来实现暗黑模式。1.在Entry页面使用@Provide注解lightMode@Entry@ComponentstructQuickTestMainPage{@Providelig......
  • 通过VITE/rollup实现一个工程的代码分别打包成SPA(单页面应用)和MPA(多页面应用)
    问题背景我们的客户开发的系统会销售给多个不同的单位使用,并且是需要私有化部署的。在有的客户那里,直接部署完就结束了。但是另外一些客户,提出了一些特别的要求。他们要求我们的系统只需要提供一个个功能页面,无需提供菜单管理等功能。功能页面的调度、管理、权限等工作,则是由......
  • SpringBoot物资管理系统的设计与实现1yyr5程序+源码+数据库+调试部署+开发环境
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、项目背景随着企业规模的扩大和业务复杂度的提升,物资管理成为企业运营中的关键环节。传统的手工管理模式存在信息更新滞后、流程繁琐等问题,已难......
  • 基于SpringBoot+Vue的库存管理系统设计与实现毕设(文档+源码)
            目录一、项目介绍二、开发环境三、功能介绍四、核心代码五、效果图六、源码获取:        大家好呀,我是一个混迹在java圈的码农。今天要和大家分享的是一款基于SpringBoot+Vue的库存试管理系统,项目源码请点击文章末尾联系我哦~目前有各类成品......
  • 基于SpringBoot+Vue的疗养院管理系统设计与实现毕设(文档+源码)
            目录一、项目介绍二、开发环境三、功能介绍四、核心代码五、效果图六、源码获取:        大家好呀,我是一个混迹在java圈的码农。今天要和大家分享的是一款基于SpringBoot+Vue的疗养院管理系统,项目源码请点击文章末尾联系我哦~目前有各类成品......
  • Docker部署Portainer CE结合内网穿透实现容器的可视化管理与远程访问
    文章目录前言1.本地安装Docker2.本地部署PortainerCE3.公网远程访问本地Portainer-CE3.1内网穿透工具安装3.2创建远程连接公网地址4.固定PortainerCE公网地址前言本篇文章介绍如何在Ubuntu中使用docker本地部署PortainerCE可视化管理工具,并......
  • 使用 R 语言实现简单的文字识别程序
    在这篇文章中,我们将使用R语言来实现一个简单的文字识别程序。R语言是一个强大的统计计算与数据分析语言,通常被用于数据科学和统计分析中。我们将手动编写图像处理和字符识别步骤,演示如何利用R来完成文字识别任务。环境准备首先,你需要安装R和一些必要的包。可以从R的官......
  • C语言实现一个打印非负整数阶乘的函数
    简单版阶层计算升级版阶层计算(c语言的基本类型不能存储)简单版阶层计算:其中N是用户传入的参数,其值不超过12。如果N是非负整数,则该函数必须返回N的阶乘,否则返回0裁判测试程序样例:#include<stdio.h>intFactorial(constintN);intmain(){intN,NF;s......
  • 基于Python Tkinter和Calendar模块实现:个人日历应用
    1.项目概述本项目旨在开发一个基本的个人日历应用,帮助查看日历、添加和管理个人事件。该应用基于Python的tkinter图形界面库和calendar模块,能够动态展示一个月的日历,并允许在指定日期添加事件。通过该应用,可以在日历上直观地查看每个月的安排,方便管理日常事务。2.技术栈与......