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执行结果(截图):