使用MySQL实现秒杀的三种方法
概述
为什么使用MySQL
秒杀是不安全的?
对于库存量限制的商品,扣减库存时一般会先查询数据库中剩余库存量,如果还有库存,则进行库存扣减操作。
在多线程场景下,假设某一时刻商品的库存量只有一个,此时正好有两个线程同时查询库存量,发现都大于零,随后他们都会执行update good set count = count - 1 where id = 1
进行库存更新,这两个线程会依次执行成功,此时库存量为-1
,出现了超卖问题。
如果使用MySQL
解决超卖问题,一般有三种做法:悲观锁
、乐观锁
、MySQL自身锁
。
悲观锁
悲观锁在第一步查询的时候就将数据锁定,保证只有自己进入更新逻辑,这样避免有多个线程同时更新扣减库存。
实现原理:
- 使用
select * from good where id = 1 for update
语句查询当前读,此时数据库会加锁,其他想要当前读的线程都会被阻塞。 - 随后使用
update good set count = count - 1 where id = 1
语句进行更新。
优缺点:
该方法的缺点在于将查询和更新串行化,保证某一时刻只能有一个线程更新数据库。当突然有大量请求后,由于请求串行化,所以大多数线程会因为等待而超时。
压测时,固定请求数,持续的时间会变长,但库存量最终会变为0,因为每个线程虽然等待的时间变长,但总会执行成功后才返回。
乐观锁
乐观锁通过版本号来实现,数据库设计时为每一条记录加一个版本号,通过比较版本号来判断是否有人更新过数据。
实现原理:
- 第一步查询时,进行快照读读取当前记录的版本号(
快照读不会加锁
) - 随后将版本号作为查询条件进行更新,如果版本号不一致,说明其他人已经更新过,此次更新失败,执行语句为
update good set count = count - 1 where id = 1 and version = old_version
。
优缺点:
该方法的优点在于响应速度快,线程第一步查询时不需要阻塞等待,减少了响应时间。缺点是当同时有大量请求进来时,由于竞争激烈,绝大多数线程都不会更新成功,最终的结果就是虽然用户响应很快,但失败次数多,秒杀后还有很多库存
。
MySQL自身锁
还可以利用MySQL自身锁来解决问题,即每次更新前判断扣减库存后是否大于零,大于零时才会进行更新。
update good set count = count - 1 where id = 1 and count - 1 > 0
优缺点:
此方法的优点是只需要一次查询,且性能较好。缺点是由于使用了>
,所以不会走索引,数据量大时性能不高。
这种方法是三种方法中性能较好的一种。
注:即使相比之下,第三种方法性能较好,但是受限于
MySQL
性能的问题(主要是读写磁盘的消耗
),上面的方法在高并发场景下并不可靠。更好的方法是借助
Redis
这种基于内存的数据库。