我想知道如何确保每次会话只能访问服务中的某个方法一次。
我将用一个小例子来说明:
假设我们有一个用户处于状态a(user.state=a)。这个用户向我们的javaspring控制器发送一个HTTPGET请求来获取一个页面,say/hello。根据他的状态,他将被发送到A或B。在此之前,我们将把他的状态更改为B(见下面的代码)。
现在,再次假设调用dao.doSomething();需要很多时间。如果用户发送另一个GET(例如通过刷新浏览器),他将调用完全相同的方法dao.doSomething(),从而产生2个调用。
如何避免这种情况
如果您同时发送2个HTTP GET,会发生什么
如何在控制器/服务/模型/数据库中保持一致
注意1:这里我们不从不同的浏览器发出2个HTTP GET。我们只是在同一个浏览器上同时制作它们(我知道最大并发会话解决方案,但这并不能解决我的问题。)
注2:该解决方案不应阻止不同用户对控制器的并发访问。
我读过一些关于服务上事务的文章,但我不确定这是否是解决方案。我也读过一些关于并发的文章,但我仍然不明白如何在这里使用它。
我将非常感谢你的帮助!谢谢
代码示例:
@Controller
public class UserController {
@RequestMapping(value='/hello')
public String viewHelloPage() {
// we get the user from a session attribute
if (user.getState() = A) {
user.setStatus(B);
return "pageA";
}
return "pageB";
}
@Service
public class UserService {
Dao dao;
@Override
public void setStatus(User user) {
dao.doSomething();
user.setStatus(B);
}
}
尽管我不推荐它(因为它基本上阻止了同一用户对的所有其他调用)。在大多数HandlerAdapter
实现中,您可以设置属性synchronizeOnSession
,默认情况下这是false
,允许来自同一客户端的并发请求。当您将此属性设置为true
时,将为该客户端排队请求。
如何设置取决于您对HandlerAdapter
的配置。
如何确保服务中的某个方法只访问一次每次会话一次。
在调用服务方法之前,请尝试锁定控制器中的会话对象
如果dao.doSomething()
正在做只想发生一次的工作,则应该使用像PUT或DELETE这样的幂等方法。没有法律强制你使用正确的方法,但最坏的情况是,这是一种自我记录的方式,告诉世界应该如何使用API。如果这对你来说还不够,大多数浏览器会根据请求的类型来帮助你。例如,浏览器通常会使用缓存来避免多个GET。
似乎你真正想知道的是如何强制执行幂等性。这是非常具体的应用程序。一种通用方法是在服务器端生成并存储一个伪唯一id,以便客户端附加到其请求。这样,在第一个请求之后具有相同id的任何请求都可以被安全地忽略。显然,应该明智地驱逐旧的id。
正如我所说,解决方案通常是特定于应用程序的。在上面的例子中,看起来您正试图在两种状态之间切换,而您的实现是一个服务器端切换。您可以利用客户端来确保多个请求不会成为问题。
@RequestMapping(value="/hello", method=RequestMethod.PUT)
public String test(@RequestParam("state") String state) {
dao.setState(user, state)
switch (state) {
case "A":
return "B";
case "B":
return "A";
default:
return "error";
}
}
如果您不介意配置和使用AOP,那么以下内容可能会帮助您
@Aspect
@Component
public class NonConcurrentAspect implements HttpSessionListener{
private Map<HttpSession, Map<Method, Object>> mutexes = new ConcurrentHashMap<HttpSession, Map<Method, Object>>();
@Around(value = "@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object handle(ProceedingJoinPoint pjp) throws Throwable {
MethodInvocationProceedingJoinPoint methodPjp = (MethodInvocationProceedingJoinPoint) pjp;
Method method = ((MethodSignature) methodPjp.getSignature()).getMethod();
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
HttpSession session = request.getSession(false);
Object mutex = getMutex(session, method);
synchronized (mutex) {
return pjp.proceed();
}
}
private Object getMutex(HttpSession session, Method method) {
Map<Method, Object> sessionMutexes = mutexes.get(session);
Object mutex = new Object();
Object existingMutex = sessionMutexes.putIfAbsent(method, mutex);
return existingMutex == null ? mutex : existingMutex;
}
@Override
public void sessionCreated(HttpSessionEvent se) {
mutexes.put(se.getSession(), new ConcurrentHashMap<Method, Object>());
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
mutexes.remove(se.getSession());
}
}
它在每个会话每个方法的互斥体上进行同步。一个限制是,这样建议的方法不应该相互调用(除非您严重违反MVC设计模式,否则这几乎不是一种情况),否则您可能会面临死锁。
这将处理所有用@RequestMapping
标记的方法,但如果您只想保护少数方法不被并发执行,然后,作为可能的解决方案之一,您可以引入自己的注释,例如
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NonConcurrent {
}
用这个注释标记特定的方法,并用自己的方法替换上面aspect类中@Around
注释中的@RequestMapping
。
在竞争激烈的环境中,您可能会想到比内部锁更高级的解决方案。
然而,我建议不要使用HandlerAdapter
的synchronizeOnSession
选项,这不仅是因为它同步了同一个互斥体上的所有调用,而且,不太明显的是,在公共可用的互斥体上同步有潜在的危险。