分布式鎖—Redisson的同步器組件

1.Redisson的分布式鎖簡單總結

Redisson分布式鎖包括:可重入鎖、公平鎖、聯鎖、紅鎖、讀寫鎖。

(1)可重入鎖RedissonLock

非公平鎖,最基礎的分布式鎖,最常用的鎖。

(2)公平鎖RedissonFairLock

各個客戶端嘗試獲取鎖時會排隊,按照隊列的順序先后獲取鎖。

(3)聯鎖MultiLock

可以一次性加多把鎖,從而實現一次性鎖多個資源。

(4)紅鎖RedLock

RedLock相當于一把鎖。雖然利用了MultiLock包裹了多個小鎖,但這些小鎖并不對應多個資源,而是每個小鎖的key對應一個Redis實例。只要大多數的Redis實例加鎖成功,就可以認為RedLock加鎖成功。RedLock的健壯性要比其他普通鎖要好。

但是RedLock也有一些場景無法保證正確性,當然RedLock只要求部署主庫。比如客戶端A嘗試向5個Master實例加鎖,但僅僅在3個Maste中加鎖成功。不幸的是此時3個Master中有1個Master突然宕機了,而且鎖key還沒同步到該宕機Master的Slave上,此時Salve切換為Master。于是在這5個Master中,由于其中有一個是新切換過來的Master,所以只有2個Master是有客戶端A加鎖的數據,另外3個Master是沒有鎖的。但繼續不幸的是,此時客戶端B來加鎖,那么客戶端B就很有可能成功在沒有鎖數據的3個Master上加到鎖,從而滿足了過半數加鎖的要求,最后也完成了加鎖,依然發生重復加鎖。

(5)讀寫鎖之讀鎖RedissonReadLock和寫鎖RedissonWriteLock

不同客戶端線程的四種加鎖情況:

情況一:先加讀鎖再加讀鎖,不互斥

情況二:先加讀鎖再加寫鎖,互斥

情況三:先加寫鎖再加讀鎖,互斥

情況四:先加寫鎖再加寫鎖,互斥

同一個客戶端線程的四種加鎖情況:

情況一:先加讀鎖再加讀鎖,不互斥

情況二:先加讀鎖再加寫鎖,互斥

情況三:先加寫鎖再加讀鎖,不互斥

情況四:先加寫鎖再加寫鎖,不互斥

2.Redisson的Semaphore簡介

(1)Redisson的Semaphore原理圖

Semaphore也是Redisson支持的一種同步組件。Semaphore作為一個鎖機制,可以允許多個線程同時獲取一把鎖。任何一個線程釋放鎖之后,其他等待的線程就可以嘗試繼續獲取鎖。

(2)Redisson的Semaphore使用演示

public class RedissonDemo {public static void main(String[] args) throws Exception {//連接3主3從的Redis CLusterConfig config = new Config();...//SemaphoreRedissonClient redisson = Redisson.create(config);final RSemaphore semaphore = redisson.getSemaphore("semaphore");semaphore.trySetPermits(3);for (int i = 0; i < 10; i++) {new Thread(new Runnable() {public void run() {try {System.out.println(new Date() + ":線程[" + Thread.currentThread().getName() + "]嘗試獲取Semaphore鎖");semaphore.acquire();System.out.println(new Date() + ":線程[" + Thread.currentThread().getName() + "]成功獲取到了Semaphore鎖,開始工作");Thread.sleep(3000);semaphore.release();System.out.println(new Date() + ":線程[" + Thread.currentThread().getName() + "]釋放Semaphore鎖");} catch (Exception e) {e.printStackTrace();}}}).start();}}
}

3.Redisson的Semaphore源碼剖析

(1)Semaphore的初始化

public class Redisson implements RedissonClient {//Redis的連接管理器,封裝了一個Config實例protected final ConnectionManager connectionManager;//Redis的命令執行器,封裝了一個ConnectionManager實例protected final CommandAsyncExecutor commandExecutor;...protected Redisson(Config config) {this.config = config;Config configCopy = new Config(config);//初始化Redis的連接管理器connectionManager = ConfigSupport.createConnectionManager(configCopy);...  //初始化Redis的命令執行器commandExecutor = new CommandSyncService(connectionManager, objectBuilder);...}@Overridepublic RSemaphore getSemaphore(String name) {return new RedissonSemaphore(commandExecutor, name);}...
}public class RedissonSemaphore extends RedissonExpirable implements RSemaphore {private final SemaphorePubSub semaphorePubSub;final CommandAsyncExecutor commandExecutor;public RedissonSemaphore(CommandAsyncExecutor commandExecutor, String name) {super(commandExecutor, name);this.commandExecutor = commandExecutor;this.semaphorePubSub = commandExecutor.getConnectionManager().getSubscribeService().getSemaphorePubSub();}...
}

(2)Semaphore設置允許獲取的鎖數量

public class RedissonSemaphore extends RedissonExpirable implements RSemaphore {...@Overridepublic boolean trySetPermits(int permits) {return get(trySetPermitsAsync(permits));}@Overridepublic RFuture<Boolean> trySetPermitsAsync(int permits) {RFuture<Boolean> future = commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,//執行命令"get semaphore",獲取到當前的數值"local value = redis.call('get', KEYS[1]); " +"if (value == false) then " +//然后執行命令"set semaphore 3"//設置這個信號量允許客戶端同時獲取鎖的總數量為3"redis.call('set', KEYS[1], ARGV[1]); " +"redis.call('publish', KEYS[2], ARGV[1]); " +"return 1;" +"end;" +"return 0;",Arrays.asList(getRawName(), getChannelName()),permits);if (log.isDebugEnabled()) {future.onComplete((r, e) -> {if (r) {log.debug("permits set, permits: {}, name: {}", permits, getName());} else {log.debug("unable to set permits, permits: {}, name: {}", permits, getName());}});}return future;}...
}

首先執行命令"get semaphore",獲取到當前的數值。然后執行命令"set semaphore 3",也就是設置這個信號量允許客戶端同時獲取鎖的總數量為3。

(3)客戶端嘗試獲取Semaphore的鎖

public class RedissonSemaphore extends RedissonExpirable implements RSemaphore {...private final SemaphorePubSub semaphorePubSub;final CommandAsyncExecutor commandExecutor;public RedissonSemaphore(CommandAsyncExecutor commandExecutor, String name) {super(commandExecutor, name);this.commandExecutor = commandExecutor;this.semaphorePubSub = commandExecutor.getConnectionManager().getSubscribeService().getSemaphorePubSub();}@Overridepublic void acquire() throws InterruptedException {acquire(1);}@Overridepublic void acquire(int permits) throws InterruptedException {if (tryAcquire(permits)) {return;}CompletableFuture<RedissonLockEntry> future = subscribe();commandExecutor.syncSubscriptionInterrupted(future);try {while (true) {if (tryAcquire(permits)) {return;}//獲取Redisson的Semaphore失敗,于是便調用本地JDK的Semaphore的acquire()方法,此時當前線程會被阻塞//之后如果Redisson的Semaphore釋放了鎖,那么當前客戶端便會通過監聽訂閱事件釋放本地JDK的Semaphore,喚醒被阻塞的線程,繼續執行while循環//注意:getLatch()返回的是JDK的Semaphore = "new Semaphore(0)" ==> (state - permits)//首先調用CommandAsyncService.getNow()方法//然后調用RedissonLockEntry.getLatch()方法//接著調用JDK的Semaphore的acquire()方法commandExecutor.getNow(future).getLatch().acquire();}} finally {unsubscribe(commandExecutor.getNow(future));}}@Overridepublic boolean tryAcquire(int permits) {//異步轉同步return get(tryAcquireAsync(permits));}@Overridepublic RFuture<Boolean> tryAcquireAsync(int permits) {if (permits < 0) {throw new IllegalArgumentException("Permits amount can't be negative");}if (permits == 0) {return RedissonPromise.newSucceededFuture(true);}return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,//執行命令"get semaphore",獲取到當前值"local value = redis.call('get', KEYS[1]); "+//如果semaphore的當前值不是false,且大于客戶端線程申請獲取鎖的數量"if (value ~= false and tonumber(value) >= tonumber(ARGV[1])) then " +//執行"decrby semaphore 1",將信號量允許獲取鎖的總數量遞減1"local val = redis.call('decrby', KEYS[1], ARGV[1]); " +"return 1; " +"end; " +//如果semaphore的值變為0,那么客戶端就無法獲取鎖了,此時返回false"return 0;",Collections.<Object>singletonList(getRawName()),permits//ARGV[1]默認是1);}...
}public class CommandAsyncService implements CommandAsyncExecutor {...@Overridepublic <V> V getNow(CompletableFuture<V> future) {try {return future.getNow(null);} catch (Exception e) {return null;}}...
}public class RedissonLockEntry implements PubSubEntry<RedissonLockEntry> {private final Semaphore latch;...public RedissonLockEntry(CompletableFuture<RedissonLockEntry> promise) {super();this.latch = new Semaphore(0);this.promise = promise;}public Semaphore getLatch() {return latch;}...
}

執行命令"get semaphore",獲取到semaphore的當前值。如果semaphore的當前值不是false,且大于客戶端線程申請獲取鎖的數量。那么就執行"decrby semaphore 1",將信號量允許獲取鎖的總數量遞減1。

如果semaphore的值變為0,那么客戶端就無法獲取鎖了,此時tryAcquire()方法返回false。表示獲取semaphore的鎖失敗了,于是當前客戶端線程便會通過本地JDK的Semaphore進行阻塞。

當客戶端后續收到一個訂閱事件把本地JDK的Semaphore進行釋放后,便會喚醒阻塞線程繼續while循環。在while循環中,會不斷嘗試獲取這個semaphore的鎖,如此循環往復,直到成功獲取。

(4)客戶端釋放Semaphore的鎖

public class RedissonSemaphore extends RedissonExpirable implements RSemaphore {...@Overridepublic void release() {release(1);}@Overridepublic void release(int permits) {get(releaseAsync(permits));}@Overridepublic RFuture<Void> releaseAsync(int permits) {if (permits < 0) {throw new IllegalArgumentException("Permits amount can't be negative");}if (permits == 0) {return RedissonPromise.newSucceededFuture(null);}RFuture<Void> future = commandExecutor.evalWriteAsync(getRawName(), StringCodec.INSTANCE, RedisCommands.EVAL_VOID,//執行命令"incrby semaphore 1""local value = redis.call('incrby', KEYS[1], ARGV[1]); " +"redis.call('publish', KEYS[2], value); ",Arrays.asList(getRawName(), getChannelName()),permits);if (log.isDebugEnabled()) {future.onComplete((o, e) -> {if (e == null) {log.debug("released, permits: {}, name: {}", permits, getName());}});}return future;}...
}//訂閱semaphore不為0的事件,semaphore不為0時會觸發執行這里的監聽回調
public class SemaphorePubSub extends PublishSubscribe<RedissonLockEntry> {public SemaphorePubSub(PublishSubscribeService service) {super(service);}@Overrideprotected RedissonLockEntry createEntry(CompletableFuture<RedissonLockEntry> newPromise) {return new RedissonLockEntry(newPromise);}@Overrideprotected void onMessage(RedissonLockEntry value, Long message) {Runnable runnableToExecute = value.getListeners().poll();if (runnableToExecute != null) {runnableToExecute.run();}//將客戶端本地JDK的Semaphore進行釋放value.getLatch().release(Math.min(value.acquired(), message.intValue()));}
}//訂閱鎖被釋放的事件,鎖被釋放為0時會觸發執行這里的監聽回調
public class LockPubSub extends PublishSubscribe<RedissonLockEntry> {public static final Long UNLOCK_MESSAGE = 0L;public static final Long READ_UNLOCK_MESSAGE = 1L;public LockPubSub(PublishSubscribeService service) {super(service);}  @Overrideprotected RedissonLockEntry createEntry(CompletableFuture<RedissonLockEntry> newPromise) {return new RedissonLockEntry(newPromise);}@Overrideprotected void onMessage(RedissonLockEntry value, Long message) {if (message.equals(UNLOCK_MESSAGE)) {Runnable runnableToExecute = value.getListeners().poll();if (runnableToExecute != null) {runnableToExecute.run();}value.getLatch().release();} else if (message.equals(READ_UNLOCK_MESSAGE)) {while (true) {Runnable runnableToExecute = value.getListeners().poll();if (runnableToExecute == null) {break;}runnableToExecute.run();}//將客戶端本地JDK的Semaphore進行釋放value.getLatch().release(value.getLatch().getQueueLength());}}
}

客戶端釋放Semaphore的鎖時,會執行命令"incrby semaphore 1"。每當客戶端釋放掉permits個鎖,就會將信號量的值累加permits,這樣Semaphore信號量的值就不再是0了。然后通過publish命令發布一個事件,之后訂閱了該事件的其他客戶端都會對getLatch()返回的本地JDK的Semaphore進行加1。于是其他客戶端正在被本地JDK的Semaphore進行阻塞的線程,就會被喚醒繼續執行。此時其他客戶端就可以嘗試獲取到這個信號量的鎖,然后再次將這個Semaphore的值遞減1。

4.Redisson的CountDownLatch簡介

(1)Redisson的CountDownLatch原理圖解

CountDownLatch的基本原理:要求必須有n個線程來進行countDown,才能讓執行await的線程繼續執行。如果沒有達到指定數量的線程來countDown,會導致執行await的線程阻塞。

(2)Redisson的CountDownLatch使用演示

public class RedissonDemo {public static void main(String[] args) throws Exception {//連接3主3從的Redis CLusterConfig config = new Config();...//CountDownLatchfinal RedissonClient redisson = Redisson.create(config);RCountDownLatch latch = redisson.getCountDownLatch("myCountDownLatch");//1.設置可以countDown的數量為3latch.trySetCount(3);System.out.println(new Date() + ":線程[" + Thread.currentThread().getName() + "]設置了必須有3個線程執行countDown,進入等待中。。。");for (int i = 0; i < 3; i++) {new Thread(new Runnable() {public void run() {try {System.out.println(new Date() + ":線程[" + Thread.currentThread().getName() + "]在做一些操作,請耐心等待。。。。。。");Thread.sleep(3000);RCountDownLatch localLatch = redisson.getCountDownLatch("myCountDownLatch");localLatch.countDown();System.out.println(new Date() + ":線程[" + Thread.currentThread().getName() + "]執行countDown操作");} catch (Exception e) {e.printStackTrace();}}}).start();}latch.await();System.out.println(new Date() + ":線程[" + Thread.currentThread().getName() + "]收到通知,有3個線程都執行了countDown操作,可以繼續往下執行");}
}

5.Redisson的CountDownLatch源碼剖析

(1)CountDownLatch的初始

public class Redisson implements RedissonClient {//Redis的連接管理器,封裝了一個Config實例protected final ConnectionManager connectionManager;//Redis的命令執行器,封裝了一個ConnectionManager實例protected final CommandAsyncExecutor commandExecutor;...protected Redisson(Config config) {this.config = config;Config configCopy = new Config(config);//初始化Redis的連接管理器connectionManager = ConfigSupport.createConnectionManager(configCopy);...  //初始化Redis的命令執行器commandExecutor = new CommandSyncService(connectionManager, objectBuilder);...}@Overridepublic RCountDownLatch getCountDownLatch(String name) {return new RedissonCountDownLatch(commandExecutor, name);}...
}public class RedissonCountDownLatch extends RedissonObject implements RCountDownLatch {...private final CountDownLatchPubSub pubSub;private final String id;protected RedissonCountDownLatch(CommandAsyncExecutor commandExecutor, String name) {super(commandExecutor, name);this.id = commandExecutor.getConnectionManager().getId();this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getCountDownLatchPubSub();}...
}

(2)trySetCount()方法設置countDown的數量

trySetCount()方法的工作就是執行命令"set myCountDownLatch 3"。

public class RedissonCountDownLatch extends RedissonObject implements RCountDownLatch {...@Overridepublic boolean trySetCount(long count) {return get(trySetCountAsync(count));}@Overridepublic RFuture<Boolean> trySetCountAsync(long count) {return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"if redis.call('exists', KEYS[1]) == 0 then " +"redis.call('set', KEYS[1], ARGV[2]); " +"redis.call('publish', KEYS[2], ARGV[1]); " +"return 1 " +"else " +"return 0 " +"end",Arrays.asList(getRawName(), getChannelName()),CountDownLatchPubSub.NEW_COUNT_MESSAGE,count);}...
}

(3)awati()方法進行阻塞等待

public class RedissonCountDownLatch extends RedissonObject implements RCountDownLatch {...@Overridepublic void await() throws InterruptedException {if (getCount() == 0) {return;}CompletableFuture<RedissonCountDownLatchEntry> future = subscribe();try {commandExecutor.syncSubscriptionInterrupted(future);while (getCount() > 0) {// waiting for open state//獲取countDown的數量還大于0,就先阻塞線程,然后再等待喚醒,執行while循環//其中getLatch()返回的是JDK的semaphore = "new Semaphore(0)" ==> (state - permits)commandExecutor.getNow(future).getLatch().await();}} finally {unsubscribe(commandExecutor.getNow(future));}}@Overridepublic long getCount() {return get(getCountAsync());}@Overridepublic RFuture<Long> getCountAsync() {//執行命令"get myCountDownLatch"return commandExecutor.writeAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.GET_LONG, getRawName());}...
}

在while循環中,首先會執行命令"get myCountDownLatch"去獲取countDown值。如果該值不大于0,就退出循環不阻塞線程。如果該值大于0,則說明還沒有指定數量的線程去執行countDown操作,于是就會先阻塞線程,然后再等待喚醒來繼續循環。

(4)countDown()方法對countDown的數量遞減

public class RedissonCountDownLatch extends RedissonObject implements RCountDownLatch {...@Overridepublic void countDown() {get(countDownAsync());}@Overridepublic RFuture<Void> countDownAsync() {return commandExecutor.evalWriteNoRetryAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"local v = redis.call('decr', KEYS[1]);" +"if v <= 0 then redis.call('del', KEYS[1]) end;" +"if v == 0 then redis.call('publish', KEYS[2], ARGV[1]) end;",Arrays.<Object>asList(getRawName(), getChannelName()),CountDownLatchPubSub.ZERO_COUNT_MESSAGE);}...
}

countDownAsync()方法會執行decr命令,將countDown的數量進行遞減1。如果這個值已經小于等于0,就執行del命令刪除掉該CoutDownLatch。如果是這個值為0,還會發布一條消息:

publish redisson_countdownlatch__channel__{anyCountDownLatch} 0

文章轉載自:東陽馬生架構

原文鏈接:分布式鎖—6.Redisson的同步器組件 - 東陽馬生架構 - 博客園

體驗地址:引邁 - JNPF快速開發平臺_低代碼開發平臺_零代碼開發平臺_流程設計器_表單引擎_工作流引擎_軟件架構

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

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

相關文章

國產編輯器EverEdit - 腳本(解鎖文本編輯的無限可能)

1 腳本 1.1 應用場景 腳本是一種功能擴展代碼&#xff0c;用于提供一些編輯器通用功能提供不了的功能&#xff0c;幫助用戶在特定工作場景下提高工作效率&#xff0c;幾乎所有主流的編輯器、IDE都支持腳本。 ??EverEdit的腳本支持js(語法與javascript類似)、VBScript兩種編程…

服務器上的nginx因漏洞掃描需要升級

前言 最近客戶聯系說nginx存在安全漏洞 F5 Nginx 安全漏洞(CVE-2024-7347) F5Nginx是美國F5公司的一款輕量級Web服務器/反向代理服務器及電子郵件(IMAP/POP3)代理服務器&#xff0c;在BSD-like協議下發行。F5 Nginx存在安全漏洞&#xff0c;該漏洞源于可能允許攻擊者使用特制的…

ASP.NET CORE MVC EF框架

1.一個視圖中的多個表單Form中的變量。 方式一&#xff1a;視圖中跨Form變量不能用&#xff0c;得各自定義變量否則編譯不能通過。變量名還不能相同。 或者方式二&#xff1a;在Form之外定義變量 {ViewData["Title"] "ExpenseForm"; } &#xfeff; {L…

【MySQL 中 `TINYINT` 類型與布爾值的關系】

MySQL 中 TINYINT 類型與布爾值的關系 在 MySQL 數據庫中&#xff0c;BOOLEAN 類型并不存在&#xff0c;BOOLEAN 或 BOOL 都是 TINYINT(1) 的別名。通常&#xff0c;TINYINT(1) 類型用于存儲布爾值。 1. TINYINT 類型介紹 TINYINT 是一個占用 1 字節的整數類型&#xff0c;取…

【Rust基礎】Rust后端開發常用庫

使用Rust有一段時間了&#xff0c;期間嘗試過使用Rust做后端開發、命令行工具開發&#xff0c;以及做端側模型部署&#xff0c;也嘗試過交叉編譯、FFI調用等&#xff0c;也算是基本入門了。在用Rust做后端接口開發時&#xff0c;常常會找不到一些合適庫&#xff0c;而這些庫在J…

[leetcode]位運算

一.AND &運算 注&#xff1a;兩個操作數做&運算結果是不會變大的 二.OR |運算 注&#xff1a;兩個操作數做|運算結果是不會變小的 三.XOR(異或) ^運算 注&#xff1a;結果可能變大也可能變小也可能不變&#xff0c;但是不會導致進位&#xff0c;比如兩個四位的數字做…

常見FUZZ姿勢與工具實戰:從未知目錄到備份文件漏洞挖掘

本文僅供學習交流使用&#xff0c;嚴禁用于非法用途。未經授權&#xff0c;禁止對任何網站或系統進行未授權的測試或攻擊。因使用本文所述技術造成的任何后果&#xff0c;由使用者自行承擔。請嚴格遵守《網絡安全法》及相關法律法規&#xff01; 目錄 本文僅供學習交流使用&am…

前置機跟服務器的關系

在復雜的IT系統架構中&#xff0c;前置機與服務器的協同配合是保障業務高效、安全運行的關鍵。兩者的關系既非簡單的上下級&#xff0c;也非獨立個體&#xff0c;而是通過功能分層與職責分工&#xff0c;構建起一套既能應對高并發壓力、又能抵御安全風險的彈性體系。 在當今復…

MySQL中有哪些索引

1&#xff0c;B-Tree索引&#xff1a;常見的索引類型 2&#xff0c;哈希索引&#xff1a;基于哈希表實現&#xff0c;只支持等值查詢 &#xff0c;只有Memory存儲引擎和NDB Cluster存儲引擎顯示支持哈希索引 3&#xff0c;全文索引&#xff1a;可在字符列上創建&#xff08;T…

Python爬蟲---中國大學MOOC爬取數據(文中有數據集)

1、內容簡介 本文為大二在校學生所做&#xff0c;內容為爬取中國大學Mooc網站的課程分類數據、課程數據、評論數據。數據集大佬們需要拿走。主要是希望大佬們能指正代碼問題。 2、數據集 課程評論數據集&#xff0c;343525條&#xff08;包括評論id、評論時間、發送評論用戶…

Tomcat 安裝

一、Tomcat 下載 官網&#xff1a;Apache Tomcat - Welcome! 1.1.下載安裝包 下載安裝包&#xff1a; wget https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.102/bin/apache-tomcat-9.0.102.tar.gz 安裝 javajdk。 yum install java-1.8.0-openjdk.x86_64 -y /etc/altern…

MC34063數據手冊解讀:功能、應用與設計指南

MC34063A/MC33063A 系列是摩托羅拉&#xff08;現 NXP&#xff09;推出的高集成度 DC-DC 轉換器控制電路&#xff0c;適用于降壓、升壓和反相應用。本文將基于官方數據手冊&#xff0c;對其核心功能、關鍵參數、典型應用及設計要點進行詳細解讀。 一、核心功能與特性 集成度高…

基于SpringBoot實現旅游酒店平臺功能十一

一、前言介紹&#xff1a; 1.1 項目摘要 隨著社會的快速發展和人民生活水平的不斷提高&#xff0c;旅游已經成為人們休閑娛樂的重要方式之一。人們越來越注重生活的品質和精神文化的追求&#xff0c;旅游需求呈現出爆發式增長。這種增長不僅體現在旅游人數的增加上&#xff0…

Linux入門 全面整理終端 Bash、Vim 基礎命令速記

Linux入門 2025 超詳細全面整理 Bash、Vim 基礎命令速記 剛面對高級感滿滿的 終端窗口是不是有點懵&#xff1f;于是乎&#xff0c;這份手冊就是為你準備的高效學習指南&#xff01;我把那些讓人頭大的系統設置、記不住的命令都整理成了對你更友好的格式&#xff0c;讓你快速學…

基于deepseek的圖像生成系統

目錄 問題 核心思路 pollinations 提示詞 基于deepseek的圖像生成系統 項目說明 詳細說明 1. 注冊流程 2. 登錄流程 3. 圖片生成流程 4. 圖片下載流程 項目結構 代碼實現 1. 配置文件 config.py 2. 數據庫模型 models.py 3. 解決循環引用 exts.py 4. 登錄和…

mac安裝mysql之后報錯zsh: command not found: mysql !

在Mac上安裝MySQL后&#xff0c;如果終端中找不到mysql命令&#xff0c;通常是 因為MySQL的命令行工具&#xff08;如mysql客戶端&#xff09;沒有被正確地添加到你的環境變量中。 檢查 MySQL 是否已安裝 ps -ef|grep mysql查看到路徑在 /usr/local/mysql/bin 查看 .bash_pro…

骨質健康護理筆記

1. 閱讀資料 《骨質疏松癥不是“老年病”&#xff01;除了補鈣、曬太陽&#xff0c;專家還推薦… —— 健康湖北》

CSS3 用戶界面設計指南

CSS3 用戶界面設計指南 引言 隨著互聯網的快速發展,用戶界面設計已經成為網站和應用程序吸引和留住用戶的關鍵因素之一。CSS3,作為Web開發中的核心技術之一,提供了豐富的工具和特性來改善用戶界面。本文將深入探討CSS3在用戶界面設計中的應用,包括基本概念、常用技巧以及…

Mybatis3 調用存儲過程

1. 數據庫MySQL&#xff0c;user表 CREATE TABLE user (USER_ID int NOT NULL AUTO_INCREMENT,USER_NAME varchar(100) NOT NULL COMMENT 用戶姓名,AGE int NOT NULL COMMENT 年齡,CREATED_TIME datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,CREATED_BY varchar(100) NOT NUL…

Uniapp組件 Textarea 字數統計和限制

Uniapp Textarea 字數統計和限制 在 Uniapp 中&#xff0c;可以通過監聽 textarea 的 input 事件來實現字數統計功能。以下是一個簡單的示例&#xff0c;展示如何在 textarea 的右下角顯示輸入的字符數。 示例代碼 首先&#xff0c;在模板中定義一個 textarea 元素&#xff…