前言
一次线上故障,数据库查询时间过长,导致前端页面频频报错,结果不仅该服务的访问受到了影响,其他服务的访问的流畅度也下降了。
分析
- 查询语句并不复杂,只涉及单表查询
- 查询已经设置了分页,也有加索引
- 查看该表的数据量,已经有两千万
解决
阶段一
看到数据量已经有两千万,是不是有人觉得我立刻就会讲分表、分区等操作。哈哈,当然不是了,线上问题当然应该尽快解决。
为了保证其他服务正常执行,并结合该服务的特点(访问量不会太多),直接设置该服务的最大查询时间,查询时间超过限制,则把错误日志打印出来,然后返回,保证其他服务的可用性。
设置的最大查询时间应该只局限于该服务,即粒度要小,别影响到其他服务。所以采用如下方式:
Future<List<Repor>> result = asyncService.get(reportDetailedsExample);
List<ReportDetailed> reportDetailed = null;
try {
reportDetailed = result.get(5, TimeUnit.SECONDS);
} catch (Exception e) {
throw new BusinessException("慢查询异常");
}
if (reportDetailed == null || reportDetailed.isEmpty()) {
return null;
}
// ...
采用Future的形式,异步获取结果,get方法设置等待时间为5秒。异步服务类AsyncService ,采用AsyncResult包裹查询结果。
/***
*
* @Author:fsn
* @Date: 2020/4/16 17:14
* @Description
*/
@Service
public class AsyncService {
@Autowired
private ReportDetailedsMapper reportDetailedsMapper;
@Async
public Future<List<ReportDetaileds>> get(ReportDetailedsExample reportDetailedsExample) {
return new AsyncResult<>(reportDetailedsMapper.selectByExampleWithBLOBs(
reportDetailedsExample));
}
}
阶段二
分表or分区,最终方案是分区。先进行一波操作,再说说缘由~
这里采用比较常见的RANGE分区,注意!!!分区键(这里是date)必须是主键的一部分!
ALTER TABLE report_detaileds_copy1 PARTITION BY RANGE (YEAR(`date`))
(
PARTITION p2019 VALUES less than (2019),
PARTITION p2020 VALUES less than (2020)
);
上述这种方式需要服务访问量比较低的情况下才做比较好,一般来说,可以再新建一张表,然后分区,再导数据到新表(导数据的时候千万小心!!!特别是表的更新、插入都很频繁的时候,还得注意是否有走索引(不是说加了索引就一定会走索引),避免锁整张表的情况发生)。
CREATE TABLE `report_detaileds_2020` (
// 此次省略一大波字段
PRIMARY KEY (`id`, `date`) USING BTREE,
KEY `rule_id` (`rule_id`) USING BTREE
) PARTITION BY RANGE (YEAR(`date`))(
PARTITION p2019 VALUES less than (2019),
PARTITION p2020 VALUES less than (2020)
);
为什么我这里采用分区呢?(1)由于业务特点,显示的信息是根据时间显示的;(2)这些信息不会全部对用户公开,只显示了一段时间内的数据;(3)对于一张大数据量的表进行分表工作量还是挺大的,还得涉及代码层面的修改,而采用merge的分表形式虽然比较简单但受限于存储引擎(需要MyISAM存储引擎,如果能在代码设计的时候,可以预估到数据量未来的大概增长情况,还是早做分表稍微好点)
注意!!
分区方案也不是随便划分的,它的缺点如下:(1)对分区表进行DDL操作难度更大风险高。(DDL操作需要锁定所有分区,导致所有分区上操作都被阻塞)(2)分区不当,导致扫描全部分区,可能导致IO次数反而更多了。
关于第二点的解释:
以Innodb存储引擎文件,我们的一个表的数据和索引保存的地方是在一个idb为后缀名的文件里头,对于分区表来说,原来的一个idb文件现在是有多个的,对应多个分区。而我们知道,InnoDb采用B+树作为索引结构,一般2次IO左右的次数就可以扫描所有数据了。而如果扫描所有分区的话,一个分区2次IO,10个分区那就是20次IO。。。。
拓展
其实还有一个更加骚的办法,文中说过该业务的特点之一是这些信息不会全部对用户公开,只显示了一段时间内的数据,我们可以写个脚本,每天晚上或者凌晨,把这段时间的数据捞出来,存到一个表中(存之前truncate一下),然后只针对该表进行查询。