【问题标题】:Spring Boot - how to avoid concurrent access to controllerSpring Boot - 如何避免并发访问控制器
【发布时间】:2020-07-11 21:48:09
【问题描述】:

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

当两个或多个客户端访问服务器上的 API 时,就会出现问题,因为该方法会检查灯是打开还是关闭(在 DB 上)以更改其状态。出现这种情况,如果灯灭了,有2个客户端同时调用服务,第一个打开灯并改变db上的状态,但第二个也访问灯,db上的状态是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);
                        }

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

【问题讨论】:

    标签: java spring


    【解决方案1】:

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

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

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

    在您的情况下,您可以使用正确的isolation level 标记您的 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");
    }
    

    【讨论】:

    • 如果服务有多个副本怎么办?
    【解决方案3】:

    其他人的答案对我来说似乎过于复杂......保持简单。

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

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

    【讨论】:

      【解决方案4】:

      也可以使用ReentrantLock,或者使用同步

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

      【讨论】:

        【解决方案5】:

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

        【讨论】:

          猜你喜欢
          • 2018-05-15
          • 2021-03-23
          • 1970-01-01
          • 2020-03-16
          • 1970-01-01
          • 2021-12-18
          • 1970-01-01
          • 2016-03-30
          • 2015-11-28
          相关资源
          最近更新 更多