MySQL 死锁产生的原因和“超卖”现象

死锁其实很好理解,就是两个会话分别在等待对方占有锁的释放。这个概念不仅仅是MySQL数据库中的,计算机中一些资源的占有和释放,都可能会产生死锁。

在MySQL数据库中,举个例子,由于代码设计不当,比如两个事务会话中, 都使用读锁去占有一条数据,但是两个会话却都想更新这条数据,如果是并发请求,则会产生死锁,看以下SQL执行顺序:

begin;
select * from goods_sku where id = 1 in share mode;
update goods_sku set stock = stock - 1 where id = 1 and stock > 0;
commit; 

如果你不了解in share mode,可以读我这篇文章MySQL中的锁

在并发场景下,两个事务都可以执行select语句,因为共享锁可以允许其他事务加共享锁,但是不允许其他事务加排它锁,因此两个事务在执行到第三条时,各发现其他事务都占有共享锁,因此各自在等待对方的锁释放,进入死锁状态。

上面的问题,解决办法就是将select语句的in share mode改成for update,将共享锁改成排它锁,因此任意一个事务占有了这条数据,其他事务时是不能给它加排它锁的,需要等第一个事务commit或者rollback才行。

以上是死锁产生的一个原因。

那什么是超卖呢?超卖就是由于不合理的设计,导致电商项目中,卖出去的货比实际库存的数量要多,一般来说,MySQL的update goods_sku set stock = stock - 1 where...句式不会照成超卖(stock字段为unsigned)。

虽然这种SQL语句不会照成超卖,但是会引发另外一个问题,在秒杀场景下,会有大量锁竞争,导致数据库压力骤增,并且很多请求都会在由于等待时间太长导致超时。用户此时肯定会怨声载道并尝试再次点击抢购按钮,并再次陷入锁竞争的漩涡中。

那什么时候会造成超卖呢?一般有以下几种情况:

  1. 先select得到stock库存数量,然后通过代码计算最终库存,使用SQL update goods_sku set stock = ? where id = 1,这种就是犯大错了,数量递增递减,这个交给MySQL去操作就好了,因为MySQL的update是自带排它锁的。
  2. 使用MySQL的递减语句 update goods_sku set stock = stock - 2 where id 1 and stock > 0;,但是不检查执行结果,因为很有可能返回的影响行数是0,即没有更新成功,代码中却认为减库存成功了,也需要多留意。
  3. 很少发生的情况,但不是不可能。我们一般规定如果买家30分钟未支付,那么认为订单取消,然后库存会还回去,如果这时候用户恰好支付了(收到了支付回调的通知),导致库存回去了,并且订单状态由 等待支付->取消->支付完成 的一个状态转换,这种情况的应对措施就是,超时自动取消逻辑处理之前,先调用第三方支付的关闭订单接口,如果关闭成功,那这个用户后续无法对该订单发起支付。如果返回订单已支付,则无需处理该订单,该订单会收到第三方支付的回调。

因此平时还是需要多注意,毕竟这种问题很少发生,但是如果发生了还是非常头疼的。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注