基于spring-data-mongodb实现多租户及mongoTemplate线程安全最佳实践
问题1:直接注入单例
MongoTemplate能否保证线程安全? Spring容器默认注入的单例MongoTemplate本身无状态,原生是线程安全的,如果不需要做租户的动态切换,REST请求线程和定时任务线程共用它不会有线程安全问题。
但你是多租户场景,通常需要根据当前上下文动态切换租户对应的数据库/集合前缀,这时候直接用默认单例MongoTemplate就会有问题:如果在运行时修改MongoTemplate持有的MongoDatabaseFactory、读写集合的前缀等属性,多线程场景下(REST请求和定时任务并发执行)会出现租户数据串扰,无法满足线程安全要求。
多租户+REST接口+定时任务场景下的最佳实现方案
这里默认你采用的是最常用的「每个租户对应独立数据库」或「所有租户共享数据库、集合加租户前缀」的多租户模式,方案如下:
- 第一步:实现租户上下文持有类,用
ThreadLocal存储当前线程的租户标识,同时专门提供定时任务场景下的租户标识设置入口,避免定时任务无请求上下文拿不到租户ID的问题
public class TenantContext { private static final ThreadLocal<String> TENANT_ID_HOLDER = new ThreadLocal<>(); public static void setTenantId(String tenantId) { TENANT_ID_HOLDER.set(tenantId); } public static String getTenantId() { return TENANT_ID_HOLDER.get(); } public static void clear() { TENANT_ID_HOLDER.remove(); } }
- 第二步:自定义动态
MongoDatabaseFactory,重写getMongoDatabase()方法,根据当前TenantContext里的租户ID返回对应租户的数据库实例;如果是集合前缀隔离的模式,就自定义MongoTemplate子类,重写getCollectionName()方法动态拼接租户前缀 - 第三步:配置全局拦截适配,覆盖两类场景:
- REST接口场景:加一个Spring Web拦截器,在请求进入时从请求头/参数中解析租户ID存入
TenantContext,请求结束后调用clear()方法清理,避免线程复用导致的租户标识串用 - 定时任务场景:每个定时任务执行的入口处,先设置当前任务对应的租户ID到
TenantContext,任务执行完毕后调用clear()清理
- REST接口场景:加一个Spring Web拦截器,在请求进入时从请求头/参数中解析租户ID存入
- 第四步:将你自定义的动态
MongoDatabaseFactory注入到MongoTemplate的Bean定义中,此时全局共用这个单例MongoTemplate即可,所有操作都会自动根据当前线程的租户上下文操作对应租户的数据,不会出现串扰,天然支持REST和定时任务的并发执行
注意:如果你的定时任务是多租户批量处理的场景,处理每个租户数据前切换一次
TenantContext的租户ID即可,处理完单个租户后就清理上下文,避免影响下一个租户的处理逻辑。
内容的提问来源于stack exchange,提问作者chiku
相关产品推荐
相关产品推荐

