7. 写锁
该章节描述了与写锁类型相关的特定语义. 写锁是锁类型的一个具体实例, 也是本规范中唯一描述的锁类型.
互斥写锁可以保护对应资源: 其防止除锁创建者之外的任何主体以及在其他任何锁令牌未提及情况下 (e.g., 持有锁的客户端进程之外的其他进程) 进行更改.
客户端在对写锁定资源进行修改的任何请求中, 必须[MUST]提交一个被授权使用的锁令牌. 写锁涵盖的可修改项包括:
- 以下任意一种更改写锁资源的情况:
- 对于集合内部成员 URI 的任意修改. 如被添加, 删除或标识为不同资源, 则该集合内部成员 URI 会被认为已修改. 有关写锁和集合的更多讨论参见第 7.4 章.
- 一个对写锁根映射的修改, 要么被映射到另一个资源,要么映射到空资源(e.g., DELETE).
在 HTTP 和 WebDAV 中定义的方法中, PUT, POST, PROPPATCH, LOCK, UNLOCK, MOVE, COPY (用于目标资源), DELETE 和 MKCOL 都会受到写锁的影响. 截止目前, HTTP/WebDAV 中定义的所有其他方法都与写锁无关, 特别是 GET方法.
接下来的几小节将更具体地描述写锁与各种操作的交互方式.
7.1. 写锁和属性
尽管那些没有写锁的主体不被允许更改资源属性, 但由于其模式要求, 即使在资源被锁定的情况下, 活属性的值仍然可能发生变化. 只有死属性和被定义为可锁定的活属性才能保证在写锁期间不发生更改.
7.2. 避免丢失更新
尽管写锁有助于防止丢失更新, 但不能保证更新永远不会丢失. 考虑以下情景:
两个客户端 A 和 B 都想编辑资源"index.html". 客户端 A 是一个 HTTP 客户端, 而不是 WebDAV 客户端, 因此不知道如何执行锁定.
客户端 A 没有锁定文档, 而是执行了 GET 操作, 然后开始编辑.
客户端 B 执行 LOCK 操作,进行了 GET 操作,然后开始编辑.
客户端 B 完成编辑, 执行 PUT 操作,然后执行 UNLOCK 操作.
客户端 A 执行 PUT 操作, 覆盖并导致 B 的所有更改丢失.
以下几个原因导致 WebDAV 协议本身无法防止这种情况发生. 首先, 服务器无法强制所有客户端使用锁, 因为其必须与不理解锁的 HTTP 客户端兼容. 其次, 客户端无法要求服务器支持锁, 因为有非常多中仓库实现, 其中一些依赖于预订 (reservations) 和合并 (mergeing), 而不是锁. 最后, 由于 HTTP 协议是无状态的, 其无法保证 LOCK / GET / PUT / UNLOCK 等操作强制序列化执行.
支持锁的 WebDAV 服务器可以通过要求客户端在修改资源之前先锁定资源, 以减少客户端意外覆盖彼此更改的可能性. 这类服务器可以有效阻止 HTTP 1.0 / 1.1 客户端修改资源.
WebDAV 客户端可以通过在与支持锁的 WebDAV 服务器交互时,
使用 锁定 (lock) -> 检索 (retrieve) -> 写入 (write) -> 解锁 (unlock)
操作序列
(至少在默认情况下) 来获得良好的表现。
HTTP 1.1 客户端可以通过在任何会修改资源的请求中使用 If-Match
标头中的实体标签,
已避免覆盖其他客户端的更改.
信息管理者 (Information managers) 可能会尝试通过在客户端实施程序, 要求客户端修改 WebDAV资源之前先对其进行锁定, 以防止内容被覆盖.
7.3. 写锁与未映射 URL
WebDAV 提供了一种向未映射 URL 发送 LOCK 请求的能力, 以便保留该名称以供使用.
这是一种在创建新资源时避免丢失更新问题的简单方式 (另一种方式是使用 [RFC2616#14.26]
中指定的 If-None-Match
标头). 其另一个好处是立即锁定新资源以供创建者使用.
请注意, 创建集合不存在丢失更新问题, 因为 MKCOL 只能用于创建集合, 而不能用于覆盖现有集合. 在尝试在创建并锁定集合时, 客户端可以尝试通过将 MKCOL 和 LOCK 请求进行管道化处理, 以增加获得锁的可能性 (但由于服务器不会将两个单独的操作转换为一个原子操作, 所以不能保证这种操作会起作用).
对未映射 URL 的锁定成功请求必会[MUST]导致创建一个带有空内容的已锁定(非集合)资源.
随后, 成功的 (有正确锁定令牌的) PUT 请求会提供资源内容.
需要注意的是, LOCK 请求没有机制让客户端提供 Content-Type
或 Content-Language
,
因此服务器将使用默认值或空值,并依赖于随后的 PUT 请求来获得正确的值.
使用 LOCK 创建的资源除了是空的外, 每个方面都与普通资源表现相同.
它与使用空正文的 PUT 请求创建资源(在这种情况下未指定 Content-Type
和
Content-Language
), 后在同一资源上使用 LOCK 请求这种行为相同.
根据此模型, 一个已被锁定的空资源:
- 可以被读取, 删除, 移动和复制, 并且在所有方面都表现都与普通非集合资源一致.
- 显示为其父集合的成员.
- 不应[SHOULD_NOT]在其锁定解除后消失 (因此客户端必须如同在任何其他操作或非空资源上一样, 要负责清理自己的烂摊子).
- 可能不会[MAY_NOT]有尚未由客户端设置的属性值, 如
DAV:getcontentlanguage
等. - 可以通过 PUT 请求更新 (添加内容).
- 不得[MUST_NOT]转换为集合. 服务器必须[MUST]拒绝 MKCOL 请求 (就像拒绝对任何现有非集合资源的 MKCOL 请求一样).
- 必须[MUST]为
DAV:lockdiscovery
和DAV:supportedlock
属性定义值. - 响应必须使用
"201 Created"
响应码指示已创建的资源(对现有资源的 LOCK 请求将导致返回"200 OK"
). 响应正文就像对现有资源的 LOCK 请求一样, 必须包含DAV:lockdiscovery
属性.
客户端会在锁定空资源后不久使用 PUT 和可能的 PROPPATCH 更新该资源, 这种行为是可预料的.
作为替代方案, 且为了与 [RFC2518] 的向后兼容性, 服务器可以选择实现 Lock-Null Resources (LNRs) (参见附录 D 中的定义). 客户端可以非常容易支持与 "LNRs" 旧模型和 "锁定空资源" 推荐模型服务器之间的互操作, 只需在 LOCK 后尝试对未映射 URL (而不是 MKCOL 或 GET) 进行 PUT, 同时不依赖于 LNRs 中的特定属性.
7.4. 写锁与集合
有两种类型的集合写锁. 一种为集合上深度为 0
的写锁, 该锁保护对应集合的属性以及其内部成员 URL,
但不保护成员资源的内容或属性(如果集合本身有任意正文, 则也会受到保护).
另一种为集合上的 深度无限
的写锁, 该锁不仅提供与对应集合相同的保护, 还为对每个成员资源提供写锁保护.
换句话说, 无论哪种类型的写锁都会保护:
因此,集合写锁可以保护以下所有操作:
- 删除 (DELETE)集合的直接内部成员
- 将内部成员移出 (MOVE out)集合
- 将内部成员移入 (MOVE into)集合
- 使用 移动 (MOVE) 操作在集合内重命名内部成员
- 将内部成员复制 (COPY) 到集合
- 发起 PUT 或 MKCOL 请求以创建新的内部成员
在需要单独锁定内部成员的情况下, 除了内部成员本身的锁令牌外, 还需要该集合的锁令牌.
此外, 深度无限锁
将影响对已锁定集合的所有成员执行的所有写操作.
使用 深度无限锁
进行锁定时,锁根标识的资源会被直接锁定, 其所有成员会被间接锁定.
- 任何被
深度无限锁
锁定集合的后代, 新添加的资源会变成间接锁定状态. - 任何间接锁定的资源如果被移出已锁定的集合并放入未锁定的集合, 将变成未锁定状态.
- 任何间接锁定的资源如果被移出已锁定的源集合并放入被
深度无限锁
锁定的目标集合, 将会保持间接锁定状态, 但此时由目标集合锁保护 (之后如果进行进一步更改需要目标集合的锁令牌).
如果向集合发出无限深度写入 LOCK 请求时, 该集合包含标识当前以与新锁冲突的方式锁资源的成员 URL
(参见第 6.1 章中第三点), 请求必须[MUST]以 423 (Locked) 状态码失败,
且响应应该[SHOULD]包含 no-conflicting-lock
前置条件.
如果锁定请求将导致资源的 URL 成为被 深度无限
锁定集合的内部成员 URL,
则新资源必须自动由该锁保护. 例如, 如果集合/a/b/
写锁定, 且资源/c
被移动到 /a/b/c
,
则资源/a/b/c
将被添加到写锁定中.
7.5. 写锁与 IF 请求标头
用户代理在对已锁定资源请求操作时, 必须表明其对锁有所了解. 否则可能会出现以下情况:
考虑如下场景, 用户A
运行的 程序A
在某个资源上获取了写锁定.
另外一个同样由 用户A
运行的 程序B
不知道 程序A
已经获取锁, 并对已锁资源执行了 PUT 请求.
在这种情况下, PUT 请求成功, 因为锁与主体关联而不是程序. 因此, 程序B
因为使用了 用户A
的凭证,
所以被允许执行 PUT 请求. 然而, 如果 程序B
了解该锁定的情况, 就不会覆盖资源,
而会更偏于向用户呈现一个描述冲突的对话框. 由于这种情况,
需要一种机制来防止不同的程序在具有相同授权的情况下意外忽略其他程序获取的锁.
为了防止这些冲突, 授权的主体必须[MUST]为所有已锁定的资源提交一个锁令牌,
以提供给可能更改的资源的方法, 否则该方法必须[MUST]失败.
锁令牌随 If
标头被提交. 例如, 如果要移动资源并且源和目标都已被锁定,
则必须[MUST]在 If
标头中提交两个锁令牌, 一个用于源, 另一个用于目标.
7.5.1. 示例 - 写锁与 COPY
>>Request
COPY /~fielding/index.html HTTP/1.1
Host: www.example.com
Destination: http://www.example.com/users/f/fielding/index.html
If: <http://www.example.com/users/f/fielding/index.html>
(<urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6>)
>>Response
HTTP/1.1 204 No Content
在该示例中, 尽管源和目标都已被锁定, 但只需要提交一个锁令牌(用于目标上的那个锁). 这是因为源资源不会被 COPY 修改, 因此也不受写锁的影响. 在该示例中, 用户代理身份验证已经先通过 HTTP 协议范围之外的机制在传输层中进行.
7.5.2. 示例 - 删除锁集合的成员
考虑一个带有 深度无限
的互斥写锁集合"/locked", 并尝试删除其内部成员 "/locked/member":
>>Request
DELETE /locked/member HTTP/1.1
Host: example.com
>>Response
HTTP/1.1 423 Locked
Content-Type: application/xml; charset="utf-8"
Content-Length: xxxx
<?xml version="1.0" encoding="utf-8" ?>
<D:error xmlns:D="DAV:">
<D:lock-token-submitted>
<D:href>/locked/</D:href>
</D:lock-token-submitted>
</D:error>
以上, 客户端需要在请求中提交锁令牌使请求成功.
为此, 可以使用各种形式的 If
标头(参见第 10.4 章).
"No-Tag-List" format:
If: (<urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf>)
"Tagged-List" format, for "http://example.com/locked/":
If: <http://example.com/locked/>
(<urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf>)
"Tagged-List" format, for "http://example.com/locked/member":
If: <http://example.com/locked/member>
(<urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf>)
需要注意的是, 为了提交锁令牌, 实际的形式并不重要; 重要的是锁令牌出现在 If标头中, 并且该 If标头本身评估为真 (true).
7.6. 写锁与 COPY/MOVE
对于 COPY方法的调用, 不得[MUST_NOT]复制源上任何活动的写锁. 但如前所述,
如果 COPY 将资源复制到一个使用 深度无限
锁定的集合中, 则资源将被添加到该锁中.
对于写锁定的资源, 一个成功的 MOVE 请求不得[MUST_NOT]将写锁与资源一起移动.
但是, 如果目标处存在锁, 则服务器必须[MUST]将移动的资源添加到目标锁范围内. 例如,
如果 MOVE 操作使资源成为具有 深度无限
锁定的集合的子级, 则资源将被添加到该集合锁中.
此外, 如果具有 深度无限
锁定的资源被移动到同一锁范围内的目标
(例如, 在锁涵盖的 URL命名空间树内), 则被移动的资源将被再次添加到锁定中.
以上两个示例必须提交一个包含源和目标的锁令牌的 If
标头, 正如第 7.5 章中规定一般.
7.7. 刷新写锁
客户端不得[MUST_NOT]重复提交相同的写锁请求. 需要注意的是,
客户端始终知道其正在重新提交相同的锁请求, 因为为了对已锁定的资源进行请求,
其必须在 If
标头中包含锁令牌.
但是, 客户端可能提交一个带有 If标头但没有正文的 LOCK 请求. 接收到没有正文LOCK 请求的服务器不得[MUST_NOT]创建新的锁 -- 这种形式的 LOCK 请求仅用于 "刷新" 现有锁 (至少意味着必须重置与锁关联的任何定时器).
客户端可能通过在锁刷新请求来提交包含任意值的 Timeout
标头.
服务器始终可以选择忽略客户端提交的 Timeout
标头,
并且服务器可以[MAY]使用与之前用于锁超时不同的时间来刷新锁,
只需要在 LOCK 刷新响应中像客户端通告新的值即可.
如果在刷新 LOCK 请求的响应中收到错误, 则客户端不得[MUST_NOT]假设锁已被刷新.