原文在我的博客:
今天对数据库的数据进行清查,发现其中有两个用户的数据与交易记录不一致,其中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)复制代码