wal*_*alv 3 cassandra spring-data-cassandra
问题:我们的一位新客户希望将数据存储在他自己的国家(法律规定)。但是,我们使用分布在不同国家/地区的几个数据中心的现有客户数据。
问题:如何在不改变现有 Cassandra 架构的情况下,将新客户的数据分离到其本国?
潜在解决方案 #1:为此客户使用单独的密钥空间。键空间之间的模式将是相同的,这增加了数据迁移等的复杂性。DataStax 支持确认可以为每个区域配置密钥空间。但是我们使用的 Spring Data Cassandra 不允许动态选择键空间。唯一的方法是使用 CqlTemplate 并在use keyspace blabla每次调用之前运行或在表之前添加键空间,select * from blabla.mytable但这对我来说听起来像是一个黑客。
潜在的解决方案#2为新客户使用单独的环境,但管理层拒绝这样做。
还有其他方法可以实现这个目标吗?
下面的示例和解释与 GitHub 中的相同
GitHub 中的示例现在正在运行。最具前瞻性的解决方案似乎是使用存储库扩展。将很快更新下面的示例。
请注意,我最初发布的解决方案存在一些缺陷,这是我在 JMeter 测试期间发现的。Datastax Java 驱动程序参考建议避免通过Session对象设置键空间。您必须在每个查询中明确设置键空间。
我更新了 GitHub 存储库并更改了解决方案的描述。
但要非常小心:如果会话由多个线程共享,则在运行时切换键空间很容易导致意外查询失败。
通常,推荐的方法是使用没有键空间的单个会话,并为所有查询添加前缀。
我将为这个特定客户设置一个单独的密钥空间,并为更改应用程序中的密钥空间提供支持。我们之前在生产中将这种方法用于 RDBMS 和 JPA。所以,我想说它也可以与 Cassandra 一起使用。解决方法如下。
我将简要描述如何准备和设置 Spring Data Cassandra 以在每个请求上配置目标键空间。
我将首先定义如何在每个请求上设置租户 ID。一个很好的例子是 REST API 的情况是使用定义它的特定 HTTP 标头:
Tenant-Id: ACME
Run Code Online (Sandbox Code Playgroud)
同样,在每个远程协议上,您可以在每条消息上转发租户 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;
}
}
Run Code Online (Sandbox Code Playgroud)
@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);
}
}
Run Code Online (Sandbox Code Playgroud)
最后,您应该Repository根据租户 ID扩展实现和设置密钥空间
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
}
Run Code Online (Sandbox Code Playgroud)
@SpringBootApplication
@EnableCassandraRepositories(repositoryBaseClass = KeyspaceAwareCassandraRepository.class)
public class DemoApplication {
...
}
Run Code Online (Sandbox Code Playgroud)
如果上面的代码有任何问题,请告诉我。
https://github.com/gitaroktato/spring-boot-cassandra-multitenant-example