解决分页查询数据重复或丢失问题

背景

APP订单列表查询展示功能,向下刷新拉取了重复的数据,偶尔还会缺失数据,和后台的数据对不上,订单中心获取订单列表的接口是个按支付时间倒排的分页接口,分页实现方法:

select * from order where[condition] order by mtime desc limit pageNum*pageSize,pageSize

问题复现

新建测试表,插入数据。

# 模拟订单表
CREATE TABLE`order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ctime` bigint(20) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
# 数据初始化
INSERT INTO`order`(`ctime`)VALUES (1);
INSERT INTO`order`(`ctime`)VALUES (2);
INSERT INTO`order`(`ctime`)VALUES (3);
INSERT INTO`order`(`ctime`)VALUES (4);
INSERT INTO`order`(`ctime`)VALUES (5);
INSERT INTO`order`(`ctime`)VALUES (6);
INSERT INTO`order`(`ctime`)VALUES (7);
INSERT INTO`order`(`ctime`)VALUES (8);
INSERT INTO`order`(`ctime`)VALUES (9);
INSERT INTO`order`(`ctime`)VALUES (10);

分页查询:新增导致重复

# 第一页
SELECT * FROM `order` ORDER BY ctime DESC limit 0,5;
# 查询结果
 id ctime
 10 10
  9  9
  8	 8
  7	 7
  6	 6
***手机页面展示***
10 9 8 7 6
# 新增两条数据
INSERT INTO`order`(`ctime`) VALUES (11);
INSERT INTO`order`(`ctime`) VALUES (12);
# 第二页
SELECT * FROM `order` ORDER BY ctime DESC limit 5,5;# 查询结果
 id ctime
	7	 7
	6	 6
	5	 5
	4	 4
	3	 3
# 重复数据:6和7
***手机页面展示***
10 9 8 7 6, 7 6 5 4 3

解决分页查询数据重复或丢失问题_第1张图片 

原因:

查询第一页完后,获取到的数据是6~10,随后新增11和12两条数据,进行查询第二页,获取到的数据是3~7,此时6和7被重复拉取

分页查询:删除导致缺失

# 第一页
SELECT * FROM `order` ORDER BY ctime DESC limit 0,5;
# 查询结果
 id ctime
 10 10
  9  9
  8	 8
  7	 7
  6	 6
***手机页面展示***
10 9 8 7 6
# 删除id=6的数据
DELETE FROM `order` WHERE id = 6;
# 第二页
SELECT * FROM `order` ORDER BY ctime DESC limit 5,5;# 查询结果
 id ctime
	4	 4
	3	 3
	2	 2
	1	 1
# 缺失数据:5
***手机页面展示***
10 9 8 7 6, 4 3 2 1

解决分页查询数据重复或丢失问题_第2张图片 

原因

第二页的数据完全依赖第一页的数据,第一页的数据变化影响第二页的数据;主要还是每次分页查询都是从起点分页,分页之前的数据变化都会影响接下来的分页。

解决方案

方法一:记录时间点

  • 查询出当前页数据后,记录下本次拉取位置(curPosition),当页数据和curPosition一并返回客户端;下次请求时,需要把上次获取的curPosition传给后端,后端从curPosition开始分页。
  • 拉取位置:最好是当前排序字段
  • 前提条件:所选字段的数据不能重复(最好是唯一索引)。

验证新增

# 使用当前排序字段ctime当作curPosition参数
# 第一页。传curPosition为空,mybatis条件查询不带该参数。
SELECT * FROM `order` ORDER BY ctime DESC limit 0,5;
# 查询结果
 id ctime
 10 10
  9  9
  8	 8
  7	 7
  6	 6
此时 curPosition = 6;
***手机页面展示***
10 9 8 7 6 
# 新增两条数据
INSERT INTO`order`(`ctime`) VALUES (11);
INSERT INTO`order`(`ctime`) VALUES (12);
# 第二页
SELECT * FROM `order` WHERE ctime<'#{curPosition=6}' ORDER BY ctime DESC limit 0,5;# 查询结果
 id ctime
	5	 5
	4	 4
	3	 3
	2    2
	1    1
# 查询正常
***手机页面展示***
10 9 8 7 6, 5 4 3 2 1 

验证删除

# 第一页
SELECT * FROM `order` ORDER BY ctime DESC limit 0,5;
# 查询结果
 id ctime
 10 10
  9  9
  8	 8
  7	 7
  6	 6
此时 curPosition = 6;
***手机页面展示***
10 9 8 7 6
# 删除id=6的数据
DELETE FROM `order` WHERE id = 6;
# 第二页
SELECT * FROM `order` WHERE ctime<'#{curPosition=6}' ORDER BY ctime DESC limit 0,5;# 查询结果
 id ctime
  5    5
  4	   4
  3	   3
  2	   2
  1    1
#查询正常
***手机页面展示***
10 9 8 7 6, 5 4 3 2 1

利弊分析

使用curPosition记录位置查询的方式,**好处在于,在第一次查询确定初始位置之后,后面的数据一定不会出现数据重复和缺失。**在手机端进行列表划动时,新刷新出来的数据都是在前一批数据的curPosition基础上查询到的。但这也带来一个问题,我们的整个查询好像只能从排序的方向依次查询,因为每次查询都依赖上一次的curPosition。假如我们要进行跳页查询,将会带来一些问题。令pageSize=5,比如查完第一页,得到curPosition = a,接下来想查询第10页,可以使用 where ctime所以在面对跳页查询时,这样的方法就不太可取了。好在是目前需求是解决手机端划动列表的加载,并不会出现跳页面的情况,但是在web端展示就不能使用这样的sql了。还有一个谈不上缺陷的缺陷就是,手机页面展示不包括在curPosition时间点之后已经更新过的数据,比如在验证新增的时候看不到新增的11、12,验证删除的时候已经删除的6还是会展示在页面上,这点在后面的方法二中会得到解决。

方法二:全量更新,重新渲染

  • 每次分页查询时候,返回当前页和当前页面之前所有页面的数据。limit 0,pageSize*pageNum
  • 每次分页查询完,客户端重新渲染所有数据。

验证新增

# 第一页。
SELECT * FROM `order` ORDER BY ctime DESC limit 0,5;
# 查询结果
 id ctime
 10 10
  9  9
  8	 8
  7	 7
  6	 6
***手机页面展示***
10 9 8 7 6
# 新增两条数据
INSERT INTO`order`(`ctime`) VALUES (11);
INSERT INTO`order`(`ctime`) VALUES (12);
# 第二页
SELECT * FROM `order` ORDER BY ctime DESC limit 0,5*2;# 查询结果
 id ctime
 12 12
 11 11
 10 10
  9  9
  8	 8
  7	 7
  6	 6
  5	 5
  4	 4
  3	 3
# 查询正常
***手机页面展示***
12 11 10 9 8,7 6 5 4 3 

验证删除

# 第一页
SELECT * FROM `order` ORDER BY ctime DESC limit 0,5;
# 查询结果
 id ctime
 10 10
  9  9
  8	 8
  7	 7
  6	 6
***手机页面展示***
10 9 8 7 6
# 删除id=6的数据
DELETE FROM `order` WHERE id = 6;
# 第二页
SELECT * FROM `order` ORDER BY ctime DESC limit 0,5*2;# 查询结果
 id ctime
 10 10
  9  9
  8	 8
  7	 7
  5  5
  4	 4
  3	 3
  2	 2
  1	 1
#查询正常
***手机页面展示***
10 9 8 7 5, 4 3 2 1

利弊分析

可以发现使用这样的方式,数据的一致性是最高的,几乎所有的数据库更改都会展示在客户端页面上,更不可能出现重复和缺失的情况。随之而来,每次请求都会请求limit 0,pageSize*pageNum的所有数据,而且客户端需要对获取的数据重新渲染展示,无疑对数据库和app端增加了很大的开销

方法三:客户端分页

第一次进入订单列表时,请求接口查询得到所有数据,在客户端进行分页展示。此后再往下划时,不需要再请求后端了,只需要把第一次请求后的数据展示出来即可。

该方法实在太不常见,弊端太明显,不做具体分析了。

方法综述

总的来说,在仅考虑手机端分页查询接口的情况下:方法一在实际项目中实行的可能性更大一些。对数据实时性要求很严格的情况下,可以考虑方法二。

展示数据实时性 请求次数 前后端压力 数据库压力
方法一 查询瞬间 一次一查 正常 正常
方法二 与数据库同步 一次一查 前端压力大 数据量随次数逐渐增多
方法三 第一次进入页面瞬间 查一次 前端压力大 一次请求大量数据

你可能感兴趣的:(数据库,mysql,java)