客户端和服务端的数据同步过程中,客户端有缓存,不需要每次都是全量刷新,所以可以采用增量的方式更新。
每次在客户端进行刷新的时候,服务端会将最新的增删改操作推送到客户端,客户端对其缓存进行操作,以保持数据的同步。
最原始的方法 - Full Transfer
[图片来自参考文献 1]
我们要实现的是数据的同步,那么我们只要每次在本地列表需要更新时,将所有后端数据库的信息拉取到本地,进行对比,如果有新增就新增,有修改就修改,如果新来的版本里面某一项不见了,就直接在本地缓存中删除该项即可。
优点:
- 写接口时很清爽,它是最简单的实现
缺点:
- 服务端发送多余的数据
- 只能用于小数据量,在数据量比较大的时候,单次刷新的操作就耗费很多网路流量。
应用场景:IMAP 协议,新闻类 APP 中的每日头条,如 Yahoo News Digest
在 CalDAV 日历协议中,每个日历事件都拥有一个 ETag 来标记它的版本信息,客户端对比服务器发来的 ETag 来决定要不要更新某项日历事件,此处的 ETag 和 HTTP headers 中 Last-Modified 和 ETag 其实是一样的东西,用于标记版本信息。这种方式即为接下来要提到的 Timestamp Transfer。
根据修改时间来拉取增删改信息 - Timestamp Transfer
[图片来自参考文献 1]
客户端存储上次拉取的数据的 Timestamp,在请求更新数据时,携带该 Timestamp 作为本地数据版本信息。数据库内每行数据设置一个 LAST_UPDATE_TIME 字段,服务器将比该时间更新的数据返回给客户端。
优点:
- 相对于 Full Transfer 来说减少了冗余数据的传输
缺点:
- 传输时 Timestamp 作为版本信息需要精确控制,请求错误的版本号可能带来本地数据的不准确
- 已经删除的数据其实已经不存在了,取不到 LAST_UPDATE_TIME
第二个缺点可以通过设置 IS_DELETE 字段来避免,每次删除数据时,仅仅更新 LAST_UPDATE_TIME 和设置 IS_DELETE 为 True 来标记已删除。此处带来的缺点是,被删除的数据继续占用空间,不过当只有一个客户端时,可以在客户端确认删除缓存中相应数据后删除数据库中 IS_DELETE 为 True 的数据,这个方法被成为 Soft Delete.
结合算法和修改时间来拉取增删改信息 - Mathematical Transfer
[图片来自参考文献 1]
服务器接收到客户端发来的更新请求时,将客户端根据 Reconciliation 算法生成的值来确定要返回给客户端的增删改信息。此处说的 Reconciliation 算法的作用与 Checksum 校验和类似,用于校验数据是否已经修改。
优点:
- 避免了查询数据库时对 LAST_UPDATE_TIME 的条件过滤
缺点:
- Reconciliation 算法普适性低
- Reconciliation 算法开发周期长
增删改日志 - SYNC
服务端记录数据的每次操作都记录进一个增量数据库,数据库内记录了每条操作的对象 ID 和操作的内容。此处思想类似于 Patch 补丁操作,客户端发送一个 Timestamp 信息,服务器将这个时间以后的所有增删改操作返回给客户端,客户端再进行打补丁操作,使得最终结果与服务端同步。
优点:
- 保持了所有数据的精确可同步
缺点:
- 客户端很久不更新以后单次的更新补丁很大
- 如果数据改动很多,那记录操作的表将会变得很大
场景
我们现在的需求是,有一个订单模块,某一用户在 APP 中可以点击刷新订单列表,将服务器上其所有订单显示出来。
# 订单更新操作
def order_update(request):
# 对其中一个订单做更新操作
order = Order.objects.get(id=1)
order.update_time = time.time()
order.save()
# 设置缓存内的版本信息
cache_time = time.time()
cache.set('order_list_' + str(request.user.id), cache_time, 3600 * 24 * 7)
return JsonResponse({})
# 返回订单列表
def order_list(request):
# 保存客户端的版本信息 since,即 timestamp
since = request.GET.get('since') or 0
since = float(str(since))
# 取出缓存中最新版本信息 since_cache
since_cache = cache.get('order_list_'+ str(request.user.id))
# 如果客户端携带版本信息为 0,说明客户端请求全量更新
# 或者当缓存中版本新于客户端版本信息,则返回 update_time 新于客户端版本的所有条目
if (since == 0) or since_cache and (since_cache > since):
orders = Order.objects.filter(user=request.user, update_time__gt=since)
data = [order.id for order in orders]
return JsonResponse({
'status': 1001,
'since': since_cache,
'data': data
})
# 没有更新,直接返回最新的缓存时间
else:
return JsonResponse({
'status': 1001,
'since': since_cache,
'data': []
})
筛选的情况
有时候我们只需要更新满足筛选条件的条目,客户端利用 Timestamp Transfer 来进行拉取所有增删改信息,但是只针对满足筛选条件的那些项目进行更新操作,其余的直接丢弃。
参考文献
- DATA SYNCHRONIZATION PATTERNS IN MOBILE APPLICATION DESIGN
- 两种增量更新方案
- RFC 4791 - CalDAV
- AppSync.org
- Evernote Synchronization via EDAM
- Object Synchronizer