[Java] Реализация мультиарендности с использованием Spring Boot, MongoDB и Redis (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В этом руководстве мы рассмотрим, как реализовать мультиарендность в Spring Boot приложении с использованием MongoDB и Redis.Используются:
- Spring Boot 2.4
- Maven 3.6. +
- JAVA 8+
- Монго 4.4
- Redis 5
Что такое мультиарендность?Мультиарендность (англ. multitenancy — «множественная аренда») — это программная архитектура, в которой один экземпляр программного приложения обслуживает нескольких клиентов. Все должно быть общим, за исключением данных разных клиентов, которые должны быть должным образом разделены. Несмотря на то, что они совместно используют ресурсы, арендаторы не знают друг друга, и их данные хранятся совершенно отдельно. Каждый покупатель называется арендатором.Предложение «программное обеспечение как услуга» (SaaS) является примером мультиарендной архитектуры. Более подробно.Модели с несколькими арендаторамиМожно выделить три основных архитектурных шаблона для мультиарендной архитектуры (Multitenancy), которые различаются степенью физического разделения данных клиента.
- База данных для каждого арендатора : каждый арендатор имеет свою собственную базу данных и изолирован от других арендаторов.
- Общая база данных, общая схема: все арендаторы совместно используют базу данных и таблицы. В каждой таблице есть столбец с идентификатором клиента, который показывает владельца строки.
- Общая база данных, отдельная схема : все арендаторы совместно используют базу данных, но имеют свои собственные схемы и таблицы базы данных.
НачнемВ этом руководстве мы реализуем мультиарендность на основе базы данных для каждого клиента.
Мы начнем с создания простого проекта 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
@Component
public 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:
@Slf4j
public 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 — это класс, отвечающий за управление всеми взаимодействиями с базой метаданных .
@Service
public 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, связанный с текущим арендатором.
@Configuration
public 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
@Slf4j
public 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. Для тестирования при запуске приложения мы загружаем фиктивные данные в каждую базу данных клиента.
@Override
public 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.
===========
Источник:
habr.com
===========
===========
Автор оригинала: Anicet Eric
===========Похожие новости:
- [Java, Kotlin] Pattern matching в Java 8
- [Программирование, Java, Kotlin, Gradle, Микросервисы] Шаблон Kotlin микросервисов
- [Тестирование IT-систем, Программирование, Java, MongoDB] Run MongoDB Atlas locally for testing
- [JavaScript] Неудачный опыт миграции Electron приложения на ECMAScript модули
- [JavaScript, ReactJS] Поиск данных в столбцах таблицы с пагинацией (front-часть)
- [Java] Как использовать шаблон Circuit Breaker в приложении Spring Boot (перевод)
- [JavaScript, ReactJS, Карьера в IT-индустрии, TypeScript] Яндекс.Практикум запустил курс «React-разработчик»
- [Java, Анализ и проектирование систем, Промышленное программирование] Как катать релизы несколько раз в день и спать спокойно. Доклад Яндекса
- [Java] Использование Google Protocol Buffers (protobuf) в Java (перевод)
- [Разработка веб-сайтов, JavaScript, Программирование, ReactJS] Разрабатываем чат на React с использованием Socket.IO
Теги для поиска: #_java, #_spring_boot, #_mongodb, #_redis, #_multiarendnost (мультиарендность), #_java
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:48
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В этом руководстве мы рассмотрим, как реализовать мультиарендность в Spring Boot приложении с использованием MongoDB и Redis.Используются:
Мы начнем с создания простого проекта 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> @Slf4j
@Component public 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) { } } @Slf4j
public 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(); } } @Service
public 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; } } Чтобы иметь возможность динамически переключаться на подключение к базе данных mongo, мы создаем класс MultiTenantMongoDBFactory, расширяющий класс SimpleMongoClientDatabaseFactory из org.springframework.data.mongodb.core. Он вернет экземплярMongoDatabase, связанный с текущим арендатором. @Configuration
public 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(); } } @Component
@Slf4j public 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 } @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; } @Override
public 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. =========== Источник: habr.com =========== =========== Автор оригинала: Anicet Eric ===========Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:48
Часовой пояс: UTC + 5