当前位置: 首页 > news >正文

分布式锁—2.Redisson的可重入锁二

大纲

1.Redisson可重入锁RedissonLock概述

2.可重入锁源码之创建RedissonClient实例

3.可重入锁源码之lua脚本加锁逻辑

4.可重入锁源码之WatchDog维持加锁逻辑

5.可重入锁源码之可重入加锁逻辑

6.可重入锁源码之锁的互斥阻塞逻辑

7.可重入锁源码之释放锁逻辑

8.可重入锁源码之获取锁超时与锁超时自动释放逻辑

9.可重入锁源码总结

4.可重入锁源码之WatchDog维持加锁逻辑

(1)异步执行完加锁的lua脚本会触发执行thenApply()里的回调

(2)根据执行加锁lua脚本的返回值来决定是否创建定时调度任务

(3)定时调度任务由Netty的时间轮机制来实现

(4)10秒后会执行定时任务并判断是否要再创建一个定时任务在10秒后执行

如果某个客户端上锁后,过了几分钟都没释放掉这个锁,而锁对应的key一开始的过期时间其实只设置了30秒而已,那么在这种场景下这个锁就不能在上锁的30秒后被自动释放。

RedissonLock中有一个WatchDog机制:

当客户端对myLock这个key成功加锁后,就会创建一个定时任务,这个定时任务会默认10秒后更新myLock这个key的过期时间为30秒。当然,前提是客户端一直持有myLock这个锁,还没有对锁进行释放。只要客户端一直持有myLock这个锁没有对其进行释放,那么就会不断创建一个定时任务,10秒后更新myLock这个key的过期时间。

下面详细介绍加锁成功后的定时任务,是如何每隔10秒去更新锁的过期时间的。

(1)异步执行完加锁的lua脚本会触发执行thenApply()里的回调

异步执行完RedissonLock.tryLockInnerAsync()方法里的加锁lua脚本内容后,会触发执行ttlRemainingFuture的thenApply()方法里的回调。其中加锁lua脚本会返回Long型的ttlRemaining变量,ttlRemainingFuture的thenApply()方法里的回调入参便是这个变量。

具体会先在RedissonLock的tryAcquireAsync()方法中,定义一个RFuture类型的名为ttlRemainingFuture的变量。这个变量封装了当前这个锁对应的key的剩余存活时间,单位毫秒,这个剩余存活时间是在执行完加锁lua脚本时返回的。

然后通过RFuture的thenApply()方法给ttlRemainingFuture添加一个回调。这样当lua脚本执行完成后,就会触发ttlRemainingFuture的thenApply()方法里的回调,而回调里的入参ttlRemaining就是执行加锁lua脚本的返回值。

public class RedissonLock extends RedissonBaseLock {protected long internalLockLeaseTime;protected final LockPubSub pubSub;final CommandAsyncExecutor commandExecutor;public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {super(commandExecutor, name);this.commandExecutor = commandExecutor;//与WatchDog有关的internalLockLeaseTime//通过命令执行器CommandExecutor可以获取连接管理器ConnectionManager//通过连接管理器ConnectionManager可以获取Redis的配置信息类Config//通过Redis的配置信息类Config可以获取lockWatchdogTimeout超时时间this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();}//加锁@Overridepublic void lock() {try {lock(-1, null, false);} catch (InterruptedException e) {throw new IllegalStateException();}}...private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {//线程ID,用来生成设置Hash的值long threadId = Thread.currentThread().getId();//尝试加锁,此时执行RedissonLock.lock()方法默认传入的leaseTime=-1Long ttl = tryAcquire(-1, leaseTime, unit, threadId);//ttl为null说明加锁成功if (ttl == null) {return;}      //加锁失败时的处理...}...private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {//默认下waitTime和leaseTime都是-1,下面调用的get方法是来自于RedissonObject的get()方法//可以理解为异步转同步:将异步的tryAcquireAsync通过get转同步return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));}private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture<Long> ttlRemainingFuture;if (leaseTime != -1) {ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {//默认情况下,由于leaseTime=-1,所以会使用初始化RedissonLock实例时的internalLockLeaseTime//internalLockLeaseTime的默认值就是lockWatchdogTimeout的默认值,30秒ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {//异步执行完tryLockInnerAsync()里加锁lua脚本内容后//就会触发执行ttlRemainingFuture.thenApply()里的回调//加锁返回的ttlRemaining为null表示加锁成功if (ttlRemaining == null) {if (leaseTime != -1) {internalLockLeaseTime = unit.toMillis(leaseTime);} else {//传入加锁成功的线程ID,启动一个定时任务scheduleExpirationRenewal(threadId);}}return ttlRemaining;});return new CompletableFutureWrapper<>(f);}//默认情况下,外部传入的leaseTime=-1时,会取lockWatchdogTimeout的默认值=30秒<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {return evalWriteAsync(getRawName(), 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(getRawName()),//锁的名字:KEYS[1]unit.toMillis(leaseTime),//过期时间:ARGV[1],默认时为30秒getLockName(threadId)//ARGV[2],值为UUID + 线程ID);}...
}

(2)根据执行加锁lua脚本的返回值来决定是否创建定时调度任务

当执行加锁lua脚本的返回值ttlRemaining为null时,则表明锁获取成功。

如果锁获取成功,且指定锁的过期时间,即leaseTime不是默认的-1,那么此时并不会创建定时任务。如果锁获取成功,且没指定锁的过期时间,即leaseTime是默认的-1,那么此时才会创建定时调度任务,并且是根据Netty的时间轮来实现创建。

所以调用scheduleExpirationRenewal()方法会创建一个定时调度任务,只要锁还被客户端持有,定时任务就会不断更新锁对应的key的过期时间。

public class RedissonLock extends RedissonBaseLock {...private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture<Long> ttlRemainingFuture;if (leaseTime != -1) {ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {//异步执行完tryLockInnerAsync()里的内容后,就会执行ttlRemainingFuture.thenApply()里的逻辑//加锁返回的ttlRemaining为null表示加锁成功if (ttlRemaining == null) {if (leaseTime != -1) {//如果指定了锁的过期时间,并不会启动定时任务internalLockLeaseTime = unit.toMillis(leaseTime);} else {//传入加锁成功的线程ID,启动一个定时任务scheduleExpirationRenewal(threadId);}}return ttlRemaining;});return new CompletableFutureWrapper<>(f);}...
}

(3)定时调度任务由Netty的时间轮机制来实现

Redisson的WatchDog机制底层并不是调度线程池,而是Netty时间轮。

scheduleExpirationRenewal()方法会创建一个定时调度任务TimerTask交给HashedWheelTimer,10秒后执行。

public abstract class RedissonBaseLock extends RedissonExpirable implements RLock {...protected void scheduleExpirationRenewal(long threadId) {ExpirationEntry entry = new ExpirationEntry();ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);if (oldEntry != null) {oldEntry.addThreadId(threadId);} else {entry.addThreadId(threadId);try {//创建一个更新过期时间的定时调度任务renewExpiration();} finally {if (Thread.currentThread().isInterrupted()) {cancelExpirationRenewal(threadId);}}}}//更新过期时间private void renewExpiration() {ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;}//使用了Netty的定时任务机制:HashedWheelTimer + TimerTask + Timeout//创建一个更新过期时间的定时调度任务,下面会调用MasterSlaveConnectionManager.newTimeout()方法//即创建一个定时调度任务TimerTask交给HashedWheelTimer,10秒后执行Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {...}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);}...
}public class MasterSlaveConnectionManager implements ConnectionManager {private HashedWheelTimer timer;//Netty的时间轮...@Overridepublic Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {try {//给Netty的时间轮添加任务taskreturn timer.newTimeout(task, delay, unit);} catch (IllegalStateException e) {if (isShuttingDown()) {return DUMMY_TIMEOUT;}throw e;}}...
}//Netty的时间轮HashedWheelTimer
public class HashedWheelTimer implements Timer {private final HashedWheelBucket[] wheel;private final Queue<HashedWheelTimeout> timeouts = PlatformDependent.newMpscQueue();private final Executor taskExecutor;...@Overridepublic Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {...//启动Worker线程start();...//封装定时调度任务TimerTask实例到HashedWheelTimeout实例HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);timeouts.add(timeout);return timeout;}...private final class Worker implements Runnable {private long tick;//轮次...@Overridepublic void run() {...do {final long deadline = waitForNextTick();if (deadline > 0) {int idx = (int) (tick & mask);processCancelledTasks();HashedWheelBucket bucket = wheel[idx];//将定时调度任务HashedWheelTimeout转换到时间轮分桶里transferTimeoutsToBuckets();//处理时间轮分桶HashedWheelBucket里的到期任务bucket.expireTimeouts(deadline);tick++;}} while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);...}private void transferTimeoutsToBuckets() {for (int i = 0; i < 100000; i++) {HashedWheelTimeout timeout = timeouts.poll();...long calculated = timeout.deadline / tickDuration;timeout.remainingRounds = (calculated - tick) / wheel.length;final long ticks = Math.max(calculated, tick); // Ensure we don't schedule for past.int stopIndex = (int) (ticks & mask);//对mask取模HashedWheelBucket bucket = wheel[stopIndex];bucket.addTimeout(timeout);}}...}private static final class HashedWheelBucket {...public void expireTimeouts(long deadline) {HashedWheelTimeout timeout = head;//process all timeoutswhile (timeout != null) {...//对定时调度任务timeout进行过期处理timeout.expire();...}}...}private static final class HashedWheelTimeout implements Timeout, Runnable {private final HashedWheelTimer timer;...public void expire() {...//执行定时调度任务,让Executor执行HashedWheelTimeout的run方法timer.taskExecutor.execute(this);...}@Overridepublic void run() {...//执行定时调度任务task.run(this);...}...}...
}

(4)10秒后会执行定时任务并判断是否要再创建一个定时任务在10秒后执行

该定时任务TimerTask实例会调用RedissonBaseLock的renewExpirationAsync()方法去执行lua脚本,renewExpirationAsync()方法会传入获取到锁的线程ID。

在lua脚本中,会通过Redis的hexists命令判断在key为锁名的Hash值里,field为UUID + 线程ID的映射是否存在。其中KEYS[1]就是锁的名字如myLock,ARGV[2]为UUID + 线程ID。

如果存在,说明获取锁的线程还在持有锁,并没有对锁进行释放。那么就通过Redis的pexpire命令,设置key的过期时间为30秒。

异步执行lua脚本完后,会传入布尔值触发future.whenComplete()方法的回调,回调中会根据布尔值来决定是否继续递归调用renewExpiration()方法。

也就是说,如果获取锁的线程还在持有锁,那么就重置锁的过期时间为30秒,并且lua脚本会返回1,接着在future.whenComplete()方法的回调中,继续调用RedissonBaseLock的renewExpiration()方法重新创建定时调度任务。

如果获取锁的线程已经释放了锁,那么lua脚本就会返回0,接着在future.whenComplete()方法的回调中,会调用RedissonBaseLock的cancelExpirationRenewal()方法执行清理工作。

public abstract class RedissonBaseLock extends RedissonExpirable implements RLock {...protected void scheduleExpirationRenewal(long threadId) {ExpirationEntry entry = new ExpirationEntry();ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);if (oldEntry != null) {oldEntry.addThreadId(threadId);} else {entry.addThreadId(threadId);try {//创建一个更新过期时间的定时调度任务renewExpiration();} finally {if (Thread.currentThread().isInterrupted()) {cancelExpirationRenewal(threadId);}}}}//更新过期时间private void renewExpiration() {ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;}//使用了Netty的定时任务机制:HashedWheelTimer + TimerTask + Timeout//创建一个更新过期时间的定时调度任务,下面会调用MasterSlaveConnectionManager.newTimeout()方法//即创建一个定时调度任务TimerTask交给HashedWheelTimer,10秒后执行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;}Long threadId = ent.getFirstThreadId();if (threadId == null) {return;}//异步执行lua脚本去更新锁的过期时间RFuture<Boolean> future = renewExpirationAsync(threadId);future.whenComplete((res, e) -> {if (e != null) {log.error("Can't update lock " + getRawName() + " expiration", e);EXPIRATION_RENEWAL_MAP.remove(getEntryName());return;}//res就是执行renewExpirationAsync()里的lua脚本的返回值if (res) {//重新调度自己renewExpiration();} else {//执行清理工作cancelExpirationRenewal(null);}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);}protected RFuture<Boolean> renewExpirationAsync(long threadId) {//其中KEYS[1]就是锁的名字如myLock,ARGV[2]为UUID+线程ID;return evalWriteAsync(getRawName(), 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(getRawName()),internalLockLeaseTime, getLockName(threadId));}}protected void cancelExpirationRenewal(Long threadId) {ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (task == null) {return;}if (threadId != null) {task.removeThreadId(threadId);}if (threadId == null || task.hasNoThreads()) {Timeout timeout = task.getTimeout();if (timeout != null) {timeout.cancel();}EXPIRATION_RENEWAL_MAP.remove(getEntryName());}}...
}

注意:如果持有锁的机器宕机了,就会导致该机器上的锁的WatchDog不执行了。从而锁的key会在30秒内自动过期,释放掉锁。此时其他客户端最多再等30秒就可获取到这把锁了。

5.可重入锁源码之可重入加锁逻辑

(1)核心一是key的命名:第一层key是锁名,第二层field是UUID + 线程ID

(2)核心二是Redis的exists命令、hexists命令、hincrby命令

public class Application {public static void main(String[] args) throws Exception {Config config = new Config();config.useClusterServers().addNodeAddress("redis://192.168.1.110:7001");//创建RedissonClient实例RedissonClient redisson = Redisson.create(config);//获取可重入锁RLock lock = redisson.getLock("myLock");//首次加锁lock.lock();//重入加锁lock.lock();//重入加锁lock.lock();lock.unlock();lock.unlock();lock.unlock();...}
}public class RedissonLock extends RedissonBaseLock {...<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {return evalWriteAsync(getRawName(), 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(getRawName()),//锁的名字:KEYS[1]unit.toMillis(leaseTime),//过期时间:ARGV[1],默认时为30秒getLockName(threadId)//ARGV[2],值为UUID + 线程ID);}...
}

实现可重入加锁的核心就是加锁的lua脚本。

(1)核心一是key的命名:第一层key是锁名,第二层field是UUID+线程ID

KEYS[1]的值就是锁的名字,ARGV[2]的值就是客户端UUID + 线程ID。由于第二层field包含了线程ID,所以可以区分申请加锁的线程是否在重入。对在key为锁名的Hash值中,field为UUID + 线程ID的value值进行递增1操作。

(2)核心二是Redis的exists命令、hexists命令、hincrby命令

判断锁是否存在,使用的是Redis的exists命令。判断锁是否正在被线程重入,则使用的是Redis的hexists命令。对field映射的value值进行递增,使用的是Redis的hincrby命令。

所以,持有锁的线程每重入锁一次,就会对field映射的value值递增1。比如:hincrby key UUID:ThreadID 1。

6.可重入锁源码之锁的互斥阻塞逻辑

(1)获取锁失败时会执行Redis的pttl命令返回锁的剩余存活时间

(2)ttlRemainingFuture的回调方法发现ttlRemaining不是null

(3)RedissonLock的tryAcquire()方法会返回这个剩余存活时间ttl

(4)RedissonLock的lock()方法会对tryAcquire()方法返回的ttl进行判断

(5)RedissonLock的lock()方法在ttl不为null时利用Semaphore进行阻塞

(1)获取锁失败时会执行Redis的pttl命令返回锁的剩余存活时间

在执行加锁的lua脚本中:首先通过"exists myLock",判断是否存在myLock这个锁key,发现已经存在。接着判断"hexists myLock 另一个UUID:另一个Thread的ID",发现不存在。于是执行"pttl myLock"并返回,也就是返回myLock这个锁的剩余存活时间。

(2)ttlRemainingFuture的回调方法发现ttlRemaining不是null

当执行加锁的lua脚本发现加锁不成功,返回锁的剩余存活时间时,ttlRemainingFuture的回调方法发现ttlRemaining不是null,于是便不会创建定时调度任务在10秒之后去检查锁是否还被申请的线程持有,而是会将返回的剩余存活时间封装到RFuture中并向上返回。

(3)RedissonLock的tryAcquire()方法会返回这个剩余存活时间ttl

主要会通过RedisObject的get()方法去获取RFuture中封装的ttl值。其中异步转同步就是将异步的tryAcquireAsync()方法通过get()方法转同步。

(4)RedissonLock的lock()方法会对tryAcquire()方法返回的ttl进行判断

如果当前线程是第一次加锁,那么ttl一定是null。如果当前线程是多次加锁(可重入锁),那么ttl也一定是null。如果当前线程加锁没成功,锁被其他机器或线程占用,那么ttl就是执行加锁lua脚本时获取到的锁对应的key的剩余存活时间。

如果ttl是null,说明当前线程加锁成功,于是直接返回。如果ttl不是null,说明当前线程加锁不成功,于是会执行阻塞逻辑。

(5)RedissonLock的lock()方法在ttl不为null时利用Semaphore进行阻塞

如果ttl不为null,即加锁不成功,那么会进入while(true)死循环。在死循环内,再次执行RedissonLock的tryAcquire()方法尝试获取锁。如果再次获取锁时返回的ttl为null,即获取到锁,就退出while循环。如果再次获取锁时返回的ttl不为null,则说明其他客户端或线程还持有锁。

于是就利用同步组件Semaphore进行阻塞等待一段ttl的时间。阻塞等待一段时间后,会继续执行while循环里的逻辑,再次尝试获取锁。以此循环往复,直到获得锁。

public class RedissonLock extends RedissonBaseLock {protected long internalLockLeaseTime;protected final LockPubSub pubSub;final CommandAsyncExecutor commandExecutor;public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {super(commandExecutor, name);this.commandExecutor = commandExecutor;//与WatchDog有关的internalLockLeaseTime//通过命令执行器CommandExecutor可以获取连接管理器ConnectionManager//通过连接管理器ConnectionManager可以获取Redis的配置信息类Config//通过Redis的配置信息类Config可以获取lockWatchdogTimeout超时时间this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();}...//加锁@Overridepublic void lock() {try {lock(-1, null, false);} catch (InterruptedException e) {throw new IllegalStateException();}}private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {//线程ID,用来生成设置Hash的值long threadId = Thread.currentThread().getId();//尝试加锁,此时执行RedissonLock.lock()方法默认传入的leaseTime=-1Long ttl = tryAcquire(-1, leaseTime, unit, threadId);//ttl为null说明加锁成功if (ttl == null) {return;}//加锁失败时的处理CompletableFuture<RedissonLockEntry> future = subscribe(threadId);if (interruptibly) {commandExecutor.syncSubscriptionInterrupted(future);} else {commandExecutor.syncSubscription(future);}try {while (true) {//再次尝试获取锁ttl = tryAcquire(-1, leaseTime, unit, threadId);//返回的ttl为null,获取到锁,就退出while循环if (ttl == null) {break;}//返回的ttl不为null,则说明其他客户端或线程还持有锁//那么就利用同步组件Semaphore进行阻塞等待一段ttl的时间if (ttl >= 0) {try {commandExecutor.getNow(future).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {if (interruptibly) {throw e;}commandExecutor.getNow(future).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);}} else {if (interruptibly) {commandExecutor.getNow(future).getLatch().acquire();} else {commandExecutor.getNow(future).getLatch().acquireUninterruptibly();}}}} finally {unsubscribe(commandExecutor.getNow(future), threadId);}}...private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {//默认下waitTime和leaseTime都是-1,下面调用的get方法是来自于RedissonObject的get()方法//可以理解为异步转同步:将异步的tryAcquireAsync通过get转同步return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));}private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture<Long> ttlRemainingFuture;if (leaseTime != -1) {ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {//默认情况下,由于leaseTime=-1,所以会使用初始化RedissonLock实例时的internalLockLeaseTime//internalLockLeaseTime的默认值就是lockWatchdogTimeout的默认值,30秒ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {//加锁返回的ttlRemaining为null表示加锁成功if (ttlRemaining == null) {if (leaseTime != -1) {internalLockLeaseTime = unit.toMillis(leaseTime);} else {scheduleExpirationRenewal(threadId);}}return ttlRemaining;});return new CompletableFutureWrapper<>(f);}//默认情况下,外部传入的leaseTime=-1时,会取lockWatchdogTimeout的默认值=30秒<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {return evalWriteAsync(getRawName(), 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(getRawName()),//锁的名字:KEYS[1]unit.toMillis(leaseTime),//过期时间:ARGV[1],默认时为30秒getLockName(threadId)//ARGV[2],值为UUID + 线程ID);}...
}public class RedissonLockEntry implements PubSubEntry<RedissonLockEntry> {private final Semaphore latch;...public Semaphore getLatch() {return latch;}...
}

7.可重入锁源码之释放锁逻辑

(1)宕机自动释放锁

(2)线程主动释放锁的流程

(3)主动释放锁的lua脚本分析

(1)宕机自动释放锁

如果获取锁的所在机器宕机,那么10秒后检查锁是否还在被线程持有的定时调度任务就没了。于是锁对应Redis里的key,就会在最多30秒内进行过期删除。之后,其他的客户端就能成功获取锁了。

当然,前提是创建锁的时候没有设置锁的存活时间leaseTime。如果指定了leaseTime,那么就不会存在10秒后检查锁的定时调度任务。此时获取锁的所在机器宕机,那么锁对应的key会在最多leaseTime后过期。

(2)线程主动释放锁的流程

主动释放锁调用的是RedissonLock的unlock()方法。在RedissonLock的unlock()方法中,会调用get(unlockAsync())方法。也就是首先调用RedissonBaseLock的unlockAsync()方法,然后再调用RedissonObject的get()方法。

其中unlockAsync()方法是异步化执行的方法,释放锁的操作就是异步执行的。而RedisObject的get()方法会通过RFuture同步等待获取异步执行的结果,可以将get(unlockAsync())理解为异步转同步。

在RedissonBaseLock的unlockAsync()方法中:首先会调用RedissonLock的unlockInnerAsync()方法进行异步释放锁,然后当完成释放锁的处理后,再通过异步去取消定时调度任务。

public class Application {public static void main(String[] args) throws Exception {Config config = new Config();config.useClusterServers().addNodeAddress("redis://192.168.1.110:7001");//创建RedissonClient实例RedissonClient redisson = Redisson.create(config);//获取可重入锁RLock lock = redisson.getLock("myLock");lock.lock();//获取锁lock.unlock();//释放锁...}
}public class RedissonLock extends RedissonBaseLock {...@Overridepublic void unlock() {...//异步转同步//首先调用的是RedissonBaseLock的unlockAsync()方法//然后调用的是RedissonObject的get()方法get(unlockAsync(Thread.currentThread().getId()));...}//异步执行释放锁的lua脚本protected RFuture<Boolean> unlockInnerAsync(long threadId) {return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +"return nil;" +"end; " +"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +"if (counter > 0) then " +"redis.call('pexpire', KEYS[1], ARGV[2]); " +"return 0; " +"else " +"redis.call('del', KEYS[1]); " +"redis.call('publish', KEYS[2], ARGV[1]); " +"return 1; " +"end; " +"return nil;",Arrays.asList(getRawName(), getChannelName()),//KEYS[1] + KEYS[2]LockPubSub.UNLOCK_MESSAGE,//ARGV[1]internalLockLeaseTime,//ARGV[2]getLockName(threadId)//ARGV[3]);}...
}public abstract class RedissonBaseLock extends RedissonExpirable implements RLock {...@Overridepublic RFuture<Void> unlockAsync(long threadId) {//异步执行释放锁的lua脚本RFuture<Boolean> future = unlockInnerAsync(threadId);CompletionStage<Void> f = future.handle((opStatus, e) -> {//取消定时调度任务cancelExpirationRenewal(threadId);if (e != null) {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);}...
}abstract class RedissonExpirable extends RedissonObject implements RExpirable {...RedissonExpirable(CommandAsyncExecutor connectionManager, String name) {super(connectionManager, name);}...
}public abstract class RedissonObject implements RObject {protected final CommandAsyncExecutor commandExecutor;protected String name;protected final Codec codec;public RedissonObject(Codec codec, CommandAsyncExecutor commandExecutor, String name) {this.codec = codec;this.commandExecutor = commandExecutor;if (name == null) {throw new NullPointerException("name can't be null");}setName(name);}...protected final <V> V get(RFuture<V> future) {//下面会调用CommandAsyncService.get()方法return commandExecutor.get(future);}...
}public class CommandAsyncService implements CommandAsyncExecutor {...@Overridepublic <V> V get(RFuture<V> future) {if (Thread.currentThread().getName().startsWith("redisson-netty")) {throw new IllegalStateException("Sync methods can't be invoked from async/rx/reactive listeners");}try {return future.toCompletableFuture().get();} catch (InterruptedException e) {future.cancel(true);Thread.currentThread().interrupt();throw new RedisException(e);} catch (ExecutionException e) {throw convertException(e);}}...
}

(3)主动释放锁的lua脚本分析

首先判断在key为锁名的Hash值中,field为UUID + 线程ID的映射是否存在。如果不存在,则表示锁已经被释放了,直接返回。如果存在,则在key为锁名的Hash值中对field为UUID + 线程ID的value值递减1。即调用Redis的hincrby命令,进行递减1处理。

接着对递减1后的结果进行如下判断:如果递减1后的结果大于0,则表示线程还在持有锁。这对应于持有锁的线程多次重入锁,此时需要重置锁的过期时间为30秒。如果递减1后的结果小于0,则表示线程不再持有锁,于是删除锁对应的key,并且通过Redis的publish命令,发布一个事件。

public class RedissonLock extends RedissonBaseLock {...//异步执行释放锁的lua脚本protected RFuture<Boolean> unlockInnerAsync(long threadId) {return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +"return nil;" +"end; " +"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +"if (counter > 0) then " +"redis.call('pexpire', KEYS[1], ARGV[2]); " +"return 0; " +"else " +"redis.call('del', KEYS[1]); " +"redis.call('publish', KEYS[2], ARGV[1]); " +"return 1; " +"end; " +"return nil;",Arrays.asList(getRawName(), getChannelName()),//KEYS[1] + KEYS[2]LockPubSub.UNLOCK_MESSAGE,//ARGV[1]internalLockLeaseTime,//ARGV[2]getLockName(threadId)//ARGV[3]);}...
}

8.可重入锁源码之获取锁超时与锁超时自动释放逻辑

(1)尝试获取锁超时

(2)锁超时自动释放

针对如下代码方式去获取锁,如果超过60秒获取不到锁,就自动放弃获取锁,不会进行永久性阻塞。如果获取到锁之后,锁在10秒内没有被主动释放,那么就会自动释放锁。

public class Application {public static void main(String[] args) throws Exception {Config config = new Config();config.useClusterServers().addNodeAddress("redis://192.168.1.110:7001");//创建RedissonClient实例RedissonClient redisson = Redisson.create(config);//获取可重入锁RLock lock = redisson.getLock("myLock");boolean res = lock.tryLock(60, 10, TimeUnit.SECONDS);//尝试获取锁if (res) lock.unlock();//尝试获取锁成功,则释放锁...}
}

(1)尝试获取锁超时

RedissonLock的tryLock()方法,会传入waitTime和leaseTime尝试获取锁。其中waitTime就是最多可以等待多少时间去获取锁,比如60秒。leaseTime就是获取锁后锁的自动过期时间,比如10秒。

如果第一次获取锁的时间超过了等待获取锁的最大时间waitTime,那么就会返回false。

如果第一次获取锁的时间还没超过等待获取锁的最大时间waitTime,那么就进入while循环,不断地再次尝试获取锁。

如果再次尝试获取锁成功,那么就返回true。如果再次尝试获取锁失败,那么计算还剩下多少时间可以继续等待获取锁。也就是使用time去自减每次尝试获取锁的耗时以及自减每次等待的耗时。

如果发现time小于0,表示等待了waitTime都无法获取锁,则返回false。如果发现time大于0,继续下一轮while循环,尝试获取锁 + time自减耗时。

public class RedissonLock extends RedissonBaseLock {...@Overridepublic boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {long time = unit.toMillis(waitTime);//最多可以等待多少时间去获取锁long current = System.currentTimeMillis();//current是第一次尝试获取锁之前的时间戳long threadId = Thread.currentThread().getId();Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);//尝试获取锁成功if (ttl == null) {return true;}//第一次尝试获取锁失败//当前时间减去第一次获取锁之前的时间戳,就是这次尝试获取锁的耗费时间,使用time进行自减time -= System.currentTimeMillis() - current;//如果第一次获取锁的时间超过了waitTime等待获取锁的最大时间,那么就会直接返回获取锁失败if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;}//如果第一次获取锁的时间还没达到waitTime等待获取锁的最大时间current = System.currentTimeMillis();CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);try {subscribeFuture.toCompletableFuture().get(time, TimeUnit.MILLISECONDS);} catch (ExecutionException | TimeoutException e) {if (!subscribeFuture.cancel(false)) {subscribeFuture.whenComplete((res, ex) -> {if (ex == null) {unsubscribe(res, threadId);}});}acquireFailed(waitTime, unit, threadId);return false;}try {time -= System.currentTimeMillis() - current;if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;}while (true) {long currentTime = System.currentTimeMillis();ttl = tryAcquire(waitTime, leaseTime, unit, threadId);//再次尝试获取锁成功if (ttl == null) {return true;}//剩余可用等待的时间,使用time去自减每次尝试获取锁的耗时time -= System.currentTimeMillis() - currentTime;if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;}//返回的ttl不为null,则说明其他客户端或线程还持有锁//那么就利用同步组件Semaphore进行阻塞等待一段ttl的时间currentTime = System.currentTimeMillis();if (ttl >= 0 && ttl < time) {commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} else {commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);}//使用time去自减每次等待的耗时time -= System.currentTimeMillis() - currentTime;if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;}}} finally {unsubscribe(commandExecutor.getNow(subscribeFuture), threadId);}}...
}

(2)锁超时自动释放

当使用RedissonLock.tryAcquire方法尝试获取锁时,传入的leaseTime不是-1,而是一个指定的锁存活时间,那么锁超时就会自动被释放。

当leaseTime不是-1时:

一.锁的过期时间就不是lockWatchdogTimeout=30秒,而是leaseTime

二.执行加锁lua脚本成功后,不会创建一个定时调度任务在10秒后检查锁

总结:当指定了leaseTime后,那么锁最多在leaseTime后,就必须被删除。因为此时没有定时调度任务去检查锁释放还在被线程持有并更新过期时间。所以,锁要么被主动删除,要么在存活leaseTime后自动过期。

public class RedissonLock extends RedissonBaseLock {...private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {//默认下waitTime和leaseTime都是-1,下面调用的get方法是来自于RedissonObject的get()方法//可以理解为异步转同步:将异步的tryAcquireAsync通过get转同步return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));}private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture<Long> ttlRemainingFuture;if (leaseTime != -1) {ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {//默认情况下,由于leaseTime=-1,所以会使用初始化RedissonLock实例时的internalLockLeaseTime//internalLockLeaseTime的默认值就是lockWatchdogTimeout的默认值,30秒ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {//加锁返回的ttlRemaining为null表示加锁成功if (ttlRemaining == null) {if (leaseTime != -1) {internalLockLeaseTime = unit.toMillis(leaseTime);} else {scheduleExpirationRenewal(threadId);}}return ttlRemaining;});return new CompletableFutureWrapper<>(f);}...
}

9.可重入锁源码总结

(1)加锁

(2)WatchDog维持加锁

(3)可重入锁

(4)锁互斥

(5)手动释放锁

(6)宕机自动释放锁

(7)尝试加锁超时

(8)超时锁自动释放

(1)加锁

在Redis里设置两层Hash数据结构,默认的过期时间是30秒。第一层Hash值的key是锁名,第二层Hash值的key是UUID + 线程ID。涉及Redis的exists命令、hexists命令、hincrby命令。

(2)WatchDog维持加锁

如果获取锁的线程一直持有锁,那么Redis里的key就会一直保持存活。获取锁成功时会创建一个定时任务10秒后检查锁是否还被线程持有。如果线程还在持有锁,就会重置key的过期时间为30秒,并且创建一个新的定时任务在10秒后继续检查锁是否还被线程持有。

(3)可重入锁

同一个线程可以多次加锁,对第二层的key为UUID + 线程ID的Hash值,每次获取锁就进行累加1。

(4)锁互斥

不同线程尝试加锁时,会由于第二层Hash的key不同,而导致加锁不成功。也就是执行加锁lua脚本时会返回锁的剩余过期时间ttl。然后利用同步组件Semaphore进行阻塞等待一段ttl时间。阻塞等待一段时间后,继续在while循环里再次尝试获取锁。以此循环等待 + 尝试,直到获得锁。

(5)手动释放锁

使用hincrby命令对第二层key(UUID + 线程ID)的Hash值递减1。递减1后还大于0表示锁被重入,需要重置锁的过期时间。递减1后小于等于0表示锁释放完毕,需要删除锁key及取消定时调度任务。

(6)宕机自动释放锁

如果持有锁的客户端宕机,那么锁的WatchDog定时调度任务也没了。此时不会重置锁key的过期时间,于是锁key会自动释放。

(7)尝试加锁超时

在while循环中不断地尝试获取锁。使用time表示还可以等待获取锁的时间。每次循环都对time:自减尝试获取锁的耗时 + 自减每次等待的耗时。在指定时间内没有成功加锁(即time小于0),就退出循环,表示加锁失败。

(8)超时锁自动释放

当指定了leaseTime时,如果获取锁没有在leaseTime内手动释放锁,那么Redis里的锁key会自动过期,自动释放锁。

相关文章:

分布式锁—2.Redisson的可重入锁二

大纲 1.Redisson可重入锁RedissonLock概述 2.可重入锁源码之创建RedissonClient实例 3.可重入锁源码之lua脚本加锁逻辑 4.可重入锁源码之WatchDog维持加锁逻辑 5.可重入锁源码之可重入加锁逻辑 6.可重入锁源码之锁的互斥阻塞逻辑 7.可重入锁源码之释放锁逻辑 8.可重入锁…...

Ribbon实现原理

文章目录 概要什么是Ribbon客户端负载均衡 RestTemplate核心方法GET 请求getForEntitygetForObject POST 请求postForEntitypostForObjectpostForLocation PUT请求DELETE请求 源码分析类图关系 与Eureka结合重试机制 概要 什么是Ribbon Spring Cloud Ribbon是一个基于HTTP和T…...

游戏引擎学习第133天

仓库:https://gitee.com/mrxiao_com/2d_game_3 回顾并设定今天的主题 今天的任务是进一步优化背景资源的流式加载&#xff0c;尤其是在内存管理方面。昨天&#xff0c;我们实现了资源流式加载&#xff0c;让游戏在加载时可以动态地加载背景&#xff0c;而不是一开始就把所有资…...

DeepSeek搭配Excel,制作自定义按钮,实现办公自动化!

今天跟大家分享下我们如何将DeepSeek生成的VBA代码&#xff0c;做成按钮&#xff0c;将其永久保存在我们的Excel表格中&#xff0c;下次遇到类似的问题&#xff0c;直接在Excel中点击按钮&#xff0c;就能10秒搞定&#xff0c;操作也非常的简单. 一、代码准备 代码可以直接询问…...

deepseek+mermaid【自动生成流程图】

成果&#xff1a; 第一步打开deepseek官网(或百度版&#xff08;更快一点&#xff09;)&#xff1a; 百度AI搜索 - 办公学习一站解决 第二步&#xff0c;生成对应的Mermaid流程图&#xff1a; 丢给deepseek代码&#xff0c;或题目要求 生成mermaid代码 第三步将代码复制到me…...

递归遍历目录 和 普通文件的复制 [Java EE]

递归遍历目录 首先 先列出当前目录所包含的内容 File[] files currentDir.listFiles();if (files null || files.length 0) {// 若是空目录或非法目录, 则直接返回return;} 然后 遍历列出的文件, 分情况两种讨论 for (File f: files) {// 加个日志, 方便查看程序执行情…...

安防监控/视频集中存储EasyCVR视频汇聚平台如何配置AI智能分析平台的接入?

EasyCVR安防视频监控平台不仅支持AI边缘计算智能硬件设备的接入&#xff0c;还能快速集成AI智能分析平台&#xff0c;接收来自智能分析平台或设备的AI告警信息&#xff0c;如烟火检测、周界入侵检测、危险区域闯入检测、安全帽/反光衣佩戴检测等。 本文将详细介绍如何在EasyCVR…...

React 之 Redux 第二十八节 学习目标与规划大纲及概要讲述

接下来 开始Redux 全面详细的文档输出&#xff0c;主要基于一下几个方面&#xff0c;欢迎大家补充指正 一、Redux 基础概念 为什么需要 Redux&#xff1f; 前端状态管理的挑战&#xff08;组件间通信、状态共享&#xff09; Redux 解决的问题&#xff1a;集中式、可预测的状态…...

五分钟快速学习优秀网站的HTML骨架布局设计

一.编写多级过滤脚本&#xff0c;在控制台执行copy方法进行提取&#xff1a; 过滤脚本脚本 // 在浏览器F12的控制台里&#xff0c;直接执行以下脚本 copy(document.documentElement.outerHTML// 一级过滤&#xff1a;移除动态内容.replace(/<script\b[^>]*>[\s\S]*?…...

神经网络AI原理回顾

长期记忆存储在大模型的参数权重中&#xff0c;不经过推理和编码无法读取&#xff0c;且必须依赖输入的提示&#xff0c;因为大模型不会无缘无故的自言自语&#xff0c;毕竟输入层是它唯一 与外界交互的窗口。 目前个性化大模型的局限就是训练成本过高&#xff0c;除非使用RAG&…...

常见webshell工具的流量特征

1、蚁剑 1.1、蚁剑webshell静态特征 蚁剑中php使用assert、eval执行&#xff1b;asp只有eval执行&#xff1b;在jsp使用的是Java类加载&#xff08;ClassLoader&#xff09;&#xff0c;同时会带有base64编码解码等字符特征。 1.2、蚁剑webshell动态特征 查看流量分析会发现…...

探秘基带算法:从原理到5G时代的通信变革【四】Polar 编解码(一)

文章目录 2.3 Polar 编解码2.3.1 Polar 码简介与发展背景2.3.2 信道极化理论基础对称容量与巴氏参数对称容量 I ( W ) I(W) I(W)巴氏参数 Z ( W ) Z(W) Z(W)常见信道信道联合信道分裂信道极化 本博客为系列博客&#xff0c;主要讲解各基带算法的原理与应用&#xff0c;包括&…...

【计算机网络】考研复试高频知识点总结

文章目录 一、基础概念1、计算机⽹络的定义2、计算机⽹络的目标3、计算机⽹络的组成4、计算机⽹络的分类5、计算机⽹络的拓扑结构6、计算机⽹络的协议7、计算机⽹络的分层结构8、OSI 参考模型9、TCP/IP 参考模型10、五层协议体系结构 二、物理层1、物理层的功能2、传输媒体3、 …...

nio多线程版本

多线程多路复用 多线程NIO&#xff0c;&#xff0c;就是多个线程&#xff0c;每个线程上都有一个Selector&#xff0c;&#xff0c;&#xff0c;比如说一个系统中一个线程用来接收请求&#xff0c;&#xff0c;剩余的线程用来读写数据&#xff0c;&#xff0c;每个线程独立干自…...

Lua | 每日一练 (5)

&#x1f4a2;欢迎来到张胤尘的技术站 &#x1f4a5;技术如江河&#xff0c;汇聚众志成。代码似星辰&#xff0c;照亮行征程。开源精神长&#xff0c;传承永不忘。携手共前行&#xff0c;未来更辉煌&#x1f4a5; 文章目录 Lua | 每日一练 (5)题目参考答案浅拷贝深拷贝使用场景…...

IO的概念和标准IO函数

作业&#xff1a; 1.使用标准IO函数&#xff0c;实现文件的拷贝 #include <stdio.h>int main(int argc, char *argv[]) {// 检查是否提供了源文件和目标文件if (argc ! 3) {printf("Usage: %s <source_file> <destination_file>\n", argv[0]);re…...

影刀RPA开发拓展--SQL常用语句全攻略

前言 SQL&#xff08;结构化查询语言&#xff09;是数据库管理和操作的核心工具&#xff0c;无论是初学者还是经验丰富的数据库管理员&#xff0c;掌握常用的 SQL 语句对于高效管理和查询数据都至关重要。本文将系统性地介绍最常用的 SQL 语句&#xff0c;并为每个语句提供详细…...

零信任架构和传统网络安全模式的

零信任到底是一个什么类型的模型&#xff1f;什么类型的思想或思路&#xff0c;它是如何实现的&#xff0c;我们要做零信任&#xff0c;需要考虑哪些问题&#xff1f; 零信任最早是约翰金德瓦格提出的安全模型。早期这个模型也是因为在安全研究上考虑的一个新的信任式模型。他最…...

【前端】HTML 备忘清单(超级详细!)

文章目录 入门hello.html注释 Comment段落 ParagraphHTML 链接Image 标签文本格式标签标题Section Divisions内部框架HTML 中的 JavaScriptHTML 中的 CSS HTML5 标签页面标题导航HTML5 TagsHTML5 VideoHTML5 AudioHTML5 RubyHTML5 kdiHTML5 progressHTML5 mark HTML 表格Table …...

React 中 useState 的 基础使用

概念&#xff1a;useState 是一个React Hook&#xff08;函数&#xff09;&#xff0c;它允许我们向组件添加状态变量&#xff0c;从而影响组件的渲染结果。 本质&#xff1a;和普通JS变量不同的是&#xff0c;状态变量一旦发生变化&#xff0c;组件的视图UI也会跟着变化&…...

蓝桥杯单片机组第十二届省赛第二批次

前言 第十二届省赛涉及知识点&#xff1a;NE555频率数据读取&#xff0c;NE555频率转换周期&#xff0c;PCF8591同时测量光敏电阻和电位器的电压、按键长短按判断。 本试题涉及模块较少&#xff0c;题目不难&#xff0c;基本上准备充分的都能完整的实现每一个功能&#xff0c;并…...

React Native 原理

React Native 是一个跨平台移动应用开发框架&#xff0c;它允许开发者使用 JavaScript 和 React 来开发 iOS 和 Android 原生应用。React Native 的核心原理是通过 桥接&#xff08;Bridge&#xff09; 技术&#xff0c;使用 JavaScript 来控制原生组件&#xff0c;并将应用逻辑…...

C++简易贪食蛇项目

一.案例介绍 二.制作思路 三.墙模块 #include "wall.h" //初始化墙 void initWall() { for (int i 0; i < HEIGHT; i) { for (int j 0; j < WIDTH;j) { if (i 0 || j 0 || i HEIGHT - 1 || j WIDTH - 1) …...

C++蓝桥杯基础篇(七)

片头 嗨~小伙伴们&#xff0c;大家好&#xff01;今天我们来一起学习蓝桥杯基础篇&#xff08;七&#xff09;&#xff0c;学习相关字符串的知识&#xff0c;准备好了吗&#xff1f;咱们开始咯&#xff01; 一、字符与整数的联系——ASCII码 每个常用字符都对应一个-128~127的…...

Sass基础

目录 什么是sass? Sass的安装 Sass的编译 Sass的语法&#xff1a; Sass的基本使用: 一、Sass变量&#xff1a; 二、嵌套语法&#xff1a; 三、import的使用&#xff1a; 四、mixin混入和include: 五、extend: 六、注释 七、if和if: 八、for: 总结&#xff1a; 什么是sas…...

Linux文档编辑相关命令详解

Linux文档编辑相关命令 1. grep grep (global regular expression) 命令用于查找文件里符合条件的字符串或正则表达式。 1.1 语法 grep [options] pattern [files] 1.2 常用选项 -i&#xff1a;忽略大小写进行匹配。-v&#xff1a;反向查找&#xff0c;只打印不匹配的行。-…...

QT实现简约美观的动画Checkbox

*最终效果: * 一共三个文件: main.cpp , FancyCheckbox.h , FancyCheckbox.cpp main.cpp #include <QApplication> #include "FancyCheckbox.h" #include <QGridLayout> int main(int argc, char *argv[]) {QApplication a(argc, argv);QWidget* w new…...

每日学习Java之一万个为什么?[MySQL面试篇]

分析SQL语句执行流程中遇到的问题 前言1 MySQL是怎么在一台服务器上启动的2 MySQL主库和从库是同时启动保持Alive的吗&#xff1f;3 如果不是主从怎么在启动的时候保证数据一致性4 ACID原则在MySQL上的体现5 数据在MySQL是通过什么DTO实现的6 客户端怎么与MySQL Server建立连接…...

git笔记

定义&#xff1a;分布式版本控制工具&#xff0c;免费开源的&#xff0c;快速高效的处理从小到大的项目&#xff0c;git占地面积小&#xff0c;性能快&#xff0c;有廉价的本地库 安装&#xff1a;官网最新版 全部点下一步就行 版本控制工具&#xff1a;使用中央服务器&#…...

Full GC 排查

在 Java 中&#xff0c;Full GC&#xff08;完全垃圾回收&#xff09;会对整个堆&#xff08;包括年轻代和老年代&#xff0c;甚至可能包括永久代/元空间&#xff09;进行垃圾回收&#xff0c;通常会导致较长的停顿&#xff08;STW&#xff0c;Stop-The-World&#xff09;。如果…...

VS2022远程调试Ubuntu中的C++程序

前言 最近想基于星火大模型的SDK开发第一些应用。但是&#xff0c;发现星火的SDK当中Linux版本的比较丰富&#xff0c;Windows 版本支持的比较少。但是&#xff0c;从调试的IDE而言&#xff0c;Visual Studio又是最方便的。所以&#xff0c;考虑采用Visual Studio Ubuntu的形式…...

Flutter 学习之旅 之 flutter 使用 flutter_screenutil 简单进行屏幕适配

Flutter 学习之旅 之 flutter 使用 flutter_screenutil 简单进行屏幕适配 目录 Flutter 学习之旅 之 flutter 使用 flutter_screenutil 简单进行屏幕适配 一、简单介绍 二、简单介绍 flutter_screenutil 三、安装 carousel_slider 四、简单案例实现 五、关键代码 六、补…...

【华为OD机考】华为OD笔试真题解析(16)--微服务的集成测试

题目描述 现在有n个容器服务&#xff0c;服务的启动可能有一定的依赖性&#xff08;有些服务启动没有依赖&#xff09;&#xff0c;其次&#xff0c;服务自身启动加载会消耗一些时间。 给你一个 n n n \times n nn的二维矩阵useTime&#xff0c;其中useTime[i][i]10表示服务…...

NCCL AI 分布式训练集合通讯库技术基本原理

目录 文章目录 目录AI 分布式训练NCCL 的简介NCCL 的核心功能NCCL 的基本工作流程NCCL 的集合通信操作方式NCCL 的 API 编程示例 AI 分布式训练 在一个最初的 AI 模型训练场景中&#xff0c;由于模型自身的程序体积、输入的参数量以及样本的数据量都比较有限&#xff0c;一张 …...

算法-回溯篇01-组合

组合 力扣题目链接 题目描述 给定两个整数 n 和 k&#xff0c;返回范围 [1, n] 中所有可能的 k 个数的组合。 你可以按 任何顺序 返回答案。 解题思路 刚开始做回溯的题目&#xff0c;关于回溯的相关知识推荐大家去看代码随想录的视频。 做了几道题&#xff0c;感觉回溯题…...

泵吸式激光可燃气体监测仪:快速精准守护燃气管网安全

在城市化进程加速的今天&#xff0c;燃气泄漏、地下管网老化等问题时刻威胁着城市安全。如何实现精准、高效的可燃气体监测&#xff0c;守护“城市生命线”&#xff0c;成为新型基础设施建设的核心课题。泵吸式激光可燃气体监测仪&#xff0c;以创新科技赋能安全监测&#xff0…...

Lumoz Chain正式上线:AI 时代的新算力破局者

新的叙事和技术突破永远是推动行业前行的核心动力。当下&#xff0c;AI Agent无疑是最炙手可热的赛道之一。 当加密世界将目光投向AI领域时&#xff0c;大多数项目仍停留在以AI为工具或应用场景的层面&#xff0c;试图通过集成AI模型或优化链上功能来吸引用户。然而&#xff0…...

算法之排序算法

排序算法 ♥常见排序算法知识体系详解♥ | Java 全栈知识体系 算法 - 排序 | CS-Notes 面试笔记 十大经典排序算法总结 | JavaGuide...

Java面试第七山!《MySQL索引》

一、索引的本质与作用 索引是帮助MySQL高效获取数据的数据结构&#xff0c;类似于书籍的目录。它通过减少磁盘I/O次数&#xff08;即减少数据扫描量&#xff09;来加速查询&#xff0c;尤其在百万级数据场景下&#xff0c;索引可将查询效率提升数十倍。 核心作用&#xff1a;…...

基于 Rust 与 GBT32960 规范的编解码层

根据架构设计&#xff0c;实现编解码层的代码设计 Cargo.toml 加入二进制序列化支持 # 序列化支持 ... bincode "1.3" # 添加二进制序列化支持 bytes-utils "0.1" # 添加字节处理工具 开始编码 错误处理&#xff08;error.rs&#x…...

二、Redis 安装与基本配置:全平台安装指南 服务器配置详解

Redis 安装与基本配置:全平台安装指南 & 服务器配置详解 Redis 作为高性能的内存数据库,其安装和配置是使用 Redis 的第一步。本篇文章将全面介绍 Redis 的安装方式,覆盖 Windows、Linux、Docker 环境,并详细讲解 Redis 的基础配置,包括 持久化、日志、端口设置等。此…...

⭐算法OJ⭐矩阵的相关操作【动态规划 + 组合数学】(C++ 实现)Unique Paths 系列

文章目录 62. Unique Paths动态规划思路实现代码复杂度分析 组合数学思路实现代码复杂度分析 63. Unique Paths II动态规划定义状态状态转移方程初始化复杂度分析 优化空间复杂度状态转移方程 62. Unique Paths There is a robot on an m x n grid. The robot is initially lo…...

基于 Elasticsearch 和 Milvus 的 RAG 运维知识库的架构设计和部署落地实现指南

最近在整理一些业务场景的架构设计和部署落地实现指南 先放一个 【基于RAG的运维知识库 (ElasticSearch + Milvus) 的详细实现指南】,其中包含了详尽的技术实现细节、可运行的示例代码、原理分析、优缺点分析和应用场景分析。 架构描述: 基于RAG的运维知识库 (ElasticSearch…...

山西青年杂志山西青年杂志社山西青年编辑部2025年第3期目录

青年争鸣 教师发展中心行动转向的价值意蕴分析框架研究与启示 于宝证;李军红;郑钰莹;何易雯; 产教融合视角下职业本科工商管理专业人才培养模式探析 杜芯铭; 青年教育研究 教育数字化背景下高职院校的课堂教学研究 张晨; 统筹职业教育、高等教育、继续教育协同…...

使用Truffle、Ganache、MetaMask、Vue+Web3完成的一个简单区块链项目

文章目录 概要初始化Truffle项目创建编写合约编译合约配置Ganache修改truffle-config.js文件编写迁移文件部署合约使用Truffle 控制台使用MetaMask和VueWeb3与链交互 概要 使用Truffle、Ganache、MetaMask、VueWeb3完成的一个简单区块链项目。 初始化Truffle项目 安装好truf…...

【GenBI优化】提升text2sql准确率:建议使用推理大模型,增加重试

引言 Text-to-SQL(文本转 SQL)是自然语言处理(NLP)领域的一项重要任务,旨在将自然语言问题自动转换为可在数据库上执行的 SQL 查询语句。这项技术在智能助手、数据分析工具、商业智能(BI)平台等领域具有广泛的应用前景,能够极大地降低数据查询和分析的门槛,让非技术用…...

LLVM - 编译器前端 - 学习将源文件转换为抽象语法树(二)

一:处理消息 在一个庞大的软件(比如编译器)中,我们不希望将消息字符串分散在各个地方。如果需要修改消息内容或将其翻译成另一种语言,最好将它们集中存放在一个地方!目前缺少的是对消息的集中定义。下面我们看看来如何实现它。 一种简单的方法是,每条消息都有一个 ID(一…...

T-SQL 语言基础: SQL 数据库对象元数据及配置信息获取

目录 介绍目录视图 获取表和架构名称获取列信息 信息架构视图 获取表信息获取列信息 系统存储过程和函数 获取对象列表获取对象详细信息获取约束信息获取数据库属性信息 总结引用 介绍 在 SQL 数据库管理中&#xff0c;获取数据库对象的元数据信息是至关重要的。元数据提供了…...

基于vue框架的游戏博客网站设计iw282(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;用户,博客信息,资源共享,游戏视频,游戏照片 开题报告内容 基于FlaskVue框架的游戏博客网站设计开题报告 一、项目背景与意义 随着互联网技术的飞速发展和游戏产业的不断壮大&#xff0c;游戏玩家对游戏资讯、攻略、评测等内容的需求日…...

批量提取 Word 文档中的页面

如何将 Word 文档中的页面提取出来形成一个新的文档呢&#xff1f;比如将 Word 文档中的第一页提取出来、将 Word 文档中的最后一页提取出来、再或者将 Word 文档中的中间几页提取出来等等。人工的处理肯定非常的麻烦&#xff0c;需要新建 Word 文档&#xff0c;然后将内容复制…...