上千万数据查询解决方案

业务场景
  • 集团下有多个业务团队,例如:团队A,团队B
  • 通过账号注册用户后,记录来自的团队,加入用户来源于团队A(A客户池),用户在团队A下单后,可以推荐团队B的业务给用户,如果用户在团队B(B客户池)成交订单,则团队A与团队B可以对用户在团队B下单的金额分红
  • 数据记录:注册时记录团队A,用户在团队B下单,则记录B
  • 如果用户在集团下多个团队有下单,则记录为多个团队的客户池
  • 集团用户在2500w左右,最后所有的私域客户池记录在5kw左右,每天几万增长
  • 客户池又分有无行业场景
  • 使用MySQL
第一版上线(虚拟机部署)
  • 当前需求涉及权限,接入内部登录授权等
  • 考虑到现有类似项目,则使用现有项目开发
  • 这版代码完全没有缓存,考虑到同一个表中,建立关键查询索引(区分业务团队字段,例如: type)
  • 上线后发现,即使type建立索引,也没有增加查询效率,由于区分度太低导致(一般区分度在80%以上才建议建立索引)
  • 客户池最多的用户排在第一个,导致几乎所有用户进入,就显示服务异常,或者15s才有数据返回(数据量在3kw左右,默认时间为最近一周)
  • 根据业务需要,调整客户池显示顺序,数据渲染有所缓解
  • 虚拟机配置为1C2G,一会儿运维就找我,反馈CPU被打爆了,然后运维重启后,依然被打爆
第二版上线(Docker部署 2C4G)
  • 切换Docker部署
  • 调整默认时间为过去一天的记录(原七天)
  • 由于是历史项目,切换过程也是一波三折,配置不规范,导致重重受阻,此处就不细说了
  • 上线后,数据渲染效果有所缓解,CPU没有占满
  • 也对每个客户池的第一页做缓存,但是是第一个用户访问时缓存
  • 这样也会存在问题,第一个用户始终数据渲染始终慢
  • 底层使用 Spring Data JPA(有一半字段前端不使用,会节省转换记录占用内存),在多条件查询时,findAll(Specification specification, Pageable pageable) 不支持指定返回参数
// 过滤字段不生效 升级Spring Data JPA版本也不行
List<Selection<?>> selections = Lists.newArrayList(root.get("userId"), root.get("name"), root.get("industry"), root.get("labels"), 
                root.get("provinceName"), root.get("cityName"));
query = query.multiselect(selections);
  • 使用这种方式可以,但是存在两个问题,多条件拼装,对应查询条件总量(前端分页使用)
/**
 * https://stackoverflow.com/questions/9191551/jpa-multiselect
 * 使用如下方式可以实现指定字段返回,但是不支持分页
 **/
criteriaBuilder.multiselect(select);
TypedQuery<Tuple> q = entityManager.createQuery(criteriaBuilder);
  • 网上说还有其他解决方案,但是经过测试不行,如果您需要尝试,可以搜索来看下
  • 这里还有个小插曲,第一版上线时数据不正确,例如:客户企业名称: “XXX公司”,客户代表为空,但是页面显示是 客户企业名称: ‘’ 客户代表: XXX公司
  • 我以为是程序从数据库查询出来后处理导致的问题,果断将处理过程放在查询语句中
  • 类似于客户所在地需要处理,将数据中的 省、市、区/县使用"-"展示,例如: 四川-成都-青羊,但是市和区都可能为空
/**
 * 至于为什么返回 Object[](使用原生SQL查询),请自行搜索
 **/
@Query(value = "select user_id userId, name, '' industry, labels, representative, \n" +
        "concat(province_name, \n" +
        "case city_name when '' then '' ELSE CONCAT('-', city_name) end, \n" +
        "case district_name when '' then '' ELSE CONCAT('-', district_name) end) region, date_format(str_to_date(CONCAT('',month_day), '%Y%m%d'), '%Y-%m-%d') as createTime\n" +
        "from table_name\n" +
        "where type = ?1 and user_id in (?2)", nativeQuery = true)
List<Object[]> getCustomerDetailList(Integer type, List<Integer> userList);
  • 然后使用笨方法,将数组一个个元素取出,塞进构造方法即可
第三版上线(定时任务加缓存)
  • 接入公司调度平台(定时任务即可)
  • 每天上班前调用,写入每个团队第一页缓存数据(与产品确认,用户一般操作场景)
  • 查询条件可第二版代码相同,但是将 findAll() 返回的 Page<?> 拆分,缓存中记录 total, List idList, sid(用户记录生成缓存是否有问题)
  • 缓存对象定义
/**
 * 区别生成缓存时操作ID和用户访问缓存时数据关联(可以省略)
 **/
private String sid;

/**
 * 默认查询条件总记录数
 **/
private long total;

/**
 * 主键ID(也可以用用户ID,但是使用用户ID需要和type查询)
 **/
private List<Integer> idList;
  • 用户操作时,通过对应页码和查询条件,获取缓存中idList,这个可以快速查询到记录
  • 取出缓存中 total 记录数,封装page对象返回皆可
  • 可以设计宽松些,对不同页码,每页展示数,团队类型可用动态配置,数据量少的团队完全可以不走缓存
  • 我们在分析问题时,如果一个方法不能处理或者解决,可以尝试拆分成几步,分别处理即可
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值