首页 > 解决方案 > 会话属性间歇性丢失 - Spring Session + Pivotal GemFire 实现

问题描述

在 Spring Session 中与 Pivotal GemFire 集成的某个时候面临一个奇怪的问题。

我们有多个 HTTP 请求,它们最终会根据几个条件以不同的顺序设置/获取会话属性。

在某个给定的点...

    (T) session.getAttribute(sessionKeyN); // (T) is template object

...正在检索null。我们已经交叉验证了 nosession.setAttribute(..)在两次session.getAttribute(..)调用之间被调用,其中一个错过了对象。

我们在 GemFire 客户端中启用了跟踪日志记录。在那里,我们看到正在读取/写入的哈希映射不匹配。分享日志:

>2018-09-24T02:46:15:342-0700 [20180924_451_5_4,20180912045104000005] TRACE org.apache.geode.DataSerializer: **Writing HashMap with 8 elements**: {LOGIN_DATE_TIME=09-24-2018 02:46:08 AM, TERMINAL=terminal.Terminal@5aa33970, org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME=testuser, LOG_TRANSACTION_ID=20180924_451_5_4,20180912045104000005, USER=user.User@70b4b11b, TRANSACTION_HEADER=TransactionHeader@4144221c, XXXX_LOGIN=true, SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@65bb8fc1: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@65bb8fc1: Principal: testuser; Credentials: [PROTECTED]; Authenticated: true; Details: null;}
>2018-09-24T02:46:15:342-0700 [20180924_451_5_4,20180912045104000005] TRACE org.apache.geode.internal.**InternalDataSerializer: basicWriteObject**: {LOGIN_DATE_TIME=09-24-2018 02:46:08 AM, TERMINAL=terminal.Terminal@5aa33970, org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME=testuser, LOG_TRANSACTION_ID=20180924_451_5_4,20180912045104000005, USER=user.User@70b4b11b, TRANSACTION_HEADER=TransactionHeader@4144221c, XXXX_LOGIN=true, SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@65bb8fc1: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@65bb8fc1: Principal: testuser; Credentials: [PROTECTED]; Authenticated: true; Details: null;}
>2018-09-24T02:46:15:342-0700 [20180924_451_5_4,20180912045104000005] TRACE org.apache.geode.DataSerializer: Writing STRING_BYTES of len=8
>2018-09-24T02:46:15:342-0700 [20180924_451_5_4,20180912045104000005] TRACE org.apache.geode.DataSerializer: Writing String "testuser"
>2018-09-24T02:46:15:342-0700 [20180924_451_5_4,20180912045104000005] TRACE org.apache.geode.DataSerializer: Writing STRING_BYTES of len=36
>2018-09-24T02:46:15:342-0700 [20180924_451_5_4,20180912045104000005] TRACE org.apache.geode.DataSerializer: Writing String "5c4948d9-7438-4dff-badc-fdc0f9997781"
>2018-09-24T02:46:15:342-0700 [20180924_451_5_4,20180912045104000005] TRACE org.apache.geode.internal.InternalDataSerializer: basicWriteObject: { @type = org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository$GemFireSession, id = 5c4948d9-7438-4dff-badc-fdc0f9997781, creationTime = 2018-09-24T09:44:23.180Z, lastAccessedTime = 2018-09-24T09:46:14.909Z, maxInactiveInterval = PT30M, principalName = testuser }
>2018-09-24T02:46:15:342-0700 [20180924_451_5_4,20180912045104000005] TRACE org.apache.geode.internal.InternalDataSerializer: DataSerializer Serializing an instance of org.apache.geode.cache.Operation
>2018-09-24T02:46:15:342-0700 [20180924_451_5_4,20180912045104000005] TRACE org.apache.geode.internal.InternalDataSerializer: basicWriteObject: UPDATE
>2018-09-24T02:46:15:342-0700 [20180924_451_5_4,20180912045104000005] DEBUG org.apache.geode.cache.client.internal.PutOp: PutOpImpl constructing message with operation=UPDATE
>2018-09-24T02:46:15:144-0700 [20180924_451_5_4,20180912045104000005] DEBUG org.apache.geode.internal.cache.LocalRegion: invoking listeners: [org.springframework.session.data.gemfire.GemFireOperationsSessionRepository@4471a4f]
>2018-09-24T02:46:15:144-0700 [20180924_451_5_4,20180912045104000005] TRACE org.apache.geode.internal.cache.LocalRegion: dispatchListenerEvent event=EntryEventImpl[op=LOCAL_LOAD_CREATE;region=/XXXX2wl;key=5c4948d9-7438-4dff-badc-fdc0f9997781;oldValue=null;newValue={ @type = org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository$GemFireSession, id = 5c4948d9-7438-4dff-badc-fdc0f9997781, creationTime = 2018-09-24T09:44:23.180Z, lastAccessedTime = 2018-09-24T09:46:15.079Z, maxInactiveInterval = PT30M, principalName = testuser };callbackArg=null;originRemote=false;originMember=tstplXXXX0004(ClientConfigXXXX2Application:28299:loner):35884:0c27e20a:ClientConfigXXXX2Application;callbacksInvoked;version={v20; rv161; mbr=10.5.230.71(server_devplgemf0066:123628)<v23>:1024; time=1537782375131; remote};isFromServer]
>2018-09-24T02:46:15:144-0700 [20180924_451_5_4,20180912045104000005] TRACE org.apache.geode.internal.cache.versions.VersionTag: deserializing class org.apache.geode.internal.cache.versions.VMVersionTag with flags 0x4
>2018-09-24T02:46:15:144-0700 [20180924_451_5_4,20180912045104000005] TRACE org.apache.geode.internal.InternalDataSerializer: basicReadObject: header=1
>2018-09-24T02:46:15:144-0700 [20180924_451_5_4,20180912045104000005] TRACE org.apache.geode.DataSerializer: **Read HashMap with 9 elements**: {LOGIN_DATE_TIME=09-24-2018 02:46:08 AM, TERMINAL=terminal.Terminal@5a2aa051, **CUSTOMER_SEARCH_RESPONSE=CustomerInfo@600fa25f**, org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME=testuser, LOG_TRANSACTION_ID=20180924_451_5_4,20180912045104000005, USER=user.User@7178708f, TRANSACTION_HEADER=TransactionHeader@30215dcd, XXXX_LOGIN=true, SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@65bb8fc1: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@65bb8fc1: Principal: testuser; Credentials: [PROTECTED]; Authenticated: true; Details: null;}

属性 CUSTOMER_SEARCH_RESPONSE 丢失,即使没有session.setAttribute(..)调用它。

这不是一个属性的 WRT,也不一致。重新运行可能不会显示此问题。

标签: springspring-sessiongemfirespring-data-gemfire

解决方案


在与另一位 Pivotal (GemFire) 客户合作解决类似问题后(也使用Spring Session和 Pivotal GemFire (SSDG) 在高度并发的 Web 应用程序/环境中管理 HTTP 会话状态),我们发现了潜在问题并最终发现Pivotal GemFire 中的错误!

简而言之,这些错误导致更新丢失,因为竞争条件在高度并发(多用户)Web 环境中被激怒,其中多个 HTTP 请求可能在负载下访问和修改同一个 HTTP 会话。而且,并发(用户)越大,负载越大(对同一 HTTP 会话的 HTTP 请求数),这个问题就越明显。

事实上,我已经写了几个集成测试来说明这个问题。

首先,我编写了一个负载集成测试( MultiThreadedClientProxyRegionSessionIntegrationTests)。此类产生 180 个线程(用户),对同一底层执行 10,000 个并发请求Session。该Session对象虽然不完全相同,但在 SSDG 的GemFireSession对象表示之后建模。

其次,我编写了另一个集成测试( TwoThreadsClientProxyRegionSessionIntegrationTests),它可靠且重复地重现了该问题。

这两个测试类都是纯粹用GemFire API编写的,从而说明问题在于关键的 GemFire,而不是 SSDG。

我在我的示例中都使用Spring Session Data GemFire编写了类似的测试,现在,也包含在 SSDG 测试套件中(以及许多其他基于多线程/并发的集成测试)本身,确保Spring Session(用于 Pivotal GemFire)永远不会再次遇到这个问题,如果是这样,我迟早会知道的。

简而言之,两个潜在的 Pivotal GemFire 错误是:

解决方法如下:

首先,您必须配置 Spring Session、GemFire 缓存客户端应用程序:

  1. PROXY用于管理状态的客户端区域Session(默认)
  2. 设置copy-on-readtrue
  3. 而且,您必须使用 GemFire DataSerialization,通过sessionSerializerBeanName适当设置:

    @SpringBootApplication @ClientCacheApplication(copyOnRead = true, subscriptionEnabled = true) @EnableGemFireHttpSession( clientRegionShortcut = ClientRegionShortcut.PROXY, sessionSerializerBeanName = GemFireHttpSessionConfiguration.SESSION_DATA_SERIALIZER_BEAN_NAME ) 类 MySpringBootSpringSessionDataGemFireApplication { ... }

例如,请参见此处。

您还需要升级到Pivotal GemFire 的 Spring Session 2.1.2.RELEASE(即将发布),因为我最近做了几个重要的增强,例如:

  1. 问题 #12 - 在非脏会话上防止 SessionRepository.save(Session)。
  2. 问题 #9 - 当 SSDG 不用于在服务器上配置 Spring Session 时,为 GemFire/Geode DataSerialization 添加服务器端配置支持。
  3. 问题 #17 - 考虑支持可自定义的 IsDirty 应用程序域对象检查。

使用带有 Delta 的 GemFire DataSerialization 不会阻止,但会大大降低丢失更新和其他竞争条件在 Web 环境中固有继承的可能性,特别是因为 Servlet 容器(例如 Tomcat)是多线程的,在单独的线程中处理每个 HTTP 请求.

虽然 SSDG 努力确保 HTTP 会话表示(即GemFireSession)是线程安全的,但您还必须确保放入 HTTP 会话中的任何对象也是线程安全的,因为它可以并且很可能会被超过高并发 Web 应用程序中的 1 个线程,尤其是 1 个线程,其中多个 HTTP 请求一次可以访问同一个 HTTP 会话(按会话 ID)。

总之,值得深思。

使用上述配置时,一切都按预期工作,否则,由于 GemFire BUGS,可能并且将会发生丢失更新!

事实上,我的负载测试显示,在 10,000 个会话更新中,添加了约 9800 个会话属性,只有约 1100 个成功,这是高达 89% 的数据丢失!!!

但是,当应用上述配置时,所有数据都会正确计算。

希望这可以帮助!


推荐阅读