那些年,我们见过的 Java 服务端“问题”
采用分页查询
“采用分页查询”是指定 startIndex(开始序号)和 pageSize(页面大小)进行数据查询,或者指定 pageIndex(分页序号)和 pageSize(页面大小)进行数据查询。例子代码如下:
/** 订单DAO接口 */
public interface OrderDAO {
/** 统计过期订单函数 */
@Select("select count(*) from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day)")
public Long countTimeout();
/** 查询过期订单函数 */
@Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day) limit #{startIndex}, #{pageSize}")
public List<OrderDO> queryTimeout(@Param("startIndex") Long startIndex, @Param("pageSize") Integer pageSize);
}
/** 订单服务接口 */
public interface OrderService {
/** 查询过期订单函数 */
public PageData<OrderVO> queryTimeout(Long startIndex, Integer pageSize);
}
适用于真正的分页查询,查询参数 startIndex(开始序号)和 pageSize(页面大小)可由调用方指定。
分页查询隐藏问题
假设,我们需要在一个定时作业(每 5 分钟执行一次)中,针对已经超时的订单(status=5,创建时间超时 30 天)进行超时关闭(status=10)。实现代码如下:
/** 订单DAO接口 */
public interface OrderDAO {
/** 查询过期订单函数 */
@Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day) limit #{startIndex}, #{pageSize}")
public List<OrderDO> queryTimeout(@Param("startIndex") Long startIndex, @Param("pageSize") Integer pageSize);
/** 设置订单超时关闭 */
@Update("update t_order set status = 10 where id = #{orderId} and status = 5")
public Long setTimeoutClosed(@Param("orderId") Long orderId)
}
/** 关闭过期订单作业类 */
public class CloseTimeoutOrderJob extends Job {
/** 分页数量 */
private static final int PAGE_COUNT = 100;
/** 分页大小 */
private static final int PAGE_SIZE = 1000;
/** 作业执行函数 */
@Override
public void execute() {
for (int i = 0; i < PAGE_COUNT; i++) {
// 查询处理订单
List<OrderDO> orderList = orderDAO.queryTimeout(i * PAGE_COUNT, PAGE_SIZE);
if (OrderDO order : orderList) {
// 进行超时关闭
......
orderDAO.setTimeoutClosed(order.getId());
}
// 检查处理完毕
if(orderList.size() < PAGE_SIZE) {
break;
}
}
}
}
粗看这段代码是没有问题的,尝试循环 100 次,每次取 1000 条过期订单,进行订单超时关闭操作,直到没有订单或达到 100 次为止。但是,如果结合订单状态一起看,就会发现从第二次查询开始,每次会忽略掉前 startIndex(开始序号)条应该处理的过期订单。这就是分页查询存在的隐藏问题:
当满足查询条件的数据,在操作中不再满足查询条件时,会导致后续分页查询中前 startIndex(开始序号)条满足条件的数据被跳过。
可以采用”设置最大数量”的方式解决,代码如下:
/** 订单DAO接口 */
public interface OrderDAO {
/** 查询过期订单函数 */
@Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day) limit 0, #{maxCount}")
public List<OrderDO> queryTimeout(@Param("maxCount") Integer maxCount);
/** 设置订单超时关闭 */
@Update("update t_order set status = 10 where id = #{orderId} and status = 5")
public Long setTimeoutClosed(@Param("orderId") Long orderId)
}
/** 关闭过期订单作业(定时作业) */
public class CloseTimeoutOrderJob extends Job {
/** 分页数量 */
private static final int PAGE_COUNT = 100;
/** 分页大小 */
private static final int PAGE_SIZE = 1000;
/** 作业执行函数 */
@Override
public void execute() {
for (int i = 0; i < PAGE_COUNT; i++) {
// 查询处理订单
List<OrderDO> orderList = orderDAO.queryTimeout(PAGE_SIZE);
if (OrderDO order : orderList) {
// 进行超时关闭
......
orderDAO.setTimeoutClosed(order.getId());
}
// 检查处理完毕
if(orderList.size() < PAGE_SIZE) {
break;
}
}
}
}
