java Spring Boot - 如何避免并发访问控制器
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/40357006/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Spring Boot - how to avoid concurrent access to controller
提问by besmart
We have a Spring Boot application which is linked to various client on the field. This application has a controller which is called from the clients and interact with the DB and with a physical switch, to turn off or on a light.
我们有一个 Spring Boot 应用程序,它链接到现场的各种客户端。这个应用程序有一个控制器,它从客户端调用并与数据库和物理开关交互,以关闭或打开灯。
The problem comes when two or more clients access an API on the server, because the method checks if the light is on or off (on the DB) to change its status. It occurs that if the light is OFF, and 2 clients call the service at the same time, the first turns on the light and change the status on the db but the second access the light too, the status is OFF on the DB but the first client has already tuned on the light, so the seconds eventually shuts it down thinking to turn it on... Maybe my explanation is a little unclear, the problem is: can I tell spring to access a controller one request at the time?
当两个或多个客户端访问服务器上的 API 时,问题就出现了,因为该方法会检查指示灯是打开还是关闭(在 DB 上)以更改其状态。发生这种情况,如果灯灭,并且 2 个客户端同时调用服务,第一个打开灯并更改 db 上的状态,但第二个也访问灯,DB 上的状态为 OFF,但第一个客户端已经打开了灯,所以秒最终关闭它想打开它......也许我的解释有点不清楚,问题是:我可以告诉 spring 在一个请求时访问控制器吗?
Thanks to the answer below, we introduced pessimistic lock on the method that toggles the switch, but we continue to have a 200 status from our clients...
感谢下面的回答,我们在切换开关的方法上引入了悲观锁定,但我们仍然有来自客户的 200 状态......
We are using spring boot + hibernate
我们正在使用 spring boot + hibernate
now the controller has the exception for pessimistic lock
现在控制器有悲观锁的例外
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
is as follows
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);
}
Then we log the request status code in our clients and they alway get 200 even if they are concurrent
然后我们在客户端中记录请求状态代码,即使它们是并发的,它们也总是得到 200
回答by Edwin Dalorzo
This is a classical locking problem. You can either use pessimistic locking: by allowing only one client at the time to operate on the data (mutual exclusion) or by optimistic locking: by allowing multiple concurrent clients to operate on the data but allowing only the first committer to succeed.
这是一个经典的锁定问题。您可以使用悲观锁定:一次只允许一个客户端操作数据(互斥)或乐观锁定:允许多个并发客户端操作数据但只允许第一个提交者成功。
There are many different ways to do that depending on the technology you are using. For example, an alternative way to solve it would be by using the right database isolation level. In your case it seems you need at least "repeatable read" isolation level.
根据您使用的技术,有许多不同的方法可以做到这一点。例如,另一种解决方法是使用正确的数据库隔离级别。在您的情况下,您似乎至少需要“可重复读取”隔离级别。
Repeatable read will ensure that if two concurrent transactions read and change the same record more or less concurrently, only one of them will succeed.
可重复读取将确保如果两个并发事务或多或少并发地读取和更改同一条记录,则只有其中一个会成功。
In your case you could mark your Spring transaction with the right isolation level.
在您的情况下,您可以使用正确的隔离级别标记您的 Spring 事务。
@Transacational(isolation=REPEATABLE_READ)
public void toggleSwitch() {
String status = readSwithStatus();
if(status.equals("on") {
updateStatus("off");
} else {
updateStatus("on");
}
}
If two concurrent clients try to update the switch status, the first to commit will win, and the second one will always fail. You just have to be prepared to tell the second client its transaction did not succeed due to concurrent failure. This second transaction is automatically rolled back. You or your client may decide to retry it or not.
如果两个并发客户端尝试更新切换状态,则第一个提交将获胜,而第二个将始终失败。你只需要准备好告诉第二个客户端它的事务由于并发失败而没有成功。第二个事务会自动回滚。您或您的客户可能会决定是否重试。
@Autowire
LightService lightService;
@GET
public ResponseEntity<String> toggleLight(){
try {
lightService.toggleSwitch();
//send a 200 OK
}catch(OptimisticLockingFailureException e) {
//send a Http status 409 Conflict!
}
}
But as I was saying, depending on what you're using (e.g. JPA, Hibernate, plain JDBC), there are multiple ways to do this with either pessimistic or optimistic locking strategies.
但是正如我所说的,根据您使用的内容(例如 JPA、Hibernate、普通 JDBC),有多种方法可以使用悲观或乐观锁定策略来做到这一点。
Why Not Just Thread Synchronization?
为什么不只是线程同步?
Other answers suggested so far are about pessimistic locking by using Java's mutual exclusion at the thread level using synchronized blocks which might work if you have a single JVMrunning your code. This strategy might prove to be ineffective if you have more than one JVM running your code or if you eventually scale horizontally and add more JVM nodes behind a load balancer, in whose case thread locking would not solve your problem anymore.
到目前为止建议的其他答案是关于悲观锁定,通过使用同步块在线程级别使用 Java 的互斥,如果您有一个JVM运行您的代码,这可能会起作用。如果您有多个 JVM 运行您的代码,或者如果您最终水平扩展并在负载均衡器后面添加更多 JVM 节点,则此策略可能被证明是无效的,在这种情况下,线程锁定将不再能解决您的问题。
But you could still implement pessimistic locking at the database level, by forcing the process to lock the database record before changing it and by this creating a mutual exclusion zone at the database level.
但是您仍然可以在数据库级别实现悲观锁定,通过强制进程在更改数据库记录之前锁定它,并通过在数据库级别创建一个互斥区。
So, what matters here is understanding the locking principles and then finding a strategy that works for your particular scenario and tech stack. Most likely, in your case, it will involve some form of locking at the database level at some point.
因此,这里重要的是了解锁定原则,然后找到适用于您的特定场景和技术堆栈的策略。最有可能的是,在您的情况下,它会在某个时候涉及数据库级别的某种形式的锁定。
回答by Sid Malani
Use synchronized - but if your user clicks quick enough then you will still have issues in that one command will execute immediately after another.
使用同步 - 但如果您的用户点击速度足够快,那么您仍然会遇到问题,一个命令将立即执行另一个。
Synchronized will make sure only one thread executes the block in
同步将确保只有一个线程执行块
synchronized(this) { ... }
at a time.
一次。
You may also want to introduce delays and reject commands in quick succession.
您可能还想快速连续引入延迟和拒绝命令。
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");
}
回答by Dariusz
Other people's answers seem overly complex to me... Keep it simple.
其他人的答案对我来说似乎过于复杂......保持简单。
Instead of toggling make the request have the new value. Inside controller put a synchronized
block. Perform action inside the synchronized block only if the new value differs current value.
而不是切换使请求具有新值。内部控制器放一个synchronized
块。仅当新值与当前值不同时才在同步块内执行操作。
Object lock = new Object();
Boolean currentValue = Boolean.FALSE;
void ligthsChange(Boolean newValue) {
synchronized(lock) {
if (!currentValue.equals(newValue)) {
doTheSwitch();
currentValue = newValue;
}
}
}
回答by Vicky
Can also use ReentrantLock, or use synchronized
也可以使用ReentrantLock,或者使用synchronized
public class TestClass {
private static Lock lock = new ReentrantLock();
public void testMethod() {
lock.lock();
try {
//your codes here...
} finally {
lock.unlock();
}
}
}