一个UPDATE语句引发的血案

318 阅读2分钟

原文在我的博客:blog.zlb37.xyz/2018-06-13_…

今天对数据库的数据进行清查,发现其中有两个用户的数据与交易记录不一致,其中user0少了200虚拟币,user多了200虚拟币。幸好用户还没有投诉,赶紧把数据改正确,但是也要研究一下发生的原因。

虚拟币是可以在网站内部流通的一种产品,允许用户间买卖和赠送。

和用户交易相关的有三张表,分别是usersell_ordertransfer_ordertransfer_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)