MySQL鎖機(jī)制和鎖算法?

MyISAM和InnoDB存儲引擎鎖區(qū)別
MyISAM采?表級鎖(table-level locking)。InnoDB?持?級鎖(row-level locking)和表級鎖,默認(rèn)為?級鎖
表級鎖和?級鎖對?
?表級鎖:MySQL中鎖定 粒度最? 的?種鎖,對當(dāng)前操作的整張表加鎖,實(shí)現(xiàn)簡單,資源消耗也 少,加鎖快,不會出現(xiàn)死鎖。其鎖定粒度最?,觸發(fā)鎖沖突的概率最?,并發(fā)度最低, MyISAM和 InnoDB引擎都?持表級鎖。??級鎖:MySQL中鎖定?粒度最??的?種鎖,只針對當(dāng)前操作的?進(jìn)?加鎖。?級鎖能??減 少數(shù)據(jù)庫操作的沖突。其加鎖粒度最?,并發(fā)度?,但加鎖的開銷也最?,加鎖慢,會出現(xiàn)死 鎖。
InnoDB鎖機(jī)制
InnoDB 表級鎖的鎖模式
MySQL 的表級鎖有兩種模式:表共享讀鎖(Table Read Lock)和表獨(dú)占寫鎖(Table Write Lock)。
InnoDB 行鎖模式及加鎖方法
InnoDB 實(shí)現(xiàn)了以下兩種類型的行鎖。
?共享鎖(S):允許一個事務(wù)去讀一行,阻止其他事務(wù)獲得相同數(shù)據(jù)集的排他鎖。?排他鎖(X):允許獲得排他鎖的事務(wù)更新數(shù)據(jù),阻止其他事務(wù)取得相同數(shù)據(jù)集的共享 讀鎖和排他寫鎖。
總結(jié):S鎖之間不存在沖突,X鎖之間存在沖突
另外,為了允許行鎖和表鎖共存,實(shí)現(xiàn)多粒度鎖機(jī)制,InnoDB 還有兩種內(nèi)部使用的意 向鎖(Intention Locks),這兩種意向鎖都是表鎖。
?意向共享鎖(IS):事務(wù)打算給數(shù)據(jù)行加行共享鎖,事務(wù)在給一個數(shù)據(jù)行加共享鎖前 必須先取得該表的IS 鎖。?意向排他鎖(IX):事務(wù)打算給數(shù)據(jù)行加行排他鎖,事務(wù)在給一個數(shù)據(jù)行加排他鎖前 必須先取得該表的IX 鎖。
如果一個事務(wù)請求的鎖模式與當(dāng)前的鎖兼容,InnoDB 就將請求的鎖授予該事務(wù);反之,如果兩者不兼容,該事務(wù)就要等待鎖釋放。
意向鎖是InnoDB 自動加的,不需用戶干預(yù)。對于UPDATE、DELETE 和INSERT 語句,InnoDB 會自動給涉及數(shù)據(jù)集加排他鎖(X);
對于普通SELECT 語句,InnoDB 不會加任何鎖;事務(wù)可以通過以下語句顯示給記錄集加共享鎖或排他鎖。
?共享鎖(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。?排他鎖(X):SELECT * FROM table_name WHERE ... FOR UPDATE。
用SELECT ... IN SHARE MODE 獲得共享鎖,主要用在需要數(shù)據(jù)依存關(guān)系時來確認(rèn)某行記錄是否存
在,并確保沒有人對這個記錄進(jìn)行UPDATE 或者DELETE 操作。但是如果當(dāng)前事務(wù)也需要對該記錄進(jìn)
行更新操作,則很有可能造成死鎖,對于鎖定行記錄后需要進(jìn)行更新操作的應(yīng)用,應(yīng)該使用SELECT...
FOR UPDATE 方式獲得排他鎖。
InnoDB 行鎖實(shí)現(xiàn)方式
InnoDB 行鎖是通過給索引上的索引項(xiàng)加鎖來實(shí)現(xiàn)的。
InnoDB 這種行鎖實(shí)現(xiàn)特點(diǎn)意味著:只有通過索引條件檢索數(shù)據(jù),InnoDB 才使用行級鎖,否則,InnoDB 將使用表鎖!
(1)在不通過索引條件查詢的時候,InnoDB 確實(shí)使用的是表鎖,而不是行鎖。

看起來session_1 只給一行加了排他鎖,但session_2 在請求其他行的排他鎖時,卻出現(xiàn)了鎖等待!原因就是在沒有索引的情況下,InnoDB 只能使用表鎖。當(dāng)我們給其增加一個索引后,InnoDB 就只鎖定了符合條件的行,
(2)由于MySQL 的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然是訪問不同行的記錄,但是如果是使用相同的索引鍵,是會出現(xiàn)鎖沖突的。

(3)當(dāng)表有多個索引的時候,不同的事務(wù)可以使用不同的索引鎖定不同的行,另外,不論 是使用主鍵索引、唯一索引或普通索引,InnoDB 都會使用行鎖來對數(shù)據(jù)加鎖。
(4)即便在條件中使用了索引字段,但是否使用索引來檢索數(shù)據(jù)是由MySQL 通過判斷不同執(zhí)行計(jì)劃的代價來決定的,如果MySQL 認(rèn)為全表掃描效率更高,比如對一些很小的表,它就不會使用索引,這種情況下InnoDB 將使用表鎖,而不是行鎖。因此,在分析鎖沖突時,別忘了檢查SQL 的執(zhí)行計(jì)劃,以確認(rèn)是否真正使用了索引。
樂觀鎖和悲觀鎖
在數(shù)據(jù)庫的鎖機(jī)制中介紹過,數(shù)據(jù)庫管理系統(tǒng)(DBMS)中的并發(fā)控制的任務(wù)是確保在多個事務(wù)同時存取數(shù)據(jù)庫中同一數(shù)據(jù)時不破壞事務(wù)的隔離性和統(tǒng)一性以及數(shù)據(jù)庫的統(tǒng)一性。
樂觀并發(fā)控制(樂觀鎖)和悲觀并發(fā)控制(悲觀鎖)是并發(fā)控制主要采用的技術(shù)手段。
悲觀鎖
在關(guān)系數(shù)據(jù)庫管理系統(tǒng)里,悲觀并發(fā)控制(又名“悲觀鎖”,Pessimistic Concurrency Control,縮寫“PCC”)是一種并發(fā)控制的方法。它可以阻止一個事務(wù)以影響其他用戶的方式來修改數(shù)據(jù)。如果一個事務(wù)執(zhí)行的操作對某行數(shù)據(jù)應(yīng)用了鎖,那只有當(dāng)這個事務(wù)把鎖釋放,其他事務(wù)才能夠執(zhí)行與該鎖沖突的操作。悲觀并發(fā)控制主要用于數(shù)據(jù)爭用激烈的環(huán)境,以及發(fā)生并發(fā)沖突時使用鎖保護(hù)數(shù)據(jù)的成本要低于回滾事務(wù)的成本的環(huán)境中。
?悲觀鎖,正如其名,它指的是對數(shù)據(jù)被外界(包括本系統(tǒng)當(dāng)前的其他事務(wù),以及來自外部系統(tǒng)的事務(wù)處理)修改持保守態(tài)度(悲觀),因此,在整個數(shù)據(jù)處理過程中,將數(shù)據(jù)處于鎖定狀態(tài)。
悲觀鎖的實(shí)現(xiàn),往往依靠數(shù)據(jù)庫提供的鎖機(jī)制 (也只有數(shù)據(jù)庫層提供的鎖機(jī)制才能真正保證數(shù)據(jù)訪問的排他性,否則,即使在本系統(tǒng)中實(shí)現(xiàn)了加鎖機(jī)制,也無法保證外部系統(tǒng)不會修改數(shù)據(jù))
悲觀鎖的具體流程:
?在對任意記錄進(jìn)行修改前,先嘗試為該記錄加上排他鎖(exclusive locking)?如果加鎖失敗,說明該記錄正在被修改,那么當(dāng)前查詢可能要等待或者拋出異常。具體響應(yīng)方式由開發(fā)者根據(jù)實(shí)際需要決定。?如果成功加鎖,那么就可以對記錄做修改,事務(wù)完成后就會解鎖了。?其間如果有其他對該記錄做修改或加排他鎖的操作,都會等待我們解鎖或直接拋出異常。
在mysql/InnoDB中使用悲觀鎖:
首先我們得關(guān)閉mysql中的autocommit屬性,因?yàn)閙ysql默認(rèn)使用自動提交模式,也就是說當(dāng)我們進(jìn)行一個sql操作的時候,mysql會將這個操作當(dāng)做一個事務(wù)并且自動提交這個操作。
1.開始事務(wù)
begin;/begin work;/start transaction; (三者選一就可以)
2.查詢出商品信息
select ... for update;
4.提交事務(wù)
commit;/commit work;
排它鎖的原理:?一個鎖在某一時刻只能被一個線程占有,其它線程必須等待鎖被釋放之后才可能獲取到鎖或者進(jìn)行數(shù)據(jù)的操作。
悲觀鎖的優(yōu)點(diǎn)和不足:
悲觀鎖實(shí)際上是采取了“先取鎖在訪問”的策略,為數(shù)據(jù)的處理安全提供了保證;
但是在效率方面,由于額外的加鎖機(jī)制產(chǎn)生了額外的開銷,并且增加了死鎖的機(jī)會。并且降低了并發(fā)性;當(dāng)一個事物所以一行數(shù)據(jù)的時候,其他事物必須等待該事務(wù)提交之后,才能操作這行數(shù)據(jù)。
樂觀鎖
在關(guān)系數(shù)據(jù)庫管理系統(tǒng)里,樂觀并發(fā)控制(又名“樂觀鎖”,Optimistic Concurrency Control,縮寫“OCC”)是一種并發(fā)控制的方法。它假設(shè)多用戶并發(fā)的事務(wù)在處理時不會彼此互相影響,各事務(wù)能夠在不產(chǎn)生鎖的情況下處理各自影響的那部分?jǐn)?shù)據(jù)。在提交數(shù)據(jù)更新之前,每個事務(wù)會先檢查在該事務(wù)讀取數(shù)據(jù)后,有沒有其他事務(wù)又修改了該數(shù)據(jù)。如果其他事務(wù)有更新的話,正在提交的事務(wù)會進(jìn)行回滾。
樂觀鎖( Optimistic Locking ) 相對悲觀鎖而言,樂觀鎖假設(shè)認(rèn)為數(shù)據(jù)一般情況下不會造成沖突,所以在數(shù)據(jù)進(jìn)行提交更新的時候,才會正式對數(shù)據(jù)的沖突與否進(jìn)行檢測,如果發(fā)現(xiàn)沖突了,則讓返回用戶錯誤的信息,讓用戶決定如何去做。
相對于悲觀鎖,在對數(shù)據(jù)庫進(jìn)行處理的時候,樂觀鎖并不會使用數(shù)據(jù)庫提供的鎖機(jī)制。一般的實(shí)現(xiàn)樂觀鎖的方式就是記錄數(shù)據(jù)版本。
數(shù)據(jù)版本,為數(shù)據(jù)增加的一個版本標(biāo)識。當(dāng)讀取數(shù)據(jù)時,將版本標(biāo)識的值一同讀出,數(shù)據(jù)每更新一次,同時對版本標(biāo)識進(jìn)行更新。當(dāng)我們提交更新的時候,判斷數(shù)據(jù)庫表對應(yīng)記錄的當(dāng)前版本信息與第一次取出來的版本標(biāo)識進(jìn)行比對,如果數(shù)據(jù)庫表當(dāng)前版本號與第一次取出來的版本標(biāo)識值相等,則予以更新,否則認(rèn)為是過期數(shù)據(jù)。
樂觀鎖的優(yōu)點(diǎn)和不足:
樂觀并發(fā)控制相信事務(wù)之間的數(shù)據(jù)競爭(data race)的概率是比較小的,因此盡可能直接做下去,直到提交的時候才去鎖定,所以不會產(chǎn)生任何鎖和死鎖。但如果直接簡單這么做,還是有可能會遇到不可預(yù)期的結(jié)果,例如兩個事務(wù)都讀取了數(shù)據(jù)庫的某一行,經(jīng)過修改以后寫回數(shù)據(jù)庫,這時就遇到了問題。
間隙鎖(Next-Key 鎖)
當(dāng)我們用范圍條件而不是相等條件檢索數(shù)據(jù),并請求共享或排他鎖時,InnoDB 會給符合條件的已有數(shù)據(jù)記錄的索引項(xiàng)加鎖;對于鍵值在條件范圍內(nèi)但并不存在的記錄,叫做“間隙(GAP)”
InnoDB 使用間隙鎖的目的
一方面是為了防止幻讀,以滿足相關(guān)隔離級別的要求;
另外一方面,是為了滿足其恢復(fù)和復(fù)制的需要。
在使用范圍條件檢索并鎖定記錄時,InnoDB 這種加鎖機(jī)制會阻塞符合條件范圍內(nèi)鍵值的并發(fā)插入,這往往會造成嚴(yán)重的鎖等待。因此,在實(shí)際應(yīng)用開發(fā)中,尤其是并發(fā)插入比較多的應(yīng)用,我們要盡量優(yōu)化業(yè)務(wù)邏輯,盡量使用相等條件來訪問更新數(shù)據(jù),避免使用范圍條件。
還要特別說明的是,InnoDB 除了通過范圍條件加鎖時使用間隙鎖外,如果使用相等條件請求給一個不存在的記錄加鎖,InnoDB 也會使用間隙鎖!
恢復(fù)和復(fù)制的需要,對InnoDB 鎖機(jī)制的影響
MySQL 通過BINLOG 錄執(zhí)行成功的INSERT、UPDATE、DELETE 等更新數(shù)據(jù)的SQL 語句,并由此實(shí)現(xiàn)MySQL 數(shù)據(jù)庫的恢復(fù)和主從復(fù)制(可以參見本書“管理篇”的介紹)。MySQL 的恢復(fù)機(jī)制(復(fù)制其實(shí)就是在Slave Mysql 不斷做基于BINLOG 的恢復(fù))有以下特點(diǎn)。
?一是MySQL 的恢復(fù)是SQL 語句級的,也就是重新執(zhí)行BINLOG 中的SQL 語句。這與 Oracle 數(shù)據(jù)庫不同,Oracle 是基于數(shù)據(jù)庫文件塊的。?二是MySQL 的Binlog 是按照事務(wù)提交的先后順序記錄的,恢復(fù)也是按這個順序進(jìn)行的。Oracle 是按照系統(tǒng)更新號(System Change Number,SCN)來恢復(fù)數(shù)據(jù)的,每個事務(wù)開始時,Oracle 都會分配一個全局唯一的SCN,SCN 的順序與事務(wù)開始的時間順序是一致的。
從上面兩點(diǎn)可知,MySQL 的恢復(fù)機(jī)制要求:
?在一個事務(wù)未提交前,其他并發(fā)事務(wù)不能插入滿足其鎖定條件的任何記錄,也就是不允許出現(xiàn)幻讀,這已經(jīng)超過了ISO/ANSISQL92“可重復(fù)讀”隔離級別的要求,實(shí)際上是要求事務(wù)要串行化。這也是許多情況下,InnoDB 要用到間隙鎖的原因,比如在用范圍條件更新記錄時,無論在Read Commited 或 是Repeatable Read 隔離級別下,InnoDB 都要使用間隙鎖,但這并不是隔離級別要求的?? 另外,對insert into tableName select fromtableNameB或者create table new_tab ...select ... From source_tab用戶并沒有對source_tab 做任何更新操作,只是簡單地讀source_tab 表的數(shù)據(jù),相當(dāng)于執(zhí)行一個普通的SELECT語句,用一致性讀就可以了。
InnoDB 也實(shí)現(xiàn)了多版本數(shù)據(jù),對普通的SELECT 一致性讀,也不需要加任何鎖;但這里InnoDB 卻給source_tab加了共享鎖,并沒有使用多版本數(shù)據(jù)一致性讀技術(shù)!?MySQL 為什么要這么做呢?
其原因還是為了保證恢復(fù)和復(fù)制的正確性。因?yàn)椴患渔i的話,如果在上述語句執(zhí)行過程中,其他事務(wù)對source_tab 做了更新操作,就可能導(dǎo)致數(shù)據(jù)恢復(fù)的結(jié)果錯誤。為了演示這一點(diǎn),我們再重復(fù)一下前面的例子,不同的是在session_1執(zhí)行事務(wù)前,先將系統(tǒng)變量nnodb_locks_unsafe_for_binlog 的值設(shè)置為“on”(其默認(rèn)值為off)
什么時候使用表鎖
對于InnoDB 表,在絕大部分情況下都應(yīng)該使用行級鎖,因?yàn)槭聞?wù)和行鎖往往是我們之所以選擇InnoDB 表的理由。但在個別特殊事務(wù)中,也可以考慮使用表級鎖。
?第一種情況是:事務(wù)需要更新大部分或全部數(shù)據(jù),表又比較大,如果使用默認(rèn)的行鎖, 不僅這個事務(wù)執(zhí)行效率低,而且可能造成其他事務(wù)長時間鎖等待和鎖沖突,這種情況下可以 考慮使用表鎖來提高該事務(wù)的執(zhí)行速度。?第二種情況是:事務(wù)涉及多個表,比較復(fù)雜,很可能引起死鎖,造成大量事務(wù)回滾。這 種情況也可以考慮一次性鎖定事務(wù)涉及的表,從而避免死鎖、減少數(shù)據(jù)庫因事務(wù)回滾帶來的 開銷。當(dāng)然,應(yīng)用中這兩種事務(wù)不能太多,否則,就應(yīng)該考慮使用MyISAM 表了。
在InnoDB 下,使用表鎖要注意以下兩點(diǎn)?(1)使用LOCK TABLES 雖然可以給InnoDB 加表級鎖,但必須說明的是,表鎖不是由InnoDB存儲引擎層管理的,而是由其上一層──MySQL Server 負(fù)責(zé)的,僅當(dāng)autocommit=0、innodb_table_locks=1(默認(rèn)設(shè)置)時,InnoDB 層才能知道MySQL 加的表鎖,MySQL Server也才能感知InnoDB 加的行鎖,這種情況下,InnoDB 才能自動識別涉及表級鎖的死鎖;否則,InnoDB 將無法自動檢測并處理這種死鎖。有關(guān)死鎖,下一小節(jié)還會繼續(xù)討論。(2)在用LOCK TABLES 對InnoDB 表加鎖時要注意,要將AUTOCOMMIT 設(shè)為0,否則MySQL 不會給表加鎖;事務(wù)結(jié)束前,不要用UNLOCK TABLES 釋放表鎖,因?yàn)閁NLOCK TABLES會隱含地提交事務(wù);COMMIT 或ROLLBACK 并不能釋放用LOCK TABLES 加的表級鎖,必須用UNLOCK TABLES 釋放表鎖。
關(guān)于死鎖
發(fā)生死鎖后,InnoDB 一般都能自動檢測到,并使一個事務(wù)釋放鎖并回退,另一個事務(wù)獲得鎖,繼續(xù)完成事務(wù)。但在涉及外部鎖,或涉及表鎖的情況下,InnoDB 并不能完全自動檢測到死鎖,這需要通過設(shè)置鎖等待超時參數(shù)innodb_lock_wait_timeout 來解決。需要說明的是,這個參數(shù)并不是只用來解決死鎖問題,在并發(fā)訪問比較高的情況下,如果大量事務(wù)因無法立即獲得所需的鎖而掛起,會占用大量計(jì)算機(jī)資源,造成嚴(yán)重性能問題,甚至拖跨數(shù)據(jù)庫。我們通過設(shè)置合適的鎖等待超時閾值,可以避免這種情況發(fā)生。
通常來說,死鎖都是應(yīng)用設(shè)計(jì)的問題,通過調(diào)整業(yè)務(wù)流程、數(shù)據(jù)庫對象設(shè)計(jì)、事務(wù)大小,以及訪問數(shù)據(jù)庫的SQL 語句,絕大部分死鎖都可以避免。
下面就通過實(shí)例來介紹幾種避免死鎖的常用方法。
(1)在應(yīng)用中,如果不同的程序會并發(fā)存取多個表,應(yīng)盡量約定以相同的順序來訪問表,這樣可以大大降低產(chǎn)生死鎖的機(jī)會。在下面的例子中,由于兩個session 訪問兩個表的順序不同,發(fā)生死鎖的機(jī)會就非常高!但如果以相同的順序來訪問,死鎖就可以避免。
(2)在程序以批量方式處理數(shù)據(jù)的時候,如果事先對數(shù)據(jù)排序,保證每個線程按固定的順序來處理記錄,也可以大大降低出現(xiàn)死鎖的可能。
(3)在事務(wù)中,如果要更新記錄,應(yīng)該直接申請足夠級別的鎖,即排他鎖,而不應(yīng)先申請共享鎖,更新時再申請排他鎖,因?yàn)楫?dāng)用戶申請排他鎖時,其他事務(wù)可能又已經(jīng)獲得了相同記錄的共享鎖,從而造成鎖沖突,甚至死鎖。
(4)在REPEATABLE-READ 隔離級別下,如果兩個線程同時對相同條件記錄用SELECT...FOR UPDATE 加排他鎖,在沒有符合該條件記錄情況下,兩個線程都會加鎖成功。程序發(fā)現(xiàn)記錄尚不存在,就試圖插入一條新記錄,如果兩個線程都這么做,就會出現(xiàn)死鎖。這種情況下,將隔離級別改成READ COMMITTED,就可避免問題。
(5)當(dāng)隔離級別為READ COMMITTED 時,如果兩個線程都先執(zhí)行SELECT...FOR UPDATE,判斷是否存在符合條件的記錄,如果沒有,就插入記錄。此時,只有一個線程能插入成功,另一個線程會出現(xiàn)鎖等待,當(dāng)?shù)? 個線程提交后,第2 個線程會因主鍵重出錯,但雖然這個線程出錯了,卻會獲得一個排他鎖!這時如果有第3 個線程又來申請排他鎖,也會出現(xiàn)死鎖。
減少鎖沖突和死鎖總結(jié):
?盡量使用較低的隔離級別;?精心設(shè)計(jì)索引,并盡量使用索引訪問數(shù)據(jù),使加鎖更精確,從而減少鎖沖突的機(jī)會;?選擇合理的事務(wù)大小,小事務(wù)發(fā)生鎖沖突的幾率也更??;?給記錄集顯示加鎖時,最好一次性請求足夠級別的鎖。比如要修改數(shù)據(jù)的話,最好直接申請排他鎖,而不是先申請共享鎖,修改時再請求排他鎖,這樣容易產(chǎn)生死鎖;?不同的程序訪問一組表時,應(yīng)盡量約定以相同的順序訪問各表,對一個表而言,盡 可能以固定的順序存取表中的行。這樣可以大大減少死鎖的機(jī)會;?盡量用相等條件訪問數(shù)據(jù),這樣可以避免間隙鎖對并發(fā)插入的影響;?不要申請超過實(shí)際需要的鎖級別;除非必須,查詢時不要顯示加鎖;?對于一些特定的事務(wù),可以使用表鎖來提高處理速度或減少死鎖的可能。
InnoDB鎖的特性
?在不通過索引條件查詢的時候,InnoDB使用的確實(shí)是表鎖!?由于 MySQL 的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然是訪問不同行 的記錄,但是如果是使用相同的索引鍵,是會出現(xiàn)鎖沖突的。?當(dāng)表有多個索引的時候,不同的事務(wù)可以使用不同的索引鎖定不同的行,另外,不論 是使用主鍵索引、唯一索引或普通索引,InnoDB 都會使用行鎖來對數(shù)據(jù)加鎖。?即便在條件中使用了索引字段,但是否使用索引來檢索數(shù)據(jù)是由 MySQL 通過判斷不同 執(zhí)行計(jì)劃的代價來決定的,如果 MySQL 認(rèn)為全表掃 效率更高,比如對一些很小的表,它 就不會使用索引,這種情況下 InnoDB 將使用表鎖,而不是行鎖。因此,在分析鎖沖突時, 別忘了檢查 SQL 的執(zhí)行計(jì)劃(explain查看),以確認(rèn)是否真正使用了索引。
InnoDB鎖算法
?record lock:單個行記錄上鎖?gap lock:間隙鎖,鎖定一個范圍,不包括記錄本身。?next-key lock:record+gap 鎖定一個范圍包括記錄本身。
當(dāng)對存在的行進(jìn)行鎖的時候(主鍵),mysql就只有行鎖。當(dāng)對未存在的行進(jìn)行鎖的時候(即使條件為主鍵),mysql是會鎖住一段范圍(有g(shù)ap鎖)
本文作者:Java技術(shù)債務(wù) 原文鏈接:https://www.cuizb.top/myblog/article/1646146254 版權(quán)聲明:本博客所有文章除特別聲明外,均采用 CC BY 3.0 CN協(xié)議進(jìn)行許可。轉(zhuǎn)載請署名作者且注明文章出處。
