事務命令
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 決定如果已有事務要如何處理(默認是使用已有事務) 但沒有決定隔離級別的屬性
注意這個注解在以下場景有可能會失效
- 注解的函數不是 public 的
- 被繼承的基類的注解不會起作用
- propagation 配置導致如果已有事務會報錯,或是直接使用當前事務,而不會啟動新事務
- 同一個類中,方法 A 調用方法 B,但 A 沒注解而 B 有注解,這是因為 AOP 只對被當前類以外的代碼調用的函數起作用
- 如果用 try...catch 捕獲異常但沒拋出,同樣導致事務不起作用,必須讓事務自己處理異常並進行回滾操作
可以定義什么異常需要 rollback 什么異常不需要 rollback
事務內不要做其他事,最好單獨一個類處理
如果事務內做的事比較多,比如直接把注解加在 controller,可能會導致一些問題
- 事務可能太大,阻塞其他操作的時間可能比較久
- 事務內混合了其他業務操作,比如事務內發了個請求給其他服務修改數據,可能會導致這個事務被回滾的時候其他服務修改的數據沒被回滾,出現數據不一致
所以比較理想的做法,是有一個單獨的處理數據庫操作的類,這個類不做其他業務,並且只在需要的時候使用事務
鎖表 (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();
使用代碼而不是注解可能靈活點但不夠方便