Tagged: 更新丢失

1

Transaction弱隔离之更新的丢失

我们在前文《Transaction弱隔离之读提交的介绍和实现》和《Snapshot的隔离和Repeatable的读》中介绍了read commit和snapshot隔离。这两篇文章其实更多地讨论的是当有写发生的时候如何进行读,除了脏写之外没有更多地讨论如何处理两个写同时发生的情况。其实当有两个写同时发生的时候还会有很多问题需要处理,其中最常见的就是两个写有冲突,这个时候就会发生更新丢失的问题。 我们举个例子,比如一个transaction先从数据库读一个数据,然后修改这个值,再把它写回数据库(读-修改-写的模式)。当有两个transaction同时发生这件事的时候,其中一个transaction的写就会丢失。这种pattern其实在现实中很常见的: 比如说你需要增加一个计数器或者更新账户的余额,需要先读出原来的值,计算出新的值,然后更新。 更新一个复杂值的一个部分,比如说你在JSON文档里面加一个元素,需要首先解析这个文档,做修改,然后写回去。 两个用户 一起写wiki页面,然后每个人都把整个页面发送给了服务器,就会有覆盖的情况发生。 既然它很常见,那么显然就已经有了很多的解决方案。我们来一起看看常见的解决方法: 原子写 很多数据库支持原子写,这样你就不用把数据读出来再进行修改了。它也能保证同时写的安全,比如说下面的语句在很多数据库中其实就是安全的: 常见的数据库比如MongoDB对更新JSON文档中部分数据就提供了原子写。但是并不是所有的操作都可以用原子写来解决,比如上面提到的更新wiki就不太容易做到,但是对于那些可以支持原子写的显然这种方法是一个很好的解决方案。 原子写常见的实现方法有两种,一种就是用一个锁来保证只要有人再读了之后,准备更新的过程中,别的读没法进行,直到更新结束才可以再读新的内容。另外一种方法就是把所有的原子操作放到一个thread中,这样也可以规避同时更新的问题。 显式锁 假如我们上文提到的原子写不能使用,另外一种方法来防止更新丢失就是对要更新的数据使用显式锁。其实和原子写的实现比较类似,这个时候别的同时发生的写就只能等待这个锁了。 下面就是一个显式锁的例子: 这里的FOR UPDATE就是告诉数据库你需要对我查询的所有行加一个锁。这个其实还是蛮有效的,只是说你写代码的时候要仔细点,不要忘了加这个FOR UPDATE。 自动探测更新丢失 我们上面提到的方法主体思想都是想办法让一个transaction先执行,然后再执行另外一个transaction。其实还有一个方法就是让两个transaction同时执行,当我们发现这两个transaction有冲突或者会造成更新丢失的时候再进行处理,比如把其中一个终止了,让他待会重试等等。 这种方法其实相对前面两种来说有其好的方面,比如说开发者不需要时刻记住需要使用原子操作或者加入FOR UPDATE在代码中,所有的这一切数据库都帮你处理了,所以说容错率比较高。 比较再更新 在一些不支持transaction的数据库中,有一种操作称之为“比较再更新”。这个操作的目的就是为了防止更新丢失的。它会限制更新只发生在你之前读的数据没有变化的时候。假如你之前读的数据发生了变化,那么这个更新就会失败,你必须重试。 比如说我们上文提到的wiki更新的例子,要想更新成功,只有在你之前读的数据和更新的时候没有变化才运行执行更新的操作。 当然这个也取决于数据库的实现。比如说你的数据运行在比较的时候content读的是snapshot的值(还记得我们之前的snapshot隔离吗),这个时候这个操作也是不安全的,不能够保证不会更新丢失。 Replication的处理 我们现实中的数据库其实会更复杂一点,比如说我们有replicated的数据库,这样一来,我们就会有多个备份在不同的节点上,尤其是对于多leader或者无leader的数据库,任何一个备份都有可能执行写操作,这个时候如何来防止更新丢失呢? 显然我们之前提到的锁和比较再更新在这个例子下是有问题的,但是我们提到的“自动探测更新丢失”则是一个不错的方案,就是运行每一个replication都做他们自己的写,到时再用一个程序来处理冲突,从而解决更新丢失的问题。 总结 本文介绍了transaction弱隔离之中的更新丢失问题,分析了多种解决这一个问题的方法,各个方法都有其针对的场景也都不完美的解决方案,希望大家读了之后能够有所了解。