开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
背景
今天在对接口进行测试的时候,发现远程调用一直超时,于是我就翻开被调用服务的日志,然后就傻眼了,因为日志一直在不停的刷SQL,并且SQL会不停的往后拼接以clinic_number作为查询条件,进入了一个死循环。
排查问题
其实通过日志找到出问题的代码很容易,这里就不过多赘述,直接贴截图。
相信大家看到代码,瞬间就明白了日志中出现的现象的原因了。
首先,代码中的死循环是造成这一现象最直接的原因;
然后,在每次循环的时候,都会执行wrapper.eq("clinic_number", clinicNumber) ,相当于会在之前查询条件的基础上再拼接一个clinic_number。
这些就直接造成了上述日志的现象。
深入问题
但又是什么造成了死循环一直是死循环,死循环没被打破的原因又是啥呢?
看代码就知道,无非是每次循环都查询到了数据库中已经有了相同的clinicNumber数据了。
而从日志可以看到,clinicNumber数据一直都停留在221115000002上。
也就是说,clinicNumber=221115000002这条数据明明在数据库中已经有了,可还是在不停循环去数据库查询是否存在这条数据。
这不仅代码陷入了死循环,逻辑上也陷入了死循环!
所以这个死循环的根源在于为啥每次循环clinicNumber还是221115000002?
于是,我的目光就被转移到这行代码上了。
clinicNumber = idGenerate.getRandom(dateStr, 6);
这是通过id生成器服务获取新的流水号的代码。
所以我们得去看看id生成器服务是怎么生成新的流水号的。
我觉得代码写的还是挺精简的,粘出来给大家看看,部分给出一些注释。
public Long getRandom(String day, int formatNum) {
return dayIdGenerate.next2(day, formatNum);
}
public long next2(String day, int formatNum) {
SimpleDateFormat format = new SimpleDateFormat("yyMMdd");
// 获取当前时间字符串
String now = format.format(new Date());
// 获取当前时间的前一天字符串
String preday = this.preDay(now);
return this.next(day, preday, formatNum);
}
public long next(String day, String preday, int formatNum) {
// 获取到nacos配置中心的配置对象
LuaScript luaScript = (LuaScript)this.luaScriptConfig.getLuas().get("DAYID");
// 为对象这是key属性
luaScript.setKeys(new String[]{day + formatNum, preday + formatNum});
return this.formatDayId(this.next(luaScript), day, formatNum);
}
public Long next(LuaScript luaScript) {
// 执行lua脚本
List<Long> results = this.luaScriptLongService.execScript(luaScript.getLuasha(), luaScript.getSlot(), luaScript.getKeyNum(), luaScript.getKeys());
return (Long)results.get(0);
}
相信大家看到这就明白了这个id生成器的原理:利用lua脚本+redis实现。那么,这个lua脚本代码如何实现就很关键了。
综合上述Java代码来分析一下这个lua代码,看看他做了啥。
首先这个lua代码定义了几个变量并赋值。
从上述Java代码可以知道KEYS[1] = day + formatNum,也就是传入的日期字符串+一个数字;KEYS[2] = preday + formatNum,也就是当前时间的前一天的字符串+一个数字。
Redis Incrby 命令将 key 中储存的数字加上指定的增量值。
如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCRBY 命令。
再来看看他们的执行逻辑:
- 获取redis中以todayKey为key的值并赋值给count,并且给这个key的值+1;
- 然后判断redis中是否存在prefix..'_'..KEYS[2]这个key的数据,如果存在则删除这条数据(说白了,这行代码就是做了个安全措施,删除前一天的id生成器缓存,防止后面占用缓存较大);
- 最后count+1;
所以理论上,这个lua代码没啥问题啊。
然后我就回头看我的测试数据,发现这个day的值传的昨天的日期,瞬间我就明白了。
因为每次执行lua脚本的时候,他都会删掉昨天的key缓存;而我传入的刚好又是昨天的日期,也就是说每次都会获取昨天的key的缓存,然后再把昨天的缓存删掉,那最后每次获取的数据肯定都一样啊。
总结问题
没想到最终造成这一系列问题的原因,没想到是我传入的参数有问题。。。
但我觉得之前的Java代码也是有一定的问题的,修改后的代码如下:
// 校验就诊日期不应小于当前日期
if (visitDate.getTime() < DateUtil.parse2(DateUtil.format(new Date(),DateUtil.DATE)).getTime()){
throw new RuntimeException("就诊日期小于当前时间,请选择正确号源");
}
Boolean flag = true;
Long clinicNumber = null;
SimpleDateFormat format = new SimpleDateFormat("yyMMdd");
String dateStr = format.format(visitDate);
while (flag) {
clinicNumber = idGenerate.getRandom(dateStr, 6);
//判断是否存在
QueryWrapper<OutpClinicRegistration> wrapper = new QueryWrapper<>();
wrapper.eq("enable", Constants.Enable_Flag.Effective.getValue());
wrapper.eq("is_red", "0");
wrapper.eq("hos_id", hosId);
wrapper.eq("clinic_number", clinicNumber);
List<OutpClinicRegistration> list = outpClinicRegistrationMapper.selectList(wrapper);
if (UtilValidate.isEmpty(list)) {
flag = false;
}
}
return clinicNumber;
最后说一句,我觉得这个死循环的次数也得做个限制,xdm觉得呢?欢迎来讨论。
文中如有不足之处,欢迎指正!一起交流,一起学习,一起成长 ^v^