系统架构:
系统用的是ngixn 、spirngcloud、nacos作为注册中心、gateway网管,数据库是mysql
数据库只有一个
服务也是单实例的
所以没有办法做负载均衡什么的。
问题描述:
公司近期开了几个活动之后由于访次数较多和代码问题导致系统挂了。
启动起来没多久后系统又崩溃了。
分析原因后发现是有首页一次性请求多个接口并且多个接口响应均在1~2秒,请求人数过多后有部分人出现了页面在等待响应的情况,等待响应人群因为响应时间过长又在频繁的切换刷新页面,系统也没对此类频繁刷新的请求进行限制,从而导致很多的无效请求产生,这些无效请求在未接收到响应之后连接就被断开了。
数据库压力比较大。
分析问题:
一打开首页就很卡顿,看控制台打开一次首页10多次请求,多个接口响应均在1~2秒。
其中红框中的这些请求里面有楼层商品,秒杀商品、特价商品等。
分析代码我发现查询活动商品需要经历大概以下步骤,我挑了其中两个列子。
查询满赠商品
SELECT * FROM ums_member_company_info WHERE id = 760; -- 返回客户的公司类型
SELECT * FROM ums_salesman_member_binding_relation WHERE company_id = 760; -- 返回用户绑定的业务员id
SELECT * FROM ums_salesman_info WHERE salesman_id = 3385; -- 返回业务员所在的省区
SELECT * FROM pms_special_goods_activity; -- 查询开启满赠活动的商品
SELECT * FROM pms_special_goods_activity_enable_area WHERE activity_id =1 and province_code = 6; -- 查询活动开启的省区 传入业务员省区
SELECT * FROM pms_special_goods_activity_enable_business_type WHERE activity_id =1 and business_type =4; -- 查询活动开启的公司类型
SELECT * FROM pms_product_info WHERE product_id in('12','13'); -- 用活动返回的商品id去查询商品信息
SELECT * FROM pms_product_area_relation WHERE product_id in('12','13') and province_code =6; -- 查询商品开放的省区,传入业务员省区
SELECT * FROM pms_product_price WHERE product_id in ('12','13'); -- 查询商品定价;
SELECT * FROM pms_product_images WHERE product_id in ('12','13'); -- 查询商品默认图片;
SELECT * FROM pms_product_stock WHERE product_id in ('12','13'); -- 查询商品的库存
查询秒杀商品
SELECT * FROM ums_member_company_info WHERE id = 760; -- 返回客户的公司类型
SELECT * FROM ums_salesman_member_binding_relation WHERE company_id = 760; -- 返回用户绑定的业务员id
SELECT * FROM ums_salesman_info WHERE salesman_id = 3385; -- 返回业务员所在的省区
SELECT * FROM pms_sec_spec_price_activity WHERE FIND_IN_SET(2,business_type); -- 查询开启秒杀活动的商品,传入客户的公司类型
SELECT * FROM pms_sec_spec_price_activity_area WHERE activity_id =1 and province_code = 6; -- 查询秒杀活动开启的省区 传入业务员省区
SELECT * FROM pms_sec_spec_price_activity_product WHERE activity_id =1; -- 查询秒杀活动开启的商品
SELECT * FROM pms_product_info WHERE product_id in('12','13'); -- 用活动返回的商品id去查询商品信息
SELECT * FROM pms_product_area_relation WHERE product_id in('12','13') and province_code =6; -- 查询商品开放的省区,传入业务员省区
SELECT * FROM pms_product_price WHERE product_id in ('12','13'); -- 查询商品定价;
SELECT * FROM pms_product_images WHERE product_id in ('12','13'); -- 查询商品默认图片;
SELECT * FROM pms_product_stock WHERE product_id in ('12','13'); -- 查询商品的库存
导致系统挂掉的主要原因也在这里
- 之前的查询是先查询活动商品,然后在
for循环里执行sql
面查询商品的基础信息、价格、图片、库存。 - 活动、商品、都是需要用到客户的公司类型,以及绑定的业务员的省区,所以每次查询商品的时候都会先查询ums_member_company_info
和ums_salesman_member_binding_relation 、ums_salesman_info 表。
其实上面的问题平时写代码也早就发现了,因为这个系统是第三方交付过来的,一开始设计就有不合理的情况。
一开始就有很多在for循环里面查询sql的问题。还有一些表可以合并的表拆分成了几张表,要优化需要改动以前的代码,且不说没有时间而且也属于吃力不讨好的行为。
随着后面新需求不断累加,开发时间紧张,后面又很多地方增加了根据登录门店公司类型、和门店绑定业务员省区来过滤商品过滤活动,只能在原有的基础上堆代码,导致后面在线人数增多系统崩溃。
针对以上的问题我们先后进行了四次优化。
四次优化
第一次优化
取消所有服务aop打印日志、只保留了sql日志,减少服务器io的压力
第二次优化
我们针对在for循环里执行sql
进行了改造,全部都移除到了for循环外面,统一查询然后匹配商品id塞入需要的数据。为什么这样做呢?是因为当在for循环里面执行sql时会有以下情况
在for循环里面每次循环都打开和关闭数据库连接,这会消耗大量的时间。
优化后大概是这个样子,在循环外面统一查询,然后再塞入。
在优化后发现确实有所改善,但是接口的响应速度依旧不快,随后又进行了优化。
第三次优化
后面我们采用了比较极端的办法,将所有商品信息、商品的开放的区域、开放的活动、库存、价格、省区价格
用一个sql作为查询,执行一次sql查询所有的详细信息,然后将这个sql创建为视图
。
使用时是这样的、多个连表最终变成了一张表
这次上线之后接口相应的速度比之前快了好几倍,响应时间在
100毫秒以下
。但是这样处理也有问题
问题1:这个视图的sql比较复杂后期维护比较困难,sql不能出一点错误。
问题2:1个商品会对多个省区开放,所以一个商品返回的数据是商品*省区数
因为视图是没办法传入参数的,所以取决于开始查询的第一张商品表的数据量大小,如果后续商品表数据量很大这个sql依旧会变慢
问题3:视图原本应该是比较通用的,这里把活动也维护进去了,而且用户能不能参与活动是在视图外面控制,最终的价格也是,-但是没办法领导要求的,要极致的速度
。
第四次优化
我们查询服务器日志发现有很多sql在查询用户表,数据库压力增大。
代码中所有查询活动或者查询商品,都需要先执行下面的sql查询到公司类型和业务员所在省区,也就是上面xml中查询方法里面的#{salesmanProvinceCode}、和#{companyBusinessType},sql如下:
SELECT * FROM ums_member_company_info WHERE id = 760; -- 返回客户的公司类型
SELECT * FROM ums_salesman_member_binding_relation WHERE company_id = 760; -- 返回用户绑定的业务员id
SELECT * FROM ums_salesman_info WHERE salesman_id = 3385; -- 返回业务员所在的省区
由于时间关系合并表肯定是来不及了,所以将这些数据统一丢入到了缓存中。
但是这样也会有问题数据库的数据如果更新了缓存没办法及时。
但系统处于崩溃边缘也管不了那么多了,所以最后我们给缓存设置了一天过期的时间,并在客户重新登录时更新缓存中的数据。
经过最后的优化系统在1000+以上人使用时也不会崩溃响应速度也很快。
总结
经过这次问题我发现大部分程序员都不愿意写sql,更加不愿意写连表的sql。