草庐IT

锁 - 分布式锁工具

Hello, World. 2023-04-18 原文

锁概述

在计算机科学中,锁是在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥要求的满足。

锁相关概念

  • 锁开销:完成一个锁可能额外耗费的资源,比如一个周期所需要的时间,内存空间。
  • 锁竞争:一个线程或进程,要获取另一个线程或进程所持有的锁,边会发生锁竞争。锁粒度越小,竞争的可能越小。
  • 死锁:多个线程争夺资源互相等待资源释放导致阻塞;由于无限期阻塞,程序不能正常终止。

分类

  • 乐观锁、悲观锁:是否锁定同步资源。
    • 乐观锁:认为其他线程对数据访问时 不会 修改数据,实际未加锁,更新数据时判断是否被其他线程更新了(读时不加锁,写时加锁)。
      • 适合多读的场景,因为读操作没有加锁。
      • 实现原理:CAS (compare-and-swap) ,无锁算法,原子操作比较更新。
      • 使用:
        • Java 中的 CAS 锁(AtomicXxx)通过 JNI 调用 CPU 中的 cmpxchg 汇编指令实现
        • 数据库表增加 version 字段,更新时判断 version 未改变。
      • 缺陷:
        • ABA 问题:数据发生类似变化(A -> B -> A),会认为数据没有改变。
          JDK 1.5 引入 AtomicStampedReference 增加标志位(1A -> 2B -> 3A)
        • 自旋问题:CAS 无法获取到锁会在超时时间内循环获取,造成 CPU 资源浪费
    • 悲观锁:认为其他线程对数据访问时 一定会 修改数据,访问数据时加锁同步处理(一开始加锁无论读写)。
      • 适合多写的场景,独占数据的读写权限,确保数据的读取和更新都是准确的。
  • 读写锁
    • 读锁:共享锁,可支持多线程并发读。
    • 写锁:独享锁,读写、写写互斥。
    • 示例:ReentrantReadWriteLock
  • 可重入锁、不可重入锁
    • 可重入锁(递归锁):一个线程在已加锁范围内代码中再次进行加锁能够获取到锁
      • synchronized 、 ReentrantLock
    • 不可重入锁:一个线程对在已加锁范围内代码中再次进行加锁操作,由于第二次加锁时需要等待上次锁释放才可以加锁造成锁的互相等待
  • 公平锁、非公平锁
    • 公平锁:多个线程按照申请锁的顺序来获取锁,依赖 AQS 队列,线程直接进入队列中排队,第一个线程才能获取到锁
    • 非公平锁:多个线程加锁时尝试直接获取锁,获取不到进入队列,可能出现后申请锁的线程先获取到锁
      • 优点:可以减少唤起线程的开销,整体吞吐效率高
      • 缺点:处于等待队列中的线程可能饿死
      • synchronized
    • 示例:ReentrantLock 默认为非公平锁,构造方法可指定为公平锁 new ReentrantLock(true);
  • 偏向锁、轻量锁、重量锁:synchronized 的三种锁状态。
    • 偏向锁:锁标志位 101,在对象头(Mark Word)和栈帧中锁记录(Lock Record)里存储线程ID,通过 对比 Mark Word 避免执行 CAS
      • JDK 6 引入,JDK 15 标记废弃,可通过 JVM 参数(-XX:+UseBiasedLocking)手动启用
    • 轻量锁:锁标志位 000,偏向锁时出现竞争升级为轻量锁,未获取到锁的线程自旋获取,通过 CAS + 自旋 避免线程阻塞唤醒
    • 重量锁:锁标志位 010,轻量锁自旋超过一定此处升级为重量锁,未获取到锁的线程休眠
  • 分段锁、自旋锁:锁设计,非特定的锁。
    • 分段锁:将要锁定的数据拆分成段后对所需数据段加锁,减少锁定范围
      • ConcurrentHashMap 在 JDK 8 之前使用 Segment (继承 ReentrantLock)对桶数组分割分段加锁
    • 自旋锁:试探获取资源,未获取到采取自旋循环 where(true) 再次试探获取,不阻塞线程
      • 轻量锁通过 CAS + 自旋 实现
      • 优点:减少上下文切换
      • 缺点:占用 CPU

相关阅读:


自定义锁工具

1 :Redis 分布式锁(简单实现)

使用 ThreadLocal 保存锁对应的唯一标识
加锁:使用 STRING 保存锁定标识, 'SET key value PX NX' 确保一个 key 只能加锁一次
解锁:Lua 脚本判断是自己加的锁进行释放

  • 工具类

    RedisSimpleLockUtil.java
    // 使用 ThreadLocal 保存锁对应的唯一标识
    private static final ThreadLocal<String> LOCK_FLAG = ThreadLocal.withInitial(() ->
            UUID.randomUUID().toString().replace("-", "").toLowerCase()
    );
    
    // 尝试加锁
    private boolean tryLock(String key, long ttl) {
        try {
            String val = LOCK_FLAG.get();
            Boolean lockRes = redisTemplate.opsForValue()
                    .setIfAbsent(key, val, ttl, TimeUnit.MILLISECONDS);
            log.debug("tryLock, key={}, val={}, lockRes={}", key, val, lockRes);
            return Boolean.TRUE.equals(lockRes);
        } catch (Exception e) {
            log.error("tryLock occurred an exception", e);
        }
    
        return false;
    }
    
    // 解锁
    public boolean unlock(String key) {
        boolean succeed = false;
        try {
            List<String> keys = Collections.singletonList(key);
            Object[] args = {LOCK_FLAG.get()};
            Long unlockRes = redisTemplate.execute(UNLOCK_SCRIPT, keys, args);
            log.debug("unlock, key={}, args={}, unlockRes={}", key, args, unlockRes);
            succeed = Optional.ofNullable(unlockRes).filter(res -> res > 0).isPresent();
        } catch (Exception e) {
            log.error("unlock occurred an exception", e);
        } finally {
            if (succeed) {
                LOCK_FLAG.remove();
            }
        }
    
        return succeed;
    }
    
  • Lua 脚本

    解锁: redis_unlock_simple.lua
    local lock_key = KEYS[1];
    local lock_flag = ARGV[1];
    
    --- 判断锁定的唯一标识与参数一致删除锁
    --- 返回值:1=解锁成功(删除成功),0=锁已失效或删除失败,-1=非自己的锁不支持解锁
    local val = redis.call('GET', lock_key);
    if (not val) then
        return 0;
    elseif (val == lock_flag) then
        return redis.call('DEL', lock_key);
    else
        return -1;
    end
    
  • 缺陷

    • 只能单次加锁(唯一标识通过 ThreadLocal 存储,解锁时会清理 ThreadLocal,多次加解锁会导致与预期不符)
    • 不可重入
  • 参考:https://github.com/realpdai/tech-pdai-spring-demos/blob/main/264-springboot-demo-redis-jedis-distribute-lock/src/main/java/tech/pdai/springboot/redis/jedis/lock/lock/RedisDistributedLock.java

2 :Redis 分布式锁

使用 ThreadLocal 保存 锁key 与 相应的唯一标识
加锁:使用 HASH 保存锁标识与加锁次数
解锁:Lua 脚本判断是自己加的锁进行释放
功能:可重入(Redis HASH)、支持对不同 key 进行加解锁(ThreadLocal<Map<String, String>>)

  • 工具类

    RedisLockUtil.java
    // 使用 ThreadLocal 保存 锁key 与 唯一标识
    private static final ThreadLocal<Map<String, String>> LOCK_FLAG =
            ThreadLocal.withInitial(HashMap::new);
    // 尝试加锁
    private long tryLock(String key, long ttl) {
        String uniqueFlag = LOCK_FLAG.get().get(key);
        if (uniqueFlag == null) {
            uniqueFlag = UUID.randomUUID().toString().replace("-", "");
            LOCK_FLAG.get().put(key, uniqueFlag);
        }
    
        try {
            List<String> keys = Collections.singletonList(key);
            Object[] args = {uniqueFlag, ttl};
            Long lockRes = redisTemplate.execute(LOCK_SCRIPT, keys, args);
            log.debug("tryLock, lock_flag={}, key={}, args={}, lockRes={}",
                    LOCK_FLAG.get(), key, args, lockRes);
            return lockRes != null ? lockRes : 0L;
        } catch (Exception e) {
            log.error("tryLock occurred an exception", e);
        }
    
        return 0L;
    }
    
    // 尝试解锁
    public long tryUnlock(String key) {
        String uniqueFlag = LOCK_FLAG.get().get(key);
        if (uniqueFlag == null) {
            return 0L;
        }
    
        long lockNum = -1L;
        try {
            List<String> keys = Collections.singletonList(key);
            Object[] args = {uniqueFlag};
            Long unlockRes = redisTemplate.execute(UNLOCK_SCRIPT, keys, args);
            log.debug("unlock, key={}, args={}, unlockRes={}", key, args, unlockRes);
            lockNum = unlockRes != null ? unlockRes : 0L;
        } catch (Exception e) {
            log.error("release lock occurred an exception", e);
        } finally {
            if (lockNum == 0L) {
                LOCK_FLAG.get().remove(key);
                if (LOCK_FLAG.get().isEmpty()) {
                    LOCK_FLAG.remove();
                }
            }
        }
    
        return lockNum;
    }
    
  • Lua 脚本

    加锁: redis_lock.lua
      ```lua
      local lock_key = KEYS[1];
      local lock_flag = ARGV[1];
      --- 锁定时长,单位:毫秒
      local lock_ttl = tonumber(ARGV[2]);
    
      --- HASH 支持可重入
      --- lock_flag 保存加锁唯一标识
      --- lock_num 保存加锁次数
      local info = redis.call("HMGET", lock_key, "lock_flag", "lock_num");
      local h_flag = info[1];
      local h_num = tonumber(info[2]);
      if (h_num == nil or h_num < 0) then
          h_num = 0;
      end
    
      --- 返回加锁次数,未加锁成功返回 -1
      if (not h_flag or h_flag == lock_flag) then
          local res_num = h_num + 1;
          redis.call("HMSET", lock_key, "lock_flag", lock_flag, "lock_num", res_num);
          redis.call("PEXPIRE", lock_key, lock_ttl);
          return res_num;
      else
          return -1;
      end
      ```
    
    解锁: redis_unlock.lua
      ```lua
      local lock_key = KEYS[1];
      local lock_flag = ARGV[1];
    
      --- HASH 支持可重入
      --- lock_flag 保存加锁唯一标识
      --- lock_num 保存加锁次数
      local info = redis.call("HMGET", lock_key, "lock_flag", "lock_num");
      local h_flag = info[1];
      local h_num = tonumber(info[2]);
      if (h_num == nil) then
          h_num = 0;
      end
    
      --- 返回剩余加锁次数,未被加锁或解锁完返回 0,非自己加锁返回 -1
      if (not h_flag) then
          return 0;
      elseif (h_flag == lock_flag) then
          if (h_num <= 0) then
              redis.call("DEL", lock_key);
              return 0;
          else
              local res_num = h_num - 1;
              redis.call("HMSET", lock_key, "lock_flag", lock_flag, "lock_num", res_num);
              return res_num;
          end
      else
          return -1;
      end
      ```
    

其他

demo 地址:https://github.com/EastX/java-practice-demos/tree/main/demo-lock

有关锁 - 分布式锁工具的更多相关文章

  1. ruby - 分布式事务和队列,ruby,erlang,scala - 2

    我有一个涉及多台机器、消息队列和事务的问题。因此,例如用户点击网页,点击将消息发送到另一台机器,该机器将付款添加到用户的帐户。每秒可能有数千次点击。事务的所有方面都应该是容错的。我以前从未遇到过这样的事情,但一些阅读表明这是一个众所周知的问题。所以我的问题。我假设安全的方法是使用两阶段提交,但协议(protocol)是阻塞的,所以我不会获得所需的性能,我是否正确?我通常写Ruby,但似乎Redis之类的数据库和Rescue、RabbitMQ等消息队列系统对我的帮助不大——即使我实现某种两阶段提交,如果Redis崩溃,数据也会丢失,因为它本质上只是内存。所有这些让我开始关注erlang和

  2. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  3. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  4. postman接口测试工具-基础使用教程 - 2

    1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,

  5. ruby-on-rails - 有没有一种工具可以在编码时自动保存对文件的增量更改? - 2

    我最喜欢的Google文档功能之一是它会在我工作时不断自动保存我的文档版本。这意味着即使我在进行关键更改之前忘记在某个点进行保存,也很有可能会自动创建一个保存点。至少,我可以将文档恢复到错误更改之前的状态,并从该点继续工作。对于在MacOS(或UNIX)上运行的Ruby编码器,是否有具有等效功能的工具?例如,一个工具会每隔几分钟自动将Gitcheckin我的本地存储库以获取我正在处理的文件。也许我有点偏执,但这点小保险可以让我在日常工作中安心。 最佳答案 虚拟机有些人可能讨厌我对此的回应,但我在编码时经常使用VIM,它具有自动保存功

  6. ruby - 使用 Ruby 开发工具包将文件上传到 Amazon S3 - 2

    我正在尝试上传文件。一个简单的hello.txt。我正在关注文档,但无法将其上传到我的存储桶。#STARTAWSCLIENTs3=Aws::S3::Resource.newbucket=s3.bucket(BUCKET_NAME)begins3.buckets[BUCKET_NAME].objects[KEY].write(:file=>FILE_NAME)puts"Uploadingfile#{FILE_NAME}tobucket#{BUCKET_NAME}."bucket.objects.eachdo|obj|puts"#{obj.key}=>#{obj.etag}"endresc

  7. ruby - 在 StockChart (highchart) 中以编程方式显示柱形图的工具提示 - 2

    我有一个Highstock图表(带有标记和阴影的线条),并且想以编程方式显示一个highstock工具提示,例如,当我选择某个表上的一行(包含图表数据)我想显示相应的highstock工具提示。这可能吗? 最佳答案 股票图表thissolution不起作用:在thisexample你必须更换这个:chart.tooltip.refresh(chart.series[0].data[i]);为此:chart.tooltip.refresh([chart.series[0].points[i]]);解决方案可用here.

  8. ABB-IRB-1200运动学分析MATLAB RVC工具分析+Simulink-Adams联合仿真 - 2

    一、机器人介绍        此处是基于MATLABRVC工具箱,对ABB-IRB-1200型号的微型机械臂进行正逆向运动学分析,并利Simulink工具实现对机械臂进行具有动力学参数的末端轨迹规划仿真,最后根据机械模型设计Simulink-Adams联合仿真。 图1.ABBIRB 1200尺寸参数示意图ABBIRB 1200提供的两种型号广泛适用于各作业,且两者间零部件通用,两种型号的工作范围分别为700 mm 和 900 mm,大有效负载分别为 7 kg 和5 kg。 IRB 1200 能够在狭小空间内能发挥其工作范围与性能优势,具有全新的设计、小型化的体积、高效的性能、易于集成、便捷的接

  9. Ruby & Syslog & 自定义工具 - 2

    我是syslog的新手。我们决定使用系统日志来跟踪Rails应用程序中的一些特殊事件。问题是我不想使用默认的/var/log/system.log文件,而是使用自定义文件,例如/var/log/myapp_events.log.我看到我必须像这样在/etc/syslog.conf中定义我自己的设施:myapp_events.*/var/log/myapp_events.log重新启动syslogd后,我发现我可以直接在bash控制台中使用它:syslog-s-kFacilitymyapp_eventsMessage"thisismymessage"该消息按预期出现在/var/log/m

  10. ruby - 停止分布式 Ruby 服务 - 2

    我有一个启动DRb服务的脚本,然后生成处理程序对象并通过DRb.thread.join等待。我希望脚本一直运行直到被明确杀死,所以我添加了trap"INT"doDRb.stop_serviceend在Ruby1.8下成功停止DRb服务并退出,但在1.9下似乎死锁(在OSX10.6.7上)。对该进程进行采样显示在semaphore_wait_signal_trap中有几个线程在旋转。我假设我在调用stop_service时做错了什么,但我不确定是什么。谁能给我任何关于如何正确处理它的指示? 最佳答案 好的,我想我已经找到了解决方案。如

随机推荐