如果采用了分布式,底层都通过api调取,那么就会存在网络问题,超时问题,因此,可能会有多次请求到库存节点,为了满足一致性,需要每个api满足幂等性,也就是 f(x) == f(f(x)),也就是多次执行的结果和一次执行的结果是一样的
对于产品库存,也就是:多次调用扣除库存的api,库存只会扣除一次,
对于库存补偿,多次调用库存补偿方法,库存只会补偿一次。
当库存api满足幂等性,我们才可能采用分布式,通过api扣除库存,返还库存。
扣除库存的幂等性
库存api节点:
库存表: Product (product_id 为 唯一索引)
product_id
qty
库存历史表: ProductHistory (order_item_id 为唯一索引)
product_id
order_item_id
qty
updated_at
ver default 0
// 扣除成功
const STOCK_DEDUCT_SUCCESS = 1;
// 库存不足
const STOCK_OUT = 2;
// 库存已经被扣除
const STOCK_HAVE_DEDUCTED = 3;
const UNKNOW_ERROR = 4;
// 回滚部分状态
// 回滚成功
const ROLLBACK_SUCCESS = 1;
// 回滚库存历史表找不到数据
const ROLLBACK_NO_HISTORY = 2;
// 回滚库存,已经被回滚了,不需要再次回滚
const ROLLBACK_HISTORY_HAS_DONE = 3;
// 回滚库存表失败
const ROLLBACK_STOCK_FAIL = 4;
const ROLLBACK_UNKNOW_ERROR = 10;
/**
* 扣除库存的函数
* @property $product_id 产品id
* @property $sale_qty 扣除库存的个数(正数)
* @property $order_item_id 订单产品表的id(这个是订单产品表的主键id,因此是唯一的)
*/
public function deductProductQty($product_id,$sale_qty,$order_item_id){
$allowDeduct = 0;
// 查找是否存在历史表
$productHistory = ProductHistory::find([
'product_id' => $product_id,
'order_item_id' => $order_item_id,
'qty' => $sale_qty,
])->asArray()->one();
if (empty($productHistory)) {
// 如果在库存历史表中不存在,则查询一下库存是否满足,如果不满足,直接退出
$product_One = Product::find(
['and', ['product_id' => $product_id], ['>=', 'qty', $sale_qty]]
)->asArray()->one();
if(empty($product_One)){
return $this->resultData(self::STOCK_OUT);
}
}
$innerTransaction = Yii::$app->db->beginTransaction();
try {
if (!empty($productHistory)) {
// 如果存在,则更新历史表
$updateCounts = $this->updateHistiryQty($product_id, $order_item_id, $sale_qty);
if ($updateCounts) {
$allowDeduct = 1;
} else {
throw new \Exception(self::STOCK_HAVE_DEDUCTED);
}
} else {
// 添加库存历史表信息
$ProductHistory = new ProductHistory;
$ProductHistory->product_id = $product_id;
$ProductHistory->order_item_id = $order_item_id;
$ProductHistory->qty = $qty;
$ProductHistory->ver = 1;
$ProductHistory->updated_at = time();
$ProductHistory->save();
$allowDeduct = 1;
}
// 允许扣库存
if ($allowDeduct) {
$updateColumns = Product::updateAllCounters(
['qty' => $sale_qty],
// 条件中加入 ['>=', 'qty', $sale_qty] 防止超卖。
['and', ['product_id' => $product_id], ['>=', 'qty', $sale_qty]]
);
if(!$updateColumns) {
throw new \Exception(self::STOCK_OUT);
}
}
$innerTransaction->commit();
} catch (Exception $e) {
$innerTransaction->rollBack();
return $this->resultData($e->getMessage());
}
return $this->resultData(self::STOCK_DEDUCT_SUCCESS);
}
/**
* 返还库存的函数
* @property $product_id 产品id
* @property $sale_qty 扣除库存的个数(正数)
* @property $order_item_id 订单产品表的id(这个是订单产品表的主键id,因此是唯一的)
*/
public function rollBackProductQty($order_item_id, $sale_qty){
// 查看在库存历史表中是否存在记录
$productHistoryOne = ProductHistory::find([
'order_item_id' => $order_item_id,
'qty' => $sale_qty,
])->asArray()->one();
if(empty($productHistoryOne)){
// 返回,无历史记录
return $this->resultRollbackData(self::ROLLBACK_NO_HISTORY);
}
$innerTransaction = Yii::$app->db->beginTransaction();
try {
$product_id = $productHistoryOne['product_id'];
$order_item_id = $productHistoryOne['order_item_id'];
// 将库存历史表回滚
$updateColumns = $this->rollBackHistiryQty($order_item_id, $sale_qty);
if(empty($updateColumns)){
// 返回,回滚库存历史表失败
throw new \Exception(self::self::ROLLBACK_HISTORY_HAS_DONE);
}
// 回滚库存表
$updateQtyColumns = Product::updateAllCounters(
['qty' => new \yii\db\Expression('qty + '.$sale_qty)],
// 条件中加入 ['>=', 'qty', $sale_qty] 防止超卖。
['product_id' => $product_id]
);
if(!$updateQtyColumns) {
// 返回,回滚产品库存失败
throw new \Exception(self::ROLLBACK_STOCK_FAIL);
}
// 回滚成功
$innerTransaction->commit();
} catch (Exception $e) {
$innerTransaction->rollBack();
return $this->resultRollbackData($e->getMessage());
}
return $this->resultRollbackData(self::ROLLBACK_SUCCESS);
}
public function resultRollbackData($message){
if ($message == self::ROLLBACK_SUCCESS){
return [true, self::ROLLBACK_SUCCESS,'库存回滚成功'];
}else if ($message == self::ROLLBACK_NO_HISTORY){
return [false, self::ROLLBACK_NO_HISTORY,'回滚库存,在库存历史表找不到数据'];
}else if ($message == self::ROLLBACK_HISTORY_HAS_DONE){
return [true, self::ROLLBACK_HISTORY_HAS_DONE,'库存已经回滚,不需要再次回滚'];
}else if ($message == self::ROLLBACK_STOCK_FAIL){
return [false, self::ROLLBACK_STOCK_FAIL,'回滚库存失败'];
}else {
return [false, self::ROLLBACK_UNKNOW_ERROR,'未知错误'];
}
}
// return [库存扣除状态,库存扣除执行状态,详细信息]
public function resultData($message){
if ($message == self::STOCK_DEDUCT_SUCCESS){
return [true, self::STOCK_DEDUCT_SUCCESS,'库存扣除成功'];
} else if ($message == self::STOCK_OUT){
return [false, self::STOCK_OUT,'库存不足'];
} else if ($message == self::STOCK_HAVE_DEDUCTED){
return [true, self::STOCK_HAVE_DEDUCTED,'库存已经被扣除过了'];
}else {
return [false, self::UNKNOW_ERROR,'未知错误'];
}
}
// 'qty' => new \yii\db\Expression('qty - '.$sale_qty),
public function updateHistiryQty($product_id, $order_item_id, $sale_qty){
$updateCounts = ProductHistory::updateAll(
[
'ver' => 1,
'updated_at' => time()
],
[
'product_id' => $product_id,
'order_item_id' => $order_item_id,
'ver' => 0,
'qty' => $sale_qty,
]
);
}
// 回滚库存历史表信息。
public function rollBackHistiryQty($order_item_id, $sale_qty){
$updateCounts = ProductHistory::updateAllCounters(
[
'ver' => 0,
'updated_at' => time()
],
[
'order_item_id' => $order_item_id,
'ver' => 1,
'qty' => $sale_qty,
]
);
}
总结:
1.调用api接口扣除库存,满足满足幂等性,即使多次调用api,库存仍旧只会扣除一次
2.回滚操作也就是函数 rollBackProductQty,也要满足幂等性,执行多次,库存的补偿只能一次
3.在下单过程,很调用很多类似库存这样的api节点,假设A,B,C,D四个api,C失败后,那么B,A都要回滚,
回滚可以调用相应的回滚函数。
4.在回滚的过程中可能宕机,因此,我们还需要有一个后台脚本,进行做检查,对于完成一半的分布式事务,调用回滚函数进行补偿,来满足最终一致性。另外,还要看这种补偿机制,是否会对业务有影响。
5.回滚补偿函数,可能会一直失败,譬如在产品库存补偿的时候,产品下架了,导致补偿一直失败,
因此,对于补偿脚本,应该要有一个记录,补偿次数超过最大次数,则不再运行补偿脚本,人工介入,查看是什么原因导致补偿脚本一直失败