如何避免java spring中控制器方法与同一会话并发访问



我想知道如何确保每次会话只能访问服务中的某个方法一次。

我将用一个小例子来说明:

假设我们有一个用户处于状态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

在竞争激烈的环境中,您可能会想到比内部锁更高级的解决方案。

然而,我建议不要使用HandlerAdaptersynchronizeOnSession选项,这不仅是因为它同步了同一个互斥体上的所有调用,而且,不太明显的是,在公共可用的互斥体上同步有潜在的危险。

最新更新