teambition-sdk
teambition-sdk copied to clipboard
列表的冗余加载(预加载)模式
一直以来前端以无限滚动加载搭配后端数据分页会存在一个潜在的 bug,考虑以下场景:
前端存有分页数据: [a, b, c, d, e, f, g, h, i, j]
(pageSize = 10 & page = 1)
后端存有全集数据: [a, b, c, d ,e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t]
当前端删除某一个操作删除了第一页中的某一条(比如 [f] )再进行分页加载时,由于此时后端做分页分析时会将数据 [k] 补位至第一页,所以通过分页加载得到的 response 的第二页数据会变成[i, m , n, o, p, q, r, s, t],但很显然数据 [k] 始终不会出现的前端缓存中,整个数据集上会缺少一条数据。
之前 yn 曾经尝试解决这个问题,他的方案是通过缓存校验,每当发生操作时,对缓存数据长度做一个校验,如果发现已缓存的某一页数据长度不足时对该页再做一次请求以保证得到正确的分页信息。
但后来我考虑到,其实这个需求可以通过设计冗余加载 (prefetch) 的机制来解决,每次对页码 i 完成请求后,自动发起一个对页码 i + 1的请求, 该请求的返回值将只存入缓存,并不返回给 QueryToken 消费者。那么当前端发生对数据补位需求时,lovefield 会自动将已缓存的第二页的前 j 条数据补位至第一页以满足我们对这类场景的需求。( 另外我们似乎可以利用这个 prefetch 机制,更好的映射到前端对 hasMore 的需求?
@bjmin , @chuan6 , @Miloas 有别的参考意见吗?
@Saviio 这个问题之前 jinchang 找我提起过。刚才思考了下,觉得脑子里缺乏资源 @~@ 找到了这篇文章:https://www.sitepoint.com/paginating-real-time-data-cursor-based-pagination/ ,我先看再来参与讨论。。。
@chuan6 你引用文章里的实现要有后端全线API的支持,有cursor字段的话我自然是没必要做这个设计了,直接fetch cursor后面那个数据就好了(差量更新 & 颗粒度足够小),这个是最完美的解决方案,但是实现成本也相对高。
我大概想了一下. 按prefetch
这样搞对无删除或者低频删除的这种情况来说,请求数无缘无故加了一倍... 我更赞成yn的做法 当做缓存失效这种搞
Mmmm也有道理,在不删除的场景下确实是会无故多1倍,我再想想有没有更清晰的实现,因为这里缓存失效的颗粒度太大了,一个列表20条数据,删其中一条等于20条全都失效,代价似乎有些高,我是希望可以把颗粒度缩小。
另外么,prefetch也有好的结果,在无删除的场景下,翻页的响应可以足够快 :p, 因为i + 1页数据已经在本地了
@Saviio 嗯嗯,不过我也可以去问问后端是不是已经有提供这样机制的打算。。。
@Miloas 我对于通过校验,发现某一页内容个数改变时重新载入这一页,的机制有个疑问。假设是删除了这一页里面的一个条目,重新载入这一页,那它的最后一个元素不是与下一页的第一个元素重复吗?所以我感觉在一页里面删除一个条目,导致的不只是这一页变成 invalid,是包含当页以及随后所有页变成 invalid。不知道对不对@~@ 我的理解是我们这种场景下的 paging 是要求所有内容连续的,所以牵一发而动全身,well,下半身。。。(我记不清操作系统虚拟内存里 paging 机制的具体假设了,回去查查看,再来参与讨论)
所以我看到的是,如果说 prefetch 的方案的单个条目操作的放大效果是 pageSize 倍的话,校验重载的放大效果会是 pageSize * 随后页面数 倍。会不会?
有一种做法是通过since
来做, 客户端传当前页中最后一条记录的ID,然后查这条ID之后的数据进行分页。
{
_id: {
$gt: _sinceId
}
}
@leeqiang , 恩 since 其实就是 cursor based pagination 了,这样的话,我可以通过 sinceId + pageSize = 1 去 fetch 这条数据,但这个 feature 后端能支持到吗? 问了下国强,他好像说不行
@Saviio 我觉得是需要支持的,针对之后的新的list api
,我建议优先考虑这种形式的分页,毕竟通过page+count分页带来的另一个问题就是skip, 大数据量的情况下,skip会存在性能问题,随着数据量增大,skip会导致查询越来越慢.
(好在ObjectId是有序的)
// page + count
db.tasks.find(conds).skip((page - 1) * count).limit(count)
// since
db.tasks.find({ _id: { $gt: _sinceId } }).limit(count)
@leeqiang 所以之后是希望把分页里的 page 字段移除,单纯依赖 sinceId + size 做分页吗?
@isayme 来啊,搞事情啊
@leeqiang @Saviio 你们说的sinceId
我们很早就加过了, 目前获取聊天和动态的分页就是这样的.
但是这个东西使用上很受限: 列表的排序规则必须是Id的正序或降序. 任何其他破坏这个规则的排序都无法支持.
这个issue在消息列表已经遇到了.. 需要加急确定方案
聊天那边不是用的maxId吗
消息列表不是
@leeqiang 之前简聊是这么做的,用了一年多其实基本上没遇到问题。并且可以做到从任意数据节点向前或者向后加载,非常方便... 但是一旦分页是这么做了,不知道在增加排序和筛选规则之后是否会遇到问题 客户端解决的话也都是奇技淫巧,这个是需要和服务端一起配合
但guoqiang的意思是,后端这个貌似不太好做,要前端自己解决的话,必然会发很多冗余请求
的确这个方法更适合于 timeline 形式的资源 https://coderwall.com/p/lkcaag/pagination-you-re-probably-doing-it-wrong 这里讲述到 pagination 的复杂性 message 的确适合用这个方法,因为 message 类型就只讲究 timeline 属性 这里也是 twitter 给出的这类问题的描述 @Saviio https://dev.twitter.com/rest/public/timelines
不是,其实我们的场景里所有分页都适用这个方法,而且也只有这个方法能真正做对分页,在传统的多页应用里不明显,但是SPA里就很明显了