弹簧启动 - 如何避免并发访问控制器

2022-09-05 00:40:08

我们有一个Spring Boot应用程序,该应用程序链接到现场的各种客户端。此应用程序具有一个控制器,该控制器从客户端调用,并与数据库和物理交换机进行交互,以关闭或打开灯。

当两个或多个客户端访问服务器上的 API 时,问题就来了,因为该方法会检查指示灯是打开还是关闭(在 DB 上)以更改其状态。如果指示灯为 OFF,并且 2 个客户端同时调用服务,则第一个客户端打开指示灯并更改 db 上的状态,但第二个客户端也访问指示灯,DB 上的状态为 OFF,但第一个客户端已经调谐了指示灯,因此秒数最终关闭它,以为打开它...也许我的解释有点不清楚,问题是:我能告诉spring在当时访问控制器一个请求吗?

由于下面的答案,我们在切换开关的方法上引入了悲观锁定,但我们继续从客户那里获得200状态...

我们正在使用弹簧靴+冬眠

现在控制器有悲观锁的例外

  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


答案 1

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

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

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

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

@Transacational(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 节点,则此策略可能被证明是无效的,在这种情况下,线程锁定将不再解决您的问题。

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

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


答案 2

使用同步 - 但是如果您的用户单击足够快,那么您仍然会遇到问题,因为一个命令将紧接一个地执行。

同步将确保只有一个线程执行块

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");
}

推荐