首页 > 编程语言 >【Java业务需求解决方案】分布式锁应用详情,多种方案选择,轻松解决,手把手操作(非全数字编码依次加一问题)

【Java业务需求解决方案】分布式锁应用详情,多种方案选择,轻松解决,手把手操作(非全数字编码依次加一问题)

时间:2024-04-07 15:32:33浏览次数:47  
标签:加一 Redisson Java 过期 数字编码 redis 释放 线程 分布式

目录

背景:

解决方案:

分布式锁方案一(不建议,但原理得懂):Redis锁setnx与业务代码处理

雏形代码

产生问题一:锁释放问题

代码改造:锁添加过期时间

产生问题二:锁被别的线程误删

代码改造:添加setnx锁请求标识防勿删

产生问题三:递归容易造成内存溢出

代码改造:递归改造while循环

产生问题四:查询锁并且删除锁产生原子性问题

代码改造:Lua原子性操作

产生问题五:业务还没执行完,锁就过期了

代码改造:setnx 锁自动续期

终极版:java代码实现

总结:

会出现的问题

分布式方案二:开源框架:Redisson

Redisson 概述

官网介绍

入门整合

测试

Redisson 分布式锁测试


背景:

现有编码格式为业务常量+数字,每新增一条数据在基础上+1,比如:

          文件类型1                编码为ZS01
          文件类型1下文件1   编码为ZS0101
          文件类型1下文件2   编码为ZS0102
          文件类型2                编码为ZS02
          文件类型2下文件1   编码为ZS0201
          文件类型2下文件2   编码为ZS0202

解决方案:

使用mysql中count()函数与where条件,查询出条数充当最大值,再此基础上加1,生成编码,通过编码工具类实现格式统一,并使用redis分布式锁解决并发问题。

分布式锁方案一(不建议,但原理得懂):Redis锁setnx与业务代码处理

redis 的 setnx区别于普通set,他是 set key if not exist ,当一个key不存在的时候,可以设置成功。那么,我们就可以把 setnx 来设定某个key为一把锁,这个key存在的时候,则表示获得锁,那么请求无法操作共享资源,除非这个key不存在了,那就行。

第一次设置成功,第二次设置不成功,因为这个key没有释放,除非删除了,或者超时清除了,那么才可以。

从上面操作可以看得出来,这其实也是分布式锁的3个关键步骤,加锁设值,删除解锁,重试(死循环或者递归)
通过如下流程可以更好梳理思路:

雏形代码

产生问题一:锁释放问题

代码改造:锁添加过期时间

思考问题:
如果业务执行的过程抛出异常了,怎么办?锁会一直没释放。
如果当前运行这段代码的计算机节点突然停电了,代码正准备删除lock,这个时候咋办?锁也会一直存在。

提出的两个问题,其实我们要保证锁最终不管怎样都要释放,所以,我们可以为锁添加过期时间,如上图。
一旦后续发生故障,那么30秒后还是能释放锁。但是这个时候还是会有问题,程序正好运行到1.1还没来得及设置过期时间,拉电了,此时锁设置成功,但是没有设置过期时间,还是有问题,所以,要么全设置成功,原子性必须得保证。我们可以使用 setnx内置的,可以多加时间参数来设置。

产生问题二:锁被别的线程误删

代码改造:添加setnx锁请求标识防勿删

产生问题三:递归容易造成内存溢出

代码改造:递归改造while循环

目前所使用的递归方案,高并发时也容易造成内存溢出,那么其实可以改造一下,改为死循环即可只要获得锁失败,则返回去尝试获得锁即可

产生问题四:查询锁并且删除锁产生原子性问题

代码改造:Lua原子性操作

图中箭头处,当我们拿出锁后,并且判断也成功了,在这一刹那间,锁也可能正好失效吧。这个时候已经进入了判断内部了,所以会执行删除锁,但是这个时候因为锁恰好失效,所以其他请求就占有锁,那么自己在删除锁的时候,其实删除的是别人的锁,这样在极端的情况下其实也会出问题的。此时怎么办?

查询锁并且删除锁,这其实也是原子性操作,因为上一节课说了,这里也是可能会删除其他的锁的因为原子性保证不了。
所以接下来我们所需要做的,就是保证查询以及判断都是原子性的操作。这里就需要结合使用LUA脚本来解决这个问题
可以打开redis官网:https://redis.io/commands

解释:get命令获得key与参数比对,如果比对一致,则删除,否则返回0。这是一段脚本,是一个命令一起运行的,所以要比我们程序代码中的调用要来的更好,因为这是原子性操作。要么全成功,要么全失败。
在命令行可以通过eval命令来进行操作:

把上述脚本转换为一个字符串(大家可以直接复制)

 // 使用LUA脚本执行删除key操作,为了保证原子性
            String lockScript =
                    " if redis.call('get',KEYS[1]) == ARGV[1] "
                            + " then "
                            +   " return redis.call('del',KEYS[1]) "
                            + " else "
                            +   " return 0 "
                            + " end "
                    ;

在通过redis调用即可

产生问题五:业务还没执行完,锁就过期了

代码改造:setnx 锁自动续期

遗留问题思考:
我在这里设置了30秒,如果业务执行时间很长,需要35秒,这个时候还没等业务执行完毕就释放锁了,那么其他请求就会进来处理共享资源,那么锁其实就失效了,没起到作用了。而且在第个请求执行到第35秒的时候,会被第一个请求的del给删除锁,这个时候完全乱套了,各自没有删除自己的锁而是删的其他请求的锁,整个都乱了,怎么办?前面我们设置了超时时间,但是如果真的业务执行很耗时,超时了,那么我们应该给他自动续期啊开启(fork)一个子线程,定时检查,如果lock还在,则在超时时间重置,如此循环,直到业务完成后删除锁。(或者使用while死循环也行)
LUA脚本:

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

        String refreshScript =
                " if redis.call('get',KEYS[1]) == ARGV[1] "
                        + " then "
                        +   " return redis.call('expire',KEYS[1],30) "
                        + " else "
                        +   " return 0 "
                        + " end "
                ;

终极版:java代码实现

那么执行过程中,会经历几次续期,结束了,就释放timer。

 @Transactional
   @Override
    public void modifyCompanyInfo3(ModifyCompanyInfoBO companyInfoBO, Integer num) throws Exception {

        String distLock = "redis-lock";
        String selfId = UUID.randomUUID().toString();
        Integer expireTimes = 30;

        while (redis.setnx(distLock, selfId, expireTimes)) {
            // 如果加锁失败,则重试循环
            System.out.println("setnx 锁生效中,一会重试~");
            Thread.sleep(50);
        }

        // 一旦获得锁,则开启新的timer执行定期检查,做lock的自动续期
        autoRefreshLockTimes(distLock, selfId, expireTimes);

        try {
            System.out.println("获得锁,执行业务~");
            // 加锁成功,执行业务
            Thread.sleep(40000);
            this.doModify(companyInfoBO);
        } finally {
            // 业务执行完毕,释放锁
//            String selfIdLock = redis.get(distLock);
//            if ( StringUtils.isNotBlank(selfIdLock) && selfIdLock.equals(selfId)) {
//                redis.del(distLock);
//            }

            // 使用LUA脚本执行删除key操作,为了保证原子性
            String lockScript =
                    " if redis.call('get',KEYS[1]) == ARGV[1] "
                            + " then "
                            +   " return redis.call('del',KEYS[1]) "
                            + " else "
                            +   " return 0 "
                            + " end "
                    ;
            long unLockResult = redis.execLuaScript(lockScript, distLock, selfId);
            if (unLockResult == 1) {
                lockTimer.cancel();
                System.out.println("释放锁,并且取消timer~");
            }
        }
    }

    private Timer lockTimer = new Timer();

    // 自动续期
    private void autoRefreshLockTimes(String distLock, String selfId, Integer expireTimes) {

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

        String refreshScript =
                " if redis.call('get',KEYS[1]) == ARGV[1] "
                        + " then "
                        +   " return redis.call('expire',KEYS[1],30) "
                        + " else "
                        +   " return 0 "
                        + " end "
                ;
        lockTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("自动续期,重置到30秒");
                redis.execLuaScript(refreshScript, distLock, selfId);
            }
        },
        expireTimes/3*1000,
        expireTimes/3*1000);
    }
   private void doModify(ModifyCompanyInfoBO companyInfoBO) {

       //业务代码
    }

总结:

会出现的问题

这种方案能解决方案一的原子性问题,但是依然会存在很大的问题,如下所示:
1、时钟不同步:如果不同的节点的系统时钟不同步,可能导致锁的过期时间计算不准确。
解决方案:使用相对时间而非绝对时间,或者使用时钟同步工具确保系统时钟同步。
2、死锁:在某些情况下,可能出现死锁,例如由于网络问题导致锁的释放操作未能执行。
解决方案:使用带有超时和重试的锁获取和释放机制,确保在一定时间内能够正常操作。
3、锁过期与业务未完成:如果业务逻辑执行时间超过了设置的过期时间,锁可能在业务未完成时自动过期,导致其他客户端获取到锁。
解决方案:可以设置更长的过期时间,确保业务有足够的时间完成。或者在业务未完成时,通过更新锁的过期时间来延长锁的生命周期。
4、锁的争用:多个客户端同时尝试获取锁,可能导致锁的频繁争用。
解决方案:可以使用带有重试机制的获取锁操作,或者采用更复杂的锁实现,如 Redlock 算法。
5、锁的释放问题:客户端获取锁后发生异常或未能正常释放锁,可能导致其他客户端无法获取锁。
6、锁被别的线程误删:假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完。

分布式方案二:开源框架:Redisson

Redisson 概述

总结一下上面的解决问题的历程和问题,用SETNX+EXPIRE可以解决分布式锁的问题,但是这种方式不是原子性操作。因此,在提出的有关原子性操作解决方法,但是依然会出现几个问题,在会出现的问题中简单罗列了几种问题与解决方法,其中一个问题中有锁过期与业务未完成有一个系统的解决方案,即接下来介绍的Redison。
Redisson 是一个基于 Redis 的 Java 驱动库,提供了分布式、高性能的 Java 对象操作服务,这里只探讨分布式锁的原理:

只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了锁过期释放,业务没执行完问题。

Watchdog 定期续期锁:
当客户端成功获取锁后,Redisson 启动一个 Watchdog 线程,该线程会定期(通常是锁过期时间的一半)检查锁是否过期,并在过期前对锁进行续期。
Watchdog 使用 Lua 脚本确保原子性:
为了确保 Watchdog 操作的原子性,Redisson 使用 Lua 脚本执行 Watchdog 操作。这样在 Watchdog 检查和续期锁的过程中,可以保证整个操作是原子的,防止出现竞争条件。
Watchdog 续期锁的过期时间:
Watchdog 线程会通过使用 PEXPIRE 或者 EXPIRE 命令来续期锁的过期时间。这样在业务未完成时,锁的过期时间会不断延长,直到业务完成释放锁。

Redisson 是 java 的 Redis 客户端之一,是 Redis 官网推荐的 java 语言实现分布式锁的项目。

Redisson 提供了一些 api 方便操作 Redis。因为本文主要以锁为主,所以接下来我们主要关注锁相关的类,以下是 Redisson 中提供的多样化的锁:

  • 可重入锁(Reentrant Lock)
  • 公平锁(Fair Lock)
  • 联锁(MultiLock)
  • 红锁(RedLock)
  • 读写锁(ReadWriteLock)
  • 信号量(Semaphore) 等等

总之,管你了解不了解,反正 Redisson 就是提供了一堆锁… 也是目前大部分公司使用 Redis 分布式锁最常用的一种方式。

本文中 Redisson 分布式锁的实现是基于 RLock 接口,而 RLock 锁接口实现源码主要是 RedissonLock 这个类,而源码中加锁、释放锁等操作都是使用 Lua 脚本来完成的,并且封装的非常完善,开箱即用。

接下来主要以 Redisson 实现 RLock 可重入锁为主。

源码地址:GitHub - niceyoo/redis-redlock: redis分布式锁之redlock应用篇

官网介绍

入门整合

和Jedis以及RedisTemplate-样,Redisson其实也是redis的一个客户端
Redisson里面封装了很多有用的api和功能实现,非常实用,当然也包含了分布式锁。Jedis这样的客户端仅仅只是把提供了客户端调用,很多功能其实需要自己去实现封装的。Redisson所提供的是实用redis最简单最便捷的方法,Redisson的宗旨也是让我们使用者关注业务本身,而不是要更关注redis,要把redis这块分离,使得我们的精力更加集中于业务上。
Redisson内部结合实用了LUA脚本实现了分布式锁,并且可以对其做到续约释放等各项功能,非常完善。当然也包含了gc里面的一些锁,JC里面的只能在本地实现,集群分布式下则失效,如果要使用则可以使用Redisson提供的工具来实现锁就行了。

上面的代码其实就是设计为可重入锁,不多整述,简单来讲,就是方法运行,可以多次使用同一把锁。或者说一个线程在不释放的情况下可以获得锁多次,不过在释放的时候也需要释放多次。(有兴趣课后建议去学习一下gc相关内容)

测试

apipost测试接口最终结果的顺序即可

Redisson 分布式锁测试

测试

1.拔电源测试会否解锁
2.自动续期测试(看门狗)
3.lock设置自定义时间,比如15秒,超时是否自动续期(无看门狗)
4. 测试可重入锁(用同一把锁):重入2次,释放2次

标签:加一,Redisson,Java,过期,数字编码,redis,释放,线程,分布式
From: https://blog.csdn.net/qq_50446805/article/details/137351690

相关文章

  • 1688详情API接口:解锁多元化应用场景java php c++
    随着互联网的快速发展,数据交换和信息共享已成为企业日常运营不可或缺的一部分。在这样的背景下,API(应用程序接口)接口作为实现数据互通的重要工具,受到了越来越多企业的青睐。1688详情API接口作为阿里巴巴旗下的重要接口之一,为企业提供了丰富多元的应用场景,助力企业高效推广一、1688......
  • Java入门基础知识第五课(超基础,超仔细)——选择结构
    今天主要讲一下if选择结构以及如何利用Math.random()来获取随机数。流程控制:用来控制代码的执行顺序  顺序结构:代码从上往下按照顺序依次执行选择结构:根据条件选择性的执行某部分代码循环结构:反复执行一段代码一、if选择结构1、单分支if选择结构         语......
  • (毕设)基于Java+Vue+Mysql的WMS仓库管理系统
      前言: WMS(WarehouseManagementSystem)仓库管理系统是一个用于优化仓库操作、提高效率和准确性的软件解决方案。以下是针对列出的WMS仓库管理系统的各个部分的简要描述:1.订单管理订单管理是WMS的核心功能之一,涉及处理、跟踪和完成客户订单。这包括:订单录入:手动或自......
  • 零基础轻松入门Java数据库连接(JDBC)
    什么是JDBC?Java数据库连接(JDBC)就像是Java程序和数据库之间的翻译官。它是一个官方的标准接口集,让Java程序能和不同的数据库“对话”。甭管你是在跟MySQL、Oracle还是SQLServer打交道,只要有了JDBC,Java就能用统一的方式去执行查询、更新数据库的操作。为什么我们需要JDBC?......
  • JavaScript中,...(三个点)是扩展运算符
    在JavaScript中,...(三个点)是扩展运算符(SpreadOperator)和剩余参数(RestParameters)的语法。它确实可以用来“展开”对象的属性或数组的元素。展开对象的属性对于对象,扩展运算符可以用来将一个对象的所有可枚举属性复制到新对象中,或者与现有的对象属性合并。javascript复制代码......
  • Java中的继承
    一、继承1.1为什么需要继承在写代码过程中两个类之间可能存在大量重复的代码,如何把这些重复的代码提取出来简化代码呢?面向对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用。1.2继承的概念 继承(inheritance):是面向对象程序设计使代码可以复用的最重要......
  • 5G网络建设【华为OD机试】(JAVA&Python&C++&JS题解)
    一.题目-5G网络建设现需要在某城市进行5G网络建设,已经选取N个地点设置5G基站,编号固定为1到N,接下来需要各个基站之间使用光纤进行连接以确保基站能互联互通,不同基站之间架设光纤的成本各不相同,且有些节点之间已经存在光纤相连,请你设计算法,计算出能联通这些基站的最小成本是......
  • 项目排期【华为OD机试】(JAVA&Python&C++&JS题解)
    一.题目项目组共有N个开发人员,项目经理接到了M个独立的需求,每个需求的工作量不同,且每个需求只能由一个开发人员独立完成,不能多人合作。假定各个需求直接无任何先后依赖关系,请设计算法帮助项目经理进行工作安排,使整个项目能用最少的时间交付。输入描述:第一行输入为M个需......
  • 找城市【华为OD机试】(JAVA&Python&C++&JS题解)
    一.题目-找城市一张地图上有n个城市,城市和城市之间有且只有一条道路相连:要么直接相连,要么通过其它城市中转相连(可中转一次或多次)。城市与城市之间的道路都不会成环。当切断通往某个城市i的所有道路后,地图上将分为多个连通的城市群,设该城市i的聚集度为DPi(DegreeofP......
  • 电脑病毒感染【华为OD机试】(JAVA&Python&C++&JS题解)
    一.题目-电脑病毒感染一个局域网内有很多台电脑,分别标注为0-N-1的数字。相连接的电脑距离不一样,所以感染时间不一样,感染时间用t表示。其中网络内一个电脑被病毒感染,其感染网络内所有的电脑需要最少需要多长时间。如果最后有电脑不会感染,则返回-1给定一个数组times表示......