乐观锁定并重新尝试

时间:2015-06-12 09:07:26

标签: optimistic-locking etcd

我不确定方法的正确设计。

我们使用乐观锁定,使用long增量版本放置在每个实体上。这种实体的每次更新都是通过比较和交换算法执行的,该算法只是成功或失败,这取决于其他客户端是否同时更新实体。经典乐观锁定例如hibernate做。

我们还需要采用重新尝试的方法。我们使用基于http的存储(etcd),并且可能会发生一些更新请求只是暂停了。

这就是问题所在。如何结合乐观锁定和重试。这是我面临的具体问题。

假设我有一个version=1的实体,我正在尝试更新它。下一个版本显然是2。我的客户端比执行条件更新。仅当持久性版本为1并且原子级更新为version=2时才会成功执行。到目前为止,非常好。

现在,假设更新请求的响应未到达。现在不可能说它是否成功。我现在唯一能做的就是再次重新尝试更新。在内存中,实体仍然包含version=1打算将值更新为2

现在出现了真正的问题。 如果第二次更新失败,因为持久性版本为2而不是1会怎样?

有两个可能的原因:

  1. 第一个请求确实导致了更新 - 操作成功但响应丢失或客户端超时,无论如何。它刚刚没有到达,但它通过了
  2. 其他一些客户端在后台同时执行了更新
  3. 现在我不能说什么是真的。我的客户端是否更新了实体或其他客户端?操作是通过还是失败?

    目前我们只使用比较持久化实体和主内存中的实体。无论是java等于还是json内容相等。如果它们相等,则更新方法被声明为成功。我对算法不满意,因为它对我来说既不便宜又合理。

    另一种可能的方法是不使用long版本,而是使用timestamp。每个客户端在更新操作中生成自己的时间戳,这意味着潜在的并发客户端将以高概率生成其他客户端。对我来说问题是概率,特别是当两个并发更新来自同一台机器时。

    还有其他解决方案吗?

2 个答案:

答案 0 :(得分:1)

恕我直言,由于etcd建立在HTTP本身就是一种不安全的协议上,因此很难有一个防弹解决方案。

经典SQL数据库使用连接的协议,事务和日记化,以允许用户确保整个事务处理完全提交或完全回滚,即使在最坏的情况下,在操作过程中断电。

因此,如果两个操作相互依赖(从一个银行账户转账到另一个银行账户),您可以确保两者都可以或没有,您可以在数据库中实现“操作”的日志即使您在提交过程中断开连接,也可以通过查阅期刊来查看是否通过了特定的状态。

但我无法想象etcd这样的解决方案。因此,除非其他人找到更好的方法,否则您将有两个选择

  • 在后端使用经典 SQL数据库,使用etcd(或等效的)作为简单缓存
  • 接受协议的弱点
BTW,我不认为代替长版本号的时间戳会强化系统,因为在高负载下,两个客户端事务使用相同时间戳的概率增加。也许你可以尝试添加一个唯一的id(客户端ID或只是技术uuid)到你的字段,当版本是n + 1时,只需比较增加它的UUID:如果是你的,那么交易如果没有id没有通过。

但是如果你现在可以读取版本,那么会出现更糟糕的问题,它不在n + 1但已经在n + 2。如果UUID是您的,您确定您的交易已通过,但如果不是,则无人能说。

答案 1 :(得分:1)

您可以使用两步协议伪造etcd中的交易。

更新算法:

第一阶段:将更新记录到etcd

  • 使用相当小的TTL添加“update-lock”节点。如果存在,请等到它消失并再试一次。
  • 为您的代码添加监视程序。如果执行后续步骤所需的时间超过锁定的TTL(或者如果您无法刷新它),则必须中止。
  • 使用[old,new]值添加“update-plan”节点。它的结构取决于您,但您需要确保在持有锁时复制旧值。
  • 添加“committed-update”节点。此时,您已经“原子地”更新了数据。

第二阶段:执行实际更新

  • 阅读“计划更新”节点并应用其描述的更改。
    • 如果更改失败,请验证新值是否存在。
    • 如果不是,那你就有一个重大问题。纾困。
  • 删除已提交的更新节点
  • 删除更新计划节点
  • 删除更新锁定节点

如果您想阅读一致的数据:

  • 虽然没有提交更新节点,但您的数据还可以。
  • 否则,请等待它被删除。

    • 只要存在commit-update但没有update-lock,就启动恢复。

事务恢复,如果您发现没有锁定的更新计划节点:

  • 获取更新锁定。
  • 如果没有已提交的更新节点,请删除该计划并释放锁定。
  • 否则,继续上面的“第二阶段”。