原文在我的部落格:blog.zlb37.xyz/2018-06-13_…
今天對資料庫的資料進行清查,發現其中有兩個使用者的資料與交易記錄不一致,其中user0
少了200虛擬幣,user
多了200虛擬幣。幸好使用者還沒有投訴,趕緊把資料改正確,但是也要研究一下發生的原因。
虛擬幣是可以在網站內部流通的一種產品,允許使用者間買賣和贈送。
和使用者交易相關的有三張表,分別是user
、sell_order
、transfer_order
,transfer_order
記錄了所有虛擬幣轉移記錄,sell_order
記錄的是買賣虛擬幣。
查一下sell_order
:
SELECT order_num, seller, buyer, count, complete_time \
FROM sell_order \
WHERE seller=user0 AND buyer=user1 \
AND complete_time > DATE_ADD(NOW, INTERVAL -1 DAY);
複製程式碼
+-----------+--------+-------+-------+--------------------+
| order_num | seller | buyer | count |complete_time |
+-----------+--------+-------+----------------------------+
| 233333333 | user0 | user1 | 200 |2018-01-01 12:00:00 |
+-----------+--------+-------+-------+--------------------+
複製程式碼
只有一條記錄,沒有異常發生。而清查資料庫的指令碼也依賴的就是這個表。那麼問題可能出在了transfer_order
裡。
SELECT order_num, from, to, count, type, complete_time \
FROM transfer_order \
WHERE from=user0 AND to=user1 \
AND complete_time > DATE_ADD(NOW, INTERVAL -1 DAY);
複製程式碼
+-----------+-------+-------+-------+----------------------------+
| order_num | from | to | count | type | complete_time |
+-----------+-------+-------+-------+----------------------------+
| 666666666 | user0 | user1 | 200 | sell | 2018-01-01 12:00:00 |
+-----------+-------+-------+-------+----------------------------+
| 666666667 | user0 | user1 | 200 | sell | 2018-01-01 12:00:00 |
+-----------+-------+-------+-------+----------------------------+
複製程式碼
這就是說雖然只有一筆賣出訂單,但卻生成了兩筆轉賬訂單,初步判斷是使用者瘋狂點選轉賬按鈕所致,那麼這個BUG就應該隱藏在程式碼裡,還原車禍現場:
def sell_order_complete(order_num, seller, buyer, count):
try:
session = DBSession()
session.query(TableSellOder) \
.filter(TableSellOder.order_num == order_num) \
.update({TableSellOder.complete_time == now(), TableSellOder.status: 'Success'})
session.query(TableUser) \
.filter(TableUser.id = seller) \
.update({TableUser.count: TableUser.count - count})
session.query(TableUser) \
.filter(TableUser.id = buyer) \
.update({TableUser.count: TableUser.count + count})
session.add(TableTrandferOrder(order_num, seller, buyer, count, "sell"))
except Exception:
session.rollback()
rasie_http_error(500)
複製程式碼
在示例程式碼裡使用了Python和SQLAlchemy,此程式碼並非真實程式碼
看上去還算正常,把所有資料庫操作放到了一個事務裡,一旦出現異常回滾。但是SQL UPDATE
語句不管有沒有真的修改了語句,都不會報異常。當使用者瘋狂執行這個函式時,雖然表sell_order
中的資料已經修改,但是使用相同的引數,這個函式可以無限執行,直到賣家的餘額不足為止。
所以在執行SQL UPDATE
語句時,需要檢測SQL UPDATE
語句執行的時影響的行數是否為預期的行數,如果不是預期的行數,需要手動出發異常,不再執行。
這是一場深刻的教訓,修改後的程式碼如下:
def sell_order_complete(order_num, seller, buyer, count):
try:
session = DBSession()
assert session.query(TableSellOder) \
.filter(TableSellOder.order_num == order_num) \
.update({TableSellOder.complete_time == now(), TableSellOder.status: 'Success'}) \
== 1
assert session.query(TableUser) \
.filter(TableUser.id = seller) \
.update({TableUser.count: TableUser.count - count}) \
== 1
assert session.query(TableUser) \
.filter(TableUser.id = buyer) \
.update({TableUser.count: TableUser.count + count}) \
== 1
session.add(TableTrandferOrder(order_num, seller, buyer, count, "sell"))
except Exception:
session.rollback()
rasie_http_error(500)
複製程式碼