事務(wù)隔離
MVCC的實(shí)現(xiàn)方法有兩種:
1.寫新數(shù)據(jù)時,把舊數(shù)據(jù)移到一個單獨(dú)的地方,如回滾段中,其他人讀數(shù)據(jù)時,從回滾段中把舊的數(shù)據(jù)讀出來;
2.寫數(shù)據(jù)時,舊數(shù)據(jù)不刪除,而是把新數(shù)據(jù)插入。
PostgreSQL數(shù)據(jù)庫使用第二種方法,而Oracle數(shù)據(jù)庫和MySQL中的innodb引擎使用的是第一種方法。
與racle數(shù)據(jù)庫和MySQL中的innodb引擎相比較,PostgreSQL的MVCC實(shí)現(xiàn)方式的優(yōu)缺點(diǎn)如下。
優(yōu)點(diǎn):
1.事務(wù)回滾可以立即完成,無論事務(wù)進(jìn)行了多少操作;
2.數(shù)據(jù)可以進(jìn)行很多更新,不必像Oracle和MySQL的Innodb引擎那樣需要經(jīng)常保證回滾段不會被用完,也不會像oracle數(shù)據(jù)庫那樣經(jīng)常遇到“ORA-1555”錯誤的困擾;
缺點(diǎn):
1.舊版本數(shù)據(jù)需要清理。PostgreSQL清理舊版本的命令成為Vacuum;
2.舊版本的數(shù)據(jù)會導(dǎo)致查詢更慢一些,因?yàn)榕f版本的數(shù)據(jù)存在于數(shù)據(jù)文件中,查詢時需要掃描更多的數(shù)據(jù)塊。
(本段轉(zhuǎn)自《PostgreSQL修煉之道》)
各個級別不希望發(fā)生的現(xiàn)象是
- 臟讀(dirty reads)
一個事務(wù)讀取了另一個未提交的并行事務(wù)寫的數(shù)據(jù)。
| 時間 | 事務(wù)A | 事務(wù)B |
|---|---|---|
| T1 | 開始事務(wù) | |
| T2 | 開始事務(wù) | |
| T3 | 查詢賬戶余額1000 | |
| T4 | 去除500元,余額500 | |
| T5 | 查詢余額為500(臟讀) |
- 不可重復(fù)讀(non-repeatable reads)
一個事務(wù)重新讀取前面讀取過的數(shù)據(jù), 發(fā)現(xiàn)該數(shù)據(jù)已經(jīng)被另一個已提交的事務(wù)修改過。
| 時間 | 事務(wù)A | 事務(wù)B |
|---|---|---|
| T1 | 開始事務(wù) | |
| T2 | 查詢余額為1000 | 開始事務(wù) |
| T3 | 查詢賬戶余額1000 | |
| T4 | 去除500元,余額500 | |
| T5 | 提交事務(wù) | |
| T6 | 查詢余額為500 | |
| T7 | 提交事務(wù) |
- 幻讀(phantom read)
當(dāng)前事務(wù)中重復(fù)執(zhí)行相同的查詢,返回的記錄數(shù)因另一個事物插入或刪除而得到不同的結(jié)果
| 時間 | 事務(wù)A | 事務(wù)B |
|---|---|---|
| T1 | 開始事務(wù) | |
| T2 | select count(*) from Foos where flag1=1 //(10條) | 開始事務(wù) |
| T3 | update Foos set flag2=2 where flag1=1 //(10條) | |
| T4 | insert into Foos (..,flag1,...) values (.., 1 ,..) | |
| T5 | 提交事務(wù) | |
| T6 | select count(*) from Foos where flag1=1 //(11條) | |
| T7 | update Foos set flag2=2 where flag1=1 //(更新11條) | |
| T8 | 提交事務(wù) |
會看到新插入的那條數(shù)據(jù)會被更新
標(biāo)準(zhǔn)SQL事務(wù)隔離級別
| 隔離級別 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
|---|---|---|---|
| 讀未提交 | 可能 | 可能 | 可能 |
| 讀已提交 | 不可能 | 可能 | 可能 |
| 可重復(fù)讀 | 不可能 | 不可能 | Allowed, but not in PG |
| 可串行化 | 不可能 | 不可能 | 不可能 |
事務(wù)隔離級別
在PostgreSQL里,你可以請求四種可能的事務(wù)隔離級別中的任意一種。
但是在內(nèi)部, 實(shí)際上只有三種獨(dú)立的隔離級別,分別對應(yīng)讀已提交,可重復(fù)讀和可串行化。
如果你選擇了讀未提交的級別, 實(shí)際上你用的是讀已提交,
在Postgre的重復(fù)讀下,幻讀是不可能的, 所以實(shí)際的隔離級別可能比你選擇的更嚴(yán)格。
讀未提交 Read Uncommitted
另一個事務(wù)中只要更新的記錄(不需要等到提交), 當(dāng)前事務(wù)就會讀取到更新的數(shù)據(jù) (臟讀)讀已提交 Read Committed
讀已提交是PostgreSQL里的缺省隔離級別。
當(dāng)一個事務(wù)運(yùn)行在這個隔離級別時, SELECT查詢(沒有FOR UPDATE/SHARE子句)只能看到其它事務(wù)已提交的數(shù)據(jù)。
實(shí)際上,SELECT 查詢看到一個在查詢開始運(yùn)行的瞬間該數(shù)據(jù)庫的一個快照。 不過,SELECT看得見其自身所在事務(wù)中之前的更新的執(zhí)行結(jié)果,即使它們尚未提交。
請注意, 在同一個事務(wù)里兩個相鄰的SELECT命令可能看到不同的快照,因?yàn)槠渌聞?wù)會坑你在兩個SELECT執(zhí)行期間提交。
不會出現(xiàn)可臟讀,但是不可重復(fù)讀可重復(fù)讀 Repeatable Read
即使數(shù)據(jù)被其他事物修改, 當(dāng)前事務(wù)也不會讀取到新的數(shù)據(jù)
重復(fù)讀事務(wù)中的查詢看到的是事務(wù)開始時的快照, 而不是該事務(wù)內(nèi)部當(dāng)前查詢開始時的快照,這樣, 同一個事務(wù)內(nèi)部后面的SELECT命令總是看到同樣的數(shù)據(jù),
也就是說,它們看不到 它們自身事務(wù)開始之后提交的其他事務(wù)所做出的改變。
不會出現(xiàn)可臟讀, 可重復(fù)讀,可以幻讀
- 可串行化 Serializable
可串行化級別提供最嚴(yán)格的事務(wù)隔離。這個級別為所有已提交事務(wù)模擬串行的事務(wù)執(zhí)行, 就好像事務(wù)將被一個接著一個那樣串行(而不是并行)的執(zhí)行。
不過,正如可重復(fù)讀隔離級別一樣, 使用這個級別的應(yīng)用必須準(zhǔn)備在串行化失敗的時候重新啟動事務(wù)。
事實(shí)上,該隔離級別和可重復(fù)讀希望的完全一樣, 它只是監(jiān)視這些條件,以所有事務(wù)的可能的序列不一致的(一次一個)的方式執(zhí)行并行的可串行化事務(wù)執(zhí)行的行為。
這種監(jiān)測不引入任何阻止可重復(fù)讀出現(xiàn)的行為,但有一些開銷的監(jiān)測,檢測條件這可能會導(dǎo)致串行化異常 將觸發(fā)串行化失敗。
讀已提交(Read Committed Isolation Level)
不可重復(fù)讀
ActiveRecord::Base.isolation_level(:read_committed) do
Foo.transaction do
print Foo.first.bar # 1
sleep(10) # 在此期間, 其它事務(wù)更新了Foo#bar
print Foo.first.bar # 2
end
end
可重復(fù)讀(Repeatable Read Isolation Level)
可重復(fù)讀
ActiveRecord::Base.isolation_level(:repeatable_read) do
Foo.transaction do
print Foo.first.bar # 1
sleep(10) # 在此期間, 其它事務(wù)更新了Foo#bar
print Foo.first.bar # 1
end
end
該級別的應(yīng)用必須準(zhǔn)備好重試事務(wù),因?yàn)榭赡軙l(fā)生串行化失敗。
下面這種情況事務(wù)T2會發(fā)生串行化失敗
# 事務(wù)T1
ActiveRecord::Base.isolation_level(:repeatable_read) do
Foo.transaction do
print Foo.where(id: 1).update_all(bar: 11)
print Foo.find(1).bar
sleep 5
end
end
# 事務(wù)T2
ActiveRecord::Base.isolation_level(:repeatable_read) do
Foo.transaction do
print Foo.where(id: 1).update_all(bar: 12)
print Foo.find(1).bar
end
end
下面這種情況事務(wù)T2不會發(fā)生串行化失敗
# 事務(wù)T1
ActiveRecord::Base.isolation_level(:repeatable_read) do
Foo.transaction do
print Foo.where(id: 1).update_all(bar: 11)
print Foo.find(1).bar
sleep 5
end
end
# 事務(wù)T2
ActiveRecord::Base.isolation_level(:repeatable_read) do
Foo.transaction do
sleep 6
print Foo.where(id: 1).update_all(bar: 12)
print Foo.find(1).bar
end
end
在Postgre的重復(fù)讀下,幻讀是不可能的
但是測試的時候發(fā)現(xiàn)這種狀況
# 事務(wù)T1
ActiveRecord::Base.isolation_level(:repeatable_read) do
Foo.transaction do
Foo.create!(bar: 2)
sleep 5
end
end
# 事務(wù)T2
ActiveRecord::Base.isolation_level(:repeatable_read) do
Foo.transaction do
print Foo.where(bar: 2).count # 1
sleep 10
print Foo.where(bar: 2).count # 2
Foo.where(bar: 2).update_all(bar: 1) # 2
end
end
可串行化(Serializable Isolation Level)
可重復(fù)讀下不會發(fā)生的串行化失敗在可串行化會失敗
下面這種情況事務(wù)T2會發(fā)生串行化失敗
# 事務(wù)T1
ActiveRecord::Base.isolation_level(:serializable) do
Foo.transaction do
print Foo.where(id: 1).update_all(bar: 11)
print Foo.find(1).bar
sleep 5
end
end
# 事務(wù)T2
ActiveRecord::Base.isolation_level(:serializable) do
Foo.transaction do
sleep 6
print Foo.where(id: 1).update_all(bar: 12)
print Foo.find(1).bar
end
end
多個事務(wù)并發(fā)時可能遇到的問題
-
Lost Update 更新丟失
- 第一類更新丟失,回滾覆蓋:撤消一個事務(wù)時,在該事務(wù)內(nèi)的寫操作要回滾,把其它已提交的事務(wù)寫入的數(shù)據(jù)覆蓋了。
- 第二類更新丟失,提交覆蓋:提交一個事務(wù)時,寫操作依賴于事務(wù)內(nèi)讀到的數(shù)據(jù),讀發(fā)生在其他事務(wù)提交前,寫發(fā)生在其他事務(wù)提交后,把其他已提交的事務(wù)寫入的數(shù)據(jù)覆蓋了。這是不可重復(fù)讀的特例。
Non-Repeatable Read 不可重復(fù)讀:一個事務(wù)中兩次讀同一行數(shù)據(jù),可是這兩次讀到的數(shù)據(jù)不一樣。
Phantom Read 幻讀:一個事務(wù)中兩次查詢,但第二次查詢比第一次查詢多了或少了幾行或幾列數(shù)據(jù)。
回滾覆蓋
| 時間 | 事務(wù)A | 事務(wù)B |
|---|---|---|
| T1 | 開始事務(wù) | |
| T2 | 開始事務(wù) | |
| T3 | 查詢余額為1000 | |
| T4 | 取出100,余額改為900 | |
| T5 | 讀余額為1000 | |
| T6 | 匯入100,余額改為1100 | |
| T7 | 提交事務(wù),余額定為1100 | |
| T8 | 撤銷事務(wù),余額改回1000 | |
| T9 | 最終余額1000,更新丟失 |
這種更新丟失在pg的隔離級別下是不會發(fā)生的
提交覆蓋
| 時間 | 事務(wù)A | 事務(wù)B |
|---|---|---|
| T1 | 開始事務(wù) | |
| T2 | 開始事務(wù) | |
| T3 | 查詢余額為1000 | |
| T4 | 讀余額為10000 | |
| T5 | 取出100,余額改為900 | |
| T6 | 提交事務(wù),余額定為900 | |
| T7 | 匯入100,余額改為1100 | |
| T8 | 提交事務(wù),余額定為1100 | |
| T8 | 最終余額1100,更新丟失 |
不做并發(fā)控制的前提下, 讀已提交隔離級別下很容易發(fā)生更新丟失的問題,
可重復(fù)讀, 可串行化 可以避免更新丟失的問題
比如下面這段代碼
# 事務(wù)T1
Foo.transaction do
Foo = Foo.find(1)
Foo.bar = Foo.bar + 10
sleep 5
Foo.save
end
# 事務(wù)T2
Foo.transaction do
Foo = Foo.find(1)
Foo.bar = Foo.bar + 5
Foo.save
end
在讀已提交隔離級別下可以通過鎖來防止更新丟失
- 拿掉代碼中的臨時變量
# 事務(wù)T1
Foo.transaction do
Foo = Foo.find(1)
Foo.increment!(:bar, 10)
sleep 5
Foo.increment!(:bar, 10)
end
# 事務(wù)T2
Foo.transaction do
Foo = Foo.find(1)
Foo.increment!(:bar, 10)
end
- 鎖
# 事務(wù)T1
Foo = Foo.find(1)
Foo.with_lock do
Foo.bar = Foo.bar + 10
Foo.save(validate: false)
sleep 10
Foo.bar = Foo.bar + 10
Foo.save(validate: false)
end
# 事務(wù)T1
Foo = Foo.find(1)
Foo.with_lock do
Foo.bar = Foo.bar + 10
Foo.save(validate: false)
end
tip
- combine query
# bad
unless rerord.approved?
# balabala # 多個thread可能同時到達(dá)這里
rerord.update(approved: true)
end # 并發(fā)下會導(dǎo)致一些問題
# better
update_count = Rerord.where(id: id, approved: false).update_all(approved: true)
# 根據(jù)上面的理論 并發(fā)下 不會導(dǎo)致某個record會被重復(fù)更新
if update_count == 1
# balabala
end
參考: https://www.postgresql.org/docs/9.5/static/transaction-iso.html