事务支持

习惯于使用关系数据库的程序员在接触 LDAP 世界时,经常会对没有事务的概念感到惊讶。这在协议中没有规定,也没有 LDAP 服务器支持它。认识到这可能是一个重大问题,Spring LDAP 为 LDAP 资源提供客户端侧补偿事务支持。

LDAP 事务支持由 ContextSourceTransactionManager 提供,它是一个 PlatformTransactionManager 实现,用于管理 Spring 对 LDAP 操作的事务支持。它与协作者一起,跟踪事务中执行的 LDAP 操作,记录每个操作之前的状态,并在需要回滚事务时采取措施恢复初始状态。

除了实际的事务管理之外,Spring LDAP 事务支持还确保在同一事务中使用相同的 DirContext 实例。也就是说,DirContext 实际上不会在事务完成之前关闭,从而允许更有效地使用资源。

虽然 Spring LDAP 用于提供事务支持的方法足以满足许多情况,但它绝不是传统意义上的“真实”事务。服务器完全不知道事务,因此(例如)如果连接断开,则无法回滚事务。虽然应该仔细考虑这一点,但也应该注意,另一种选择是完全没有事务支持。Spring LDAP 的事务支持几乎是最好的了。
客户端事务支持在原始操作所需的工作之外增加了一些开销。虽然在大多数情况下,这种开销不应成为问题,但如果您的应用程序在同一事务中没有执行多个 LDAP 操作(例如,modifyAttributes 后跟 rebind),或者如果不需要与 JDBC 数据源进行事务同步(请参阅 JDBC 事务集成),那么使用 LDAP 事务支持几乎没有益处。

配置

如果您习惯于配置 Spring 事务,那么配置 Spring LDAP 事务看起来会非常熟悉。您可以使用 @Transactional 注释您的事务类,创建 TransactionManager 实例,并在您的 bean 配置中包含 <tx:annotation-driven> 元素。以下示例展示了如何执行此操作

<ldap:context-source
       url="ldap://127.0.0.1:389"
       base="dc=example,dc=com"
       username="cn=Manager"
       password="secret" />

<ldap:ldap-template id="ldapTemplate" />
<ldap:transaction-manager>
    <!--
    Note this default configuration will not work for more complex scenarios;
    see below for more information on RenamingStrategies.
    -->
   <ldap:default-renaming-strategy />
</ldap:transaction-manager>

<!--
   The MyDataAccessObject class is annotated with @Transactional.
-->
<bean id="myDataAccessObject" class="com.example.MyRepository">
  <property name="ldapTemplate" ref="ldapTemplate" />
</bean>

<tx:annotation-driven />
...
虽然此设置对于大多数简单用例来说效果很好,但一些更复杂的场景需要额外的配置。具体来说,如果您需要在事务中创建或删除子树,则需要使用替代的 TempEntryRenamingStrategy,如 重命名策略 中所述。

在实际情况中,您可能会在服务对象级别而不是存储库级别应用事务。前面的示例演示了总体思路。

JDBC 事务集成

使用 LDAP 时的一个常见用例是,一些数据存储在 LDAP 树中,而其他数据存储在关系数据库中。在这种情况下,事务支持变得更加重要,因为不同资源的更新应该同步。

虽然不支持实际的 XA 事务,但通过向 <ldap:transaction-manager> 元素提供 data-source-ref 属性,可以提供支持来概念性地将 JDBC 和 LDAP 访问包装在同一事务中。这将创建一个 ContextSourceAndDataSourceTransactionManager,它将这两个事务虚拟地管理为一个事务。在执行提交时,LDAP 部分的操作始终先执行,如果 LDAP 提交失败,则让两个事务都回滚。JDBC 部分的事务与 DataSourceTransactionManager 中的管理方式完全相同,只是不支持嵌套事务。以下示例展示了带有 data-source-ref 属性的 ldap:transaction-manager 元素

<ldap:transaction-manager data-source-ref="dataSource" >
  <ldap:default-renaming-strategy />
<ldap:transaction-manager />
提供的支持完全是客户端的。包装的事务不是 XA 事务。不会执行两阶段提交,因为 LDAP 服务器无法对结果进行投票。

您可以通过向 <ldap:transaction-manager> 元素提供 session-factory-ref 属性来实现 Hibernate 集成中的相同操作,如下所示

<ldap:transaction-manager session-factory-ref="dataSource" >
  <ldap:default-renaming-strategy />
<ldap:transaction-manager />

LDAP 补偿事务解释

Spring LDAP 通过在每个修改操作(bindunbindrebindmodifyAttributesrename)之前记录 LDAP 树中的状态来管理补偿事务。这使系统能够在需要回滚事务时执行补偿操作。

在许多情况下,补偿操作非常简单。例如,bind 操作的补偿回滚操作是取消绑定条目。但是,其他操作需要不同的、更复杂的方法,因为 LDAP 数据库具有一些特定特征。具体来说,并非总是可以获取条目所有 Attributes 的值,这使得上述策略对于(例如)unbind 操作来说不够充分。

这就是为什么在 Spring LDAP 管理的事务中执行的每个修改操作在内部被分成四个不同的操作:记录操作、准备操作、提交操作和回滚操作。下表描述了每个 LDAP 操作。

LDAP 操作 记录 准备 提交 回滚

绑定

记录要绑定的条目的 DN。

绑定条目。

无操作。

使用记录的 DN 解除绑定条目。

重命名

记录原始 DN 和目标 DN。

重命名条目。

无操作。

将条目重命名回其原始 DN。

解除绑定

记录原始 DN 并计算临时 DN。

将条目重命名到临时位置。

解除绑定临时条目。

将条目从临时位置重命名回其原始 DN。

重新绑定

记录原始 DN 和新的 Attributes 并计算临时 DN。

将条目重命名到临时位置。

在原始 DN 处绑定新的 Attributes 并从其临时位置解除绑定原始条目。

将条目从临时位置重命名回其原始 DN。

修改属性

记录要修改的条目的 DN 并计算要进行的修改的补偿 ModificationItem 实例。

执行 modifyAttributes 操作。

无操作。

使用计算的补偿 ModificationItem 实例执行 modifyAttributes 操作。

有关 Spring LDAP 事务支持内部工作原理的更详细说明,请参阅 Javadoc

重命名策略

如上一节表格中所述,某些操作的事务管理需要在实际修改在提交中进行之前,将受操作影响的原始条目临时重命名。计算条目临时 DN 的方式由 TempEntryRenamingStrategy 管理,该策略在配置中 <ldap:transaction-manager > 声明的子元素中指定。Spring LDAP 包含两个实现

  • DefaultTempEntryRenamingStrategy(默认):使用 <ldap:default-renaming-strategy /> 元素指定。在条目 DN 的最低有效部分添加后缀。例如,对于 DN cn=john doe, ou=users,此策略返回临时 DN cn=john doe_temp, ou=users。您可以通过设置 temp-suffix 属性来配置后缀。

  • DifferentSubtreeTempEntryRenamingStrategy:使用 <ldap:different-subtree-renaming-strategy /> 元素指定。它将子树 DN 附加到 DN 的最低有效部分。这样做会使所有临时条目都放置在 LDAP 树中的特定位置。临时子树 DN 通过设置 subtree-node 属性来配置。例如,如果 subtree-nodeou=tempEntries 并且条目的原始 DN 是 cn=john doe, ou=users,则临时 DN 是 cn=john doe, ou=tempEntries。请注意,配置的子树节点需要存在于 LDAP 树中。

DefaultTempEntryRenamingStrategy 在某些情况下无法正常工作。例如,如果您计划进行递归删除,则需要使用 DifferentSubtreeTempEntryRenamingStrategy。这是因为递归删除操作实际上包括对子树中每个节点的深度优先删除。由于您无法重命名具有任何子节点的条目,并且 DefaultTempEntryRenamingStrategy 会将每个节点保留在同一个子树中(使用不同的名称)而不是实际删除它,因此此操作将失败。如有疑问,请使用 DifferentSubtreeTempEntryRenamingStrategy