多实例的心跳检测不要用lock锁

背景描述

多实例情况下每个实例做了心跳检测,往redis中放值,代码如下:

scss 复制代码
```
@Scheduled(fixedDelayString = "${cost.loop.duration}", timeUnit = TimeUnit.SECONDS)
public void onceReport() throws Exception { 
    for (int i=0;i<5;i++) {
          try (ClosableLocker closableLocker = new ClosableLocker(LsbcConstant.INSTANCE_HEALTH_CHECK_CACHE_KEY)) {
              if (!closableLocker.isLocked()) {
                  //加锁不上,可能其他实例正在报告其状态,等待100ms后再试,总计5次
                  Thread.currentThread().sleep(100);
                  continue;
              }
              //判断实例时否死亡
              Map<String,Long> instanceLastTimeMap = readInstanceHealthCheckMapExcludeDeathTime(loopDuration * 5 * 1000);
              //刷新当前实例的时间。
              instanceLastTimeMap.put(EnvironmentCheckUtil.getInstanceId(),new Date().getTime());
              //重新写入公共缓存   
              writeInstanceHealthCheckMap(instanceLastTimeMap);
              break;
          }
 }

按照以上代码会将每个实例的刷新时间放入Redis中的一个key中,字符串按格式:"instanceId1:time1;instanceId2:time2" 方式存储。转为MAP;

问题描述

在A服务中启用长任务的时候,B服务会查询到执行中的任务,判断服务是否中断,核心逻辑如下: 其中 isInterrupted方法打印出日志发现时B服务把A服务上的长任务中断,原因时map中没有A服务的刷新时间;

vbnet 复制代码
/**
* 判断服务是否超过死亡时间
*/
public static Map<String, Long> readInstanceHealthCheckMapExcludeDeathTime(long deathTime) throws Exception {
    Map<String,Long> result = new HashMap<>();
    if (AppContext.cache().exists(LsbcConstant.INSTANCE_HEALTH_CHECK_CACHE_KEY)) {
        String instanceHealthCheckString  = AppContext.cache().get(LsbcConstant.INSTANCE_HEALTH_CHECK_CACHE_KEY);
        //字符串按格式:"instanceId1:time1;instanceId2:time2" 方式存储。转为MAP
        String[] instanceHealthCheckArray = instanceHealthCheckString.split(";");
        for (String instanceHealthCheck : instanceHealthCheckArray) {
            String[] instanceHealthCheckItem = instanceHealthCheck.split(":");
            if (instanceHealthCheckItem.length == 2) {
                Long historyTime = Long.valueOf(instanceHealthCheckItem[1]);
                if (new Date().getTime() - historyTime > deathTime) {
                    continue;
                }
                result.put(instanceHealthCheckItem[0],Long.valueOf(instanceHealthCheckItem[1]));
            }
        }
    }
    logger.info("readInstanceHealthCheckMapExcludeDeathTime :{}", JSON.toJSONString(result));
}
```
/**
 * 判断是否实际中断
 * @param taskQueue
 * @param serviceId
 * @param instanceLastTimeMap
 * @return
 */
private boolean isInterrupted(ProcessTaskQueue taskQueue, String serviceId, Map<String, Long> instanceLastTimeMap) {
    if (!serviceId.equals(taskQueue.getExeSeviceId()) ){
        //如果任务队列不是当期服务实例,则判断实例健康MAP中是否存在
        if (!instanceLastTimeMap.containsKey(taskQueue.getExeSeviceId())){
            logger.info("非任务服务实例发起的异常中断 taskQueueTaskId:{} serviceId:{} instanceLastTimeMap: {}",taskQueue.getTaskId(),serviceId,JSON.toJSONString(instanceLastTimeMap));
            return true ;
        }
    }else {
        //如果等于当前服务实例。两种情况:A:正常执行中;B:服务实例重启过(其服务实例ID不发生变化,但首次启用为true)
        if (this.bFirstPolling){
            logger.info("任务服务实例发起的异常中断 taskQueueTaskId:{} serviceId:{} bFirstPolling: {}",taskQueue.getTaskId(),serviceId,bFirstPolling);
            return true;
        }
    }

    return false;
}
```
```

问题定位

onceReport()方法中,假设B服务一直拿着锁,导致A服务拿不到从而进一步缓存中没有刷新到A服务的存活时间,进一步B实例认为A实例中断了;

问题解决

onceReport()方法中修改 修改为 hset(String key, String field, String value);

HSET 命令:如果你在多个实例中对 不同的 Key 执行 HSET 操作,Redis 会串行化这些写操作,确保每个 HSET 命令在执行时不会与其他命令交错。因此,对于 不同的 Key,并发写入不会导致数据竞争。

虽然 Redis 本身保证了单个命令的原子性,但在多实例环境下,如果多个实例操作的是 同一个 Key,则可能会出现并发问题。例如: 实例 A 执行 HSET key field1 value1 实例 B 执行 HSET key field2 value2 在这种情况下,Redis 会串行化这两个操作,确保每个操作在执行时是完整的。最终的结果取决于哪个命令最后执行,但不会出现数据交错的问题。

相关推荐
程序员爱钓鱼6 分钟前
Go语言实战案例-批量重命名文件
后端·google·go
大熊计算机8 分钟前
大模型推理加速实战,vLLM 部署 Llama3 的量化与批处理优化指南
后端
程序员爱钓鱼8 分钟前
Go语言实战案例-遍历目录下所有文件
后端·google·go
青云交9 分钟前
Java 大视界 -- Java 大数据机器学习模型在金融市场波动预测与资产配置动态调整中的应用(355)
java·大数据·机器学习·lstm·金融市场·波动预测·资产配置
喵个咪14 分钟前
WSL2下的Ubuntu 24.0突然apt update报错 Could not wait for server fd 的解决方案
后端
徐子童17 分钟前
初识Redis---Redis的特性介绍
java·数据库·redis
赵星星52020 分钟前
Cursor如何解决循环依赖!看完太妙了!
后端
Dubhehug29 分钟前
6.String、StringBuffer、StringBuilder区别及使用场景
java·面试题·stringbuilder·string·stringbuffer
枣伊吕波1 小时前
第十八节:第七部分:java高级:注解的应用场景:模拟junit框架
java·数据库·junit
白鲸开源1 小时前
从批到流,Zoom 基于 DolphinScheduler 的流批统一调度系统演进
java·大数据·开源