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

bes*_*art 3 java spring

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

当两个或多个客户端访问服务器上的API时会出现问题,因为该方法会检查指示灯是打开还是关闭(在数据库上)以更改其状态.如果指示灯熄灭,并且2个客户端同时调用该服务,则第一个打开指示灯并更改数据库上的状态,但第二个访问指示灯也是如此,数据库上的状态为OFF但是第一个客户端已经调整了灯光,所以秒钟最终将其关闭以为打开它...也许我的解释有点不清楚,问题是:我可以告诉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);
                        }
Run Code Online (Sandbox Code Playgroud)

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


}
Run Code Online (Sandbox Code Playgroud)

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

Edw*_*rzo 8

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

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

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

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

@Transacational(isolation=REPEATABLE_READ)
public void toggleSwitch() {
    String status = readSwithStatus();
    if(status.equals("on") {
         updateStatus("off");
    } else {
         updateStatus("on");
    }
}
Run Code Online (Sandbox Code Playgroud)

如果两个并发客户端尝试更新交换机状态,则第一个提交将获胜,第二个将始终失败.您必须准备好告诉第二个客户端由于并发故障导致其事务未成功.第二个事务会自动回滚.您或您的客户可能决定是否重试.

@Autowire
LightService lightService;

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

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

为什么不只是线程同步?

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

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

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

  • 谢谢,这个解释很清楚,我认为悲观的数据库锁定是这种情况下的最佳策略 (2认同)