用例是我们将Spring应用程序从One-Database-For-All-Clients改为One-Database-Per-Client架构。我们使用Hibernate提供的接口,例如CurrentTenantIdentifierResolver
和AbstractDataSourceBasedMultiTenantConnectionProviderImpl.
等。
对于每个HTTP请求,在身份验证之后,我们将当前数据库名称设置为String类型的InheritableThreadLocal
对象,让我们根据登录的用户详细信息将其称为TenantContext,
作为Spring过滤器的一部分,并在请求处理完成后将其清除为null
。
我们遇到了一个bug,客户端B的数据间歇性地存储在客户端A的数据库中。(http-nio-8080-exec-567
)线程处理了这两个请求。在TenantContext
中,db_name aka tenant被设置为master
。然后客户端B的请求来了,同一个tomcat工作线程处理了该请求。相同的日志在这里
[07:15:48.564] [http-nio-8080-exec-567] [70055303-3d0f-48c9-97b5-e7c3b54e9ddc] [] [] [c.s.w.d.t.CurrentTenantIdentifierResolverImpl:27] [DEBUG] - Tenant code is not null. Returning master
[07:15:48.564] [http-nio-8080-exec-567] [70055303-3d0f-48c9-97b5-e7c3b54e9ddc] [] [] [c.s.w.d.d.DynamicDataSourceBasedMultiTenantConnectionProvider:79] [DEBUG] - Selected master datasource. tenantCode master PoolName masterDataSource
[07:15:48.570] [http-nio-8080-exec-567] [70055303-3d0f-48c9-97b5-e7c3b54e9ddc] [] [] [c.s.w.c.CustomAuthenticationSuccessHandler:47] [INFO] - notification token deactivated while logging out for user 987654321
[07:15:48.570] [http-nio-8080-exec-567] [70055303-3d0f-48c9-97b5-e7c3b54e9ddc] [] [] [c.s.w.c.CustomAuthenticationSuccessHandler:52] [INFO] - targetUrl /
[07:15:48.572] [http-nio-8080-exec-567] [70055303-3d0f-48c9-97b5-e7c3b54e9ddc] [] [] [c.s.w.c.SecurityConfig$2:293] [INFO] - audit after request [ POST /some/other/api]
[07:16:00.767] [http-nio-8080-exec-567] [40591c10-5b5d-4882-a999-3abe5e28fc7b] [] [] [c.s.w.c.SecurityConfig$2:287] [INFO] - audit before request [ POST /some/api]
[07:16:00.771] [http-nio-8080-exec-567] [40591c10-5b5d-4882-a999-3abe5e28fc7b] [Some_user] [] [c.s.w.d.t.CurrentTenantIdentifierResolverImpl:27] [DEBUG] - Tenant code is not null. Returning master
最后两个日志行对应于客户端B,其中它正在重用来自早期请求的master
租户代码。
当我们深入挖掘时,我们发现了一些问题
- 不知何故,租户代码在请求完成后没有被清除,一些请求。我们仍然不知道为什么会发生这种情况。
- 我们已经在过滤器中编写了这样的代码,它从经过身份验证的用户设置数据库名称,如下所示
if (TenantContext.isNotNull()){ return TenantContext}
/* Set the TenantContext value from the authenticated user in the next steps */
一旦我们删除了这个,我们就根本没有看到这个问题,这就是造成这个怀疑的原因。
apache工作线程(http-nio-8080-exec-567
)是否存储了来自InheritableThreadLocal
请求的值,不知何故它没有清除,并且在下一个请求中重用了相同的值?
根据我的研究,由于它们是不同的线程组,它们不应该这样做。但不知何故,这种预感起作用了。我不知道为什么。
1条答案
按热度按时间svgewumm1#
我会说,因为连接请求是http,所以你在应用程序中使用了某种类型的Servlet,无论你是接管了主tomcat应用程序还是加载了一个。
在任何情况下,http(https)请求都是通过SQL或类似的驱动程序向数据库服务器发出的,然而,关于线程的重要一点是“同步”,以防止同一线程受到并发请求的影响。
Servlet使用接口SingleThreadModel和/或在同步块中锁定的前servlet“filter”类中的单独请求。第二个安全机制是在doPost或doGet之后仅使用servlet中的同步方法。
如果代码是服务器的一些内部修改,为了防止master被切换到其他名称,执行该工作的方法名称应该同步到进程的末尾。
要么将每个请求的代码和设置完全分开并使用同步,要么如果您有必须更改的全局设置并共享到代码中,请检查请求的完成情况,然后按声明顺序更改volatile全局设置。
Hibernate相信它的代码是线程安全的,所以问题可能是Tomcat代码或自定义应用程序代码不是线程安全的,至少在应用程序的自定义代码中需要synchronized关键字。如果synchronized方法调用非synchronized代码,java文档说效果是“未定义的”,大概意味着不清楚接下来会发生什么。
可能也是这样,如果是不同的数据库,则是不同的数据库会话,因此您需要不同的连接池,以便预加载的SQL驱动程序查询数据库。validateExistingCurrentSessions 驱动程序将使用passw、user、dbname、... e.t.c加载就绪
...“不知何故,请求完成后租户代码没有被清除,一些请求”... JDBC连接在使用后可能没有被释放到连接池。
如果是tomcat的现代版本,java和tomcat都有线程设置,java有cpu内核数量的“亲和性”,tomcat有线程和并发的各种设置。