---
title: postgres 併發
---
[TOC]
###### tags: `Database`
# postgres 併發
## 隔離等級
必讀官方文件說明: http://www.postgres.cn/docs/9.3/transaction-iso.html
### 重點:
1. 在 PostgreSQL 內部實際上實作的只有三種隔離等級: Read committed, Repeatable read(使用这个级别的应用必须准备好重试事务,因为可能会发生串行化失败) , Serializable。 預設為 Read committed
2. Read commited(RC) 與 Repeatable read(RR) 的具體差異:
a. RR需要處理交易重試: RC 將會在事務過程中讀取到其他會話的commit,使得在事務中兩次讀取內容不一致,其他會話的增刪改事務一樣可以繼續; RR 在事務中查詢內容皆為當前會話的事務開啟時首次query的snapshot(如果開啟rr事務,沒有query目標表,此時被其他會話改變目標資料, rr事務此時才建立快照,因此會在新的基礎上修改資料), 後續內容不會被其他會話的commit影響,如果內容被其他會話改過,異動內容時會報 `ERROR: could not serialize access due to concurrent update`, 必須重開事務。 因此使用RR isolation, 需要在代碼層面處理交易重試
b. RC特性可能導致某些交易處理異常: 如果交易都是單純的以id為where條件,即使併發仍然可能達到如預期的結果。 但是也有許多交易是帶有比較複雜的條件,或者有些表根本沒有設置id,就可能導致交易結果異常:
例如update ....where user_name = A and age = 18, 如果在併發中其他會話改變了其中任一個條件,都將使當前會話的更新失敗。 這時候 RR的嚴謹特性就可以避免類似錯誤發生
3. Serializable效能差,一般盡量不使用,但psql沒有間隙鎖,因此需要避免幻讀的場景,要透過鎖表、或者採用Serializable
psql 在Serializable採用謂詞鎖,它能够判断在它先运行的情况下,什么时候一个写操作会对一个并发事务中之前读取的结果产生影响。在PostgreSQL中,这些锁并不导致任何阻塞,并且因此不会导致死锁。 相比下鎖表對性能的影響會是更加嚴重,死鎖的機率也會上升
如要採用,且系統應用場景對效能要求時,參考官方文檔推薦的優化效能配套措施
4. serializable視圖資料的建立timing: 假設打算合計表內每筆資料的金額時,一張表正在不斷被其他會話新增資料,當一個串行級別開始事務,它尚未有其他表的視圖,在進入事務第一次對table select時建立起視圖,之後其他會話提交的資料不會對我們進行的合計結果產生影響
5. 目前測試心得是認為,一般時候使用rc+一些技巧即可應用大多場景,需要更嚴謹的避免不可重複讀、update為空對象等情境使用RR, 會對整表處理的,在事務中採用臨時變更隔離政策的方式改成serializable
### 操作
doc: http://www.postgres.cn/docs/9.3/sql-set-transaction.html
1. 查看當前事務隔離級別:
`
show transaction_isolation;
`
2. 查看預設隔離級別:
`show default_transaction_isolation;`
3. 为当前事务设置特性,对后面的事务没有影响:
`set transaction ISOLATION LEVEL [level]`
如果执行SET TRANSACTION之前没有执行START TRANSACTION 或BEGIN,那么它会显得没有效果一样,因为事务将立即结束
4. 为一个会话中随后的每个事务设置的事务特性。 在单独的事务中,可以用SET TRANSACTION覆盖这些默认事务特性:
`SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL [level]`
5. 開啟事務同時设置当前事务隔离级别:
`START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
或:
BEGIN ISOLATION LEVEL READ UNCOMMITTED READ WRITE;`
6. 修改預設隔離級別:
`set default_transaction_isolation='repeatable read';`
需重啟docker or db
7. 查看数据库事务隔离级别 (跟 2可能一樣效果)
`SELECT name, setting FROM pg_settings WHERE name ='default_transaction_isolation';`
或
`SELECT current_setting('default_transaction_isolation');`
8. 设置全局事务隔离级别 (跟 6 可能一樣效果,需要時再測試一下即可,不是這裡重點)
方法一:修改配置文件postgresql.conf,设置默认的隔离级别,如下:
vim postgresql.conf
default_transaction_isolation = 'read committed'
重新加载配置:
pg_ctl -D /data/pg reload
方法二:`alter system set default_transaction_isolation to 'REPEATABLE READ';`
需重啟docker or db
9. 修改time out
`SET LOCAL lock_timeout = '1s'`;
## psql lock mode
ACCESS SHARE,ROW SHARE, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE,SHARE ROW EXCLUSIVE,EXCLUSIVE,ACCESS EXCLUSIVE
##### 注意:PostgreSQL採用多版本併發控制(MVCC),以確保當查詢需要修改或刪除資料時,資料在高併發環境中可用且一致。 由於每個事務都使用其資料庫的副本,因此寫入或讀取操作都不會阻塞對方。 -- 因此防止超賣、餘額為負數等情境,需要搭配其他的方法
https://hevodata.com/learn/postgresql-locks/#table
## 鎖行鎖表
select for update [nowait];
select for share [nowait];
set local lock_timeout='3s'; 设置lock超时时间,0=取消
在事务处理模块(BEGIN/COMMIT)中才能生效
psql沒有間隙鎖,即使手動鎖不存在的目標範圍也不會生效
#### postgres 在rc, rr模式下 select for update 有帶入where條件情況下,無論有無使用到索引,都只會鎖行,除非不帶入where。 這點跟mysql不同,算是psql在效能上的進化
#### for update鎖行,不會阻擋其他會話新增紀錄,需要鎖表或者串行
#### 连表查询加锁时,不支持单边连接形式
例如:
select u.*,r.* from db_user u left join db_role r on u.roleid=r.id for update;
支持以下形式,并锁住了两个表中关联的数据:
select u.*,r.* from db_user u, db_role r where u.roleid=r.id for update;
測試案例:https://blog.csdn.net/qq_40891009/article/details/106007658
#### 查看鎖表予解除 1
--查询是否锁表了
select oid from pg_class where relname='lockTableName';
select pid from pg_locks where relation='上面查出的oid';
--如果查询到了结果,表示该表被锁 则需要释放锁定
select pg_cancel_backend(上面查到的pid);
#### 查看鎖表予解除 2
select a.locktype,a.database,a.pid,a.mode,a.relation,b.relname
from pg_locks a
join pg_class b on a.relation = b.oid
where upper(b.relname) = 'TABLE_NAME';
以上为查询某表上是否存在锁的SQL语句。
查到后发现确实存在锁,如下:
locktype | database | pid | mode | relation | relname
----------+----------+-------+-----------------+----------+---------
relation | 439791 | 26752 | AccessShareLock | 2851428 |table_name
relation | 439791 | 26752 | ExclusiveLock | 2851428 |table_name
再根据上面查出来的pid去表pg_stat_activity查询一下该锁对应的SQL语句:
select usename,current_query ,query_start,procpid,client_addr from pg_stat_activity where procpid=17509;
如下:
usename | current_query | query_start | procpid | client_addr
-----------+---------------------------------------------------------------------------------------------------------------+-------------------------------+---------+----------------
gpcluster | DELETE FROM TABLE_NAME WHERE A = 1 | 2011-05-14 09:35:47.721173+08 | 17509 | 192.168.165.18
(1 row)
通过以上可以发现,就是上面的锁导致该语句一直挂在那里。然后把该锁结束掉后,应用很快跑完。
然后核查应用的代码,发现代码里面两个事务都没有提交操作。后增加提交操作后,重新跑数,很快跑完。
如果要杀死,首先向相关人员确认这个语句是否为关键进程。
杀掉方法:在PG数据库mydb服务器中,查询这个进程PID然后Kill掉。
> ps -ef|grep 17509
postgres 17509 4868 1 Nov18 ? 00:11:19 postgres: postgres mydb 192.168.165.18(56059) SELECT
postgres 30832 30800 0 15:18 pts/3 00:00:00 grep 17509
> ps -ef|grep 17509
postgres 17509 4868 1 Nov18 ? 00:11:19 postgres: postgres mydb 192.168.165.18(56059) SELECT
postgres 30838 30800 0 15:19 pts/3 00:00:00 grep 17509
> kill -9 17509
## 併發場景 - 防超賣、餘額不足或負數
#### 一般思維 select 餘額足夠後update,在併發情境下多個線程會同時通過select餘額檢核,造成餘額為不足或負數的情況產生
有多種解決方案,此處列出兩種參考:
#### 參考解法一、先update 並帶上扣款金額,然後query檢核,餘額不足則rollback並報error, 利用增刪改會lock的特性進行排隊
轉帳一千元的情境
```
beginTranse(开启事务)
try{
// 修改餘額後查詢
$update->query('update account set amount = amount - 1000 where amount>=1000 and USER_ID = 12345');
if 其他意外情形{
rollBack(回滚)
}
}catch($e Exception){
rollBack(回滚)
}
commit(提交事务)
```
#### 參考解法二、扣款流程使用select for update
驗證餘額是否足夠同時上鎖
餘額仍能查詢,但其他併發在驗證餘額時即會進入等待前一個交易完成並釋放鎖。
##### 補充
對於秒殺級商品,肯定不能全靠數據庫支撐,要另外設計緩存機制。 例如十件商品搶購,緩存設計100個計數,後面的全擋掉,只讓一百人進入頁面或搶購,降低db request最多到100併發
## 其他參考資料
psql 官方文檔 - 併發: http://www.postgres.cn/docs/14/mvcc.html
psql vs mysql: https://dbaplus.cn/news-11-3235-1.html
關於序列化的特別現象:
---
當A 是序列化,B是RC: A select表 -> B 插入新資料 + commit -> A 插入一筆全表合計 commit
結果是 A的合計資料不包含B新增 : 可以理解,因為A的視圖建立在首次query
當A,B都是 序列化: A select -> B select -> A insert 一筆全表合計 -> B insert 一筆全表合計 + commit -> A commit
結果是 A 報錯誤 `錯誤:由於事務之間的讀/寫依賴性,無法序列化訪問
詳細信息:原因代碼:在提交嘗試期間,在標識為樞軸時被取消。
提示:如果重試,事務可能會成功。`
為何如此
同時存在不同隔離等級的會話,讓結果變得更複雜了
可能是不同會話,但是對同一張表聲的視圖會覆蓋的,這樣就可以解釋這種狀態,而當兩個s產生不同視圖並影響彼此時,psql的謂詞鎖檢測到以後便報error; 當一個會話不是s,謂詞鎖不會檢測到被影響,因此可以產生忽略其他會話交易的結果