Эта статья участвует в «Месяце тем Java — составление вопросов по Java», подробнее см.Ссылка на мероприятие
Мульти аренды (Multi-Tenant) является важной концепцией в SaaS. Это технология архитектуры программного обеспечения. В среде нескольких арендаторов один и тот же экземпляр системы используется совместно, а данные между арендаторами изолированы, то есть один арендатор не может получить доступ к данным другого арендатора. На основе различных уровней изоляции обычно существуют следующие три схемы реализации:
1. Каждый арендатор использует независимыйDataBase, высокий уровень изоляции, хорошая производительность, но высокая стоимость
2. Разделение между арендаторамиDataBase, используя отдельныйSchema
3. Разделение между арендаторамиSchema, добавив в таблицу поле арендатора с наивысшей степенью общих данных и самым низким уровнем изоляции.
Mybatis-plusНа уровне изоляции уровня 3 предоставляется мультиарендное решение на основе подключаемых модулей подкачки, которое мы представим. Перед официальным запуском сначала проведите подготовительную работу, создайте две таблицы и добавьте поле арендатора после базового поля.tenant_id:
CREATE TABLE `user` (
`id` bigint(20) NOT NULL,
`name` varchar(20) DEFAULT NULL,
`phone` varchar(11) DEFAULT NULL,
`address` varchar(64) DEFAULT NULL,
`tenant_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`)
)
CREATE TABLE `dept` (
`id` bigint(20) NOT NULL,
`dept_name` varchar(64) DEFAULT NULL,
`comment` varchar(128) DEFAULT NULL,
`tenant_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`)
)
Импортируйте необходимые зависимости в проект:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>3.1</version>
</dependency>
Класс конфигурации Mybatis-plus:
@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
public class MybatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
List<ISqlParser> sqlParserList=new ArrayList<>();
TenantSqlParser tenantSqlParser=new TenantSqlParser();
tenantSqlParser.setTenantHandler(new TenantHandler() {
@Override
public Expression getTenantId(boolean select) {
String tenantId = "3";
return new StringValue(tenantId);
}
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public boolean doTableFilter(String tableName) {
return false;
}
});
sqlParserList.add(tenantSqlParser);
paginationInterceptor.setSqlParserList(sqlParserList);
return paginationInterceptor;
}
}
Основные реализованные здесь функции:
-
Создайте коллекцию парсеров SQL
-
Создать анализатор SQL арендатора
-
Настройте обработчик арендатора так, чтобы он специально обрабатывал логику арендатора.
На данный момент идентификатор арендатора зафиксирован как 3 для тестирования. Тест выполняет полный оператор таблицы:
public List<User> getUserList() {
return userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId));
}
Используйте подключаемый модуль для анализа выполняемого оператора SQL, и вы увидите, что условие фильтра арендатора автоматически добавляется после условия запроса:
Итак, в реальном проекте, как передать информацию об арендаторе обработчику арендатора, мы можем получить ее из кеша или заголовка запроса в зависимости от ситуации, взяв заголовок запроса в качестве примера:
@Override
public Expression getTenantId(boolean select) {
ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String tenantId = request.getHeader("tenantId");
return new StringValue(tenantId);
}
Когда внешний интерфейс инициирует HTTP-запрос, в заголовок добавляется поле tenantId. После того, как серверная часть получает его в процессоре, оно устанавливается в качестве условия фильтрации клиентов текущего запроса.
Если это основано на том факте, что заголовок запроса содержит информацию о арендаторе, может быть использована яма.Если используется многопоточность, вновь открытый асинхронный поток не будет автоматически переносить запрос запроса текущего потока.
@Override
public List<User> getUserListByFuture() {
Callable getUser=()-> userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId));
FutureTask<List<User>> future=new FutureTask<>(getUser);
new Thread(future).start();
try {
return future.get();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
Выполняя вышеуказанный метод, можно увидеть, что текущий запрос запроса не может быть получен, поэтому невозможно получить идентификатор арендатора, что приведет к последующему исключению нулевого указателя ошибки:
Модификация также очень проста, откройте общий доступ к подпотоку RequestAttributes и измените приведенный выше код:
@Override
public List<User> getUserListByFuture() {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
Callable getUser=()-> {
RequestContextHolder.setRequestAttributes(sra, true);
return userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId));
};
FutureTask<List<User>> future=new FutureTask<>(getUser);
new Thread(future).start();
try {
return future.get();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
После этой модификации информацию об арендаторе можно будет нормально получать в асинхронных потоках.
Затем некоторые друзья могут спросить, не все запросы в бизнесе должны фильтровать условия арендатора.Есть два способа справиться с этой ситуацией.
1. Если все операции SQL над всей таблицей не нужно выполнять над арендаторами, то отфильтруйте таблицу, измените метод doTableFilter и добавьте имя таблицы:
@Override
public boolean doTableFilter(String tableName) {
List<String> IGNORE_TENANT_TABLES= Arrays.asList("dept");
return IGNORE_TENANT_TABLES.stream().anyMatch(e->e.equalsIgnoreCase(tableName));
}
Таким образом, все запросы в таблице dept не фильтруются:
2. Если есть какие-то конкретные операторы SQL, которые не хотят фильтроваться исполняющим тенантом, их можно открыть в виде аннотации @SqlParser, при этом аннотацию можно добавить только к методам интерфейса Mapper:
@SqlParser(filter = true)
@Select("select * from user where name =#{name}")
User selectUserByName(@Param(value="name") String name);
Либо укажите метод, который нужно отфильтровать в пейджинговом перехватчике:
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
paginationInterceptor.setSqlParserFilter(metaObject->{
MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject);
// 对应Mapper、dao中的方法
if("com.cn.tenant.dao.UserMapper.selectUserByPhone".equals(ms.getId())){
return true;
}
return false;
});
...
}
Вышеупомянутые два метода выполняют одну и ту же функцию, но если нужно отфильтровать много SQL-операторов, второй метод будет сложнее настроить, поэтому рекомендуется фильтровать по аннотации.
Кроме того, есть еще одна яма, на которую легче наступить: при копировании bean-компонентов не копируйте поле tenant id, иначе оператор SQL сообщит об ошибке:
public void createSnapshot(Long userId){
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, userId));
UserSnapshot userSnapshot=new UserSnapshot();
BeanUtil.copyProperties(user,userSnapshot);
userSnapshotMapper.insert(userSnapshot);
}
Глядя на отчет об ошибке, можно увидеть, что когда поле tenant самого bean-компонента не пусто, SQL автоматически добавляет условие запроса tenant, что приводит к ошибке:
Мы можем изменить оператор копирования bean-компонента и вручную игнорировать поле идентификатора арендатора.Здесь мы используем класс инструментов BeanUtil hutool, который может добавлять поля игнорирования.
BeanUtil.copyProperties(user,userSnapshot,"tenantId");
После игнорирования копии идентификатора арендатора запрос может выполняться в обычном режиме.
Наконец, давайте взглянем на поддержку запросов к таблице соединений, сначала взглянем на SQL, содержащий подзапросы:
@Select("select * from user where id in (select id from user_snapshot)")
List<User> selectSnapshot();
Глядя на результаты выполнения, можно увидеть, что внутри подзапроса также автоматически добавляются условия запроса арендатора:
Давайте взглянем на запрос таблицы соединений с помощью Join:
@Select("select u.* from user u left join user_snapshot us on u.id=us.id")
List<User> selectSnapshot();
Точно так же условия фильтрации арендаторов будут добавлены как в левую, так и в правую таблицы:
Давайте еще раз взглянем на обычный запрос таблицы соединения без соединения:
@Select("select u.* from user u ,user_snapshot us,dept d where u.id=us.id and d.id is not null")
List<User> selectSnapshot();
Глядя на результаты выполнения, можно увидеть, что в этом случае в первую таблицу после ключевого слова FROM добавляется только условие фильтра арендатора, поэтому при использовании этого метода запроса нужно обратить особое внимание, а пользователь необходимо вручную добавить его в оператор SQL Добавить фильтр арендатора.