【问题标题】:Cassandra: customer data per keyspaceCassandra:每个键空间的客户数据
【发布时间】:2018-04-08 10:42:23
【问题描述】:

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

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

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

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

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

【问题讨论】:

  • 如果所有客户共享相同的密钥空间并且其数据分布在多个国家/地区的数据中心,我不知道如何在不创建新密钥空间的情况下实现它。键空间是您指定仅将数据放在特定数据中心(客户国家/地区的数据中心)的位置。
  • @Edu,是的,我们正在以同样的方式思考(潜在的解决方案#1)但是使用 Spring Data Cassandra 不可能使用键空间的动态切换(至少我的几个小时研究没有帮助)。
  • @walv:你为什么说在表前添加“键空间从 blabla.mytable 中选择 *”听起来像一个 hack?这是引用表格的一种正常方式,并且非常常用。它就像一个完全限定的表名。
  • @walv 我不知道您的用例的具体情况,但似乎您总是需要按请求了解客户区域并在“按请求生活方式”上定义键空间,使用它在客户在该请求中进行的 cassandra 查询中。
  • @Edu,是的,每个http请求都会包含用户的companyId,所以我们可以很容易的映射出什么公司属于什么集群。

标签: cassandra spring-data-cassandra


【解决方案1】:

更新 3

下面的示例和解释与 GitHub 中的相同

更新 2

GitHub 中的示例现在正在运行。最面向未来的解决方案似乎是使用存储库扩展。将很快更新下面的示例。

更新

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

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

不过要非常小心:如果会话由多个线程共享, 在运行时切换键空间很容易导致意外的查询失败。

一般来说,推荐的方法是使用单个会话,没有 键空间,并为所有查询添加前缀。

解决方案说明

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

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

第 1 步:准备服务

我将首先定义如何在每个请求上设置租户 ID。一个很好的例子是 REST API 是使用定义它的特定 HTTP 标头:

Tenant-Id: ACME

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

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

接下来,您应该将传入的标头存储在控制器内的每个请求中。您可以使用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 步:在数据访问层设置租户 ID

最后你应该根据租户 ID 扩展 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

参考文献

【讨论】:

  • 对于步骤 1 和 2,我们在 nginx 级别处理它,因为每个 API 都包含公司 ID,例如api/v1/companies/123/... 第 3 步可能适用于 JPA,但绝对不适用于 Spring Data Cassandra。这就是我正在寻找的 - 如何在运行时在 Spring Data Cassandra 中设置键空间。
  • 对于第 1 步和第 2 步,您可以通过重写 HTTP 请求将客户上下文从 nginx 转发到应用程序。应用程序必须知道每个请求需要处理哪个客户请求。步骤#3到底有什么问题? Spring data JPA 也可以与 Cassandra 互操作。你想在你的代码库中避免使用 JPA 并在另一个接口中使用 spring-data-cassandra 吗?
  • 对于第 1 步和第 2 步,您是对的,通过来自 url 的公司 ID,我们定义了应该使用的密钥空间。关于第 3 步,我不确定 JPA 是否可以与 Cassandra 一起使用。也许您的意思是 Spring Data 可以使用 Cassandra 而不是 JPA?有了方法拦截的思路,听起来不错。我唯一怀疑的是,如果我用 CassandraTemplate(单例)替换 EntityManager,并且我查询“USE KEYSPACE”,那么我可以在多线程环境中获得不可预测的读/写,因为 Cassandra 没有事务。但也许我理解错了?
  • 谢谢,只要我有更多时间,我会不时纠正这个问题。
  • 我根据我们的讨论纠正了一些编程错误,现在它在本地对我有用。
【解决方案2】:

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

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

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

【讨论】:

  • 我真的认为你可以在同一个实例中做到这一点,但如果不扩展 spring-data-cassandra 库,这是不可能的。太糟糕了,他们没有引入多租户作为一种选择。有一张已关闭的 JIRA 票证,但与此功能无关:jira.spring.io/browse/…
  • 不知何故我会尝试推动这个并完成扩展库以具有多租户选项。只是举个例子说明如何做到这一点。
  • 我用另一种解决方案更新了我的答案。你可以看看
【解决方案3】:

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

如果其他情况下,存储库的选择可以很简单

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

【讨论】:

  • 恐怕在我们的例子中,很难预测我们将只需要两个键空间而不是更多。明天可能是第三天和后天。这就是为什么我们一直在寻找动态解决方案来避免将来出现这种 PITA。
  • 如果每个键空间有多个会话,应用程序必须为每个租户保留一个单独的连接池。如果租户数量增加,这可能会导致不必要的开销。
猜你喜欢
  • 2016-08-01
  • 2023-03-27
  • 2019-09-07
  • 1970-01-01
  • 2020-08-02
  • 1970-01-01
  • 2015-01-01
  • 2015-11-10
  • 1970-01-01
相关资源
最近更新 更多