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>
,因此根据该值,我们可以将请求重定向到使用正确密钥空间的正确服务器。
问题:我们的一位新客户希望将数据存储在他自己的国家(法律规定)。但是,我们使用分布在不同国家/地区的几个数据中心的现有客户数据。
问题:我们如何在不改变现有 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>
,因此根据该值,我们可以将请求重定向到使用正确密钥空间的正确服务器。