天猫权益平台如何10倍的提升数据库查询响应时间

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL DuckDB 分析主实例,集群系列 8核16GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介:

背景:

上周双旦项目上线完成,在上线过程中,遇到了一系统的问题,我个人这边主要遇到的问题是: 前端查询奖品的相关信息,包括奖品的库存信息,这一块大概的需求场景是这样的: 一个真实的奖品有多个虚拟的批次库存信息,每个批次库存信息由一个资格数来指定有允许有多少个人来抽奖,只有指定的人数满足以后,才可以开奖,开奖完成之后,才能进入一下批次库存信息进行抽奖活动。

拿单个奖品查询的sql来说,sql是这样的:

select
        total as total,
        bestow as bestow,
        benefit_id as benefitId,
        sub_type_id as subTypeId,
        id as id
from
        bp_inventory
where
        benefit_id = #{benefitId} and
        type=#{type} and
        total > bestow 
order by id limit 1

即查询指定奖品当前正在消费的批次库存
bp_inventory在(benefit_id, type, sub_type_id)三个列上有唯一索引, 如下图所示:
image.png

在每个benefit_id对应的批次库存数量不是很大的情况下,查询应该是非常快的,如下图所示:
image.png
从上图中可以看出,查询一次消耗的时间差不多在2ms左右,是一个典型的快sql查询,接下来问题来了: 项目中真实的场景是,根据前端传进来的activityCode去解析出一批奖品的id,一个activityCode解析出奖品id的个数,少的情况有5-6个,平均有30个左右,最多有120左右,很显然,用单个奖品串行去查数据库是不能接受的,因为有的奖品的批次库存比较多,根据limit 1查询的时间比较长, 串行查20个大概需要150-250ms左右,如下所示:
image.png
这个时间响应是不能接受的,毕竟响应时间平均超过30ms,用户就可以有明显慢的感觉,在这种情况下一次用户的首页的请求,查询120个奖品需要消耗的时间大概在400ms+,为了优化这个响应时间,首先想到一个最简单的优化办法,用group by语法小批量并行异步查询数据库, sql如下:

 select
          total as total,
          bestow as bestow,
          benefit_id as benefitId,
          sub_type_id as subTypeId,
          id as id
        from
            bp_inventory
        WHERE
            benefit_id in
        <foreach collection="benefitIds" index="index" item="item" open="(" separator="," close=")">
            #{item}
        </foreach>
        and type=#{type} and total > bestow  group by benefit_id
    </select>

sql的执行计划如下:
image.png
从执行计划可以看出,确实是走到索引, 查询效果如下:
image.png
小批次查询9个奖品的批次库存信息消耗5ms,这也是一个典型的快sql, 通过这次优化,查询响应时间可以控制在100ms左右
如果查询时间都能控制在100ms左右,也没什么问题,终于有一天,问题爆发了,有3个上万的库存奖品上线,批次库存量分别是: 10000, 10000, 50000,并且这三个奖品都是在同一个活动下面,小批次查询库存的sql如下:
image.png
小批量sql查询的消耗时间一下子跳到了200ms+, 当时人有点凝惑,这是怎么回事,还是先看一下这条SQL执行计划:
image.png
刚开始只注意到possible_keys 和 key这两列,是走到(benefit_id, type, sub_type_id)这个索引,仔细分析发现extra这一列不太一样,mysql提示是using where,显然实际上mysql执行这条sql语句是没有走到索引,而是用全表扫描的方式,进一步分析发现,这三个批次库厚的总数相加在7w,而整个表的的总数据行数是18w左右,显然, 已经超过索引区分度30%的上限,因此mysql在物理查询优化阶段才会去用全表扫描的方式去查询,问题原因终于定位到了,下面就来想方案来解决这个大库存查询慢的问题。

即然大库存用小指量查询的速度比较慢,会导致有大量的慢性sql问题,

方案一:

首先想到的方案就是把这些大库存的奖品全部隔离,大库存的奖品单独查询,小库存的奖品小用指量查询,这个方法最快,上线后,线上情况马上稳定 ,但这带来一个问题,遇到大库存时,单条查询的时间依然比较慢,基本上都在100ms+以上,这个不能接受,只能继续优化;

方案二:

用mysql二级索引的方式不能从根本上解决大库存奖品查询慢的问题,但是我们业务场景是每个奖品的批次库存是根据id从小到大依次消费,有这个业务上的约束条件之后,就好处理了,能不能把大库存奖品的所有有效的批次库存信息的id存储在每台机器本地,根据id去查询大库存奖品当前有效的库存信息,这个方案看起来比较靠谱,即结合了实际的业务场景,又充分利用了数据库的一级索引,方案定来以后,说干就干,经过3个小时紧张的奋战,这个方案终于在预发上验证通过了,主要的思路如下:

1. 添加一个switch开发,配置上大库存奖品的id,因为大库存奖品我们通过数据库查询出来是已知的:

@AppSwitch(des = "2018双旦项目大的benefitId", level = Switch.Level.p2)
public static Set<Long> new_year_2018_big_benefitIds = new HashSet<Long>();

2. 在每个机器上添加一个本地的定时任务, 这个任务定时的从数据库捞取最近10min新生效的批次库存信息

 /**
     * 权益对应的锁
  */
private ConcurrentMap<Long, Object>  benefitIdLocks = new ConcurrentHashMap<Long, Object>();

    /**
     * 权益批次inventory信息
     */
private ConcurrentHashMap<Long, BenefitItem> benefitIdsMap = new ConcurrentHashMap<Long, BenefitItem>(32);

 /**
  * 每个奖品对应的批次库存统计信息的内部类定义
 */
private class BenefitItem {

        public List<Long> ids;

        public Long nextMinId;

        public BenefitItem() { }

        public BenefitItem(List<Long> ids, Long nextMinId) {
            this.ids = ids;
            this.nextMinId = nextMinId;
        }
  }

 //定时任务
 scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
 // 10分钟执行一次
 scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
       @Override
       public void run() {
                // 初始化权益
                for (Long benefitId : new_year_2018_big_benefitIds) {
                    benefitIdLocks.putIfAbsent(benefitId, new Object());
                }
                for (Long benefitId : benefitIdLocks.keySet()) {
                    if (!new_year_2018_big_benefitIds.contains(benefitId)) {
                        benefitIdLocks.remove(benefitId);
                    }
                }
                // 权益对应的有效库存
           for (Long benefitId : new_year_2018_big_benefitIds) {
                    try {
                        BenefitItem benefitItem = benefitIdsMap.get(benefitId);
                        if (benefitItem == null) {
                            benefitItem = new BenefitItem(new LinkedList<Long>(), 0L);
                            benefitIdsMap.put(benefitId, benefitItem);
                        }
                        Long nextMinId = benefitItem.nextMinId;
                        List<Long> currentIds = benefitRepository.batchQueryInventoryIdByBenefitIdOfpage(benefitId, SPECIAL_TYPE, nextMinId, 500);
                        while (currentIds != null && currentIds.size() > 0) {
                            synchronized (benefitIdLocks.get(benefitId)) {
                                benefitItem.ids.addAll(currentIds);
                            }
                            nextMinId = benefitItem.nextMinId = currentIds.get(currentIds.size() - 1);
                            currentIds = benefitRepository.batchQueryInventoryIdByBenefitIdOfpage(benefitId, SPECIAL_TYPE, nextMidId, 500);
                        }
                    } catch (Exception e) {
                        logger.error("scheduleWithFixedDelay failed e=", e);
                    }
                }
                for (Long benefitId : benefitIdsMap.keySet()) {
                    if (!new_year_2018_big_benefitIds.contains(benefitId)) {
                        benefitIdsMap.remove(benefitId);
                    }
                }
            }
 }, 0, 10, TimeUnit.MINUTES);

每次查询都会记录下当前最大的nextMinId,下次查询时从这个nextMinId开始查询,这也是一条非常高效的sql,因此不会对数据库造成任何的压力,查询sql如下:

 select
       id as id
 from
            bp_inventory
WHERE
            benefit_id = #{benefitId} and type=#{type} and id > #{minId} and total > bestow
order by id
limit  #{pageSize};

3.在查询大商品库存时的主要代码如下:

private BenefitInventoryVO processForSingle(Long benefitId) {

        BenefitInventoryVO  benefitInventoryVO = null;
        try {
            if (benefitIdLocks.containsKey(benefitId)) {
                Long inventoryId = null;
                if (benefitIdsMap.get(benefitId).ids.size() > 0) {
                    inventoryId = benefitIdsMap.get(benefitId).ids.get(0);
                }
                // 防止已经消费完成
                if (inventoryId != null) {
                    benefitInventoryVO = benefitRepository.queryInventoryDOById(inventoryId);
                }
                //  一般情况只有2-3次,可以优化查询次数,第二次可以多查几条,直接判断
                while ( benefitInventoryVO != null &&
                         benefitInventoryVO.getTotal() != null &&
                         benefitInventoryVO.getTotal() > 0 &&
                         benefitInventoryVO.getTotal().equals(benefitInventoryVO.getBestow()) ) {  // 本批次已经消费完成

                    if (benefitIdsMap.get(benefitId).ids.size() > 0) {
                        synchronized (benefitIdLocks.get(benefitId)) {
                            // 删除第一个
                            if (benefitIdsMap.get(benefitId).ids.size() > 0) {
                                benefitIdsMap.get(benefitId).ids.remove(0);
                            }
                        }
                    }
                    if (benefitIdsMap.get(benefitId).ids.size() > 0) {
                        inventoryId = benefitIdsMap.get(benefitId).ids.get(0);
                    } else {
                        inventoryId = null;
                    }
                    // 库存已经用完了
                    if (inventoryId  == null) {
                        break;
                    }
                    // 查数据库
                    benefitInventoryVO = benefitRepository.queryInventoryDOById(inventoryId);
                }
            } else {
                // 条件查询数据库
                benefitInventoryVO = benefitRepository.singleQueryInventoryDTO(benefitId, SPECIAL_TYPE);
            }
            if (benefitInventoryVO == null) {
                benefitInventoryVO = BenefitInventoryVO.of(0, 0, 0L, benefitId);
            }
            return benefitInventoryVO;
        } catch (Exception e) {
            logger.error("processForSinle error benefitId={}, e={}",benefitId, e);
            return BenefitInventoryVO.of(0, 0, 0L, benefitId);
        }
    }

思想很简单,如果当前id对应的批次库存已经消费完成了,就从列表取下一批,直到找到有效的为止,一般情况下是2-3次根据id查询,因为根据主键id查询数据库的表的速度非常非常的高效,就算多查几次,也不会有任何的性能问题
下面是优化前和优化后查询性能的对比:
优化前:
image.png
优化后:
image.png
上线前后查询性能对比图如下:
image.png
数据库的平均响应时间提高了10倍以上, HSF服务响应时间绝大部分终于压制住在20ms以下;

因写作时间比较仓促,有些地方写得不是很详细,有问题线下单独沟通。
其实核心的地方,大家只需要注意这几地方:

  1. mysql的数据根据索引的区分度超过30%, 默认不是会走索引的,如果要强制走索引,可以用force index语法来实现,不过不建议这么用,因为在这种情况下,用索引查询也会很慢性;
  2. 遇到了区分度超过30%的数据,要根据实际情况来分析解析,例如用canal就是一个不错的解决方案,也很方便;
  3. 遇到慢sql,不要慌乱,要定到具体的慢sql,以及相关的原因,对症下药,实在不行,说明数据模型上有问题,需要重新评估原来的数据模型;
    4.平时多积累一下mysql优仳器的原理知识,建议大家有空看一下李海翔老师的数据库方面的巨作<<数据库查询优化器艺术>>

image.png

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
6月前
|
人工智能 安全 机器人
无代码革命:10分钟打造企业专属数据库查询AI机器人
随着数字化转型加速,企业对高效智能交互解决方案的需求日益增长。阿里云AppFlow推出的AI助手产品,借助创新网页集成技术,助力企业打造专业数据库查询助手。本文详细介绍通过三步流程将AI助手转化为数据库交互工具的核心优势与操作指南,包括全场景适配、智能渲染引擎及零代码配置等三大技术突破。同时提供Web集成与企业微信集成方案,帮助企业实现便捷部署与安全管理,提升内外部用户体验。
637 12
无代码革命:10分钟打造企业专属数据库查询AI机器人
|
8月前
|
Cloud Native 关系型数据库 分布式数据库
|
8月前
|
并行计算 关系型数据库 MySQL
如何用 esProc 将数据库表转储提速查询
当数据库查询因数据量大或繁忙变慢时,可借助 esProc 将数据导出为文件进行计算,大幅提升性能。以 MySQL 的 3000 万行订单数据为例,两个典型查询分别耗时 17.69s 和 63.22s。使用 esProc 转储为二进制行存文件 (btx) 或列存文件 (ctx),结合游标过滤与并行计算,性能显著提升。例如,ctx 并行计算将原查询时间缩短至 0.566s,TopN 运算提速达 30 倍。esProc 的简洁语法和高效文件格式,特别适合历史数据的复杂分析场景。
|
9月前
|
前端开发 Java 关系型数据库
基于ssm的考研图书电子商务平台,附源码+数据库+论文
考研图书电子商务平台是一个基于Java的B/S架构系统,适用于Windows环境。该平台设有管理员和用户权限,管理员可管理商品、用户、留言板及订单,用户可管理收货地址、订单、收藏及购买商品。技术框架包括前端Vue+HTML+JavaScript+CSS+LayUI,后端SSM,数据库为MySQL。项目包含17个数据库表,支持Maven构建。提供演示视频和详细文档,支持免费远程调试安装,确保顺利运行。
160 13
基于ssm的考研图书电子商务平台,附源码+数据库+论文
|
9月前
|
SQL 关系型数据库 MySQL
如何优化SQL查询以提高数据库性能?
这篇文章以生动的比喻介绍了优化SQL查询的重要性及方法。它首先将未优化的SQL查询比作在自助餐厅贪多嚼不烂的行为,强调了只获取必要数据的必要性。接着,文章详细讲解了四种优化策略:**精简选择**(避免使用`SELECT *`)、**专业筛选**(利用`WHERE`缩小范围)、**高效联接**(索引和限制数据量)以及**使用索引**(加速搜索)。此外,还探讨了如何避免N+1查询问题、使用分页限制结果、理解执行计划以及定期维护数据库健康。通过这些技巧,可以显著提升数据库性能,让查询更高效流畅。
|
9月前
|
前端开发 Java 关系型数据库
基于ssm的培训学校教学管理平台,附源码+数据库+论文
金旗帜文化培训学校网站项目包含管理员、教师和用户三种角色,各角色功能通过用例图展示。技术框架采用Java语言,B/S架构,前端为Vue+HTML+CSS+LayUI,后端为SSM,数据库为MySQL,运行环境为JDK8+Tomcat8.5。项目含12张数据库表,非前后端分离,支持演示视频与截图查看。购买后提供免费安装调试服务,确保顺利运行。
181 14
|
9月前
|
数据库 Python
【YashanDB知识库】python驱动查询gbk字符集崖山数据库CLOB字段,数据被驱动截断
【YashanDB知识库】python驱动查询gbk字符集崖山数据库CLOB字段,数据被驱动截断
|
9月前
|
数据库
【YashanDB知识库】数据库用户所拥有的权限查询
【YashanDB知识库】数据库用户所拥有的权限查询
|
9月前
|
存储 运维 监控
百万指标,秒级查询,零宕机——时序数据库 TDengine 在 AIOps 中的硬核实战
本篇文章详细讲述了七云团队在运维平台中如何利用 TDengine 解决海量时序数据存储与查询的实际业务需求。内容涵盖了从数据库选型、方案落地到业务挑战及解决办法的完整过程,特别是分享了升级 TDengine 3.x 时的实战经验,给到有需要的小伙伴参考阅读。
327 1

热门文章

最新文章