事务支持

习惯于使用关系数据库的程序员来到 LDAP 世界时,常常惊讶于没有事务的概念。协议中没有指定,也没有 LDAP 服务器支持它。认识到这可能是一个主要问题,Spring LDAP 提供了对 LDAP 资源的客户端补偿事务支持。

LDAP 事务支持由 ContextSourceTransactionManager 提供,这是一个管理 Spring LDAP 操作事务支持的 PlatformTransactionManager 实现。它及其协作对象会跟踪事务中执行的 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://localhost: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,如 命名策略 中所述。

在实际应用中,你可能倾向于在服务对象级别而不是仓库(repository)级别应用事务。前面的示例演示了总体思路。

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 操作的补偿回滚操作是 unbind 该条目。然而,由于 LDAP 数据库的一些特殊特性,其他操作需要一种不同的、更复杂的方法。具体来说,并非总是能获取条目所有 Attributes 的值,这使得上述策略对于(例如)unbind 操作来说是不够的。

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

LDAP 操作 记录 准备 提交 回滚

bind

记录要绑定的条目的 DN。

绑定条目。

无操作。

使用记录的 DN 解绑条目。

rename

记录原始 DN 和目标 DN。

重命名条目。

无操作。

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

unbind

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

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

解绑临时条目。

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

rebind

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

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

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

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

modifyAttributes

记录要修改的条目的 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