# Twiker (twitter_clone) 專案實作學習筆記 ## 關於 ### 簡介 本專案的主要技術組合包括以 [Node.js](https://nodejs.org/en) + [Express.js](https://expressjs.com/) 作為後端伺服器,採用 [PostgreSQL](https://www.postgresql.org/) 為資料庫管理系統,並以 [node-postgres](https://node-postgres.com/) 與 [Sequelize](https://sequelize.org/) 連接 Node.js 與 PostgreSQL。 後端資料快取機制則利用 [Redis](https://redis.io/),以 [node-redis](https://github.com/redis/node-redis) 銜接 Node.js 與 Redis。 身份驗證機制採用 [JSON Web Tokens](https://jwt.io/)、 Node.js [bcrypt](https://github.com/kelektiv/node.bcrypt.js#readme) 套件對密碼進行雜湊、 [dotenv](https://github.com/motdotla/dotenv#readme) 用以作全域參數調整與隱藏敏感資訊。 ### [Github 連結](https://github.com/Libright1558/twitter_clone/tree/master) ## 介紹 ### 1. Database Schema (PostgreSQL) ![postgresql diagram](https://hackmd.io/_uploads/S1mDf6KVA.png) user_table:記載使用者資訊。 * userId:用戶識別號碼。 * firstname 與 lastname:使用者姓名。 * username:使用者帳號名稱。 * email:使用者電子信箱。 * password:使用者密碼。 * profilepic:使用者大頭貼照片位置。 * createdAt:使用者的註冊日期。 * updatedAt:使用者的帳號更新日期。 </br> post_table:使用者的發文紀錄。 * postId:文章識別號碼。 * postby:該文章作者。 * content:文章內容。 * createdAt:發文時間。 </br> like_table:文章的按讚者。 * postId:文章識別號碼。 * username:按讚的使用者。 * createdAt:按讚時間點。 </br> retweet_table:文章的推文者。 * postId:文章識別號碼。 * username:推文的使用者。 * createdAt:推文時間點。 </br> pinned_table:使用者釘選的文章。 * postId:文章識別號碼。 </br> ### 2. Redis key-value pair ![redisDiagram](https://hackmd.io/_uploads/HkhpvVi40.png) Hash (user info):以 Hash Table 暫存使用者資訊。以下鍵值皆以 userId 為 Field。 * firstname 與 lastname:使用者姓名。 * username:使用者帳號名。 * email:使用電子信箱資訊。 * profilepic:使用者大頭貼照片位置。 </br> Hash (post info):以 Hash Table 暫存使用者文章。以下鍵值皆以 userId 為 Field。 * postOwner:發文作者。 * content:文章內容。 * createdAt:發文時間。 * likeNums:按讚數。 * retweetNums:推文數。 * \{ userId \}_selfLike:作者是否給自己的文章按讚。(以 "0" 與 "1" 的形式儲存,前者為否,後者為是。userId 為用戶識別號碼) * \{ userId \}_selfRetweet:作者是否給自己的文章推文。(以 "0" 與 "1" 的形式儲存,前者為否,後者為是。userId 為用戶識別號碼) * firstname 與 lastname:發文者姓名。 * profilepic:發文者大頭貼照片位置。 </br> List (post info):暫存依發文時間排序後的使用者文章識別號碼,以 Redis list 方式暫存。發文時間較晚者位於 list 左方。 </br> Set (Locks):以 set 方式暫存,暫存文章按讚時間與推文時間更新的時間點,likeNums 或 retweetNums 更新時,\{ postId \}_postLikeNums 或 \{ postId \}_postRetweetNums 位於同一個 transaction 內一起執行,並依照 timestamp 來判斷是否更新 likeNums 或 retweetNums,以此確保 likeNums 或 retweetNums 更新時,不會把時間點上較舊的資料覆寫上去。 </br> ### 3. Database 存取過程 #### [postInfo (function)](https://github.com/Libright1558/twitter_clone/blob/3f87e75484b47d4efb576c6c0f8acbe55a0accd3/controller/databaseController/get.js#L40C1-L114C2) 該函式用以取得使用者的發文資訊。首先會建立 [temporary tables](https://github.com/Libright1558/twitter_clone/blob/3f87e75484b47d4efb576c6c0f8acbe55a0accd3/database/queryString/queryPost.js#L1C1-L62C18) 用以暫存文章的資訊,之後再將這些 temporary tables 用 `JOIN` 進行合併,再一併將資料傳到 application server 進行處理。 以下為 temporary tables 範例: ``` const likeNum = `CREATE TEMP TABLE IF NOT EXISTS "lN" ( "likeNum" INT, "postId" UUID ) ON COMMIT DROP` ``` 暫存文章資訊到 temporary tables: ``` const appendLikeNum = `INSERT INTO "lN" ("postId", "likeNum") SELECT post_table."postId", COUNT(like_table.username) FROM post_table LEFT JOIN like_table ON post_table."postId" = like_table."postId" WHERE postby = $1 GROUP BY post_table."postId"` ``` 將 temporary tables 用 `JOIN` 進行合併: ``` const fetchPost = `SELECT "pOwn".postby, "pCon".content, "pT"."createdAt", post_table."postId", "likeNum", "retweetNum", "selfRetweet", "selfLike", firstname, lastname, profilepic FROM post_table LEFT JOIN "lN" ON post_table."postId" = "lN"."postId" LEFT JOIN "rN" ON post_table."postId" = "rN"."postId" LEFT JOIN "sL" ON post_table."postId" = "sL"."postId" LEFT JOIN "sR" ON post_table."postId" = "sR"."postId" LEFT JOIN "pOwn" ON post_table."postId" = "pOwn"."postId" LEFT JOIN "pCon" ON post_table."postId" = "pCon"."postId" LEFT JOIN "pT" ON post_table."postId" = "pT"."postId" LEFT JOIN "pFn" ON post_table."postId" = "pFn"."postId" LEFT JOIN "pLn" ON post_table."postId" = "pLn"."postId" LEFT JOIN "postPropic" ON post_table."postId" = "postPropic"."postId" WHERE post_table.postby = $1` ``` 該函式中,傳入的 fetchList 參數為陣列,內含值為 0 或 1,代表以下資料是否存在於 Redis 快取當中: \[content, createdAt, likeNums, retweetNums, selfLike, selfRetweet, postOwner, firstname, lastname, profilepic\] 舉例,如果 Redis 快取當中有缺漏 likeNums 這一項資料,該陣列對應到的 likeNums 位置,其值為 0,反之為 1。 application server 只會向資料庫拿取在 Redis 快取中有缺漏的資料,以減少資料傳輸量,降低傳輸延遲。 ### 4. Redis 作為快取機制 #### 為何使用 Redis ? 減少資料存取的反應時間:因為 Redis 將資料暫存在記憶體,相較於從硬碟中存取資料,能有效降低反應時間,不需每次在存取資料時都透過 database,因此可減輕 database 的負荷。 #### 存取使用者發文資訊 在 `/redis/get.js` 當中, [`getPosts`](https://github.com/Libright1558/twitter_clone/blob/3f87e75484b47d4efb576c6c0f8acbe55a0accd3/redis/get.js#L13C1-L20C2) 函式會呼叫 `/redis/transaction.js` 中的 [`getPostInfo`](https://github.com/Libright1558/twitter_clone/blob/3f87e75484b47d4efb576c6c0f8acbe55a0accd3/redis/transaction.js#L22C1-L54C1) 函式,將存取不同鍵值的執行用 transaction 包裹,一次性完成所有操作,並且透過 `HMGET` 一次性存取資料,避免重複請求,產生不必要的 round-trip。 [`fetchPostIdArray`](https://github.com/Libright1558/twitter_clone/blob/3f87e75484b47d4efb576c6c0f8acbe55a0accd3/redis/get.js#L22C1-L29C2) 函式用途為取得已經依照發文時間排序好的 PostId 陣列,之後將該陣列作為 `getPosts` 的參數之一,所取得的發文資訊都會是排序好的。 #### 寫入使用者發文資訊 在 `/redis/write.js` 當中,[`postWriteBack`](https://github.com/Libright1558/twitter_clone/blob/6bcec175db2d90c0e48c64d2acc8169af6b3dab1/redis/write.js#L12C1-L18C2) 函式會呼叫 `/redis/transaction.js` 中的 [`writePostInfo`](https://github.com/Libright1558/twitter_clone/blob/6bcec175db2d90c0e48c64d2acc8169af6b3dab1/redis/transaction.js#L104C1-L144C2) 函式。listArray 參數為陣列,用以表示對應的發文資訊是否存在於 Redis 中,如無則寫入。 #### 按讚或推文 在 `/redis/update.js` 中, [`updateLikeNums`](https://github.com/Libright1558/twitter_clone/blob/6bcec175db2d90c0e48c64d2acc8169af6b3dab1/redis/update.js#L13C1-L19C2) 與 [`updateRetweetNums`](https://github.com/Libright1558/twitter_clone/blob/6bcec175db2d90c0e48c64d2acc8169af6b3dab1/redis/update.js#L30C1-L36C2) 分別根據使用者按讚或推文,對 redis 中的按讚數或推文數進行更新。上述兩個函式分別會呼叫 `/redis/transaction.js` 中的 [`renewLikeNums`](https://github.com/Libright1558/twitter_clone/blob/6bcec175db2d90c0e48c64d2acc8169af6b3dab1/redis/transaction.js#L160C1-L180C2) 與 [`renewRetweetNums`](https://github.com/Libright1558/twitter_clone/blob/6bcec175db2d90c0e48c64d2acc8169af6b3dab1/redis/transaction.js#L191C1-L211C2)函式。 renewLikeNums 與 renewRetweetNums 函式中,分別會建立 postId + 'postLikeNums' 與 postId + 'postRetweetNums' ,屬於 redis SET 底下的 key-value pair,value 儲存 LikeNums 與 RetweetNums 更新的時間點 (即更新並寫入 database 的時間點)。 更新 LikeNums 、 RetweetNums 時,會先檢查是否已經有值存在,如果存在,則會先檢查 postId + 'postLikeNums' 、 postId + 'postRetweetNums' 這兩個對應的值(時間戳記),如果已經存在值的時間戳記小於更新時的值所帶的時間戳記,將舊值以新值進行覆寫,如否則不更新,避免舊值將新值覆蓋。 使用 WATCH 觀察對應的值,client 在 transaction 內將值進行變更時,該值是否被另一個 client 變更,如已被另一個 client 變更,則 abort 整個 transaction,此即 optimistic lock,假設在 transaction 間衝突較小,因此等到 commit 之際再檢查是否有衝突發生。