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

news2024/12/25 2:25:38

背景:

现有编码格式为业务常量+数字,每新增一条数据在基础上+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相关内容)

Redisson 支持单点模式、主从模式、哨兵模式、集群模式,这里以单点模式为例,redisson这个框架重度依赖了Lua脚本和Netty,代码很牛逼,各种Future及FutureListener的异步、同步操作转换。

测试

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

Redisson 分布式锁测试

测试

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

扩展知识

常见分布式锁方案对比

分类方案实现原理优点缺点
基于数据库基于mysql 表唯一索引1.表增加唯一索引
2.加锁:执行insert语句,若报错,则表明加锁失败
3.解锁:执行delete语句
完全利用DB现有能力,实现简单1.锁无超时自动失效机制,有死锁风险
2.不支持锁重入,不支持阻塞等待
3.操作数据库开销大,性能不高
基于MongoDB findAndModify原子操作1.加锁:执行findAndModify原子命令查找document,若不存在则新增
2.解锁:删除document
实现也很容易,较基于MySQL唯一索引的方案,性能要好很多1.大部分公司数据库用MySQL,可能缺乏相应的MongoDB运维、开发人员
2.锁无超时自动失效机制
基于分布式协调系统基于ZooKeeper1.加锁:在/lock目录下创建临时有序节点,判断创建的节点序号是否最小。若是,则表示获取到锁;否,则则watch /lock目录下序号比自身小的前一个节点
2.解锁:删除节点
1.由zk保障系统高可用
2.Curator框架已原生支持系列分布式锁命令,使用简单
需单独维护一套zk集群,维保成本高
基于缓存基于redis命令1. 加锁:执行setnx,若成功再执行expire添加过期时间
2. 解锁:执行delete命令
实现简单,相比数据库和分布式系统的实现,该方案最轻,性能最好1.setnx和expire分2步执行,非原子操作;若setnx执行成功,但expire执行失败,就可能出现死锁
2.delete命令存在误删除非当前线程持有的锁的可能
3.不支持阻塞等待、不可重入
基于redis Lua脚本能力1. 加锁:执行SET lock_name random_value EX seconds NX 命令

2. 解锁:执行Lua脚本,释放锁时验证random_value 
-- ARGV[1]为random_value,  KEYS[1]为lock_name

if redis.call("get", KEYS[1]) == ARGV[1] then

    return redis.call("del",KEYS[1])

else

    return 0

end

同上;实现逻辑上也更严谨,除了单点问题,生产环境采用用这种方案,问题也不大。不支持锁重入,不支持阻塞等待

表格中对比了几种常见的方案,redis+lua基本可应付工作中分布式锁的需求。然而,当偶然看到redisson分布式锁实现方案(传送门),相比以上方案,redisson保持了简单易用、支持锁重入、支持阻塞等待、Lua脚本原子操作,不禁佩服作者精巧的构思和高超的编码能力。下面就来学习下redisson这个牛逼框架,是怎么实现的。

分布式锁需满足四个条件

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

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。
  4. 具有容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。

redisson加锁&解锁Lua脚本

加锁、解锁Lua脚本是redisson分布式锁实现最重要的组成部分。首先不看代码,先研究下Lua脚本都是什么逻辑

1、加锁Lua脚本
  • 脚本入参
参数示例值含义
KEY个数1KEY个数
KEYS[1]my_first_lock_name锁名
ARGV[1]60000持有锁的有效时间:毫秒
ARGV[2]58c62432-bb74-4d14-8a00-9908cc8b828f:1唯一标识:获取锁时set的唯一值,实现上为redisson客户端ID(UUID)+线程ID
  • 脚本内容
 
  1. -- 若锁不存在:则新增锁,并设置锁重入计数为1、设置锁过期时间

  2. if (redis.call('exists', KEYS[1]) == 0) then

  3. redis.call('hset', KEYS[1], ARGV[2], 1);

  4. redis.call('pexpire', KEYS[1], ARGV[1]);

  5. return nil;

  6. end;

  7. -- 若锁存在,且唯一标识也匹配:则表明当前加锁请求为锁重入请求,故锁重入计数+1,并再次设置锁过期时间

  8. if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then

  9. redis.call('hincrby', KEYS[1], ARGV[2], 1);

  10. redis.call('pexpire', KEYS[1], ARGV[1]);

  11. return nil;

  12. end;

  13. -- 若锁存在,但唯一标识不匹配:表明锁是被其他线程占用,当前线程无权解他人的锁,直接返回锁剩余过期时间

  14. return redis.call('pttl', KEYS[1]);

  • 脚本解读

Q:返回nil、返回剩余过期时间有什么目的? 
A:当且仅当返回nil,才表示加锁成功;客户端需要感知加锁是否成功的结果

2、解锁Lua脚本
  • 脚本入参
参数示例值含义
KEY个数2KEY个数
KEYS[1]my_first_lock_name锁名
KEYS[2]redisson_lock__channel:{my_first_lock_name}解锁消息PubSub频道
ARGV[1]0redisson定义0表示解锁消息
ARGV[2]30000设置锁的过期时间;默认值30秒
ARGV[3]58c62432-bb74-4d14-8a00-9908cc8b828f:1唯一标识;同加锁流程
  • 脚本内容
 
  1. -- 若锁不存在:则直接广播解锁消息,并返回1

  2. if (redis.call('exists', KEYS[1]) == 0) then

  3. redis.call('publish', KEYS[2], ARGV[1]);

  4. return 1;

  5. end;

  6. -- 若锁存在,但唯一标识不匹配:则表明锁被其他线程占用,当前线程不允许解锁其他线程持有的锁

  7. if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then

  8. return nil;

  9. end;

  10. -- 若锁存在,且唯一标识匹配:则先将锁重入计数减1

  11. local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);

  12. if (counter > 0) then

  13. -- 锁重入计数减1后还大于0:表明当前线程持有的锁还有重入,不能进行锁删除操作,但可以友好地帮忙设置下过期时期

  14. redis.call('pexpire', KEYS[1], ARGV[2]);

  15. return 0;

  16. else

  17. -- 锁重入计数已为0:间接表明锁已释放了。直接删除掉锁,并广播解锁消息,去唤醒那些争抢过锁但还处于阻塞中的线程

  18. redis.call('del', KEYS[1]);

  19. redis.call('publish', KEYS[2], ARGV[1]);

  20. return 1;

  21. end;

  22. return nil;

  • 脚本解读

Q1:广播解锁消息有什么用? 
A:是为了通知其他争抢锁阻塞住的线程,从阻塞中解除,并再次去争抢锁。

Q2:返回值0、1、nil有什么不一样? 
A:当且仅当返回1,才表示当前请求真正触发了解锁Lua脚本;但客户端又并不关心解锁请求的返回值,好像没什么用?

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

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

相关文章

Vue - 3( 15000 字 Vue 入门级教程)

一:初识 Vue 1.1 收集表单数据 收集表单数据在Vue.js中是一个常见且重要的任务,它使得前端交互变得更加灵活和直观。 Vue中,我们通常使用v-model指令来实现表单元素与数据之间的双向绑定,从而实现数据的收集和更新。下面总结了…

Springboot引入swagger

讲在前面&#xff1a;在spring引入swagger时&#xff0c;由于使用的JDK、Spring、swagger 的版本不匹配&#xff0c;导致启动报错&#xff0c;一直存在版本依赖问题。所以在此声明清楚使用版本。JDK 1.8、Spring boot 2.6.13、 Swagger 2.9.2。 引入maven依赖 <dependency&…

【Canvas与艺术】绘制金色Brand Award品牌嘉奖奖章

【成果图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>金色Brand Award品牌嘉奖</title><style type"text/…

WebGL异步绘制多点

异步绘制线段 1.先画一个点 2.一秒钟后&#xff0c;在左下角画一个点 3.两秒钟后&#xff0c;我再画一条线段 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"…

Games101-几何(基本表示方法)

几何分类 几何分类&#xff1a;隐式几何和显示几何 隐式几何&#xff1a;不会告诉空间中的点具体在哪&#xff0c;告诉这些点满足的一定关系。 如球的描述 x 2 y 2 z 2 1 x^2 y^2 z^2 1 x2y2z21 缺点&#xff1a;这个面都有哪些点是不容易看出来的&#xff0c;从上述的…

[Apple Vision Pro]开源项目 Beautiful Things App Template

1. 技术框架概述&#xff1a; - Beautiful Things App Template是一个为visionOS设计的免费开源软件&#xff08;FOSS&#xff09;&#xff0c;用于展示3D模型画廊。 2. 定位&#xff1a; - 该模板作为Beautiful Things网站的延伸&#xff0c;旨在为Apple Vision Pro用户…

从300亿分子中筛出6款,结构新且易合成,斯坦福抗生素设计AI模型登Nature子刊

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了免费的人工智能中文站https://ai.weoknow.com 新建了收费的人工智能中文站https://ai.hzytsoft.cn/ 更多资源欢迎关注 全球每年有近 500 万人死于抗生素耐药性&#xff0c;因此迫切需要新的方法来对抗耐药菌株。 …

最具有影响力的三个视觉平台 | 3D高斯、场景重建、三维点云、工业3D视觉、SLAM、三维重建、自动驾驶

大家好&#xff0c;我是小柠檬 这里给大家推荐三个国内具有影响力的3D视觉方向平台&#xff01; 原文&#xff1a;最具有影响力的三个视觉平台 | 3D高斯、场景重建、三维点云、工业3D视觉、SLAM、三维重建、自动驾驶

青风环境带您了解2024第13届生物发酵展

参展企业介绍 浙江青风环境股份有限公司创立于1998年&#xff0c;是一家集科研、生产及贸易为一体的高新技术企业。公司座落于浙江省丽水市水阁工业区&#xff0c;占地面积120亩&#xff0c;建筑面积近11万平方米&#xff0c;年产值可达20亿元&#xff0c;建有标准的冷&#x…

【JAVASE】带你了解instanceof和equals的魅力

✅作者简介&#xff1a;大家好&#xff0c;我是橘橙黄又青&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;再无B&#xff5e;U&#xff5e;G-CSDN博客 1.instanceof instanceof 是 Java 的保留关键字。它的作用是测试…

编译原理实验3(基于算符优先文法分析的语法分析器 )

实验目的 加深对语法分析器工作过程的理解&#xff1b;加强对算符优先分析实现语法分析程序的掌握&#xff1b;能够产用一种编程语言实现简单的语法分析程序&#xff1b;能够使用自己编写的分析程序对简单的程序段进行语法分析。 实验要求 根据简单表达式文法构造算符优先分…

Retrofit2 完全解析 探索与okhttp之间的关系

//用于访问zhy的信息 http://192.168.1.102:8080/springmvc_users/user/zhy //用于访问lmj的信息 http://192.168.1.102:8080/springmvc_users/user/lmj 即通过不同的username访问不同用户的信息&#xff0c;返回数据为json字符串。 那么可以通过retrofit提供的PATH注解非…

自动驾驶汽车关键技术_感知

自动驾驶汽车关键技术|感知 附赠自动驾驶学习资料和量产经验&#xff1a;链接 两套标准 分别由美国交通部下属的国家高速路安全管理局(NationalHighwayTraffic Safety Administration &#xff0c;NHSTA) 和国际汽车工程师协会&#xff08;Societyof Automotive Engineers&am…

Linux grep和find命令常用类型

1. grep命令的使用。 查找文件中符合条件的字符串或正则表达式&#xff0c;然后将含有范本样式的那一列显示出来。若不指定任何文件名称&#xff0c;或是给的文件名为-&#xff0c;则gerp命令会从标准输入设备读取数据。 用于测试的文件目录结构如下&#xff1a; 1.1 在单个文…

软考中级之软件设计师---知识点汇总总结

软考中级之软件设计师---知识点汇总总结 软考介绍资格设置证书样本 计算机组成原理操作系统1. 进程的三态模型2. 磁盘调度算法 计算机网络1. 网络的分类2. 各层的互连设备3. 网络模型&#xff0c;协议簇4. 传输层协议TCP、UDP4.1 TCP (Transmission Control Protocol,传输控制协…

高压防触电警示牌是什么

在现代社会中&#xff0c;电力已成为人们生活中不可或缺的能源之一。随着电力行业发展&#xff0c;电力设施也不断增多&#xff0c;电力安全成为人们关注的一个重要话题。电力事故可能导致人员伤亡、财产损失甚至社会安全问题。因此&#xff0c;为了提醒人们注意远离带电设备&a…

vue前端项目到后端执行逻辑——自己改的话要怎么改

文章目录 vue前端项目到后端流程——自己改的话要怎么改 vue前端项目到后端流程——自己改的话要怎么改

07 Python进阶:多线程

python线程概念 在 Python 中&#xff0c;线程&#xff08;Thread&#xff09;是用于实现多任务并发执行的基本单元。线程允许程序同时执行多个部分&#xff0c;每个部分称为一个线程&#xff0c;因此能够提高程序的效率&#xff0c;特别适用于需要同时执行多个任务的情况。下面…

docker-compose安装dozzle

dozzle是一个docker日志的webui工具 安装配置 docker-compose.yaml version: "3" services:dozzle:container_name: dozzleimage: amir20/dozzle:v4.11.4volumes:- /var/run/docker.sock:/var/run/docker.sockrestart: unless-stoppedports:- 20342:8080networks:cu…

HarmonyOS4.0 ArkUI常用组件

一、Image 语法&#xff1a; Image(src:string|PixelMap|Resource)使用方式&#xff1a; string格式&#xff1a;用来加载网络图片&#xff0c;需要在module.json5中申请网络访问权限&#xff1a;ohos.permission.INTERNET Image("http://xxx.png")PixelMap格式&am…