# 7. 写入锁(Write Lock)

本章节描述写入锁类型的语义细节。写入锁是锁类型的一个特定实例,并且是本规范中描述的唯一的锁类型。

排他写入锁会阻止以下情形来保护一个资源:

  • 来自锁创建者之外的任何主体试图进行更改
  • 在任何情况下试图进行更改却没有提交锁令牌(例如,没有提交锁的客户端进程)。

客户端必须在涉及修改的请求中提交一个允许修改(已经被写入锁定的)资源的锁令牌。写入锁涵盖的修改包括:

  • 对任何已被写入锁定的资源所进行的以下任何方面的更改:

    • 任何变体
    • 任何死属性
    • 任何可锁定的活属性(除非另有定义,否则活属性默认是可锁定的)
  • 对于集合来说,其内部成员URI的任何修改。如下情况被视作一个集合的内部成员URI被修改:

    • 被添加
    • 被删除
    • 被识别为另外一个资源(即映射被改变),关于写入锁和集合的更多讨论见第7.4节。
  • 对写入锁的锁根映射进行修改,要么指向了另一个资源,要么没有了指向(比如映射被删除)

在HTTP和WebDAV定义的methods当中,PUT、POST、PROPPATCH、LOCK、UNLOCK、MOVE、COPY(针对目标资源)、DELETE和MKCOL都会受到写入锁的影响。到目前为止定义的所有其他HTTP/WebDAV methods(特别是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客户端。

其次,由于存储库对WebDAV的实现多种多样,并不是所有的服务器都支持锁定功能,比如有一些就是依赖预占跟合并机制而非锁定功能来保证内容一致性。

最后,WebDAV(包括HTTP)是无状态的,它不能强制客户端必须按照LOCK/GET/PUT/UNLOCK的顺序进行操作。

支持锁定的WebDAV服务器通过要求客户机在修改资源之前锁定资源,可以减少客户机意外覆盖彼此更改的可能性。这样的服务器将有效地阻止HTTP 1.0和HTTP 1.1客户端修改资源。

当WebDAV客户端与支持锁定的WebDAV服务器交互时,WebDAV客户端应当使用LOCK/GET/PUT/UNLOCK这样的操作顺序(至少在默认情况下)。

HTTP 1.1客户端应当通过在Ifheaders中使用实体标记(entity tags)来避免覆盖其他客户端的更改。

信息管理员可以尝试通过执行“请求修改资源前必须先锁定”的客户端流程来防止覆盖。

# 7.3 写入锁和未映射的url

WebDAV提供了向未映射URL发送LOCK请求的能力,以便保留这个URL供使用。这是一种避免创建新资源时丢失更新问题的简单方法(另一种方法是使用在[RFC2616]章节14.26中指定的If-None-Match头)。它对资源创建者来说有一个附带的好处就是可以立即锁定新资源(以确保不会等到提交时才发现锁冲突)。

注意,丢失更新的问题对集合来说不是问题,因为MKCOL只能用于创建集合,而不能覆盖现有的集合。当试图在创建时锁定一个集合资源时,客户端可以尝试通过将MKCOL和LOCK一起进行流水线化请求的方式来增加获得锁的可能性(但这并不会将两个单独的操作变成一个原子操作,所以不能保证这样一定有效)。

对未映射URL的LOCK请求成功后必须创建一个包含空内容的锁定(非集合)资源。随后,一个成功的PUT请求(带有正确的锁令牌)将为资源提供内容。请注意,LOCK请求并没有让客户机提供Content-Type或Content-Language的机制,因此服务器将使用默认值或空值,并依赖于后续的PUT请求获得正确的值。

用LOCK请求创建的资源除了内容是空的之外在其他方面都跟正常资源一模一样。它的行为方式与一个先用空内容(同时没有指定内容类型和内容语言)进行PUT请求来创建资源然后再发出LOCK请求这种方式完全相同。遵循这个模型,一个被锁定的空资源:

  • 可以读取、删除、移动和复制,在所有方面都表现为正常的非集合资源。

  • 作为其父集合的成员出现。

  • 当锁消失时资源本身不应消失(因此客户端必须负责为自己善后,与任何其它操作或其它非空资源一样)。

  • 可能类似DAV:getcontentlanguage这样的属性还没有值,因为客户端还没来得及设定。

  • 可以用PUT请求来更新(添加内容)。

  • 不能转换为集合。服务器必须让MKCOL请求失败(就像对任何一个现有的非集合资源发出MKCOL请求一样)。

  • 必须已经定义了DAV:lockdiscovery和DAV:supportedlock属性的值。

  • 服务器响应必须使用“201 created”响应代码(如果是对现有资源的LOCK请求将返回200 OK)来表明资源已经创建。消息内容主体必须包括DAV:lockdiscovery属性,就像对现有资源的LOCK请求一样。

客户端应该在锁定空资源后短时间内就更新它,使用PUT或许还有PROPPATCH method。

作为一种向后兼容(RFC2518)的选择,服务器也可以实现Lock-Null资源(LNRs),参见附录D的定义。客户端可以很容易地与支持两种模式(LNRs旧模式、空资源锁定的推荐模式)的服务器来互动,只要尝试在LOCK一个未映射的URL之后再发出一个不依赖LNRs特定属性的PUT请求(注意不是MKCOL或者GET)即可区分。

# 7.4 写入锁和集合

有两种类型的集合写入锁。深度为0的写入锁会保护集合属性和该集合的内部成员url映射,但不会保护成员资源的内容或属性(如果集合本身有任何实体内容,它们也受到保护)。深度为depth-infinity(无限深度)的写入锁在除了给该集合提供上面相同的保护之外,还为每个成员资源提供了写入锁保护。

这两种类型的写入锁都会阻止任何在被写入锁定的集合中创建新资源的请求、任意在被写入锁定的集合中删除内部成员URL的请求以及任何更改内部成员的路径分段名称的请求。

因此,集合写入锁会保护以下所有操作:

  • DELETE一个集合的直接内部成员,
  • MOVE一个内部成员到集合之外,
  • MOVE一个新的内部成员到集合中,
  • MOVE来实现重命名集合中的内部成员,
  • COPY一个内部成员到一个集合中,
  • 能够创建新内部成员的PUT或MKCOL请求。

如果一个被锁定集合的某个内部成员同时被独立锁定,那么(修改这个成员时)除了内部成员本身的锁令牌之外,还需要它所属集合的锁令牌。

此外,无限深度的锁会影响到被锁定集合的所有后代成员的所有写操作。对于无限深度锁,由锁根所标识的资源是直接锁定的,它的所有后代成员是间接锁定的。

  • 任何作为被无限深度锁定集合的后代被添加进来的新资源都将被间接锁定。
  • 任何被间接锁定的资源从被锁定的集合移动到一个未锁定的集合之后都会被解锁。
  • 任何被间接锁定的资源从被锁定源集合转移到另一个被无限深度锁定的目标集合后仍然是间接锁定的,但保护它的间接锁变成了目标集合上的锁(也就是说今后做其它更改时需要的是目标集合的锁令牌)。

如果一个无限深度的写入锁请求被指定给一个集合,而这个集合包含的成员URL所标识资源的当前锁定方式与新的锁相冲突(参见6.1节,点3),该请求必须失败并返回423状态码(已被锁定),并且响应中应该包含一个“no-conflicting-lock”的procondition error。

如果一个请求导致某资源的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事先知道这个锁,它就不会覆盖这个资源,而是倾向于向用户显示一个描述冲突的对话框。由于这种情况的存在,就需要有一种机制来防止不同程序不小心忽略掉具有相同授权的其他程序取出的锁。

为了防止这些冲突,授权主体必须为所有可能更改资源的method提交锁令牌,否则这个method必须失败。把锁令牌放入请求的If header,它就会被提交。例如,如果要移动一个资源,并且源和目标都被锁定,那么必须在if header中提交两个锁令牌,一个用于源,另一个用于目标。

# 7.5.1 实例 - 写入锁和COPY

请求内容:

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>)
1
2
3
4
5

返回内容:

HTTP/1.1 204 No Content
1

在本例中,即使源和目标都被锁定,也只需要提交一个锁令牌(目标上的锁)。这是因为源资源没有被COPY修改,因此不受写锁的影响。在本例中,用户身份验证已经通过其它机制完成。

# 7.5.2 实例 - 从一个锁定集合中删除成员

假定一个集合/locked有一个排他的无限层级的写入锁,然后我们尝试删除它的一个内部成员/locked/member

请求内容:

DELETE /locked/member HTTP/1.1
Host: example.com
1
2

返回内容:

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>
1
2
3
4
5
6
7
8
9

显然,这里需要客户端将锁令牌一起提交请求才能成功。为此,可以使用各种形式的If头文件(见10.4节)。

“No-Tag-List”格式:

If: (<urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf>)
1

"Tagged-List" 格式, for "http://example.com/locked/":

If: <http://example.com/locked/>
 (<urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf>)
1
2

"Tagged-List" 格式, for "http://example.com/locked/member":

If: <http://example.com/locked/member>
 (<urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf>)
1
2

注意,为了提交锁令牌,实际的表单并不重要,重要的是锁令牌出现在If头中,并且If头提供的内容是有效的。

# 7.6 写入锁和COPY/MOVE

COPY方法的调用不能复制任何资源上的活跃写入锁。但是,如前所述,如果这个COPY方法将资源复制到一个被无限深度锁定的集合中,那么该资源将被添加到这个目标资源的锁中。

一个成功作用在被写入锁定资源上的MOVE请求也不能移动它的写入锁,但是如果目标位置上已经存在一个锁,服务器必须将移动的资源添加到目标位置的锁范围内。例如,MOVE请求使资源成为一个具有无限深度锁的集合的子资源,则该资源将被添加到该集合的锁范围中。

此外,如果一个带有无限深度锁的资源被移动到同一锁的范围内的目标(例如在被该锁涵盖的URL命名空间树之下),那么被移动的资源将再次被添加到该锁中。在这两种情况下,如7.5节所述,必须提交一个If头,其中包含源和目标的锁令牌。

# 7.7 刷新写入锁

客户端不能两次提交相同的写入锁请求。注意,客户端总是可以知道它是否在重新提交相同的锁请求,因为它必须在If header中包含锁令牌,以便对已经锁定的资源发出请求。

然而,客户端可能会提交一个带有If header但没有body内容的LOCK请求。当服务器收到一个没有body内容的LOCK请求时,一定不要创建一个新的锁——这种形式的请求目的只在于“refresh”一个现有的锁(这意味着,任何与锁相关的计时器必须重置)。

客户端可以使用Timeout header来随意设置超时时长。但是服务器可能会忽略客户端提交的Timeout header,并且服务器可能会使用与该锁前一个超时时长不同的值来刷新锁,然后在LOCK refresh请求的返回中予以告知。

如果在刷新锁请求时收到错误响应,客户端一定不能假定锁已刷新。