库存安全
下单逻辑
- 判断库存
- 减去库存
- 生成订单
/**
* 下单操作1
*
* @param req
*/
private place1($req) {
$user = userDao.findOne($req.getUserId());
$product = productDao.findOne($req.getProductId());
//下单数量
$num = $req.getNum();
//可用库存
$availableNum = $product.getAvailableNum();
//可用预定
if ($availableNum >= $num) {
//减库存
$count = productDao.reduceStock1($product.getId(), $num);
if ($count == 1) {
//生成订单
createOrders($user, $product, $num);
} else {
logger.info("库存不足 3");
}
return 1;
} else {
logger.info("库存不足 4");
return -1;
}
}
低并发处理方案
- redis锁,限制并发
/**
* 采用 Redis 锁 通一个时间 只能一个 请求修改 同一个商品的数量
* <p>
* 缺点并发不高,同时只能一个用户抢占操作,用户体验不好!
*
* @param req
*/
public void placeOrder2($req) {
$lockKey = "placeOrder:" + $req.getProductId();
$isLock = redisService.lock($lockKey);
if (!$isLock) {
logger.info("系统繁忙稍后再试!");
return 2;
}
//place2(req);
place1($req);
//这两个方法都可以
redisService.unLock($lockKey);
}
高并发处理方案
- 商品库存保存到redis,redis increment操作为原子性
- 库存检查与减少库存不是原子性,以increment > 0为准
public placeOrder3($req) {
$key = "product:" + $req.getProductId();
// 先检查 库存是否充足
$num = (int) redisService.get($key);
if ($num < $req.getNum()) {
logger.info("库存不足 1");
}else{
//不可在这里下单减库存,否则导致数据不安全, 情况类似 方法1;
}
//减少库存
$value = redisService.increment($key, -$req.getNum().longValue());
//库存充足
if ($value >= 0) {
logger.info("成功抢购 ! ");
//TODO 真正减 扣 库存 等操作 下单等操作 ,这些操作可用通过 MQ 或 其他方式
place1(req);
} else {
//库存不足,需要增加刚刚减去的库存
redisService.increment($key, $req.getNum().longValue());
logger.info("库存不足 2 ");
}
}
高并发核心技术-MQ
安装服务端
- 安装Erlanghttps://www.erlang.org/downloads
- 安装RabbitMQhttps://www.rabbitmq.com/
- 配置环境变量
– ERLANG_HOME=C:\soft\erl10.4
– RABBITMQ_SERVER=C:\soft\RabbitMQ Server\rabbitmq_server-3.7.17
– Path=%ERLANG_HOME%\bin;%RABBITMQ_SERVER%\sbin
4. 基础命令 进入C:\soft\RabbitMQ Server\rabbitmq_server-3.7.17\sbin
– 查看mq状态
rabbitmqctl status
- 安装 RabbitMQWeb的管理插件
rabbitmq-plugins enable rabbitmq_management
- RabbitMQWeb管理,输入命令行查看用户列表
rabbitmqctl.bat list_users
- http://127.0.0.1:15672/ quest quest
php客户端开发代码 AMQP
引用amqplib包
composer require php-amqplib/php-amqplib
代码实现部分
// config.php
<?php
return [
'vendor' => [
'path' => dirname(__DIR__) . '/vendor'
],
'rabbitmq' => [
'host' => '127.0.0.1',
'port' => '5672',
'login' => 'guest',
'password' => 'guest',
'vhost' => '/'
]
];
// send.php 生产者
<?php
$config = require "./config.php";
require_once $config['vendor']['path'] . '/autoload.php';
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
// 1.创建连接
$connection = new AMQPStreamConnection($config['rabbitmq']['host'], $config['rabbitmq']['port'],
$config['rabbitmq']['login'], $config['rabbitmq']['password'], $config['rabbitmq']['vhost']);
// 2.(在连接中)创建频道
$channel = $connection->channel();
//发送方其实不需要设置队列, 不过对于持久化有关,建议执行该行
// 3.声明频道中队列
$channel->queue_declare('hello', false, false, false, false);
// 4. 发送消息
$msg = new AMQPMessage('Hello World!');
$channel->basic_publish($msg, '', 'hello');
echo " [x] Sent 'Hello World!'\n";
// 5. 关闭频道
$channel->close();
// 6. 关闭连接
$connection->close();
<?php
// receive.php
$config = require "./config.php";
require_once $config['vendor']['path'] . '/autoload.php';
use PhpAmqpLib\Connection\AMQPStreamConnection;
// use PhpAmqpLib\Message\AMQPMessage;
// 1.创建连接
$connection = new AMQPStreamConnection($config['rabbitmq']['host'], $config['rabbitmq']['port'],
$config['rabbitmq']['login'], $config['rabbitmq']['password'], $config['rabbitmq']['vhost']);
// 2.创建频道
$channel = $connection->channel();
// 3.声明队列
$channel->queue_declare('hello', false, false, false, false);
echo ' [*] Waiting for messages. To exit press CTRL+C', "\n";
$callback = function($msg) {
echo " [x] Received ", $msg->body, "\n";
};
// 4.接收消息
$channel->basic_consume('hello', '', false, true, false, false, $callback);
//while(count($channel->callbacks)) {
// $channel->wait();
//}
// 5.检测消费者,存在则频道等待中
while($channel->is_consuming()) {
$channel->wait();
}
$channel->close();
$connection->close();