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

时间:2016-11-01 09:11:30

标签: java spring

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

当两个或多个客户端访问服务器上的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);
                        }

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

5 个答案:

答案 0 :(得分:7)

这是一个经典的锁定问题。您可以使用pessimistic locking:当时只允许一个客户端对数据进行操作(互斥)或optimistic locking:允许多个并发客户端对数据进行操作但仅允许第一个承诺成功。

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

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

在您的情况下,您可以使用正确的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节点,则此策略可能会失效,在这种情况下,线程锁定将无法解决您的问题。

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

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

答案 1 :(得分:1)

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

而不是切换使请求具有新值。内部控制器放置void GameStateStart::draw() { this->game->window.clear(); /*Modify the colour of the background*/ background.getRectangle().setFillColor(sf::Color::Green); background.drawObject(&game->window); /*Create the font for the text*/ sf::Font font; font.loadFromFile("Pacifco.ttf"); /*Test*/ sf::Text title2; title2.setFillColor(sf::Color::Blue); title2.setFont(font); title2.setPosition(sf::Vector2f(300, 100)); title2.setString("Tic Tac SFML"); title2.setCharacterSize(25); this->game->window.draw(title2); /*Edit title text objects*/ title.getText()->setFillColor(sf::Color::Blue); title.getText()->setFont(font); title.getText()->setPosition(sf::Vector2f(300, 100)); title.getText()->setString("Tic Tac SFML"); /*Edit instructions text object*/ instructions.getText()->setFillColor(sf::Color::Blue); instructions.getText()->setFont(font); instructions.getText()->setPosition(sf::Vector2f(300, 450)); instructions.getText()->setString("Press the spacebar to continue"); /*draw the 2 text objects*/ title.drawObject(&this->game->window); instructions.drawObject(&this->game->window); this->game->window.display(); return; } 块。仅当新值与当前值不同时,才在同步块内执行操作。

synchronized

答案 2 :(得分:0)

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

答案 3 :(得分:0)

还可以使用ReentrantLock,或使用同步

public class TestClass {

private static Lock lock = new ReentrantLock();

 public void testMethod() {
        lock.lock();
        try {         
            //your codes here...
        } finally {
            lock.unlock();
        }
    }
}

答案 4 :(得分:0)

我认为此API违反了PUT API应该是幂等的规则。最好有单独的打开和关闭API,这样可以避免此问题。