01 前情回顾
在这里跟同学们分享一个前几天在线上遇见的 bug…
bug描述:客户端轮询服务端接口获取数据做打字机效果展示,会偶现输出到一半就停止不动了,但是数据还没输出完(如下图,到红色部分就卡住了)。
老泪纵横 😭😭😭
说一下业务场景::
- 这是一个大模型问答的服务
- 我方服务端通过 sse 的方式对接三方大模型
- 我方服务端将请求到的数据通过一个 nextKey 进行缓存
- 我方客户端异步轮询到服务端获取数据
以上简单的概括了一下大致场景,当然细节还是比较多的,这里有同学可能会有疑问:服务端获取到三方的流直接返回给客户端不就好了吗,为啥还要将数据放到缓存再去异步获取呢? 这不费劲吗? 确实,但是为了避免一些复杂的业务所以就不得不这么做了,比如:复杂的数据结构、多模态,ios、安卓、鸿蒙对于 sse 的适配等。
02 步入正题
我先将修改之前的代码贴出来,大家看看能不能找出问题
public CommonResponse<CommonChatVO> tmpData(String nextKey) {
LocalUser user = LocalUserUtils.getUser();
String key = CommonConstant.TMP_DATA_KEY_REDIS + nextKey;
log.info("获取缓存临时数据请求信息: nextKey:{}, username:{}", nextKey, user.getUsername());
CommonChatVO result = null;
int attempts = 0;
while (attempts < tmpDataProperties.getNum()) {
try {
result = (CommonChatVO) redisManager.get(key);
if (result != null) {
if (result.getStatus() == 1) {
log.info("获取数据结束, status=1, 用户标识:{}, nextKey:{}", user.getUsername(), nextKey);
}
break; // 一旦找到数据,就跳出循环
}
Thread.sleep(tmpDataProperties.getWaitTime()); // 如果没有数据,则等待
attempts++;
} catch (InterruptedException e) {
// 处理中断,可能需要记录错误或返回错误响应
Thread.currentThread().interrupt(); // 重新设置中断状态
log.error("轮询过程被中断", e);
}
}
redisManager.del(key);
return CommonResponse.success(result);
}
简单解释一下上面的代码:
- 客户端轮询调用这个接口获取数据,当获取到一个固定的 type 就结束获取,当然这里一定要有一个前提就是如果数据的存储方异常或者失败一定要考虑容错,异常的话也要想办法更新这个结束状态,不然客户端会一直获取。
- 然后服务端为了提升效率在后台用一个while循环去缓存获取数据,结束条件是到达固定的次数或者获取到数据就结束,如果没有获取到并且没有达到阈值那就等待固定的时间再次获取。
上面的代码会有啥 bug 呢
在代码的最后会执行一次删除 key 的操作,乍一看删除 key 也没啥问题 为了避免内存被浪费 获取到数据就把缓存删掉,如果没获取到数据删掉好像也没事(因为缓存里压根也没有)
但是在快速的持续写入和持续获取数据的时候问题就出现了
比如:我们判断缓存中没有数据,然后在在判断之后代码没有进到 if 代码快中,这个时候另一个线程将数据写到了缓存,然后我们的代码恰好又执行到了删除 key 的地方,这里就将刚刚存起来的数据给删掉了, 然后出现的问题就是客户端一直去服务端获取数据一直获取不到,因为数据已经被删掉了,也没有给客户端同学返回结束状态,就出现了本次的 bug。
最后将改造后的代码贴出来
public CommonResponse<CommonChatVO> tmpData(String nextKey) {
LocalUser user = LocalUserUtils.getUser();
String key = CommonConstant.TMP_DATA_KEY_REDIS + nextKey;
log.info("获取缓存临时数据请求信息: nextKey:{}, username:{}", nextKey, user.getUsername());
CommonChatVO result = null;
int attempts = 0;
while (attempts < tmpDataProperties.getNum()) {
try {
result = (CommonChatVO) redisManager.get(key);
if (result != null) {
if (result.getStatus() == 1) {
log.info("获取数据结束, status=1, 用户标识:{}, nextKey:{}", user.getUsername(), nextKey);
}
redisManager.del(key);
break; // 一旦找到数据,就跳出循环
}
Thread.sleep(tmpDataProperties.getWaitTime()); // 如果没有数据,则等待
attempts++;
} catch (InterruptedException e) {
// 处理中断,可能需要记录错误或返回错误响应
Thread.currentThread().interrupt(); // 重新设置中断状态
log.error("轮询过程被中断", e);
}
}
return CommonResponse.success(result);
}
同学们一定要注意 获取到数据之后再执行删除操作,或者是设置超时时间(本次其实是设置了超时时间的),多余删啊。。。
这个业务场景大家还有没有其他的解决方案或者优化方案 希望留言~
本篇文章到这里就结束了,最后送大家一句话:
喷泉之所以好看 是因为他有压力;瀑布之所以壮观 是因为没有退路;滴水之所以穿石 是因为贵在坚持。