中秋前某日,夜黑風高,工程師小A正準備下班,忽然被加急: 中秋要發月餅,作個系統讓員工預訂一下吧。一人只能預訂兩個,杏仁的一共50個,黃桃的有100個。html
這是一家傳統軟件公司,公司CTO有一些技術偏執,堅信MySQL大法好,因此這家公司數據庫只有 MySQL
!mysql
畢竟小A是久經沙場的CRUD工程師,這種小業務對於小A而言就好像單手開法拉利同樣輕鬆。git
id | name | capacity |
---|---|---|
1 | 杏仁月餅 | 50 |
2 | 黃桃月餅 | 100 |
id | user_id | mooncake_id | create_time |
---|---|---|---|
1 | 131 | 1 | 2019/9/10 22:22:00 |
2 | 231 | 1 | 2019/9/10 22:22:00 |
小A猶如雷布斯附體,三下五除二,擼出了預訂的代碼。github
func orderMoon(moonId int64){
userId = passport.GetUserID()
ordered = db.query("select count(1) from people_mooncake_ref where userId = ?",userId)
Assert (ordered < 2, throw Exception("超出最大預訂數量"))
moon = db.query("select * from mooncake where id = ?", moonId)
Assert (moon != nil, throw Exception("月餅不存在"))
Assert (moon.capacity >= 1, throw Exception("月餅餘量不足")
db.execute("update mooncake set capacity = capacity-1 where moonId = ?",moonId)
db.insert("insert into people_mooncake_ref values(nill,?,?)",userId,moonId)
}
複製代碼
寫完,小A看了看時間,纔過去30秒。恩,一次debug,完美運行,美滋滋。正準備回家,忽然想起來公司摳門的程度,暗暗心想:golang
汗水順着小A的額頭滴下來,小A趕忙掏出墊顯示器的《高性能Mysql》,仔細研讀。恩,果真有問題。sql
一旦第9行代碼執行失敗了( 網絡錯誤 / 鏈接超時 / 機器掉電 ... ),月餅就永遠被少定一個了。 久經沙場的小A一眼看出了問題,兩個寫操做,沒有事務。Easy,加!數據庫
func orderMoon(moonId int64){
userId = passport.GetUserID()
ordered = db.query("select count(1) from people_mooncake_ref where userId = ?",userId)
Assert (ordered < 2 , throw Exception("超出最大預訂數量"))
moon = db.query("select * from mooncake where id = ?", moonId)
Assert (moon != nil, throw Exception("月餅不存在"))
Assert (moon.capacity >= 1, throw Exception("月餅餘量不足")
db.begin()
db.execute("update mooncake set capacity = capacity-1 where id = ?",moonId)
db.insert("insert into people_mooncake_ref values(nill,?,?,now())",userId,moonId)
db.commit()
}
複製代碼
小A愉快地回家了,中秋,卒。後來小B接手了小A的代碼。做爲一名更資深的CRUD工程師,一眼看出了問題。性能優化
先查,再改,有先後依賴。在並行的狀況下,會出現月餅被扣爲負數的狀況。這是一類典型的問題,先查詢進行判斷,而後去修改數據,因爲是非原子操做,在並行的狀況下會出現非預期結果。網絡
加鎖無非,要麼悲觀鎖,要麼樂觀鎖。要麼可重入,要麼不可重入。要麼公平,要麼非公平。架構
func orderMoon(moonId int64){
userId = passport.GetUserID()
db.begin() // 上移事務開始時機,讓X鎖對整個事務生效
ordered = db.query("select count(1) from people_mooncake_ref where userId = ?",userId)
Assert (ordered < 2, throw Exception("超出最大預訂數量"))
moon = db.query("select * from mooncake where id = ? for update", moonId) // X鎖,由於where是主鍵,鎖行
Assert (moon != nil, throw Exception("月餅不存在"))
Assert (moon.capacity >= 1, throw Exception("月餅餘量不足")
db.execute("update mooncake set capacity = capacity-1 where id = ?",moonId)
db.insert("insert into people_mooncake_ref values(nill,?,?,now())",userId,moonId)
db.commit()
}
複製代碼
func orderMoon(moonId int64){
userId = passport.GetUserID()
ordered = db.query("select count(1) from people_mooncake_ref where userId = ?",userId)
Assert (ordered < 2, throw Exception("超出最大預訂數量"))
db.begin()
effectiveLine = db.execute("update mooncake set capacity = capacity-1 where id = ? and capacity > 1",moonId) // 根據生效行數,判斷是否更新成功
Assert (moon.capacity != 0, throw Exception("月餅餘量不足")
db.insert("insert into people_mooncake_ref values(nill,?,?,now())",userId,moonId)
db.commit()
}
複製代碼
這種方式用RDBMS原生的MVCC來進行自旋,更優雅,高效地解決了這個問題。 其它辦法 固然,這種問題還會有不少其它的解決方案,例如:
小B驚奇地發現,雖然解決了超賣的問題,可是同一個員工可能會定超過兩個月餅。小E就由於寫了個腳本,一下預約了10個月餅,被公司XXX委員會開除。小B深感自責,決定完全修復這個問題。 先檢查,再插入。一樣會由於非原子操做,而致使檢查操做失效。
因爲insert與update不一樣,無法把鎖加在現有的數據行上面,要解決這個問題,只能尋求其它粒度的鎖,好比:鎖表。鎖表有兩種方法。
func orderMoon(moonId int64){
userId = passport.GetUserID()
db.begin()
db.execute("lock table `people_mooncake_ref` WRITE")
ordered = db.query("select count(1) from people_mooncake_ref where userId = ?",userId)
Assert (ordered == 0, throw Exception("請勿重複預訂"))
effectiveLine = db.execute("update mooncake set capacity = capacity-1 where id = ? and capacity > 1",moonId) // 根據生效行數,判斷是否更新成功
Assert (effectiveLine < 1, throw Exception("月餅餘量不足")
db.insert("insert into people_mooncake_ref values(nill,?,?,now())",userId,moonId)
db.execute("unlock tables")
db.commit()
}
複製代碼
特別要注意的是,當一個事務須要多處鎖的時候,要先加上大粒度的鎖。先加鎖的,後釋放。不然會發生死鎖的狀況。本覺得故事到這裏就圓滿結束了。 小B開開心心寫完,結果次日早會被DBA打進醫院: 你竟然故意加表鎖!!!
小B能夠經過添加惟一索引,經過記錄的惟一性來限制員工預訂超過兩個月餅。修改預訂表,增長orderNum字段。
/* 添加順序號 */ ALTER TABLE `people_mooncake_ref` ADD `order_num` INT NULL DEFAULT NULL COMMENT '順序號' AFTER `people_id`;
/* 添加惟一索引 */ ALTER TABLE `people_mooncake_ref` ADD UNIQUE INDEX `ORDER_UNION` (`people_id`, `order_num`);
people_mooncake_ref表:
複製代碼
func orderMoon(moonId int64){
userId = passport.GetUserID()
orderNum = db.query("select case when max(order_num) is null then 1 else max(order_num) from people_mooncake_ref where userId = ?",userId)
Assert (orderNum < 2, throw Exception("超出最大可預訂數量"))
db.begin()
effectiveLine = db.execute("update mooncake set capacity = capacity-1 where id = ? and capacity > 1",moonId) // 根據生效行數,判斷是否更新成功
Assert (effectiveLine < 1, throw Exception("月餅餘量不足")
effectiveLine = db.insert("insert IGNORE into people_mooncake_ref values(nill,?,?,?,now())",userId,orderNum+1,moonId) // 根據生效行數決定是否成功
Assert (effectiveLine != 0, throw Exception("系統繁忙,請稍後再試"))
db.commit()
}
複製代碼
其它方法 固然,這種問題還會有不少其它的解決方案,例如:
小B長長地舒了一口氣,不再會由於系統有問題被各類diss了,把系統扔一邊開始刷leetcode。可是畢竟仍是太年輕,惟一不變的就是變化,公司來了一個新的CTO,CTO一看系統架構急了,全公司上下開始了一場轟轟烈烈技術革新的運動,堪比福報廠去IOE。