Postgres 的事務和鎖




事務命令

postgres=#
postgres=# \h begin
Command:     BEGIN
Description: start a transaction block
Syntax:
BEGIN [ WORK | TRANSACTION ] [ transaction_mode [, ...] ]

where transaction_mode is one of:

    ISOLATION LEVEL { SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED }
    READ WRITE | READ ONLY
    [ NOT ] DEFERRABLE

postgres=#
postgres=# \h end
Command:     END
Description: commit the current transaction
Syntax:
END [ WORK | TRANSACTION ]

postgres=#
postgres=# \h commit
Command:     COMMIT
Description: commit the current transaction
Syntax:
COMMIT [ WORK | TRANSACTION ]

begin 和 end (或者 commit) 之間的所有 SQL 組成一個事務,數據庫保證同一個事務的所有操作或者都成功,或者都回滾,同時隔離不同的事務防止其互相干擾

最典型的例子就是轉賬,必須保證轉出和轉入都成功,如果轉出成功但轉入失敗,應該能回滾

事務隔離級別

事務之間有以下幾種不同的隔離級別,由低(寬松)到高(嚴格)分別是

  • READ UNCOMMITTED : 讀未提交,可以讀到其他會話未提交的數據,等於沒隔離,PG 不支持,但可以設置,只是會被當成 READ COMMITTED
  • READ COMMITTED : 讀已提交(默認),只能讀到其他會話已提交的數據,有寫鎖避免同時修改同一個數據,修改同一數據需要等待直到先做修改的事務提交
  • REPEATABLE READ : 可重復讀,事務開始后,不會讀到其他會話提交的數據,有寫鎖避免同時修改同一個數據,並且如果修改同一數據會報錯
  • SERIALIZABLE : 串行化,最嚴格,哪怕沒修改同一個數據,同樣可能會有沖突

可以 begin 的同時設置隔離級別

postgres=# begin transaction isolation level repeatable read;
BEGIN

也可以 begin 后再設置

postgres=# set transaction isolation level repeatable read;
SET

查看隔離級別

postgres=# show transaction_isolation;
 transaction_isolation
-----------------------
 repeatable read
(1 row)

不設置默認就是 READ COMMITTED

READ COMMITTED 讀已提交 (默認)

在會話 1 開啟事務並查看

postgres=#
postgres=# begin;
BEGIN
postgres=#
postgres=# select * from test;
 id |  name
----+--------
  1 | name_A
  3 | name_C
  4 | name_d
  2 | name_b
(4 rows)

postgres=#
postgres=# select * from test where id = 2;
 id |  name
----+--------
  2 | name_b
(1 row)

在會話 2 開啟事務並更新

postgres=# begin;
BEGIN
postgres=#
postgres=# update test set name = 'name_bb' where id = 2;
UPDATE 1

繼續在會話 1 查看,可以看到查詢結果沒有改變

postgres=# select * from test where id = 2;
 id |  name
----+--------
  2 | name_b
(1 row)

postgres=#
postgres=# select * from test;
 id |  name
----+--------
  1 | name_A
  3 | name_C
  4 | name_d
  2 | name_b
(4 rows)

在會話 2 提交事務

postgres=# end;
COMMIT

繼續在會話 1 查看,可以看到查詢結果變了

postgres=# select * from test where id = 2;
 id |  name
----+---------
  2 | name_bb
(1 row)

postgres=#
postgres=# select * from test;
 id |  name
----+---------
  1 | name_A
  3 | name_C
  4 | name_d
  2 | name_bb
(4 rows)

在會話 2 再啟動事務,改數據,但不提交

postgres=# begin;
BEGIN
postgres=#
postgres=#
postgres=# update test set name = 'name_a22' where id = 1;
UPDATE 1
postgres=#
postgres=#

在會話 1 修改同一條記錄,可以看到,無法執行,在等待

postgres=# update test set name = 'name_a11' where id = 1;

在會話 2 提交事務

postgres=# end;
COMMIT

這時會話 1 的修改才被執行

postgres=# update test set name = 'name_a11' where id = 1;
UPDATE 1

所以這種模式就是,不能查看其他事務未提交的數據,可以查看其他事務已提交的數據,並且有行寫鎖,不允許同時修改同一行數據,除非另一個事務提交了

REPEATABLE READ 可重復讀

在會話 1 開啟事務並查看

postgres=# begin transaction isolation level repeatable read;
BEGIN
postgres=#
postgres=# select * from test where id = 2;
 id |  name
----+--------
  2 | name_b
(1 row)

postgres=#
postgres=# select * from test;
 id |  name
----+--------
  1 | name_A
  3 | name_C
  4 | name_d
  2 | name_b
(4 rows)

在會話 2 開啟事務並更新,然后提交事務

postgres=# begin;
BEGIN
postgres=#
postgres=# update test set name = 'name_bb' where id = 2;
UPDATE 1
postgres=#
postgres=# end;
COMMIT

繼續在會話 1 查看,可以看到查詢結果沒有改變,哪怕事務 2 已經提交了,這樣保證事務 1 的查詢結果的一致性

postgres=# select * from test where id = 2;
 id |  name
----+--------
  2 | name_b
(1 row)

postgres=#
postgres=# select * from test;
 id |  name
----+--------
  1 | name_A
  3 | name_C
  4 | name_d
  2 | name_b
(4 rows)

在會話 2 再啟動事務,添加新數據,並提交事務

postgres=# begin;
BEGIN
postgres=#
postgres=# insert into test values(5, 'name_e');
INSERT 0 1
postgres=#
postgres=# end;
COMMIT

在會話 1 繼續查看,可以看到數據還是沒有改變,哪怕事務 2 添加新數據並提交

postgres=# select * from test where id = 2;
 id |  name
----+--------
  2 | name_b
(1 row)

postgres=#
postgres=# select * from test;
 id |  name
----+--------
  1 | name_A
  3 | name_C
  4 | name_d
  2 | name_b
(4 rows)

在會話 1 提交事務再查看,可以看到事務 2 修改的數據和添加的數據,都可以看到了

postgres=# end;
COMMIT
postgres=#
postgres=# select * from test;
 id |  name
----+---------
  1 | name_A
  3 | name_C
  4 | name_d
  2 | name_bb
  5 | name_e
(5 rows)

在會話 1 再啟動事務,這次不做 select all 只先查看一條數據

postgres=# begin transaction isolation level repeatable read;
BEGIN
postgres=#
postgres=# select * from test where id = 2;
 id |  name
----+---------
  2 | name_bb
(1 row)

在會話 2 再啟動事務,修改添加數據,並提交事務

postgres=# begin;
BEGIN
postgres=#
postgres=# update test set name = 'name_b' where id = 2;
UPDATE 1
postgres=#
postgres=# update test set name = 'name_cc' where id = 3;
UPDATE 1
postgres=#
postgres=# insert into test values(6, 'name_f');
INSERT 0 1
postgres=#
postgres=# end;
COMMIT

在會話 1 繼續查看,可以看到數據還是沒有改變,哪怕事務 2 修改添加的數據,事務 1 之前並沒有命中

postgres=# select * from test where id = 2;
 id |  name
----+---------
  2 | name_bb
(1 row)

postgres=#
postgres=# select * from test where id = 3;
 id |  name
----+--------
  3 | name_C
(1 row)

postgres=#
postgres=# select * from test;
 id |  name
----+---------
  1 | name_A
  3 | name_C
  4 | name_d
  2 | name_bb
  5 | name_e
(5 rows)

在會話 1 提交事務,再查看數據,可以看到事務 2 的修改了

postgres=# end;
COMMIT
postgres=#
postgres=# select * from test;
 id |  name
----+---------
  1 | name_A
  4 | name_d
  5 | name_e
  2 | name_b
  3 | name_cc
  6 | name_f
(6 rows)

這種模式下同樣會有行寫鎖,如果修改同一行數據,需要等另一個事務先完成,但和 READ COMMITTED 不同的是,等另一個事務完成后,當前事務會失敗,報如下錯誤

postgres=# update test set name = 'name_A22' where id = 1;
ERROR:  could not serialize access due to concurrent update

使用 select ... for update 同樣會報這個錯誤

出現這個錯誤就需要退出事務,然后從頭開始重新執行事務

可以看到這個模式比 READ COMMITTED 更嚴格,它保證事務開始后,對數據的讀寫,完全不受其他事務的影響

如果兩個事務修改同一行數據,不僅會等待還會報錯,需要重新執行事務,或需要通過應用程序的鎖實現兩個事務的互斥

SERIALIZABLE 串行化

和 Repeatable Read 幾乎一樣的,只是更加嚴格地,保證兩個事務不沖突,保證串行化

在會話 1 啟動事務,執行下面命令

postgres=# begin transaction isolation level serializable;
BEGIN
postgres=#
postgres=# select count(*) from test where id = 3;
 count
-------
     1
(1 row)

postgres=#
postgres=# insert into test values(1, 'name_a1');
INSERT 0 1
postgres=#

在會話 2 啟動事務,執行下面命令,並提交

postgres=# begin transaction isolation level serializable;
BEGIN
postgres=#
postgres=# select count(*) from test where id = 1;
 count
-------
     1
(1 row)

postgres=#
postgres=# insert into test values(3, 'name_c2');
INSERT 0 1
postgres=#
postgres=# end;
COMMIT
postgres=#

在會話 1 提交事務,發現報錯了

postgres=# end;
ERROR:  could not serialize access due to read/write dependencies among transactions
DETAIL:  Reason code: Canceled on identification as a pivot, during commit attempt.
HINT:  The transaction might succeed if retried.
postgres=#

如果在 REPEATABLE READ 模式是不會報錯的,因為兩個事務沒有修改相同數據的沖突

但實際上,這兩個事務互相依賴,即事務 1 的 insert 命令影響事務 2 的 count 命令,而事務 2 的 insert 命令影響事務 1 的 count 命令,如果都允許提交,會導致數據不一致,所以后面提交的事務就報錯了

如果是 A 影響 B,而 B 影響 C,且 C 又影響 A 的循環,同樣會有這樣的問題

@Transactional 注解

@Transactional 注解可以用在類或函數上,使得進入函數的時候會啟動事務,離開函數的時候會提交事務

這個注解可以是 javax.transaction.Transactional
或是 org.springframework.transaction.annotation.Transactional (用於 springboot)

后者功能更強,並有 propagation 決定如果已有事務要如何處理(默認是使用已有事務) 和 isolation 決定隔離級別
前者有 value 決定如果已有事務要如何處理(默認是使用已有事務) 但沒有決定隔離級別的屬性

注意這個注解在以下場景有可能會失效

  1. 注解的函數不是 public 的
  2. 被繼承的基類的注解不會起作用
  3. propagation 配置導致如果已有事務會報錯,或是直接使用當前事務,而不會啟動新事務
  4. 同一個類中,方法 A 調用方法 B,但 A 沒注解而 B 有注解,這是因為 AOP 只對被當前類以外的代碼調用的函數起作用
  5. 如果用 try...catch 捕獲異常但沒拋出,同樣導致事務不起作用,必須讓事務自己處理異常並進行回滾操作

可以定義什么異常需要 rollback 什么異常不需要 rollback

事務內不要做其他事,最好單獨一個類處理

如果事務內做的事比較多,比如直接把注解加在 controller,可能會導致一些問題

  1. 事務可能太大,阻塞其他操作的時間可能比較久
  2. 事務內混合了其他業務操作,比如事務內發了個請求給其他服務修改數據,可能會導致這個事務被回滾的時候其他服務修改的數據沒被回滾,出現數據不一致

所以比較理想的做法,是有一個單獨的處理數據庫操作的類,這個類不做其他業務,並且只在需要的時候使用事務

鎖表 (lock 命令)

鎖表只能在事務中

postgres=# \h lock
Command:     LOCK
Description: lock a table
Syntax:
LOCK [ TABLE ] [ ONLY ] name [ * ] [, ...] [ IN lockmode MODE ] [ NOWAIT ]

where lockmode is one of:

    ACCESS SHARE | ROW SHARE | ROW EXCLUSIVE | SHARE UPDATE EXCLUSIVE
    | SHARE | SHARE ROW EXCLUSIVE | EXCLUSIVE | ACCESS EXCLUSIVE

  • ONLY 表示只鎖當前表,否則當前表及其后代表都會被鎖住
  • name 是表的名字
  • lockmode 指定哪些鎖會與當前鎖沖突,默認是 ACCESS EXCLUSIVE 即所有都沖突
  • NOWAIT 表示如果有另一個鎖鎖住了這個表,那 lock 命令是等待,還是直接報錯返回

鎖住表后,其他會話對這個表的所有操作,哪怕最簡單的 select 命令都會阻塞,直到擁有鎖的事務結束,鎖被釋放

鎖行 (排它鎖 select ... for update 和共享鎖 select ... for share)

鎖行不在事務內不會報錯,但不會起作用,所以還是要在事務內執行

SELECT ... FOR { UPDATE | SHARE } [ OF table_name [, ...] ] [ NOWAIT | SKIP LOCKED ] [...]
  • select for update 表示排他鎖,或者叫寫鎖,鎖住命中的行,不允許其他會話執行 select for update 或 select for share,但 select 可以
  • select for share 表示共享鎖,或者叫讀鎖,鎖住命中的行,不允許其他會話執行 select for update 但可以執行 select for share 或 select
  • of table_name 指定要鎖住的表 (select 語句可能涉及多個表)
  • nowait 表示如果無法獲取鎖,要等待,還是直接報錯
  • skip locked 表示要不要跳過無法獲取鎖的行並立刻返回 (select 的結果可能部分被鎖,部分沒被鎖,默認有被鎖的就等待或報錯)

依靠事務的隔離級別,需要真正修改數據的時候才鎖,或者要等到提交事務時才知道有沖突,甚至會報錯

使用 select for update/share 方便自己控制,而且能處理一些依靠事務不好處理的場景

頁級鎖

https://www.postgresql.org/docs/12/explicit-locking.html#LOCKING-PAGES

不了解,知道有這個就好,基本不會用到

死鎖

如果線程 A 先鎖數據 1 再鎖數據 2,而線程 B 先鎖數據 2 再鎖數據 1,並且沒有超時機制

這時如果兩個線程同時執行並且互相等待對方釋放鎖,就造成了死鎖

減少死鎖方法

  • 按相同順序鎖住數據
  • 超時機制
  • 事務盡可能簡單,減少鎖住的時間
  • 盡量使用較低隔離級別

遵循這種設計一般就不會有死鎖

@Lock 注解

@Lock 注解可以用在函數上,

注意 javax.ejb.Lock 不是數據庫的,而是給函數加讀鎖或者寫鎖的

數據庫的是 org.springframework.data.jpa.repository.Lock

@Lock(value = LockModeType.PESSIMISTIC_READ)
@Query(value = "select t from User t where t.name = :name")
User findByUserName(@Param("name") String name);

鎖模式

  • PESSIMISTIC_READ:悲觀讀鎖,或共享鎖,就是 select ... for share nowait
  • PESSIMISTIC_WRITE:悲觀寫鎖,或排他鎖,就是 select ... for update nowait
  • READ:樂觀讀鎖,實際上沒有鎖,要求表有 version 字段,通過檢查 version 字段在操作前后的一致性來保證不沖突
  • WRITE:樂觀寫鎖,在 READ 的基礎上,操作結束后不僅會檢查 version 字段,還會對 version + 1
  • OPTIMISTIC:和 READ 一樣
  • OPTIMISTIC_FORCE_INCREMENT:和 WRITE 一樣
  • PESSIMISTIC_FORCE_INCREMENT:在 PESSIMISTIC_WRITE 基礎上操作后對 version + 1

READ 是操作前取 version (或者操作的第一步一起取了),操作后執行

select version from [table] where [key] =?

對 version 復查

WRITE 操作后執行的是

update [table] set version=[操作前的值+1] where [key]=? and version=[操作前的值]

不僅對 version 復查,還加 1

悲觀鎖和樂觀鎖比較

悲觀鎖,是先取鎖再操作,會減少並發能力,影響性能,優點是有真正的排他性,適合要求嚴格、沖突概率較大、並發要求不高的場景

樂觀鎖,先操作,提交時再檢查 version 字段,不依賴數據庫機制,並發能力強,性能好,適合讀多寫少、大概率不沖突、要求高並發的場景,缺點是沒有真正的排他性,存在數據不一致的可能

JAP 如何不靠注解使用事務和鎖

    // 如果是用 @Autowired 或 @Inject 等方式注入 manager 的話,可能會報錯
    // Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT

    EntityManager entityManager = entityManagerFactory.createEntityManager();
    entityManager.getTransaction().begin();

    String sql = "select u from User as u where name = " + name;
    Query query = entityManager.createQuery(sql);

    query.setLockMode(LockModeType.PESSIMISTIC_WRITE);

    User user = (User) query.getResultList().get(0);
    System.out.println(user);

    entityManager.getTransaction().commit();

使用代碼而不是注解可能靈活點但不夠方便




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM