【Redis实现秒杀业务②】超卖问题的解决方案

我们在上一篇的文章中,实现了秒杀的基本业务,但是在并发的场景下,我们会发现代码存在诸多问题,超卖就是经典的问题之一。让我们来一起研究一下这玩意。

什么是超卖

超卖现象是秒杀业务中常见的现象,意指用户在购买指定商品的过程中,售卖的商品超过了商户的预期。

超卖是怎么产生的

如果只剩最后一件商品的时候,恰好这时有两个线程同时进入这个红色框框,就会一起进入更新的代码,于是就会产生超卖现象了。

在这里插入图片描述

图解超卖

正常情况

在这里插入图片描述
超卖情况
在这里插入图片描述

解决方案

超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:

  • 悲观锁
  • 乐观锁

悲观锁

认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。
例如Synchronized、Lock都属于悲观锁。

悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。

但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会,同时也会降低并发性能。

悲观锁的实现----MySQL版
要使用悲观锁,我们必须关闭mysql数据库的自动提交属性。MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。我们可以使用命令设置MySQL为非autocommit模式:set autocommit=0;

//开始事务
begin; 
//查询出商品库存信息,使用 for update 加上排它锁
select quantity from items where id=1 for update;
//修改商品库存
update items set quantity=100 where id = 1;
//提交事务
commit;

乐观锁

认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。

如果没有修改则认为是安全的,自己才更新数据。

如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常。

相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。

乐观锁不会产生任何锁和死锁,拥有更好的性能。

乐观锁的实现
通过一个单独的可以顺序递增的version字段,可以避免ABA问题:

//查询出商品信息,version = 1
select version from items where id=1
//修改商品库存为2
update items set quantity=2,version = 3 where id=1 and version = 2;

除了version以外,还可以使用时间戳,因为时间戳天然具有顺序递增性。

减小乐观锁力度,可以最大程度的提升吞吐率,提高并发能力:

//修改商品库存
update item 
set quantity=quantity - 1 
where id = 1 and quantity - 1 > 0

选择悲观锁还是乐观锁

  • 乐观锁并未真正加锁,效率高。然而,如果锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
  • 悲观锁依赖数据库锁,效率低。更新失败的概率比较低。
  • 在高并发的业务场景下,悲观锁越来越少被使用了。

我们下一章就来具体实现一下乐观锁的方案。

猜你喜欢

转载自blog.csdn.net/CSDN_SAVIOR/article/details/125274005
今日推荐