分布式鎖—2.Redisson的可重入鎖一

大綱

1.Redisson可重入鎖RedissonLock概述

2.可重入鎖源碼之創建RedissonClient實例

3.可重入鎖源碼之lua腳本加鎖邏輯

4.可重入鎖源碼之WatchDog維持加鎖邏輯

5.可重入鎖源碼之可重入加鎖邏輯

6.可重入鎖源碼之鎖的互斥阻塞邏輯

7.可重入鎖源碼之釋放鎖邏輯

8.可重入鎖源碼之獲取鎖超時與鎖超時自動釋放邏輯

9.可重入鎖源碼總結

1.Redisson可重入鎖RedissonLock概述

(1)在pom.xml里引入依賴

(2)構建RedissonClient并使用Redisson

(3)Redisson可重入鎖RedissonLock簡單使用

(1)在pom.xml里引入依賴

<dependencies><dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.16.8</version></dependency> 
</dependencies>

(2)構建RedissonClient并使用Redisson

參考官網中文文檔,連接上3主3從的Redis Cluster。

//https://github.com/redisson/redisson/wiki/目錄
public class Application {public static void main(String[] args) throws Exception {//連接3主3從的Redis CLusterConfig config = new Config();config.useClusterServers().addNodeAddress("redis://192.168.1.110:7001").addNodeAddress("redis://192.168.1.110:7002").addNodeAddress("redis://192.168.1.110:7003").addNodeAddress("redis://192.168.1.111:7001").addNodeAddress("redis://192.168.1.111:7002").addNodeAddress("redis://192.168.1.111:7003");//創建RedissonClient實例RedissonClient redisson = Redisson.create(config);//獲取可重入鎖RLock lock = redisson.getLock("myLock");lock.lock();lock.unlock();RMap<String, Object> map = redisson.getMap("myMap");map.put("foo", "bar");  map = redisson.getMap("myMap");System.out.println(map.get("foo"));   }
}

(3)Redisson可重入鎖RedissonLock簡單使用

Redisson可重入鎖RLock實現了java.util.concurrent.locks.Lock接口,同時還提供了異步(Async)、響應式(Reactive)和RxJava2標準的接口。

RLock lock = redisson.getLock("myLock");
//最常見的使用方法
lock.lock();

如果設置鎖的超時時間不合理,導致超時時間已到時鎖還沒能主動釋放,但實際上鎖卻被Redis節點通過過期時間釋放了,這會有問題。

為了避免這種情況,Redisson內部提供了一個用來監控鎖的WatchDog。WatchDog的作用是在Redisson實例被關閉前,不斷地延長鎖的有效期。

WatchDog檢查鎖的默認超時時間是30秒,可通過Config.lockWatchdogTimeout來指定。

RLock的tryLock方法提供了leaseTime參數來指定加鎖的超時時間,超過這個時間后鎖便自動被釋放。

//如果沒有主動釋放鎖的話,10秒后將會自動釋放鎖
lock.lock(10, TimeUnit.SECONDS);//加鎖等待最多是100秒;加鎖成功后如果沒有主動釋放鎖的話,鎖會在10秒后自動釋放
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {try {...} finally {lock.unlock();}
}

RLock完全符合Java的Lock規范,即只有擁有鎖的進程才能解鎖,其他進程解鎖則會拋出IllegalMonitorStateException錯誤。如果需要其他進程也能解鎖,那么可以使用分布式信號量Semaphore。

2.可重入鎖源碼之創建RedissonClient實例

(1)初始化與Redis的連接管理器ConnectionManager

(2)初始化Redis的命令執行器CommandExecutor

使用Redisson.create()方法可以根據配置創建一個RedissonClient實例,因為Redisson類會實現RedissonClient接口,而創建RedissonClient實例的主要工作其實就是:

一.初始化與Redis的連接管理器ConnectionManager

二.初始化Redis的命令執行器CommandExecutor

(1)初始化與Redis的連接管理器ConnectionManager

Redis的配置類Config會被封裝在連接管理器ConnectionManager中,后續可以通過連接管理器ConnectionManager獲取Redis的配置類Config。

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);...}
}//創建RedissonClient實例的源碼
public class Redisson implements RedissonClient {protected final Config config;//Redis配置類protected final ConnectionManager connectionManager;//Redis的連接管理器protected final CommandAsyncExecutor commandExecutor;//Redis的命令執行器...public static RedissonClient create(Config config) {return new Redisson(config);}protected Redisson(Config config) {this.config = config;Config configCopy = new Config(config);//根據Redis配置類Config實例創建和Redis的連接管理器connectionManager = ConfigSupport.createConnectionManager(configCopy);RedissonObjectBuilder objectBuilder = null;if (config.isReferenceEnabled()) {objectBuilder = new RedissonObjectBuilder(this);}//創建Redis的命令執行器commandExecutor = new CommandSyncService(connectionManager, objectBuilder);evictionScheduler = new EvictionScheduler(commandExecutor);writeBehindService = new WriteBehindService(commandExecutor);}...
}public class ConfigSupport {...//創建Redis的連接管理器public static ConnectionManager createConnectionManager(Config configCopy) {//生成UUIDUUID id = UUID.randomUUID();...if (configCopy.getClusterServersConfig() != null) {validate(configCopy.getClusterServersConfig());//返回ClusterConnectionManager實例return new ClusterConnectionManager(configCopy.getClusterServersConfig(), configCopy, id);}...}...
}public class ClusterConnectionManager extends MasterSlaveConnectionManager {public ClusterConnectionManager(ClusterServersConfig cfg, Config config, UUID id) {super(config, id);...this.natMapper = cfg.getNatMapper();//將Redis的配置類Config封裝在ConnectionManager中this.config = create(cfg);initTimer(this.config);Throwable lastException = null;List<String> failedMasters = new ArrayList<String>();for (String address : cfg.getNodeAddresses()) {RedisURI addr = new RedisURI(address);//異步連接Redis節點CompletionStage<RedisConnection> connectionFuture = connectToNode(cfg, addr, addr.getHost());...//通過connectionFuture阻塞獲取建立好的連接RedisConnection connection = connectionFuture.toCompletableFuture().join();...List<ClusterNodeInfo> nodes = connection.sync(clusterNodesCommand);...CompletableFuture<Collection<ClusterPartition>> partitionsFuture = parsePartitions(nodes);Collection<ClusterPartition> partitions = partitionsFuture.join();List<CompletableFuture<Void>> masterFutures = new ArrayList<>();for (ClusterPartition partition : partitions) {if (partition.isMasterFail()) {failedMasters.add(partition.getMasterAddress().toString());continue;}if (partition.getMasterAddress() == null) {throw new IllegalStateException("Master node: " + partition.getNodeId() + " doesn't have address.");}CompletableFuture<Void> masterFuture = addMasterEntry(partition, cfg);masterFutures.add(masterFuture);}CompletableFuture<Void> masterFuture = CompletableFuture.allOf(masterFutures.toArray(new CompletableFuture[0]));masterFuture.join();...}...}...
}public class MasterSlaveConnectionManager implements ConnectionManager {protected final String id;//初始化時為UUIDprivate final Map<RedisURI, RedisConnection> nodeConnections = new ConcurrentHashMap<>();...protected MasterSlaveConnectionManager(Config cfg, UUID id) {this.id = id.toString();//傳入的是UUIDthis.cfg = cfg;...}protected final CompletionStage<RedisConnection> connectToNode(NodeType type, BaseConfig<?> cfg, RedisURI addr, String sslHostname) {RedisConnection conn = nodeConnections.get(addr);if (conn != null) {if (!conn.isActive()) {closeNodeConnection(conn);} else {return CompletableFuture.completedFuture(conn);}}//創建Redis客戶端連接實例RedisClient client = createClient(type, addr, cfg.getConnectTimeout(), cfg.getTimeout(), sslHostname);//向Redis服務端發起異步連接請求,這個future會層層往外返回CompletionStage<RedisConnection> future = client.connectAsync();return future.thenCompose(connection -> {if (connection.isActive()) {if (!addr.isIP()) {RedisURI address = new RedisURI(addr.getScheme() + "://" + connection.getRedisClient().getAddr().getAddress().getHostAddress() + ":" + connection.getRedisClient().getAddr().getPort());nodeConnections.put(address, connection);}nodeConnections.put(addr, connection);return CompletableFuture.completedFuture(connection);} else {connection.closeAsync();CompletableFuture<RedisConnection> f = new CompletableFuture<>();f.completeExceptionally(new RedisException("Connection to " + connection.getRedisClient().getAddr() + " is not active!"));return f;}});}//創建Redis客戶端連接實例@Overridepublic RedisClient createClient(NodeType type, RedisURI address, int timeout, int commandTimeout, String sslHostname) {RedisClientConfig redisConfig = createRedisConfig(type, address, timeout, commandTimeout, sslHostname);return RedisClient.create(redisConfig);}...
}//Redisson主要使用Netty去和Redis服務端建立連接
public final class RedisClient {private final Bootstrap bootstrap;private final Bootstrap pubSubBootstrap;...public static RedisClient create(RedisClientConfig config) {return new RedisClient(config);}private RedisClient(RedisClientConfig config) {...bootstrap = createBootstrap(copy, Type.PLAIN);pubSubBootstrap = createBootstrap(copy, Type.PUBSUB);this.commandTimeout = copy.getCommandTimeout();}private Bootstrap createBootstrap(RedisClientConfig config, Type type) {Bootstrap bootstrap = new Bootstrap().resolver(config.getResolverGroup()).channel(config.getSocketChannelClass()).group(config.getGroup());bootstrap.handler(new RedisChannelInitializer(bootstrap, config, this, channels, type));bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, config.getConnectTimeout());bootstrap.option(ChannelOption.SO_KEEPALIVE, config.isKeepAlive());bootstrap.option(ChannelOption.TCP_NODELAY, config.isTcpNoDelay());config.getNettyHook().afterBoostrapInitialization(bootstrap);return bootstrap;}//向Redis服務端發起異步連接請求public RFuture<RedisConnection> connectAsync() {CompletableFuture<InetSocketAddress> addrFuture = resolveAddr();CompletableFuture<RedisConnection> f = addrFuture.thenCompose(res -> {CompletableFuture<RedisConnection> r = new CompletableFuture<>();//Netty的Bootstrap發起連接ChannelFuture channelFuture = bootstrap.connect(res);channelFuture.addListener(new ChannelFutureListener() {@Overridepublic void operationComplete(final ChannelFuture future) throws Exception {if (bootstrap.config().group().isShuttingDown()) {IllegalStateException cause = new IllegalStateException("RedisClient is shutdown");r.completeExceptionally(cause);return;}if (future.isSuccess()) {RedisConnection c = RedisConnection.getFrom(future.channel());c.getConnectionPromise().whenComplete((res, e) -> {bootstrap.config().group().execute(new Runnable() {@Overridepublic void run() {if (e == null) {if (!r.complete(c)) {c.closeAsync();}} else {r.completeExceptionally(e);c.closeAsync();}}});});} else {bootstrap.config().group().execute(new Runnable() {public void run() {r.completeExceptionally(future.cause());}});}}});return r;});return new CompletableFutureWrapper<>(f);}...
}

(2)初始化Redis的命令執行器CommandExecutor

首先,CommandSyncService繼承自CommandAsyncService類。

而CommandAsyncService類實現了CommandExecutor接口。

然后,ConnectionManager連接管理器會封裝在命令執行器CommandExecutor中。

所以,通過CommandExecutor命令執行器可以獲取連接管理器ConnectionManager。

//Redis命令的同步執行器CommandSyncService
public class CommandSyncService extends CommandAsyncService implements CommandExecutor {//初始化CommandExecutorpublic CommandSyncService(ConnectionManager connectionManager, RedissonObjectBuilder objectBuilder) {super(connectionManager, objectBuilder, RedissonObjectBuilder.ReferenceType.DEFAULT);}public <T, R> R read(String key, RedisCommand<T> command, Object... params) {return read(key, connectionManager.getCodec(), command, params);}public <T, R> R read(String key, Codec codec, RedisCommand<T> command, Object... params) {RFuture<R> res = readAsync(key, codec, command, params);return get(res);}public <T, R> R evalRead(String key, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {return evalRead(key, connectionManager.getCodec(), evalCommandType, script, keys, params);}public <T, R> R evalRead(String key, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {RFuture<R> res = evalReadAsync(key, codec, evalCommandType, script, keys, params);return get(res);}public <T, R> R evalWrite(String key, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {return evalWrite(key, connectionManager.getCodec(), evalCommandType, script, keys, params);}public <T, R> R evalWrite(String key, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {RFuture<R> res = evalWriteAsync(key, codec, evalCommandType, script, keys, params);return get(res);}
}//Redis命令的異步執行器CommandAsyncService
public class CommandAsyncService implements CommandAsyncExecutor {//Redis連接管理器final ConnectionManager connectionManager;final RedissonObjectBuilder objectBuilder;final RedissonObjectBuilder.ReferenceType referenceType;public CommandAsyncService(ConnectionManager connectionManager, RedissonObjectBuilder objectBuilder, RedissonObjectBuilder.ReferenceType referenceType) {this.connectionManager = connectionManager;this.objectBuilder = objectBuilder;this.referenceType = referenceType;}@Overridepublic <V> V getNow(CompletableFuture<V> future) {try {return future.getNow(null);} catch (Exception e) {return null;}}@Overridepublic <T, R> R read(String key, Codec codec, RedisCommand<T> command, Object... params) {RFuture<R> res = readAsync(key, codec, command, params);return get(res);}@Overridepublic <T, R> RFuture<R> readAsync(String key, Codec codec, RedisCommand<T> command, Object... params) {NodeSource source = getNodeSource(key);return async(true, source, codec, command, params, false, false);}private NodeSource getNodeSource(String key) {int slot = connectionManager.calcSlot(key);return new NodeSource(slot);}public <V, R> RFuture<R> async(boolean readOnlyMode, NodeSource source, Codec codec, RedisCommand<V> command, Object[] params, boolean ignoreRedirect, boolean noRetry) {CompletableFuture<R> mainPromise = createPromise();RedisExecutor<V, R> executor = new RedisExecutor<>(readOnlyMode, source, codec, command, params, mainPromise, ignoreRedirect, connectionManager, objectBuilder, referenceType, noRetry);executor.execute();return new CompletableFutureWrapper<>(mainPromise);}@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腳本加鎖邏輯

(1)通過Redisson.getLock()方法獲取一個RedissonLock實例

(2)加鎖時的執行流程

(3)加鎖時執行的lua腳本

(4)執行加鎖lua腳本的命令執行器邏輯

(5)如何根據slot值獲取對應的節點

(1)通過Redisson.getLock()方法獲取一個RedissonLock實例

在Redisson.getLock()方法中,會傳入命令執行器CommandExecutor來創建一個RedissonLock實例,而命令執行器CommandExecutor是在執行Redisson.create()方法時初始化好的,所以命令執行器CommandExecutor會被封裝在RedissonLock實例中。

因此,通過RedissonLock實例可以獲取一個命令執行器CommandExecutor,通過命令執行器CommandExecutor可獲取連接管理器ConnectionManager,通過連接管理器ConnectionManager可獲取Redis的配置信息類Config,通過Redis的配置信息類Config可以獲取各種配置信息。

RedissonLock類繼承自實現了RLock接口的RedissonBaseLock類。在RedissonLock的構造方法里面,有個internalLockLeaseTime變量,這個internalLockLeaseTime變量與WatchDog看門狗有關系。interlnalLockLeaseTime的默認值是30000毫秒,即30秒;

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();...}
}//創建Redisson實例
public class Redisson implements RedissonClient {protected final Config config;//Redis配置類protected final ConnectionManager connectionManager;//Redis的連接管理器protected final CommandAsyncExecutor commandExecutor;//Redis的命令執行器...public static RedissonClient create(Config config) {return new Redisson(config);}protected Redisson(Config config) {...//根據Redis配置類Config實例創建和Redis的連接管理器connectionManager = ConfigSupport.createConnectionManager(configCopy);//創建Redis的命令執行器commandExecutor = new CommandSyncService(connectionManager, objectBuilder);...}...@Overridepublic RLock getLock(String name) {return new RedissonLock(commandExecutor, name);}...
}//創建RedissonLock實例
//通過RedissonLock實例可以獲取一個命令執行器CommandExecutor;
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();}...
}//創建Redis的命令執行器
//通過命令執行器CommandExecutor可以獲取連接管理器ConnectionManager
public class CommandAsyncService implements CommandAsyncExecutor {final ConnectionManager connectionManager;...public CommandAsyncService(ConnectionManager connectionManager, RedissonObjectBuilder objectBuilder, RedissonObjectBuilder.ReferenceType referenceType) {this.connectionManager = connectionManager;this.objectBuilder = objectBuilder;this.referenceType = referenceType;}@Overridepublic ConnectionManager getConnectionManager() {return connectionManager;}...
}//創建Redis的連接管理器
//通過連接管理器ConnectionManager可以獲取Redis的配置信息類Config
public class ClusterConnectionManager extends MasterSlaveConnectionManager {...public ClusterConnectionManager(ClusterServersConfig cfg, Config config, UUID id) {super(config, id);...}...
}//創建Redis的連接管理器
//通過連接管理器ConnectionManager可以獲取Redis的配置信息類Config
public class MasterSlaveConnectionManager implements ConnectionManager {private final Config cfg;protected final String id;//初始化時為UUID...protected MasterSlaveConnectionManager(Config cfg, UUID id) {this.id = id.toString();//傳入的是UUIDthis.cfg = cfg;...}@Overridepublic Config getCfg() {return cfg;}...
}//配置信息類Config中的lockWatchdogTimeout變量初始化為30秒,該變量與WatchDog有關
public class Config {private long lockWatchdogTimeout = 30 * 1000;...//This parameter is only used if lock has been acquired without leaseTimeout parameter definition. //Lock expires after "lockWatchdogTimeout" if watchdog didn't extend it to next "lockWatchdogTimeout" time interval.//This prevents against infinity locked locks due to Redisson client crush or any other reason when lock can't be released in proper way.//Default is 30000 millisecondspublic Config setLockWatchdogTimeout(long lockWatchdogTimeout) {this.lockWatchdogTimeout = lockWatchdogTimeout;return this;}public long getLockWatchdogTimeout() {return lockWatchdogTimeout;}
}

默認情況下,調用RedissonLock.lock()方法加鎖時,傳入的leaseTime為-1。此時鎖的超時時間會設為lockWatchdogTimeout默認的30秒,從而避免出現死鎖的情況。

public class RedissonLock extends RedissonBaseLock {...//加鎖@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 {long threadId = Thread.currentThread().getId();Long ttl = tryAcquire(-1, leaseTime, unit, threadId);...}//解鎖@Overridepublic void unlock() {try {get(unlockAsync(Thread.currentThread().getId()));} catch (RedisException e) {if (e.getCause() instanceof IllegalMonitorStateException) {throw (IllegalMonitorStateException) e.getCause();} else {throw e;}}}...
}

(2)加鎖時的執行流程

首先會調用RedissonLock的tryAcquire()方法處理異步RFuture相關,然后調用RedissonLock的tryAcquireAsync()方法對執行腳本的結果進行處理,接著調用RedissonLock.tryLockInnerAsync方法執行加鎖的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;}//加鎖失敗時的處理CompletableFuture<RedissonLockEntry> future = subscribe(threadId);if (interruptibly) {commandExecutor.syncSubscriptionInterrupted(future);} else {commandExecutor.syncSubscription(future);}try {while (true) {ttl = tryAcquire(-1, leaseTime, unit, threadId);// lock acquiredif (ttl == null) {break;}// waiting for messageif (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 abstract class RedissonBaseLock extends RedissonExpirable implements RLock {final String id;final String entryName;final CommandAsyncExecutor commandExecutor;public RedissonBaseLock(CommandAsyncExecutor commandExecutor, String name) {super(commandExecutor, name);this.commandExecutor = commandExecutor;this.id = commandExecutor.getConnectionManager().getId();this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();this.entryName = id + ":" + name;}protected String getLockName(long threadId) {return id + ":" + threadId;}...
}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腳本

public class RedissonLock extends RedissonBaseLock {//默認情況下,外部傳入的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],比如"myLock"unit.toMillis(leaseTime),//過期時間:ARGV[1],默認時為30秒getLockName(threadId)//ARGV[2],值為UUID + 線程ID);}...
}

首先執行Redis的exists命令,判斷key為鎖名的Hash值是否不存在,也就是判斷key為鎖名myLock的Hash值是否存在。

一.如果key為鎖名的Hash值不存在,那么就進行如下加鎖處理

首先通過Redis的hset命令設置一個key為鎖名的Hash值。該Hash值的key為鎖名,value是一個映射。也就是在value值中會有一個field為UUID + 線程ID,value為1的映射。比如:hset myLock UUID:ThreadID 1,lua腳本中的ARGV[2]就是由UUID + 線程ID組成的唯一值。

然后通過Redis的pexpire命令設置key為鎖名的Hash值的過期時間,也就是設置key為鎖名的Hash值的過期時間為30秒。比如:pexpire myLock 30000。所以默認情況下,myLock這個鎖在30秒后就會自動過期。

二.如果key為鎖名的Hash值存在,那么就執行如下判斷處理

首先通過Redis的hexists命令判斷在key為鎖名的Hash值里,field為UUID + 線程ID的映射是否已經存在。

如果在key為鎖名的Hash值里,field為UUID + 線程ID的映射存在,那么就通過Redis的hincrby命令,對field為UUID + 線程ID的value值進行遞增1。比如:hincrby myLock UUID:ThreadID 1。也就是在key為myLock的Hash值里,把field為UUID:ThreadID的value值從1累加到2,發生這種情況的時候往往就是當前線程對鎖進行了重入。接著執行:pexpire myLock 30000,再次將myLock的有效期設置為30秒。

如果在key為鎖名的Hash值里,field為UUID + 線程ID的映射不存在,發生這種情況的時候往往就是其他線程獲取不到這把鎖而產生互斥。那么就通過Redis的pttl命令,返回key為鎖名的Hash值的剩余存活時間,因為不同線程的ARGV[2]是不一樣的,ARGV[2] = UUID + 線程ID。

(4)執行加鎖lua腳本的命令執行器邏輯

在RedissonLock的tryLockInnerAsync()方法中,會通過RedissonBaseLock的evalWriteAsync()方法執行lua腳本,即通過CommandAsyncService的evalWriteAsync()方法執行lua腳本。

在CommandAsyncService的evalWriteAsync()方法中,首先會執行CommandAsyncService的getNodeSource()方法獲取對應的節點。然后執行CommandAsyncService的evalAsync()方法來執行lua腳本。

在CommandAsyncService的getNodeSource()方法中,會根據key進行CRC16運算,然后再對16384取模,計算出key的slot值。然后根據這個slot值創建一個NodeSource實例進行返回。

在CommandAsyncService的evalAsync()方法中,會將獲得的NodeSource實例封裝到Redis執行器RedisExecutor里。然后執行RedisExecutor,實現將腳本請求發送給對應的Redis節點處理。

public abstract class RedissonBaseLock extends RedissonExpirable implements RLock {//從外部傳入的:在創建實現了RedissonClient的Redisson實例時,初始化的命令執行器CommandExecutorfinal CommandAsyncExecutor commandExecutor;public RedissonBaseLock(CommandAsyncExecutor commandExecutor, String name) {super(commandExecutor, name);this.commandExecutor = commandExecutor;this.id = commandExecutor.getConnectionManager().getId();this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();this.entryName = id + ":" + name;}...protected <T> RFuture<T> evalWriteAsync(String key, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {//獲取可用的節點,并繼續封裝一個命令執行器CommandBatchServiceMasterSlaveEntry entry = commandExecutor.getConnectionManager().getEntry(getRawName());int availableSlaves = entry.getAvailableSlaves();CommandBatchService executorService = createCommandBatchService(availableSlaves);//通過CommandAsyncService.evalWriteAsync方法執行lua腳本RFuture<T> result = executorService.evalWriteAsync(key, codec, evalCommandType, script, keys, params);if (commandExecutor instanceof CommandBatchService) {return result;}//異步執行然后獲取結果RFuture<BatchResult<?>> future = executorService.executeAsync();CompletionStage<T> f = future.handle((res, ex) -> {if (ex == null && res.getSyncedSlaves() != availableSlaves) {throw new CompletionException(new IllegalStateException("Only " + res.getSyncedSlaves() + " of " + availableSlaves + " slaves were synced"));}return result.getNow();});return new CompletableFutureWrapper<>(f);}private CommandBatchService createCommandBatchService(int availableSlaves) {if (commandExecutor instanceof CommandBatchService) {return (CommandBatchService) commandExecutor;}BatchOptions options = BatchOptions.defaults().syncSlaves(availableSlaves, 1, TimeUnit.SECONDS);return new CommandBatchService(commandExecutor, options);}...
}public class CommandBatchService extends CommandAsyncService {...public CommandBatchService(CommandAsyncExecutor executor, BatchOptions options) {this(executor.getConnectionManager(), options, executor.getObjectBuilder(), RedissonObjectBuilder.ReferenceType.DEFAULT);}private CommandBatchService(ConnectionManager connectionManager, BatchOptions options, RedissonObjectBuilder objectBuilder, RedissonObjectBuilder.ReferenceType referenceType) {super(connectionManager, objectBuilder, referenceType);this.options = options;}...
}public class CommandAsyncService implements CommandAsyncExecutor {final ConnectionManager connectionManager;final RedissonObjectBuilder objectBuilder;final RedissonObjectBuilder.ReferenceType referenceType;public CommandAsyncService(ConnectionManager connectionManager, RedissonObjectBuilder objectBuilder, RedissonObjectBuilder.ReferenceType referenceType) {this.connectionManager = connectionManager;this.objectBuilder = objectBuilder;this.referenceType = referenceType;}...@Overridepublic <T, R> RFuture<R> evalWriteAsync(String key, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {//獲取key對應的節點NodeSource source = getNodeSource(key);//讓對應的節點執行lua腳本請求return evalAsync(source, false, codec, evalCommandType, script, keys, false, params);}//獲取key對應的Redis Cluster節點private NodeSource getNodeSource(String key) {//先計算key對應的slot值int slot = connectionManager.calcSlot(key);//返回節點實例return new NodeSource(slot);}//執行lua腳本private <T, R> RFuture<R> evalAsync(NodeSource nodeSource, boolean readOnlyMode, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, boolean noRetry, Object... params) {if (isEvalCacheActive() && evalCommandType.getName().equals("EVAL")) {CompletableFuture<R> mainPromise = new CompletableFuture<>();Object[] pps = copy(params);CompletableFuture<R> promise = new CompletableFuture<>();String sha1 = calcSHA(script);RedisCommand cmd = new RedisCommand(evalCommandType, "EVALSHA");List<Object> args = new ArrayList<Object>(2 + keys.size() + params.length);args.add(sha1);args.add(keys.size());args.addAll(keys);args.addAll(Arrays.asList(params));//將根據key進行CRC16運算然后對16384取模獲取到的NodeSource實例,封裝到Redis執行器RedisExecutor中RedisExecutor<T, R> executor = new RedisExecutor<>(readOnlyMode, nodeSource, codec, cmd, args.toArray(), promise, false, connectionManager, objectBuilder, referenceType, noRetry);//通過執行Redis執行器RedisExecutor,來實現將lua腳本請求發送給對應的Redis節點進行處理executor.execute();...}...}...
}public class ClusterConnectionManager extends MasterSlaveConnectionManager {public static final int MAX_SLOT = 16384;//Redis Cluster默認有16384個slot...//對key進行CRC16運算,然后再對16384取模@Overridepublic int calcSlot(String key) {if (key == null) {return 0;}int start = key.indexOf('{');if (start != -1) {int end = key.indexOf('}');if (end != -1 && start + 1 < end) {key = key.substring(start + 1, end);}}int result = CRC16.crc16(key.getBytes()) % MAX_SLOT;return result;}...
}

(5)如何根據slot值獲取對應的節點

因為最后會執行封裝了NodeSource實例的RedisExecutor的excute()方法,而NodeSource實例中又會封裝了鎖名key對應的slot值,所以RedisExecutor的excute()方法可以通過getConnection()方法獲取對應節點的連接。

其中RedisExecutor的getConnection()方法會調用到MasterSlaveConnectionManager的connectionWriteOp()方法,該方法又會通過調用ConnectionManager的getEntry()方法根據slot值獲取節點,也就是由ClusterConnectionManager的getEntry()方法去獲取Redis的主節點。

其實在初始化連接管理器ClusterConnectionManager時,就已經根據配置初始化好哪些slot映射到那個Redis主節點了。

public class RedisExecutor<V, R> {NodeSource source;...public void execute() {...//異步獲取建立好的Redis連接CompletableFuture<RedisConnection> connectionFuture = getConnection().toCompletableFuture();...}protected CompletableFuture<RedisConnection> getConnection() {...connectionFuture = connectionManager.connectionWriteOp(source, command);return connectionFuture;}...
}public class MasterSlaveConnectionManager implements ConnectionManager {...@Overridepublic CompletableFuture<RedisConnection> connectionWriteOp(NodeSource source, RedisCommand<?> command) {MasterSlaveEntry entry = getEntry(source);...}private MasterSlaveEntry getEntry(NodeSource source) {if (source.getRedirect() != null) {return getEntry(source.getAddr());}MasterSlaveEntry entry = source.getEntry();if (source.getRedisClient() != null) {entry = getEntry(source.getRedisClient());}if (entry == null && source.getSlot() != null) {//根據slot獲取Redis的主節點entry = getEntry(source.getSlot());}return entry;}...
}public class ClusterConnectionManager extends MasterSlaveConnectionManager {//slot和Redis主節點的原子映射數組private final AtomicReferenceArray<MasterSlaveEntry> slot2entry = new AtomicReferenceArray<>(MAX_SLOT);//Redis客戶端連接和Redis主節點的映射關系private final Map<RedisClient, MasterSlaveEntry> client2entry = new ConcurrentHashMap<>();...@Overridepublic MasterSlaveEntry getEntry(int slot) {//根據slot獲取Redis的主節點return slot2entry.get(slot);}...//初始化連接管理器ClusterConnectionManager時//就已經根據配置初始化好那些slot映射到那個Redis主節點了public ClusterConnectionManager(ClusterServersConfig cfg, Config config, UUID id) {...for (String address : cfg.getNodeAddresses()) {...CompletableFuture<Collection<ClusterPartition>> partitionsFuture = parsePartitions(nodes);Collection<ClusterPartition> partitions = partitionsFuture.join();List<CompletableFuture<Void>> masterFutures = new ArrayList<>();for (ClusterPartition partition : partitions) {...CompletableFuture<Void> masterFuture = addMasterEntry(partition, cfg);masterFutures.add(masterFuture);}...}...}private CompletableFuture<Void> addMasterEntry(ClusterPartition partition, ClusterServersConfig cfg) {...CompletionStage<RedisConnection> connectionFuture = connectToNode(cfg, partition.getMasterAddress(), configEndpointHostName);connectionFuture.whenComplete((connection, ex1) -> {//成功連接時的處理if (ex1 != null) {log.error("Can't connect to master: {} with slot ranges: {}", partition.getMasterAddress(), partition.getSlotRanges());result.completeExceptionally(ex1);return;}MasterSlaveServersConfig config = create(cfg);config.setMasterAddress(partition.getMasterAddress().toString());//創建Redis的主節點MasterSlaveEntry entry;if (config.checkSkipSlavesInit()) {entry = new SingleEntry(ClusterConnectionManager.this, config);} else {Set<String> slaveAddresses = partition.getSlaveAddresses().stream().map(r -> r.toString()).collect(Collectors.toSet());config.setSlaveAddresses(slaveAddresses);entry = new MasterSlaveEntry(ClusterConnectionManager.this, config);}CompletableFuture<RedisClient> f = entry.setupMasterEntry(new RedisURI(config.getMasterAddress()), configEndpointHostName);f.whenComplete((masterClient, ex3) -> {if (ex3 != null) {log.error("Can't add master: " + partition.getMasterAddress() + " for slot ranges: " + partition.getSlotRanges(), ex3);result.completeExceptionally(ex3);return;}//為創建的Redis的主節點添加slot值for (Integer slot : partition.getSlots()) {addEntry(slot, entry);lastPartitions.put(slot, partition);}...});});...}//添加slot到對應節點的映射關系private void addEntry(Integer slot, MasterSlaveEntry entry) {MasterSlaveEntry oldEntry = slot2entry.getAndSet(slot, entry);if (oldEntry != entry) {entry.incReference();shutdownEntry(oldEntry);}client2entry.put(entry.getClient(), entry);}...
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/896828.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/896828.shtml
英文地址,請注明出處:http://en.pswp.cn/news/896828.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

iOS實現一個強大的本地狀態記錄容器

我們開發中經常會遇到這樣的場景&#xff0c;就是我們客戶端用戶進行了某個操作&#xff0c;這個操作影響了數據的狀態&#xff0c;但是我們又不方便重新請求一次數據&#xff0c; 這個時候&#xff0c;就需要我們記錄一下本地狀態在內存中&#xff0c;隨著業務越來越復雜&…

vue中帶$的是什么

在Vue.js中&#xff0c;帶的 $ 符號用于表示 Vue實例的屬性和方法。 這些屬性和方法是Vue框架內部定義的&#xff0c;主要用于方便開發者在組件內部訪問和使用。 常見的帶$的屬性和方法: ?$data?&#xff1a;用于訪問組件的內部數據對象&#xff0c;包含組件內定義的所有響…

杰和科技工業整機AF208|防塵+靜音+全天候運行

在特殊的工業環境中&#xff0c;實現快速生產離不開各類工業計算機的強大支持。杰和科技工業計算機AF208&#xff0c;作為核心控制單元&#xff0c;憑借其堅固可靠的外殼、先進的散熱技術以及緊湊靈活的部署特點&#xff0c;發揮著關鍵作用。 硬實力外殼&#xff0c;無懼塵埃 …

【django】模型部署過程

模型部署 示例&#xff1a;保存 Scikit-learn 模型myapp/views.py全局加載模型tasks.py&#xff08;Celery任務&#xff09;views.py 修改為異步調用views.py 準備工作 模型保存格式 確保你的模型已保存為可加載的格式&#xff1a; ● TensorFlow/Keras&#xff1a;.h5 或 Save…

一、計算機網絡技術——概述、性能指標

網絡技術發展歷程 第一階段 一九六九年美國國防部研制的ARPANET&#xff0c;采用“接口報文處理機”將四臺獨立的計算機主機互聯在一起&#xff0c;實現數據的轉發。 這一階段的主要特點是TCP/IP協議初步成型 第二階段&#xff1a; 采用三級結構&#xff0c;這一階段的主要…

【向量數據庫Weaviate】與ChromaDB的差異、優劣

以下是 Weaviate 和 ChromaDB 的詳細對比&#xff0c;涵蓋設計目標、核心功能、性能、適用場景及優劣勢分析&#xff1a; 1. 核心定位與設計目標 維度WeaviateChromaDB類型向量數據庫 圖數據庫&#xff08;支持混合搜索&#xff09;輕量級純向量數據庫&#xff08;專注嵌入存…

Lua | 每日一練 (4)

&#x1f4a2;歡迎來到張胤塵的技術站 &#x1f4a5;技術如江河&#xff0c;匯聚眾志成。代碼似星辰&#xff0c;照亮行征程。開源精神長&#xff0c;傳承永不忘。攜手共前行&#xff0c;未來更輝煌&#x1f4a5; 文章目錄 Lua | 每日一練 (4)題目參考答案線程和協程調度方式上…

Fiji —— 基于 imageJ 的免費且開源的圖像處理軟件

文章目錄 一、Fiji —— 用于科學圖像處理和分析1.1、工具安裝&#xff08;免費&#xff09;1.2、源碼下載&#xff08;免費&#xff09; 二、功能詳解2.0、Fiji - ImageJ&#xff08;Web應用程序&#xff09;2.1、常用功能&#xff08;匯總&#xff09;2.2、Fiji - Plugins&am…

PyQT(PySide)的上下文菜單策略設置setContextMenuPolicy()

在 Qt 中&#xff0c;QWidget 類提供了幾種不同的上下文菜單策略&#xff0c;這些策略通過 Qt::ContextMenuPolicy 枚舉類型來定義&#xff0c;用于控制控件&#xff08;如按鈕、文本框等&#xff09;在用戶右鍵點擊時如何顯示上下文菜單。 以下是 Qt::ContextMenuPolicy 枚舉中…

快慢指針【等分鏈表、判斷鏈表中是否存在環】

一、等分鏈表&#xff1a;找到鏈表的中間節點 Java 實現 class ListNode {int val;ListNode next;ListNode(int val) {this.val val;this.next null;} }public class MiddleOfLinkedList {public ListNode findMiddleNode(ListNode head) {if (head null) {return null;}L…

系統架構設計師—計算機基礎篇—計算機網絡

文章目錄 網絡互聯模型網絡協議與標準應用層協議FTP協議TFTP協議 HTTP協議HTTPS協議 DHCP動態主機配置協議DNS協議迭代查詢遞歸查詢 傳輸層協議網絡層協議IPV4協議IPV6協議IPV6數據報的目的地址IPV4到IPV6的過渡技術 網絡設計分層設計接入層匯聚層核心層 網絡布線綜合布線系統工…

計算機基礎面試(操作系統)

操作系統 1. 什么是進程和線程&#xff1f;它們的核心區別是什么&#xff1f; 專業解答&#xff1a; 進程是操作系統分配資源的基本單位&#xff0c;擁有獨立的內存空間&#xff1b;線程是進程內的執行單元&#xff0c;共享同一進程的資源。區別在于&#xff1a;進程間資源隔離…

考研408數據結構線性表核心知識點與易錯點詳解(附真題示例與避坑指南)

一、線性表基礎概念 1.1 定義與分類 定義&#xff1a;線性表是由n&#xff08;n≥0&#xff09;個相同類型數據元素構成的有限序列&#xff0c;元素間呈線性關系。 分類&#xff1a; 順序表&#xff1a;元素按邏輯順序存儲在一段連續的物理空間中&#xff08;數組實現&…

【實戰 ES】實戰 Elasticsearch:快速上手與深度實踐-1.2.2倒排索引原理與分詞器(Analyzer)

&#x1f449; 點擊關注不迷路 &#x1f449; 點擊關注不迷路 &#x1f449; 點擊關注不迷路 文章大綱 1.2.2倒排索引原理與分詞器&#xff08;Analyzer&#xff09;1. 倒排索引&#xff1a;搜索引擎的基石1.1 正排索引 vs 倒排索引示例數據對比&#xff1a; 1.2 倒排索引核心結…

Springboot項目本地連接并操作MySQL數據庫

目錄 前提 準備工作 用cmd在本地創建數據庫、表&#xff1a; 1.創建springboot項目&#xff08;已有可跳過&#xff09; 2.編輯Mybatis配置 3.連接數據庫 4.創建模型類&#xff0c;用于與數據庫里的數據表相連 5.創建接口mapper&#xff0c;定義對數據庫的操作 6.創建…

《寶塔 Nginx SSL 端口管理實戰指南:域名解析、端口沖突與后端代理解析》

&#x1f4e2; Nginx & SSL 端口管理分析 1?? 域名解析與 SSL 申請失敗分析 在使用寶塔申請 www.mywebsite.test 的 SSL 證書時&#xff0c;遇到了解析失敗的問題。最初&#xff0c;我認為 www 只是一個附加的前綴&#xff0c;不屬于域名的關鍵部分&#xff0c;因此只為…

java和Springboot和vue開發的企業批量排班系統人臉識別考勤打卡系統

演示視頻&#xff1a; https://www.bilibili.com/video/BV1KU9iYsEBU/?spm_id_from888.80997.embed_other.whitelist&t52.095574&bvidBV1KU9iYsEBU 主要功能&#xff1a; 管理員管理員工&#xff0c;采集員工人臉特征值存入數據庫&#xff0c;可選擇多個員工批量排班…

DeepSeek學習規劃

DeepSeek是一個專注于深度學習和人工智能技術研究與應用的平臺&#xff0c;旨在通過系統化的學習和實踐&#xff0c;幫助用戶掌握深度學習領域的核心知識和技能。為了在DeepSeek平臺上高效學習&#xff0c;制定一個科學合理的學習規劃至關重要。以下是一個詳細的學習規劃&#…

打開 Windows Docker Desktop 出現 Docker Engine Stopped 問題

一、關聯文章: 1、Docker Desktop 安裝使用教程 2、家庭版 Windows 安裝 Docker 沒有 Hyper-V 問題 3、安裝 Windows Docker Desktop - WSL問題 二、問題解析 打開 Docker Desktop 出現問題,如下: Docker Engine Stopped : Docker引擎停止三、解決方法 1、檢查服務是否…

突破Ajax跨域困境,解鎖前端通信新姿勢

一、引言 在當今的 Web 開發領域&#xff0c;前后端分離的架構模式已經成為主流&#xff0c;它極大地提升了開發效率和項目的可維護性。在這種開發模式下&#xff0c;前端通過 Ajax 技術與后端進行數據交互&#xff0c;然而&#xff0c;跨域問題卻如影隨形&#xff0c;成為了開…