【问题标题】:Transaction processing - Lookup millions of documents to validate transactions - Cost effective approach交易处理 - 查找数百万个文档以验证交易 - 经济高效的方法
【发布时间】:2022-08-21 19:57:18
【问题描述】:

我们最近从关系型转向 NoSQL(Couchbase)。我们正面临着扩展我们的后端以处理越来越多的用户(代表)的挑战。我们的一位产品大师拥有超过 50+ 百万份文档,并且每天修改超过一百万份文档。每天处理的交易很少,其中包含产品详细信息,大约处理了 100,000 笔此类交易,其中包含大约 50 种产品。需要检查这些产品是否在产品主文件中可用,这是对 Couchbase 中 50+ 百万个文档的验证。

我们现有的查找 50+ 百万个文档的方法不具有成本效益,并且增加了我们服务的整体运行时间。我们正在探索在本地缓存此操作的选项,或者寻找一种替代方法以更有效的方式执行此操作。

这是我们现有数据模型的概述。 数据结构 1 – 授权代表的产品

RepID ProductId Prod Start Dt Prod End Dt
1001 1 2022-05-15 9999-12-25
1001 2 2022-05-15 9999-12-25
1001 3 2022-05-15 9999-12-25
1001 4 2022-05-15 9999-12-25
1002 1 2022-05-15 9999-12-25
1002 2 2022-05-15 9999-12-25
1002 5 2022-05-15 9999-12-25
1002 6 2022-05-15 9999-12-25
1003 1 2022-05-15 9999-12-25
1003 2 2022-05-15 9999-12-25
1003 8 2022-05-15 9999-12-25
1003 9 2022-05-15 9999-12-25

同样,15,000 名代表平均拥有 3000-4000 个文档,总共 5000 万个文档。

交易文件模型

Order RepID ProductId
1234 1001 1
1234 1001 2
1234 1001 3
1234 1001 4
1234 1001 5
100 1002 1
100 1002 2
100 1002 3
1111 1003 1
1111 1003 2
1111 1003 3
1111 1003 4

在上面的示例中,rep 1001 无权销售产品 5。rep 1002 无权销售产品 3,rep 1003 无权销售产品 3,4。 与授权产品的数百万文档相比,在订单交易中验证这些产品的最有效方法是什么 建议的一种方法是使用带有代表列表的 HashMap,然后为每个代表的授权产品使用另一个映射。这将是内存缓存,并在将新产品添加到产品主控时进行更新。

建议的第二种方法是使用 GraphQL 或 Postgres SQL,因为它可能对这种操作有效。 这是一个简单的问题,唯一的挑战是以经济高效的方式完成此任务,而无需多次 DB 调用,甚至在处理事务时通过将此详细信息保存在内存中来消除所有 DB 调用。如果已经解决过类似问题的人可以分享他们在技术堆栈和使用的工具方面的经验,我将不胜感激。

  • 如果您使用的查询语言对操作的成本效益有任何影响,我会感到非常惊讶。我认为这类问题的经典答案是“索引”和“分片”

标签: python postgresql graphql hashmap couchbase


【解决方案1】:

您可以尝试考虑使用利用 Couchbase 事件的纯 KV 解决方案。我想您会发现,这种实时解决方案本质上是对 lambda 或触发器进行编码,在数以百万计的文档中表现非常出色。为此,我将在下面给出一个具体的例子:

您似乎有两种类型的文件

首先,这里的销售代表授权列表本质上是您作为 JSON 文档的数据

  KEY  auth:1003 
  {
    "Products": [
      {
        "EndDt": "9999-12-25",
        "ProductId": 1,
        "StartDt": "2022-05-15"
      },
      {
        "EndDt": "9999-12-25",
        "ProductId": 2,
        "StartDt": "2022-05-15"
      },
      {
        "EndDt": "9999-12-25",
        "ProductId": 8,
        "StartDt": "2022-05-15"
      },
      {
        "EndDt": "9999-12-25",
        "ProductId": 9,
        "StartDt": "2022-05-15"
      }
    ],
    "id": 1003,
    "type": "auth"
  }


  KEY  auth:1002 
  {
    "Products": [
      {
        "EndDt": "9999-12-25",
        "ProductId": 1,
        "StartDt": "2022-05-15"
      },
      {
        "EndDt": "9999-12-25",
        "ProductId": 2,
        "StartDt": "2022-05-15"
      },
      {
        "EndDt": "9999-12-25",
        "ProductId": 5,
        "StartDt": "2022-05-15"
      },
      {
        "EndDt": "9999-12-25",
        "ProductId": 6,
        "StartDt": "2022-05-15"
      }
    ],
    "id": 1002,
    "type": "auth"
  }

  KEY  auth:1001 
  {
    "Products": [
      {
        "EndDt": "9999-12-25",
        "ProductId": 1,
        "StartDt": "2022-05-15"
      },
      {
        "EndDt": "9999-12-25",
        "ProductId": 2,
        "StartDt": "2022-05-15"
      },
      {
        "EndDt": "9999-12-25",
        "ProductId": 3,
        "StartDt": "2022-05-15"
      },
      {
        "EndDt": "9999-12-25",
        "ProductId": 4,
        "StartDt": "2022-05-15"
      }
    ],
    "id": 1001,
    "type": "auth"
  }

其次,您要在这里验证的一堆订单本质上是您作为 JSON 文档的数据(我冒昧地添加了一个以获得成功)

  KEY: order:1234
  {
    "ProductIds": [
      1,2,3,4,5
    ],
    "RepID": 1001,
    "id": 1234,
    "type": "order"
  }

  KEY: order:1111
  {
    "ProductIds": [
      1,2,3,4
    ],
    "RepID": 1003,
    "id": 1111,
    "type": "order"
  }

  KEY: order:2222
  {
    "ProductIds": [
      8,9
    ],
    "RepID": 1003,
    "id": 2222,
    "type": "order"
  }

  KEY: order:100
  {
    "ProductIds": [
      1,2,3
    ],
    "RepID": 1002,
    "id": 100,
    "type": "order"
  }

现在这是一个事件函数(它将在 6.X 和 7.X 模式下运行,尽管如果您利用存储桶支持的缓存,7.X 会快得多)

// Need two buckets (if 7.0+ keyspaces of _default._default)
//     "eventing"
//     "data"
// Need one bucket binding 
//     alias = src_col bucket = data mode = r+w
// For performance set workers to 2X VCPUs for large data sets
// or for very fast mutation rates.

function OnUpdate(doc, meta) {
    // only process and validate orders (might add more filters here).
    if (doc.type !== "order") return;
    
    // level 1 is what you want, else to look at issue just raise the #
    var DEBUG = 1;
    // Use bucket backed caching to speed up loading of check document by 25X
    var VERSION_AT_702 = false;

    if (DEBUG > 1) log("checking order", meta.id);
    
    // load the rep's authorized products fromthe bucket binding.
    
    var auths;
    if (VERSION_AT_702 == false) {
        auths = src_col["auth:" + doc.RepID];
    } else {
        // use bucket backed caching.  Will only read KV at most once per 
        // second per each Eventing node. Costs  just 1/25th of a std. Bucket Op.
        var result = couchbase.get(src_col,{"id": "auth:" + doc.RepID}, {"cache": true});
        if (!result.success) {
            auths = null;
        } else {
            auths = result.doc;
        }
    }
    if (!auths) {
        if (DEBUG > 0) log("no auth record found for RepID", doc.RepID);
        return;
    }
    if (DEBUG > 4) log(auths);
    
    // since I save the lists this isn't an optimal check
    var is_authed = [];
    var is_not_authed = [];
    // now make sure the rep is authorized to sell all products
    for (var k = 0; k < doc.ProductIds.length; k++){
        var prod = doc.ProductIds[k];
        if (DEBUG > 1) log("checking product",prod);
        var okay = false;
        for (var j = 0; j < auths.Products.length; j++){
            var auth = auths.Products[j];
            if (DEBUG > 6) log("\t1.",auth);
            if (auth.ProductId == prod) {
                if (DEBUG > 8) log("\t\t2.",auth.ProductId," === ", prod, "GOOD");
                okay = true;
            } else {
                if (DEBUG > 8) log("\t\t2.",auth.ProductId," === ", prod, "BAD");
            }
        }
        if (okay === false) {
            is_not_authed.push(prod);
        } else {
            is_authed.push(prod);
        }
        if (DEBUG > 5) log("prod",prod,"authed",okay);
    }
    
    // =====================================================
    // we have an issue id is_not_authed.length > 0 
    //======================================================
    if (is_not_authed.length > 0) {
            if (DEBUG > 0) log("BAD illegal order", meta.id, "rep", doc.RepID, "can sell products", is_authed, "but can't sell products", is_not_authed);
    } else {
            if (DEBUG > 0) log("VALID legal order", meta.id, "rep", doc.RepID, "can sell products", is_authed);
    }
    // =====================================================
    // we could move the document or modify it but that's
    // you business logic.  Typically we might do something like:
    // 1. update the document with a new tag.
    //     doc.verify_status = (is_not_authed.length == 0)
    //     src_col[meta.id] = doc;
    // 2. at the top of the Function add another filter to 
    //    prevent redoing the same work.
    //     if (doc.verify_status) return;
    //======================================================    
}

针对上述数据运行上述事件函数,我得到以下日志消息。

2022-08-03T19:14:50.936+00:00 [INFO] "BAD illegal order" "order:1111" "rep" 1003 "can sell products" [1,2] "but can't sell products" [3,4] 

2022-08-03T19:14:50.848+00:00 [INFO] "BAD illegal order" "order:100" "rep" 1002 "can sell products" [1,2] "but can't sell products" [3] 

2022-08-03T19:14:50.812+00:00 [INFO] "VALID legal order" "order:2222" "rep" 1003 "can sell products" [8,9] 

2022-08-03T19:14:50.797+00:00 [INFO] "BAD illegal order" "order:1234" "rep" 1001 "can sell products" [1,2,3,4] "but can't sell products" [5] 

当然,除了记录消息之外,您还想做一些事情,也许您想移动文档,添加或更新文档中的属性,或者在您使用纯 JavaScript 并通过 KV(或数据服务)访问您的Couchbase 中的数据。

请注意,在上面的代码中,我保留了哪些“可以”和“不能”出售的列表,但是如果您不需要,您可以通过中断优化循环(JavaScript v8 很快)但我确实看到了您的规模效率是关键。

也许将 Products 分成三个数组,然后您可以执行以下操作:

KEY  auth:1001 
{
  "id": 1001,
   "type": "auth",
  "Product": [ 1, 2, 3, 4 ],
  "StartDt": [ "2022-05-15", "2022-05-15", "2022-05-15", "2022-05-15" ],
  "StartDt": [ "9999-12-25", "9999-05-15", "9999-12-25", "9999-12-25" ]
}

消除for循环:

const includesAll = (arr, values) => values.every(v => arr.includes(v));
log(meta.id,includesAll(auths.Product, doc.ProductIds));

如果交集“工作”太长,请查看 FastBitSet.js 之类的内容以缩短分析时间。

提高性能的最简单方法是启用存储桶支持的缓存(需要 7.0.2 或更高版本),但是如果您没有重用,这将无济于事。顺便说一句,发出日志消息也会减慢速度,因此请避免这种情况。

恕我直言,您应该能够在小型集群上处理 100K 文档/秒,在大型调谐集群上处理高达 1M 文档/秒。

如果您不熟悉 Eventing Service,您应该先运行几个 "step by step" examples 以获得基本了解。

如果由于某种原因您需要更高的性能(我认为您不会),我可以分享一些高级的三项赛技巧来加快速度,即使没有 - 只需 DM 我,我们会安排一些时间来谈谈。

【讨论】:

  • 仅供参考,我在 100 万个产品中加载了 2000 万个“订单”(随机选择 50 个产品的平均值)和 15000 个销售代表(授权销售平均 3500 个随机选择的产品)。根据您的规格 1)基本简单的实施我处理了 5000 个订单/秒,以及 2) 在 Eventing Service 中使用一些“魔术/技巧”实现了 216K 订单/秒。在一 (1) 个节点 Couchbase 服务器上。
  • 感谢您的输入乔恩。我们的 Couchbase 架构师提出了类似的方法,但我们放弃了它,因为我们决定不在存在这些文档的存储桶中使用事件处理。我将由他运行您的解决方案。再次感谢您的回复。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多