MySQL 处理高并发,防止库存超卖(乐观锁和悲观锁)

商品库存肯定是很有限的,如何控制库存不让出现超买是一个非常重要的问题。事务是控制库存超卖的必要不充分条件。


举例:

总库存:4个商品

请求人:a、购买1个商品 b、购买2个商品 c、购买3个商品


程序如下:

beginTranse
try{
  result = db->query('select amount from s_store where postID = 12345');
  if(result->amount > 0){
    //quantity为请求减掉的库存数量
    db->query('update s_store set amount = amount - quantity where postID = 12345');
  }
}catch(err){
  rollBack
}
commit


当前有三个用户a、b、c三个用户进入到了这个事务中,这个时候会产生一个共享锁,所以在select的时候,这三个用户查到的库存数量都是4个,同时还要注意,mysql innodb查到的结果是有版本控制的,在其他用户更新没有commit之前(也就是没有产生新版本之前),当前用户查到的结果依然是4个;


然后是update,假如这三个用户同时到达update这里,这个时候update更新语句会把并发串行化,也就是给同时到达这里的是三个用户排个序,一个一个执行,并生成排他锁,在当前这个update语句commit之前,其他用户等待执行,commit后,生成新的版本;这样执行完后,库存肯定为负数了。



我们修改一下代码就不会出现超买现象了,代码如下:

beginTranse
try{
  db->query('update s_store set amount = amount - quantity where postID = 12345');
  result = db->query('select amount from s_store where postID = 12345');
  if(result->amount < 0){
    throw new Error('库存不足');
  }
}catch(err){
  rollBack
}
commit


以上方法有个概念,叫做“乐观锁”。乐观锁的该项是相对于悲观锁的。下面详细解释一下概念


悲观锁(Pessimistic Lock)

当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。


悲观锁,正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。


在MySQL中使用悲观锁,必须关闭MySQL的自动提交,set autocommit=0,MySQL默认使用自动提交autocommit模式,也即你执行一个更新操作,MySQL会自动将结果提交。


//step1: 查出商品状态
select quantity from items where id=100 for update;
//step2: 根据商品信息生成订单
insert into orders(id,item_id) values(null,100);
//step3: 修改商品的库存
update Items set quantity=quantity-2 where id=100;


select...for update是MySQL提供的实现悲观锁的方式。此时在items表中,id为100的那条数据就被我们锁定了,其它的要执行select quantity from items where id=100 for update的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。

select...for update语句执行中所有扫描过的行都会被锁上,因此在MySQL中用悲观锁务必须确定走了索引,而不是全表扫描,否则将会将整个数据表锁住。


因为悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。如果加锁的时间过长,其他用户长时间无法访问,影响了程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是对长事务而言,这样的开销往往无法承受,这时就需要乐观锁。



乐观锁(Optimistic Locking)

乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。

//step1: 查询出商品信息
select (quantity,version) from items where id=100;
//step2: 根据商品信息生成订单
insert into orders(id,item_id) values(null,100);
//step3: 修改商品的库存
update items
  set quantity=quantity-1,version=version+1 
  where id=100 and version=#{version};


既然可以用version,那还可以使用时间戳字段,该方法同样是在表中增加一个时间戳字段,和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。


需要注意的是,如果你的数据表是读写分离的表,当master表中写入的数据没有及时同步到slave表中时会造成更新一直失败的问题。此时,需要强制读取master表中的数据(将select语句放在事物中)。


秒杀的情况下,肯定不能如此高频率的去读写数据库,会严重造成性能问题的,一般都是用redis缓存商品。


参考:

《高性能MySQL》

https://www.jianshu.com/p/ed896335b3b4

https://zhuanlan.zhihu.com/p/98871669

声明:本站所有文章和图片,如无特殊说明,均为原创发布。商业转载请联系作者获得授权,非商业转载请注明出处。
随机推荐
选择排序
JavaScript 使用 qrcode 生成二维码
CSS 图片缩小出现锯齿
浏览器的同源和跨域
JavaScript document 对象
Node.js process 模块
二分查找法
MySQL DATETIME 时间查询和转换