--- title: 用一個領取折價券來舉例時常出現的錯誤 date: 2020-01-14 13:54:40 tags: --- 在我們寫專案的過程中,時常碰到需要先判斷是否已經有這筆資料了,沒有的話就寫入,有的話就彈出錯誤。下面我們來用領取折價券來模擬這個狀況。 我們先構建出一個基本的領取折價券的資料表結構 | 名稱 | 類型 | 註解 | | -------- | -------- | -------- | | id | int | PK | | user_id | int | 用戶ID | | amount_id | int | 價格ID | | serial_number | char | 折價券序號 | CREATE TABLE `test`.`test` ( `id` int(10) NOT NULL AUTO_INCREMENT, `user_id` int(10) NOT NULL, `amount_id` int(10) NOT NULL, `serial_number` char(32) NOT NULL, `created_at` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), `updated_at` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (`id`) ); 這時候有一個需求,一個用戶只能領取五次折價券,這時候我們一般思考步驟是 1. 先判斷這個用戶有沒有領取超過五次 2. 沒有則寫入並返回成功,有則不寫入並返回失敗。 之後我們按照這邏輯開始寫程式碼。(這邊使用PHP來舉例) ``` <?php $DBNAME = "test"; $DBUSER = "root"; $DBPASSWD = "root"; $DBHOST = "localhost"; $userId = 1; $conn = mysqli_connect($DBHOST, $DBUSER, $DBPASSWD); if (empty($conn)) { print mysqli_error($conn); die ("無法連結資料庫"); } if (!mysqli_select_db($conn, $DBNAME)) { die ("無法選擇資料庫"); } // 設定連線編碼 mysqli_query($conn, "SET NAMES 'utf8'"); $sql = "select count(*) from `test` where user_id=" . $userId; $count = mysqli_query($conn, $sql); $count = mysqli_fetch_array($count); if ($count[0] >= 5) { die ("折價券領取的次數超過五次"); } $sql = "INSERT INTO `test`.`test`(`user_id`, `amount_id`, `serial_number`) VALUES ($userId, 1, '11111111111111111111111111111111');"; $result = mysqli_query($conn, $sql); if ($result) { echo '領取成功'; exit; } echo '領取失敗'; exit; ``` 到這邊,邏輯就寫完了,我們寫一個ajax來試著敲敲看,看看會發生什麼事情。 ![](https://i.imgur.com/Coz4C48.png) ![](https://i.imgur.com/0dhA9ww.png) 資料庫內顯示有六筆,為什麼會這樣? 因為我們先判斷後寫入的這個流程所導致,當請求幾乎是併發過來的時候,會先通過判斷是否超過五個,這時候我們資料尚未寫入,所以這邊會是成功的,但是就是因為成功,所以可能導致五個之後的請求也會通過這個判斷。 那我們怎麼做可以避免這個情況呢? 1. **可以試著修改資料庫語句** ``` INSERT INTO test ( user_id, amount_id, serial_number ) SELECT '1', '112', '1123123' FROM (SELECT count(user_id) as count FROM test WHERE user_id = 1) as temp where temp.count < 5 ``` 把兩句判斷合併再一起就可以避免這個情況 2. 使用UNIQUE,不過因為我們這次的需求是可以領取五張,五張以上才不能領,所以這方法在這需求下無法使用