使用 ETag 避免编辑冲突

什么是编辑冲突 ?

比如我们很多人在同一时间编辑同一页上的wiki百科, 当我们点击保存的时候, 如果这时候其他人和你一样, 编辑了同一份源文件, 但是比你提前保存了, 也就是你正在编辑的这份文件时旧的. 因此, 你在点击保存的时候, 修改的内容就会与最新的内容发生冲突.


这种编辑冲突也称为 “空中碰撞”


我们首先来了解几个头字段和HTTP方法.


几个头字段:

ETag:

// eg:
ETag: “33a64df551425fcc55e4d42a148795d9f25f89d4"

资源的特定版本的标识符.

这里我们要明确一个概念, ETag 的使用范围只是一个URL, 如果给定URL中的资源更改了, 则一定生成新的ETag值.

两个不同的URL可能会有相同的 ETag, 但是没关系.


If-Match:

// eg:
If-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

使用了这个头字段以后, 表示这是一个条件请求, 表示在请求服务器的时候, 带上这个值, 只有在服务器的资源与 If-Match中指定的 ETag 匹配的时候, 才允许操作.

不匹配的话, 服务器返回 412 Precondition Failed


If-None-Match:

// eg:
If-None-Match: *
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

这也是一个条件请求, 表示当服务器上没有资源与这个 ETag 匹配的时候, 服务器才会返回所请求的资源.

如果服务器上有资源与这个 ETag 匹配, 服务器返回 304 Not Modified.

星号 * 是一个特殊值, 代表任何资源. 用于资源上传 PUT 请求的时候. 用来生成事先并不知道是否存在的文件, 可以确保先前并没有进行过类似的上传操作, 防止之前操作数据的丢失.

也就是说, 对于这份资源, 我想匹配任何关于这份URL资源的版本, 如果匹配上了, 证明这份资源是存在的, 返回 412 Precondition Failed.



几个 HTTP 方法:


HEAD:

与GET方法一样, 也是向服务器请求资源.

不过服务器不会传回实体主体部分, 只有一些关于资源的头字段.

PUT:

向服务器上的指定资源提交数据.

数据在请求的实体主体部分, 这个请求可能会创建新的资源, 或者修改已有的资源.


ok, 进入正题...


这种编辑冲突会分为两种情况:

1 这份文档是我们新建的, 我们想进行保存操作.

( 也就是我们不知道这份文档是否存在, 可能有另一个家伙比我们提前新建了一样名字的文档并进行了保存, 于是服务器上的是那家伙已经新建好的文档 )

2 这份文档是在已有文档上进行编辑的, 我们想进行保存操作.

( 也就是我们知道这份文档已经存在了, 可能有另一个家伙也在编辑这份文档, 并且那家伙比我们提前进行了save, 于是服务器上的文件比我们手头的进行编辑那份原来的文件要新一点 )

我们的save操作会触发一个PUT请求用于实际更新或者创建资源.

接下来会结合实际例子来解释, 例子来源是 w3.org.
假设我们本地ip是: `18.29.0.116.1270`, 服务器ip是: `18.29.0.24.80`


对于第一种情况:

不知道这个文档已经存在, 我们想要创建这份文档.

对于这种情况, 在实际的 PUT 请求之前, 会先发起一个HEAD请求, 用于进行确认.

在第一种情况下, 又会分两种小情况:

小情况1: 这份文档的确没存在, 没有其他家伙比我们更早的新建这份文档.

18.29.0.116.1270 -> 18.29.0.24.80 over TCP
       HEAD /frystyk/test/test.html HTTP/1.1.
       Accept: */*;q=0.3.
       Accept-Encoding: deflate.
       TE: trailers,deflate.
       Host: jigedit.w3.org.
       User-Agent: WebCommander/1.1 libwww/5.2.1.
       Connection: TE,Keep-Alive.
       .

这里我们先发起了一个HEAD请求去确认这个资源是否存在.


18.29.0.24.80 -> 18.29.0.116.1270 over TCP
       <No data>
-----------------------------------------------------------------
18.29.0.24.80 -> 18.29.0.116.1270 over TCP
       HTTP/1.1 404 Not Found.
       Date: Wed, 25 Nov 1998 21:22:09 GMT.
       Content-Length: 148.
       Content-Type: text/html.
       Server: Jigsaw/2.0beta1.
       . 

服务器返回404, 说这个资源不存在, 你可以放心地进行 PUT.


18.29.0.116.1270 -> 18.29.0.24.80 over TCP
       PUT /frystyk/test/test.html HTTP/1.1.
       Accept: */*;q=0.3.
       Accept-Encoding: deflate.
       TE: trailers,deflate.
       Expect: 100-continue.
       Host: jigedit.w3.org.
       If-None-Match: *.
       User-Agent: WebCommander/1.1 libwww/5.2.1.
       Connection: TE.
       Date: Wed, 25 Nov 1998 21:22:09 GMT.
       Content-Length: 104.
       Content-Type: text/html.
       Last-Modified: Fri, 04 Sep 1998 12:50:18 GMT.
       .
-----------------------------------------------------------------
18.29.0.24.80 -> 18.29.0.116.1270 over TCP
       <No data>
-----------------------------------------------------------------
18.29.0.24.80 -> 18.29.0.116.1270 over TCP
       HTTP/1.1 100 Continue.
       Date: Fri, 13 Nov 1998 14:18:30 GMT.
       Server: Jigsaw/2.0beta1.
       .

-----------------------------------------------------------------
18.29.0.116.1270 -> 18.29.0.24.80 over TCP
       <No data>
-----------------------------------------------------------------
18.29.0.116.1270 -> 18.29.0.24.80 over TCP
       <HTML>
       <HEAD>
         <TITLE>this is a testfile</TITLE>
       </HEAD>
       <BODY>
       <P>
       this is a test file
       </BODY></HTML>

-----------------------------------------------------------------
18.29.0.24.80 -> 18.29.0.116.1270 over TCP
       <No data>

于是, 我们在 PUT 请求里带上我们的资源, 向服务器发起请求.

注意这里的 PUT 请求带上了一个 头字段 If-None-Match: *, 解释见前文.


18.29.0.24.80 -> 18.29.0.116.1270 over TCP
       HTTP/1.1 201 Created.
       Date: Wed, 25 Nov 1998 21:22:11 GMT.
       Transfer-Encoding: deflate,chunked.
       Content-Type: text/html.
       Etag: "jrbkiq:qhcii3ug".
       Location: http://jigedit.w3.org/frystyk/test/test.html.
       Server: Jigsaw/2.0beta1.
       .
       2A.
       x..).s.+.,.TH.O.T(N,KMQ(.MNN-N+...TP.....|.

完成 PUT 请求, 服务器返回 201 Created.


小情况2: 这份文档在我们进行新建编写内容的时候, 有个家伙比我们提前新建了一样名字的文档并进行了保存, 我们此时不知道这份文档是已经存在的.

在这种小情况下, 用户可以选择下载已经存在的版本, 或者覆盖已经存在的版本.

18.29.0.116.1311 -> 18.29.0.24.80 over TCP
       HEAD /frystyk/test/test.html HTTP/1.1.
       Accept: */*;q=0.3.
       Accept-Encoding: deflate.
       TE: trailers,deflate.
       Host: jigedit.w3.org.
       User-Agent: WebCommander/1.1 libwww/5.2.1.
       Connection: TE,Keep-Alive.
       .

同样的, 我们点了save以后发起一个 HEAD 请求进行确认.


18.29.0.24.80 -> 18.29.0.116.1311 over TCP
       HTTP/1.1 200 OK.
       Date: Wed, 25 Nov 1998 22:00:22 GMT.
       Content-Length: 104.
       Content-Type: text/html.
       Etag: "jrbkiq:qhcjujp0".
       Last-Modified: Wed, 25 Nov 1998 21:46:29 GMT.
       Server: Jigsaw/2.0beta1.
       .

ヾ(。`Д´。)我艹, 返回了 200, 表示这份文档已经存在了, ( 哪个家伙比我提前新建了这份文档 !! )

同时我们发现这里带上了 ETag 头字段, 用于标识这个资源的版本.


18.29.0.116.1311 -> 18.29.0.24.80 over TCP
       PUT /frystyk/test/test.html HTTP/1.1.
       Accept: */*;q=0.3.
       Accept-Encoding: deflate.
       TE: trailers,deflate.
       Expect: 100-continue.
       Host: jigedit.w3.org.
If-Match: "jrbkiq:qhcjujp0".
       User-Agent: WebCommander/1.1 libwww/5.2.1.
       Connection: TE.
       Date: Wed, 25 Nov 1998 22:00:25 GMT.
       Content-Length: 104.
       Content-Type: text/html.
       Last-Modified: Fri, 04 Sep 1998 12:50:18 GMT.
       .

这里我们决定更新那家伙新建的文档, 我们将 If-Match 设置为之前 HEAD 方法返回的 ETag, 表示, 我们想修改这个资源的这个 ETag 版本, 如果匹配上了, 我们的修改就会成功.


18.29.0.24.80 -> 18.29.0.116.1311 over TCP
       HTTP/1.1 100 Continue.
       Date: Fri, 13 Nov 1998 14:18:30 GMT.
       Server: Jigsaw/2.0beta1.
       .

服务器说: ok, 你继续发你的内容给我们吧.


18.29.0.116.1311 -> 18.29.0.24.80 over TCP
       <HTML>
       <HEAD>
         <TITLE>this is a testfile</TITLE>
       </HEAD>
       <BODY>
       <P>
       this is a test file
       </BODY></HTML>

我们发送 PUT 内容过去. ( 上面三段内容, 应该是一次 PUT 请求内的 client-server 交流 )


18.29.0.24.80 -> 18.29.0.116.1311 over TCP
       HTTP/1.1 204 No Content.
       Date: Wed, 25 Nov 1998 22:00:28 GMT.
       Content-Length: 104.
       Content-Type: text/html.
Etag: "jrbkiq:qhcko64g".
       Last-Modified: Wed, 25 Nov 1998 22:00:28 GMT.
       Server: Jigsaw/2.0beta1.
       .

服务器返回 204 No Content, 说明访问的是同一份资源.

返回带有新的 Etag, 说明这份文档资源已经被更新了.



接下来, 我们讨论第二种情况, 我们在原文档上进行编辑然后想save我们的新更改.

这里同样也分了两种小情况:

小情况1: 没有其他家伙和我们在同一时间编辑保存同一份资源, 我们可以放心大胆进行save.

18.29.0.116.1321 -> 18.29.0.24.80 over TCP
       PUT /frystyk/test/test.html HTTP/1.1.
       Accept: */*;q=0.3.
       Accept-Encoding: deflate.
       TE: trailers,deflate.
       Expect: 100-continue.
       Host: jigedit.w3.org.
       If-Match: "jrbkiq:qhcliqlo".
       User-Agent: WebCommander/1.1 libwww/5.2.1.
       Connection: TE.
       Date: Wed, 25 Nov 1998 22:15:02 GMT.
       Content-Length: 104.
       Content-Type: text/html.
       Last-Modified: Fri, 04 Sep 1998 12:50:18 GMT.
       .

-----------------------------------------------------------------
18.29.0.24.80 -> 18.29.0.116.1321 over TCP
       <No data>
-----------------------------------------------------------------
18.29.0.24.80 -> 18.29.0.116.1321 over TCP
       HTTP/1.1 100 Continue.
       Date: Fri, 13 Nov 1998 14:18:30 GMT.
       Server: Jigsaw/2.0beta1.
       .

-----------------------------------------------------------------
18.29.0.116.1321 -> 18.29.0.24.80 over TCP
       <No data>
-----------------------------------------------------------------
18.29.0.116.1321 -> 18.29.0.24.80 over TCP
       <HTML>
       <HEAD>
         <TITLE>this is a testfile</TITLE>
       </HEAD>
       <BODY>
       <P>
       this is a test file
       </BODY></HTML>

因为我们知道文档已经存在, 所以不用先发一个 HEAD 请求去探测一下文档是否存在. 因为 HEAD 肯定会返回 200.

这里我们带上了 If-Match 头字段, 说明我们想修改的资源的版本标识是 "jrbkiq:qhcliqlo".


18.29.0.24.80 -> 18.29.0.116.1321 over TCP
       HTTP/1.1 204 No Content.
       Date: Wed, 25 Nov 1998 22:15:05 GMT.
       Content-Length: 104.
       Content-Type: text/html.
       Etag: "jrbkiq:qhcliuio".
       Last-Modified: Wed, 25 Nov 1998 22:15:05 GMT.
       Server: Jigsaw/2.0beta1.
       .

成功修改资源, 因为是同一份资源, 返回 204.

修改了资源, 产生了新的 Etag 返回.



接下来我们讨论小情况2: 我们在编辑这份文档的同时, 有另一个家伙也在修改这份文档, 并且他早早地点了保存, 我们之后才进行保存.

18.29.0.116.1297 -> 18.29.0.24.80 over TCP
       PUT /frystyk/test/test.html HTTP/1.1.
       Accept: */*;q=0.3.
       Accept-Encoding: deflate.
       TE: trailers,deflate.
       Expect: 100-continue.
       Host: jigedit.w3.org.
If-Match: "jrbkiq:qhcii3ug".
       User-Agent: WebCommander/1.1 libwww/5.2.1.
       Connection: TE,Keep-Alive.
       Date: Wed, 25 Nov 1998 21:46:19 GMT.
       Content-Length: 104.
       Content-Type: text/html.
       Last-Modified: Fri, 04 Sep 1998 12:50:18 GMT.
       .

这种情况下我们知道文档已经存在, 不用发 HEAD 请求.

同时我们带上了 If-Match, 代表我们想修改的资源的版本.


18.29.0.24.80 -> 18.29.0.116.1297 over TCP
       HTTP/1.1 412 Precondition Failed.
       Date: Wed, 25 Nov 1998 21:46:23 GMT.
       Transfer-Encoding: deflate,chunked.
       Content-Type: text/html.
       Server: Jigsaw/2.0beta1.
       .
       1D.
       x..(J.M..K.,...SHK..IM...W....

返回 412, 说明我们的 If-Match 与服务器上的资源版本不匹配.

说明已经有人更新了服务器上的相同资源.


18.29.0.116.1297 -> 18.29.0.24.80 over TCP
       PUT /frystyk/test/test.html HTTP/1.1.
       Accept: */*;q=0.3.
       Accept-Encoding: deflate.
       TE: trailers,deflate.
       Expect: 100-continue.
       Host: jigedit.w3.org.
If-None-Match: "jrbkiq:qhcii3ug".
       User-Agent: WebCommander/1.1 libwww/5.2.1.
       Connection: TE.
       Date: Wed, 25 Nov 1998 21:46:26 GMT.
       Content-Length: 104.
       Content-Type: text/html.
       Last-Modified: Fri, 04 Sep 1998 12:50:18 GMT.
       .

-----------------------------------------------------------------
18.29.0.24.80 -> 18.29.0.116.1297 over TCP
       <No data>
-----------------------------------------------------------------
18.29.0.24.80 -> 18.29.0.116.1297 over TCP
       HTTP/1.1 100 Continue.
       Date: Fri, 13 Nov 1998 14:18:30 GMT.
       Server: Jigsaw/2.0beta1.
       .

-----------------------------------------------------------------
18.29.0.116.1297 -> 18.29.0.24.80 over TCP
       <No data>
-----------------------------------------------------------------
18.29.0.116.1297 -> 18.29.0.24.80 over TCP
       <HTML>
       <HEAD>
         <TITLE>this is a testfile</TITLE>
       </HEAD>
       <BODY>
       <P>
       this is a test file
       </BODY></HTML>

不管了, 我们要覆盖那家伙的修改 !

我们在 If-None-Match 带上我们想修改的版本, 也就是那个之前我们再 If-Match 里面想匹配的那个版本, 也就是我们想修改的那个文件的原来的版本, 也就是那家伙还没修改保存之前的和我们一样的版本...


18.29.0.24.80 -> 18.29.0.116.1297 over TCP
       HTTP/1.1 204 No Content.
       Date: Wed, 25 Nov 1998 21:46:29 GMT.
       Content-Length: 104.
       Content-Type: text/html.
       Etag: "jrbkiq:qhcjujp0".
       Last-Modified: Wed, 25 Nov 1998 21:46:29 GMT.
       Server: Jigsaw/2.0beta1.
       .

返回 204, 成功修改. 并带上了资源新版本的标识, 也就是新的 Etag.

对于 PUT 方法来说, 当且仅当没有已存在的资源的 Etag 与我们 If-None-Match 里面指定的值相匹配时, 服务器对请求会进行相应的处理. ( 来自 MDN 文档 )

所以这里服务器为我们的 PUT 方法进行了相应处理, 把服务器上的这份资源进行了更新.





最后, 我们说一下如果冲突发生了会怎样.


有两种情况会触发冲突:

1 HEAD 请求的时候, 发现文档已经存在了

2 PUT 请求的时候, 发现 If-Match 头字段与服务器的资源版本不匹配

如果发生了冲突, 我们有两种选择:

1 下载服务器上资源的最新版本, 采用一些merge算法, 再次发送 PUT 请求.

2 覆盖服务器上已有的资源

如果我们想覆盖服务器上的版本, 就会发起第二次请求, PUT 请求. 两种情况:

1 如果我们知道文档已经存在, 新的 PUT 请求的 If-None-Match 头字段与 第一次 PUT 请求中的 If-Match 字段的值一样. 说明了我们想覆盖的资源版本.

2 如果我们不知道文档已经存在, 新的 PUT 请求的 If-Match 头字段与第一次 HEAD 请求返回 200 带的 Etag 字段值一样. 说明我们想更新这个被他人新建的资源




参考资料:

w3.org / Editing

HTTP头字段

超文本传输协议 请求方法

HTTP ETag

HTTP状态码

ETag

If-Match

If-None-Match

编辑于 2018-04-16

文章被以下专栏收录