多租户数据隔离方案实践( 二 )

虽然大部分sql都需要做条件过滤,但也有些特殊情况某些sql可能不需要过滤companyId条件,所以增加一个注解,如果不需要拦截的sql可以在Mapper类或方法上添加此注解,这样可以兼容不需要拦截的方法 。
添加 IgnoreAutoFill 注解:
/** * 用于标注在不需要被拦截器处理的SQL上(Mapper类) */@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface IgnoreAutoFill {String userType() default "";}Mapper示例:
public interface PostRecordDOMapper {long countByExample(PostRecordDOExample example);int deleteByExample(PostRecordDOExample example);int deleteByPrimaryKey(Long id);int insert(PostRecordDO record);int insertSelective(PostRecordDO record);List<PostRecordDO> selectByExample(PostRecordDOExample example);@IgnoreAutoFillList<PostRecordDO> selectByExampleAllCompany(PostRecordDOExample example);PostRecordDO selectByPrimaryKey(Long id);int updateByExampleSelective(@Param("record") PostRecordDO record, @Param("example") PostRecordDOExample example);int updateByExample(@Param("record") PostRecordDO record, @Param("example") PostRecordDOExample example);int updateByPrimaryKeySelective(PostRecordDO record);int updateByPrimaryKey(PostRecordDO record);void batchInsert(@Param("items") List<PostRecordDO> items);}在拦截器中,我们使用阿里的druid做sql解析,修改sql 。
加入 druid 依赖:
<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.6</version></dependency>拦截修改sql时,对于select、update、delete语句,我们直接添加company_id条件,对于insert语名,先判断原sql的参数列表里有没有company_id字段,如果有的话不作处理(说明原来就做了字段隔离),没有才自动给它添加company_id字段及值 。
 

多租户数据隔离方案实践

文章插图
 
至此,我们解决了统一修改sql的问题,那还有一个重要问题,填充的字段值从哪里取得呢?因为调用持久层Mapper类方法的入参并不一定带有租户字段(companyId)信息过来,有些方法甚至只会传一个id的参数,像 deleteByPrimaryKey(Long id);selectByPrimaryKey(Long id);即使是传对象参数,对象实体类也不一定有租户字段(companyId) 。所以如何传递租户字段(companyId)是一个改造难点 。
三、多租户字段值的传递考虑一翻,我们是否可以用 ThreadLocal 来存取呢?答案是肯定的 。
要传递多租户字段(companyId)值,得先取得companyId值 。因为每一个系统用户都有所属的companyId,所以只要在用户登录系统的时候,从token中拿到用户所属的companyId,然后set进ThreadLocal 。后续线程的处理都可以从ThreadLocal中取得companyId 。这样Mybatis拦截器也就随时都可以取得companyId的值进行sql参数或者条件的拼接改造了 。
多租户上下文信息:
@Slf4jpublic class CompanyContext implements AutoCloseable {private static final TransmittableThreadLocal<String> COMPANY_ID_CTX = new TransmittableThreadLocal<>();public CompanyContext(String companyId) {COMPANY_ID_CTX.remove();COMPANY_ID_CTX.set(companyId);}public static String getCompanyId(){return COMPANY_ID_CTX.get();}@Overridepublic void close() throws Exception {COMPANY_ID_CTX.remove();}public static void remove(){COMPANY_ID_CTX.remove();}}但是,系统的业务处理不可能只用一个线程从头处理到结束,很多时候为了加快业务的处理,都是需要用到线程池的 。
那么,问题又来了,不同线程间如何将这个companyId的ThreadLocal值传递下去呢?
这也是有解决方案的 。
Transmittable ThreadLocal
Alibaba 有一个 Transmittable ThreadLocal 库,提供了一个TransmittableThreadLocal,它是 ThreadLocal 的一个扩展,提供了将变量的值从一个线程传递到另一个线程的能力 。当一个任务被提交到线程池时,TransmittableThreadLocal 变量的值被捕获并传递给执行任务的工作线程 。这确保了正确的值在工作线程中可用,即使它最初在不同的线程中设置 。
使用Transmittable ThreadLocal 库,需引入依赖:
<dependency><groupId>com.alibaba</groupId><artifactId>transmittable-thread-local</artifactId><version>2.11.5</version></dependency>使用的时候,调用一下TtlExecutors工具提供的getTtlExecutor静态方法,传入一个Executor,即可获取一个支持 TTL (TransmittableThreadLocal)传递的 Executor 实例,此线程池就确保了上下文信息的正确传递,可放心使用了,如下所示:


推荐阅读