Spring Boot -如何避免并发访问控制器



我们有一个Spring Boot应用程序,它链接到该领域的各种客户端。这个应用程序有一个控制器,它可以从客户端调用,并与DB和物理开关交互,以关闭或打开灯。

当两个或多个客户端访问服务器上的API时,问题就出现了,因为该方法检查灯(在DB上)是开着还是关着以改变其状态。如果灯是关闭的,并且两个客户端同时调用服务,第一个客户端打开灯并改变db上的状态,但是第二个客户端也访问灯,db上的状态是关闭的,但是第一个客户端已经打开了灯,所以第二个客户端最终关闭它,以为打开它…也许我的解释是有点不清楚,问题是:我可以告诉spring访问控制器一个请求的时间?

由于下面的答案,我们在切换开关的方法上引入了悲观锁,但我们仍然从客户端获得200状态…

我们正在使用spring boot + hibernate

现在控制器有悲观锁的异常

  try {
                                String pinName = interruttore.getPinName();
                                // logger.debug("Sono nel nuovo ciclo di
                                // gestione interruttore");
                                if (!interruttore.isStato()) { // solo se
                                                                // l'interruttore
                                                                // è
                                                                // spento
                                    GpioPinDigitalOutput relePin = interruttore.getGpio()
                                            .provisionDigitalOutputPin(RaspiPin.getPinByName(pinName));
                                    interruttoreService.toggleSwitchNew(relePin, interruttore, lit);                                                            // accendo
                                    interruttore.getGpio().unprovisionPin(relePin);
                                }

                        } catch (GpioPinExistsException ge) {
                            logger.error("Gpio già esistente");
                        } catch (PessimisticLockingFailureException pe){
                            logger.error("Pessimistic Lock conflict", pe);
                            return new ResponseEntity<Sensoristica>(sensoristica, HttpStatus.CONFLICT);
                        }

toggleSwitchNew如下

@Override
@Transactional(isolation=Isolation.REPEATABLE_READ)
public void toggleSwitchNew(GpioPinDigitalOutput relePin, Interruttore interruttore, boolean on) {
    Date date = new Date();
    interruttore.setDateTime(new Timestamp(date.getTime()));
    interruttore.setStato(on);
    String log = getLogStatus(on) + interruttore.getNomeInterruttore();
    logger.debug(log);
    relePin.high();
    try {
        Thread.sleep(200);
    } catch (InterruptedException e) {
        logger.error("Errore sleep ", e);
    }
    relePin.low();
    updateInterruttore(interruttore);
    illuminazioneService.createIlluminazione(interruttore, on);

}

然后我们在客户端记录请求状态码他们总是得到200即使他们是并发的

这是一个经典的锁定问题。您可以使用悲观锁定:只允许一个客户端同时操作数据(互斥),也可以使用乐观锁定:允许多个并发客户端操作数据,但只允许第一个提交者成功。

根据你所使用的技术,有许多不同的方法可以做到这一点。例如,解决这个问题的另一种方法是使用正确的数据库隔离级别。在你的情况下,你似乎至少需要"可重复读取"。隔离级别。

可重复读取将确保如果两个并发事务或多或少并发地读取和修改同一记录,只有一个会成功。

在您的情况下,您可以用正确的隔离级别标记Spring事务。

@Transactional(isolation=REPEATABLE_READ)
public void toggleSwitch() {
    String status = readSwithStatus();
    if(status.equals("on") {
         updateStatus("off");
    } else {
         updateStatus("on");
    }
}

如果两个并发客户端尝试更新交换机状态,第一个提交的将获胜,第二个将总是失败。您只需要准备好告诉第二个客户机,由于并发故障,它的事务没有成功。第二个事务将自动回滚。您或您的客户端可以决定是否重试。

@Autowire
LightService lightService;
@GET
public ResponseEntity<String> toggleLight(){
   try {
       lightService.toggleSwitch();
       //send a 200 OK
   }catch(OptimisticLockingFailureException e) {
      //send a Http status 409 Conflict!
   }
}

但是正如我所说的,取决于你使用的是什么(例如JPA, Hibernate,普通JDBC),有多种方法可以使用悲观或乐观的锁定策略来做到这一点。

为什么不只是线程同步?

到目前为止建议的其他答案是关于悲观锁定,通过在线程级别使用同步块使用Java的互斥,如果您有一个单个JVM运行您的代码,这可能会起作用。如果您有多个JVM运行您的代码,或者如果您最终横向扩展并在负载平衡器后面添加更多JVM节点,那么此策略可能被证明是无效的,在这种情况下,线程锁定将不再解决您的问题。

但是您仍然可以在数据库级别实现悲观锁定,通过强制进程在更改数据库记录之前锁定数据库记录,并通过在数据库级别创建互斥区。

所以,这里重要的是理解锁定原则,然后找到适用于您的特定场景和技术堆栈的策略。在您的情况下,最有可能的是,它将涉及某种形式的锁定在某个数据库级别。

别人的答案在我看来过于复杂了……保持简单。

让请求具有新值而不是切换。控制器内放置一个synchronized块。只有当新值与当前值不同时,才在同步块内执行操作。

Object lock = new Object();
Boolean currentValue = Boolean.FALSE;
void ligthsChange(Boolean newValue) {
  synchronized(lock) {
    if (!currentValue.equals(newValue)) {
      doTheSwitch();
      currentValue = newValue;
    }
  }
}

也可以使用ReentrantLock,或者使用synchronized

public class TestClass {
private static Lock lock = new ReentrantLock();
 public void testMethod() {
        lock.lock();
        try {         
            //your codes here...
        } finally {
            lock.unlock();
        }
    }
}

我觉得这个API违反了PUT API应该是幂等的规则。最好有单独的turn和off api,这样可以避免这个问题。

使用synchronized -但是如果你的用户点击足够快,那么你仍然会有一个命令在另一个命令之后立即执行的问题。

Synchronized将确保只有一个线程执行

中的块
synchronized(this) { ... } 

您可能还希望在快速连续中引入延迟和拒绝命令。

try {
    synchronized(this) {
        String pinName = interruttore.getPinName();                     
            if (!interruttore.isStato()) { // switch is off
            GpioPinDigitalOutput relePin = interruttore.getGpio()
                .provisionDigitalOutputPin(RaspiPin.getPinByName(pinName));
            interruttoreService.toggleSwitchNew(relePin, interruttore, lit); // turn it on
            interruttore.getGpio().unprovisionPin(relePin);
       }
   }
} catch (GpioPinExistsException ge) {
    logger.error("Gpio già esistente");
}

您也可以使用基于缓存的锁定。下面是一个使用redis缓存的例子。你甚至可以允许等幂键,以防你也需要并发但有限制的访问。

@Autowired
CacheManager cacheManager;
@PostMapping(path = "/upload", consumes=MediaType.MULTIPART_FORM_DATA_VALUE)
public void uploadFile(@RequestPart String idempotencyKey,
                       HttpServletResponse response) {
    Cache cache = cacheManager.getCache(CacheConfig.GIFT_REDEEM_IDEMPOTENCY);
    if (cache.get(idempotencyKey) != null) {
        throw new ForbiddenException("request being processed with the idempotencyKey = " + idempotencyKey);
    }
    cache.put(idempotencyKey, "");
    try {...
        }
}

最新更新