Contents

Spring Data Couchbase 的实体验证,乐观的锁定和查询一致性

1. 简介

在介绍了 Spring Data Couchbase 之后,在第二个教程中,我们将重点介绍对 Couchbase 文档数据库的实体验证 (JSR-303)、乐观锁定和不同级别的查询一致性的支持。

2. 实体验证

Spring Data Couchbase 提供对 JSR-303 实体验证注解的支持。为了利用此功能,首先我们将 JSR-303 库添加到 Maven 项目的依赖项部分:

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.1.0.Final</version>
</dependency>

然后我们添加一个 JSR-303 的实现。我们将使用 Hibernate 实现:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.2.4.Final</version>
</dependency>

最后,我们将验证器工厂 bean 和相应的 Couchbase 事件侦听器添加到 Couchbase 配置中:

@Bean
public LocalValidatorFactoryBean localValidatorFactoryBean() {
    return new LocalValidatorFactoryBean();
}
@Bean
public ValidatingCouchbaseEventListener validatingCouchbaseEventListener() {
    return new ValidatingCouchbaseEventListener(localValidatorFactoryBean());
}

等效的 XML 配置如下所示:

<bean id="validator"
  class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
<bean id="validatingEventListener" 
  class="org.springframework.data.couchbase.core.mapping.event.ValidatingCouchbaseEventListener"/>

现在我们将 JSR-303 注释添加到我们的实体类中。在持久性操作期间遇到约束冲突时,操作将失败,并抛出ConstraintViolationException

以下是我们可以强制执行的涉及Student实体的约束示例:

@Field
@NotNull
@Size(min=1, max=20)
@Pattern(regexp="^[a-zA-Z .'-]+$")
private String firstName;
...
@Field
@Past
private DateTime dateOfBirth;

3. 乐观锁定

Spring Data Couchbase 不支持类似于您可以在其他 Spring Data 模块中实现的多文档事务,例如 Spring Data JPA(通过*@Transactional*注解),也不提供回滚功能。

然而,它通过使用*@Version*注解以与其他 Spring Data 模块大致相同的方式支持乐观锁定:

@Version
private long version;

在幕后,Couchbase 使用所谓的“比较和交换”(CAS) 机制来实现数据存储级别的乐观锁定。

Couchbase 中的每个文档都有一个关联的 CAS 值,只要文档的元数据或内容发生更改,该值就会自动修改。每当从 Couchbase 检索文档时,在字段上使用*@Version*注解会导致该字段填充当前 CAS 值。

当您尝试将文档保存回 Couchbase 时,会根据 Couchbase 中的当前 CAS 值检查此字段。如果值不匹配,持久性操作将失败并出现OptimisticLockingException

请务必注意,切勿尝试访问或修改代码中的此字段。

4.查询一致性

在 Couchbase 上实现持久层时,您必须考虑过时读取和写入的可能性。这是因为在插入、更新或删除文档时,可能需要一些时间才能更新支持视图和索引以反映这些更改。

如果你有一个由 Couchbase 节点集群支持的大型数据集,这可能会成为一个严重的问题,尤其是对于 OLTP 系统。

Spring Data 为某些存储库和模板操作提供了强大的一致性级别,以及几个选项,可让您确定应用程序可接受的读写一致性级别。

4.1. 一致性水平

Spring Data 允许您通过org.springframework.data.couchbase.core.query包中的Consistency枚举为您的应用程序指定不同级别的查询一致性和陈旧性。

此枚举定义了以下级别的查询一致性和陈旧性,从最低到最严格:

  • EVENTUALLY_CONSISTENT
    • 允许过时的读取
    • 索引根据 Couchbase 标准算法更新
  • UPDATE_AFTER
    • 允许过时的读取
    • 每次请求后更新索引
  • DEFAULT_CONSISTENCY(与READ_YOUR_OWN_WRITES相同)
  • READ_YOUR_OWN_WRITES
    • 不允许过时的读取
    • 每次请求后更新索引
  • STRONGLY_CONSISTENT
    • 不允许过时的读取
    • 索引在每条语句后更新

4.2. 默认行为

考虑您的文档已从 Couchbase 中删除,并且支持视图和索引尚未完全更新的情况。

CouchbaseRepository内置方法*deleteAll()*安全地忽略支持视图找到但视图尚未反映其删除的文档。

同样,CouchbaseTemplate内置方法findByViewfindBySpatialView通过不返回最初由支持视图找到但已被删除的文档来提供类似级别的一致性。

对于所有其他模板方法、内置存储库方法和派生存储库查询方法,根据撰写本文时的官方 Spring Data Couchbase 2.1.x 文档,Spring Data 使用默认一致性级别Consistency.READ_YOUR_OWN_WRITES

值得注意的是,该库的早期版本使用默认值Consistency.UPDATE_AFTER

无论您使用哪个版本,如果您对盲目接受提供的默认一致性级别有任何保留,Spring 提供了两种方法,您可以通过这些方法以声明方式控制正在使用的一致性级别,如以下小节所述。

4.3. 全局一致性设置

如果您正在使用 Couchbase 存储库并且您的应用程序需要更高级别的一致性,或者如果它可以容忍较弱的级别,那么您可以通过覆盖Couchbase 配置中的*getDefaultConsistency()*方法来覆盖所有存储库的默认一致性设置。

以下是在 Couchbase 配置类中覆盖全局一致性级别的方法:

@Override
public Consistency getDefaultConsistency() {
    return Consistency.STRONGLY_CONSISTENT;
}

这是等效的 XML 配置:

<couchbase:template consistency="STRONGLY_CONSISTENT"/>

请注意,更严格的一致性级别的代价是增加了查询时的延迟,因此请务必根据应用程序的需要调整此设置。

例如,数据仓库或报告应用程序通常仅在批处理中附加或更新数据,这将是EVENTUALLY_CONSISTENT的良好候选者,而 OLTP 应用程序可能应该倾向于更严格的级别,例如READ_YOUR_OWN_WRITESSTRONGLY_CONSISTENT

4.4. 自定义一致性实现

如果您需要更精细调整的一致性设置,您可以通过为您想要独立控制其一致性级别的任何查询提供您自己的存储库实现并使用queryView和/或CouchbaseTemplate提供的queryN1QL方法。

让我们为我们的Student实体实现一个名为findByFirstNameStartsWith的自定义存储库方法,我们不想允许过时读取。

首先,创建一个包含自定义方法声明的接口:

public interface CustomStudentRepository {
    List<Student> findByFirstNameStartsWith(String s);
}

接下来,实现接口,将底层 Couchbase Java SDK 的Stale设置设置为所需的级别:

public class CustomStudentRepositoryImpl implements CustomStudentRepository {
    @Autowired
    private CouchbaseTemplate template;
    public List<Student> findByFirstNameStartsWith(String s) {
        return template.findByView(ViewQuery.from("student", "byFirstName")
          .startKey(s)
          .stale(Stale.FALSE),
          Student.class);
    }
}

最后,通过让标准存储库接口扩展通用CrudRepository接口和自定义存储库接口,客户端将可以访问标准存储库接口的所有内置和派生方法,以及您在自定义存储库类中实现的任何自定义方法:

public interface StudentRepository extends CrudRepository<Student, String>,
  CustomStudentRepository {
    ...
}