双休日在知乎上和朋友讨论这个问题时想到的一个对乐观更新的思考。
客户端有时会需要这样一种场景的支持:
前端会发起一个数据 C/U/D 的操作,但需求是在远端确认之前将率先修改后的变化响应在客户端界面。
之前考虑这个问题时,就觉得操作更新的rollback似乎很难做,原因有二:
- 如果要回滚,那 rollback handler 里必须有数据整行快照 (有潜在的实现成本,亦可能不能被实现)
- 可能需要引入数据的版本控制。 设:有两个请求,第一个失败,第二个成功(先返回),那必须有机制保证 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)
}
}
当然这个方案还有一些问题需要解决:
- 如何设计 patch action。
- 如何为每个已存在的查询接口过滤不必要的预写层的数据修改的推送。
- 由于联表查询会被转换成一个嵌套的结构体,如果要重放 patch action 则必然会导致对结构不断遍历,有性能担忧。
- 接口的消费者会收到两次推送,第一次是预写层的变化的 push,第二次是 lovefield observer 的 push,但其实2次推送出的结果应该是 deep equal 的。
但乐观一些的话,预写层里的数据生命周期应该并不会太长,静态场景下预写层的 length 应该为 0。
途做草稿,暂时先记录一下。@suyu34 , @Miloas , @bjmin , @zry656565 , @chuan6
ref:
teambition/teambition-sdk#269
双休日在知乎上和朋友讨论这个问题时想到的一个对乐观更新的思考。
客户端有时会需要这样一种场景的支持:
前端会发起一个数据 C/U/D 的操作,但需求是在远端确认之前将率先修改后的变化响应在客户端界面。
之前考虑这个问题时,就觉得操作更新的rollback似乎很难做,原因有二:
和朋友讨论的时候又有了一个粗糙的想法:
我们可以再真正的存储层前再做一层预写入的抽象。方案可以是这样,考虑到我们所有客户端缓存资源一定存在一个id,如果某一个资源在远端尝试更新而本地需要立刻响应时,就将该次修改行为 map 成一个 patch action 放进这个预写层 (可以是一个 Rx.Subject),这个 patch action 保存了对状态的修改,并且支持将自己转换成一个 Lovefield C/U/D Query。同时在这之后所有对符合 id 资源的 C/U/D 操作 (例如 socket 的推送) 全都 proxy 到预写层,然后把预写层的改动投影到每个查询上,不断重放,直到远端响应了第一个操作的结果,然后把所有累积的操作结果做一次性写入。
伪码大致如下:
当然这个方案还有一些问题需要解决:
但乐观一些的话,预写层里的数据生命周期应该并不会太长,静态场景下预写层的 length 应该为 0。
途做草稿,暂时先记录一下。@suyu34 , @Miloas , @bjmin , @zry656565 , @chuan6
ref:
teambition/teambition-sdk#269