增量更新模型的讨论

原创
2015/09/16 11:50
阅读数 4.5K

客户端和服务端的数据同步过程中,客户端有缓存,不需要每次都是全量刷新,所以可以采用增量的方式更新。

每次在客户端进行刷新的时候,服务端会将最新的增删改操作推送到客户端,客户端对其缓存进行操作,以保持数据的同步。

最原始的方法 - Full Transfer

Full Transfer

[图片来自参考文献 1]

我们要实现的是数据的同步,那么我们只要每次在本地列表需要更新时,将所有后端数据库的信息拉取到本地,进行对比,如果有新增就新增,有修改就修改,如果新来的版本里面某一项不见了,就直接在本地缓存中删除该项即可。

优点:

  • 写接口时很清爽,它是最简单的实现

缺点:

  • 服务端发送多余的数据
  • 只能用于小数据量,在数据量比较大的时候,单次刷新的操作就耗费很多网路流量。

应用场景:IMAP 协议,新闻类 APP 中的每日头条,如 Yahoo News Digest

在 CalDAV 日历协议中,每个日历事件都拥有一个 ETag 来标记它的版本信息,客户端对比服务器发来的 ETag 来决定要不要更新某项日历事件,此处的 ETag 和 HTTP headers 中 Last-Modified 和 ETag 其实是一样的东西,用于标记版本信息。这种方式即为接下来要提到的 Timestamp Transfer。

根据修改时间来拉取增删改信息 - 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

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 来进行拉取所有增删改信息,但是只针对满足筛选条件的那些项目进行更新操作,其余的直接丢弃。

参考文献

展开阅读全文
打赏
0
55 收藏
分享
加载中
有干货😙
2015/09/16 21:48
回复
举报

引用来自“宅男小何”的评论

如果client长时间不在线,突然登录client,这种情况在登录的时候全量拉取,而不是拉取这段时间的变更日志,逐步更新client的数据,client一致在线的时候可以用除了第一种方案以后的其他方案。

所以可以考虑这些方案的一个结合,避免你文章说的各个方案的缺点。

引用来自“xh4n3”的评论

没错,我们一开始确实是打算用推送来进行每次更新的,但是推送是不能百分百成功的,原因有二,一是推送失败,二是用户没有用推送消息进入 APP,所以就退化成了 APP 主动请求更新,可能是由推送,或者用户点击刷新,或者进入 APP 界面触发的。 用户重新安装或者清空缓存后确实是要进行全量拉取,用户长时间没有上线的情况下,也可以进行全量拉取。 但是当全量更新太巨量而增量更新也很大时,我们可以用参考文献二中所述的分段拉取更新数据。
是的,消息太多了,需要分成几次拉取的,呵呵!
2015/09/16 15:07
回复
举报
xh4n3博主

引用来自“宅男小何”的评论

如果client长时间不在线,突然登录client,这种情况在登录的时候全量拉取,而不是拉取这段时间的变更日志,逐步更新client的数据,client一致在线的时候可以用除了第一种方案以后的其他方案。

所以可以考虑这些方案的一个结合,避免你文章说的各个方案的缺点。
没错,我们一开始确实是打算用推送来进行每次更新的,但是推送是不能百分百成功的,原因有二,一是推送失败,二是用户没有用推送消息进入 APP,所以就退化成了 APP 主动请求更新,可能是由推送,或者用户点击刷新,或者进入 APP 界面触发的。 用户重新安装或者清空缓存后确实是要进行全量拉取,用户长时间没有上线的情况下,也可以进行全量拉取。 但是当全量更新太巨量而增量更新也很大时,我们可以用参考文献二中所述的分段拉取更新数据。
2015/09/16 14:35
回复
举报
如果client长时间不在线,突然登录client,这种情况在登录的时候全量拉取,而不是拉取这段时间的变更日志,逐步更新client的数据,client一致在线的时候可以用除了第一种方案以后的其他方案。

所以可以考虑这些方案的一个结合,避免你文章说的各个方案的缺点。
2015/09/16 14:05
回复
举报
简单的设计可以这样吧,优化下方案一,client拉取数据的时候,比对一个etag or timestramp ,有更新才全量拉取,这样可以减少带宽,设计也简单。
还有一种比较好的设计方案是:ws推送更新,有更新的时候直接推送给client,这样带宽小,实时。
2015/09/16 14:03
回复
举报
xh4n3博主

引用来自“i5ting”的评论

不错的

谢谢支持!
2015/09/16 13:39
回复
举报
不错的
2015/09/16 13:38
回复
举报
更多评论
打赏
7 评论
55 收藏
0
分享
返回顶部
顶部