CASE1:优付公众号上的自由职业者签约
优付公众号上的自由职业者签约——用户关注我司公众号,访问公众号里的自由职业者签约H5,在这个H5页面上完成自由职业者签约。
程序实现逻辑是,页面通过微信公众号静默授权方式获取openId后,携带openId调用后端“获取自由职业者信息”接口。
后端程序是SSM框架:SohoSignController → SohoSignService → SohoSignDAO → SohoSignMapper.xml
@RestController class SohoSignController { @Autowired SohoSignService sohoSignService; @GetMapping("/sohosign") public Result sohoSign(String openId) { Map<String, Object> mapQuery = new HashMap<String, Object>(); mapQuery.put("openId", openId); return Result.success(sohoSignService.selectSohoList(mapQuery)); } } @Service class SohoSignService { @Autowired SohoSignDAO sohoSignDAO; public List<Soho> selectSohoList(Map<String, Object> mapQuery) { return sohoSignDAO.selectSohoList(mapQuery); } } interface SohoSignDAO { List<Soho> selectSohoList(Map<String, Object> mapQuery); } SohoSignMapper.xml <select id="selectSohoList" parameterType="java.util.Map" resultMap="BaseResultMap"> select * from t_soho a <where> <if test="mobile != null"> and a.mobile = #{mobile} </if> <if test="openId != null"> and a.open_id = #{openId} </if> </where> order by a.id desc </select>
t_soho表有300w+数据量,open_id字段上有索引。每个open_id存在10多条数据记录。因此,按openId查询数据没有性能问题。
代码看似正常。
突然某几天里,优付这个签约服务tomcat频繁出现假死和OOM。
后来经排查,才知道,有人直接访问了这个签约url——是的,没有传openId参数——这意味着什么?技术敏感的同学必然有答案。
CASE2:老风控的一个不起眼的定时任务,引发蝴蝶效应
老risk里有一个定时任务,每天凌晨4点跑。程序逻辑里,有一段代码是根据用户的身份证号获取用户姓名。见下方第2个方法 getUserNameByIdCard ,所调用的UserAPI#list在 zhenghe-provider 服务里。
-- com.emax.risk.provider.modules.riskstrategy.provider.BaseRiskStrategyService#handleRiskWarningPerson for (TotalMerSohoMonth sohoMonth: personList) { ... warning = new RiskPersonalWarning(); warning.setIdCard(sohoMonth.getIdCard()); //获取userName String userName = riskCommonService.getUserNameByIdCard(sohoMonth.getIdCard()); if (StringUtils.isNotEmpty(userName)){ warning.setUserName(userName); } ... } -- com.emax.risk.provider.modules.riskauth.RiskCommonService#getUserNameByIdCard public String getUserNameByIdCard(String idCard){ //先查数据中心,数据中心查不到再查整合 List<SbhSoho> sbhSohoList = sbhSohoManager.list(new LambdaQueryWrapper<SbhSoho>() .eq(SbhSoho::getIdCard, AesEncryptUtil.encrypt(idCard))); if (CollectionUtils.isNotEmpty(sbhSohoList)){ return sbhSohoList.get(0).getName(); } Result<List<UserDTO>> listResult = userAPI.list(new UserQuery().setIdcardNo(idCard)); if (listResult.isSuccess()){ List<UserDTO> result = listResult.getResult(); if (CollectionUtils.isNotEmpty(result)){ return result.get(0).getUserName(); } } return ""; }
在 getUserNameByIdCard 方法中,SbhSohoManager#list操作的数据表 sbh_soho 有 13,602,397条数据,UserAPI#list操作的数据表 t_user 有 3,988,931 条数据。 这两个表均有10多个字段,并且在身份证号字段上均有索引。
可是,然而,but,however,正是这段代码的不严谨,导致 zhenghe-provider 服务出现假死。在假死期间,bosskg的结算下发调用zhenghe-provider的协议校验接口失败,导致用户结算失败。
这段代码有什么问题?问题出在 getUserNameByIdCard 方法未对入参做非空校验。
题外顺便问一句:getUserNameByIdCard 方法中所调用的SbhSohoManager#list,会查 sbh_soho 表的全量数据吗?
CASE3:渠道商docker容器内存不足故障
上周四晚间(20240628)渠道商发版的时候,出现了一个奇怪的现象。agent-system 所在的POD出现假死,重启后,过几分钟又假死。
经分析agent-system服务的日志,发现有好几个大文本日志,原来是调用 AreaAPI时,未指定请求参数,导致返回了全量的4.7万条area数据。
agent-system服务设置的容器运行内存上限是1.5G(spec.containers[].resources.limits.memory = 1500Mi),POD内存不足导致容器中的agent-system程序崩溃或无法正常运行。
下面是 agent-system 里获取area数据的这几行代码,这是一个RestAPI,根据特定条件(页面传参)调用 zhenghe-provider 的 AreaAPI 查询行政地区数据。
@PostMapping("/list") @ApiOperation(value = "根据条件获取地区列表") public Result<List<PubAreaVO>> getAreasByVO (@RequestBody PubAreaVO vo) { return areaAPI.list(vo); }
系统业务方面,是渠道商的收款银行卡在本次迭代发版中新增了所在城市的功能。在渠道商列表页面,编辑渠道商数据时,弹出框窗体会自动带出来所属省份和城市。
之所以会出现“查询地区全量数据”这种情况, 是由于存量的渠道商收款银行卡数据记录的地区字段值是null,而上面的RESTAPI 程序未校验空值数据,直接请求了 zhenghe-provider 的RPC接口。碰巧,zhenghe-provider 的 AreaAPI#list里,也没有对入参做必要的非空判断,进而查了全表数据。
@Override public Result<List<PubAreaVO>> list(PubAreaVO vo) { LambdaQueryWrapper<PubArea> wrapper = getWrapperByVO(vo); List<PubArea> list = pubAreaManager.list(wrapper); List<PubAreaVO> result = new ArrayList<>(); list.forEach(pubArea -> { PubAreaVO pubAreaVO = BeanMapper.map(pubArea, PubAreaVO.class); result.add(pubAreaVO); }); return Result.success(result); }
原罪:上面3个情况,都败在了基本的数据校验。
那么,先来个灵魂拷问三连击↓
- 如何修正我们的程序?
- 如何全面修正我们的程序?
- 如何更彻底地规避这类因全表查询所带来的BUG?
接下来,我们逐个解答。
🍀 1. 如何修正我们的程序?
哪里出问题,就解决哪里。
对于CASE1,入口对openId做参数判空。
对于CASE2,同CASE1。
对于CASE3,同CASE2。
🍀 2. 如何全面修正我们的程序?
按上面 🍀1 的修正方式,调用入口是修正了,那,其他的调用入口呢?
就是说,两头都要修正。
对于RPC接口,接口服务提供者自然是要做参数校验的。——这是你的职责,如果以“调用方做了判空了,我这里就不用了”,那你太单纯了,单纯到不靠谱。
🍀 3. 如何更彻底地规避这类因全表查询所带来的BUG?
按上面 🍀2 的修正方式,好像可以高枕无忧了,按说也挺全面了。
如果我再提个问题,也许会让你头疼———— 上面CASE涉及到的大数据量表 t_soho、sbh_soho、t_user、pub_area 这些表,修正后的程序引发OOM的概率小了, 那么,我们的系统里还存在着 诸如 task_apply、order_detail、tax_user_sign 等等这些 大数据量表, 这些表的程序 是不是也会引发OOM呢?
会的,一定会!
该-怎么办?
MybatisSqlSelectLimitInterceptor 对PreparingSQL整体拦一道。
2024-07-25
I have a dream. -CQRS
REF:
添加评论