Русский
Русский
English
Статистика
Реклама

Мультиарендность

Перевод Реализация мультиарендности с использованием Spring Boot, MongoDB и Redis

28.02.2021 18:05:07 | Автор: admin

В этом руководстве мы рассмотрим, как реализовать мультиарендность в Spring Boot приложении с использованием MongoDB и Redis.

Используются:

  • Spring Boot 2.4

  • Maven 3.6. +

  • JAVA 8+

  • Монго 4.4

  • Redis 5

Что такое мультиарендность?

Мультиарендность (англ. multitenancy множественная аренда) это программная архитектура, в которой один экземпляр программного приложения обслуживает нескольких клиентов.Все должно быть общим, за исключением данных разных клиентов, которые должны быть должным образом разделены.Несмотря на то, что они совместно используют ресурсы, арендаторы не знают друг друга, и их данные хранятся совершенно отдельно.Каждый покупатель называется арендатором.

Предложение программное обеспечение как услуга (SaaS) является примером мультиарендной архитектуры.Более подробно.

Модели с несколькими арендаторами

Можно выделить три основных архитектурных шаблона для мультиарендной архитектуры (Multitenancy), которые различаются степенью физического разделения данных клиента.

  1. База данных для каждого арендатора: каждый арендатор имеет свою собственную базу данных и изолирован от других арендаторов.

  2. Общая база данных, общая схема:все арендаторы совместно используют базу данных и таблицы.В каждой таблице есть столбец с идентификатором клиента, который показывает владельца строки.

  3. Общая база данных, отдельная схема: все арендаторы совместно используют базу данных, но имеют свои собственные схемы и таблицы базы данных.

Начнем

В этом руководстве мы реализуем мультиарендность на основе базы данных для каждого клиента.

Мы начнем с создания простого проекта Spring Boot наstart.spring.ioсо следующими зависимостями:

<dependencies>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-data-mongodb</artifactId>    </dependency>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>    </dependency>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-data-redis</artifactId>    </dependency>    <dependency>        <groupId>redis.clients</groupId>        <artifactId>jedis</artifactId>    </dependency>    <dependency>        <groupId>org.projectlombok</groupId>        <artifactId>lombok</artifactId>        <optional>true</optional>    </dependency></dependencies>

Определение текущего идентификатора клиента

Идентификатор клиента необходимо определить для каждого клиентского запроса.Для этого мы включим поле идентификатора клиента в заголовок HTTP-запроса.

Давайте добавим перехватчик, который получает идентификатор клиента из http заголовкаX-Tenant.

@Slf4j@Componentpublic class TenantInterceptor implements WebRequestInterceptor {    private static final String TENANT_HEADER = "X-Tenant";    @Override    public void preHandle(WebRequest request) {        String tenantId = request.getHeader(TENANT_HEADER);        if (tenantId != null && !tenantId.isEmpty()) {            TenantContext.setTenantId(tenantId);            log.info("Tenant header get: {}", tenantId);        } else {            log.error("Tenant header not found.");            throw new TenantAliasNotFoundException("Tenant header not found.");        }    }    @Override    public void postHandle(WebRequest webRequest, ModelMap modelMap) {        TenantContext.clear();    }    @Override    public void afterCompletion(WebRequest webRequest, Exception e) {    }}

TenantContextэто хранилище, содержащее переменную ThreadLocal.ThreadLocal можно рассматривать как область доступа (scope of access), такую как область запроса (request scope) или область сеанса (session scope).

Сохраняя tenantId в ThreadLocal, мы можем быть уверены, что каждый поток имеет свою собственную копию этой переменной и что текущий поток не имеет доступа к другому tenantId:

@Slf4jpublic class TenantContext {    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();    public static void setTenantId(String tenantId) {        log.debug("Setting tenantId to " + tenantId);        CONTEXT.set(tenantId);    }    public static String getTenantId() {        return CONTEXT.get();    }    public static void clear() {        CONTEXT.remove();    }}

Настройка источников данных клиента (Tenant Datasources)

В нашей архитектуре у нас есть экземпляр Redis, представляющий базу метаданных (master database), в которой централизована вся информация о базе данных клиента.Таким образом, из каждого предоставленного идентификатора клиента информация о подключении к базе данных извлекается из базы метаданных .

RedisDatasourceService.java это класс, отвечающий за управление всеми взаимодействиями с базой метаданных .

@Servicepublic class RedisDatasourceService {    private final RedisTemplate redisTemplate;    private final ApplicationProperties applicationProperties;    private final DataSourceProperties dataSourceProperties;public RedisDatasourceService(RedisTemplate redisTemplate, ApplicationProperties applicationProperties, DataSourceProperties dataSourceProperties) {        this.redisTemplate = redisTemplate;        this.applicationProperties = applicationProperties;        this.dataSourceProperties = dataSourceProperties;    }        /**     * Save tenant datasource infos     *     * @param tenantDatasource data of datasource     * @return status if true save successfully , false error     */        public boolean save(TenantDatasource tenantDatasource) {        try {            Map ruleHash = new ObjectMapper().convertValue(tenantDatasource, Map.class);            redisTemplate.opsForHash().put(applicationProperties.getServiceKey(), String.format("%s_%s", applicationProperties.getTenantKey(), tenantDatasource.getAlias()), ruleHash);            return true;        } catch (Exception e) {            return false;        }    }        /**     * Get all of keys     *     * @return list of datasource     */         public List findAll() {        return redisTemplate.opsForHash().values(applicationProperties.getServiceKey());    }        /**     * Get datasource     *     * @return map key and datasource infos     */         public Map<String, TenantDatasource> loadServiceDatasources() {        List<Map<String, Object>> datasourceConfigList = findAll();        // Save datasource credentials first time        // In production mode, this part can be skip        if (datasourceConfigList.isEmpty()) {            List<DataSourceProperties.Tenant> tenants = dataSourceProperties.getDatasources();            tenants.forEach(d -> {                TenantDatasource tenant = TenantDatasource.builder()                        .alias(d.getAlias())                        .database(d.getDatabase())                        .host(d.getHost())                        .port(d.getPort())                        .username(d.getUsername())                        .password(d.getPassword())                        .build();                save(tenant);            });        }        return getDataSourceHashMap();    }        /**     * Get all tenant alias     *     * @return list of alias     */         public List<String> getTenantsAlias() {        // get list all datasource for this microservice        List<Map<String, Object>> datasourceConfigList = findAll();        return datasourceConfigList.stream().map(data -> (String) data.get("alias")).collect(Collectors.toList());    }        /**     * Fill the data sources list.     *     * @return Map<String, TenantDatasource>     */         private Map<String, TenantDatasource> getDataSourceHashMap() {        Map<String, TenantDatasource> datasourceMap = new HashMap<>();        // get list all datasource for this microservice        List<Map<String, Object>> datasourceConfigList = findAll();        datasourceConfigList.forEach(data -> datasourceMap.put(String.format("%s_%s", applicationProperties.getTenantKey(), (String) data.get("alias")), new TenantDatasource((String) data.get("alias"), (String) data.get("host"), (int) data.get("port"), (String) data.get("database"), (String) data.get("username"), (String) data.get("password"))));        return datasourceMap;    }}

В этом руководстве мы заполнили информацию о клиенте из yml-файла (tenants.yml).В производственном режиме можно создать конечные точки для сохранения информации о клиенте в базе метаданных.

Чтобы иметь возможность динамически переключаться на подключение к базе данных mongo, мы создаемкласс MultiTenantMongoDBFactory, расширяющий класс SimpleMongoClientDatabaseFactory из org.springframework.data.mongodb.core.Он вернетэкземплярMongoDatabase, связанный с текущим арендатором.

@Configurationpublic class MultiTenantMongoDBFactory extends SimpleMongoClientDatabaseFactory {@Autowired    MongoDataSources mongoDataSources;public MultiTenantMongoDBFactory(@Qualifier("getMongoClient") MongoClient mongoClient, String databaseName) {        super(mongoClient, databaseName);    }    @Override    protected MongoDatabase doGetMongoDatabase(String dbName) {        return mongoDataSources.mongoDatabaseCurrentTenantResolver();    }}

Нам нужно инициализироватьконструктор MongoDBFactoryMultiTenantс параметрами по умолчанию (MongoClientиdatabaseName).

Это реализует прозрачный механизм для получения текущего клиента.

@Component@Slf4jpublic class MongoDataSources {    /**     * Key: String tenant alias     * Value: TenantDatasource     */    private Map<String, TenantDatasource> tenantClients;    private final ApplicationProperties applicationProperties;    private final RedisDatasourceService redisDatasourceService;    public MongoDataSources(ApplicationProperties applicationProperties, RedisDatasourceService redisDatasourceService) {        this.applicationProperties = applicationProperties;        this.redisDatasourceService = redisDatasourceService;    }    /**     * Initialize all mongo datasource     */    @PostConstruct    @Lazy    public void initTenant() {        tenantClients = new HashMap<>();        tenantClients = redisDatasourceService.loadServiceDatasources();    }    /**     * Default Database name for spring initialization. It is used to be injected into the constructor of MultiTenantMongoDBFactory.     *     * @return String of default database.     */    @Bean    public String databaseName() {        return applicationProperties.getDatasourceDefault().getDatabase();    }    /**     * Default Mongo Connection for spring initialization.     * It is used to be injected into the constructor of MultiTenantMongoDBFactory.     */    @Bean    public MongoClient getMongoClient() {        MongoCredential credential = MongoCredential.createCredential(applicationProperties.getDatasourceDefault().getUsername(), applicationProperties.getDatasourceDefault().getDatabase(), applicationProperties.getDatasourceDefault().getPassword().toCharArray());        return MongoClients.create(MongoClientSettings.builder()                .applyToClusterSettings(builder ->                        builder.hosts(Collections.singletonList(new ServerAddress(applicationProperties.getDatasourceDefault().getHost(), Integer.parseInt(applicationProperties.getDatasourceDefault().getPort())))))                .credential(credential)                .build());    }    /**     * This will get called for each DB operations     *     * @return MongoDatabase     */    public MongoDatabase mongoDatabaseCurrentTenantResolver() {        try {            final String tenantId = TenantContext.getTenantId();            // Compose tenant alias. (tenantAlias = key + tenantId)            String tenantAlias = String.format("%s_%s", applicationProperties.getTenantKey(), tenantId);            return tenantClients.get(tenantAlias).getClient().                    getDatabase(tenantClients.get(tenantAlias).getDatabase());        } catch (NullPointerException exception) {            throw new TenantAliasNotFoundException("Tenant Datasource alias not found.");        }    }73}

Тест

Давайте создадим CRUD пример с документом Employee.

@Builder@Data@AllArgsConstructor@NoArgsConstructor@Accessors(chain = true)@Document(collection = "employee")public class Employee  {    @Id    private String id;    private String firstName;    private String lastName;    private String email;}

Также нам нужно создать классы EmployeeRepository, EmployeeServiceиEmployeeController.Для тестирования при запуске приложения мы загружаем фиктивные данные в каждую базу данных клиента.

@Overridepublic void run(String... args) throws Exception {    List<String> aliasList = redisDatasourceService.getTenantsAlias();    if (!aliasList.isEmpty()) {        //perform actions for each tenant        aliasList.forEach(alias -> {            TenantContext.setTenantId(alias);            employeeRepository.deleteAll();            Employee employee = Employee.builder()                    .firstName(alias)                    .lastName(alias)                    .email(String.format("%s%s", alias, "@localhost.com" ))                    .build();            employeeRepository.save(employee);            TenantContext.clear();        });    }}

Теперь мы можем запустить наше приложение и протестировать его.

Итак, мы все сделали. Надеюсь, это руководство поможет вам понять, что такое мультиарендность и как она может быть реализована в Spring Boot проекте с использованием MongoDB и Redis.

Полный исходный код примера можно найти наGitHub.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru