我们在上一篇的文章中,实现了秒杀的基本业务,但是在并发的场景下,我们会发现代码存在诸多问题,超卖就是经典的问题之一。让我们来一起研究一下这玩意。
什么是超卖
超卖现象是秒杀业务中常见的现象,意指用户在购买指定商品的过程中,售卖的商品超过了商户的预期。
超卖是怎么产生的
如果只剩最后一件商品的时候,恰好这时有两个线程同时进入这个红色框框,就会一起进入更新的代码,于是就会产生超卖现象了。
图解超卖
正常情况
超卖情况
解决方案
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:
- 悲观锁
- 乐观锁
悲观锁
认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。
例如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
选择悲观锁还是乐观锁
- 乐观锁并未真正加锁,效率高。然而,如果锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
- 悲观锁依赖数据库锁,效率低。更新失败的概率比较低。
- 在高并发的业务场景下,悲观锁越来越少被使用了。
我们下一章就来具体实现一下乐观锁的方案。