实战说明
最近在一个项目营销活动中,一位同事用到了Redis来实现商品的库存管理。在压测的过程中,发现存在超卖的情况。这里总结一篇如何正确使用Redis来解决秒杀场景下,超卖的情况。
演示步骤
这里不会直接给大家说明,该怎么去实现安全、高效的分布式锁。而是通过循序渐进的方式,通过不同的方式实现锁,并发现每一种锁的缺点以及针对该类型的锁进行如何优化,最终达到实现一个高效、安全的分布锁。
第一种场景
该场景是利用Redis来存储商品数量。先获取库存,针对库存判断,如果库存大于0,则减少1,再更新Redis库存数据。大致示意图如下:
-
当第一个请求来之后,去判断Redis的库存数量。接着给商品库存减少一,然后去更新库存数量。
-
当在第一个请求处理库存逻辑之间,第二个请求来了,同样的逻辑,去读Redis库存,判断库存数量接着执行减少库存操作。此时他们操作的商品其实就是同一个商品。
-
然后这样的逻辑,在秒杀这样大量请求来,就很容易实际商品售卖的数量远远大于商品库存数量。
public function demo1(ResponseInterface $response)
{
$application = ApplicationContext::getContainer();
$redisClient = $application->get(Redis::class);
/** @var int $goodsStock 商品当前库存*/
$goodsStock = $redisClient->get($this->goodsKey);
if ($goodsStock > 0) {
$redisClient->decr($this->goodsKey);
// TODO 执行额外业务逻辑
return $response->json(['msg' => '秒杀成功'])->withStatus(200);
}
return $response->json(['msg' => '秒杀失败,商品库存不足。'])->withStatus(500);
}
复制代码
问题分析:
-
该方式使用Redis来管理商品库存,减少对MySQL的压力。
-
假设此时库存只有1,第一请求在判断库存为大于0,减少库存的过程中。如果存在第二个请求来读取到了数据,发现商品库存是大于0的。两者都会执行秒杀的逻辑,然而库存只有一个,就遇到了超卖的情况。
-
此时,我们试想一下,如果我们只能让一个请求处理库存,其他的请求只有等待直到上一个请求结束才能去进行获取商品库存,是不是就能实现超卖呢?这就是下面几种场景提到的锁机制实现。
第二种场景
使用文件锁,第一请求来了之后,打开文件锁。处理完毕业务之后,释放当前的文件锁,接着处理下一个请求,依次循环。保证当前的所有请求,只有一个请求在处理库存。请求处理完毕之后,则释放锁。
-
使用文件锁,来一个请求给一个文件加锁。此时另外的请求就会被阻塞,直到上一个请求成功释放锁文件,下一个请求才会执行。
-
所有的请求就犹如一个队列一样,前一个先入队列后一个后入队列,一次按照FIFO的顺序进行。
public function demo3(ResponseInterface $response)
{
$fp = fopen("/tmp/lock.txt", "r+");
try {
if (flock($fp, LOCK_EX)) { // 进行排它型锁定
$application = ApplicationContext::getContainer();
$redisClient = $application->get(Redis::class);
/** @var int $goodsStock 商品当前库存*/
$goodsStock = $redisClient->get($this->goodsKey);
if ($goodsStock > 0) {
$redisClient->decr($this->goodsKey);
/