如何避免在java spring中使用相同会话同时访问控制器方法?

时间:2013-11-27 23:17:57

标签: java spring spring-mvc concurrency

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

我将通过一个小例子说明:

假设我们的用户处于状态A(user.state = A)。此用户向我们的java spring控制器发送HTTP GET请求以获取页面,例如/ 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);
    }
}

4 个答案:

答案 0 :(得分:1)

虽然我不推荐它(因为它基本上阻止来自同一用户的所有其他呼叫)。在大多数HandlerAdapter实现中,您可以默认设置属性synchronizeOnSession,这是false,允许并发请求来自同一客户端。当您将此属性设置为true时,请求将排队等候该客户端。

如何设置取决于HandlerAdapter的配置。

答案 1 :(得分:0)

如果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";
    }
}

答案 2 :(得分:0)

  

如何确保服务中的某些方法只被访问一次   每次会议的时间。

在调用服务方法

之前,尝试锁定控制器中的会话对象

答案 3 :(得分:0)

如果您不介意配置和使用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 {
}

使用此批注标记特定方法,并将上述方面类中的@RequestMapping批注中的@Around替换为您自己的方法。

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

但是,我建议不要使用HandlerAdapter的{​​{1}}选项,这不仅是因为它同步了同一个互斥锁上的所有调用,而且不太明显,公开可用的同步互斥是有潜在危险的。