辛比記 https://tec.xenby.com 實作與技術探討 Sun, 07 Aug 2022 05:32:37 +0000 zh-TW hourly 1 https://wordpress.org/?v=6.0.9 https://tec.xenby.com/wp-content/uploads/2020/07/icon_src2.png 辛比記 https://tec.xenby.com 32 32 180727598 Typescript Decorators 介紹與使用情境範例 https://tec.xenby.com/47-typescript-decorators-%e4%bb%8b%e7%b4%b9%e8%88%87%e4%bd%bf%e7%94%a8%e6%83%85%e5%a2%83%e7%af%84%e4%be%8b https://tec.xenby.com/47-typescript-decorators-%e4%bb%8b%e7%b4%b9%e8%88%87%e4%bd%bf%e7%94%a8%e6%83%85%e5%a2%83%e7%af%84%e4%be%8b#respond Sun, 24 Oct 2021 04:24:38 +0000 https://tec.xenby.com/?p=47 有寫過 Java Spring Framework 應該對於... Read more »

Typescript Decorators 介紹與使用情境範例〉這篇文章最早發佈於《辛比記》。]]>

有寫過 Java Spring Framework 應該對於 @Annotation 用法並不陌生, 其常常用來實現 Decorator Pattern, 只要在函數或屬性上標記就能把功能附上或注入值, 巧妙的讓程式碼變的更加簡潔, 之後有些套件也是使用 Annotation 方式處理 (例如:Lombok), 是一個非常好用的語法特性, 而這種語法在近幾年也開始有其他程式語言開始支援, 例如:Python, Dart…等, 這語法特性在 JS 也有提出 proposal, 而在 Typescript 上已經優先支援此語法並且使用在 Angular, Vue3, NestJS…等一些框架上, 本篇將介紹一些 Decorator 的實際情境實作範例。

語法特性文件

Javascript Decorator Proposal: https://github.com/tc39/proposal-decorators

英文:https://www.typescriptlang.org/docs/handbook/decorators.html

中文:https://typescript.bootcss.com/decorators.html

示範使用情境

使用 Decorator 注入 property 資料範例

往往我們都會在 .env 或是環境變數設定一些 key 或是設定, 下面的範例是使用 Decorator 注入的方式替換掉取 jwtKey 變數時的值

// 定義一個回傳 Property 的函數
function config (name: string) {
  // 相關設定可能來自 .env 或是系統環境變數, 這邊使用一個 MAP 示範
  const map = new Map()
  map.set('jwt-key', 't37XJOl8Ayxd2y3JphVLBOCWbYqxWcTk')

  return (target: any, key: string) => {
    Object.defineProperty(target, key, {
      configurable: false,
      get: function (this: { [name: string]: any}) {
        return map.get(name)
      },
      set: function (value) {
        // do nothing. 不可更改
      }
    })
  }
}

class Encrypter {
    @config('jwt-key')
    jwtKey = '';
}

const encrypter = new Encrypter();
console.log(encrypter.jwtKey); // 輸出 `t37XJOl8Ayxd2y3JphVLBOCWbYqxWcTk`

使用 Decorator 替換函數實作範例

下面的範例是抄至 Spring Data JPA 的使用情境, 使用 Decorator 中已定義的查詢功能進行查詢替換掉函數實際功能, 只需要在函數上定義 SQL 就可以查詢, 節省部分重複的程式碼

function query (sql: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.value = async function (...args: any[]): Promise<any[]> {
      for (const i in args) {
        sql = sql.replaceAll('$' + (Number(i) + 1), args[i])
      }

      // 執行查詢...
      const rows = [] as any[]
      let conn
      try {
        const conn = createDBConn()
        const [rows] = await conn.execute(sql)
      } finally {
        if (conn !== undefined) {
          conn.end()
        }
      }

      return rows
    }
  }
}

class StudentRepository {
  @query('SELECT * FROM Student WHERE Name = "$1"')
  findByName (name: string): any { /* do nothing. 由 Decorator 實作 */ }

  @query('SELECT * FROM Student WHERE ID = $1')
  findByID (id: number): any { /* do nothing. 由 Decorator 實作 */ }
}

使用 Decorator 實現 AOP 方式的 Cache method data

有些時候為了節省效能會使用到 Cache, 而往往讀取與寫入 Cache 的程式碼片段會遍佈在不同程式中, 這時可以針對某些函數成功的回傳值進行 Cache

function cacheData () {
  let cacheDatas = {} as { [key: string]: any; };

  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalFunc = descriptor.value;
    descriptor.value = function (...args: any[]): any {
        // 建立 cache 所用的 key, 依據自己情況改變 key 組成方式
        let key = originalFunc.toString() + '-' + JSON.stringify(args);
        if (cacheDatas.hasOwnProperty(key)) {
            return cacheDatas[key]
        }

        let returnVal = originalFunc(...args);
        if (returnVal instanceof Promise) {
          // 如果是 promise 成功才會 cache
          returnVal.then((value) => {
            cacheDatas[key] = Promise.resolve(value);
            return value;
          })
        } else {
          cacheDatas[key] = returnVal;
        }

        return returnVal;
    }
  }
}

class MyClass {
  @cacheData()
  async foo (name: string, id: number): Promise<number> {
    return (new Date()).getTime();
  }
}

async function main() {
  let obj = new MyClass();
  console.log('Bob', await obj.foo("Bob", 1));
  console.log('Alice', await obj.foo("Alice", 2));
  console.log('Jake', await obj.foo("Jake", 3));
  console.log('Bob', await obj.foo("Bob", 1)); // 這邊會是使用 cache 的結果
  console.log('Bob', await obj.foo("Bob", 3));
}
main();

指定函數忽略 Exception (Error) 並回傳預設值 (AOP 應用)

有時使用第三方或自己寫功能時, 會遇到當其失敗會丟出 Exception 情境, 而如果想要忽略掉此錯誤, 會需要寫一個 try catch 來包住該函數功能, 但是這樣在程式碼可能會變複雜, 且可能會有許多 try catch 產生, 此時可以用 Decorator 方式忽略掉該函數中可能丟出的錯誤, 以替代的值返回

function ignoreError (defaultVal: any) {
  let resolvePrmise = async (promise: Promise<any>, defaultValue: any) => {
    try {
      return await promise
    } catch (e) {
      // do nothing
    }
    return defaultValue
  }

  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalFunc = descriptor.value;
    descriptor.value = function (...args: any[]): any {
        try {
          let returnVal = originalFunc(...args);
          if (!(returnVal instanceof Promise)) {
            return returnVal
          }

          return resolvePrmise(returnVal, defaultVal)
        } catch (e) {
          // do nothing
        }

        return defaultVal;
    }
  }
}

class MyClass {
  @ignoreError('default value')
  async foo (name: string): Promise<number> {
    // do something
  }

  @ignoreError('default value2')
  async foo2 (name: string): Promise<number> {
    // do something
  }
}

async function main() {
  let obj = new MyClass();
  console.log('Bob', await obj.foo("Bob"));
  console.log('Alice', await obj.foo2("Alice"));
}

這幾個一些小小的情境應用, 還能作非常多的用途 (例如:對函數賦予 Lock 功能, 資料庫連線管理… 等), 有許多的框架用來作了不少好用的功能, 相信學習此語法特性會對於開發會有很多幫助

Typescript Decorators 介紹與使用情境範例〉這篇文章最早發佈於《辛比記》。]]>
https://tec.xenby.com/47-typescript-decorators-%e4%bb%8b%e7%b4%b9%e8%88%87%e4%bd%bf%e7%94%a8%e6%83%85%e5%a2%83%e7%af%84%e4%be%8b/feed 0 47
結合 JWT 與 Refresh Token 達到黑名單失效機制 https://tec.xenby.com/44-%e7%b5%90%e5%90%88-jwt-%e8%88%87-refresh-token-%e9%81%94%e5%88%b0%e9%bb%91%e5%90%8d%e5%96%ae%e5%a4%b1%e6%95%88%e6%a9%9f%e5%88%b6 https://tec.xenby.com/44-%e7%b5%90%e5%90%88-jwt-%e8%88%87-refresh-token-%e9%81%94%e5%88%b0%e9%bb%91%e5%90%8d%e5%96%ae%e5%a4%b1%e6%95%88%e6%a9%9f%e5%88%b6#respond Mon, 15 Feb 2021 09:59:43 +0000 https://tec.xenby.com/?p=44 JWT 是現在常常使用的使用者驗證之一,其 token 特點... Read more »

結合 JWT 與 Refresh Token 達到黑名單失效機制〉這篇文章最早發佈於《辛比記》。]]>

JWT 是現在常常使用的使用者驗證之一,其 token 特點是可以讓 API 達到無狀態性 (Stateless),不需要透過外部儲存服務來紀錄該使用者資料,伺服器只要驗證 token 的簽名是否有被竄改,就能直接讓使用者通行,不過因為 token 本身是無狀態性的,本身並不支援黑名單失效,所以有些會透過 redis 或 database 來做黑名單,但每次都存取 redis 這樣就違背了 JWT 的無狀態性原則,使其變得跟 session 一樣,本篇將介紹如何將 JWT 與 Refresh Token 機制結合來減少詢問 redis 或 database 的次數。

JWT 與 Refresh token 介紹

在介紹機制前,要先了解什麼是 JWT 以及什麼是 Refresh token。

JWT

不知道什麼是 JWT 的話,可以參考此文章:https://yami.io/jwt/

Refresh token

Refresh token 機制最常用於 OAuth 2.0 情境

其有主要兩種 token:

  • refresh token:用來取得 access token 的 (時效較長,例:7 天)
  • access token:用來存取資源的 token (時效較短,例:30 分鐘)

大致流程如下:

當 client 拿到 access token,就可以一直使用 access token 進行資源存取

而如果 access token 使用過期的話,再重新從 step.4 要求新的 access token

JWT 與 Refresh Token 機制結合

下面介紹一下,如何結合兩個機制來減少存取 redis 的次數

觀察上面 Refresh token 的流程可以注意到大部分跟伺服器請求的時候都是使用 access token 進行資源存取,只有很少的機會會使用 refresh token,兩種 token 使用的比例上可能會是 100:1 或是更高。

由此觀察結果調整,可以在 step.8 的時候,伺服器取得 JWT 時只驗證是否有被竄改以及過期,不檢查是否已被加入黑名單失效,而當 access token 過期了,就會需要透過 refresh token 來要求新的 refresh token (step.4),伺服器只在這個時機檢查該 refresh token 是否已經被加入黑名單中了,如果已經被加入黑名單便可以不發送 access token,回應無存取權讓使用者重新走登入驗證流程。

並且在結合之後,可以取得 refresh token 的好處,當伺服器程式升級要使用新版本的 JWT Payload 時,可以直接把不相容的 access token 拒絕掉,要求客戶端使用 refresh token 重新要一個新格式的 access token。

總結

由上面介紹歸類出以下優缺點,可以依照自己的情境決定是否使用:

優點:

  • 減少存取外部儲存服務驗證是否在黑名單中的次數
  • access token 可以達到 Stateless
  • 得到 refresh token 的好處,可以更快發送新格式 JWT token

缺點:

  • 流程較一般的 JWT 複雜
  • 不能夠馬上失效,對於安全要求極高的無法使用
結合 JWT 與 Refresh Token 達到黑名單失效機制〉這篇文章最早發佈於《辛比記》。]]>
https://tec.xenby.com/44-%e7%b5%90%e5%90%88-jwt-%e8%88%87-refresh-token-%e9%81%94%e5%88%b0%e9%bb%91%e5%90%8d%e5%96%ae%e5%a4%b1%e6%95%88%e6%a9%9f%e5%88%b6/feed 0 44
龐大資料庫分頁方案 Cursor-based pagination https://tec.xenby.com/36-%e9%be%90%e5%a4%a7%e8%b3%87%e6%96%99%e5%ba%ab%e5%88%86%e9%a0%81%e6%96%b9%e6%a1%88-cursor-based-pagination https://tec.xenby.com/36-%e9%be%90%e5%a4%a7%e8%b3%87%e6%96%99%e5%ba%ab%e5%88%86%e9%a0%81%e6%96%b9%e6%a1%88-cursor-based-pagination#respond Tue, 08 Sep 2020 07:39:17 +0000 https://tec.xenby.com/?p=36 往往做網頁的或是 APP 的頁面都會做成一頁不顯示太多資料,... Read more »

龐大資料庫分頁方案 Cursor-based pagination〉這篇文章最早發佈於《辛比記》。]]>

往往做網頁的或是 APP 的頁面都會做成一頁不顯示太多資料,這時 API 有分頁的需求,傳統都會使用 offset limit 來處理,但是這種方式並不適合用於資料量大的資料庫,本篇文章將探討傳統方法的問題以及如何使用 Cursor-based pagination 方式取代。

資料庫 Schema

※ 本篇以 MySQL 5.7 版本做說明

假設現在有個書本管理系統,系統中紀錄了書本的資料,資料包含書名、統一商品編碼以及價格,並且在統一商品編碼以及價格上做了索引。

CREATE TABLE `book` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(200) NOT NULL COMMENT '書名',
  `isbn` VARCHAR(15) NOT NULL COMMENT '統一商品編碼',
  `price` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '價格',
  PRIMARY KEY (`id`),
  UNIQUE INDEX `__index__isbn` (`isbn` ASC),
  INDEX `__index__price` (`price` ASC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

大型資料庫的致命傷

在龐大資料量的資料庫查詢時,有一些查詢方式應該避免,使用者不多的時候使用可能沒有太大影響,但是如果同時許多人使用時,會使的資料庫頻繁處於高負載狀態,如果最後資料庫無法負荷,資料庫可能會倒掉,導致服務無法運作,這邊介紹兩個最常遇到的功能,但是對於大型資料庫應該避免使用的。

致命傷 一、offset limit

最常使用的傳統分頁方案是使用 SQL 查詢使用 offset 與 limit 搭配使用來解決需求。

例如一個分頁有 10 筆資料,而如果要查詢價格小於 100 的並且在第七頁的時候會使用下面的 SQL 查詢:

SELECT * FROM book WHERE price > 100 LIMIT 10 OFFSET 60;

而對於資料庫有稍微深入了解的話,就會知道 offset 在運作的時候需要從頭開始尋訪 (Scan table) 才能知道要取哪一段資料,所以當分頁越後面的時候查詢時間就會越久,在小型資料庫並不會有特別的感覺,但是當資料量非常龐大 (到達幾千萬或是上億時) 時,就能感受到時間明顯的差距。

例如下面有人直接跳到 300001 頁的時候,所花費的時間就明顯上升了不少:

致命傷 二、總頁數

另外一個對於龐大資料量的資料庫傷害蠻大的是總頁數,要知道符合條件總數量,資料庫系統必須尋訪過每個符合條件的節點才能夠算出總數量。

如果再搭配上面 offset limit,往往在顯示一個頁面的時候,會使用兩個非常消耗資料庫效能的 SQL,當使用者一多的時候資料庫可能會處理不來導致網站倒掉。

這時就會需要想一下資料量龐大時總頁數跟總數量真的是必要的嗎?如果是必要的話資料是否一定要很精準,並且可否使用其他代替資料庫?

替代 offset limit 的方案 – Cursor-based pagination

對於大型資料庫做分頁的其中一個替代方案便是 Cursor-based pagination,其能夠有效使用資料庫的 index 進行尋訪,不會對於資料庫造成傷害。

其運作方式是將有 index 的欄位的值記錄下來,丟給 Client,而 Client 如果如果要查詢下一頁時欄位丟給 API,API 再以後續欄位進行查詢。

直接以 SQL 進行說明,假如我們要查詢第一頁大於 400 塊的書會輸入以下的 SQL:

SELECT * FROM book WHERE price > 400 limit 10 order by id;

這時可看到最後一個 id 是 29,只需要將 29 的值丟給 Client,而 Client 要查詢下一頁的資料只需要將這個 29 丟給 API 就再以 30 開始進行查詢

SELECT * FROM book WHERE price > 400 and id > 29 order by id limit 10;

即使使用者一直按下一頁到達了很後面的頁數也不會對資料庫有大的傷害

實際實作

Page Token 介紹

在實作時可以在丟給前端的資料動手腳,不是把單一 id 丟給 Client,而是另外使用一種實現方式:pageToken。

pageToken 是用來跟 API 請求資料使用的 token,裡面會存放一些後端才會用到的資料,對於前端來說就只是一個單純的字串而已,而對後端的好處是:封裝性,一般會是將資料進行加密來達成,對於前端單純就是一串字串,而如果哪一天可能因為後端架構改變,而需要改變 token 內容時對前端並不會有任何影響,這邊使用 pageToken 內存放資料查詢的分界點。

單純查詢實例

我們以前面第一頁的 pageToken 來做示範,首先我們以下面的 SQL 查到第一頁的的資料以及最大的 id 是 29

SELECT * FROM book WHERE price > 400 limit 10 order by id;

這時候 API 丟給前端的 pageToken 中就可以包含 id 29 這個資料 (這邊 token 內容以 json 格式當作範例,為了方便解說所以使用 base64 方式做加密,實際實作可以用其他演算法如 DES、AES 這種需要 key 才能解密的演算法才更安全)

token 內容是 {"id": 29},經過 base64 過後資料為 eyJpZCI6IDI5fQ==,這個字串就是查詢下一頁的 pageToken

API 便能回應以下的資料:

{
    "data": [...],
    "previousPageToken": null,
    "nextPageToken": "eyJpZCI6IDI5fQ=="
}

而當客戶端丟出 /api/books?nextPageToken=eyJpZCI6IDI5fQ==,API 解開後得到 id 是 29,便可知道要查詢大於 29 的資料

SELECT * FROM book WHERE price > 400 and id > 29 order by id limit 10;

這時產生下一頁的 pageToken 資料 {"id": 51},過 base64 後為 eyJpZCI6IDUxfQ== 為,而查詢的 pageToken 就能夠當作 previousPageToken

API 就能夠回應:

{
    "data": [...],
    "previousPageToken": "eyJpZCI6IDI5fQ==",
    "nextPageToken": "eyJpZCI6IDUxfQ=="
}

含有排序查詢實例

前面的查詢並不含排序,這邊介紹如果有排序的話可以如何實作,假設現在需求是需要價格從低到高進行排序。

剛剛的範例中我們,只有一個欄位 id,因為值是絕對唯一的,很適合做分界面,可是假如只使用 price 做排序時,分界點就比較難以區分因為 price 很有機率重複,所以這邊有個技巧就是把 id 加入組成絕對不會的組合來做分界點。

其中因為是由價格低到高排序,所以 price 當做高位數 (排序優先權高),id 當做低位數,這樣就能組成絕對不會重複的分界點,並且達成類似於下圖的排列方式。

這邊以 price 這欄位進行排序做示範,查詢第一頁的 SQL 如下:

SELECT * FROM book WHERE price > 400 order by price, id limit 10;

取得的 id 最大 629 ,price 最大是 410, 將這兩個值組成 {"id": 629, "price": 410},經過 base64 加密後取得 pageToken 便是 eyJpZCI6IDYyOSwgInByaWNlIjogNDEwfQ==

API 就能夠回應:

{
    "data": [...],
    "previousPageToken": null,
    "nextPageToken": "eyJpZCI6IDYyOSwgInByaWNlIjogNDEwfQ=="
}

而 client 在查詢下一頁時,/api/books?nextPageToken=eyJpZCI6IDYyOSwgInByaWNlIjogNDEwfQ==,便能取得分界點是 {"id": 629, "price": 410},使用下面的 SQL 查詢下一頁:

SELECT * FROM book WHERE (price = 410 AND id > 629) OR (price > 410) order by price, id limit 10;

而 previousPageToken 部分運作方式則與前面方式相同

龐大資料庫分頁方案 Cursor-based pagination〉這篇文章最早發佈於《辛比記》。]]>
https://tec.xenby.com/36-%e9%be%90%e5%a4%a7%e8%b3%87%e6%96%99%e5%ba%ab%e5%88%86%e9%a0%81%e6%96%b9%e6%a1%88-cursor-based-pagination/feed 0 36
Github 多帳號使用不同 ssh key 設定指南 https://tec.xenby.com/42-github-%e5%a4%9a%e5%b8%b3%e8%99%9f%e4%bd%bf%e7%94%a8%e4%b8%8d%e5%90%8c-ssh-key-%e8%a8%ad%e5%ae%9a%e6%8c%87%e5%8d%97 https://tec.xenby.com/42-github-%e5%a4%9a%e5%b8%b3%e8%99%9f%e4%bd%bf%e7%94%a8%e4%b8%8d%e5%90%8c-ssh-key-%e8%a8%ad%e5%ae%9a%e6%8c%87%e5%8d%97#respond Mon, 10 Aug 2020 14:09:43 +0000 https://tec.xenby.com/?p=42 在 Github 上不同的帳號之間是不能共用公鑰的,所以假如... Read more »

Github 多帳號使用不同 ssh key 設定指南〉這篇文章最早發佈於《辛比記》。]]>

在 Github 上不同的帳號之間是不能共用公鑰的,所以假如在一台電腦中同時要給兩個 Github 帳號使用的話,因為是同個 domain 所以需要特別調整設定,本篇將教學如何調整。

1. 首先要先設定 ~/.ssh/config

假設在 Github 上有兩個帳號分別為 userA 與 userB,而對應的 key 如下:

  • userA 的 key 是在 ~/.ssh/id_rsa_github_userA
  • userB 的 key 是在 ~/.ssh/id_rsa_github_userB

這時要在 ssh config (位置:~/.ssh/config) 中設定如下:

Host github.com-userA
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_rsa_github_userA

Host github.com-userB
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_rsa_github_userB

2. 對想要使用的 project 進行 clone

假設現在想要 clone 的 repo url 是 git@github.com:demo/project-a.git

這時只需要將 github.com 替換成 github.com-userA 或是 github.com-userB 就能夠使用不同的 key 跟身分進行連線

例如以下的方式就是使用 userA 的 key 進行 clone:

git git@github.com-userA:demo/project-a.git
Github 多帳號使用不同 ssh key 設定指南〉這篇文章最早發佈於《辛比記》。]]>
https://tec.xenby.com/42-github-%e5%a4%9a%e5%b8%b3%e8%99%9f%e4%bd%bf%e7%94%a8%e4%b8%8d%e5%90%8c-ssh-key-%e8%a8%ad%e5%ae%9a%e6%8c%87%e5%8d%97/feed 0 42
MySQL 組合型 index 查詢技巧 https://tec.xenby.com/40-mysql-%e7%b5%84%e5%90%88%e5%9e%8b-index-%e6%9f%a5%e8%a9%a2%e6%8a%80%e5%b7%a7 https://tec.xenby.com/40-mysql-%e7%b5%84%e5%90%88%e5%9e%8b-index-%e6%9f%a5%e8%a9%a2%e6%8a%80%e5%b7%a7#respond Tue, 21 Jul 2020 15:13:16 +0000 https://tec.xenby.com/?p=40 前一篇已講解如何設計 index,本篇將介紹如何知道目前使用... Read more »

MySQL 組合型 index 查詢技巧〉這篇文章最早發佈於《辛比記》。]]>

前一篇已講解如何設計 index,本篇將介紹如何知道目前使用的 SQL 用到了哪個 index 以及在有 index 的情況下如何使用有效使用 index。

範例 schema

這邊使用一個範例是電商的商品,資料庫存了商品的名稱、條碼種類、條碼值以及價格,並且有兩個 index 分別用來查詢條碼以及價格。

CREATE TABLE `item` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL COMMENT '商品名稱',
  `code_type` varchar(5) NOT NULL COMMENT '條碼種類',
  `code_value` varchar(20) NOT NULL COMMENT '條碼數值',
  `price` varchar(45) NOT NULL COMMENT '價格',
  PRIMARY KEY (`id`),
  KEY `__code_type__code_value__index` (`code_type`,`code_value`),
  KEY `__price` (`price`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

MySQL 如何知道 Query 的 SQL 使用了哪個 index

假設我們進行複雜 SQL 查詢時,不確定會不會使用 index 的時候,或是想知道是使用哪一組 index 時,可以在輸入的 SQL 前面加上 desc 進行查詢,便會顯示查詢可能會使用的 index

以上這個 SQL 顯示 possible_keys 是 null,表示查詢並沒有使用 index , rows 表示會需要檢索過多少筆才能找到資料。

而上面這個 SQL 則顯示會使用 __code_type__code_value__index 這個 index 來進行查詢,而 rows 為 1 只需檢索一筆資料就能找到結果。

※ 對於資料庫效能考量,rows 這個值是越小越好,表示經過 index 濾出後不需要對太多筆資料進行值的比對。

有效使用 index

原本的情境是假設查詢 code_value 時一定會給對應的 code_type 一起查詢,但是假如今天遇到一個臨時狀況是有人只有提供條碼為 7977761108033,但是 code_type 已經遺失了不確定是什麼,那這種時候要如何處理呢?

直覺可能會直接使用一個 SQL 查詢:

select * from item where code_value = '7977761108033';

這邊在一個資料筆數 500 多萬筆可以看到查詢時間花費了 2.33 秒,是因為沒有使用到 index,可能不會覺得太久,但是如果資料量到達上億的時候,這時間可能就會超過幾分鐘了,且不會因為這個臨時需求增加 index,因為對於資料量達上億筆的 table alter 可能需要執行好幾個小時,所需要的成本太高了。

那這邊要怎樣解決這個問題呢?

我們可以先窮舉出所有 code_type 後,再搭配一起查詢 code_value 這樣就能使用到 index。

select distinct code_type from item;

因為有 __code_type__code_value__index 這個 index 的關係,在進行窮舉的時候速度非常快,接著可以將所有的 type 使用 where in 方式搭配進去查詢

select * from item where code_value = '7977761108033' and code_type in ('Code39', 'EAN', 'SKU', 'UPC');

由這邊可以看出,雖然我們多使用了一個 SQL 來查詢,但是整體時間卻大幅度的提升了。

MySQL 組合型 index 查詢技巧〉這篇文章最早發佈於《辛比記》。]]>
https://tec.xenby.com/40-mysql-%e7%b5%84%e5%90%88%e5%9e%8b-index-%e6%9f%a5%e8%a9%a2%e6%8a%80%e5%b7%a7/feed 0 40
MySQL 基礎索引設計與選擇 https://tec.xenby.com/38-mysql-%e5%9f%ba%e7%a4%8e%e7%b4%a2%e5%bc%95%e8%a8%ad%e8%a8%88%e8%88%87%e9%81%b8%e6%93%87 https://tec.xenby.com/38-mysql-%e5%9f%ba%e7%a4%8e%e7%b4%a2%e5%bc%95%e8%a8%ad%e8%a8%88%e8%88%87%e9%81%b8%e6%93%87#respond Tue, 14 Jul 2020 00:47:02 +0000 https://tec.xenby.com/?p=38 對於關聯式資料庫來說,index (索引) 是一個很重要的東... Read more »

MySQL 基礎索引設計與選擇〉這篇文章最早發佈於《辛比記》。]]>

對於關聯式資料庫來說,index (索引) 是一個很重要的東西,如果欄位有 index 的話在查詢速度會很快,如果沒有設計 index 就會需要 scan table,但是加上 index 並不是無成本的,首先 index 需要有空間進行儲存,多餘的 index 會浪費儲存空間,多個索引甚至可能比 table 還大,另外 MySQL 中是使用 B+Tree 進行索引,在 insert 資料時會需要更新樹的結構,當 index 太多的時候就會需要重整太多的樹會消費比較多的時間,本篇介紹可以如何設計 index。

索引的設計

假設現在有個書店,儲存書本的表格如下:

CREATE TABLE `book` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '書名',
  `isbn` varchar(15) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '統一商品編碼',
  `price` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '價格',
  `language` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '內文語言',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

這邊以這個 table 當作範例

依照使用需求加上索引 – 單一索引

假如現在電商的功能是以價格範圍進行搜尋,查詢的 SQL 如下:

SELECT * FROM book WHERE price >= ? AND price <= ?;

因為 WHERE 條件查詢只有一個欄位,所以這很簡單那索引 (price)

ALTER TABLE `book` 
ADD INDEX `__index__price` (`price`);

依照使用需求加上索引 – 組合索引

而假如現在有情境變成搜尋價格指定語言的書本

SELECT * FROM book WHERE price >= ? AND price <= ? AND language = ?;

這時條件式有兩個欄位,多了 language 這個欄位,這時不能只加一個 index (language),因為在同一張 table 中, simple query 只為選擇使用一個 index 來查詢

所以要弄一個組合式 index (price, language) 或 (language, price),而有兩種組合哪一種比較好呢? 答案是選擇分類比較少的放前面會比較快,例如這邊語系實際上只有約兩百個值,而 price 是數字值比較多變化,所以這邊要選擇 (language, price)

來比較一下單一索引與組合型索引的速度差異

下圖為 (price) + (language) 兩個索引的查詢速度 (0.89 秒)

下圖為 (price, language) 一個索引的查詢速度 (0.42 秒)

下圖為 (language, price) 一個索引的查詢速度 (0.02 秒)

MySQL 基礎索引設計與選擇〉這篇文章最早發佈於《辛比記》。]]>
https://tec.xenby.com/38-mysql-%e5%9f%ba%e7%a4%8e%e7%b4%a2%e5%bc%95%e8%a8%ad%e8%a8%88%e8%88%87%e9%81%b8%e6%93%87/feed 0 38
[講解] nginx 與 php-fpm 運作介紹與設定 https://tec.xenby.com/20-nginx-%e8%88%87-php-fpm-%e9%81%8b%e4%bd%9c%e4%bb%8b%e7%b4%b9%e8%88%87%e8%a8%ad%e5%ae%9a%e8%ac%9b%e8%a7%a3 https://tec.xenby.com/20-nginx-%e8%88%87-php-fpm-%e9%81%8b%e4%bd%9c%e4%bb%8b%e7%b4%b9%e8%88%87%e8%a8%ad%e5%ae%9a%e8%ac%9b%e8%a7%a3#respond Tue, 26 May 2020 05:00:14 +0000 https://tec.xenby.com/?p=20 nginx + php-fpm 是現在常常用來與代替以前的 ... Read more »

[講解] nginx 與 php-fpm 運作介紹與設定〉這篇文章最早發佈於《辛比記》。]]>

nginx + php-fpm 是現在常常用來與代替以前的 php apache 伺服器,其最大的優點是使用的系統資源較少,並且 nginx 對於靜態檔案的處理速度非常快,本篇將講解 nginx 與 php-fpm 之間是如何運作的。

nginx 是什麼 ?

首先來了解一下 nginx 是什麼,nginx 是一個 web server,其主要功能是反向代理、負載平衡器和 HTTP 快取,nginx 本身就能夠做一個靜態檔案伺服器,現在最常主要負責:

  • 處理靜態檔案
  • 依據規則轉發 request
  • 資料壓縮 (gzip)
  • 傳輸的資料加密 (https)

其沒有一定要用於 php,可以依據情況將 request 丟給其他程式語言處理,或是將 request 丟給不同的 php 版本處理。

php-fpm 是什麼 ?

fpm 是 FastCGI Process Manager 的縮寫,其功能非常的單純,專門接收特定 request 並且運行 php 腳本產生結果。

fpm-fpm 可以啟動多個 child process 來運行 php 腳本,有效的使用多核心 cpu 的效能

而每個 process 並不是 request 執行完就關閉,而是會先閒置,等待處理下一個 request,如果閒置太久,且已經太多 process 都閒置狀態才會關閉,所以不會有反覆啟動 processs 導致資源浪費。


例如下面得設定

pm.start_servers = 4
pm.max_children = 10
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500

其效果為:

  • 伺服器啟動時就開啟 4 個 process
  • 最多 10 個 children process
  • 最小閒置 process 數量
  • 最大閒置 process 數量
  • 一個 process 處理 500 個 request 後會重啟

nginx 與 php-fpm 運作說明

這邊以常見的 php-fpm 設定來說明

server {
    gzip on;
  
    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php7.2-fpm.sock;
    }
}

這設定有幾項規則:

  • 啟用壓縮 (gzip)
  • url path 是以 .php 結尾的丟給 php-fpm 處理
  • 其他 request 檢查該路徑是否是檔案或資料夾,如果是傳回檔案,否則回應 404 not found

而靜態檔案與 PHP 的 request 實際運作經過是不同的,下面圖片解說

檔案 request 的運作

  1. 客戶端發送 request
  2. nginx 尋找到對應的檔案並讀取
  3. 返回檔案的資料給使用者

PHP request (.php 結尾的 request) 的運作

  1. 客戶端發送 request
  2. 將 request 丟給 php-fpm 來處理
  3. php-fpm 找到對應的 php 腳本檔案並且執行
  4. 講腳本執行的結果丟回給 nginx
  5. nginx 將結果返回給使用者

常見問題

網頁出現 504 Gateway timeout 原因與解決方法?

以上面這張圖解說,nginx 轉發 request 給 php-fpm 後會開始等 output,而 nginx 會設定一個等待回應 timeout 的設定值,假如 php-fpm 超過這個時間都沒有回應 (php執行太久) 那就會顯示 504 Gateway timeout

※ 預設 php-fpm 是沒有設定執行時間上限

解決方法有兩種:

  1. 調高 nginx 的等待時間

調整 nginx.conf 設定 fastcgi_read_timeout:

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php7.2-fpm.sock;
        fastcgi_read_timeout 300;
    }
  1. 降低 php-fpm 的可執行時間上限

調整 php-fpm 的 php.ini 設定 max_execution_time:

max_execution_time = 30
[講解] nginx 與 php-fpm 運作介紹與設定〉這篇文章最早發佈於《辛比記》。]]>
https://tec.xenby.com/20-nginx-%e8%88%87-php-fpm-%e9%81%8b%e4%bd%9c%e4%bb%8b%e7%b4%b9%e8%88%87%e8%a8%ad%e5%ae%9a%e8%ac%9b%e8%a7%a3/feed 0 20
PHP 7.4 安裝 PHP SQL Server 2008 驅動指南 https://tec.xenby.com/50-php-7-4-%e5%ae%89%e8%a3%9d-php-sql-server-2008-%e9%a9%85%e5%8b%95%e6%8c%87%e5%8d%97 https://tec.xenby.com/50-php-7-4-%e5%ae%89%e8%a3%9d-php-sql-server-2008-%e9%a9%85%e5%8b%95%e6%8c%87%e5%8d%97#respond Sat, 04 Apr 2020 00:42:47 +0000 https://tec.xenby.com/?p=50 參考來源 : https://docs.microsoft.... Read more »

PHP 7.4 安裝 PHP SQL Server 2008 驅動指南〉這篇文章最早發佈於《辛比記》。]]>

參考來源 : https://docs.microsoft.com/zh-tw/sql/connect/php/installation-tutorial-linux-mac?view=sql-server-ver15

系統環境:

– Ubuntu 18.04

– SQL SQL Server 2008 Express

– PHP 7.4

安裝 ODBC 套件

sudo su
curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
curl https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/prod.list > /etc/apt/sources.list.d/mssql-release.list
exit
sudo apt-get update
sudo ACCEPT_EULA=Y apt-get install -y msodbcsql17
# optional: for unixODBC development headers
sudo apt-get install -y unixodbc-dev

安裝 PHP DEV 套件

sudo apt install php7.4-dev
sudo apt install php7.4-xml

安裝 php 驅動

版本來源: https://github.com/Microsoft/msphpsql/releases

sudo pecl install sqlsrv-5.7.1preview
sudo pecl install pdo_sqlsrv-5.7.1preview
sudo su
printf "; priority=20\nextension=sqlsrv.so\n" > /etc/php/7.4/mods-available/sqlsrv.ini
printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/7.4/mods-available/pdo_sqlsrv.ini
exit
sudo phpenmod -v 7.4 sqlsrv pdo_sqlsrv

Docker 的 Dockerfile 檔案 (Nginx + PHP-FPM)

FROM --platform=linux/amd64 ubuntu:18.04

## 基本時區設定
ARG DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Taipei
RUN apt-get update

## 安裝 nginx
RUN apt-get install -y lsb-release
RUN apt-get install -y nginx

## 安裝 php
RUN apt-get install -y curl apt-transport-https
RUN apt-get -y install software-properties-common
RUN add-apt-repository ppa:ondrej/php
RUN apt-get update
RUN apt-get install -y php7.4 php7.4-fpm php7.4-dev php7.4-xml php7.4-intl php7.4-dev

## 安裝 ODBC
RUN apt-get install -y gnupg2
RUN apt-get install -y curl apt-transport-https wget
RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
RUN curl https://packages.microsoft.com/config/ubuntu/18.04/prod.list > /etc/apt/sources.list.d/mssql-release.list
RUN apt-get update
RUN ACCEPT_EULA=Y apt-get install -y msodbcsql17

## 安裝 php pdo 驅動
RUN apt-get install -y unixodbc-dev
RUN pecl install sqlsrv-5.7.1preview
RUN pecl install pdo_sqlsrv-5.7.1preview
RUN printf "; priority=20\nextension=sqlsrv.so\n" > /etc/php/7.4/mods-available/sqlsrv.ini
RUN printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/7.4/mods-available/pdo_sqlsrv.ini
RUN phpenmod -v 7.4 sqlsrv pdo_sqlsrv

## 設定 nginx
COPY ./entrypoints/nginx-php-entrypoint.sh /etc/entrypoint.sh

RUN chmod +x /etc/entrypoint.sh

EXPOSE 80

ENTRYPOINT ["/etc/entrypoint.sh"]

entrypoint.sh 內容

#!/bin/bash
service nginx start
/etc/init.d/php7.4-fpm
tail -f /dev/null
PHP 7.4 安裝 PHP SQL Server 2008 驅動指南〉這篇文章最早發佈於《辛比記》。]]>
https://tec.xenby.com/50-php-7-4-%e5%ae%89%e8%a3%9d-php-sql-server-2008-%e9%a9%85%e5%8b%95%e6%8c%87%e5%8d%97/feed 0 50