SqlServer事務詳解(事務隔離性和隔離級別詳解)

概述

  不少人對於事務的使用侷限於begin transaction:開始事務、commit transaction:提交事務、rollback transaction:回滾事務的初步運用。

並且知道使用事務後, 事務中所有操作命令必須作爲一個整體提交或回滾,如果事務中任何操作命令失敗,則整個事務將因失敗而回滾。

  除了這個概念性的東西后,其他就不怎麼知道了,比如事務的隔離性,具體怎麼隔離、有幾種隔離方式、執行順序是什麼。

我們今天來聊一聊這一塊的內容(主要是事務的隔離性)。


 

什麼是事務(定義)

  事務(Transaction),一般是指要做的或所做的事情。在計算機術語中是指訪問並可能更新數據庫中各種數據項的一個程序執行單元(unit)。

事務通常由高級數據庫操縱語言或編程語言(如SQL,C++或Java)書寫的用戶程序的執行所引起,並用形如begin transaction和end transaction語句(或函數調用)來界定。

事務由事務開始(begin transaction)和事務結束(end transaction)之間執行的全體操作組成。

事務是作爲單個邏輯單元執行的一系列操作,它是一個不可分割的工作邏輯單元。它包含了一組數據庫操作命令,這組命令要麼全部執行,要麼全部不執行。

 


事務的4個特性(特性)

 

事務是恢復和併發控制的基本單位。
 
事務應該具有4個屬性:原子性一致性隔離性持久性。這四個屬性通常稱爲ACID特性。

 

原子性(atomicity)

  原子性是指事務包含的所有操作要麼全部成功,要麼全部失敗回滾,這和前面兩篇博客介紹事務的功能是一樣的概念,
因此事務的操作如果成功就必須要完全應用到數據庫,如果操作失敗則不能對數據庫有任何影響。

 

一致性(consistency)

  一致性是指事務必須使數據庫從一個一致性狀態變換到另一個一致性狀態,也就是說一個事務執行之前和執行之後都必須處於一致性狀態。
比如轉賬,假設張三和李四兩個人的錢加起來一共是2000,那麼不管張三和李四之間怎麼轉賬,轉幾次賬,事務結束後兩個人的錢加起來還得是2000,這就是事務的一致性。

 

隔離性(isolation)

  隔離性是當多個用戶併發訪問數據庫時,比如操作同一張表時,數據庫爲每一個用戶開啓的事務,不能被其他事務的操作所幹擾,多個併發事務之間要相互隔離。
一個事務的執行不能被其他事務干擾。即一個事務內部的操作及使用的數據對併發的其他事務是隔離的,併發執行的各個事務之間不能互相干擾。
對於任意兩個併發的事務T1和T2,在事務T1看來,T2要麼在T1開始之前就已經結束,要麼在T1結束之後纔開始,這樣每個事務都感覺不到有其他事務在併發地執行。

 

持久性(durability)

持久性也稱永久性(permanence),指一個事務一旦提交,它對數據庫中數據的改變就應該是永久性的。接下來的其他操作或故障不應該對其有任何影響。

   在提交事務方法後,提示用戶事務操作完成,當我們程序執行完成直到看到提示後,就可以認定事務以及正確提交,

即使這時候數據庫出現了問題,也必須要將我們的事務完全執行完成,否則就會造成我們看到提示事務處理完畢,但是數據庫因爲故障而沒有執行事務的重大錯誤。


事務的分類 (分類)

事務分爲三類:顯式事務隱式事務自動提交事務

(1) 顯式事務:用 begin transaction 明確指定事務的開始,由 commit transaction 提交事務、rollback transaction 回滾事務到事務結束。

(2) 隱式事務:通過設置 set implicit_transactions on 語句,將隱式事務模式設置爲打開。

當以隱式事務模式操作時,不必使用 begin transaction 開啓事務,當一個事務結束後,這個模式會自動啓用下一個事務,

只需使用 commit transaction 提交事務或 Rollback Transaction 回滾事務即可。

(3) 自動提交事務: 這是 SQL Server 的默認模式,它將每條單獨的 T-SQL 語句視爲一個事務。如果成功執行,則自動提交。如果錯誤,則自動回滾。


 

簡單事務應用示例

我們先看一個簡單的應用。

--這裏指定不指定事務名稱均可
BEGIN TRANSACTION tran_UpdateTable --開啓事務(tran_UpdateTable:事務名稱)

DECLARE @tran_error int;--定義錯誤變量
SET @tran_error=0;        --錯誤變量的初始值爲0

--使用 try catch進行錯誤捕捉
BEGIN TRY
UPDATE a_Students SET Name='孫悟空' WHERE Number='100001';
UPDATE a_StudentsScore SET C# ='我是分數,應該是數字' WHERE Number='100001';
END TRY

BEGIN CATCH
set @tran_error=@tran_error+1; --(計算捕捉到的錯誤數)加分號或不加都能正常執行
END CATCH

--判斷是否有執行錯誤
IF(@tran_error>0)
    BEGIN 
        ROLLBACK TRANSACTION tran_UpdateTable  --執行出錯,回滾事務(tran_UpdateTable:指定事務名稱)
        PRINT '有【'+CONVERT(VARCHAR(50),@tran_error)+'】條執行失敗,進行回滾:'; 
    END 
ELSE
    BEGIN
        COMMIT TRANSACTION tran_UpdateTable --沒有異常,提交事務(tran_UpdateTable:指定事務名稱)
        --事務執行成功後,查看修改後的數據
        SELECT  s.Number ,
        s.Name ,
        sc.ClassName ,
        ss.C# ,
        ss.SqlDB ,
        ss.Java ,
        ss.Python
        FROM    a_Students s
        INNER JOIN a_StudentClass sc ON s.ClassId = sc.ClassId
        INNER JOIN a_StudentsScore ss ON s.Number = ss.Number
        WHERE s.Number='100001'
    END 

 

上面事務表示:根據學生編號修改學生姓名和學生學科C#的成績,如果有任何一條執行失敗則全部返回不執行,否則執行修改成功。


事務不隔離導致的問題

以上介紹完事務的四大特性(簡稱ACID),現在重點來說明下事務的隔離性,

當多個線程都開啓事務操作數據庫中的數據時,數據庫系統要能進行隔離操作,以保證各個線程獲取數據的準確性,

在介紹數據庫提供的各種隔離級別之前,我們先看看如果不考慮事務的隔離性,會發生的幾種問題:

更新丟失(Lost update)髒讀(Dirty Reads)不可重複讀(Non-repeatable Reads)

1、更新丟失(Lost update)

 兩個事務都同時更新一行數據,但是第二個事務卻中途失敗退出,導致對數據的兩個修改都失效了。這是因爲系統沒有執行任何的鎖操作,因此併發事務並沒有被隔離開來。

 

2、髒讀(Dirty Reads)

 一個事務開始讀取了某行數據,但是另外一個事務已經更新了此數據但沒有能夠及時提交。這是相當危險的,因爲很可能所有的操作都被回滾。

  當一個事務正在多次修改某個數據,而在這個事務中這多次的修改都還未提交,這時一個併發的事務來訪問該數據,就會造成兩個事務得到的數據不一致。

例如:用戶A向用戶B轉賬1000元,對應SQL命令如下:

--用戶A向用戶B轉賬1000元,B的錢增加
update account set money=money+1000 where name='B';


--此時A通知B我給你轉錢了,A的錢減少
update account set money=money - 1000 where name='A';

  當只執行第一條SQL時,A通知B查看賬戶,B發現確實錢已到賬(此時即發生了髒讀),

而之後無論第二條SQL是否執行,只要該事務不提交,則所有操作都將回滾,那麼當B以後再次查看賬戶時就會發現錢其實並沒有轉。

 

3、不可重複讀(Non-repeatable Reads

 不可重複讀是指在對於數據庫中的某個數據,一個事務範圍內多次查詢卻返回了不同的數據值,這是由於在查詢間隔,被另一個事務修改並提交了。

(1) 讀:事務T1讀取某一數據後,事務T2對其做了修改,當事務T1再次讀該數據時得到與前一次不同的值。

(2) 幻讀:事務在操作過程中進行兩次查詢,第二次查詢的結果包含了第一次查詢中未出現的數據或者缺少了第一次查詢中出現的數據(這裏並不要求兩次查詢的SQL語句相同)

這是因爲在兩次查詢過程中有另外一個事務插入數據造成的。


 

事務的隔離級別

爲了避免上面出現的幾種情況,在標準SQL規範中,定義了4+2個事務隔離級別,不同的隔離級別對事務的處理不同

1、未提交讀取(相當於with(nolock)):第一級別

  也稱爲未授權讀取:允許髒讀取,但不允許更新丟失。

如果一個事務已經開始寫數據,則另外一個事務則不允許同時進行寫操作,但允許其他事務讀此行數據。

  該隔離級別可以通過“排他寫鎖”實現。

  缺點:會產生髒讀、不可重複讀、幻讀。

  案例解讀:以前交易所炒股的時候,股民老王購買了5000股,操作員操作錄入(此時開啓事務),操作時手誤,多輸入了一個0,數據保存但是未提交。

此時老王查詢自己的持股信息,發現自己居然有50000股,瞬間血壓升高,昏倒在地。

然後操作員發現自己錄入錯誤,修改成正確的信息,錄入完成(事務結束)。

老王被救醒後,哆嗦這查詢自己的持股,發現只有5000,沒有增減,他之前看到的就是髒讀數據。

  解決方案:採用更高級的隔離機制,如提交讀。

 

2、提交讀取(Oracle和SQLServer默認的):第二級別

  這是大多數數據庫系統的默認隔離級別(Oracle和SQLServer默認的)。

  也稱爲授權讀取:允許不可重複讀取,但不允許髒讀取。

這可以通過“瞬間共享讀鎖”和“排他寫鎖”實現。讀取數據的事務允許其他事務繼續訪問該行數據,但是未提交的寫事務將會禁止其他事務訪問該行。

  缺點:會產生不可重複讀、幻讀。

  案例解讀:股市升高後,老王查看自己持有5000股,就想賣掉4000股,在老王賣股票的時候,老王的老婆看股市太高,就登錄老王的賬號,賣掉3000股。

當老王想賣股票時,發現自己只有2000股,不是之前看到的5000股,這就是不可重複讀問題。

  解決方案:採用更高級的隔離機制,如可重複讀。

 

3、可重複讀取(相當於(HOLDLOCK)):第三級別

  MySQL的默認事務隔離級別。

  可重複讀取(Repeatable Read):禁止不可重複讀取和髒讀取,但是有時可能出現幻讀數據。

這可以通過“共享讀鎖”和“排他寫鎖”實現。讀取數據的事務將會禁止寫事務(但允許讀事務),寫事務則禁止任何其他事務。

  缺點:會產生幻讀。

  問題解讀:股市忽漲忽跌,老王焦慮不安,按捺不住,想把持有的多種股票全部拋掉。與此同時,老王老婆聽信專家所言,使用老王的賬號買了某隻神股。

老王拋掉所有股票後,查看自己的持股,猛然發現自己居然還持有一隻股票,瞬間覺得一臉懵逼,這就是幻讀導致。

  解決方案:採用更高級的隔離機制,序列化。

 

4、序列化(這是最高的隔離級別):第四級別

  序列化(Serializable):提供嚴格的事務隔離。

它要求事務序列化執行,事務只能一個接着一個地執行,不能併發執行。

僅僅通過“行級鎖”是無法實現事務序列化的,必須通過其他機制保證新插入的數據不會被剛執行查詢操作的事務訪問到。

  缺點:可以解決併發事務的所有問題。但是效率地下,消耗數據庫性能,一般不使用。

 

  隔離級別越高,越能保證數據的完整性和一致性,但是對併發性能的影響也越大。

對於多數應用程序,可以優先考慮把數據庫系統的隔離級別設爲Read Committed。它能夠避免髒讀取,而且具有較好的併發性能。

儘管它會導致不可重複讀、幻讀和第二類丟失更新這些併發問題,在可能出現這類問題的個別場合,可以由應用程序採用悲觀鎖樂觀鎖來控制。

 

5、快照

(1)SNAPSHOT 在SNAPSHOT隔離級別下,當讀取數據時可以保證操作讀取的行是事務開始時可用的最後提交版本。

(2)同時SNAPSHOT隔離級別也滿足前面的已提交讀,可重複讀,不幻讀;該隔離級別實用的不是共享鎖,而是行版本控制。

 

6、已提交讀快照

READ COMMITTED SNAPSHOT也是基於行版本控制,但是READ COMMITTED SNAPSHOT的隔離級別是讀操作之前的最後已提交版本,

而不是事務前的已提交版本,有點類似前面的READ COMMITTED能保證已提交讀,但是不能保證可重複讀,不能避免幻讀,

但是又比 READ COMMITTED隔離級別多出了不需要獲取共享鎖就可以讀取數據。

 


 

事務的隔離級別設置示例

  SQL Server通過在鎖資源上使用不同類型的鎖來隔離事務。

爲了開發安全的事務,定義事務內容以及應在何種情況下回滾至關重要,定義如何以及在多長時間內在事務中保持鎖定也同等重要。

這由隔離級別決定,應用不同的隔離級別,SQL Server賦予開發者一種能力,讓他們爲每一個單獨事務定義與其他事務的隔離程度。事務隔離級別的定義如下:

 

 獲取事務隔離級別(isolation level)

DBCC USEROPTIONS 

 

1、未提交讀取

新建回話並將學生編號爲100001的成績+1;

BEGIN TRANSACTION
UPDATE a_StudentsScore 
SET C#=C#+1
WHERE Number='100001'

SELECT * FROM a_StudentsScore 
WHERE Number='100001'

 

 

然後執行錯誤的回話,進行回滾

BEGIN TRANSACTION
UPDATE a_StudentsScore 
SET C#=C#+''
WHERE Number='100001'

 

 然後在查詢數據

--首先不添加隔離級別,默認是READ COMMITTED,由於數據之前的更新操作使用了排他鎖,所以查詢一直在等待鎖釋放*/
SELECT * FROM a_StudentsScore 
WHERE Number='100001'
---將查詢的隔離級別設置爲READ UNCOMMITTED允許未提交讀,讀操作之前不請求共享鎖。
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM a_StudentsScore 
WHERE Number='100001'
--當然也可以使用表隔離,效果是一樣的
SELECT * FROM a_StudentsScore WITH (NOLOCK)
WHERE Number='100001'

 

 我們發現執行兩個事務回話,第一個執行成了,但是第二個執行失敗了進行回滾,最後查詢的數據是第一個執行前的數據,沒有任何改變。

 

2、提交讀取

新建回話1並將學生編號爲100001的成績+1,此時回話的排他鎖鎖住了學生編號爲100001的成績

BEGIN TRANSACTION
UPDATE a_StudentsScore 
SET C#=C#+1
WHERE Number='100001'

SELECT * FROM a_StudentsScore 
WHERE Number='100001'

在回話2中執行查詢,將隔離級別設置爲READ COMMITTED

SET TRANSACTION ISOLATION LEVEL READ COMMITTED
SELECT * FROM a_StudentsScore 
WHERE Number='100001'
--由於READ COMMITTED需要申請獲得共享鎖,而鎖與回話1的排他鎖衝突,回話被堵塞,

--在回話1中執行事務提交
COMMIT TRANSACTION
--由於回話1事務提交,釋放了學生100001的排他鎖,此時回話2申請共享鎖成功查到學生100001的C#成績爲修改後的成績81,READ COMMITTED由於是已提交讀隔離級別,所以不會讀髒數據.

重置數據

UPDATE a_StudentsScore 
SET C#=80
WHERE Number='100001'

注意:但是由於READ COMMITTED讀操作一完成就立即釋放共享鎖,讀操作不會在一個事務過程中保持共享鎖,

也就是說在一個事務的的兩個查詢過程之間有另一個回話對數據資源進行了更改,會導致一個事務的兩次查詢得到的結果不一致,這種現象稱之爲不可重複讀.

 

3、可重複讀取

在回話1中查詢學生編號爲100001的成績,,將回話級別設置爲REPEATABLE READ

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRANSACTION
SELECT * FROM a_StudentsScore 
WHERE Number='100001'

新建回話2修改學生編號爲100001的成績

UPDATE a_StudentsScore 
SET C#=C#+1
WHERE Number='100001'
---由於回話1的隔離級別REPEATABLE READ申請的共享鎖一直要保持到事務結束,所以回話2無法獲取排他鎖,處於等待狀態

在回話1中執行下面語句,然後提交事務

SELECT * FROM a_StudentsScore 
WHERE Number='100001'
COMMIT TRANSACTION

 

 

回話1的兩次查詢得到的結果一致,前面的兩個隔離級別無法得到一致的數據,此時事務已提交同時釋放共享鎖,回話2申請排他鎖成功,對行執行更新

REPEATABLE READ隔離級別保證一個事務中的兩次查詢到的結果一致,同時保證了丟失更新
丟失更新:兩個事務同時讀取了同一個值然後基於最初的值進行計算,接着再更新,就會導致兩個事務的更新相互覆蓋。
例如酒店訂房例子,兩個人同時預定同一酒店的房間,首先兩個人同時查詢到還有一間房間可以預定,然後兩個人同時提交預定操作,事務1執行number=1-0,同時事務2也執行number=1-0最後修改number=0,這就導致兩個人其中一個人的操作被另一個人所覆蓋,REPEATABLE READ隔離級別就能避免這種丟失更新的現象,當事務1查詢房間時事務就一直保持共享鎖直到事務提交,而不是像前面的幾個隔離級別查詢完就是否共享鎖,就能避免其他事務獲取排他鎖。

 

4、序列化

 SERIALIZABLE(可序列化),對於前面的REPEATABLE READ能保證事務可重複讀,但是事務只鎖定查詢第一次運行時獲取的數據資源(數據行),

而不能鎖定查詢結果之外的行,就是原本不存在於數據表中的數據。因此在一個事務中當第一個查詢和第二個查詢過程之間,有其他事務執行插入操作且插入數據滿足第一次查詢讀取過濾的條件時,

那麼在第二次查詢的結果中就會存在這些新插入的數據,使兩次查詢結果不一致,這種讀操作稱之爲幻讀。

爲了避免幻讀需要將隔離級別設置爲SERIALIZABLE。

在回話1中執行查詢操作,並將事務隔離級別設置爲REPEATABLE READ(先測試一下前面更低級別的隔離)

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRANSACTION 
SELECT * FROM a_StudentsScore 
WHERE Number='100001'

在回話2中執行修改操作

UPDATE a_StudentsScore 
SET C#=C#+1
WHERE Number='100001'

返回回話1重新執行查詢操作並提交事務

SELECT * FROM a_StudentsScore 
WHERE Number='100001'
COMMIT TRANSACTION

結果回話1中第二次查詢到的數據包含了回話2新修改的數據,兩次查詢結果不一致(驗證之前的隔離級別不能保證幻讀)

 

接下來將回話級別設置爲SERIALIZABLE,在回話1中執行查詢操作,並將事務隔離級別設置爲SERIALIZABLE。

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION 
SELECT * FROM a_StudentsScore 
WHERE Number='100001'

在回話2中執行修改操作

UPDATE a_StudentsScore 
SET C#=C#+1
WHERE Number='100001'

 

返回回話1重新執行查詢操作並提交事務

SELECT * FROM a_StudentsScore 
WHERE Number='100001'
COMMIT TRANSACTION

 

兩次執行的查詢結果相同

重置所有打開回話的默認隔離級別

SET TRANSACTION ISOLATION LEVEL READ COMMITTED

 

5、快照

SNAPSHOT 在SNAPSHOT隔離級別下,當讀取數據時可以保證操作讀取的行是事務開始時可用的最後提交版本
同時SNAPSHOT隔離級別也滿足前面的已提交讀,可重複讀,不幻讀;該隔離級別實用的不是共享鎖,而是行版本控制
使用SNAPSHOT隔離級別首先需要在數據庫級別上設置相關選項

在打開的所有查詢窗口中執行以下操作

ALTER DATABASE TEST SET ALLOW_SNAPSHOT_ISOLATION ON;

 

--在回話1中打開事務,將學生100001的成績加1,並查詢跟新後的成績
BEGIN TRANSACTION
UPDATE a_StudentsScore 
SET C#=C#+1
WHERE Number='100001'

SELECT * FROM a_StudentsScore 
WHERE Number='100001'
---查詢到更新後的成績爲81

---在回話2中將隔離級別設置爲SNAPSHOT,並打開事務(此時查詢也不會因爲回話1的排他鎖而等待,依然可以查詢到數據)
SET TRANSACTION ISOLATION LEVEL SNAPSHOT
BEGIN TRANSACTION
SELECT * FROM a_StudentsScore 
WHERE Number='100001'

---查詢到的結果還是回話1修改前的成績,由於回話1在默認的READ COMMITTED隔離級別下運行,SQL SERVER必須在更新前把行的一個副本複製到TEMPDB數據庫中
--在SNAPSHOT級別啓動事務會請求行版本

---現在在回話1中執行提交事務,此時學生100001的成績爲81
COMMIT TRANSACTION

---再次在回話2中查詢學生100001的成績並提交事務,結果還是80,因爲事務要保證兩次查詢的結果相同
SELECT * FROM a_StudentsScore 
WHERE Number='100001'

COMMIT TRANSACTION

---此時如果在回話2中重新打開一個事務,查詢到的學生100001的成績爲81
BEGIN TRANSACTION
SELECT * FROM a_StudentsScore 
WHERE Number='100001'

COMMIT TRANSACTION

--SNAPSHOT隔離級別保證操作讀取的行是事務開始時可用的最後已提交版本,由於回話1的事務未提交,所以學生100001的最後提交版本還是修改前的成績80,
--所以回話2讀取到的成績是回話2事務開始前的已提交版本成績80,當回話1提交事務後,回話2重新新建一個事務此時事務開啓前的成績已經是81了,
--所以查詢到的成績是81,同時SNAPSHOT隔離級別還能保證SERIALIZABLE的隔離級別

 

6、已提交讀快照

READ COMMITTED SNAPSHOT也是基於行版本控制,但是READ COMMITTED SNAPSHOT的隔離級別是讀操作之前的最後已提交版本,而不是事務前的已提交版本,

有點類似前面的READ COMMITTED能保證已提交讀,但是不能保證可重複讀,不能避免幻讀,但是又比 READ COMMITTED隔離級別多出了不需要獲取共享鎖就可以讀取數據

要啓用READ COMMITTED SNAPSHOT隔離級別同樣需要修改數據庫選項,

在回話1,回話2中執行以下操作(執行下面的操作當前連接必須是數據庫的唯一連接,可以通過查詢已連接當前數據庫的進程,然後KILL掉那些進程,然後再執行該操作,否則可能無法執行成功)。

--在回話1中打開事務,將學生100001的成績加1,並查詢跟新後的成績,並保持事務一直處於打開狀態
UPDATE a_StudentsScore 
SET C#=C#+1
WHERE Number='100001'

--查詢到的成績是81
SELECT * FROM a_StudentsScore 
WHERE Number='100001'

--在回話2中打開事務查詢學生100001並一直保持事務處於打開狀態(此時由於回話1還未提交事務,所以回話2中查詢到的還是回話1執行事務之前保存的行版本)
BEGIN TRANSACTION
SELECT * FROM a_StudentsScore 
WHERE Number='100001'
--查詢到的成績還是80

--在回話1中提交事務
COMMIT TRANSACTION 

--在回話2中再次執行查詢學生100001的成績,並提交事務
SELECT * FROM a_StudentsScore 
WHERE Number='100001'
COMMIT TRANSACTION 
--此時的成績爲回話1修改後的成績81,而不是事務之前已提交版本的成績,也就是READ COMMITTED SNAPSHOT隔離級別在同一事務中兩次查詢的結果不一致.

關閉所有連接,然後打開一個新的連接,禁用之前設置的數據庫快照隔離級別選項。

ALTER DATABASE TEST SET ALLOW_SNAPSHOT_ISOLATION OFF;

ALTER DATABASE TEST SET READ_COMMITTED_SNAPSHOT OFF;

 

參考文獻1:百度百科:事務

參考文獻2:百度百科:事務隔離級別

 

 
歡迎關注訂閱微信公衆號【熊澤有話說】,更多好玩易學知識等你來取
作者:熊澤-學習中的苦與樂
公衆號:熊澤有話說
出處: https://www.cnblogs.com/xiongze520/p/14821536.html
創作不易,任何人或團體、機構全部轉載或者部分轉載、摘錄,請在文章明顯位置註明作者和原文鏈接。  

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章