PostgreSQL Practice & Tips - 4 - 事務(wù)與鎖

事務(wù)

介紹

事務(wù) transaction 可能是關(guān)系型數(shù)據(jù)庫最重要的功能之一,往往我們通過事務(wù)可以組織一系列的操作流纹,并且不必?fù)?dān)心一致性漱凝,這為我們編寫業(yè)務(wù)代碼提供了很大的便利茸炒,此外壁公,當(dāng)數(shù)據(jù)庫被并發(fā)訪問時(shí)紊册,事務(wù)的存在為我們提供了隔離變化的機(jī)制湿硝,讓開發(fā)人員不必考慮過于復(fù)雜的并發(fā)問題。

對(duì)于一個(gè)關(guān)系型數(shù)據(jù)庫來說铺浇,事務(wù)必須具有 ACID 特性鳍侣,以下引用自 Wikipedia:

  • 原子性(Atomicity):事務(wù)作為一個(gè)整體被執(zhí)行倚聚,包含在其中的對(duì)數(shù)據(jù)庫的操作要么全部被執(zhí)行惑折,要么都不執(zhí)行惨驶。
  • 一致性(Consistency):事務(wù)應(yīng)確保數(shù)據(jù)庫的狀態(tài)從一個(gè)一致狀態(tài)轉(zhuǎn)變?yōu)榱硪粋€(gè)一致狀態(tài)粗卜。一致狀態(tài)的含義是數(shù)據(jù)庫中的數(shù)據(jù)應(yīng)滿足完整性約束续扔。
  • 隔離性(Isolation):多個(gè)事務(wù)并發(fā)執(zhí)行時(shí)纱昧,一個(gè)事務(wù)的執(zhí)行不應(yīng)影響其他事務(wù)的執(zhí)行砌些。
  • 持久性(Durability):已被提交的事務(wù)對(duì)數(shù)據(jù)庫的修改應(yīng)該永久保存在數(shù)據(jù)庫中仑荐。

對(duì)于 PostgreSQL 來說粘招,事務(wù)的使用和其他數(shù)據(jù)庫沒什么特別的區(qū)別洒扎,都是使用 BEGIN 來啟動(dòng)一個(gè)事務(wù)袍冷,并且在合適的時(shí)機(jī)使用 COMMIT 或者 ROLLBACK 進(jìn)行提交或者回滾胡诗。當(dāng)然煌恢,PostgreSQL 還支持 SAVEPOINT 存檔功能瑰抵,對(duì)于一個(gè)較長的事務(wù)二汛,你可以自己設(shè)置回滾點(diǎn)习贫,例如下面的操作:

for_test=# BEGIN;
BEGIN
for_test=# INSERT INTO consumers(id, consumer_id) VALUES (1, 'jerry');
INSERT 0 1
for_test=# 
for_test=# INSERT INTO consumers(id, consumer_id) VALUES (2, 'Tom');
INSERT 0 1
for_test=# SELECT * FROM consumers;
-[ RECORD 1 ]------
id          | 1
consumer_id | jerry
-[ RECORD 2 ]------
id          | 2
consumer_id | Tom

for_test=# SAVEPOINT save_point_1;
SAVEPOINT
for_test=# INSERT INTO consumers(id, consumer_id) VALUES (3, 'Bob');
INSERT 0 1
for_test=# INSERT INTO consumers(id, consumer_id) VALUES (1, 'Harry');
ERROR:  duplicate key value violates unique constraint "consumers_pk"
DETAIL:  Key (id)=(1) already exists.
for_test=# SELECT * FROM consumers;
ERROR:  current transaction is aborted, commands ignored until end of transaction block
for_test=# ROLLBACK TO SAVEPOINT save_point_1;
ROLLBACK
for_test=# SELECT * FROM consumers;
-[ RECORD 1 ]------
id          | 1
consumer_id | jerry
-[ RECORD 2 ]------
id          | 2
consumer_id | Tom

for_test=# COMMIT;
COMMIT

可以注意的是颤绕,因?yàn)?INSERT INTO consumers(id, consumer_id) VALUES (1, 'Harry'); 的失敗奥务,導(dǎo)致事務(wù)無法繼續(xù)氯葬,此時(shí)我們進(jìn)行查詢會(huì)獲得 ERROR: current transaction is aborted, commands ignored until end of transaction block帚称,這時(shí)候你可以回滾到你的存檔點(diǎn)就可以繼續(xù)其他操作了闯睹,可以看到我們?cè)?ROLLBACK TO SAVEPOINT save_point_1; 后,依舊可以進(jìn)行 COMMIT 去提交事務(wù)的孩锡。

此外躬窜,PostgreSQL 的 MVCC 實(shí)現(xiàn)可以允許你將很多 DDL 語句放入事務(wù)中斩披,也就是說對(duì)表的結(jié)構(gòu)進(jìn)行修改、增加索引等都可以事務(wù)化仍劈,例如下面的例子:

for_test=# BEGIN;
BEGIN
for_test=# ALTER TABLE consumers ADD created_at TIMESTAMPTZ NOT NULL DEFAULT now();
ALTER TABLE
for_test=# \d consumers
                    Table "public.consumers"
   Column    |           Type           |       Modifiers        
-------------+--------------------------+------------------------
 id          | bigint                   | not null
 consumer_id | text                     | 
 created_at  | timestamp with time zone | not null default now()
Indexes:
    "consumers_pk" PRIMARY KEY, btree (id)

for_test=# ROLLBACK;
ROLLBACK
for_test=# \d consumers
     Table "public.consumers"
   Column    |  Type  | Modifiers 
-------------+--------+-----------
 id          | bigint | not null
 consumer_id | text   | 
Indexes:
    "consumers_pk" PRIMARY KEY, btree (id)

可以清楚的看到讹弯,ROLLBACK 后组民,我們新增的列就不存在了臭胜。這個(gè)功能可以說是與其他數(shù)據(jù)庫最大的區(qū)別了耸三。

隔離級(jí)別

講事務(wù)就不能不說隔離級(jí)別,基本上在后端服務(wù)開發(fā)的領(lǐng)域积锅,使用事務(wù)最多的場(chǎng)景就是支持并發(fā)處理缚陷,事務(wù)提供的不同隔離級(jí)別能夠滿足我們的業(yè)務(wù)場(chǎng)景蹬跃,此外蝶缀,還需要理解下一部分鎖才算是完全清楚關(guān)系型數(shù)據(jù)庫的并發(fā)處理機(jī)制。我們這里先說事務(wù)的隔離級(jí)別:Transaction Isolation柄慰。

對(duì)于 SQL 標(biāo)準(zhǔn)坐搔,默認(rèn)是存在四種事務(wù)的隔離級(jí)別的概行,對(duì)于最嚴(yán)格的 Serializable 一般可以理解為凳忙,對(duì)于并發(fā)執(zhí)行一組 Serializable 的事務(wù)涧卵,保證他們執(zhí)行完的效果是與按照一定順序執(zhí)行的效果完全一致的勤家,所以我們一般認(rèn)為是串行的。而另外三種隔離級(jí)別是根據(jù)并發(fā)事務(wù)不一致的情況所描述的柳恐,而這些情況在 Serializable 級(jí)別下都是不可能發(fā)生的伐脖,在其它級(jí)別下,是可能發(fā)生的(可能發(fā)生的意思是說胎撤,如果跑了一次一組事務(wù)后晓殊,一致性沒有問題伤提,這種情況下無法判斷事務(wù)之間的影響會(huì)造成一致性的問題巫俺,但實(shí)際上這些異常現(xiàn)象是可能發(fā)生的)肿男。所以介汹,在說隔離級(jí)別之前,需要提一下我們希望避免的不一致性的情況

  • 臟讀(dirty read):一個(gè)事務(wù)讀取了另一個(gè)正在執(zhí)行的舶沛,沒有提交的事務(wù)的寫入數(shù)據(jù)嘹承。
  • 不可重復(fù)讀(non-repeatable read):一個(gè)事務(wù)重新讀取前面讀取過的數(shù)據(jù)時(shí),發(fā)現(xiàn)該數(shù)據(jù)已經(jīng)被另一個(gè)事務(wù)所修改了如庭,往往另一個(gè)事務(wù)的修改提交在第一次讀取之后叹卷,理解不可重復(fù)讀可以直接從字面上理解,就是重復(fù)讀會(huì)出事兒F核V柚瘛!往毡。
  • 幻讀(phantom read):一個(gè)事務(wù)重新執(zhí)行了一個(gè)查詢語句蒙揣,返回的滿足查詢條件的一組數(shù)據(jù),這時(shí)發(fā)現(xiàn)开瞭,這一組數(shù)據(jù)已經(jīng)被另一個(gè)事務(wù)所修改(往往出現(xiàn)于當(dāng)前事務(wù)讀取一組記錄懒震,這組記錄中包含了另一個(gè)事務(wù)增加或者刪除的記錄)。
  • 序列化異常(serialization anomaly):成功提交一組事務(wù)的結(jié)果與按照所有可能的順序運(yùn)行這些事務(wù)的結(jié)果嗤详,存在不一致性个扰。

按照 SQL 標(biāo)準(zhǔn),PostgreSQL 也實(shí)現(xiàn)了這些隔離級(jí)別葱色,如下表:

事務(wù)隔離級(jí)別 臟讀 不可重復(fù)讀 幻讀 序列化異常
讀未提交 Read Uncommitted 可能出現(xiàn) NPG 可能出現(xiàn) 可能出現(xiàn) 可能出現(xiàn)
讀提交 Read Committed 不可能 可能出現(xiàn) 可能出現(xiàn) 可能出現(xiàn)
可重讀 Repeatable Read 不可能 不可能 可能出現(xiàn) NPG 可能出現(xiàn)
序列化 Serializable 不可能 不可能 不可能 可能出現(xiàn)

這些標(biāo)準(zhǔn)的隔離級(jí)別是 PostgreSQL 支持的锨匆,你都可以在 transaction 開始時(shí)指定這些標(biāo)準(zhǔn)的隔離級(jí)別,但是,實(shí)際上 PostgreSQL 只實(shí)現(xiàn)了三種隔離級(jí)別恐锣,對(duì)于 讀未提交 Read Uncommitted 來說,其行為是與 讀提交 Read Committed 完全一致的舞痰,使用它只是為了適配 SQL 隔離級(jí)別的標(biāo)準(zhǔn)土榴。

注意,在上表中响牛,帶 NPG 的 cell (例如 可能出現(xiàn) NPG)有極其重要的特殊含義玷禽,即:在 SQL 標(biāo)準(zhǔn)中,這些不一致的現(xiàn)象是可能會(huì)出現(xiàn)的呀打,但是在 PostgreSQL 中不會(huì)出現(xiàn)矢赁。所以 PostgreSQL 中沒有讀未提交,并且 Repeatable Read 不會(huì)出現(xiàn)幻讀的現(xiàn)象贬丛。

怎么去練習(xí)隔離級(jí)別撩银?

很多同學(xué)對(duì)于隔離級(jí)別的理解不準(zhǔn)確,或者不深是缺乏相應(yīng)的練習(xí)豺憔,導(dǎo)致對(duì)這些不一致的現(xiàn)象并不敏感额获,從而在項(xiàng)目中忽略了可能會(huì)造成不一致問題的點(diǎn)從而遭受挫折,下面我們就可以試試這些隔離級(jí)別到底是怎么一回事恭应。當(dāng)然寫本文的時(shí)候抄邀,我發(fā)覺我的電腦上并沒有安裝 PostgreSQL环础,剛好我們可以使用 docker荐类,練習(xí)隔離級(jí)別時(shí),我們需要兩個(gè)不同的 session 模擬并發(fā)的情況锹漱,所以我們需要做如下的準(zhǔn)備:

# 啟動(dòng)數(shù)據(jù)庫服務(wù)
docker run --name some-postgres -d postgres:9.5 # 版本 11胆屿,10奥喻,9 都可以

# 連接數(shù)據(jù)庫
docker exec -it some-postgres psql -U postgres

# 創(chuàng)建數(shù)據(jù)庫
postgres=# create database for_test;
CREATE DATABASE
postgres=# \c for_test
You are now connected to database "for_test" as user "postgres".

為了描述方便,我們將這個(gè) session 其稱為 T1莺掠,下來我們?cè)?TI 中創(chuàng)建測(cè)試數(shù)據(jù)衫嵌,并對(duì)當(dāng)前 session 關(guān)閉 auto commit:

CREATE SEQUENCE user_id_seq;

CREATE TABLE users (
    id BIGINT NOT NULL DEFAULT nextval('user_id_seq'),
    type VARCHAR(10) NOT NULL,
    name VARCHAR(128) NOT NULL,
    address TEXT,
    married BOOLEAN DEFAULT false,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    PRIMARY KEY (id)
);

-- 搞點(diǎn)測(cè)試數(shù)據(jù),這次來 20 個(gè)
INSERT INTO users (type, name, address)
SELECT 'testing', left(md5(i::text), 10), left(md5(random()::text), 50)
FROM generate_series(1, 20) s(i);

-- 在 psql client 中關(guān)閉 auto commit
\set AUTOCOMMIT off
練習(xí)1 讀未提交 Read Uncommitted讀未提交 Read Committed

我們需要開啟另一個(gè) terminal彻秆,就叫它 T2 好了楔绞,然后連接 PostgreSQL 服務(wù),并關(guān)閉 AUTOCOMMIT:

docker exec -it some-postgres psql -U postgres -d for_test
for_test=# \set AUTOCOMMIT off

這時(shí)候唇兑,在 T1 中開啟 transaction酒朵,并使用 Read Uncommitted 隔離級(jí)別:

BEGIN TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

然后我們?cè)?T2 中進(jìn)行數(shù)據(jù)修改,注意扎附,這里是不進(jìn)行 COMMIT 提交修改的

BEGIN;
UPDATE users SET name = 'from t2' WHERE id = 11;

這時(shí)候蔫耽,T1 去進(jìn)行查詢操作,你會(huì)發(fā)現(xiàn),數(shù)據(jù)并沒有被改變匙铡,那是因?yàn)?PostgreSQL 并沒讀未提交的隔離級(jí)別图甜,也就是說,你只能讀取到已經(jīng)提交的數(shù)據(jù)鳖眼。

SELECT * FROM users WHERE id = 11;
 id |  type   |    name    |             address              | married |          created_at           |          updat
ed_at           
----+---------+------------+----------------------------------+---------+-------------------------------+---------------
----------------
 11 | testing | 6512bd43d9 | 29c8eaa9ceb56fda9f6838d2bbcd744a | f       | 2019-05-25 16:22:10.503297+00 | 2019-05-25 16:
22:10.503297+00
(1 row)

然后黑毅,你如果你在 T2 中進(jìn)行提交:

COMMIT;

再在 T1 中重讀:

SELECT * FROM users WHERE id = 11;
 id |  type   |  name   |             address              | married |          created_at           |          updated_
at           
----+---------+---------+----------------------------------+---------+-------------------------------+------------------
-------------
 11 | testing | from t2 | 29c8eaa9ceb56fda9f6838d2bbcd744a | f       | 2019-05-25 16:22:10.503297+00 | 2019-05-25 16:22:
10.503297+00
(1 row)

數(shù)據(jù)已經(jīng)被修改了,T1 讀取到了 T2 提交后的數(shù)據(jù)钦讳。這里我們稍微總結(jié)一下:

  1. PostgreSQL 的讀未提交與讀提交等價(jià)矿瘦,如果 transaction 的隔離級(jí)別設(shè)置成了這兩種,都表示愿卒,只能讀取到被提交的數(shù)據(jù)缚去。
  2. 可以看到 T1 中我們讀取了兩次數(shù)據(jù)琼开,但是兩次的結(jié)果不一致易结,所以對(duì)于 SELECT * FROM users WHERE id = 11; 存在不可重讀的情況。
  3. PostgreSQL 默認(rèn)的隔離級(jí)別是 讀提交 Read Committed稠通,不是比較嚴(yán)格的 Repeatable Read衬衬。

所以為了加深印象,我們可以進(jìn)行這種額外的練習(xí):

  1. 幻讀:T1 中使用 Read Committed 開啟事務(wù)改橘,T2 中開啟另一個(gè)事務(wù)滋尉,并執(zhí)行 INSERT,然后 T1 進(jìn)行 SELECT * FROM users;全表搜索飞主,觀察是否有 T2 插入的數(shù)據(jù)狮惜,然后 T2 COMMIT,T1 重復(fù)執(zhí)行碌识,觀察是否有 T2 插入的數(shù)據(jù)碾篡。
  2. 真正的 讀未提交:既然有了 docker,使用 MySQL 鏡像重復(fù)之前的練習(xí)筏餐,可以使用 innoDB 作為數(shù)據(jù)庫引擎开泽,觀察在 MySQL 在讀未提交的情況下的結(jié)果。例如這樣啟動(dòng) MySQL:
docker run --name some-mysql -p 3306:3306  -e MYSQL_ROOT_PASSWORD=99887766 -d mysql
練習(xí)2 可重讀 Repeatable Read

在 T1 中開啟 transaction魁瞪,并使用 Repeatable Read 隔離級(jí)別:

BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;

然后我們?cè)?T2 中進(jìn)行數(shù)據(jù)修改穆律,注意,這里是不進(jìn)行 COMMIT 提交修改的

BEGIN;
UPDATE users SET name = 'from t2 RR' WHERE id = 11;

這時(shí)候导俘,T1 去進(jìn)行查詢操作峦耘,數(shù)據(jù)并沒有被改變(依舊 name 值是上次 from t2),那是因?yàn)?可重讀 Repeatable Read 不會(huì)發(fā)生臟讀旅薄。

SELECT * FROM users WHERE id = 11;
 id |  type   |  name   |             address              | married |          created_at           |          updated_
at           
----+---------+---------+----------------------------------+---------+-------------------------------+------------------
-------------
 11 | testing | from t2 | 29c8eaa9ceb56fda9f6838d2bbcd744a | f       | 2019-05-25 16:22:10.503297+00 | 2019-05-25 16:22:
10.503297+00
(1 row)

然后 T2 提交:

COMMIT;

T1 再進(jìn)行查詢:

SELECT * FROM users WHERE id = 11;
 id |  type   |  name   |             address              | married |          created_at           |          updated_
at           
----+---------+---------+----------------------------------+---------+-------------------------------+------------------
-------------
 11 | testing | from t2 | 29c8eaa9ceb56fda9f6838d2bbcd744a | f       | 2019-05-25 16:22:10.503297+00 | 2019-05-25 16:22:
10.503297+00
(1 row)

可以發(fā)現(xiàn)本質(zhì)性的區(qū)別辅髓,name 的值依舊是 from t2,因?yàn)?code>可重讀 Repeatable Read 確保了兩次讀取的值的一致性。同時(shí)洛口,我們知道矫付,PostgreSQL 中 可重讀 Repeatable Read 其實(shí)并不會(huì)出現(xiàn)幻讀現(xiàn)象,如果你在 T2 中進(jìn)行 INSERT 并 COMMIT绍弟,T1 中還是之前的結(jié)果(21 條數(shù)據(jù)技即,但是 T2 中已經(jīng)有 22 條了)。

照例樟遣,我們可以額外練習(xí)下:

  1. 幻讀:完成之前提到的幻讀的實(shí)驗(yàn),理解在 PostgreSQL 的 Repeatable Read 中不會(huì)出現(xiàn)幻讀的現(xiàn)象身笤。
  2. 重復(fù)幻讀:在 MySQL 中使用 Repeatable Read 重復(fù)幻讀實(shí)驗(yàn)豹悬,對(duì)照結(jié)果。
  3. 思考:不可重復(fù)讀液荸、幻讀這兩種不一致的現(xiàn)象會(huì)對(duì)我們的應(yīng)用產(chǎn)生什么影響瞻佛?我們應(yīng)該怎么合理的使用隔離級(jí)別有什么意義?

萬事大吉娇钱?

假設(shè)我們有這樣一段業(yè)務(wù)邏輯伤柄,當(dāng)用戶在網(wǎng)站上進(jìn)行購買時(shí),我們需要對(duì)用戶的賬戶進(jìn)行扣款文搂,因?yàn)槲覀兪遣辉试S用戶白吃白喝的适刀。所以,我們的業(yè)務(wù)邏輯大約是煤蹭,先讀取用戶有多少錢笔喉,然后再扣去用戶購買產(chǎn)品的價(jià)格,如果用戶賬戶的錢不夠的話硝皂,就返回錯(cuò)誤不進(jìn)行任何修改常挚,我們寫的代碼大約這個(gè)樣子的:

  1. 打開一個(gè) transaction,準(zhǔn)備進(jìn)行結(jié)算操作稽物。
  2. 讀取目標(biāo)用戶的賬戶中有多少錢奄毡,并且存放在代碼的變量中。
  3. 檢查總共的錢數(shù)與購買產(chǎn)品的價(jià)格贝或,如果合理進(jìn)行扣款吼过,并進(jìn)行提交,事務(wù)結(jié)束傀缩。
  4. 如果購買的錢數(shù)大約賬戶數(shù)那先,就不進(jìn)行扣款,直接報(bào)錯(cuò)給用戶赡艰,事務(wù)結(jié)束售淡。

從我們之前學(xué)習(xí)到的知識(shí)來說,我們肯定希望讀取的錢數(shù)是已經(jīng)提交成功的,所以應(yīng)該使用 Read Committed 隔離級(jí)別揖闸,這樣揍堕,如果同時(shí)用戶對(duì)賬戶進(jìn)行了兩次扣款,我們只能讀取到已經(jīng)提交后的賬戶余額汤纸,感覺上是沒錯(cuò)的衩茸。但是很遺憾的是,不論是 Read Committed 或者 Repeatable Read 都無法解決另一個(gè)問題贮泞,也就是當(dāng)我們的 transaction 完成第二歩并讀取到了最新的余額例如 50¥楞慈,這時(shí)候,另一個(gè) transaction 對(duì)余額進(jìn)行了修改啃擦,將其改為了 30¥囊蓝,這時(shí)候我們的 transcation 進(jìn)行第三步扣款 40¥的操作就會(huì)出現(xiàn)問題,導(dǎo)致余額變成了 -10¥令蛉。這時(shí)候我們就知道聚霜,單單使用事務(wù)與其隔離級(jí)別是完全不夠的,我們需要一種機(jī)制在第二歩和第三步之間鎖定記錄珠叔,只允許我們進(jìn)行修改蝎宇,那么這就是下面我們要講的 PostgreSQL 中的鎖。

表鎖與行鎖

如同其他關(guān)系型數(shù)據(jù)庫一樣祷安,PostgreSQL 也有行鎖和表鎖之分姥芥,顧名思義,他們面向的操作對(duì)象是不同的辆憔。但是這些鎖并不是按照我們的想法是“鎖定某些對(duì)象”撇眯,而更貼近描述對(duì)數(shù)據(jù)庫進(jìn)行 CRUD 時(shí)狀態(tài)的記錄,并根據(jù)這些狀態(tài)來決定操作虱咧。簡(jiǎn)單來說熊榛,當(dāng)需要增刪改查時(shí),我們先要獲得表上的鎖腕巡,然后再獲得行鎖玄坦,然后才能進(jìn)行操作。

以下是 PostgreSQL 的表級(jí)鎖:

  • ACCESS SHARE
    只與 ACCESS EXCLUSIVE 沖突
    SELECT 命令將會(huì)在引用的表上加上該鎖绘沉,通常任何讀取并不修改的查詢都會(huì)要求這種表鎖煎楣,所以 SELECT 并發(fā)是沒有沖突的,因?yàn)?ACCESS SHARE 和 ACCESS SHARE 并不沖突车伞。

  • ROW SHARE
    EXCLUSIVEACCESS EXCLUSIVE 這兩種鎖沖突
    只有 SELECT FOR UPDATESELECT FOR SHARE 這兩個(gè)命令會(huì)要求這樣的表鎖择懂,我們常常用來鎖定一些需要被修改的數(shù)據(jù)。

  • ROW EXCLUSIVE
    SHARE SHARE ROW EXCLUSIVE EXCLUSIVE ACCESS EXCLUSIVE 鎖沖突另玖。
    UPDATE DELETE INSERT 這些命令會(huì)請(qǐng)求這樣的表鎖困曙,如果這些命令 SELECT 了其他表的資源表伦,同樣還會(huì)產(chǎn)生 ACCESS SHARE 的鎖。簡(jiǎn)單來說慷丽,任何修改數(shù)據(jù)的請(qǐng)求都會(huì)申請(qǐng)這個(gè)表鎖蹦哼。

  • SHARE UPDATE EXCLUSIVE
    SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVEACCESS EXCLUSIVE 這些表鎖沖突,這個(gè)鎖防止了并發(fā)的表結(jié)構(gòu)更改與 VACUUM 運(yùn)行要糊。
    命令 VACUUM (without FULL), ANALYZE, CREATE INDEX CONCURRENTLY ALTER TABLE VALIDATE 或者其他 ALTER TABLES 會(huì)申請(qǐng)這個(gè)鎖纲熏。

  • SHARE
    ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE 鎖模式?jīng)_突,這個(gè)鎖防止并發(fā)的數(shù)據(jù)改動(dòng)锄俄。
    只有非并發(fā)式創(chuàng)建索引會(huì)請(qǐng)求這個(gè)表(也就是 CREATE INDEX)局劲,所以有了這個(gè)鎖修改數(shù)據(jù)是不行的,但是可以訪問數(shù)據(jù)奶赠,ACCESS SHARE 是不沖突的容握。

  • SHARE ROW EXCLUSIVE
    ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE 沖突,這個(gè)鎖保護(hù)表不會(huì)受到數(shù)據(jù)修改车柠,并且這個(gè)鎖與自己排他,表示只有一個(gè) session 能夠持有這個(gè)鎖塑猖。
    CREATE TRIGGERALTER TABLE 會(huì)請(qǐng)求這個(gè)鎖竹祷,全世界只有一個(gè)這個(gè)鎖

  • EXCLUSIVE
    ROW SHARE, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, 以及 ACCESS EXCLUSIVE 鎖模式?jīng)_突羊苟。
    只有 REFRESH MATERIALIZED VIEW CONCURRENTLY 會(huì)請(qǐng)求這個(gè)鎖塑陵,不是很重要。

  • ACCESS EXCLUSIVE
    與所有鎖模式?jīng)_突蜡励,這個(gè)鎖模式保證只有一個(gè) transaction 可以訪問被鎖的數(shù)據(jù)表令花。
    DROP TABLE, TRUNCATE, REINDEX, CLUSTER, VACUUM FULL, REFRESH MATERIALIZED VIEW (without CONCURRENTLY) 都會(huì)請(qǐng)求這個(gè)鎖,所以 VACUUM FULL 是非常危險(xiǎn)的凉倚,就像 major GC 一樣兼都,會(huì) block 到所有的操作。某些 ALTER TABLE 也會(huì)請(qǐng)求這個(gè)鎖稽寒, 這也是 LOCK TABLE 語句的默認(rèn)鎖級(jí)別扮碧。

PostgreSQL 和其他關(guān)系型數(shù)據(jù)庫一樣,都花了一個(gè)表格來描述鎖之間的沖突杏糙,但是有這么多鎖難以記下來并且理解慎王,首先需要明確的是,這些鎖都是表鎖宏侍,一開始只有 SHARE 和 EXCLUSIVE 鎖赖淤,SHARE 鎖表示不能修改數(shù)據(jù),則與所有的 EXCLUSIVE 鎖沖突谅河,而 EXCLUSIVE 鎖則具有排他性咱旱,與所有的 SHARE 鎖沖突确丢,但是如果只有這兩個(gè)鎖性能也太差了,特別是并發(fā)的情況下莽龟,我們經(jīng)常對(duì)數(shù)據(jù)表又讀又寫蠕嫁,使用這兩個(gè)鎖是沒有效率的。于是我們就引入了 ACCESS SHARE 鎖毯盈,表示可以在查詢時(shí)允許對(duì)表內(nèi)的數(shù)據(jù)修改剃毒,這就是說你在一個(gè) SESSION 中 SELECT 并不影響另一個(gè) SESSION 對(duì)其進(jìn)行 INSERT 或者 UPDATE。ACCESS SHARE 的描述范圍太大了搂赋,有時(shí)候我們只需要操作幾行數(shù)據(jù)進(jìn)行更新赘阀,所以我們引入了 ROW SHARE 和 ROW EXCLUSIVE 這兩種鎖,他們都是表級(jí)鎖脑奠,但他們并不互相沖突基公,他們只是表示會(huì)進(jìn)一步的使用行鎖來控制并發(fā),可以說這兩個(gè)鎖是最重要的鎖宋欺。

ROW EXCLUSIVE 通過 UPDATE轰豆、INSERT、DELETE 命令生成齿诞,所以必須是排他的酸休,但是在表中我們并不知道 UPDATE 的數(shù)據(jù)范圍,因?yàn)橥瑫r(shí) UPDATE 兩行不同的數(shù)據(jù)是被允許的祷杈,所以 ROW EXCLUSIVE 一定不能和自己沖突斑司。ROW SHARE 也是一樣的,我們通過其“鎖定”某一些數(shù)據(jù)但汞,而并不是全表宿刮,所以它與它自己也是不沖突的。既然這兩個(gè)鎖都無法互相沖突私蕾,那么我們還需要另外一種機(jī)制來控制并發(fā)僵缺,也就是行鎖。對(duì)于 PostgreSQL 來說是目,也存在行級(jí)的讀鎖與寫鎖谤饭,因?yàn)樽x鎖沒有排他性,所以控制并發(fā)的責(zé)任實(shí)際上是行鎖決定的懊纳。

對(duì)于一個(gè) UPDATE 語句揉抵,它先會(huì)要求在表上的 ROW EXCLUSIVE 鎖,如果獲取到了嗤疯,它就會(huì)繼續(xù)要求所修改數(shù)據(jù)的行鎖冤今,這時(shí)候行鎖如果被另一個(gè)語句所獲取,那它就必須等待茂缚,直到鎖被釋放戏罢。我們可以根據(jù)下面的實(shí)驗(yàn)重現(xiàn)這個(gè)場(chǎng)景:

打開第一個(gè) ternimal屋谭,稱為 T1:

\set AUTOCOMMIT off

BEGIN;

-- 獲得進(jìn)程號(hào)
SELECT pg_backend_pid();
 pg_backend_pid 
----------------
            920
(1 row)

-- 進(jìn)行修改不提交
UPDATE users SET name = 'from t1 updating' WHERE id = 11;

-- 查看表鎖
SELECT locktype,
       relation::regclass as relation,
       transactionid,
       MODE,
       GRANTED
FROM pg_locks
WHERE pid = 920;

   locktype    |  relation  | transactionid |       mode       | granted 
---------------+------------+---------------+------------------+---------
 relation      | pg_locks   |               | AccessShareLock  | t
 relation      | users_pkey |               | RowExclusiveLock | t
 relation      | users      |               | RowExclusiveLock | t
 virtualxid    |            |               | ExclusiveLock    | t
 transactionid |            |           636 | ExclusiveLock    | t
(5 rows)

可以看到在第三行, "relation | users | | RowExclusiveLock | t" 表示龟糕,我們已經(jīng)在表上獲取了 RowExclusiveLock桐磁。

這時(shí)候我們打開 T2:

BEGIN;

SELECT pg_backend_pid();
 pg_backend_pid 
----------------
            276
(1 row)

-- 進(jìn)行更新
UPDATE users SET name = 'from t2 updating' WHERE id = 11;
-- 注意,此時(shí)這句話會(huì)被卡主讲岁,因?yàn)槲覀冊(cè)趪L試更新同一條數(shù)據(jù)

然后我們回到 T1我擂,再查看下 T2 的鎖:

SELECT locktype,
       relation::regclass as relation,
       transactionid,
       MODE,
       GRANTED
FROM pg_locks
WHERE pid = 276;
   locktype    |  relation  | transactionid |       mode       | granted 
---------------+------------+---------------+------------------+---------
 relation      | users_pkey |               | RowExclusiveLock | t
 relation      | users      |               | RowExclusiveLock | t
 virtualxid    |            |               | ExclusiveLock    | t
 transactionid |            |           636 | ShareLock        | f
 transactionid |            |           637 | ExclusiveLock    | t
 tuple         | users      |               | ExclusiveLock    | t
(6 rows)

第二條數(shù)據(jù) " relation | users | | RowExclusiveLock | t" 表示,進(jìn)程 276 也就是 T2 同樣獲得了 RowExclusiveLock缓艳,因?yàn)槲覀冎罢f過校摩,RowExclusiveLock 這個(gè)表鎖是不會(huì)相互沖突的,所以是被 granted 的阶淘,但是為什么 T2 還被卡主了呢衙吩?這就是之前提到過的行級(jí)的寫鎖所產(chǎn)生的排他效果,T2 必須等待 T1 完成并釋放溪窒,然后才能完成結(jié)果坤塞。如果你這時(shí)候再 T1 進(jìn)行 COMMIT,你就可以看到 T2 的 UPDATE 返回結(jié)果 UPDATE 1 了澈蚌。

PostgreSQL 不會(huì)在內(nèi)存中記錄行鎖尺锚,因?yàn)樾墟i的開銷實(shí)在是太大了,如果每一條記錄的行鎖都被記錄惜浅,我們的共享內(nèi)存是不夠的。這時(shí)候你可以觀察上表中的 transactionid 發(fā)現(xiàn)伏嗜,637 也就是 T2 的事務(wù)是被 T1 中的 636 所互斥的坛悉,通過這一點(diǎn)我們就可以 debug 死鎖的問題了。

悲觀鎖與樂觀鎖

對(duì)于 PostgreSQL 的鎖模式我們已經(jīng)學(xué)習(xí)過了承绸,但是遺憾的是這對(duì)于我們?cè)谥疤岬竭^的扣款付費(fèi)的業(yè)務(wù)場(chǎng)景是無效的裸影,因?yàn)槲覀儗?shí)際上是需要兩個(gè)操作的配合才能完成業(yè)務(wù),根本的需求便是在獲取存款后军熏,我們不希望任何人改變存款轩猩,只能由我們進(jìn)行扣款。經(jīng)過修改荡澎,我們的業(yè)務(wù)大約是這個(gè)樣子:

  1. 打開一個(gè) transaction均践,準(zhǔn)備進(jìn)行結(jié)算操作。
  2. 讀取目標(biāo)用戶的賬戶中有多少錢摩幔,并鎖定彤委,并且存放在代碼的變量中。
  3. 檢查總共的錢數(shù)與購買產(chǎn)品的價(jià)格或衡,如果合理進(jìn)行扣款焦影,解除鎖定车遂,事務(wù)結(jié)束。
  4. 如果購買的錢數(shù)大約賬戶數(shù)斯辰,就不進(jìn)行扣款舶担,解除鎖定,事務(wù)結(jié)束彬呻。

一般來說衣陶,這種 OCC 問題在沖突可能性不大的情況下,我們會(huì)使用樂觀鎖废岂,往往樂觀鎖并不是一個(gè)真正的鎖祖搓,它并不記錄互斥關(guān)系,所以樂觀鎖的實(shí)現(xiàn)往往是在應(yīng)用程序中實(shí)現(xiàn)的湖苞,我們?cè)谶@里就不贅述了拯欧,可以參考 JPA 的實(shí)踐來進(jìn)行理解,這方面的文章也特別多财骨,就不寫了镐作。

但是對(duì)于悲觀鎖,往往是在數(shù)據(jù)庫層面實(shí)現(xiàn)的隆箩,在 PostgreSQL 中我們就可以使用 ROW SHARE 這個(gè)表鎖以及行鎖來達(dá)到目的该贾,例如:

SELECT name FROM users WHERE id = 11 FOR UPDATE;
-[ RECORD 1 ]----------
name | from t2 updating

SELECT locktype,                             
       relation,
       transactionid,
       MODE,
       GRANTED
FROM pg_locks
WHERE pid = 992;
-[ RECORD 3 ]-+----------------
locktype      | relation
relation      | 16387
transactionid | 
mode          | RowShareLock
granted       | t

這時(shí)候,RowShareLock 和行鎖已經(jīng)被加到 id = 11 的行中了捌臊,任何人都無法修改(你可以開啟另一個(gè) session 嘗試修改杨蛋,并查看下鎖的情況),然后我們就可以進(jìn)行余額計(jì)算等更新事務(wù)了理澎。所以在我們第一節(jié)提到的“萬事大吉逞力?”的問題上,我們已經(jīng)解決了這個(gè)并發(fā)問題糠爬。悲觀鎖認(rèn)為修改發(fā)生沖突的可能性很大寇荧,所以真是在加互斥的鎖來解決這個(gè)問題,顯而易見的是這樣效率會(huì)比較低执隧,因?yàn)槿绻?qǐng)求過多那么大家可能都在等待鎖揩抡。因?yàn)檫@一部分網(wǎng)上已經(jīng)有很多文章在描述與分析了,我們就不啰嗦了镀琉,可以參考下面:

所以總結(jié)一下峦嗤,不論是悲觀鎖還是樂觀鎖,都可以幫助我們解決一致性的問題屋摔,但是具體使用哪一種以及如何實(shí)現(xiàn)寻仗,還需要按照自己的數(shù)據(jù)庫、應(yīng)用凡壤、事務(wù)控制等綜合考慮署尤。數(shù)據(jù)庫優(yōu)化是一項(xiàng)復(fù)雜的事情耙替,性能、一致性曹体、查詢方式等都是需要深思熟慮的俗扇,在這一層面請(qǐng)不要迷信任何簡(jiǎn)單的解決方案,熟悉自己的應(yīng)用程序所做的事情箕别,再進(jìn)行調(diào)整優(yōu)化才是有意義的铜幽,三張表的 JOIN 未必會(huì)慢,樂觀鎖的性能未必會(huì)好串稀,base on fact and know your application first除抛。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市母截,隨后出現(xiàn)的幾起案子到忽,更是在濱河造成了極大的恐慌,老刑警劉巖清寇,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喘漏,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡华烟,警方通過查閱死者的電腦和手機(jī)翩迈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來盔夜,“玉大人负饲,你說我怎么就攤上這事∥沽矗” “怎么了绽族?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長衩藤。 經(jīng)常有香客問我,道長涛漂,這世上最難降的妖魔是什么赏表? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮匈仗,結(jié)果婚禮上瓢剿,老公的妹妹穿的比我還像新娘。我一直安慰自己悠轩,他們只是感情好间狂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著火架,像睡著了一般鉴象。 火紅的嫁衣襯著肌膚如雪忙菠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天纺弊,我揣著相機(jī)與錄音牛欢,去河邊找鬼。 笑死淆游,一個(gè)胖子當(dāng)著我的面吹牛傍睹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播犹菱,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼拾稳,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了腊脱?” 一聲冷哼從身側(cè)響起访得,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎虑椎,沒想到半個(gè)月后震鹉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡捆姜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年传趾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泥技。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡浆兰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出珊豹,到底是詐尸還是另有隱情簸呈,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布店茶,位于F島的核電站蜕便,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏贩幻。R本人自食惡果不足惜轿腺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望丛楚。 院中可真熱鬧族壳,春花似錦、人聲如沸趣些。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至拢操,卻和暖如春锦亦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背庐冯。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來泰國打工孽亲, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人展父。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓返劲,卻偏偏與公主長得像,于是被迫代替她去往敵國和親栖茉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子篮绿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容