Кассандра: данные о клиентах в каждом пространстве ключей

Проблема: один из наших новых клиентов хочет, чтобы данные хранились в его собственной стране (нормативные акты). Однако мы используем существующие данные клиентов, распределенные по нескольким центрам обработки данных в разных странах.

Вопрос: как мы можем отделить данные о новых клиентах, чтобы они находились в своей стране, без существенного изменения существующей архитектуры Cassandra?

Потенциальное решение № 1: использовать отдельное пространство клавиш для этого клиента. Схемы будут одинаковыми между пространствами ключей, что добавляет сложности для переноса данных и так далее. Поддержка DataStax подтвердила, что можно настроить пространство ключей для каждого региона. Однако Spring Data Cassandra, которую мы используем, не позволяет динамически выбирать пространство клавиш. Единственный способ - использовать CqlTemplate и запустить use keyspace blabla каждый раз перед звонком или добавить пространство перед таблицей select * from blabla.mytable но это звучит как хак для меня.

Потенциальное решение № 2 использовать отдельную среду для нового клиента, но руководство отказывается это сделать.

Есть ли другие способы достижения этой цели?

3 ответа

Решение

Обновление 3

Пример и объяснение ниже такие же, как в GitHub

Обновление 2

Пример в GitHub теперь работает. Казалось, что самое перспективное решение - использовать расширения репозитория. Скоро обновлю пример ниже.

Обновить

Обратите внимание, что у решения, которое я первоначально разместил, были некоторые недостатки, которые я обнаружил во время тестов JMeter. Справочник по драйверу Java Datastax рекомендует избегать установки пространства ключей через Session объект. Вы должны установить пространство ключей явно в каждом запросе.

Я обновил репозиторий GitHub, а также изменил описание решения.

Однако будьте очень осторожны: если сеанс используется несколькими потоками, переключение пространства клавиш во время выполнения может легко вызвать непредвиденные сбои запроса.

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

Описание решения

Я бы настроил отдельное пространство ключей для этого конкретного клиента и предоставил бы поддержку для изменения пространства ключей в приложении. Мы использовали этот подход ранее с RDBMS и JPA в производстве. Так что я бы сказал, что он может работать и с Кассандрой. Решение было похоже, как показано ниже.

Я кратко опишу, как подготовить и настроить Spring Data Cassandra для настройки целевого пространства ключей на каждый запрос.

Шаг 1: Подготовка ваших услуг

Сначала я бы определил, как устанавливать идентификатор арендатора для каждого запроса. Хорошим примером может служить API REST в случае использования конкретного HTTP-заголовка, который его определяет:

Tenant-Id: ACME

Точно так же на каждом удаленном протоколе вы можете пересылать идентификатор арендатора на каждое сообщение. Допустим, если вы используете AMQP или JMS, вы можете переслать это внутри заголовка сообщения или свойств.

Шаг 2: Получение идентификатора клиента в приложении

Далее, вы должны хранить входящий заголовок для каждого запроса внутри ваших контроллеров. Ты можешь использовать ThreadLocal или вы можете попробовать использовать bean-объект в области запроса.

@Component
@Scope(scopeName = "request", proxyMode= ScopedProxyMode.TARGET_CLASS)
public class TenantId {

    private String tenantId;

    public void set(String id) {
        this.tenantId = id;
    }

    public String get() {
        return tenantId;
    }
}

@RestController
public class UserController {

    @Autowired
    private UserRepository userRepo;
    @Autowired
    private TenantId tenantId;

    @RequestMapping(value = "/userByName")
    public ResponseEntity<String> getUserByUsername(
            @RequestHeader("Tenant-ID") String tenantId,
            @RequestParam String username) {
        // Setting the tenant ID
        this.tenantId.set(tenantId);
        // Finding user
        User user = userRepo.findOne(username);
        return new ResponseEntity<>(user.getUsername(), HttpStatus.OK);
    }
}

Шаг 3. Установка идентификатора клиента на уровне доступа к данным

Наконец вы должны продлить Repository реализации и настройки пространства ключей в соответствии с идентификатором клиента

public class KeyspaceAwareCassandraRepository<T, ID extends Serializable>
        extends SimpleCassandraRepository<T, ID>  {

    private final CassandraEntityInformation<T, ID> metadata;
    private final CassandraOperations operations;

    @Autowired
    private TenantId tenantId;

    public KeyspaceAwareCassandraRepository(
            CassandraEntityInformation<T, ID> metadata,
            CassandraOperations operations) {
        super(metadata, operations);
        this.metadata = metadata;
        this.operations = operations;
    }

    private void injectDependencies() {
        SpringBeanAutowiringSupport
                .processInjectionBasedOnServletContext(this,
                getServletContext());
    }

    private ServletContext getServletContext() {
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getRequest().getServletContext();
    }

    @Override
    public T findOne(ID id) {
        injectDependencies();
        CqlIdentifier primaryKey = operations.getConverter()
                .getMappingContext()
                .getPersistentEntity(metadata.getJavaType())
                .getIdProperty().getColumnName();

        Select select = QueryBuilder.select().all()
                .from(tenantId.get(),
                        metadata.getTableName().toCql())
                .where(QueryBuilder.eq(primaryKey.toString(), id))
                .limit(1);

        return operations.selectOne(select, metadata.getJavaType());
    }

    // All other overrides should be similar
}

@SpringBootApplication
@EnableCassandraRepositories(repositoryBaseClass = KeyspaceAwareCassandraRepository.class)
public class DemoApplication {
...
}

Дайте мне знать, если есть какие-либо проблемы с кодом выше.

Пример кода в GitHub

https://github.com/gitaroktato/spring-boot-cassandra-multitenant-example

Рекомендации

После многих перемоток мы решили не выполнять динамическое разрешение пространства клавиш в одной и той же JVM.

Было принято решение выделить Jetty/Tomcat для каждого пространства ключей и на уровне маршрутизатора nginx, чтобы определить, на какой сервер должен быть перенаправлен запрос (на основе companyId из URL-адреса запроса).

Например, все наши конечные точки имеют /companyId/<value> поэтому, основываясь на значении, мы можем перенаправить запрос на соответствующий сервер, который использует правильное пространство ключей.

Совет с 2-мя клавишами правильн. Если вопрос в том, чтобы иметь только 2 пространства ключей, почему бы не настроить 2 пространства ключей. для зависимого от региона клиента - пишите обоим
для других - запись только в одно (основное) пространство ключей. Перенос данных не требуется. Вот пример того, как настроить репозитории Spring для работы с различными пространствами ключей: http://valchkou.com/spring-boot-cassandra.html

выбор хранилища может быть простым, если еще

if (org in (1,2,3)) { 
   repoA.save(entity)
   repoB.save(entity)
} else {
   repoA.save(entity)
}
Другие вопросы по тегам