最近在做聚划算商品的更新接口,商品有一個字段存儲了商品的一些擴展屬性,以鍵值對數組的形式存放,格式如下:
key1:value1;key2:value2;
在根據商品id對商品屬性進行更新的時候,業務上需要把客戶端傳入的新的鍵值對數組和數據庫中已經有的鍵值對數組進行合並,偽代碼如下:
select old_attributes from table where primary_key = ? ---step1
attributes = merge(old_attributes,new_attributes) ----step2
update table set attributes_column = attributes where primary_key = ? ----step3
但是這樣的話,存在一個丟失更新的問題,兩個線程ThreadA 和 ThreadB 同時運行到了step1得到相同的old_attributes,
然后同時做step2,最后ThreadA先做step3,而ThreadB后做step3,這樣ThreadB就把ThreadA的屬性更新給丟失了!
目前因為接口的調用量比較小,還沒有暴露出這個問題。
解決辦法:
思路1:把對屬性的更新變成串行操作,每個線程把自己要更新的attributes寫入一個隊列,由單線程從隊列中讀出屬性,然后順序更新到數據庫記錄。
缺點是把數據庫更新操作進行了人為的分解,提高了代碼的復雜度; 另外,屬性的更新操作和其他更新操作被分離開來,沒有保證事務。並且這種異步
更新的方式對一些實時性要求很高的場景(數據庫更新后立即要讀出的場景)不適用。
思路2: 給存在這種丟失更新的記錄增加版本號,在對一行進行更新的時候 限制條件=主鍵+版本號,同時對記錄的版本號進行更新。
偽代碼如下:
start transaction;
select attributes, old_version from table where primary_key = ?
屬性合並
update table set version = old_verison + 1 , attributes_column = attributes_value where primary_key = ? and version = old_version
commit;
事務提交以后,看最后一步更新操作的記錄更新數是否為1,如果不是,則在業務上提示重試。(表明此時更新操作的並發度較高。)
目前,我們使用的Mysql 5的數據庫隔離級別是repeatable read ,所謂可重復讀,指的是事務A和事務B同時對一行進行更新,但是事務A的更新操作
在commit之前是不會反映到事務B中的,這滿足了ACID特性中的Isolation(隔離性) 。
下面是一個具體的實驗: (環境:1、操作系統:Mac Mountain Lion 2 、數據庫: Mysql 5.5.29-log Source distribution)
數據庫事務的隔離級別采用repeatable-read
事務A 事務B
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql > start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select count ,version from user where id=1;
+-------+---------+
| count | version |
+-------+---------+
| 9 | 6 |
+-------+---------+
1 row in set (0.00 sec)
mysql> select count ,version from user where id=1;
+-------+---------+
| count | version |
+-------+---------+
| 9 | 6 |
+-------+---------+
1 row in set (0.00 sec)
mysql> update user set count=10 ,version=7 where id =1 and version=6;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
(此時id=1這一行被加了間隙鎖(即next-key lock) 后面會有說明)
mysql> update user set count=10 ,version=7 where id =1 and version=6;
(此更新操作會被阻塞)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> update user set count=10 ,version=7 where id =1 and version=6;
Query OK, 0 rows affected (3.28 sec)
Rows matched: 0 Changed: 0 Warnings: 0
(注意:此時,生效的行數是0,因為版本號已經更新成7了,但是接下來我們再次做
select 操作)
mysql> select * from user where id=1;
+----+------+-------+---------+
| id | name | count | version |
+----+------+-------+---------+
| 1 | NULL | 9 | 6 |
+----+------+-------+---------+
1 row in set (0.00 sec)
(仍然是事務A更新之前的值,但是前面的更新操作作用記錄數是0,
這就是所謂的可重復讀,但是更新操作的時候,是以db里面已經生效的版本 為依據的)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where id=1;
+----+------+-------+---------+
| id | name | count | version |
+----+------+-------+---------+
| 1 | NULL | 10 | 7 |
+----+------+-------+---------+
1 row in set (0.00 sec)
(提交事務B,再次查詢,已經得到了事務A的提交) 。
而之前提到的所謂間隙鎖(next-key lock),指的是一條更新操作,會鎖住它控制的一個記錄的范圍,
比如:
事務A: 事務B:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update user set version=6 where id < 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> insert into user(id) values(2);
(此事務會被阻塞)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into user(id) values(2);
Query OK, 1 row affected (4.15 sec)
我們可以看到所謂間隙鎖,鎖的范圍是 閉區間,比如上面的例子 ,where條件是 id<2, 但是id=2的記錄也是不能insert的。
我現在正在做的事情是把版本控制功能模塊化,思路是:
1、通過配置文件告知版本控制模塊哪個表的記錄要做版本控制。
2、改寫數據層,在需要進行版本控制的表的行的更新操作之前,注入一個行版本號的檢查,如果DB中的版本比當前版本大,則此次更新操作失敗 。
代碼正在開發中。