使用REST API同步离线数据
在client-server模式中, 由于client并不总是处于有网络的状态下, 因此同步是一个重要的功能.
没有网络时存储在本地数据库, 有网络时再同步服务器和本地的数据.
### 一个不怎么优雅的同步方法
在某app早前一版的api设计中, 同步是这么设计的:
触发同步
curl /sync -X POST -d 'since=$time&list=\'[{"id":1,"field":1},{"id":2,"field":0}]\''
服务器应用客户端的增量变更
服务器返回$time后的增量数据
客户端应用服务器的增量变更
同步结束.
这个流程比较繁琐, 它要求服务器和客户端两边同时要有sync的逻辑.
### 一个优雅的同步方法
事实上, REST api提供的GET/POST/PUT/DELETE 4个接口既可以用于实时变更, 也可以用于离线同步.
这个流程是可以简化的.
我们可以简化到所有的同步逻辑都存在于客户端, 而服务器只需要提供CRUD接口.
要想做到这一点, 是有一些要求的.
首先, 服务器的存储数据要有这两个标识:
* 版本标识: 一般可以使用`update_time`.
* 软删除标识: 也叫`假删除`, 即设置一个`is_deleted`的二值属性, 删除操作并不真正删除, 而是设is_deleted=true.
客户端的存储数据除此之外, 还要一个 `dirty` flag, 用于标识数据在客户端是否已修改.
有了这三个值, 就可以保证顺利的使用REST api进行同步了.
大致的流程为:
* 获取服务器增量数据
* 应用到本地
* 本地回传更改到服务器
更详细的流程为:
获取服务器数据:
获取上次更新的时间(客户端实现 可以在本地动态配置或数据库中记录)
下载服务器自此时间点之后的变更(对应rest中 GET /collections?since=)
应用到本地:
检查是否本地已存在
不存在:
插入本地数据库
存在:
检查本地是否已修改
未修改:
检查软删除标识
True:
设置本地条目的is_deleted=true
False:
用服务器条目替换本地条目
已修改:
调用解决冲突的函数, 重设dirty flag
本地回传更改到服务器
获取本地的所有dirty flag为true的数据
如果需要新建(没有id):
POST /collections
如果需要修改:
PUT /collections/:id
如果需要删除:
DELETE /collections/:id
取回服务器该条目数据, 更新update_time, 重设dirty flag
设置本地的变更时间
可能会丢失同步期间的变更(一客户端本地回传时另一客户端发起了修改), 这可能需要定期跑一次全量更新(?)
### 如何解决冲突?
如果服务器和客户单同时修改条目, 冲突策略可以是:
+ 服务器为准, 这种策略比较常见. 即如果遇到冲突忽略客户端的修改.
+ 客户端为准
+ 用户解决: evernote就是少见的由用户自行解决冲突的案例.
具体采用哪种策略按照需求自行决定.
### demo
上述流程的demo简介:
server.py 提供了一系列rest接口:
+ GET /bananas
+ POST /bananas
+ PUT /bananas/:id
+ DELETE /bananas/:id
其中, GET /bananas支持使用参数`since`获取增量数据.
client.py 则模拟了app的行为: 既要维护本地的数据库, 又要支持数据同步; 既可以使用rest接口实时交互数据, 也可以延迟稍后同步.
Usage:
# requirements: flask, requests, pytest
git clone https://gist.github.com/8108066.git
cd 8108006
./run
[gist 8108066](https://gist.github.com/soasme/8108066)
参考:
http://havrl.blogspot.ie/2013/08/synchronization-algorithm-for.html?m=1
没有网络时存储在本地数据库, 有网络时再同步服务器和本地的数据.
### 一个不怎么优雅的同步方法
在某app早前一版的api设计中, 同步是这么设计的:
触发同步
curl /sync -X POST -d 'since=$time&list=\'[{"id":1,"field":1},{"id":2,"field":0}]\''
服务器应用客户端的增量变更
服务器返回$time后的增量数据
客户端应用服务器的增量变更
同步结束.
这个流程比较繁琐, 它要求服务器和客户端两边同时要有sync的逻辑.
### 一个优雅的同步方法
事实上, REST api提供的GET/POST/PUT/DELETE 4个接口既可以用于实时变更, 也可以用于离线同步.
这个流程是可以简化的.
我们可以简化到所有的同步逻辑都存在于客户端, 而服务器只需要提供CRUD接口.
要想做到这一点, 是有一些要求的.
首先, 服务器的存储数据要有这两个标识:
* 版本标识: 一般可以使用`update_time`.
* 软删除标识: 也叫`假删除`, 即设置一个`is_deleted`的二值属性, 删除操作并不真正删除, 而是设is_deleted=true.
客户端的存储数据除此之外, 还要一个 `dirty` flag, 用于标识数据在客户端是否已修改.
有了这三个值, 就可以保证顺利的使用REST api进行同步了.
大致的流程为:
* 获取服务器增量数据
* 应用到本地
* 本地回传更改到服务器
更详细的流程为:
获取服务器数据:
获取上次更新的时间(客户端实现 可以在本地动态配置或数据库中记录)
下载服务器自此时间点之后的变更(对应rest中 GET /collections?since=)
应用到本地:
检查是否本地已存在
不存在:
插入本地数据库
存在:
检查本地是否已修改
未修改:
检查软删除标识
True:
设置本地条目的is_deleted=true
False:
用服务器条目替换本地条目
已修改:
调用解决冲突的函数, 重设dirty flag
本地回传更改到服务器
获取本地的所有dirty flag为true的数据
如果需要新建(没有id):
POST /collections
如果需要修改:
PUT /collections/:id
如果需要删除:
DELETE /collections/:id
取回服务器该条目数据, 更新update_time, 重设dirty flag
设置本地的变更时间
可能会丢失同步期间的变更(一客户端本地回传时另一客户端发起了修改), 这可能需要定期跑一次全量更新(?)
### 如何解决冲突?
如果服务器和客户单同时修改条目, 冲突策略可以是:
+ 服务器为准, 这种策略比较常见. 即如果遇到冲突忽略客户端的修改.
+ 客户端为准
+ 用户解决: evernote就是少见的由用户自行解决冲突的案例.
具体采用哪种策略按照需求自行决定.
### demo
上述流程的demo简介:
server.py 提供了一系列rest接口:
+ GET /bananas
+ POST /bananas
+ PUT /bananas/:id
+ DELETE /bananas/:id
其中, GET /bananas支持使用参数`since`获取增量数据.
client.py 则模拟了app的行为: 既要维护本地的数据库, 又要支持数据同步; 既可以使用rest接口实时交互数据, 也可以延迟稍后同步.
Usage:
# requirements: flask, requests, pytest
git clone https://gist.github.com/8108066.git
cd 8108006
./run
[gist 8108066](https://gist.github.com/soasme/8108066)
参考:
http://havrl.blogspot.ie/2013/08/synchronization-algorithm-for.html?m=1