举一个用户余额的例子,余额可以是现金、虚拟币、积分等,可以看作对数据准确度要求高的数据,一旦出错,就会造成客户或公司的直接损失。
在 SQL Server 建立一个测试表:
--测试用余额表 create table balance ( id int primary key, --用户ID value int --余额 ) --插入测试数据 delete balance insert balance values(1, 1)
ID 为 1 的用户有 1 块钱的余额
执行一个【假如用户的余额超过或等于 1 元,则扣除 1 元】的操作:
--检查用户的余额是否足够 if(exists(select * from balance where id = 1 and value >= 1)) begin --延长处理时间 waitfor delay '0:00:10' --更新余额 update balance set value = value - 1 end --查看处理结果 select * from balance
在 10 秒之内,另开一个连接,再次执行上面的操作。这是一个并发的操作,在语句模拟的处理时间之内执行。
第二次操作之后,发现用户的余额变成了 -1,这种情况是不能容忍的。
出现这个问题的原因是,在两个并发的操作中,检查余额的语句执行的时候,用户的余额都是足够的,因此两个扣钱的操作都执行了。
从技术的角度上来说,就是两个操作并没有互斥,本来第二个操作应该在第一个操作完成扣钱之后,再去查询余额的。
其实数据库是可以做这样的互斥的,只不过这样做会牺牲性能,于是大部分数据库的隔离级别是【读取提交(READ COMMITTED)】,查询语句也是使用的共享锁。
默认的选择是一种适合大多数情况的,平衡的做法。但显然不适合这种计算余额的情况。
事务型数据库早就想到了这一点,只要在 SQL 语句做一点修改,就可以解决这个问题。