高并发的情况,如何防止库存超卖

1.创建数据表

#1.商品库存表
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `number` int(11) DEFAULT NULL COMMENT '总库存',
  `sale` int(11) DEFAULT '0' COMMENT '下单数量',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

#添加数据
INSERT INTO `storage` VALUES ('1', '100');

#2.购买记录
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `number` int(11) DEFAULT NULL COMMENT '总库存',
  `sale` int(11) DEFAULT '0' COMMENT '下单数量',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.什么并发情况下会出现超卖现象?
截图:
在这里插入图片描述
原因是:同一个库存的情况,可能有几个用户同步购买,列如当前库存是99,由于并发的原因,几个用户当前下单的时候库存都是99,导致库存是99的时候,几个用户下单现有库存都是99,然后库存是99的时候产生了几笔订单

3.高并发的情况,防止库存超卖解决方案

  • mysql加排它锁 ,一步到位给锁
  • 使用redis的分布式锁,利用setnx方法
  • 使用redis的乐观锁。利用watch方法

4.未使用锁的情况下,库存超卖

创建文件nolock.php

<?php 
 //模拟活动商品 并发情况下,下单导致库存超卖
/*
测试的时候初始化:storage 的数量 为 100 或 1000
truncate table orders 
ab -c 100 -n 100 http://127.0.0.1/nolock.php
*/
    $pdo = new PDO('mysql:host=127.0.0.1;dbname=diaoshi', 'root', '123456ok');
    $sql="select `number` from  storage where id=1 limit 1";
    $res = $pdo->query($sql)->fetch();
    $number = $res['number'];
    if($number>0)
    {
	$sale=1; //下单数量
        $sql ="insert into `orders` (number,sale) VALUES ($number,$sale)";
     
        $order_id = $pdo->query($sql);
        if($order_id)
        {
     
            $sql="update storage set `number`=`number`-1 WHERE id=1";
            $pdo->query($sql);
        }
    }else{
	echo "库存为空";	
}

ab执行压测

ab工具说明:https://www.cnblogs.com/lidabo/p/9077781.html

[root@localhost html]# ab -c 200 -n 200 http://127.0.0.1/nolock.php

orders执行结果(截图):
在这里插入图片描述

5.使用mysql的悲观锁(排它锁),使用for update,防止超卖

创建mysqllock.php

<?php 
 //模拟活动商品 并发情况下,下单导致库存超卖 
//如何防止库存超卖:利用mysql的锁机制

//查看orders表的下单库存变化就可以知道是否可以达到防止超卖的情况

/*
测试的时候初始化:storage 的数量 为 100 或 1000
truncate table orders 
ab -c 100 -n 100 http://127.0.0.1/nolock.php
*/
    $pdo = new PDO('mysql:host=127.0.0.1;dbname=diaoshi', 'root', '123456ok');
    

   #使用事务
try {
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); //设置错误模式,发生错误时抛出异常
    //开启事务
    $pdo->beginTransaction();	
    
    //设置不自动提交
    $pdo->setAttribute(PDO::ATTR_AUTOCOMMIT,0);

	
    $sql="select `number` from  storage where id=1 for update"; //此处加上排他锁  //加上行锁后,如果user1在买书,并且user1的买书过程没有结束,user2就不能执行SELECT查询书籍数量的操作,这样就保证了不会出现只有1本书,却两个人同时买的状况
    $res = $pdo->query($sql)->fetch();
    $number = $res['number'];
    if($number>0)
    {  
		$sale=1; //下单数量
        	$sql ="insert into `orders` (number,sale) VALUES ($number,$sale)";
    	    	$order_id = $pdo->query($sql);
       	        if($order_id)
        	{ 
               		$sql="update storage set `number`=`number`-1 WHERE id=1 "; //加上行锁后,如果user1在买书,并且user1的买书过程没有结束,user2就不能执行SELECT查询书籍数量的操作,这样就保证了不会出现只有1本书,却两个人同时买的状况
       			$res=$pdo->query($sql);
			if(empty($res)){
			$pdo->rollBack();	
			}
       		 }else{
			$pdo->rollBack();	
		}
	 $pdo->commit();
    }else{
   $pdo->rollBack();
    echo "库存不足";
    }

}catch(Exception $e) {
	$pdo->rollBack();
	die('Transaction Error Message: ' . $e->getMessage());
}

ab执行压测

[root@localhost html]# ab -c 200 -n 200 http://127.0.0.1/mysqllock.php

orders执行结果(截图):
在这里插入图片描述

6.使用redis的分布式锁,利用setnx方法,防止超卖

创建文件redislock.php

<?php 
 //模拟活动商品 并发情况下,下单导致库存超卖 
//如何防止库存超卖:利用redis的锁机制

//查看orders表的下单库存变化就可以知道是否可以达到防止超卖的情况

/*
测试的时候初始化:storage 的数量 为 100 或 1000
truncate table orders 
ab -c 100 -n 100 http://127.0.0.1/redis.php
*/

//大概的实现逻辑就是如下,目前这个demo存在问题,主要是没有优化代码,以及内存的使用

 $pdo = new PDO('mysql:host=127.0.0.1;dbname=diaoshi', 'root', '123456ok');

    

#使用redis
$Redis=new Redis();
$Redis->connect('127.0.0.1');
$lock_key='numbers_lock'; //锁名称



/**
*@desc redis 加锁方法
*@param $Redis redis对象
*@param $key 锁名称
*@param $expTime 过期时间
*/
function redis_set($Redis,$key,$expTime){
	//初步加锁
	$islock=$Redis->setnx($key,time()+$expTime);
	if($islock){
		return true;
	}else{
		//加锁失败的情况下,判断锁是否已经存在,如果锁存在而却已经过期,那么删除
		$val=$Redis->get($key);
		if($val && $val<time()){
			$Redis->del($key);
		}
		//重新设置锁
		return $Redis->setnx($key,time()+$expTime);
	}
}


/**
*@desc redis 解锁操作
*@param $Redis redis对象
*@param $key 解锁
*/
function redis_del($Redis,$key){
	$Redis->del($key);
}


   #使用事务
try {
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); //设置错误模式,发生错误时抛出异常
    //开启事务
    $pdo->beginTransaction();	
    
    //redis锁实现的原理和mysql锁原理差不多,就是防止user1在购买的时候,user1的操作还没有执行完,user2的操作也开始执行了	
   if($lock=redis_set($Redis,$lock_key,10)){	

   	$sql="select `number` from  storage where id=1";
   	$res = $pdo->query($sql)->fetch();
    	$number = $res['number'];
    	if($number>0)
    	{  
			$sale=1; //下单数量
        	$sql ="insert into `orders` (number,sale) VALUES ($number,$sale)";
    	    	$order_id = $pdo->query($sql);
       	        if($order_id)
        	{ 
               		$sql="update storage set `number`=`number`-1 WHERE id=1 ";
       			$res=$pdo->query($sql);
			if(empty($res)){
			$pdo->rollBack();	
			}
       		 }else{
			$pdo->rollBack();	
		}
	 $pdo->commit();
    
  	 		 //事务提交后释放redis锁	
    		redis_del($Redis,$lock_key);	
   		 }
	
	}else{
  		 $pdo->rollBack();
   		 echo "库存不足";
   	 }

}catch(Exception $e) {
	$pdo->rollBack();
	die('Transaction Error Message: ' . $e->getMessage());
}

ab执行压测

[root@localhost html]# ab -c 200 -n 200 http://127.0.0.1/redislock.php

orders执行结果(截图):
在这里插入图片描述

7.使用redis乐观锁,利用watch和事务

创建文件rediswatch.php

<?php 
//模拟活动商品 并发情况下,下单导致库存超卖 
//如何防止库存超卖:利用redis的锁机制(乐观锁)


//查看orders表的下单库存变化就可以知道是否可以达到防止超卖的情况

/*
测试的时候初始化:storage 的数量 为 100 或 1000
truncate table orders 
ab -c 100 -n 100 http://127.0.0.1/rediswatchk.php
*/

//大概的实现逻辑就是如下,目前这个demo存在问题,主要是没有优化代码,以及内存的使用

$pdo = new PDO('mysql:host=127.0.0.1;dbname=diaoshi', 'root', '123456ok');

//使用redis
$Redis=new Redis();
$Redis->connect('127.0.0.1');

$lock_key='numbers_lock'; //锁名称

//秒杀商品数量[此数量从storage获取]
$limit=100;

//开始redis监听
$Redis->watch($lock_key);

//获取抢购数量
$count=$Redis->get($lock_key);
if($count>$limit){
    exit('活动已结束');
}


//开始redis事务
$Redis->multi();

//业务逻辑
$Redis->incr($lock_key); //模拟一个数量一个数量抢购


//开启事务
$pdo->beginTransaction();	

$sale=1; //下单数量
$sql ="insert into `orders` (number,sale) VALUES ($count,$sale)";
$order_id = $pdo->query($sql);

if($order_id){ 
	$sql1="update storage set `number`=`number`-1 WHERE id=1 ";
	$res=$pdo->query($sql1);
	if(empty($res)){
		$pdo->rollBack();
	}			
}else{
	$pdo->rollBack();
}

//默认redis事务不回去管理命令成功还是失败,redis事务不能回滚
$redis_result=$Redis->exec();
if(empty($redis_result)){
	exit('网络有问题');
}

//sql执行成功提交事务
$pdo->commit();

ab执行压测

[root@localhost html]# ab -c 200 -n 200 http://127.0.0.1/rediswatch.php

orders执行结果(截图):
在这里插入图片描述

### Java高并发场景下商品库存的解决方案 #### 分布式锁 分布式锁是一种常见的解决高并发场景下库存问题的方法。它通过确保多个线程在同一时刻只能有一个线程持有锁,从而避免了并发修改带来的数据不一致问题[^3]。 在Java中,可以通过Redis实现分布式锁。Redis提供了`SETNX`命令(Set if Not Exists),可以在键不存在时设置键值对并返回成功状态,这可以用来模拟加锁的过程[^2]。当一个线程成功获取到锁后,其他线程会被阻塞直到锁被释放。为了避免死锁情况的发生,在解锁时需要验证当前线程持有的锁是否为自己创建的锁。 ```java public class RedisDistributedLock { private StringRedisTemplate stringRedisTemplate; public boolean tryLock(String key, long timeoutMillis) { Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "LOCKED", Duration.ofMillis(timeoutMillis)); return Boolean.TRUE.equals(result); } public void unlock(String key) { if ("LOCKED".equals(stringRedisTemplate.opsForValue().get(key))) { stringRedisTemplate.delete(key); } } } ``` #### 悲观锁 悲观锁假设冲突不可避免,并因此阻止任何可能引起冲突的操作发生。在数据库层面,通常使用`SELECT ... FOR UPDATE`语句来锁定记录,这样只有获得锁的事务才能对该条目进行更改[^5]。 ```sql BEGIN TRANSACTION; -- 锁定指定的商品库存记录 SELECT stock FROM products WHERE product_id = ? FOR UPDATE; -- 更新库存数量 UPDATE products SET stock = stock - 1 WHERE product_id = ? AND stock >= 1; COMMIT; ``` 这种方法虽然简单有效,但在高并发情况下可能导致性能瓶颈,因为许多请求可能会等待同一个锁。 #### 乐观锁 与悲观锁不同的是,乐观锁认为大多数时候不会有冲突,所以在读取数据时不施加锁,而是在提交更新的时候检测是否有冲突。如果发现冲突,则重试整个过程直至成功。 一种常见做法是引入版本号字段,每次更新都会增加此字段的数值: ```sql UPDATE products SET stock = ?, version = version + 1 WHERE product_id = ? AND version = ? ``` 上述SQL仅会在目标行未被他人改动的情况下生效;否则将触发失败逻辑重新尝试操作。 #### Redis原子操作 除了利用分布式锁外,还可以直接借助Redis提供的原生原子指令完成扣减动作。例如`DECRBY`可以直接减少某个key对应的value值,而且这一系列动作都是在一个步骤里完成的,天然具备原子特性[^4]。 ```java stringRedisTemplate.opsForValue().decrement("product_stock_key"); ``` 这种策略适用于那些不需要复杂业务流程就能快速响应的应用场合。 --- ### 总结 针对Java高并发环境下的商品库存管理挑战,开发者可以根据实际需求选用合适的方案。对于简单的计数器类应用而言,Redis自带的原子操作可能是最简洁高效的途径之一;而对于更复杂的交易型业务来说,则需综合考虑诸如一致性保障、吞吐量等因素后再决定采用何种同步机制——无论是传统的关系型数据库所提供的悲观/乐观锁还是新兴技术如Redlock算法所代表的新一代分布协调工具皆可成为候选选项[^1]. ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值