postgres 事務(wù)隔離級別

事務(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容