学习

使用 Redis 实现实时库存中的可用承诺

Will Johnston
作者
Will Johnston, Redis 开发者增长经理
Prasan Kumar
作者
Prasan Kumar, Redis 技术解决方案开发者
GITHUB 代码

以下是克隆本教程中使用应用的源代码的命令

git clone https://github.com/redis-developer/redis-real-time-inventory-solutions

#什么是可用承诺 (ATP)?

零售库存系统的主要要求是向购物者和门店员工呈现准确、实时的库存视图,实现线上购买门店自提 (BOPIS)。优化多库存地点的履单。

可用承诺 (ATP) 是预测的可供销售的库存量,不包括已分配的库存。它使企业能够控制向客户的分销并预测库存。ATP 模型帮助零售商降低库存成本,例如订货成本、持有成本和缺货成本。只要消费者购买预测保持准确,ATP 就很有用。对零售商而言,有效实施 ATP 流程可能意味着持续增长与库存反复缺货、错过销售机会并损害客户体验之间的区别。

#如何计算可用承诺

计算可用承诺是一项相对简单的任务。完成以下公式即可准确细分可用承诺能力

Available-to-promise = QuantityOnHand + Supply - Demand

此公式包含以下要素

  • QuantityOnHand:公司立即可用的产品总数
  • Supply:可供销售的产品总库存量
  • Demand:消费者愿意购买的特定产品数量

#实时库存中的当前挑战

  • 库存过剩和不足:在采用多渠道业务模式(线上和店内)时,缺乏库存可见性导致不同地区和门店的库存过剩和不足。
  • 消费者追求便利:跨地区门店搜索并立即提货而非等待配送的能力是零售商的关键差异化优势。
  • 消费者追求速度:所有零售商,即使是小型或家族经营的,都必须与阿里巴巴、FlipKart、Shopee 和亚马逊等大型在线零售商的客户体验竞争。
  • 高库存成本:零售商通过消除缺货场景造成的销售损失来降低库存成本,这也导致更高的“库存周转率”。
  • 品牌价值:不准确的门店库存计数导致客户不满和销售下降。运营痛点将影响现状。

#为何应将 Redis 用于可用承诺

  • 提高库存可见性:Redis Cloud 提供高度可伸缩的实时库存同步,在门店之间提供可用承诺 (Available-To-Promise) 的库存视图。客户希望从能够跨多个地点检查库存并提供本地可用库存实时视图的零售商处购买。
  • 增强客户体验:亚毫秒级延迟意味着在线客户可以轻松获得购物车、定价和库存可用性的实时视图。Redis Cloud 内置的搜索引擎提供实时的库存全文搜索和聚合分面搜索,可扩展性能,即时搜索数百万种产品类型的库存,帮助客户更快地填满购物车,保持他们的参与度和忠诚度。
  • 大规模成本效率:Redis Cloud 提供门店之间的实时双向一致性,并与企业系统进行数据集成,无需管理消息代理、审计和对账的复杂性和成本。

#使用 Redis 实现实时库存

使用 Redis,系统可实现门店、在途库存和仓库之间的实时库存同步。为零售商提供整个门店网络中最准确、及时的库存数据,并为消费者提供搜索和查找库存的积极客户体验。

Redis 数据集成 (RDI) 能力实现了准确的实时库存管理和系统记录同步。Redis 先进的库存搜索和查询能力为多渠道和全渠道客户以及门店员工提供准确的可用库存信息。

此解决方案提高了库存周转率,从而降低了库存成本,提高了收入和利润。它还减少了客户搜索对系统记录和库存管理系统 (IMS) 的影响。

#客户证言

GITHUB 代码

以下是克隆本教程中使用应用的源代码的命令

git clone https://github.com/redis-developer/redis-real-time-inventory-solutions

管理库存或 SKU(库存单位) 流程包含一些活动,例如

  1. 1.RetrieveSKU:获取产品的当前数量
  2. 2.UpdateSKU:更新产品的最新数量
  3. 3.IncrementSKU:按特定值增加数量(例如,采购更多产品时)
  4. 4.DecrementSKU:按特定值减少数量(例如,产品订单履单后)
  5. 5.RetrieveManySKUs:获取多个产品的当前数量(例如,支付前验证产品库存)
  6. 6.DecrementManySKUs:减少多个产品的数量(例如,包含多个产品的订单履单后)

#RetrieveSKU

以下代码展示了 retrieveSKU 活动的 API 请求和响应示例。

retrieveSKU API 请求

GET http://localhost:3000/api/retrieveSKU?sku=1019688

retrieveSKU API 响应

{
  "data": {
    "sku": 1019688,
    "name": "5-Year Protection Plan - Geek Squad",
    "type": "BlackTie",
    "totalQuantity": 10
  },
  "error": null
}

发出请求时,它通过 API 网关到达库存服务。最终,它会调用一个如下所示的 retrieveSKU 函数

代码

src/inventory-service.ts

static async retrieveSKU(_productId: number): Promise<IProduct> {
    /**
    Get current Quantity of a Product.

    :param _productId: Product Id
    :return: Product with Quantity
    */
    const repository = ProductRepo.getRepository();
    let retItem: IProduct = {};

    if (repository && _productId) {
        //fetch product by ID (using redis om library)
        const product = <IProduct>await repository.fetch(_productId.toString());

        if (product) {
            retItem = {
                sku: product.sku,
                name: product.name,
                type: product.type,
                totalQuantity: product.totalQuantity
            }
        }
        else {
            throw `Product with Id ${_productId} not found`;
        }
    }
    else {
        throw `Input params failed !`;
    }

    return retItem;
}

#UpdateSKU

以下代码展示了 updateSKU 活动的 API 请求和响应示例。

updateSKU API 请求

POST http://localhost:3000/api/updateSKU
{
    "sku":1019688,
    "quantity":25
}

updateSKU API 响应

{
  "data": {
    "sku": 1019688,
    "name": "5-Year Protection Plan - Geek Squad",
    "type": "BlackTie",
    "totalQuantity": 25 //updated value
  },
  "error": null
}

发出请求时,它通过 API 网关到达库存服务。最终,它会调用一个如下所示的 updateSKU 函数

src/inventory-service.ts
 static async updateSKU(_productId: number, _quantity: number): Promise<IProduct> {
        /**
        Set Quantity of a Product.

        :param _productId: Product Id
        :param _quantity: new quantity
        :return: Product with Quantity
        */
        const repository = ProductRepo.getRepository();
        let retItem: IProduct = {};

        if (repository && _productId && _quantity >= 0) {
            //fetch product by ID (using redis om library)
            const product = <IProduct>await repository.fetch(_productId.toString());

            if (product) {
                //update the product fields
                product.totalQuantity = _quantity;

                // save the modified product
                const savedItem = <IProduct>await repository.save(<RedisEntity>product);

                retItem = {
                    sku: savedItem.sku,
                    name: savedItem.name,
                    type: savedItem.type,
                    totalQuantity: savedItem.totalQuantity
                }
            }
            else {
                throw `Product with Id ${_productId} not found`;
            }
        }
        else {
            throw `Input params failed !`;
        }

        return retItem;
    }

#IncrementSKU

以下代码展示了 incrementSKU 活动的 API 请求和响应示例。

incrementSKU API 请求

POST http://localhost:3000/api/incrementSKU
{
    "sku":1019688,
    "quantity":2
}

incrementSKU API 响应

{
  "data": {
    "sku": 1019688,
    "name": "5-Year Protection Plan - Geek Squad",
    "type": "BlackTie",
    "totalQuantity": 12 //previous value 10
  },
  "error": null
}

发出请求时,它通过 API 网关到达库存服务。最终,它会调用一个如下所示的 incrementSKU 函数

src/inventory-service.ts
static async incrementSKU(_productId: number, _incrQuantity: number, _isDecrement: boolean, _isReturnProduct: boolean): Promise<IProduct> {
        /**
        increment quantity of a Product.

        :param _productId: Product Id
        :param _incrQuantity: new increment quantity
        :return: Product with Quantity
        */

        const redisOmClient = getRedisOmClient();
        let retItem: IProduct = {};

        if (!_incrQuantity) {
            _incrQuantity = 1;
        }
        if (_isDecrement) {
            _incrQuantity = _incrQuantity * -1;
        }
        if (redisOmClient && _productId && _incrQuantity) {

            const updateKey = `${ProductRepo.PRODUCT_KEY_PREFIX}:${_productId}`;

            //increment json number field by specific (positive/ negative) value
            await redisOmClient.redis?.json.numIncrBy(updateKey, '$.totalQuantity', _incrQuantity);

            if (_isReturnProduct) {
                retItem = await InventoryServiceCls.retrieveSKU(_productId);
            }

        }
        else {
            throw `Input params failed !`;
        }

        return retItem;
    }

#DecrementSKU

以下代码展示了 decrementSKU 活动的 API 请求和响应示例。

decrementSKU API 请求

POST http://localhost:3000/api/decrementSKU
{
    "sku":1019688,
    "quantity":4
}

decrementSKU API 响应

{
  "data": {
    "sku": 1019688,
    "name": "5-Year Protection Plan - Geek Squad",
    "type": "BlackTie",
    "totalQuantity": 16 //previous value 20
  },
  "error": null
}

发出请求时,它通过 API 网关到达库存服务。最终,它会调用一个如下所示的 decrementSKU 函数

src/inventory-service.ts
static async decrementSKU(_productId: number, _decrQuantity: number): Promise<IProduct> {
        /**
        decrement quantity of a Product.

        :param _productId: Product Id
        :param _decrQuantity: new decrement quantity
        :return: Product with Quantity
        */
        let retItem: IProduct = {};

        //validating if product in stock
        let isValid = await InventoryServiceCls.validateQuantityOnDecrementSKU(_productId, _decrQuantity);

        if (isValid) {
            const isDecrement = true; //increments with negative value
            const isReturnProduct = true;
            retItem = await InventoryServiceCls.incrementSKU(_productId, _decrQuantity, isDecrement, isReturnProduct);
        }

        return retItem;
    }

    static async validateQuantityOnDecrementSKU(_productId: number, _decrQuantity?: number): Promise<boolean> {
        let isValid = false;

        if (!_decrQuantity) {
            _decrQuantity = 1;
        }

        if (_productId) {
            const product = await InventoryServiceCls.retrieveSKU(_productId);
            if (product && product.totalQuantity && product.totalQuantity > 0
                && (product.totalQuantity - _decrQuantity >= 0)) {

                isValid = true;
            }
            else {
                throw `For product with Id ${_productId},  available quantity(${product.totalQuantity}) is lesser than decrement quantity(${_decrQuantity})`;
            }

        }
        return isValid;
    }

#RetrieveManySKUs

以下代码展示了 retrieveManySKUs 活动的 API 请求和响应示例。

retrieveManySKUs API 请求

POST http://localhost:3000/api/retrieveManySKUs
[{
    "sku":1019688
},{
    "sku":1003622
},{
    "sku":1006702
}]

retrieveManySKUs API 响应

{
  "data": [
    {
      "sku": 1019688,
      "name": "5-Year Protection Plan - Geek Squad",
      "type": "BlackTie",
      "totalQuantity": 24
    },
    {
      "sku": 1003622,
      "name": "Aquarius - Fender Stratocaster 1,000-Piece Jigsaw Puzzle - Black/Red/White/Yellow/Green/Orange/Blue",
      "type": "HardGood",
      "totalQuantity": 10
    },
    {
      "sku": 1006702,
      "name": "Clash of the Titans [DVD] [2010]",
      "type": "Movie",
      "totalQuantity": 10
    }
  ],
  "error": null
}

发出请求时,它通过 API 网关到达库存服务。最终,它会调用一个如下所示的 retrieveManySKUs 函数

src/inventory-service.ts
static async retrieveManySKUs(_productWithIds: IProductBodyFilter[]): Promise<IProduct[]> {
        /**
        Get current Quantity of specific Products.

        :param _productWithIds: Product list with Id
        :return: Product list
        */
        const repository = ProductRepo.getRepository();
        let retItems: IProduct[] = [];

        if (repository && _productWithIds && _productWithIds.length) {

            //string id array
            const idArr = _productWithIds.map((product) => {
                return product.sku?.toString() || ""
            });

           //fetch products by IDs (using redis om library)
            const result = await repository.fetch(...idArr);

            let productsArr: IProduct[] = [];

            if (idArr.length == 1) {
                productsArr = [<IProduct>result];
            }
            else {
                productsArr = <IProduct[]>result;
            }

            if (productsArr && productsArr.length) {

                retItems = productsArr.map((product) => {
                    return {
                        sku: product.sku,
                        name: product.name,
                        type: product.type,
                        totalQuantity: product.totalQuantity
                    }
                });
            }
            else {
                throw `No products found !`;
            }
        }
        else {
            throw `Input params failed !`;
        }

        return retItems;
    }

#DecrementManySKUs

以下代码展示了 decrementManySKUs 活动的 API 请求和响应示例。

decrementManySKUs API 请求

POST http://localhost:3000/api/decrementManySKUs
[{
    "sku":1019688,
    "quantity":4
},{
    "sku":1003622,
     "quantity":2
},{
    "sku":1006702,
    "quantity":2
}]

decrementManySKUs API 响应

{
  "data": [
    {
      "sku": 1019688,
      "name": "5-Year Protection Plan - Geek Squad",
      "type": "BlackTie",
      "totalQuantity": 28 //previous value 32
    },
    {
      "sku": 1003622,
      "name": "Aquarius - Fender Stratocaster 1,000-Piece Jigsaw Puzzle - Black/Red/White/Yellow/Green/Orange/Blue",
      "type": "HardGood",
      "totalQuantity": 8 //previous value 10
    },
    {
      "sku": 1006702,
      "name": "Clash of the Titans [DVD] [2010]",
      "type": "Movie",
      "totalQuantity": 8 //previous value 10
    }
  ],
  "error": null
}

发出请求时,它通过 API 网关到达库存服务。最终,它会调用一个如下所示的 decrementManySKUs 函数

src/inventory-service.ts
 static async decrementManySKUs(_productsFilter: IProductBodyFilter[]): Promise<IProduct[]> {
        /**
        decrement quantity  of specific Products.

        :param _productWithIds: Product list with Id
        :return: Product list
        */
        let retItems: IProduct[] = [];

        if (_productsFilter && _productsFilter.length) {
            //validation only
            const promArr: Promise<boolean>[] = [];
            for (let p of _productsFilter) {
                if (p.sku) {
                  //validating if all products in stock
                    const promObj = InventoryServiceCls.validateQuantityOnDecrementSKU(p.sku, p.quantity);
                    promArr.push(promObj)
                }
            }
            await Promise.all(promArr);

            //decrement only
            const promArr2: Promise<IProduct>[] = [];
            for (let p of _productsFilter) {
                if (p.sku && p.quantity) {
                    const isDecrement = true; //increments with negative value
                    const isReturnProduct = false;
                    const promObj2 = InventoryServiceCls.incrementSKU(p.sku, p.quantity, isDecrement, isReturnProduct);
                    promArr2.push(promObj2)
                }
            }
            await Promise.all(promArr2);


            //retrieve updated products
            retItems = await InventoryServiceCls.retrieveManySKUs(_productsFilter);
        }
        else {
            throw `Input params failed !`;
        }

        return retItems;
    }

#准备好在实时库存系统中使用 Redis 了吗?

希望本教程能帮助您了解如何在实时库存系统中使用 Redis 来显示不同地点门店的产品可用性。如需了解与此主题相关的其他资源,请查看以下链接

#其他资源

使用 Redis 实现实时库存

通用