错误:10已终止:在这些文档上的争用太多。请重试

时间:2018-09-14 22:34:12

标签: javascript node.js firebase google-cloud-firestore

此错误是什么意思?

特别是什么意思:请重试

这是否意味着事务失败,我必须手动重新运行事务? 根据我对文档的了解,

  

该交易读取了在   交易。在这种情况下,交易自动再次运行。   重试该交易次数。

如果是,请问哪些文件? 该错误并不表示它正在谈论哪个文档。我刚得到这个堆栈:

  

{错误:10已终止:在这些文档上的争用太多。请   再试一次。       在Object.exports.createStatusErrornode_modules \ grpc \ src \ common.js:87:15)       在ClientReadableStream._emitStatusIfDone \ node_modules \ grpc \ src \ client.js:235:26处)       在ClientReadableStream._receiveStatus \ node_modules \ grpc \ src \ client.js:213:8)       在Object.onReceiveStatus \ node_modules \ grpc \ src \ client_interceptors.js:1256:15)       在InterceptingListener._callNext node_modules \ grpc \ src \ client_interceptors.js:564:42)       在InterceptingListener.onReceiveStatus \ node_modules \ grpc \ src \ client_interceptors.js:614:8)       在C:\ Users \ Tolotra Samuel \ PhpstormProjects \ CryptOcean \ node_modules \ grpc \ src \ client_interceptors.js:1019:24   代码:10,元数据:元数据{_internal_repr:{}​​},详细信息:“太   这些文件上有很多争论。请再试一遍。' }

要重新创建此错误,只需按照documentation

所示对db.runTransaction方法运行一个for循环。

5 个答案:

答案 0 :(得分:3)

我们遇到了与Firebase Firestore数据库相同的问题。即使是只有不到30件商品的小型柜台,也遇到了这个问题。

我们的解决方案不是分配计数器,而是增加交易的尝试次数,并为这些重试增加延迟时间。

第一步是保存事务操作,因为可以将const witch传递给另一个函数。

#!/usr/bin/env ipython
# ---------------------
l = [
         [0, 0, 0, 0, 0, 0, 0, 0, 0],
         [0, 2, 1, 1, 0, 1, 1, 1, 0],
         [0, 1, 0, 1, 0, 0, 0, 1, 0],
         [0, 1, 0, 1, 1, 1, 0, 1, 0],
         [0, 1, 0, 0, 0, 1, 0, 1, 0],
         [0, 1, 1, 1, 0, 1, 0, 1, 0],
         [0, 0, 0, 1, 0, 1, 0, 1, 0],
         [0, 1, 1, 1, 0, 1, 0, 1, 0],
         [0, 1, 0, 0, 0, 0, 0, 1, 0],
         [0, 1, 0, 1, 1, 1, 0, 1, 0],
         [0, 1, 0, 1, 0, 1, 0, 1, 0],
         [0, 1, 1, 1, 0, 1, 1, 4, 0],
         [0, 0, 0, 0, 0, 0, 0, 0, 0]
    ]
# ----------------------------------
def search(value,listin):
    coords = [[ival,kkval] for ival,dd in enumerate(listin) for kkval,val in enumerate(dd) if val==value]
    return coords
# ----------------------------------
result = search(4,l)
print result

第二步是创建两个辅助函数。一个用于等待指定的时间,另一个用于运行事务并捕获错误。如果发生代码10的中止错误,我们将再次运行该事务以进行特定的重试次数。

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">

    <!-- Font for glyphicons -->
    <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">

    <!-- Own CSS styles -->
    <link rel="stylesheet" type="text/css" href="styles.css">
   
    <nav class="navbar navbar-dark bg-dark navbar-expand-lg">
        <a class="navbar-brand" href="#">Title</a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>

        <div class="collapse navbar-collapse">
            <ul class="navbar-nav mr-auto">

            </ul>
            <form class="form-inline my-2 my-lg-0">
                <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
                <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
            </form>
            <ul class="navbar-nav ml-auto">
                <li class="nav-item">
                    <a class="nav-link" href="#"><i class="fa fa-user-o" aria-hidden="true"></i></a>
                </li>
            </ul>
        </div>
    </nav>

    <div class="container">
        <h1>Content</h1>
    </div>

    <footer class="footer">
        <div class="container">
            <span class="text-muted">Title, 2018</span>
        </div>
    </footer>

    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>

现在,我们有了所有需要的东西,我们可以使用const taskCountTransaction = async transaction => { const taskDoc = await transaction.get(taskRef) if (taskDoc.exists) { let increment = 0 if (change.after.exists && !change.before.exists) { increment = 1 } else if (!change.after.exists && change.before.exists) { increment = -1 } let newCount = (taskDoc.data()['itemsCount'] || 0) + increment return await transaction.update(taskRef, { itemsCount: newCount > 0 ? newCount : 0 }) } return null } 来调用我们的辅助函数,并且事务调用将比默认调用运行更长的时间,并且时间会有所延迟。

const wait = ms => { return new Promise(resolve => setTimeout(resolve, ms))}


const runTransaction = async (taskCountTransaction, retry = 0) => {
  try {
    await fs.runTransaction(taskCountTransaction)
    return null
  } catch (e) {
    console.warn(e)
    if (e.code === 10) {
      console.log(`Transaction abort error! Runing it again after ${retry} retries.`)

      if (retry < 4) {
        await wait(1000)
        return runTransaction(taskCountTransaction, ++retry)
      }
    }
  }
}

我对这种解决方案的满意之处在于,它并不意味着要编写更多或复杂的代码,并且大多数已编写的代码可以保持原样。仅当计数器达到必须计数更多项目的程度时,它才会使用更多时间和资源。其他时间和资源与您拥有默认事务的时间和资源相同。

要扩大规模,我们可以增加重试次数或等待时间。两者也都在影响Firebase的成本。对于等待的部分,我们还需要增加函数的超时时间。

免责声明:我没有对成千上万个项目进行压力测试。在我们的特定情况下,问题始于20多个项目,而一项任务最多需要50个项目。我用200个项目进行了测试,问题再没有出现。

答案 1 :(得分:0)

Firestore仅将事务重新运行有限次。在撰写本文时,this number is hard-coded as 5, and cannot be changed。为了避免在许多用户使用同一文档时出现拥塞/争用,通常我们使用指数退避算法(但这将导致事务处理花费更长的时间,在某些用例中可以接受)。

但是,截至撰写之时,this has not been implemented in the Firebase SDK yet-交易会立即重试。幸运的是,我们可以在事务中实现自己的指数补偿算法:

const createTransactionCollisionAvoider = () => {
  let attempts = 0
  return {
    async avoidCollision() {
      attempts++
      await require('delay')(Math.pow(2, attempts) * 1000 * Math.random())
    }
  }
}

...可以这样使用:

// Each time we run a transaction, create a collision avoider.
const collisionAvoider = createTransactionCollisionAvoider()
db.runTransaction(async transaction => {
  // At the very beginning of the transaction run,
  // introduce a random delay. The delay increases each time
  // the transaction has to be re-run.
  await collisionAvoider.avoidCollision()

  // The rest goes as normal.
  const doc = await transaction.get(...)
  // ...
  transaction.set(...)
})

注意:上面的示例可能会导致您的交易最多需要1.5分钟才能完成。这对我的用例来说很好。您可能需要针对用例调整退避算法。

答案 2 :(得分:0)

runTransaction code中找到了maxAttempts,应该修改5次默认尝试(但尚未测试)。

无论如何,我认为随机等待(最终加上队列)仍然是更好的选择。

答案 3 :(得分:0)

我实现了一个简单的退避解决方案,以共享:维护一个全局变量,该变量为每个失败的连接分配一个不同的“重试插槽”。例如,如果同时出现5个连接,而其中4个出现争用错误,则每个连接都会有500ms,1000ms,1500ms,2000ms的延迟,直到再次尝试。因此,它有可能全部同时解决,而不会引起更多争用。

我的交易是调用Firebase Functions的响应。每个功能计算机实例都可以具有一个全局变量nextRetrySlot,该变量将一直保留到关闭为止。因此,如果error.code === 10因争用问题而被捕获,则延迟时间可以为(nextRetrySlot + 1) * 500,例如,您可以选择nextRetrySlot = (nextRetrySlot + 1) % 10,以便下一个连接在500ms〜5000ms范围内获得不同的时间循环。 / p>

下面是一些基准测试:

我的情况是,我希望每个新的Firebase Auth注册都获得一个从唯一的Firebase UID派生的更短的ID,因此它有发生冲突的风险。

我的解决方案是简单地检查所有注册的短ID,如果查询返回什么,只需生成另一个,直到没有。然后,我们将此新的简短ID注册到数据库中。因此,该算法不能仅依赖Firebase UID,而是可以确定性的方式“移至下一个”。 (不仅是随机的)。

这是我的交易,它首先读取所有使用过的短ID的数据库,然后自动写入一个新ID,以防止极不可能的事件,即同时出现2个新寄存器,并且派生了另一个Firebase UID。相同的短ID,并且都看到该短ID同时是空的。

我运行了一个测试,该测试有意注册了20个不同的Firebase UID,这些UID都衍生为相同的短ID。 (极不可能的情况)所有这​​些同时运行。首先,我尝试在下一次重试中使用相同的延迟,因此我希望它会在缓慢解决一些连接问题时一次又一次地发生冲突。

  • 相同的500ms重试延迟时间:45000ms〜60000ms
  • 重试相同的1000ms延迟:30000ms〜49000ms
  • 重试相同的1500ms延迟:43000ms〜49000ms

然后在时隙中分配延迟时间:

  • 500ms *重试5个时隙:20000ms〜31000ms
  • 500ms *重试10个时隙:22000ms〜23000ms
  • 500ms *重试20个时隙:19000ms〜20000ms
  • 1000ms *重试5个时隙:〜29000ms
  • 1000ms *重试10个时隙:〜25000ms
  • 1000ms *重试20个时隙:〜26000ms

确认不同的延迟时间肯定有帮助。

答案 4 :(得分:0)

Firestore现在支持服务器端increment()decrement()原子操作。

您可以增加或减少任意数量。有关详细信息,请参见他们的blog post。在许多情况下,这将消除对客户端事务的需求。

示例:

document("fitness_teams/Team_1").
  updateData(["step_counter" : FieldValue.increment(500)])

这仍然限于每个文档1 QPS的持续写入限制,因此,如果需要更高的吞吐量,请考虑使用distributed counters。这将增加您的读取成本(因为您需要阅读所有分片文档并计算总数),但允许您通过增加分片数量来扩展吞吐量。现在,如果您确实需要在事务中增加计数器,则由于更新争用而导致失败的可能性大大降低。