3分钟将5000w订单的订单号从数据库中加载到Java内存中
需求:订单号是由20个字符组成,其中第一个字符是大写字母,其余19个字符是由雪花算法基于时间生成的数字;数据库中出库单表中有5000w订单信息,将数据库中所有订单的订单号加载到Java内存中。
环境:
本地Docker启动mysql8.0,内存=1024m,cpu=1;
IDEA Java虚拟机:4核8线程,内存4096MB
先看一下最后的统计结果:
1.查询效率优化
最初的时候是考虑使用的多线程分页查询,但是随着分页深度的加深,查询会变得越来越慢;
多次尝试后,最后选择使用游标分页,代码如下
@Select("SELECT order_no FROM stock_out_order")
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000)
@ResultType(StockOutOrder.class)
void streamOrders(ResultHandler<StockOutOrder> handler);
2.存储性能优化
因为订单是由单个字符和一串数字组成的,所以是使用varchar类型存储的,Java中是使用的String类型。
如果使用String类型做为最终存储类型,经过测试发现2000w条左右就需要200MB内存,很耗内存
那么是否可以将字符串中的字母和数字分开,类似: A+1000000L (数字本身就是雪花算法生成的Long类型数字,本身就可以使用Long类型存储)
随后考虑到是否可以使用布隆过滤器,但是布隆过滤器会有哈希冲突导致误判,特别是数量达到5000w基本上是100%会冲突
随后有考虑到可以使用bitset位图来存储,但是bitset只支持int类型,长度远远不够
最后选择了Roaring64Bitmap,一种支持long的位图
private final Roaring64Bitmap orderNos = new Roaring64Bitmap();
/**
* 加载订单并加载到内存
*/
private void loadCompressedOrders() {
orderNos.clear();
super.baseMapper.streamOrders(ctx -> {
String orderNo = ctx.getResultObject().getOrderNo();
addOrder(orderNo);
});
}
/**
* 将订单添加到内存中,这里加了synchronized ,Roaring64Bitmap 不是线程安全的
*
* @param orderNo 订单号
* @return boolean
*/
private synchronized boolean addOrder(String orderNo) {
long snowflakeId = Long.parseLong(orderNo.substring(1));
if (orderNos.contains(snowflakeId)) {
// 重复订单,不用添加
return false;
}
orderNos.addLong(snowflakeId);
return true;
}
总结:
在这个场景中,要获取数据库中全部订单编号,深分页情况下,多线程并不能解决问题,反而利用数据库使用游标分页是有效的优化手段。
License:
CC BY 4.0