---
# System prepended metadata

title: 用一個領取折價券來舉例時常出現的錯誤

---

---
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，不過因為我們這次的需求是可以領取五張，五張以上才不能領，所以這方法在這需求下無法使用