Cassandra:每个键空间的客户数据

Cassandra: customer data per keyspace

问题:我们的一位新客户希望将数据存储在他自己的国家(法律规定)。但是,我们使用分布在不同国家/地区的几个数据中心的现有客户数据。

问题:我们如何在不改变现有 Cassandra 架构的情况下,将新客户的数据分离到自己的国家?

可能的解决方案 #1:为此客户使用单独的密钥空间。键空间之间的模式将相同,这增加了数据迁移等的复杂性。 DataStax 支持确认可以按区域配置键空间。 但是 Spring 我们使用的 Data Cassandra 不允许动态选择键空间。 唯一的方法是每次在调用之前使用 CqlTemplate 和 运行 use keyspace blabla 或在 table select * from blabla.mytable 之前添加键空间,但这对我来说听起来很糟糕。

可能的解决方案 #2 为新客户使用单独的环境,但管理层拒绝这样做。

还有其他方法可以实现这个目标吗?

更新 3

下面的示例和解释与GitHub

中的相同

更新 2

GitHub 中的示例现在可以使用了。最适合未来的解决方案似乎是使用存储库扩展。将很快更新下面的示例。

更新

请注意,我最初发布的解决方案存在一些我在 JMeter 测试期间发现的缺陷。 Datastax Java driver 参考建议避免通过 Session object 设置键空间。您必须在每个查询中显式设置键空间。

我更新了 GitHub 存储库并更改了解决方案的描述。

Be very careful though: if the session is shared by multiple threads, switching the keyspace at runtime could easily cause unexpected query failures.

Generally, the recommended approach is to use a single session with no keyspace, and prefix all your queries.

解决方案说明

我会 set-up 为这个特定客户提供一个单独的键空间,并支持在应用程序中更改键空间。我们之前在生产中将此方法与 RDBMS 和 JPA 一起使用。所以,我想说它也可以与 Cassandra 一起工作。解决方案与下面类似。

我将简要描述如何准备和 set-up Spring Data Cassandra 在每个请求上配置目标键空间。

第 1 步:准备您的服务

我会首先定义如何在每个请求上设置租户 ID。一个很好的例子是 in-case-of REST API 是使用定义它的特定 HTTP header:

Tenant-Id: ACME

类似地,在每个远程协议上,您都可以在每条消息上转发租户 ID。假设您使用的是 AMQP 或 JMS,则可以转发此内部消息 header 或属性。

第 2 步:在应用程序中获取租户 ID

接下来,您应该将每个请求的传入 header 存储在您的控制器中。您可以使用 ThreadLocal 或者您可以尝试使用 request-scoped 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 步:在 data-access 层设置租户 ID

最后,您应该根据租户 ID

扩展 Repository 实现和 set-up 键空间
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

参考资料

具有 2 个键空间的建议是正确的。 如果问题是关于只有 2 个键空间,为什么不配置 2 个键空间。 对于 Region Dependent 客户端 - 写入两者
对于其他人 - 仅写入一个(主)键空间。 不需要数据迁移。 以下是如何配置 Spring 存储库以命中不同键空间的示例: http://valchkou.com/spring-boot-cassandra.html#multikeyspace

存储库的选择可以很简单 if else

if (org in (1,2,3)) { 
   repoA.save(entity)
   repoB.save(entity)
} else {
   repoA.save(entity)
}

经过多次反复,我们决定不在同一个 JVM 中进行动态键空间解析。

已决定为每个键空间和 nginx 路由器级别设置专用的 Jetty/Tomcat,以定义应将请求重定向到哪个服务器(基于请求 url 中的 companyId)。

例如,我们所有的端点都有 /companyId/<value>,因此根据该值,我们可以将请求重定向到使用正确密钥空间的正确服务器。