一次数据库与es程序优化的过程

关键词: 桶数限制, 分区partition, cardinality, script

最近生产上的定时任务发生了OOM异常.很遗憾的是,st环境无法重现,生产环境也由于权限的问题无法定位,所以找不到问题的原因.

不过从程序来看,代码还是有很大的优化空间.

目前业务现状(模拟的业务场景):

1,表中有约140万条不同的会员卡(卡信息表)数据,对应有大约3200万条消费记录存在es中.

2.需要对半个月/一个月都未消费的会员卡发送短信提醒,如果卡有异常(另一表记录),则不发送

3.同一张会员卡可以给多个人使用(会员与卡映射表)

现有程序设计:

1.分批遍历卡信息,剔除有异常的会员卡

2.用每张卡去ES查询最后一条消费记录的时间,然后判断是否半个月/一个月都未消费

3.符合条件的卡发短信提醒卡的所有成员

缺点:

1.需要全部卡都进行遍历,并到ES进行聚合查询,再做判断,占内存多,计算量大,耗时

2.查卡过程的SQL使用了not exist,查询时间长

3.很多会员卡不符合条件,做了不必要的筛选,耗时且浪费巨大

因为无法找到内存溢出的原因,只有通过其他方法进行代码优化.


=>改进办法:

1.设计思路: 原先是 会员卡->es查询最后一笔消费时间->符合条件的数据->发短信 (正向)

改为:es分别获取最后一笔交易是半个月前,一个月前的会员卡->数据库查会员卡信息->发短信 (反向)

2.发短信步骤改为异步

为什么原先的设计没有按这种思路来呢? 是因为当时一直找到符合要求的es脚本去筛选数据.所以只能遍历会员卡,而不是相反先从es筛选开始. 光是这一步就有巨大的区别.


参考资料:

Elasticsearch 中如何巧妙地使用聚合函数达到数据库中having的效果

ElasticSearch 聚合筛选,类似SQL里面的having

How to use Scroll on Elasticsearch aggregation?

filtering_values_with_partitions

在es使用聚合查询时,由于桶数过多超出设置会抛异常,因此需要对查询进行分区(即分批次循环查询)

先获取es总记录数

private int getRecordTotalNum(RestHihLevelclient client) {

    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    CardinalityAgeregationBuilder countAee = AggregationBuilders.cardinality(CARD_CNT).field(CARD_ID);
    sourceBuilder.agregation(countAgg.size(0);

    SearchRequest request = new SearchRequest(INDEX_NAME).types(TYPE_NAME).source (sourceBuilder);

    long total = 0;

    try {
        SearchResponse search = client.search(request);
        Cardinality cntAgg = sesearch.getAggregations().get(CARD_CNT);
        total = cntAgg.getValue();    
    catch (Exception e) {
        log.error("查询ES异常", e);
    }
    return total;
}

至于每个批次数据量多少,要根据实际情况而定,如综合查询所需时间,cup消耗等.

在分区的聚合查询中,要对聚合结果进行having过滤(筛选消费时间为半个月前).

而此时又发现新的问题:查询结果会比实际的时间少8小时--原来是时区原因导致的.

所以格式化要设置时区, es脚本大致如下:

{
	"size": 0,
	"aggregations": {
		"by_card_id": {
			"terms": {
				"field": "card_id",
				"size": 1000000,
				"min_doc_count": 1,
                    .
                    .
                    .
				"include": {
					"partition": 0,
					"num_partitions": 12
				}
			},
			"aggregations": {
				"by_trx_tim": {
					"max": {
						"field": "trx_tim"
					}
				},
				"having": {
					"buckket_selector": {
						"buckets_path": {
							"trxTim": "by_trx_tim"
						},
						"script": {
							"source": "SimpleDateFormat sdf = new SimpleDateFormat(\"yyyy-MM-dd\");
										sdf.setTimeZone(TimeZone.getTimeZone(\"GMT0\"));
										String tt = sdf.format(params.trxTim);
										return tt.compareTo(\"2020-08-26\") == 0;",
							"lang": "painless"
						},
						"gap_policy": "skip"
					}
				}
			}
		}
	}
}

由此获取会员卡卡号,就是满足半个月无消费的条件的数据了. 再做一次满足一个月的查询,就得到全部要发短信的会员卡.

此时再单独查出消费异常的会员卡,剔除掉即可,不用和以前那样做not exist耗时的操作了.

剩下的就是组装信息发短信了.

总结:

原先对ES了解很浅,并不知道ES有类似having的查询,也不知道可以插入自定义查询脚本. 当时也试图找过相关的资料,可能是查资料不得法,没有找到这块内容.所以问题驱动开发,能不断扩充知识.

有疑问或不同的观点,欢迎提出,一起探讨.

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值