我们有一个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
答案 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,这样可以避免此问题。