本文目录导读:

在PHP项目中实现出库单扣减库存,建议采用事务处理 + 锁机制(乐观锁/悲观锁) 来保证数据一致性,以下是完整的实现逻辑和代码示例:
核心逻辑流程
public function processOutboundOrder($orderData) {
// 开启数据库事务
$this->db->beginTransaction();
try {
// 1. 校验出库单商品数据
$this->validateOutboundItems($orderData['items']);
// 2. 锁定库存(防止并发)
$this->lockInventory($orderData['items']);
// 3. 逐项扣减库存
foreach ($orderData['items'] as $item) {
$this->deductStock($item['product_id'], $item['quantity']);
}
// 4. 记录出库单
$this->saveOutboundOrder($orderData);
// 提交事务
$this->db->commit();
} catch (Exception $e) {
$this->db->rollback();
throw $e;
}
}
扣减库存方法实现
方案1:悲观锁(推荐高并发场景)
// 使用 SELECT ... FOR UPDATE 锁定行
public function deductStock($productId, $quantity) {
// 锁定并查询库存
$sql = "SELECT id, stock_quantity, version
FROM products
WHERE id = ?
FOR UPDATE";
$product = $this->db->fetchOne($sql, [$productId]);
// 检查库存是否充足
if ($product['stock_quantity'] < $quantity) {
throw new Exception("商品{$productId}库存不足");
}
// 执行扣减
$updateSql = "UPDATE products
SET stock_quantity = stock_quantity - ?,
updated_at = NOW()
WHERE id = ?
AND stock_quantity >= ?"; // 二次校验
$affected = $this->db->execute($updateSql, [$quantity, $productId, $quantity]);
if ($affected === 0) {
throw new Exception("库存扣减失败,可能已被其他请求更新");
}
}
方案2:乐观锁(适用于冲突较少的场景)
public function deductStock($productId, $quantity, $maxRetries = 3) {
$retries = 0;
while ($retries < $maxRetries) {
// 查询当前库存和版本号
$product = $this->db->fetchOne(
"SELECT stock_quantity, version FROM products WHERE id = ?",
[$productId]
);
if ($product['stock_quantity'] < $quantity) {
throw new Exception("库存不足");
}
$newVersion = $product['version'] + 1;
// 带版本号的更新
$sql = "UPDATE products
SET stock_quantity = stock_quantity - ?,
version = ?,
updated_at = NOW()
WHERE id = ?
AND version = ?
AND stock_quantity >= ?";
$affected = $this->db->execute($sql, [
$quantity,
$newVersion,
$productId,
$product['version'],
$quantity
]);
if ($affected > 0) {
return true; // 成功
}
// 更新失败,重试
$retries++;
usleep(100000); // 100ms 延迟
}
throw new Exception("库存扣减失败,请重试");
}
完整示例代码
<?php
class InventoryService {
private $db;
public function processOutboundOrder($orderId, $items) {
try {
$this->db->beginTransaction();
// 1. 创建出库单记录
$outboundId = $this->createOutbound($orderId);
// 2. 处理每个商品
foreach ($items as $item) {
// 锁定商品行
$product = $this->db->fetchOne(
"SELECT * FROM products WHERE id = ? FOR UPDATE",
[$item['product_id']]
);
if (!$product) {
throw new Exception("商品不存在: " . $item['product_id']);
}
// 校验库存
$requestedQty = $item['quantity'];
$availableQty = $product['stock_quantity'];
if ($availableQty < $requestedQty) {
throw new Exception(
"商品 {$product['name']} 库存不足:需求{$requestedQty},剩余{$availableQty}"
);
}
// 扣减库存
$this->db->execute(
"UPDATE products
SET stock_quantity = stock_quantity - ?,
updated_at = NOW()
WHERE id = ?
AND stock_quantity >= ?",
[$requestedQty, $item['product_id'], $requestedQty]
);
// 记录出库明细
$this->createOutboundDetail($outboundId, $item);
}
$this->db->commit();
return $outboundId;
} catch (Exception $e) {
$this->db->rollback();
// 日志记录错误
error_log("出库失败: " . $e->getMessage());
throw $e;
}
}
private function createOutbound($orderId) {
$sql = "INSERT INTO outbound_orders (order_id, status, created_at)
VALUES (?, 'pending', NOW())";
$this->db->execute($sql, [$orderId]);
return $this->db->lastInsertId();
}
private function createOutboundDetail($outboundId, $item) {
$sql = "INSERT INTO outbound_details
(outbound_id, product_id, quantity, unit_price)
VALUES (?, ?, ?, ?)";
$this->db->execute($sql, [
$outboundId,
$item['product_id'],
$item['quantity'],
$item['unit_price'] ?? 0
]);
}
}
数据库表结构建议
-- 商品表
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
stock_quantity INT NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 1, -- 乐观锁版本号
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 出库单主表
CREATE TABLE outbound_orders (
id INT PRIMARY KEY AUTO_INCREMENT,
order_id VARCHAR(50) NOT NULL,
status ENUM('pending', 'completed', 'cancelled') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 出库明细表
CREATE TABLE outbound_details (
id INT PRIMARY KEY AUTO_INCREMENT,
outbound_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(10,2) DEFAULT 0.00,
FOREIGN KEY (outbound_id) REFERENCES outbound_orders(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
最佳实践建议
- 数据库引擎:使用 InnoDB(支持事务和行级锁)
- 库存充足校验:在 UPDATE 语句中加入
AND stock_quantity >= ?二次校验 - 并发控制:高并发时优先使用悲观锁(FOR UPDATE)
- 性能优化:对
product_id建立索引 - 监控报警:库存低于阈值时触发告警
- 日志记录:每次操作记录详细的日志
// 库存充足监控示例
public function checkStockThreshold($productId, $threshold = 10) {
$sql = "SELECT stock_quantity FROM products WHERE id = ?";
$stock = $this->db->fetchOne($sql, [$productId]);
if ($stock['stock_quantity'] <= $threshold) {
// 发送告警通知
$this->notifyLowStock($productId, $stock['stock_quantity']);
}
}
通过以上方案,可以有效地保证在并发场景下库存数据的准确性和一致性,建议根据实际业务量选择合适的锁机制,并做好异常处理和日志记录。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播