Web項目演化系列--分布式鎖
項目初期的時候,一般會發布到一臺主機上,當達到負載極限時,要想提升其性能,要么提升硬件,要么多臺主機,然而成本上的花銷,后者比前者便宜太多了,雖然便宜,但是卻更加復雜。
大多數編程語言提供的各種鎖只會對同一項目的同一主機的代碼產生作用,當同一項目發布在多臺主機的時候,這些主機中的項目要形成一個整體,因此原先同步訪問共享資源的代碼將會失去效果。
由于共享資源多種多樣,如:文件、業務的臨時狀態、數據庫數據等,本章的同步鎖主要解決的是不依賴于主機環境的共享資源,如:數據庫數據; 而共享資源依賴于項目環境時,想要同步訪問共享資源,則當某主機共享資源變動時,需要將其同步到其他主機,也就是集群服務器了,如果不想要搭建集群服務器,可將相應的功能剝離出來成為單一的項目,也就是分布式結構。
由于后期必然會演變成分布式架構,而各個結構又是集群,因此如果當前情況下就把項目構架得太多復雜,投入再多的人力也是很難完成的,因此要先簡化結構,一步步實現,至于先集群還是先分布,看個人喜好了。
實現
實現的主要目標就是保證任意時刻,只能有一個線程可以得到操作的權利。
首先來定義鎖的接口,可以提供2個方法:Lock、Unlock,也可以只提供Lock,然后返回Unlock,如果Unlock為null則表示加鎖失敗。
既然講到唯一,如果不依賴其他的額外資源的情況下,很多人應該已經想到了,那就是數據庫表的主鍵,因此實現思路就是加鎖的時候向數據庫中插入一條記錄,那么成功插入的操作就獲取到了鎖,然后解鎖時,刪除這條記錄即可,初步實現如下:
public delegate void UnlockDelegate(); public UnlockDelegate Lock(string key) { using (var conn = new SqlConnection(this.connectionString)) { conn.Open(); using (var cmd = new SqlCommand(string.Empty, conn)) { try { var createdRows = Create(cmd, key); if (createdRows > 0) { return () => { DeleteById(cmd, key); conn.Close(); }; } } catch { conn.Close(); } return null; } } } private int Create(SqlCommand cmd, string key) { cmd.Parameters.Clear(); cmd.CommandText = this.insertSql; cmd.Parameters.AddWithValue("@id", key); return cmd.ExecuteNonQuery(); } private int DeleteById(SqlCommand cmd, string key) { cmd.Parameters.Clear(); cmd.CommandText = this.deleteSql; cmd.Parameters.AddWithValue("@id", key); return cmd.ExecuteNonQuery(); }
項目運行過程當中沒有絕對的安全,總有一些內因、外因導致項目出現錯誤,如果某個主機獲取了鎖以后,該主機因為某些原因沒有釋放鎖,那么其他的主機將會無法再獲取到該鎖了。
那么鎖就需要一個過期時間,因此我們需要在表中增加一個表示鎖的創建時間,那么在創建鎖之前就需要先根據key去獲取鎖是否存在,如果存在且已經過期,那么刪除該記錄才能繼續創建鎖。
該處的刪除跟解鎖時的刪除是不一樣的,因為在多線程、并發環境下,程序并不能保證只有唯一一個線程獲取到了已存在的鎖數據,有可能多個線程都獲取到了鎖數據,有的可能已經準備刪除,而有的才剛剛獲取到,因此此處的刪除必須保證返回的影響行數大于0,否則直接返回null,重構后的代碼如下:
public UnlockDelegate Lock(string key, int expires = 5) { using (var conn = new SqlConnection(this.connectionString)) { conn.Open(); using (var cmd = new SqlCommand(string.Empty, conn)) { var createdOn = GetCreatedOnById(cmd, key); if (createdOn > 0) { var nowOn = DateTime.Now.ToUnix(); if (nowOn - createdOn > expires) { var deletedRow = DeleteById(cmd, key); if (deletedRow == 0) { conn.Close(); return null; } } } try { var createdRows = Create(cmd, key, expires); if (createdRows > 0) { return () => { DeleteById(cmd, key); conn.Close(); }; } } catch { conn.Close(); } return null; } } }
由于DateTime并沒有直接轉換成時間戳的方法,因此該方法需要自己擴展,實現思路就是當前時間-1970年的總毫秒數,這里就不提供代碼了,因為長時間都是依賴于orm來開發的,對sql已經很生疏了,因此各位要的是理解以上實現,不要太在意代碼。
簡化
使用數據庫來實現雖然代碼量不多,但需要數據庫的支持,連接字符串、表、字段都是可變的,如果不寫死的話,就需要提供不少的配置。
由于項目必然會使用到緩存,如:redis、memcache等高性能的緩存系統,而redis中提供了SetNX、Expires這樣的api,如果基于redis實現的話,只要幾行代碼便可完成。
相應的庫可以去 redis官網 查詢,這里的例子使用的是 Sider ,代碼如下:
private ThreadwisePool pool; public RedisMutex(string host) { this.pool = new ThreadwisePool(host); } public UnlockDelegate Lock(string key, int expires = 5) { var client = this.pool.GetClient(); var ok = client.SetNX(key, string.Empty); if (!ok) return null; client.Expire(key, new TimeSpan(0, 0, expires)); return () => client.Del(key); }
結束語
那么今天分享的文章就到這里了,如果代碼有錯誤或者有問題的話,請留言,謝謝。