我最近一直在研究一个新项目,它需要在并发访问之间保持数据强一致性。为了更好地理解这个问题,可以考虑以下应用场景:
- 如何始终保持您的银行帐户余额正确
- 如何正确处理货架上的产品数量
- 如何正确处理视频或文章的喜欢或观看次数
你应该想知道管理这些问题的复杂性是什么。为此,让我向您介绍更新丢失的问题。
这个问题在计算机科学领域是众所周知的,就像名称所说的那样,由于并发访问而丢失了更新。如您所见,库存中可用物品的最终数量为5,其实应为2。
要解决这个问题,我们需要考虑锁,锁的定义为
“一种机制,可以同时将多个用户同时访问同一条数据”。
我们可以通过数据库锁定轻松解决这个问题,但是当我们谈论分布式系统时,它会变得更加棘手。
数据库锁定
关于隔离级别的说明:隔离确定了其他用户和系统对事务完整性的可见性。但是必须了解这还不能解决本文中描述的问题,这点是非常重要的。
数据库默认值一般为Read Committed,Read Commited无法防止脏读,而Repeatable Read无法防止幻影读(Phantom Reeds),这些可能会使跟踪问题变得更加困难,并会引导您进入一个全新的不一致世界,当然串行化是最严格防止丢失更新发生,但是性能更低。
即使您允许脏读或幻影读,也会有一个时刻,您访问的数据也不会在并发事务之间同步。
悲观和乐观的锁
解决丢失更新问题的最简单方法就是在实际更新之前锁定要更新的行(悲观锁定)。大多数数据库支持以下语句:
SELECT * FROM products WHERE id = 1 FOR UPDATE; |
这意味着其他会话可以读取该行,但在您的事务提交之前无法修改它。因此,我们可以通过...排除并发访问来轻松解决并发访问!数据库可以解决大小公司的大多数问题,并且可以在大量的事务中工作,但有时这可能会导致性能问题。
乐观锁人为冲突是可能的,但是非常罕见的。允许快速性能和高并发性,代价是:偶尔拒绝这些写入:最初接受但在最后一秒发现与另一个用户的更改冲突的数据。
我们知道,前面UPDATE语句已经锁定了写入行,因此它一次只更新一行。这样我们可以在更新时添加旧值的检查:
UPDATE products SET units=5 WHERE units=10; |
其他并发事务将不满足该WHERE子句,因此只能更新零行(不更新)。如果发生这种情况,您需要进行回滚并通知用户UPDATE失败,或者您可以再次执行整个事务。
分布式系统
让我向您介绍一些新问题:
- 我们无法使用本机支持锁定或事务的数据库
- 我们正在使用的是需要锁定对共享资源的访问权限的分布式系统
首先,让我提醒您分布式对象的第一定律:不要分发您的对象(感谢Martin Fowler)。如果您可以避免进入分布式系统,那么就这样做。这将需要解决比上面列出的问题更多的问题,例如:故障处理(分布式回滚),数据一致性和复制,容错,弹性,限制/速率限制,重试等等......
外部锁定
如果你只能进入分布式系统,那么你就需要外部锁定。以下是一些可以帮助您完成这一新冒险的文章和工具:
- Apache Zookeeper,像软件的Apache的Hadoop(数据的分布式处理),Apache Kafka(分布式流媒体平台)和Apache Mesos(分布式系统内核)使用动物园管理员为分布式协调和同步。
- 使用Redis的分布式锁,Redis是一个内存数据库,具有许多现成的数据结构和算法。本文介绍如何使用称为Redlock的算法来实现DLM(分布式锁管理器)。您还可以使用带有SETNX命令的单节点Redis (不鼓励使用第一个)。
- 如何使用MySQL的GET_LOCK协调分布式工作。高性能MySQL(由O'Reilly Media出版)的合着者Baron Schwartz撰写的一篇非常好的文章,讨论了一个名为GET_LOCK的MySQL函数,它能够锁定用户定义的字符串。当然你不应该使用它来锁定行(MySQL已经做得很好)但可能锁定外部资源。
- 如何进行分布式锁定,这是由设计数据密集型应用程序(也由O'Reilly Media出版)的作者Martin Kleppmann撰写的一篇非常好的文章,讨论了锁定的有用性,关于Redlock算法和防护令牌的一些注意事项。
分布式数据存储
CAP定理:如上所述,有几种方法可以进行锁定并避免不一致。RDBMS已经被认为是这项工作的好工具,它们就是所谓的CP系统,即它们提供了一个具有C onsistency和Partition的环境。
CAP定理指出,在网络分区的存在,只能在一致性和可用性之间进行选择。
分布式数据存储更侧重于提供可靠性而非一致性,并且具有一些可以帮助我们以不同方式处理并发访问的功能。
冲突解决和最终的一致性
性能和可用性需要付出代价。您可以在群集的同一资源和不同节点中进行多个更新。当发生这种情况时,你最终会得到两个不同版本的数据(也称为兄弟姐妹),它们可以(或不能)最终收敛。
解决冲突的一些方法是:
最后两项是根据逻辑时间而不是按时间顺序跟踪对象更新的机制(与时间戳一样),从而实现更好的冲突解决。但如果冲突无法解决,则应用程序有责任处理冲突。
CRDT可以很容易地解决问题。它们是可以在没有协调器的情况下独立和同时更新的数据结构(没有事务协调器就没有单点风险,否则整个分布式事务可靠性依赖协调器中间件的可靠性了),并且在数学上总是可以解决可能导致的不一致问题。Riak数据类型就是一个很好的例子。
这里的要点是:
您可以设计系统以了解最终的一致性和冲突解决方案。无需显式锁定即可轻松建模和解决某些问题。
结论
关系数据库是一种长期存在的技术,是处理所有这些问题的非常准确的方法。另一方面,可能具有更高的成本,如果追求更高的性能,可用性等是分布式系统和分布式数据存储的优势,他们只是以非常不同的方式处理问题。