3月 04, 2020 MySQL
在使用 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 的情況,需要使用第二種方法來處理,在第二篇文章再介紹。
過去文章
2025 (9)
4 (5)
3 (1)
2 (3)
2024 (25)
11 (3)
10 (3)
9 (1)
3 (18)
2022 (6)
10 (1)
6 (2)
5 (1)
3 (1)
1 (1)
2021 (21)
11 (7)
7 (1)
6 (2)
5 (2)
4 (6)
3 (2)
2 (1)
2020 (92)
12 (1)
11 (2)
10 (4)
9 (10)
8 (5)
7 (1)
6 (3)
5 (1)
4 (4)
3 (25)
2 (7)
1 (29)
2019 (57)
12 (25)
11 (7)
9 (25)