.NET+PostgreSQL實踐與避坑指南

簡介

.NET+PostgreSQL(簡稱PG)這個組合我已經用了蠻長的一段時間,感受仍是挺不錯的。不過大多數人提及.NET平臺,仍是會想起跟它「原汁原味」配套的Microsoft SQL Server(簡稱MSSQL),其實沒有MSSQL也沒有任何問題,甚至沒有Windows Server都沒問題,誰說用.NET就必定要上微軟全家桶?這都什麼年代了……html

PG和MSSQL的具體比較我就不詳細展開了,自行搜一下,這種比較分析文章不少。應該說兩個RDBMS各有特點,MSSQL工具集龐大(大多咱們都用不到或不會用),安裝較爲麻煩,PG比較小巧,但功能也不弱,咱們要的它都有,性能方面我作過簡單的增刪查改的測試,二者看不出什麼明顯差異,MSSQL貌似最近才提供了Linux版,而PG天生跨平臺,MSSQL的受權費彷佛不低(沒深究),PG開源免費,對比較摳的客戶來講,是不太願意另外花錢買一套MSSQL的,PG就是很是不錯的選擇。git

但願你看完本文以後,也同我同樣以爲.NET + PostgreSQL,Rocks!沒問題的了。github

PG的版本

PG應該選擇什麼版本?Linux仍是Windows?固然是首選Linux,但開發環境無所謂,你在你本身的工做電腦上安裝一個Windows版也是沒問題的,有人說二者性能差距較大,Linux明顯要好於Windows,但我有作過測試,這個並無被證明如此,然而,我仍是推薦Linux,一來安裝簡便,二來配置簡單(命令行界面用起來感受比較一致),三來方便寫一些腳原本實現數據庫定時備份之類的。其實你並不須要擔憂安裝了PG後電腦會變慢,我徹底感受不出來,它是個安靜的乖萌寵,你不叫它,它就靜靜坐在那裏,個人Windows電腦上也安裝了一個PG,我常常用它來作一些腳本測試或試驗。另外,如今也能在Windows下直接安裝Linux版本的PG了,WSL瞭解下?sql

PG有不少的版本,如今的最新版是10.4,它前面的版本是9.6.x,嗯?有點奇怪不是?10.4只有「兩段」,而9.6.x有三段,其實以前一直是三段,9表示大版本,6表示中版本,後面是小版本,小版本只有小的功能改進,不會對數據格式形成任何影響,就是說,你的PG從9.6.1升級到9.6.9,你直接升了把舊程序替換掉就是,保證沒有任何問題。但若是你以前的版本是9.5.3,要升級到9.6.9,那就不行了,由於中間版本變了,你須要用一個遷移工具去把你的舊的數據格式轉爲新的方可,那對10.4這個版本而言,哪一個是大版本,哪一個是中版本,哪一個是小版本?這裏我感受有點不連貫,PG在從9升級到10的時候,彷佛丟掉了「大版本」,10雖然是9的後繼,但它應該算一箇中版本,因此,10.1升級到10.4是不用轉換數據的,直接升級程序便可。那PG的下一個中版本是什麼?沒錯,是11,再下一個應該就是12了。軟件這個東西,若是你沒什麼歷史包袱,我以爲直接選擇最新的,好比選擇10.4,未來升級10.5,10.6的時候也簡單。數據庫

說點額外的,PG10是去年(2017)正式推出的,距離如今都不到一年,剛出來的時候我就想,這個「重大升級」(想一想看iPhone X,Mac OS X,10這個數字是很特別不是?)能不能帶來性能上的大提高呢?我試了一下,結論是:沒有。確實它的升級文檔上也沒說起到性能有什麼明顯提高,它主要增長了對錶分區的原生支持,表分區,就是你的表中的數據的數量不少不少的時候,經過表分區來提升讀寫速度,至於表要多大才推薦分區呢?PG的官方文檔說是:若是表的尺寸遇上了你主機的內存的時候,能夠考慮表分區……因此,對於那些只有區區幾千萬行或幾百萬行數據的表,你肯定要分區嗎?緩存

Npgsql

要用.NET使用PG,就得用nuget引入Npgsql這個包,這是它的官方網站:http://www.npgsql.org/,徹底開源,它其實就是針對PG數據庫的ADO.NET引擎(ADO.NET Data Provider)。這裏是它的幫助手冊:http://www.npgsql.org/doc/index.html安全

這裏邊並無太多難點,你所須要作的,就是安裝好你的PG數據庫(Windows版/Linux版都行,沒有什麼影響),而後建立一個.NET項目(我推薦使用.NET Core),引入Npgsql,而後照着說明手冊上的簡單例子入一下門便可。app

本文固然不會具體帶你如何開始使用SELECT語句,下面主要講述在使用過程當中,咱們所克服的一些困難或踩過的坑。分佈式

NVARCHAR呢?

MSSQL中用得最多的的文本類型是NVARCHAR,這是一個帶長度限制的文本類型,對應地,PG中有VARCHAR,這樣用沒問題,但PG中的文本類型其實跟MSSQL中的文本類型是有點區別的,PG的文本基本上能夠認爲不限長度,VARCHAR及TEXT對PG內部來講,並無什麼差異,只是在寫入的時候,VARCHAR會檢查一下長度,因此性能上來看,VARCHAR並不比TEXT要快,較真的話可能還會慢點,由於它要檢查長度嘛,因此你在設計數據庫的時候能夠無腦地將全部文本類型設置爲TEXT(或後面提到的CITEXT),長度檢查工做放在業務系統中去作便可。ide

想要大小寫不敏感怎麼辦?

絕大多數時候,咱們都是但願大小寫不敏感的,大小寫敏感反倒會帶來不少困惑,查詢不出,或者系統中存在同名的用戶,一個叫John另外一個叫john,MSSQL能夠在建立庫的時候指定大小寫不敏感,而PG彷佛沒有這樣的功能,它須要藉助一個額外的組件,叫CITEXT,CI的意思就是Case Insensitive。要使用CITEXT組件,你須要安裝postgresql10-contrib包(假設你安裝的是PG10,若是不是的話你去找對應的包),再使用如下命令建立CITEXT類型:

CREATE EXTENSION IF NOT EXISTS CITEXT WITH SCHEMA public;

注:一個database只須要執行一次這個命令便可

若是你使用的是psql客戶端連上去使用PG的話,這時候已經OK了,你會發現CITEXT的字段已是大小寫不敏感了,但若是你用的是Npgsql用代碼去訪問PG的話,CITEXT彷佛沒生效,其實緣由是這樣的,CITEXT並非PG的原生類型,你在用查詢語句的時候,須要在參數後面加上「::CITEXT」顯式地告訴PG,你的參數是CITEXT類型,例子以下:

SELECT * FROM test_table WHERE test_name=@TextName::CITEXT AND category=@Category::CITEXT

嗯,我認可是有點麻煩,但習慣就好,我如今還不知道有什麼更佳方法。

使用CITEXT時候出現NotSupportedException

這個異常的呈現內容大體如此:

System.NotSupportedException: The field 'application_id' has a type currently unknown to Npgsql (OID 41000). You can retrieve it as a string by marking it as unknown, please see the FAQ.
在 Npgsql.NpgsqlDataReader.GetValue(Int32 ordinal)
在 Npgsql.NpgsqlDataReader.get_Item(Int32 ordinal)
……

這個錯誤對咱們而言,曾經像個幽靈似的,時不時出現,出現的時候重啓一下服務程序就行了,再也不出現,而後過幾個星期或者幾個月又出現,有時候一天出現屢次也不是沒有可能。最後是到github上面求助才最終搞懂了緣由。連接:https://github.com/npgsql/npgsql/issues/1635

簡單地說,PG對各類數據類型,是有一個內部的ID值的(叫oid),Npgsql在第一次鏈接數據庫的時候,會獲取到這些oid值並緩存起來,對於PG的內部類型,如INT什麼的,這些oid值是固定的,但對於CITEXT彷佛不是這樣,由於CITEXT這個類型是我門本身用CREATE EXTENSION命令建立的(請參考本文前面內容),建立的時候肯定其oid。咱們在還原數據庫的時候,也至關於從新建立了CITEXT類型,這樣會致使CITEXT的oid發生變化,但Npgsql並不知道,因此就出現了這個異常。咱們在開發過程當中經常須要作還原數據庫的動做,因此致使了這個問題的發生。

解決方法1,當數據庫還原了以後,調用NpgsqlConnection.ReloadTypes(),刷新各種型oid,但這個很難,由於還原數據庫都是手動操做,作完以後打開網頁,在上面點一下通知程序嗎?

解決方法2,重啓一下程序。這個其實跟解決方法1差很少,只不過不須要寫什麼額外代碼,考慮到還原數據庫這個動做其實也不是太頻繁,只是在開發環境中作,因此重啓就重啓吧,咱們如今就幹,規定還原數據庫後本身重啓下服務程序。(寫個腳本幹這個事情很簡單)

使用事務進行大量操做時候致使程序崩潰

這個問題我一樣到github上求助了,連接:https://github.com/npgsql/npgsql/issues/1838

這個問題比前面的問題可能更嚴重,由於我極可能捕捉不到異常(就是說有時候能夠捕捉到,有時候不行),程序直接崩潰了,對於一個.NET程序來講,這是很不該該的事情,即使我沒單獨寫try-catch,程序的最外層異常處理器應該也能捕捉到相關的Exception並log對不?但偏不,沒有log,也捕捉不到。因此至今我懷疑這是一個.NET的bug,可能跟Npgsql並無關係。

問題的緣由如github上所描述,是找到了,但卻沒法從根本上修正,這個問題實際上是個簡單的「事務超時」問題。

咱們的程序在第一次啓動的時候會初始化數據庫的表,插入大量的初始化數據,因爲咱們公司的開發環境比較特殊,數據庫延遲十分高,因此致使插入速度很慢,每條插入耗時可高達幾十毫秒,(生產環境並無這個問題)這樣一萬多條數據下來就致使了事務超時(事務超時默認時間是1分鐘)。解決方法固然很明顯了:初始化的時候,臨時增長 TransactionScope的超時值,增長到10分鐘,這樣總歸沒問題了。

相似這種問題咱們只能經過一些外部的workaround來預防,很難從根本上解決。

55000: 禁用已準備好的事務

這又是一個有點棘手的事情,首先是這個中文翻譯得很很差,這是一條數據庫拋出來的出錯信息,它的英文是「Prepared transactions are disabled」,其正確的中文翻譯我以爲應該是:預處理事務已被禁用。唉,因此我說爲何要英文版,若是提示中文,想在網上找答案都會多些障礙。

對事務的使用,這裏有個簡單的例子:

    using (NpgsqlConnection conn = new NpgsqlConnection(connectionStr)) {
        conn.Open();
        using (TransactionScope ts = new TransactionScope()) {
            conn.EnlistTransaction(Transaction.Current);
                //SQLs...
            }
            ts.Complete();
        }
    }

什麼叫「預處理事務」?其實很簡單,就是「事務包事務」,就是能夠分步提交的事務,好比我先開啓了一個事務A,在這個事務中我又開啓了一個事務B,B提交,A再提交。PG對於預處理事務是默認關閉的,固然了,你能夠打開它,編輯配置文件postgresql.conf,把max_prepared_transactions改成100(默認是0,0表示禁用),重啓PG服務便可。

但你肯定你真的用獲得預處理事務嗎?我看下來咱們是用不到的,但爲何出現這個問題?——仍是咱們程序寫得有問題,即使你從單個方法上看不出來事務包事務。如下兩種場景可能會出現「預處理事務」:

1,我建立了一個方法A訪問數據庫,這個方法可能會被其它方法調用,因此它有個DbConnection類型的參數,表示調用者負責打開數據庫鏈接傳遞過來,而A裏面開啓了事務,而調用者並不知情,也開啓了事務,造成預處理事務

2,這種狀況更隱晦些,數據庫鏈接字符串,如:Host=192.168.1.101; Username=postgres; Password=123456; Database=testdb; Enlist=true,在後面有個叫Enlist的參數爲true,這表示這個鏈接在打開的時候,會自動Enlist到當前執行上下文的Transaction中去,若是當前執行上下文中打開了事務(從代碼上看包含在了using(TransactionScope)中),那這個數據庫鏈接就自動Enlist上去了,再考慮這樣的場景:A方法會本身打開數據庫鏈接去查詢點什麼東西,B方法也會訪問數據庫,且B方法會使用事務,事務中調用了A方法,A方法打開數據庫鏈接的時候發現當前執行上下文中存在Transaction,因而自動Enlist上去了,不經意間造成了預處理事務,且仍是「分佈式」的(A和B打開的多是不一樣的數據庫鏈接),這種狀況應該並非你所須要的

那咱們應該怎麼作?下面是個人作法:

1,max_prepared_transactions仍是設置爲0,關掉,由於咱們真用不到,若是用獲得,那就是咱們代碼寫錯了,因此一旦出現「禁用已準備好的事務」這個異常,就回去檢查代碼

2,把Enlist=true在數據庫鏈接字符串中去掉,這麼一來,每次使用事務都須要顯式地調用 conn.EnlistTransaction(Transaction.Current),雖然對了一行代碼,但語義更明確,也不用考慮究竟是TransactionScope包DbConnection或反過來DbConnection包TransactionScope

3,規範化咱們的數據庫訪問代碼,明確哪些是須要事務哪些是不須要的,在各個方法的註釋上註明

40001:因爲多個事務間的讀/寫依賴而沒法串行訪問

它對應的英文是:Cound not serialize access due to read/write dependencies among transactions,這個應該怎麼理解呢?其實瞭解數據庫事務隔離級別的人對這個應該不會陌生。.NET的TransactionScope默認使用的是事務隔離級別中的最高級別——Serializable(可序列化)。這個級別最大程度上確保了數據的一致性,但代價也挺高,一來速度較慢,二來很容易出現「事務間讀/寫依賴」,就是這個錯誤了,舉個簡單的例子:

A、B兩個事務,同時訪問test表中id爲50的一條記錄,A讀出這條記錄,接着B更新了這條記錄並提交,根據可序列化的隔離級別的規則,A並不知道B更新了記錄,A在B提交後嘗試修改這條記錄,這時候數據庫就會讓A事務失敗,並拋出這個異常,由於讓A修改爲功的話,就會致使B以前的修改不經意間丟失了,可序列化隔離級別並不容許這種狀況的發生。

因此,這是個「正常的錯誤」,按常規的業務邏輯來講,應該不多會出現,若是真的出現,且頻繁出現,那須要考慮下是否是業務邏輯設計得不太合理,看看能不能從設計上避免這個問題,若是業務邏輯必定如此,那能夠用下面的方法嘗試一下:

1,將這種並行事務用客戶端代碼排個隊,弄個線程安全隊列,逐個執行,這樣速度會慢點,但確保了每一個事務都能成功

2,捕捉這個異常,而後自動重試,其實這也是數據庫推薦的正統的作法

3,下降事務隔離級別,這個可能會出現問題,也可能不出現,這徹底取決於你的業務,關於事務隔離級別,這是個蠻大的話題,我考慮適當時候再寫一篇文章

4,對於極少出現的頻次來講,能夠不處理,僅僅須要捕捉這個異常類型,而後提示用戶重試便可,不少網站貌似都這麼幹的

總結

有時間的話我會另外開一篇文章來寫寫PG的一些常規用法,如熱備冷備還原維護等,但不太能保證何時能寫出來。

暫時先總結那麼多,誰若是有這方面的問題的話,歡迎留言。

相關文章
相關標籤/搜索