2

我有一个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()来自 ONE 客户端的日志记录(时间戳)结果:

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

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

我无法重新设计所有客户端应用程序以确保它们在最初发送另一个请求之前至少获得一个响应,所以我想在我的过滤器中这样做。

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

问题

  • 有没有办法将来自同一客户端(IP 地址)的后续请求放入“limbo”,直到从第一个请求建立会话?

  • 而且,如果我能做到这一点,当我之后打电话时,我怎样才能得到正确HttpSession的(我附加用户数据的那个)aSubsequentRequest.getSession()?我认为我不能为请求分配会话,但我可能是错的。

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

4

6 回答 6

1

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

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

于 2012-08-07T22:01:27.807 回答
1

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

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

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

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

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

我在我的基于 Spring 的应用程序中使用了上述技术,所以我在做上面的一些细微的修改。

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

  2. 我不使用过滤器并使用自然键,例如使用配置文件缓存以用户 ID 为键,并且在有人询问用户 ID 12304 的用户配置文件之前不会填充它。

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

如果您需要代码示例,请告诉我。

另一种可能性是您可以在过滤器中使用同步,这会降低性能但会使事情变得连续。

于 2012-08-07T18:41:46.043 回答
1

我会缓存数据库查找并找到某种方法在数据库更改或在缓存中使用超时时使缓存无效。例如,谷歌的 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);
}
于 2012-08-01T00:49:37.270 回答
0

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

您应该改用:

request.getSession()

如果您检查 HttpServletRequest 的 javadoc,您将看到:

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

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

于 2012-08-01T00:35:40.060 回答
0

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

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

在执行该功能时,对相同键的任何其他访问都会被阻止。

因此,如果您尝试访问特定 IP 的缓存,缓存会发现它没有条目。然后它调用您的例程从数据库加载。在加载时,其他尝试相同 IP 的人只是等到例程完成,然后他们都返回相同的值。

ehcache 是一个支持这个的框架,当然还有其他的。

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

于 2012-08-04T04:23:56.570 回答
0
  1. 只是想问一下,你怎么能在实时世界中真正有这样的场景——多个请求(超过 2-3 个)是从同一个 IP 或同一个客户端发送的,只有 20 毫秒的差异?我正在使用的应用程序,当我尝试再次单击提交按钮时,它不会再次提交页面并且以智能方式运行。

  2. 基本上,我们通常会确保申请是双重提交证明。请参阅此链接以获取更多信息。 解决双重提交问题

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

于 2012-08-03T18:25:47.027 回答