这行代码是怎么造成死循环的?

3,130 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

背景

今天在对接口进行测试的时候,发现远程调用一直超时,于是我就翻开被调用服务的日志,然后就傻眼了,因为日志一直在不停的刷SQL,并且SQL会不停的往后拼接以clinic_number作为查询条件,进入了一个死循环。

1668586068766.png

排查问题

其实通过日志找到出问题的代码很容易,这里就不过多赘述,直接贴截图。

1668586134282.png

相信大家看到代码,瞬间就明白了日志中出现的现象的原因了。

首先,代码中的死循环是造成这一现象最直接的原因;

然后,在每次循环的时候,都会执行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脚本代码如何实现就很关键了。

1668586222328.png

综合上述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^