1. 程式人生 > 其它 >tp5某個欄位相同的資料只取一次_Thinkphp 模型和資料庫:效能和安全

tp5某個欄位相同的資料只取一次_Thinkphp 模型和資料庫:效能和安全

技術標籤:tp5某個欄位相同的資料只取一次

本章的內容主要講解了如何給資料庫的CURD查詢添加回調事件,以及如何在最底層的SQL層面進行監聽和做出效能分析及對查詢效能做出優化建議,最後給出了一些安全方面的建議,學習內容主要從效能分析和優化,以及安全三個方面進行講解:

  • 效能分析
  • 資料庫除錯模式
  • 獲取查詢次數
  • 獲取SQL
  • 開啟效能分析
  • SQL監聽
  • 效能優化
  • SQL優化
  • 欄位快取
  • 資料快取
  • 模型快取
  • 查詢事件
  • 資料安全
  • 底層防護
  • 寫入過濾
  • 安全建議
  • 總結

效能分析

除了一些糟糕的業務邏輯,框架的效能瓶頸一般都是在資料庫(其它方面的效能沒什麼好糾結的)。業務邏輯的優化暫時不在本書的討論範疇,我們首先來學習如何進行資料庫的效能分析。

資料庫除錯模式

和應用的除錯模式不同,資料庫有自己獨立的除錯模式開關,在第一章我們已經提過,資料庫配置引數中的debug引數就是資料庫除錯模式的開關。

// 資料庫除錯模式'debug'           => true,

資料庫除錯模式開啟後,可以支援下列行為:

  • 記錄SQL日誌;
  • 分析SQL效能;
  • 支援SQL監聽;

由於上述行為不可避免會產生額外的開銷,因此對效能存在一定的影響,但並不大,因為所有的日誌是最終統一一次性寫入,而且可以設定為某個使用者才寫入日誌。

在生產模式下面,必須關閉應用除錯模式(app_debug),否則會暴露你的伺服器敏感資訊。和應用除錯模式不同,開啟資料庫除錯模式並不會對外暴露任何安全資訊,因此是否開啟資料庫除錯模式,看自己的需求。

獲取查詢次數

使用Db::getQueryTimes()方法可以獲取當前的資料庫查詢次數,如果使用true作為引數的話可以獲取包括寫操作在內的查詢次數。

// 獲取讀操作次數$read = Db::getQueryTimes();// 獲取所有的查詢次數$count = Db::getQueryTimes(true);

如果開啟了頁面Trace顯示的話,可以直觀的看到當前請求的查詢資訊。

0c363aef50000834035809980e9e5d84.png

呼叫儲存過程會被認為是執行一次查詢操作而非寫操作,儘管儲存過程內部可能會有寫入操作。

獲取SQL

可以用getLastsql方法獲取最後一次執行的SQL語句,無論是使用Db類還是模型類,所以下面的方式都是有效的:

Db::name('user')->where('id', '>', 0)->select();echo Db::getLastSql();$user = User::get(1);echo $user->getLastsql();

getLastSql方法即使關閉資料庫除錯模式一樣有效

如果使用了檔案型別記錄日誌,並且開啟了資料庫除錯模式的話,在日誌檔案中可以看到所有的SQL歷史記錄。

開啟效能分析

框架不但能記錄SQL日誌,而且可以對查詢的SQL語句作出效能分析,幫助你快速找出資料庫效能瓶頸。

確保在資料庫配置檔案中開啟下面兩個引數:

// 開啟資料庫除錯模式        'debug'           => true,        // 開啟SQL效能分析        'sql_explain'     => true,

開啟sql_explain引數後,會對查詢的SQL做EXPLAIN解析(由每個聯結器類的getExplain方法完成查詢SQL分析),並把解析結果合併記錄到SQL日誌中(注意:目前僅對Mysql資料庫有效)。

下面是一個查詢的分析日誌例子:

[ SQL ] SELECT * FROM `user` WHERE `id` IN (2) [ RunTime:0.000703s ][ EXPLAIN : array ( 'id' => 1, 'select_type' => 'SIMPLE', 'table' => 'think_user', 'partitions' => NULL, 'type' => 'system', 'possible_keys' => 'PRIMARY', 'key' => NULL, 'key_len' => NULL, 'ref' => NULL, 'rows' => 1, 'filtered' => 100.0, 'extra' => NULL, ) ]

SQL日誌中會記錄每個SQL的執行時間以及EXPLAIN分析結果,框架只是記錄分析結果,至於如何查出問題和解決則需要你具備一定的SQL效能分析和優化知識。

當EXPLAIN分析結果中的extra中使用了filesort或者temporary的話,系統會額外記錄一個警告錯誤告訴我們某條SQL存在效能問題需要處理。

SQL監聽

如果覺得內建的效能分析不夠全面,完全可以對執行的SQL進行監聽並且對接第三方的SQL分析類庫。使用listen方法註冊SQL監聽,例如可以在應用公共檔案或者某個行為擴充套件中新增如下程式碼:

Db::listen(function ($sql, $time, $explain) {    // 記錄SQL    Log::record($sql . ' [' . $time . 's]', 'sql');    // 檢視效能分析結果    dump($explain);});

如果關閉了sql_explain引數,explain引數就是一個空陣列,你可以在監聽方法中自行分析SQL效能問題。

監聽的閉包方法支援傳入三個引數,分別是:SQL語句、執行時間(秒)和效能分析結果(陣列),並注意如下事項:

  • 如果註冊了多個SQL監聽方法,則會依次呼叫;
  • 一旦註冊了SQL監聽,則SQL日誌和分析日誌自動無效,由監聽方法接管;

效能優化

現在我們已經基本掌握了效能分析的手段,那麼如何進行效能優化(本書中的優化範疇主要是資料庫操作層面的)就是擺在開發人員面前的一件棘手大事,如果是一般的應用可能主要做好資料表的索引就基本上沒什麼大的效能問題,對於大流量及高併發的應用,優化的手段和空間就比較多,因為這個情況下任何一個細小的優化都能帶來可觀的效能改進。

SQL優化

這裡說的SQL優化主要針對資料庫層面的優化,對於Mysql資料庫來說,下面是一些比較常規的建議:

  • 儘量少用SQL函式(會減少資料庫自身查詢快取的命中率)而是用PHP變數傳入;
  • 給常用的查詢欄位建立索引或者聯合索引;
  • 對JOIN的條件欄位建立索引,並且採用相同的資料型別(包括字符集);
  • 避免使用ORDER BY RAND();
  • 儘量呼叫field方法顯式列出查詢的欄位,即使用field(true) ;
  • 養成給資料表設定自增主鍵的習慣;
  • 合理設計你的資料表字段型別;
  • 對於大資料表使用垂直分表把資料表分為固定長度和不定長的兩個表;

更深層次的優化可以對Mysql的配置引數進行優化配置(沒有一勞永逸的配置優化,一定是針對應用場景的),相信大部分應用暫時還不需要到優化配置的地步,首先考慮的還是架構設計的優化,資料庫配置的優化策略對應用的部署遷移會造成額外的成本以及不可預知的問題,如果你不是一個DBA角色不建議頻繁調整配置引數。

欄位快取

說完了資料庫層面的優化,我們後面著重來說下框架和應用層面的優化。

為了更安全的進行資料庫操作,框架底層在查詢資料表資料的時候,會首先獲取該資料表的欄位資訊,包括欄位名稱、欄位型別以及主鍵名,對於不在欄位列表中的欄位則會進行忽略處理甚至丟擲異常,欄位型別則用於進行寫入和查詢的自動引數繫結,雖然說每個資料表只會獲取一次欄位資訊,但每次請求都要重新獲取一次不免覺得有點效能浪費。不過在開發階段,如果經常會涉及到欄位資訊的變化,還是無所謂,但如果已經部署上線了的話,還是建議使用欄位快取,也可以有效提高查詢效能,我們會在頁面Trace的SQL欄中看到類似的資訊

[ SQL ] SHOW COLUMNS FROM `user` [ RunTime:0.001582s ]

其實就是查詢資料表user的欄位資訊的SQL語句(不同的資料庫查詢欄位資訊的SQL語句是不同的,由聯結器類的getFields方法完成查詢)。

部署上線後,可以在命令列下執行以下指令生成欄位快取,在命令列切換到應用的根目錄(think檔案所在目錄),輸入:

php think optimize:schema

會自動生成當前資料庫配置檔案中定義的資料表字段快取,執行後會自動在runtime/schema目錄下面按照資料表生成欄位快取檔案,快取檔案的命名格式為:

資料庫名.資料表名.php

如果你的應用有多個數據庫的操作,也可以指定資料庫生成欄位快取(必須有使用者許可權),例如,下面用--db引數指定生成demo資料庫下面的所有資料表的欄位快取資訊。

php think optimize:schema --db demo

如果你的應用不同的模組使用了不同的資料庫連線,還可以根據模組來生成,用--module引數指定模組如下:

php think optimize:schema --module index

會讀取index模組的模型來生成資料表字段快取,沒有繼承thinkModel類的模型和抽象類不會生成。

每次執行指令都會重新生成資料表字段快取檔案,如果只是更改了資料表的某個欄位或者增加了新的欄位,重新部署上線的時候,支援單獨更新某個資料表的快取。

使用 --table引數指定需要更新的資料表:

php think optimize:schema --table user

支援指定資料庫名稱

php think optimize:schema --table demo.think_user

生成欄位快取後,你會發現資料庫的查詢效能提升明顯,尤其是在請求中操作大量資料表的情況下。

資料快取

資料庫的優化手段有時候比不過架構和快取的設計優化,而架構的優化是一個綜合的範疇,需要針對具體的邏輯和場景,並且優化的手段通常多元化,模型關聯的設計也是底層提供的架構設計的優化手段之一(使用預載入查詢可以有效減少資料庫查詢次數),現在我們要講的是如何利用資料快取策略來減少資料庫的查詢開銷,這是一個不依賴資料庫的普適優化策略。

資料庫的資料快取並不是你理解的直接使用Cache類進行操作,那樣太麻煩了,每次都要手動設定及額外讀取,也許像下面這樣:

$user = Cache::get('user_cache');if (!$user) {    $user = Db::table('user')        ->where('id', 10)        ->find();    Cache::set('user_cache', $user);}

查詢類封裝了一個數據快取的鏈式方法cache,可以很方便的進行查詢資料的自動快取和讀取,以及快取資料的自動更新。資料庫的快取策略主要就是掌握cache鏈式方法的使用,下面我們仔細給你講解下用法。

先給出一個最簡單的用法:

Db::table('user')    ->cache(600)    ->where('id', 10)    ->find();Db::table('user')    ->where('status', 1)    ->cache(600)    ->count();

可以對find、select、value和column方法及其衍生方法使用資料快取功能,不支援原生查詢query方法。

cache方法如果傳入數字,表示查詢資料的快取時間(秒),所以上面的查詢在10分鐘以內多次呼叫的話不會重複查詢資料庫,而是直接讀取快取資料(使用當前配置的快取型別和快取引數)。

如果需要在外部呼叫快取資料(儘管並不常見,但在跨模組的時候可能會需要),可以指定快取標識,例如:

Db::table('user')    ->cache('user_cache_key', 600)    ->where('id', 10)    ->find();

cache方法的第一個引數使用字串表示快取標識,這個時候第二個引數就表示快取有效期,然後可以在外部呼叫快取的使用者資料:

// 快取資料有效期為10分鐘$userData = Cache::get('user_cache_key');

內建的資料快取策略對原生查詢不起作用(只能單獨使用快取方法來進行快取),相比快取的優勢用原生查詢的那點效能優越感這個時候已經蕩然無存了,查詢構造器的優勢就很明顯了。

資料快取策略的關鍵是如何及時更新快取資料,我們來看下如何做到自動更新快取,下面的內容才是資料快取要講的關鍵。

只需要在呼叫更新或者刪除方法之前呼叫cache方法(見證奇蹟的時刻到了):

Db::table('user')    ->cache('user_data')    ->select([1, 3, 5]);Db::table('user')    ->cache('user_data')    ->update(['id' => 1, 'name' => 'thinkphp']);Db::table('user')    ->cache('user_data')    ->select([1, 3, 5]);

在更新資料的時候呼叫cache手動清除快取,所以最後查詢的資料不會受第一條查詢快取的影響,查詢出來的資料依然是同步更新後的資料。

同樣,如果進行了刪除操作,也會自動清除快取資料。

Db::table('user')    ->cache('user_data')    ->select([1, 3, 5]);Db::table('user')    ->cache('user_data')    ->delete(1);Db::table('user')    ->cache('user_data')    ->select([1, 3, 5]);

確保查詢和更新或者刪除使用相同的快取標識才能自動清除快取。

比較常用的資料快取是以主鍵為查詢條件的單個數據的快取,所以如果使用find方法並且使用主鍵查詢的情況,快取更新更智慧。update或者delete方法可以不需要呼叫cache方法,也會自動清理快取,例如:

Db::table('user')    ->cache(true)    ->find(1);Db::table('user')    ->update(['id' => 1, 'name' => 'topthink']);Db::table('user')    ->cache(true)    ->find(1);

根據主鍵查詢的話,快取更新是自動的,因此上面的例子最後查詢的資料會是更新後的資料。

使用where方法查詢主鍵條件的話,效果一樣:

Db::table('user')    ->cache(true)    ->where('id', 1)    ->find();Db::table('user')    ->where('id', 1)    ->update(['name' => 'topthink']);Db::table('user')    ->cache(true)    ->where('id', 1)    ->find();

模型快取

除了使用Db類,模型類還提供了更方便的方法進行資料快取。如果是快取讀取單個數據,可以使用:

// 查詢資料並快取讀取$user = User::get(1, [], true);// 設定快取有效期$user = User::get(1, [], 600);

由於第二個引數是預載入查詢,所以查詢快取屈居二線了_,不過如果你的版本在5.0.6以上的話,可以直接寫成:

// 查詢資料並快取讀取$user = User::get(1, true);// 設定快取有效期$user = User::get(1, 600);

當使用主鍵查詢、更新和刪除模型資料的時候,會自動更新模型資料快取。如果你的查詢條件不是主鍵,可以指定快取標識,並在刪除的時候帶上快取標識,例如:

模型資料快取標識不能直接在外部讀取,因為快取的資料都是陣列而不是物件,所以下面才是正確的姿勢。// 查詢name為thinkphp的使用者資料並快取讀取$user = User::cache('user_key_thinkphp')    ->getByName('thinkphp');// 刪除資料並更新快取資料$user->cache('user_key_thinkphp')    ->delete();

模型資料快取標識不能直接在外部讀取,因為快取的資料都是陣列而不是物件,所以下面才是正確的姿勢。

// 查詢name為thinkphp的使用者資料並快取讀取$user = User::cache('user_key_thinkphp')    ->getByName('thinkphp');// 外部讀取模型資料快取$data = new User(Cache::get('user_key_thinkphp'));

同樣的用法,如果要快取讀取多個數據,使用下面的方式:

// 查詢多個數據並快取讀取$users = User::all([1, 2, 3], [], true);// 設定快取有效期$users = User::all([1, 2, 3], [], 3600);

5.0.6版本以上同樣可以使用

// 查詢多個數據並快取讀取$users = User::all([1, 2, 3], true);// 設定快取有效期$users = User::all([1, 2, 3], 3600);

模型的資料快取配合關聯預載入查詢的話效果更佳,關於如何使用關聯預載入查詢請參考上一章的內容。

查詢事件

使用查詢事件可以在不改變原有資料查詢程式碼的前提下制定獨立的快取策略,先來了解下什麼是查詢事件。

查詢事件是針對資料庫的CURD操作而設計的回撥方法,主要包括:

事件 描述 before_select select查詢前回調 before_find find查詢前回調 after_insert insert操作成功後回撥 after_update update操作成功後回撥 after_delete delete操作成功後回撥

使用下面的方式註冊一個查詢事件

Db::event('before_select', function ($options, $query) {    // 事件處理});

如果before_select或者before_find回撥方法有返回資料,則表示提前返回查詢結果,不會繼續執行查詢操作。

Db::event('before_find', function ($options, $query) {    // 事件處理    if ('user' == $options['table']) {        $result = ['id' => 1, 'name' => 'thinkphp'];        return $result;    }});$user = Db::table('user')->find();

user變數最終的結果是['id'=>1,'name'=>'thinkphp']。

下面的例子我們沒有使用cache方法進行資料快取,而是利用查詢事件來定製自己的資料快取策略。

// after_insert回撥方法Db::event('after_insert', function ($options, $query) {    $pk   = $query->getPk($options);    $guid = $options['table'] . '_' . $options['data'][$pk];    Cache::set($guid, $options['data'], 0);});// after_update回撥方法Db::event('after_update', function ($options, $query) {    $pk   = $query->getPk($options);    $guid = $options['table'] . '_' . $options['data'][$pk];    $data = Cache::get($guid);    $data = array_merge($data, $options['data']);    Cache::set($guid, $data, 0);});// after_delete回撥方法Db::event('after_delete', function ($options, $query) {    $pk   = $query->getPk($options);    $guid = $options['table'] . '_' . $options['data'][$pk];    Cache::set($guid, null, 0);});// before_find回撥方法Db::event('before_find', function ($options, $query) {    $pk   = $query->getPk($options);    $guid = $options['table'] . '_' . $options['data'][$pk];    $data = Cache::get($guid);    if ($data) {        return $data;    }});

註冊完查詢回撥方法後,下面的查詢除了寫操作會執行資料庫操作,其它的查詢方法都直接讀取快取資料,而且始終保持最新的資料。

$id = Db::table('user')    ->insert(['name'=>'thinkphp']);Db::table('user')->find($id);Db::table('user')    ->where('id',$id)    ->update(['name'=>'topthink']);Db::table('user')->find($id);Db::table('user')    ->delete($id);Db::table('user')->find($id);  

資料安全

安全和優化就如同魚和熊掌一般,很難兼得。從某種程度上說,資料安全比效能優化更重要,因此為了更加安全和穩健執行,犧牲一定的效能都是值得的,下面我們來學習下基本的安全策略。

底層防護

5.0版本提供了更高的底層安全策略,雖然不至於因此而高枕無憂,但也完全不必杞人憂天,主要體現在:

  • WEB訪問目錄和應用目錄隔離;
  • 內建使用PDO預處理和自動引數繫結機制;
  • 預設使用者提交資料不支援陣列;
  • 支援資料自動過濾機制;

只要善於運用系統提供的安全手段和做好一些配置,可確保你的應用安全無虞,聽我給你細細道來。

寫入過濾

由於系統的安全機制,任何非資料表的欄位如果要寫入資料庫都會導致異常,如果你不希望非資料表字段寫入資料庫的時候丟擲異常,而只是忽略就行,那麼可以使用下面兩種方式。

如果是僅僅當前操作忽略,則可以使用strict方法,例如:

Db::table('user')    ->strict(false)    ->insert([        'name'     => 'thinkphp',        'nickname' => '流年',        'test'     => '測試資料',    ]);

由於user表中並不存在test欄位,因此test資料會被直接忽略,但由於使用了strict(false)方法,而不會丟擲異常。

如果希望全域性不丟擲異常,可以在資料庫配置檔案中設定

        // 是否嚴格檢查欄位是否存在        'fields_strict'   => false,

但有些時候我們還需要限制寫入資料庫的欄位,避免被使用者提交更新一些敏感資料,並非只有查詢的時候可以使用field方法指定欄位列表,我們還可以在寫入資料的時候使用field方法限制欄位寫入。

Db::table('user')    ->field('name,nickname')    ->where('id', 1)    ->update([        'name'     => 'thinkphp',        'nickname' => '流年',        'email'    => '[email protected]',    ]);

上面的例子中,由於我們用field方法限制了寫入的欄位列表,因此email資料不會被更新,而是直接忽略。

同樣,field方法也支援排除某些欄位

Db::table('user')    ->field('email,score', true)    ->where('id', 1)    ->update([        'name'     => 'thinkphp',        'nickname' => '流年',        'email'    => '[email protected]',    ]);

如果使用模型操作的話,我們還可以使用allowField方法提前對資料進行欄位過濾

$user           = User::get(1);$user->name     = 'thinkphp';$user->nickname = '流年';$user->email    = '[email protected]';$user->allowField('name,nickname')    ->save();

allowField過濾資料並不會導致異常,和field方法不同,allowField方法並不支援欄位排除,如果呼叫allowField(true) 表示過濾資料表字段之外的資料

模型還額外提供了一個只讀欄位的功能,針對某些欄位只提供寫入功能而不提供更新功能,具體可以參考模型高階用法一章的內容。

安全建議

為了讓你的應用更安全,綜合之前提到的各種安全因素,在資料庫的層面我們給出如下安全建議:

  • 對使用者輸入的資料做盡可能的驗證;
  • 對寫入的資料做好過濾,避免異常;
  • 避免直接使用使用者提交資料作為查詢條件;
  • 查詢欄位名不應該由表單或者使用者決定;
  • 對於get和find方法的引數建議做好Null判斷;
  • 資料輸出的時候注意做好XSS安全過濾;
  • 對於模型資料儘量隱藏敏感資料後輸出;
  • 對於業務資料的寫入操作應當做好許可權檢查;
  • 寫入資料嚴格使用field方法限制寫入欄位;

舉個例子,如果你開放查詢欄位名給使用者提交而未作判斷直接作為查詢條件,例如下面的程式碼:

$where = request()->param();// 查詢使用者是否存在$user = Db::table('user')    ->where($where)    ->find();

假設你的表單裡面有一個name欄位,那麼,使用者就可以在瀏覽器構造一個name|email欄位完成OR查詢,查詢的結果可能完全不同了,極有可能造成邏輯漏洞。

正確的查詢方式應該是:

// 查詢使用者是否存在$user = Db::table('user')    ->where('name',request()->param('name'))    ->find();


作者:寒冬夜行人_51a4
連結:https://www.jianshu.com/p/04853e463c81
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。