ReactiveDB icon indicating copy to clipboard operation
ReactiveDB copied to clipboard

乐观更新

Open Saviio opened this issue 7 years ago • 5 comments

双休日在知乎上和朋友讨论这个问题时想到的一个对乐观更新的思考。

客户端有时会需要这样一种场景的支持: 前端会发起一个数据 C/U/D 的操作,但需求是在远端确认之前将率先修改后的变化响应在客户端界面。

之前考虑这个问题时,就觉得操作更新的rollback似乎很难做,原因有二:

  1. 如果要回滚,那 rollback handler 里必须有数据整行快照 (有潜在的实现成本,亦可能不能被实现)
  2. 可能需要引入数据的版本控制。 设:有两个请求,第一个失败,第二个成功(先返回),那必须有机制保证 rollback 不会重置第二个请求的成功更新

和朋友讨论的时候又有了一个粗糙的想法:

我们可以再真正的存储层前再做一层预写入的抽象。方案可以是这样,考虑到我们所有客户端缓存资源一定存在一个id,如果某一个资源在远端尝试更新而本地需要立刻响应时,就将该次修改行为 map 成一个 patch action 放进这个预写层 (可以是一个 Rx.Subject),这个 patch action 保存了对状态的修改,并且支持将自己转换成一个 Lovefield C/U/D Query。同时在这之后所有对符合 id 资源的 C/U/D 操作 (例如 socket 的推送) 全都 proxy 到预写层,然后把预写层的改动投影到每个查询上,不断重放,直到远端响应了第一个操作的结果,然后把所有累积的操作结果做一次性写入。

伪码大致如下:

if (OptimisticCache.length > 0) {
  let action = OptimisticCache.peek(id)
  while (action && action.acquired) {
    lf.exec(action.toQuery())
    action = OptimisticCache.peek(id)
  }
}

当然这个方案还有一些问题需要解决:

  1. 如何设计 patch action。
  2. 如何为每个已存在的查询接口过滤不必要的预写层的数据修改的推送。
  3. 由于联表查询会被转换成一个嵌套的结构体,如果要重放 patch action 则必然会导致对结构不断遍历,有性能担忧。
  4. 接口的消费者会收到两次推送,第一次是预写层的变化的 push,第二次是 lovefield observer 的 push,但其实2次推送出的结果应该是 deep equal 的。

但乐观一些的话,预写层里的数据生命周期应该并不会太长,静态场景下预写层的 length 应该为 0。

途做草稿,暂时先记录一下。@suyu34 , @Miloas , @bjmin , @zry656565 , @chuan6

ref: https://github.com/teambition/teambition-sdk/issues/269

Saviio avatar Jul 10 '17 03:07 Saviio

通过 RDB 解决这个问题看上去还是蛮有趣的!我想了想,基于状态做多次操作的依赖检查也许是可行的。

zry656565 avatar Jul 10 '17 04:07 zry656565

这个方式以前的确考虑过,在 patch 策略下有个我没有想出解决方案的问题是,patch 对现有查询的影响怎么做。比如从列表删除条目这样一个乐观更新,实际 db 里面还有数据,但是界面要求不能有数据,也就是会对查询结果再进行一次 patch

joooye34 avatar Jul 10 '17 05:07 joooye34

我的想法上面已经给出了,所有的查询在从get接口输出的时候自动combineLatest OptimisticCache, 并且尝试封装出一层 filter 的逻辑,然后就可以根据 action type 从 db 里的资源过滤掉那些已经乐观删除的资源了?

Saviio avatar Jul 10 '17 07:07 Saviio

噢 原来如此 对 db api 理解还是不到位

joooye34 avatar Jul 10 '17 07:07 joooye34

我只是假想的,也还没有具体调研过可行性....

Saviio avatar Jul 10 '17 07:07 Saviio