问题:我们的一位新客户希望将数据存储在他自己的国家/地区(法律法规)。但是,我们使用现有客户的数据分布在不同国家/地区的少数数据中心。
问题:如何在不改变现有Cassandra架构的情况下将新客户的数据分离到自己的国家?
潜在解决方案#1 :为此客户使用单独的密钥空间。关键空间之间的模式将是相同的,这增加了数据迁移的复杂性等等。 DataStax支持确认可以为每个区域配置密钥空间。
但是我们使用的Spring Data Cassandra不允许动态选择键空间。
唯一的方法是使用CqlTemplate并在调用之前每次运行use keyspace blabla
或在表select * from blabla.mytable
之前添加键空间,但这对我来说听起来像是黑客。
潜在解决方案#2 为新客户使用单独的环境,但管理层拒绝执行此操作。
实现这一目标的其他方法是什么?
答案 0 :(得分:3)
下面的示例和说明与GitHub相同
GitHub中的示例现在正在运行。最具前瞻性的解决方案似乎是使用存储库扩展。将很快更新下面的例子。
请注意,我最初发布的解决方案存在一些我在JMeter测试期间发现的缺陷。 Datastax Java驱动程序参考建议避免通过Session
对象设置键空间。您必须在每个查询中明确设置键空间。
我已更新GitHub存储库并更改了解决方案的说明。
但要非常小心:如果会话由多个线程共享, 在运行时切换键空间很容易导致意外的查询失败。
通常,推荐的方法是使用没有的单个会话 密钥空间,并为所有查询添加前缀。
我会为此特定客户设置一个单独的键空间,并为更改应用程序中的键空间提供支持。我们以前使用这种方法在生产中使用RDBMS和JPA。所以,我想说它也适用于Cassandra。解决方案如下所示。
我将简要介绍如何准备和设置Spring Data Cassandra以在每个请求上配置目标密钥空间。
我首先要定义如何在每个请求上设置租户ID。 REST API的一个很好的例子就是使用定义它的特定HTTP头:
Tenant-Id: ACME
同样,在每个远程协议上,您都可以在每条消息上转发租户ID。让我们说如果你正在使用AMQP或JMS,你可以转发这个内部邮件标题或属性。
接下来,您应该在控制器内的每个请求上存储传入的标头。您可以使用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);
}
}
最后,您应该根据租户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 {
...
}
如果上述代码存在任何问题,请与我们联系。
https://github.com/gitaroktato/spring-boot-cassandra-multitenant-example
答案 1 :(得分:0)
有2个键空间的建议是正确的。 如果问题是只有2个键空间,为什么不配置2个键空间。 对于Region Dependent客户端 - 写入两者 对于其他人 - 仅写入一个(主)键空间。 不需要数据迁移。 下面是如何配置Spring存储库以命中不同的键空间的示例: http://valchkou.com/spring-boot-cassandra.html#multikeyspace
如果没有
,选择存储库可以很简单if (org in (1,2,3)) {
repoA.save(entity)
repoB.save(entity)
} else {
repoA.save(entity)
}
答案 2 :(得分:0)
经过多次来回,我们决定不在同一个JVM中进行动态密钥空间解析。
决定在每个密钥空间和nginx路由器级别上设置专用的Jetty / Tomcat,以定义应该将请求重定向到哪个服务器(基于来自请求URL的companyId)。
例如,我们所有的端点都有/companyId/<value>
所以根据该值,我们可以将请求重定向到使用正确密钥空间的正确服务器。