利用SQL實現簡單的分布式鎖
分布式鎖和普通鎖的主要區別在於參與主體跨不同節點,因此需要考慮到節點失效和網絡故障的問題。搞清楚問題要點,可以用各種不同的東西去實現,比如Redis,ZooKeeper等。但是其實用SQL實現也是非常容易的,下面以PostgreSQL為例進行說明。
1. 方法1:會話鎖
利用PostgreSQL中特有的排他會話級別咨詢鎖。
pg_advisory_lock(key bigint)
pg_advisory_unlock(key bigint)
pg_try_advisory_lock(key bigint)
詳細參考: http://www.postgres.cn/docs/9.4/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS-TABLE
這種鎖是會話級的,在釋放鎖之前,鎖的獲得得者必須一直持有這個會話,也就是連接,否則鎖就會被釋放。
這個特性自然而然地解決了鎖的獲得者發生故障時鎖的釋放問題。
但是,對於需要長時間持有的鎖,它會產生長連接,而數據庫的連接是比較耗資源的,往大了配一般也就幾千個,這是需要注意的地方。
另外一個需要考慮的問題是,當網絡或節點發生故障時連接的兩端未必能立刻感知到,因此TCP的KeepAlive是必須的,幸好PostgreSQL的客戶端和服務端都支持這個設置。
下面是服務端的參數:
tcp_keepalives_idle
tcp_keepalives_interval
tcp_keepalives_count
2. 方法2:期限鎖
鎖對象是持久的,為防止拿到鎖的客戶端奔潰導致鎖無法釋放,每個鎖都有一個過期期限。
在PostgreSQL中可以按下面的方式實現
建表
- postgres=# create table distlock(id int primary key,expired_time interval,owner text,ts timestamptz);
- CREATE TABLE
- postgres=# insert into distlock(id) values(1);
- INSERT 0 1
加鎖和續期
- postgres=# update distlock set owner='node1',ts=now(),expired_time=interval '20 second' where id=1 and (owner='node1' or owner is null or now() > ts + expired_time);
- UPDATE 1
獲得鎖的客戶端如果要長時間持有鎖必須定期執行相同的方法對鎖進行續租,否則會丟鎖。
此時,其它客戶端取鎖會失敗
- postgres=# update distlock set owner='node2',ts=now(),expired_time=interval '20 second' where id=1 and (owner='node2' or owner is null or now() > ts + expired_time);
- UPDATE 0
等鎖過期後取鎖成功
- postgres=# update distlock set owner='node2',ts=now(),expired_time=interval '20 second' where id=1 and (owner='node2' or owner is null or now() > ts + expired_time);
- UPDATE 1
釋放鎖
- postgres=# update distlock set owner=null,ts=now() where id=1 and owner='node2';
- UPDATE 1
3. 總結
可以看到用關系數據庫實現分布式鎖並不復雜。尤其上面基於表實現的鎖輔以靠譜的HA部署可以保障鎖信息的持久性和不丟失,但用表更新實現鎖畢竟比較重,不適合對鎖的性能要求非常高的場景。