pcursor基于游标的分页

一、基于偏移量分页&基于游标分页#

1.基于偏移量的分页#

适用于内容是静态的,或者不用实时返回数据最新的变化。特点:1.只要有新增或删除,就会有大量的数据偏移量的改变,造成重复展示或者漏展示。2.存在数据量大时的offset慢查询问题

img

2.基于游标的分页#

适用于查询结果在用户浏览过程中是变化的。特点:1.时间线里出现新的数据或删除数据,这些变化都可以在 “前一页” 或者 “后一页” 体现出来。 2.没有offset问题

eg: 发现页 下拉获取最新,上拉获取更久

二、接口实现#

请求参数

参数名 类型 逻辑
参数名 类型 逻辑
pcursor String 初始值传 “” , 后续值使用后端返回值
count Integer 本次想获取的数据量

响应信息

参数名 类型 逻辑
pcursor String 透传给下一页查询 调用方判断是否为 “no_more”, 若是则不再调用,否则永远停不下来了

三、底层实现#

根据更新时间排序

排序规则:order by update_time desc, id desc 原因: 更新时间 + id 的排序, 可以为所有数据确定顺序,即使更新时间相同,即确定每条记录的游标,游标为更新时间+id,只要查询条件不变,即游标可在该顺序下唯一确定一个位置

1. pcursor游标如何拼接#

  • 根据上面排序规则查询出数据
  • 取最后一个数据last,即为该排序下此次查询的终止位置
  • 根据last数据拼接成pcursor
    pcursor = encrypt ( (“updateTime”, long, 1589439219430); (“id”, long, 95424 ); )

2.pcursor游标如何使用#

  • pcursor解析为 上次查询last的 last.updateTime, last.id
  • 本次查询where条件中,updateTime < last.updateTime or (update_time = last.updateTime and id < last.id) order by update_time desc, id desc;
    如下表格,为order by update_time desc, id desc,上次查询游标为last游标位置,则下次查询 updateTime < 1555500001 or (updateTime = 1555500001 and id < 32) order by update_time desc, id desc, 即为后三行数据
update_time id
1555500001 33
1555500001 32 last 游标位置
1555500001 31
1555500000 44
1555500000 42

3. 生成查询SQL#

若查询条件为 userId, status, updateTime 则查询语句为

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT *
FROM
ad_xxx_info
WHERE
user_id = xxx
AND STATUS = xxx
AND (
update_time < xxx
OR ( update_time = xxx AND id < xxx )
)
ORDER BY
update_time DESC, id DESC
LIMIT 20;

创建联合索引 (user_id, status, update_time) 查询即可全部走索引。注:主键也是索引的一部分,Extra为Using where。using where is fine https://stackoverflow.com/questions/9533841/mysql-fix-using-where

幂等设计

请求幂等性总结:

1.是否需要幂等。比如查询,insert含唯一索引的表,update set数据,delete by id 等简单场景是天然幂等。不需要额外做幂等操作。无法这样的比如要做数据的累加,就要做幂等。

2.如何判断重复。

业务唯一键,查记录表或流水表,根据记录的有无和状态来判断。

3.实现。

  1. 简单的话接口直接实现, 但通常幂等逻辑是有通用性的
  2. 如果服务多接口使用写幂等工具类
  3. 如果多服务使用一套幂等逻辑,写sdk
  4. 最复杂的多服务间幂等,还无法都获取到记录结果,就要在sdk统一读写幂等表,读写相同的幂等记录做幂等

4.并发处理

  1. 单机情况下,使用java锁synchronized
  2. 使用数据库锁,悲观锁:select for update,乐观锁:update set XXX and version = 2 where version = 1
  3. 使用分布式锁:
    redis锁:1.过期时间导致并发问题,2.主从切换时锁丢失
    zookeeper锁:写能力不如redis

原文:

  1. 维度1 : 是否需要幂等处理?

    1. 查询场景

      select 字段 from 表 where 条件 ,这种是不会对数据产生任何变化,所以查询场景具备天然幂等性;注意这里的幂等是对系统资源的改变,而不是返回数据的结果,即使返回结果不相同但是该操作本身没有副作用,所以幂等

    2. 新增场景

      insert into 表 (order_id,字段) value(1,字段) :

      1)如果order_id为唯一键,重复操作上面的业务,只会插入一条数据,具备天然幂等性。

      2)如order_id不是唯一键,那上面业务多次操作,数据都会新增多条,不具备幂等性。

    3. 修改场景

      1)直接赋值场景:update 表 set price = 20 where id=1 ;分析: 这种场景不管执行多少次,price都一样,具备天然幂等性;

      2)计算赋值场景:update 表 set price = price + 20 where id=1,每次操作price数据都不一样,不具备幂等性;

    4. 删除场景

      1)delete from 表 where id=1 ,多次操作,结果一样,具备天然幂等性

      2)delete from 表 where price > 20,多次操作,结果一样,具备天然幂等性

      总结:单纯的查询接口,删除接口,没有计算的更新接口,每次执行结果是天然幂等的,其他情况则可能需要做幂等

  2. 维度2 : 是否需要并发控制?

    1. 场景1: 唯一键数据的重复插入

      此场景不需要并发控制,天然并发安全,但需要业务方处理DuplicateKeyException异常

    2. 场景2:转账,若 a >= 100, a -100, b+100

      此场景为了保证数据一致性,事务原子性,需要顺序执行

      总结:除了极其简单的场景外,大部分幂等场景都需要并发控制,需要强锁还是弱锁

  3. 维度3 : 如何判断是重复请求?

    1. 场景1: 无业务主键的新增

      此场景下需要服务端颁发token,通过token判断是否重复,做防重复提交

    2. 场景2: 有业务唯一键的新增

      此场景下可以使用业务唯一键判断是否重复

    3. 场景3: 有业务唯一键的更新

      此场景下可以使用业务数据对应的状态判断是否重复

  4. 维度4 : 重复请求处理方式?

    1. 场景1:消息重复消费

      此场景下,不需要给上游一个返回,重复执行只需要丢弃

    2. 场景2:前端重复提交

      此场景下,需要给用户一个提示,那么重复请求可以直接抛出一个DuplicateReqeustException异常,由上层捕获,前端展示“请不要重复请求”

    3. 场景3: 底层的转账接口

      此场景下,同一笔转账,重复调用第一次若成功了,第二次应该返回相同“成功”,若第一次失败了,第二次返回业务执行结果

      结论:根据不同场景,应该有不同的结束方式,应该由业务方自己决定是丢弃,抛异常,直接返回,还是执行业务逻辑

  5. 维度5 : 重复请求返回是否相同?

    1. 场景1:第一次执行成功,第二次返回应该与第一次相同

    2. 场景2:第一次执行因为代码原因导致的异常而失败,第二次返回应该与第一次相同

    3. 场景3:第一次执行因为网络原因失败,第二次应该执行业务逻辑,返回结果可能不一致

      总结:根据第一次执行的成功和失败,判断第二次应该执行业务逻辑还是直接返回相同结果

  6. 维度6 : 重复请求调用下游如何处理?

    1. 场景1: 业务逻辑中,有调用下游rpc接口,第一次执行失败,第二次重复执行业务逻辑再次调用下游接口

      总结:此场景本质上是一个分布式一致性问题,需要业务方自己解决,或者保证下游接口也是幂等的

  7. 识别相同请求

    1. token机制:每次提交上来的请求,都带上一个token标识,同一个token只处理一次,例如:新增场景的防止重复提交
    2. 业务唯一标识:使用id或者unique key,例如:支付后,支付凭证落库
    3. 业务唯一标识 + 状态:支付后,通过支付凭证号和当前订单状态,更新订单状态
    4. 在业务侧建立同库幂等数据表,记录请求唯一键和执行状态,执行成功后更新状态为成功
  8. 并发处理方式

    1. 单机情况下,使用java锁synchronized

    2. 使用数据库锁,悲观锁:select for update,乐观锁:update set XXX and version = 2 where version = 1

    3. 使用分布式锁:

      redis锁:1.过期时间导致并发问题,2.主从切换时锁丢失

      zookeeper锁:写能力不如redis

  9. 一致性保证

    1. 业务方需保证原子性,本地更新都在一个事务里
    2. 若无法保证分布式事务,下游接口需是幂等的,且本地事务必须重试达到最终一致

SDK设计#

  1. 并发控制
    1. 提供注解,按业务自动划分,提供指定幂等唯一键
    2. 提供并发控制,分布式锁服务(redis或zk),指定分布式锁时间
    3. 提供并发时幂等处理方式,丢弃/抛异常/等待锁
    4. 提供降级选择,不强依赖于锁服务
    5. 提供token注解,防止重复提交
  2. 判断重复请求

处理方式 优点 缺点

方案一 业务方自己判断 业务方需要自己考虑判重

方案二 sdk提供判重接口,由业务方实现 可以实现重复请求的通用处理

方案三 幂等表:在业务侧建立同库幂等数据表,记录请求唯一键和执行状态,执行成功后更新状态为成功 业务方无需再关心判重问题 数据量大,开发量大,复杂度较高

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×