在使用 DB 的時候,有時需要先從 DB 內抽出記錄,然後經過一些運算後,才會寫入到 DB 。但是如果在運算中的時間,有其他的 Thread 來 DB 修改記錄的話,就可能會有互相覆寫的情況出現。
### 實例
想像以下的情況 :
Table : fruits
|id|name|quantity|
|---|---|---|
|1|apple|10|
|2|orange|5|
使用以下的代碼進行運算,目的是把水果的數量減去一定數量,減去後水果數量如果少於 0 ,就要拒絕。代碼如下 :
```js
// generate a random number
var random = Math.round(Math.random() * 8);
// fetch data from db
var rows = await db.query(`select id, name, quantity from fruits where name = 'apple'`);
// get first record data
var {id, quantity} = rows[0];
// check quantity is enough
if( quantity >= random ) {
// new quantity
var newQuantity = quantity - random;
// update db record
await db.query(`update fruits set quantity = ? where id = ?`, [newQuantity, id]);
} else {
// not enough quantity
throw new Error('not enough quantity');
}
```
以上的代碼在單一的 Thread 上運行是沒有問題的,但是如果是在 Multi Thread 的情況下,就可能會有機會出現覆寫舊值的情況。
### 覆寫舊值
當 Thread 1 和 Thread 2 同時啟動這段代碼時,就會有機同時在 DB 內抽取到相同的值 (因為互相還未寫入新值到 DB)。 然後分別寫入各自減去 `Random` 的數值入 DB 內,這樣 DB 內的數量就會不正確。
### 解決方法
要解決就需要進行阻塞 (Blocking),要後執行的 Thread 等待先執行的執行完成,才會開始進入執行階段,像是 Java 內的 Synchronized Method 一樣。
在 MySQL 內我們可以通過使用 `lock table` 方法來達成。
以下這句可以防止其他 Connection 寫入表格 fruits :
```sql
lock tables fruits read;
```
以下這句可以防止其他 Connection 讀取及寫入表格 fruits :
```sql
lock tables fruits write;
```
記住 `lock table` 後一定要 `unlock table` 才行,不然一直 lock 住就會變成為 Dead Lock 了。
```sql
unlock tables;
```
### 改寫代碼
我們把上面的代碼改寫一下 :
```js
// generate a random number
var random = Math.round(Math.random() * 8);
// lock tables
await db.query(`lock tables fruits write`);
// fetch data from db
var rows = await db.query(`select id, name, quantity from fruits where name = 'apple'`);
// get first record data
var {id, quantity} = rows[0];
// check quantity is enough
if( quantity >= random ) {
// new quantity
var newQuantity = quantity - random;
// update db record
await db.query(`update fruits set quantity = ? where id = ?`, [newQuantity, id]);
// unlock tables
await db.query(`unlock tables`);
} else {
// unlock tables
await db.query(`unlock tables`);
// not enough quantity
throw new Error('not enough quantity');
}
```
只需要使用 Lock Table 功能把需要讀出運算及寫入的邏輯包裝起來,就可以防止其他的 DB Connection 在中間讀取未更新的參數。
*** 但是 Lock Table 在 Transaction 中會引發無法 Rollback 的情況,需要使用第二種方法來處理,在第二篇文章再介紹。