Redisson 源码解析 - 分布式锁实现过程
一、Redisson 分布式锁源码解析
Redisson
是架设在Redis
基础上的一个Java
驻内存数据网格。在基于NIO
的Netty
框架上,充分的利用了Redis
键值数据库提供的一系列优势,在Java
实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。
其中比较具体特色的就是 Redisson
对分布式锁的支持,不仅简化了分布式锁的应用过程还支持 Fair Lock、MultiLock、RedLock、ReadWriteLock
等锁的实现。本文就 Redisson
分布式锁的加锁和解锁过程的源码进行大致的解析。
下面是Redisson
源码地址:
https://github.com/redisson/redisson
如果对 Redisson
的使用还不了解的小伙伴可以先看下下面这篇文章:
https://xiaobichao.blog.csdn.net/article/details/112726748
Redisson
中的分布式锁在使用起来非常简便,例如:
public class TestLock {@ResourceRedissonClient redissonClient;@Testpublic void test() {RLock lock = null;try {// 获取可重入锁lock = redissonClient.getLock("redislock");// 获取锁,如果获取不到会等待lock.lock();Thread.sleep(30000000);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {if (lock != null) {// 释放锁lock.unlock();}}}@Testpublic void test1() {RLock lock = null;try {// 获取可重入锁lock = redissonClient.getLock("redislock");// 尝试获取锁,返回获取锁的状态Boolean isLock = lock.tryLock();Thread.sleep(30000000);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {if (lock != null) {// 释放锁lock.unlock();}}}
}
下面分别从 lock
、tryLock
、unlock
、三个地方进行源码的解析。
二、lock 获取锁和看门狗机制
先看下 redissonClient.getLock
方法,它默认创建了一个 RedissonLock
对象,并将锁的key
传递进来:
而 RedissonLock
对象又继承至RedissonBaseLock
类:
因此我们下面大多的源码分析都基于这两个类进行。
首先进到 RedissonLock
类下的 lock()
方法中:
这里主要又调用了 lock(long leaseTime, TimeUnit unit, boolean interruptibly)
方法,注意如果没有指定过期时间默认为 -1
,下面看到 lock(long leaseTime, TimeUnit unit, boolean interruptibly)
方法中:
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {// 当前线程IDlong threadId = Thread.currentThread().getId();// 尝试获取锁,如果已经有锁的话返回锁的剩余时间Long ttl = tryAcquire(-1, leaseTime, unit, threadId);// 获取锁成功if (ttl == null) {return;}// 如果获取锁失败,订阅当前线程,以便后续获取锁时得到通知。CompletableFuture<RedissonLockEntry> future = subscribe(threadId);//设置超时处理,当订阅的future完成时,触发超时处理。pubSub.timeout(future);//定义一个RedissonLockEntry对象,用于表示当前线程在分布式锁中的状态。RedissonLockEntry entry;if (interruptibly) {// 可中断entry = commandExecutor.getInterrupted(future);} else {entry = commandExecutor.get(future);}try {// 循环尝试获取锁while (true) {// 尝试获取锁ttl = tryAcquire(-1, leaseTime, unit, threadId);// 获取锁成功if (ttl == null) {break;}// 如果已经存在锁的过期时间大于等于0,需要等待通知if (ttl >= 0) {try {// 通过Semaphore 的 tryAcquire方法等待指定时间entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {if (interruptibly) {throw e;}entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);}} else { //如果剩余时间小于0,就一直等待。if (interruptibly) {entry.getLatch().acquire();} else {entry.getLatch().acquireUninterruptibly();}}}} finally {// 无论加锁成功或失败,都取消订阅unsubscribe(entry, threadId);}
}
代码中加了注释,这里我总结下,首先调用 tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId)
方法尝试获取锁,如果锁存在的话则返回过期时间,为 null
的话表示获取锁成功。如果获取锁失败,则将自己加入到订阅中,然后开启一个死循环,在循环中再次尝试获取锁,如果还是没有获取到的话则使用 Semaphore
的 tryAcquire
方法阻塞当前线程,如果其他线程释放了锁,则这里继续循环再次尝试获取锁。
下面主要看下 tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId)
尝试获取锁的逻辑,看到该方法下:
tryAcquire
方法又调用了 tryAcquireAsync0
方法,然后又主要调用了 tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId)
方法,下面主要看到这个方法下:
private RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture<Long> ttlRemainingFuture;if (leaseTime > 0) {//如果指定了锁持有时间,则根据指定的时间设置 key 的过期时间ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {// 没指定,默认锁持有 30sttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}// 执行 lua 操作CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);ttlRemainingFuture = new CompletableFutureWrapper<>(s);CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {// 如果加锁成功if (ttlRemaining == null) {if (leaseTime > 0) {internalLockLeaseTime = unit.toMillis(leaseTime);} else {// 没指定的话// 启动看门狗,延长锁持有时间scheduleExpirationRenewal(threadId);}}// 返回锁的过期时间return ttlRemaining;});return new CompletableFutureWrapper<>(f);
}
这里其中 tryLockInnerAsync
方法主要是指定了 Lua
脚本,主要注意的是如果没有指定了锁的过期则默认为 30s
的时间,然后在 Lua
脚本执行后,同样的判断,如果获取到锁的话并且没有指定锁的过期时间则开启看门狗机制,为锁延长时间续命的操作。
这里先看下核心操作 tryLockInnerAsync
方法中 Lua
脚本:
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {// lua 脚本return evalWriteSyncedAsync(getRawName(), LongCodec.INSTANCE, command,// 如果锁不存在,或者哈希表中锁对应的线程ID存在的话"if ((redis.call('exists', KEYS[1]) == 0) " +"or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " +// 对hash中的内容值 +1"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +// 设置过期时间"redis.call('pexpire', KEYS[1], ARGV[1]); " +//表示脚本执行成功,且不需要返回特定的值。"return nil; " +"end; " +// 如果if条件不满足,返回剩余过期时间(以毫秒为单位)"return redis.call('pttl', KEYS[1]);",// 对应这 lua 脚本中的参数,第一个参数就是 KEYS[1],以此类推Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
这里主要利于 Lua
的原子性将整个判断操作过程给原子化了,其中这里锁的结构是以 hash
的形式存放的,key
为锁的名称,hash
中的key
为线程ID
(UUID
+线程ID
的形式),因为分布式情况下线程ID
也有可能重复,value
为数字表示锁重入的次数, lua
脚本如果执行加锁逻辑成功则返回 null
,否则返回锁的过期时间,也就对应前面获取锁的时候判断的依据。
下面回到上面的 tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId)
方法中,在 ttlRemainingFuture.thenApply
中如果获取锁成功,并且没有指定锁的过期时间则会开启看门狗机制为锁进行续命操作,主要调用的是 scheduleExpirationRenewal(long threadId)
方法,下面看到该方法下的逻辑:
protected void scheduleExpirationRenewal(long threadId) {ExpirationEntry entry = new ExpirationEntry();// 加入看门狗记录中ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);// 如果已经存在if (oldEntry != null) {// 重新指定线程IDoldEntry.addThreadId(threadId);} else { // 如果不存在的话就开启看门狗entry.addThreadId(threadId);try {// 启动看门狗renewExpiration();} finally {// 如果线程已经终止,则关闭看门狗if (Thread.currentThread().isInterrupted()) {cancelExpirationRenewal(threadId);}}}
}
主要的逻辑在 renewExpiration()
方法下,继续看到该方法中:
private void renewExpiration() {// 获取当前信息ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;}// 执行计时任务Timeout task = getServiceManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {//再次获取信息ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ent == null) {return;}// 获取线程IDLong threadId = ent.getFirstThreadId();if (threadId == null) {return;}// 延长锁的过期时间CompletionStage<Boolean> future = renewExpirationAsync(threadId);future.whenComplete((res, e) -> {if (e != null) { //如果有异常删除该任务log.error("Can't update lock {} expiration", getRawName(), e);EXPIRATION_RENEWAL_MAP.remove(getEntryName());return;}if (res) { // 如果执行成功// 递归继续执行renewExpiration();} else { // 执行失败// 关闭看门狗cancelExpirationRenewal(null);}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);
}
这里主要通过递归延时任务的方式实现循环执行的效果,其中延时的时间为 internalLockLeaseTime
的三分之一,也就是默认 10s
触发一次,在任务中主要通过 renewExpirationAsync(long threadId)
方法,对锁进行了延时续命操作,看到该方法中:
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {// lua 脚本return evalWriteSyncedAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,// 如果锁和线程ID存在"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +// 重置过期时间"redis.call('pexpire', KEYS[1], ARGV[1]); " +// 成功返回 1"return 1; " +"end; " +// 失败返回 0"return 0;",// lua 脚本中对应的参数Collections.singletonList(getRawName()),internalLockLeaseTime, getLockName(threadId));
}
这里还是依靠 Lua
脚本的方式,如果锁存在的话就重置过期时间,达到续命的效果。
三、tryLock 获取锁
tryLock
和lock
是两种获取分布式锁的方法,它们的主要区别在于获取锁的方式和阻塞行为。tryLock
默认是一种非阻塞的获取锁的方法,也可以通过设置 waitTime
变成阻塞的。而lock
默认就是一种阻塞的获取锁的方法。
他们俩的最终处理逻辑都是一样的,只不过默认的 tryLock
没有订阅阻塞的操作。
下面看下默认的 tryLock
的操作 ,进到 RedissonLock
下的 tryLock()
中:
再进入 tryLockAsync()
方法中:
这里调用了 tryLockAsync
方法,并将当前线程的ID
传递了进来,继续看到 tryLockAsync
方法中:
在看到 tryAcquireOnceAsync
方法中,注意这里的等待时间和上面 lock()
默认一样,是 -1
:
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {CompletionStage<Boolean> acquiredFuture;if (leaseTime > 0) {//如果指定了锁持有时间,则根据指定的时间设置 key 的过期时间acquiredFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);} else {// 没指定,默认锁持有 30sacquiredFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);}// 执行 lua 操作acquiredFuture = handleNoSync(threadId, acquiredFuture);CompletionStage<Boolean> f = acquiredFuture.thenApply(acquired -> {// 如果加锁成功if (acquired) {// 如果指定了锁持有时间if (leaseTime > 0) {internalLockLeaseTime = unit.toMillis(leaseTime);} else { // 没指定的话,// 看门狗,延长锁持有时间scheduleExpirationRenewal(threadId);}}// 返回获取锁的状态return acquired;});return new CompletableFutureWrapper<>(f);
}
这里的逻辑相比于前面 lock()
的逻辑就差不多了,只不过缺少了订阅和阻塞等待重试的操作,再下面的操作和lock()
的逻辑是一致的。
四、unlock 解锁和关闭看门狗
解锁的逻辑看到 RedissonBaseLock
下的 unlock()
方法中:
继续看到 unlockAsync
方法中:
主要逻辑在 unlockAsync0
方法中:
private RFuture<Void> unlockAsync0(long threadId) {// 解锁CompletionStage<Boolean> future = unlockInnerAsync(threadId);CompletionStage<Void> f = future.handle((opStatus, e) -> {// 关闭看门狗cancelExpirationRenewal(threadId);if (e != null) { // 如果执行有异常if (e instanceof CompletionException) {throw (CompletionException) e;}throw new CompletionException(e);}if (opStatus == null) { // 如果结果为空的话,表示锁不存在IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "+ id + " thread-id: " + threadId);throw new CompletionException(cause);}return null;});return new CompletableFutureWrapper<>(f);
}
主要做了两件事,解锁和关闭看门狗,先看下 unlockInnerAsync(long threadId)
方法解锁的过程:
protected final RFuture<Boolean> unlockInnerAsync(long threadId) {String id = getServiceManager().generateId();MasterSlaveServersConfig config = getServiceManager().getConfig();int timeout = (config.getTimeout() + config.getRetryInterval()) * config.getRetryAttempts();timeout = Math.max(timeout, 1);// 解锁RFuture<Boolean> r = unlockInnerAsync(threadId, id, timeout);CompletionStage<Boolean> ff = r.thenApply(v -> {CommandAsyncExecutor ce = commandExecutor;if (ce instanceof CommandBatchService) {ce = new CommandBatchService(commandExecutor);}ce.writeAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.DEL, getUnlockLatchName(id));if (ce instanceof CommandBatchService) {((CommandBatchService) ce).executeAsync();}// 释放锁的结果return v;});return new CompletableFutureWrapper<>(ff);
}
这里的重点主要关注 unlockInnerAsync
方法,通过使用 Lua 脚本进行解锁的操作:
protected RFuture<Boolean> unlockInnerAsync(long threadId, String requestId, int timeout) {// lua 脚本return evalWriteSyncedAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,// 从Redis中获取锁的状态。"local val = redis.call('get', KEYS[3]); " +//如果不是false"if val ~= false then " +//将其转换为数字并返回,也就是 true 返回 1"return tonumber(val);" +"end; " +// 如果哈希表锁中不存在线程ID,表示锁已经被释放,返回nil。"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +"return nil;" +"end; " +//对锁中的线程ID的值减1,并将结果存储在 counter 变量中。这是一个计数器的操作。"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +//如果计数器值大于0,表示锁仍然被持有。"if (counter > 0) then " +// 更新哈希表锁的过期时间。"redis.call('pexpire', KEYS[1], ARGV[2]); " +// 设置键锁的状态值为0,并设置过期时间,表示锁仍然被持有。"redis.call('set', KEYS[3], 0, 'px', ARGV[5]); " +//返回0,表示锁仍然被持有"return 0; " +"else " + //如果计数器值不大于0,表示锁即将被释放。//删除锁"redis.call('del', KEYS[1]); " +"redis.call(ARGV[4], KEYS[2], ARGV[1]); " +// 设置键锁的状态值为1,并设置过期时间,表示锁已经被释放。"redis.call('set', KEYS[3], 1, 'px', ARGV[5]); " +//返回1,表示锁已经被释放"return 1; " +"end; ",Arrays.asList(getRawName(), getChannelName(), getUnlockLatchName(requestId)),LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime,getLockName(threadId), getSubscribeService().getPublishCommand(), timeout);
}
需要注意的是,在 Lua
脚本中,如果锁还存在的话,就对 hash
中的 value
减一,如果此时 value
结果还大于 0
的话,则表示这是重入锁的场景,此时不能直接删除锁,而是对重入的次数进行减一,并且要重置过期时间。
下面再回到 unlockAsync0(long threadId)
方法中,释放锁通过 Lua
脚本实现了,下面看下 cancelExpirationRenewal(Long threadId)
关闭看门狗的操作:
protected void cancelExpirationRenewal(Long threadId) {// 从记录中获取信息ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (task == null) {return;}if (threadId != null) {// 移除线程IDtask.removeThreadId(threadId);}if (threadId == null || task.hasNoThreads()) {// 关闭计时任务Timeout timeout = task.getTimeout();if (timeout != null) {timeout.cancel();}// 从缓存记录中删除EXPIRATION_RENEWAL_MAP.remove(getEntryName());}
}
这里就比较好理解了,停止计时任务,从缓存记录中移除。
相关文章:

Redisson 源码解析 - 分布式锁实现过程
一、Redisson 分布式锁源码解析 Redisson是架设在Redis基础上的一个Java驻内存数据网格。在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常…...

玩转贝启科技BQ3588C开源鸿蒙系统开发板 —— 开发板详情与规格
本文主要参考: BQ3588C_开发板详情-开源鸿蒙技术交流-Bearkey-开源社区 BQ3588C_开发板规格-开源鸿蒙技术交流-Bearkey-开源社区 厦门贝启科技有限公司-Bearkey-官网 1. 开发板详情 RK3588 核心板是一款由贝启科技自主研发的基于瑞芯微 RK3588 AI 芯片的智能核心…...

Qt pro文件
1. 项目通常结构 2.pri文件 pri文件可定义通用的宏,例如创建一个COMMON.pri文件内容为 COMMON_PATH D:\MyData 然后其它pri或者pro文件如APPTemplate.pro文件中通过添加include(Common.pri) ,QtCreator就会自动在项目结构树里面创建对应的节点 3.变量…...

实验笔记之——服务器链接
最近需要做NeRF相关的开发,需要用到GPU,本博文记录本人配置服务器远程链接的过程,本博文仅供本人学习记录用~ 连上服务器 首先先确保环境是HKU的网络环境(HKU AnyConnect也可)。伙伴已经帮忙创建好用户(第一次登录会提示重新设置密码)。用cmd ssh链接ssh -p 60001 <u…...

微服务-java spi 与 dubbo spi
Java SPI 通过一个案例来看SPI public interface DemoSPI {void echo(); } public class FirstImpl implements DemoSPI{Overridepublic void echo() {System.out.println("first echo");} } public class SecondImpl implements DemoSPI{Overridepublic void ech…...

redis复习笔记03(小滴课堂)
Redis6常见数据结构概览 0代表存在,1代表不存在。 1表示删除成功,0表示失败。 查看类型,默认string类型。 也可以设置set类型。 list类型。 查看key的过期时间: Redis6数据结构之String类型介绍和应用场景 批量设置: …...

【Spring Cloud】关于Nacos配置管理的详解介绍
🎉🎉欢迎来到我的CSDN主页!🎉🎉 🏅我是Java方文山,一个在CSDN分享笔记的博主。📚📚 🌟推荐给大家我的专栏《Spring Cloud》。🎯🎯 &am…...

基于Java SSM框架实现校园网络维修系统项目【项目源码】
基于java的SSM框架实现校园网络维修系统演示 java简介 Java主要采用CORBA技术和安全模型,可以在互联网应用的数据保护。它还提供了对EJB(Enterprise JavaBeans)的全面支持,java servlet API,JSP(java serve…...

项目框架构建之3:Nuget服务器的搭建
本文是“项目框架构建”系列之3,本文介绍一下Nuget服务器的搭建,这是一项简单的工作,您或许早已会了。 1.打开vs2022创建Asp.net Web应用程序 框架选择.net framework4.8,因为nuget服务器只支持.net framework。 2.选择空项目和保…...

外包干了1个月,技术退步一大半。。。
先说一下自己的情况,本科生,19年通过校招进入广州某软件公司,干了接近4年的功能测试,今年年初,感觉自己不能够在这样下去了,长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试…...
167. 木棒(dfs剪枝,经典题)
167. 木棒 - AcWing题库 乔治拿来一组等长的木棒,将它们随机地砍断,使得每一节木棍的长度都不超过 50 个长度单位。 然后他又想把这些木棍恢复到为裁截前的状态,但忘记了初始时有多少木棒以及木棒的初始长度。 请你设计一个程序࿰…...

用HTML的原生语法实现两个div子元素在同一行中排列
代码如下: <div id"level1" style"display: flex;"><div id"level2-1" style"display: inline-block; padding: 10px; border: 1px solid #ccc; margin: 5px;">这是第一个元素。</div><div id"…...

C++进阶--map和set的介绍及使用
map和set的介绍及使用 一、关联式容器与键值对关联式容器键值对pair树形结构的关联式容器 二、set2.1 set的介绍2.2 set的使用2.2.1 set的模板参数列表2.2.2 set的构造2.2.3 set的迭代器2.2.4 set的容量2.2.5 set修改操作2.2.6 set的使用举例 三、multiset3.1 multiset的介绍3.…...

MIML-DA
图3呢?且作者未提供代码...

[ROS2 Foxy]#1.3 安装使用 turtlesim
官网教程: https://docs.ros.org/en/foxy/Tutorials/Turtlesim/Introducing-Turtlesim.html 1.turtlesim安装和使用 turtlesim是一个轻量级的模拟程序,用来学习ROS2 .通过turtlesim来介绍ROS2在一个基础的水平上都要做了那些事,以此让我们了解将来在真的 robot或者模拟器上使用…...

嵌入式培训机构四个月实训课程笔记(完整版)-Linux系统编程第三天-Linux进程(物联技术666)
更多配套资料CSDN地址:点赞+关注,功德无量。更多配套资料,欢迎私信。 物联技术666_嵌入式C语言开发,嵌入式硬件,嵌入式培训笔记-CSDN博客物联技术666擅长嵌入式C语言开发,嵌入式硬件,嵌入式培训笔记,等方面的知识,物联技术666关注机器学习,arm开发,物联网,嵌入式硬件,单片机…...

1-01初识C语言
一、概述 C语言是贝尔实验室的Ken Thompson(肯汤普逊)、Dennis Ritchie(丹尼斯里奇)等人开发的UNIX 操作系统的“副产品”,诞生于1970年代初。 Thompson和Ritchie共同创作完成了Unix操作系统,他们都被称为…...
Python字符串
定义字符串 Python中要定义一个字符串,有比较多的一种方式。 示例代码: s "你好,张大鹏" print(s, type(s))s 你好,张大鹏 print(s, type(s))s """你好,张大鹏""" prin…...

PHP 基础编程 1
文章目录 前后端交互尝试php简介php版本php 基础语法php的变量前后端交互 - 计算器体验php数据类型php的常量和变量的区别php的运算符算数运算符自增自减比较运算符赋值运算符逻辑运算 php的控制结构ifelseelse if 前后端交互尝试 前端编程语言:JS (Java…...

Android studio BottomNavigationView 应用设计
一、新建Bottom Navigation Activity项目: 二、修改bottom_nav_menu.xml: <itemandroid:id="@+id/navigation_beijing"android:icon="@drawable/ic_beijing_24dp"android:title="@string/title_beijing" /><itemandroid:id="@+i…...
java_网络服务相关_gateway_nacos_feign区别联系
1. spring-cloud-starter-gateway 作用:作为微服务架构的网关,统一入口,处理所有外部请求。 核心能力: 路由转发(基于路径、服务名等)过滤器(鉴权、限流、日志、Header 处理)支持负…...

Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)
概述 在 Swift 开发语言中,各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过,在涉及到多个子类派生于基类进行多态模拟的场景下,…...

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...
大语言模型如何处理长文本?常用文本分割技术详解
为什么需要文本分割? 引言:为什么需要文本分割?一、基础文本分割方法1. 按段落分割(Paragraph Splitting)2. 按句子分割(Sentence Splitting)二、高级文本分割策略3. 重叠分割(Sliding Window)4. 递归分割(Recursive Splitting)三、生产级工具推荐5. 使用LangChain的…...

React19源码系列之 事件插件系统
事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...
反射获取方法和属性
Java反射获取方法 在Java中,反射(Reflection)是一种强大的机制,允许程序在运行时访问和操作类的内部属性和方法。通过反射,可以动态地创建对象、调用方法、改变属性值,这在很多Java框架中如Spring和Hiberna…...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...
uniapp中使用aixos 报错
问题: 在uniapp中使用aixos,运行后报如下错误: AxiosError: There is no suitable adapter to dispatch the request since : - adapter xhr is not supported by the environment - adapter http is not available in the build 解决方案&…...
Go 语言并发编程基础:无缓冲与有缓冲通道
在上一章节中,我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道,它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好࿰…...
基于鸿蒙(HarmonyOS5)的打车小程序
1. 开发环境准备 安装DevEco Studio (鸿蒙官方IDE)配置HarmonyOS SDK申请开发者账号和必要的API密钥 2. 项目结构设计 ├── entry │ ├── src │ │ ├── main │ │ │ ├── ets │ │ │ │ ├── pages │ │ │ │ │ ├── H…...