延迟来自客户端的并发请求,直到创建 HttpSession
Posted
技术标签:
【中文标题】延迟来自客户端的并发请求,直到创建 HttpSession【英文标题】:Delay concurrent requests from a client until HttpSession is created 【发布时间】:2012-07-29 20:28:02 【问题描述】:我有一个 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
如您所见,几乎所有会话都不同。这些时间戳还可以很好地了解请求之间的间隔距离(20 毫秒 - 50 毫秒)。
我无法重新设计所有客户端应用程序以确保它们在最初发送另一个请求之前至少获得一个响应,所以我想在我的过滤器中这样做。
另外,我不想只是让后续请求失败,我想找出一种方法来处理它们。
问题
有没有办法将来自同一客户端(IP 地址)的后续请求置于“limbo”中,直到从第一个请求建立会话?
而且,如果我能做到这一点,当我之后调用aSubsequentRequest.getSession()
时,我如何才能获得正确的HttpSession
(我附加了用户数据的那个)?我认为我不能为请求分配会话,但我可能是错的。
也许有一些更好的方法可以完全解决这个问题。我基本上只是想阻止此过滤器在 2 秒内不必要地运行查找查询 10 到 20 次。
【问题讨论】:
在这种情况下可能是 applicationContext 有助于跟踪现有请求?如果请求来自同一个 IP,请记录 applicationContext,在发出新请求之前确保它不在应用程序上下文中? @thinksteep 有道理,但是有没有办法将存储的会话应用于具有相同 IP 地址的不同请求? 这是一个相当奇怪的要求。根据您在以下答案之一中的评论,我了解到您认为 this 将是正确解决方案的具体问题归结为由从单个页面触发多个 ajax 请求引起没有在队列中被解雇。解决方案实际上非常简单:只需在 javascript 端的队列中触发它们。很多现有的基于 ajax 的 MVC 框架(如 JSF)已经在幕后做到了这一点。 @BalusCjust fire them in a queue in JavaScript side
这是否意味着“在发送另一个请求之前等待 JS 中的响应”?我不会在“ajax”中丢失“A”吗?如果我在 java 端这样做,我只能在需要时(即没有建立会话时)使其同步。
不幸的是,这对你不起作用。代理服务器、NAT 等都将与您合谋,使得某些客户端(您永远无法猜测哪些客户端)彼此无法区分,您最终会将它们混合在一起并最终(可能)交叉授粉他们的凭据(这显然很糟糕)。这些自动化客户端是否使用某种基于 HTTP 的 API?如果是这样,我认为您需要强制客户端首先获取会话,然后开始轰炸您的服务附加请求。否则,客户可能会获得多个会话。
【参考方案1】:
我认为您需要做的是要求您的客户首先进行身份验证(成功),然后再提出其他请求。否则,它们会冒着生成多个会话的风险(并且必须单独维护它们)。这对 IMO 的要求还不错。
如果您能够依赖 NTLM 凭据,那么您也许可以设置一个 user->token 映射,您可以在第一次连接时将令牌放入映射中,然后所有请求都会阻塞(或失败)直到其中一个请求成功完成身份验证步骤,此时令牌被删除(或更新,以便您可以使用首选会话 ID)。
【讨论】:
【参考方案2】:我会缓存数据库查找,并在数据库更改或在缓存中使用超时时找到某种方法使缓存无效。例如,谷歌的 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);
【讨论】:
问题是在另一个请求从同一个IP地址进来之前,第一个请求还没有完成对用户ID的查找。在我需要再次找到它之前,我无法将任何东西放入缓存中。除非我误解了你的回答。我会尽量让问题更清楚。 来自同一 IP 地址的请求将被阻塞(即不进入数据库),直到检索到用户 ID。【参考方案3】:您正在处理雷鸣群问题。解决它的最佳方法是使用可以处理此问题的缓存实现。这是解决它的一种方法。
在过滤器中使用 Google Guava 加载缓存并使用 SessionId 查找您想要的信息。 Google guava 的设计是这样的,如果一个键不在缓存中,并且一个线程同时访问缓存来寻找一个对象,那么只有一个线程会调用 load 方法,而其他线程会阻塞,同时将项目带入缓存.不要为此番石榴缓存设置上限,因为缓存的大小将与 http 会话的数量相同,因为您希望在会话中存储项目。如果问题是容器正在为同时到达的请求创建多个 httpSession,则根据请求中的某些内容进行缓存,这些内容不会更改此类用户 ID 或示例代码中 queryParams 中的某些字段。
编写一个 HttpSessionListener,当会话过期或在 HtttpSessionListener 中失效时,servlet 容器会自动调用它,然后您可以在 Google guava 缓存上调用 invalidate 方法,这样您最终会将项目添加到缓存中在第一个请求上并在会话到期时被踢出缓存。
1234563会话尚未过期,因此它被钝化。在钝化事件中,将您的项目从缓存中逐出并在激活事件中将其放回缓存中是有意义的。您必须确保放入缓存中的项目是线程安全的,我建议使用安全对象构造技术构建的不可变对象。
我在我的基于 Spring 的应用程序中使用了上述技术,因此我在进行上述操作时稍作修改。
我正在使用 Spring 应用程序上下文事件来触发一个事件,当发生可以使缓存无效的事情时,缓存可以只侦听 Spring 应用程序上下文中的事件并使其状态无效。会话激活/钝化和创建/销毁触发事件,然后多个缓存可以做出反应。
我不使用过滤器并使用自然键,例如,使用配置文件缓存以用户 ID 为键,并且在有人询问用户 ID 12304 的用户配置文件之前不会填充它。
我非常重视线程安全,并确保在所有缓存中使用不可变对象。这意味着您必须拥有不可变的数据结构,例如列表、地图……等等,这是 Google Guava 令人惊叹的另一个领域,您可以获得很多有用的数据结构。不可变列表、映射、集合、多映射...等。
如果您需要代码示例,请告诉我。
另一种可能性是您可以在过滤器中使用同步,这会降低性能但会使事情变得连续。
【讨论】:
【参考方案4】:通过首先进行检查(查看请求是否有会话),您就有了竞争条件。
你应该改用:
request.getSession()
如果您检查 HttpServletRequest 的 javadoc,您会看到:
返回与此请求关联的当前会话,或者如果请求没有会话,则创建一个。
如果您使用该方法,两个调用都应该返回相同的会话,那么您可以在尝试设置之前检查 userID
属性是否存在。
【讨论】:
If you use that method both calls should return the same session
这也是我的想法,但它给了我不同的会话。我认为因为来自同一个客户端的这些请求彼此如此接近,所以过滤器在创建第一个会话之前从客户端接收第二个请求。 (我认为)
是的,再想一想我意识到这还不够 - 竞争条件仍然存在......可能会更好地设计你的 userId 解决方案,这样它就无关紧要了。【参考方案5】:
真正最简单的解决方案是使用提供自填充策略的多个缓存框架之一。
这基本上意味着,当您访问特定键的缓存时,如果该键不存在,则您提供了一个函数来为该键创建数据。
当该函数正在执行时,任何其他对该相同键的访问都会阻塞。
因此,如果您尝试访问特定 IP 的缓存,缓存会发现它没有条目。然后它调用您的例程从数据库加载。在加载时,其他尝试相同 IP 的人只是等到例程完成,然后他们都返回相同的值。
ehcache 是一个支持这个的框架,当然还有其他的。
您想为此使用一个框架,因为他们已经为您经历了管理锁和争用等的所有痛苦。
【讨论】:
【参考方案6】:只是想问一下,你怎么能在实时世界中真正有这样的场景,其中多个请求(超过 2-3 个)从同一个 IP 或同一个客户端发送,只有 20 毫秒的差异?我正在使用的应用程序,当我尝试再次单击提交按钮时,它不会提交 重新打开页面并以智能方式运行。
基本上,我们通常会确保申请是双重提交证明。请参阅此链接以获取更多信息。 Solving the Double Submission Problem
我认为,如果您可以尝试避免双重提交或来自同一客户端的多次提交等情况,您的问题就不会出现。
【讨论】:
此过滤器的场景(它的用途)是将标识用户 ID 附加到来自单页 Intranet webapps 的 Ajax 请求。它从不用于处理提交的表单,因此它不仅仅是一个双击问题。我显示记录的特定 web 应用程序(为同一客户端创建了 14 个会话)是因为它立即使用 14 个不同的 Ajax 请求进行初始化。 感谢您的澄清,现在我只能想到一个选项来使用基于锁定/同步的方法,例如使用 concurrentHashMap 来存储 IP 地址,然后使用 putifAbsent 方法执行查找以实现线程安全。以上是关于延迟来自客户端的并发请求,直到创建 HttpSession的主要内容,如果未能解决你的问题,请参考以下文章
如何使用套接字(socket.io)在 NodeJS 中找到客户端的响应时间(延迟)?
Oracle 更新/插入卡住,DB CPU 100%,并发高,来自客户端的 SQL*Net 等待消息