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…...

[10-3]软件I2C读写MPU6050 江协科技学习笔记(16个知识点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...
什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南
文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果作为Spring生态中的AI集成框架,其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似,但特别为多语…...
稳定币的深度剖析与展望
一、引言 在当今数字化浪潮席卷全球的时代,加密货币作为一种新兴的金融现象,正以前所未有的速度改变着我们对传统货币和金融体系的认知。然而,加密货币市场的高度波动性却成为了其广泛应用和普及的一大障碍。在这样的背景下,稳定…...

听写流程自动化实践,轻量级教育辅助
随着智能教育工具的发展,越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式,也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建,…...

安全突围:重塑内生安全体系:齐向东在2025年BCS大会的演讲
文章目录 前言第一部分:体系力量是突围之钥第一重困境是体系思想落地不畅。第二重困境是大小体系融合瓶颈。第三重困境是“小体系”运营梗阻。 第二部分:体系矛盾是突围之障一是数据孤岛的障碍。二是投入不足的障碍。三是新旧兼容难的障碍。 第三部分&am…...

群晖NAS如何在虚拟机创建飞牛NAS
套件中心下载安装Virtual Machine Manager 创建虚拟机 配置虚拟机 飞牛官网下载 https://iso.liveupdate.fnnas.com/x86_64/trim/fnos-0.9.2-863.iso 群晖NAS如何在虚拟机创建飞牛NAS - 个人信息分享...

【Linux手册】探秘系统世界:从用户交互到硬件底层的全链路工作之旅
目录 前言 操作系统与驱动程序 是什么,为什么 怎么做 system call 用户操作接口 总结 前言 日常生活中,我们在使用电子设备时,我们所输入执行的每一条指令最终大多都会作用到硬件上,比如下载一款软件最终会下载到硬盘上&am…...

Linux部署私有文件管理系统MinIO
最近需要用到一个文件管理服务,但是又不想花钱,所以就想着自己搭建一个,刚好我们用的一个开源框架已经集成了MinIO,所以就选了这个 我这边对文件服务性能要求不是太高,单机版就可以 安装非常简单,几个命令就…...
土建施工员考试:建筑施工技术重点知识有哪些?
《管理实务》是土建施工员考试中侧重实操应用与管理能力的科目,核心考查施工组织、质量安全、进度成本等现场管理要点。以下是结合考试大纲与高频考点整理的重点内容,附学习方向和应试技巧: 一、施工组织与进度管理 核心目标: 规…...