延迟客户端的并发请求,直到创建HttpSession

时间:2012-08-01 00:10:12

标签: java session java-ee servlet-filters java-6

我有一个servlet.Filter实现,它在数据库表中查找客户端的用户ID(基于IP地址),它将此数据附加到HttpSession属性。只要收到来自没有定义HttpSession的客户端的请求,过滤器就会执行此操作。

换句话说,如果请求没有附加会话,则过滤器将:

  • 为客户创建会话
  • 对用户ID执行数据库查找
  • 将用户ID附加为会话属性

如果来自“无会话”客户端的请求之间有一段时间,这一切都可以正常工作。

但是如果“无会话”客户端在几毫秒内发送10个请求,我最终会得到10个会话和10个数据库查询。它仍然“有效”,但出于资源原因,我不喜欢所有这些会话和查询。

认为这是因为请求非常接近。当“无会话”客户端发送请求并在发送另一个请求之前获得响应时,我没有这个问题。

我的过滤器的相关部分是:

// some other imports

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.MapHandler;

public class QueryFilter implements Filter {

    private QueryRunner myQueryRunner;  
    private String myStoredProcedure;
    private String myPermissionQuery;
    private MapHandler myMapHandler;

    @Override
    public void init(final FilterConfig filterConfig) throws ServletException {
        Config config = Config.getInstance(filterConfig.getServletContext());
        myQueryRunner = config.getQueryRunner();
        myStoredProcedure = config.getStoredProcedure();
        myUserQuery = filterConfig.getInitParameter("user.query");
        myMapHandler = new MapHandler();
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws ServletException {

        HttpServletRequest myHttpRequest = (HttpServletRequest) request;
        HttpServletResponse myHttpResponse = (HttpServletResponse) response;
        HttpSession myHttpSession = myHttpRequest.getSession(false);
        String remoteAddress = request.getRemoteAddr();

        // if there is not already a session
        if (null == myHttpSession) {

            // create a session
            myHttpSession = myHttpRequest.getSession();

            // build a query parameter object to request the user data
            Object[] queryParams = new Object[] { 
                myUserQuery, 
                remoteAddress
            };

            // query the database for user data
            try {
                Map<String, Object> userData = myQueryRunner.query(myStoredProcedure, myMapHandler, queryParams);

                // attach the user data to session attributes
                for (Entry<String, Object> userDatum : userData.entrySet()) {
                    myHttpSession.setAttribute(userDatum.getKey(), userDatum.getValue());
                }

            } catch (SQLException e) {
                throw new ServletException(e);
            }

            // see below for the results of this logging
            System.out.println(myHttpSession.getCreationTime());
        }

        // ... some other filtering actions based on session
    }
}

以下是从一个客户端记录myHttpSession.getCreationTime()(时间戳)的结果:

1343944955586
1343944955602
1343944955617
1343944955633
1343944955664
1343944955680
1343944955804
1343944955836
1343944955867
1343944955898
1343944955945
1343944955945
1343944956007
1343944956054

如您所见,几乎所有会话都不同。这些时间戳还可以很好地了解请求的距离(20ms - 50ms)。

我无法重新设计所有客户端应用程序,以确保它们在发送另一个请求之前至少得到一个响应,因此我想在我的过滤器中执行此操作。

另外,我不想让后续请求失败,我想找出一种处理它们的方法。

问题

  • 在第一次请求建立会话之前,有没有办法将来自同一客户端(IP地址)的后续请求置于“边界”?

  • 而且,如果我管理了这些内容,那么当我之后调用HttpSession时,如何才能获得正确的aSubsequentRequest.getSession()(我附加用户数据的那个)?我不认为我可以为请求分配会话,但我可能是错的。

也许有更好的方法可以完全解决这个问题。我基本上只想阻止此过滤器在2秒的时间内不必要地运行查询查询10-20次。

6 个答案:

答案 0 :(得分:1)

我会缓存数据库查找并找到一些方法在数据库更改或在缓存中使用超时时使缓存无效。例如,Google的Gauva有一个缓存,它会在指定的时间后使条目无效。这是一些基本代码。在会话中使用相同的值设置属性应该没问题。也可以使用HttpSessionListener在会话被销毁时使包含“userID”的特定缓存条目无效。

static LoadingCache<String, String> ipAddressToUserLookupCache = CacheBuilder.newBuilder()
        .maximumSize(10000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build(
            new CacheLoader<String, String>() {
              public String load(String ipAddress) throws Exception {
                // find the user ID
                return "<user id>";
              }
            });

@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain fc) throws IOException,
        ServletException {
    final String ipAddress = req.getRemoteAddr();
    final String userName = ipAddressToUserLookupCache.get(ipAddress);
    ((HttpServletRequest)req).getSession(true).setAttribute("username", userName);
}

答案 1 :(得分:1)

你正在处理雷鸣群问题。解决它的最佳方法是使用一个缓存实现来处理这个问题。这是解决它的一种方法。

  1. 在过滤器中使用Google Guava加载缓存并使用SessionId查找您要使用的信息。 Google guava的设计是为了在密钥不在缓存中并且线程同时在缓存中查找对象时,只有一个线程会调用load方法而其他线程会在项目进入缓存时阻塞。不要在此番石榴缓存上设置上限,因为缓存的大小将与http会话的数量相同,因为您希望在会话中存储项目。如果问题是容器正在为同时到达的请求创建多个httpSession,则根据请求中的某些内容进行缓存,这些内容不会更改此类用户ID或示例代码中的queryParams中的某些字段。

  2. 编写一个HttpSessionListener,当会话过期或在HtttpSessionListener中无效时,servlet容器将自动调用它,然后您可以调用Google番石榴缓存上的invalidate方法,最终将项目添加到缓存中在第一次请求并在会话到期时被赶出缓存。

  3. 您还可以实现HttpSessionActivationListener,它会在Web容器将会话钝化到磁盘时通知您,这可能由于各种原因(如内存不足或客户端暂时未发送请求)而发生会话尚未到期,因此它被钝化了。在钝化事件中,将项目从缓存中逐出并将激活事件放回缓存中是有意义的。

  4. 您必须确保放入缓存中的项目是线程安全的,我建议使用安全对象构造技术构建不可变对象。

  5. 我在我的应用程序中使用上述技术,这是基于Spring的,所以我做了一些略微的修改。

    1. 我正在使用Spring Application Context事件来触发一个事件,当一些可以使缓存无效的事件发生时,缓存只能监听spring应用程序上下文中的事件并使其状态无效。会话激活/钝化和创建/销毁火灾事件然后多个缓存可以做出反应。

    2. 我没有使用过滤器并使用自然键,例如,使用配置文件缓存是在用户ID上键入的,并且在有人要求用户ID为12304的用户配置文件之前不会填充它。 / p>

    3. 我对线程安全很虔诚,并确保在所有缓存中使用不可变对象。这意味着您必须拥有不可变数据结构,例如列表,地图等等。这是Google Guava令人惊叹的另一个领域,您将获得许多有用的数据结构。不可变列表,地图,集合,多图等......

    4. 如果你需要代码样本,请告诉我。

      另一种可能性是你可以在你的过滤器中使用同步来杀死性能,但会使事情串行化。

答案 2 :(得分:1)

我认为您需要做的是要求您的客户首先进行身份验证(成功),然后再提出其他请求。否则,它们会冒生成多个会话的风险(并且必须单独维护它们)。对IMO的要求来说,这真的不是那么糟糕。

如果您能够依赖NTLM凭据,那么您可以设置user-&gt;令牌的映射,在第一次连接时将令牌放入映射,然后所有请求阻止(或失败),直到其中一个他们成功完成了身份验证步骤,此时令牌被删除(或更新,以便您可以使用首选的会话ID)。

答案 3 :(得分:0)

首先进行检查(查看请求是否有会话),您有竞争条件。

您应该使用:

request.getSession()

如果你检查了HttpServletRequest的javadoc,你会看到:

返回与此请求关联的当前会话,或者如果请求没有会话,则创建一个。

如果您使用该方法,两个调用都应返回相同的会话,那么您可以在尝试设置之前检查是否存在userID属性。

答案 4 :(得分:0)

  1. 只想问一下,你怎么能在实时世界中真正拥有这样的场景?在同一个IP或同一个客户端发送多个请求(超过2-3个),只有20毫秒的差异?我工作的应用程序,当我再次尝试单击提交按钮时,它将不会提交 页面再次以聪明的方式表现。

  2. 基本上,我们通常会确保该应用程序是Double Submit proof。有关详细信息,请参阅此链接。 Solving the Double Submission Problem

  3. 我认为,如果您可以尝试避免来自同一客户的双重提交或多次提交等情况,那么您的问题就不会出现。

答案 5 :(得分:0)

真正最简单的解决方案是使用提供自填策略的几个缓存框架之一。

基本上这意味着当您转到特定键的缓存时,如果该键不存在,您提供了一个函数来为该键创建数据。

当该函数正在执行时,对该相同键的任何其他访问都会阻塞。

因此,如果您尝试点击特定IP的缓存,缓存会看到没有条目。然后它调用您的例程从数据库加载。当它加载时,其他所有尝试使用相同IP的人只需等待例程完成,然后它们都会返回相同的值。

ehcache是​​一个支持这个的框架,肯定有其他的。

你想使用一个框架,因为他们已经经历了为你管理锁和争用等所有的痛苦。