learn-tech/专栏/重学操作系统-完/19乐观锁、区块链:除了上锁还有哪些并发控制方法?.md
2024-10-16 11:12:24 +08:00

160 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
19 乐观锁、区块链:除了上锁还有哪些并发控制方法?
这一讲我带来的面试题是:除了上锁还有哪些并发控制方法?
上面这道面试题是在“有哪些并发控制方法?”这个问题的基础上加了一个限制条件。
在我面试候选人的过程中,“上锁”是我听到过回答频次最多的答案,也就是说大多数程序员都可以想到这个并发控制方法。因此,是否能回答出上锁以外的方法,是检验程序员能力的一个分水岭,其实锁以外还有大量优秀的方法。
你掌握的方法越多,那么在解决实际问题的时候,思路就越多。即使你没有做过高并发场景的设计,但是如果脑海中有大量优秀的方法可以使用,那么公司也会考虑培养你,将高并发场景交给你去解决。今天我们就以这道面试题为引,一起探讨下“锁以外的并发控制方法”。
悲观锁/乐观锁
说到并发场景设计系统的目的往往是达到同步Synchronized的状态同步就是大家最终对数据的理解达成了一致。
同步的一种方式,就是让临界区互斥。 这种方式每次只有一个线程可以进入临界区。比如多个人修改一篇文章这意味着必须等一个人编辑完另一个人才能编辑。但是从实际问题出发如果多个人编辑的不是文章的同一部分是可以同时编辑的。因此让临界区互斥的方法对临界区上锁具有强烈的排他性对修改持保守态度我们称为悲观锁Pressimistic Lock
通常意义上,我们说上锁,就是悲观锁,比如说 MySQL 的表锁、行锁、Java 的锁本质是互斥mutex
和悲观锁PressimisticLock持相反意见的是乐观锁Optimistic Lock。你每天都用的基于乐观锁的应用就是版本控制工具 Git。Git 允许大家一起编辑,将结果先存在本地,然后都可以向远程仓库提交,如果没有版本冲突,就可以提交上去。这就是一种典型的乐观锁的场景,或者称为基于版本控制的场景。
Git 的类比
比如现在代码仓库的版本是 100。Bob 和 Alice 把版本 100 拷贝到本地Bob 在本地写到了 106 版本Alice 在本地写到 108 版本。那么如果 Alice 先提交,代码仓库的版本就到了 108。 Bob 再提交的时候,发现版本已经不是 100 了,就需要把最新的代码 fetch 到本地,然后合并冲突,再尝试提交一个更新的版本,比如 110。
这种方式非常类似cas指令的形式就是每次更新的发起方需要明确地知道想从多少版本更新到多少版本。以 Git 为例可以写出cas的伪代码
cas(&version, 100, 108); // 成功
cas(&version, 100, 106); // 失败因为version是108
上面代码第二次cas操作时因为版本变了更新失败这就是一个乐观锁——Alice 和 Bob 可以同时写,先更新的人被采纳,后更新的人负责解决冲突。
购物车的类比
再举个例子比如说要实现一个购物车。用户可能在移动端、PC 端之间切换,比如他用一会手机累了,然后换成用电脑,当他用电脑累了,再换回手机。
在移动端和 PC 端,用户都在操作购物车。 比如在移动端上,用户增加了商品 A然后用户打开 PC 端,增加了商品 B然后用户又换回了移动端想增加商品 C。
这种时候,如果用悲观锁,用户登录移动端后,一种方案就是把 PC 端下线——当然这个方案显然不合理。 合理的方案是给购物车一个版本号,假设是 MySQL 表,那么购物车表中就会多一个版本字段。这样当用户操作购物车的时候,检查一下当前购物车的版本号是不是最新的,如果是最新的,那么就正常操作。如果不是最新的,就提示用户购物车在其他地方已被更新,需要刷新。
去中心化方案:区块链的类比
继续类比,我们可以思考一个更加有趣的方案。在传统的架构中,我们之所以害怕并发,是因为中心化。比如说 DNS 系统,如果全球所有的 DNS 查询都执行一个集群,这个吞吐量是非常恐怖的,因此 DNS 系统用了一个分级缓存的策略。
但是交易数据分布的时候,比如下单、支付、修改库存,如果用分布式处理,就牵扯到分布式锁(分布式事务)。那么,有没有一个去中心化的方案,让业务不需要集中处理呢?比如说双 11 期间你在淘宝上买东西,可不可以直接和商家下单,而不用通过淘宝的中心系统呢?——如果可以,这也就相当于实现了同步,或者说去掉了高并发的同步。
解决最基本的信用问题
考虑购买所有的网购产品,下单不再走中心化的平台。比如阿里、拼多多、 京东、抖音……这些平台用户都不走平台的中心系统下单,而是用户直接和商家签订合同。这个技术现在已经实现了,叫作电子合同。
举例AliceA向苹果店 B 购买了一个 iPhone。那么双方签订电子合同合同内容 C 是:
from=A, to=B, price=10000, signature=alice的签名
from=B, to=A, object=iphone, signature=苹果店的签名
上面两条记录,第 1 条是说 A 同意给 B 转 10000 块钱;第 2 条记录说B 同意给 A 一个 iPhone。如果 A 收了 iPhone 不给 B 打款B 可以拿着这个电子合同去法院告 A。因为用 A 的签名,可以确定是 Alice 签署了这份协议。同理,如果苹果店不给 Alice iPhoneAlice 可以去法院告苹果店,因为 Alice 可以用苹果店的签名证明合同是真的。
解决货币和库存的问题
有了上面的例子最基本的信用问题解决了。接下来你可能会问Alice 怎么证明自己有足够的钱买 iPhone苹果店怎么证明有足够的 iPhone
比如在某个对公开放的节点中,记录了:
account=alice, money=10000
account=bob, iphone=100
…… 以及很多其他的数据
我们假设这里的钱可能是 Alice 用某种手段放进来的。或者我们再简化这个模型,比如全世界所有人的钱,都在这个系统里,这样我们就不用关心钱从哪里来这个问题了。如果是比特币,钱是需要挖矿的。
如图,这个结构也叫作区块链。每个 Block 下面可以存一些数据,每个 Block 知道上一个节点是谁。每个 Block 有上一个节点的摘要签名。也就是说,如果 Block 10 是 Block 11 的上一个节点,那么 Block 11 会知道 Block 10 的存在,且用 Block 11 中 Block 10 的摘要签名,可以证明 Block 10 的数据没有被篡改过。
区块链构成了一个基于历史版本的事实链前一个版本是后一个版本的历史。Alice 的钱和苹果店的 iPhone 数量,包括全世界所有人的钱,都在这些 Block 里。
购买转账的过程
下面请你思考Alice 购买了 iPhone需要提交两条新数据到上面的区块链。
from=A, to=B, price=10000, signature=alice的签名
from=B, to=A, object=iphone, signature=苹果店的签名
那么我们可以在末端节点上再增加一个区块,代表这次交易,如下图:
比如Alice 先在本地完成这件事情,本地的区块链就会像上图那样。 假设有一个中心化的服务器专门接收这些区块数据Alice 接下来就可以把数据提交到中心化的服务器,苹果店从中心化服务器上看到这条信息,认为交易被 Alice 执行了,就准备发货。
如果世界上有很多人同时在这个末端节点上写新的 Block。那么可以考虑由一个可信任的中心服务帮助合并新增的区块数据。就好像多个人同时编辑了一篇文章发生了冲突那就可以考虑由一个人整合大家需要修改和新增的内容避免同时操作产生混乱。
解决欺诈问题
正常情况下所有记录都可以直接合并。但是比如Alice在一家店购买了 1 个 iPhone在另外一家店购买了 2 个 iPhone这个时候 Alice 的钱就不够付款了。 或者说 Alice 想用 20000 块买 3 个 iPhone她还想骗一个。
那么 Alice 最终就需要写这样的记录:
from=A, to=B, price=10000, signature=alice的签名
from=B, to=A, object=iphone, signature=一个苹果店的签名
from=A, to=B1, price=20000, signature=alice的签名
from=B1, to=A, object=iphonex2, signature=另一个苹果店的签名
无论 Alice 以什么顺序写入这些记录,她的钱都是不够的,因为她只有 20000 的余额。 这样简单地就解决了欺诈问题。
如果 Alice 想要修改自己的余额,那么 Alice 怎么做呢?
Alice 需要新增一个末端的节点,比如她在末端节点上将自己的余额修改为 999999。那么 Alice 的余额,就和之前 Block 中记录的冲突了。简单一查,就知道 Alice 在欺诈。如果 Alice 想要修改之前的某个节点的数据,这个节点的摘要签名就会发生变化了, 那么后面所有的节点就失效了。
比如 Alice 修改了 Block 9 的数据,并把整个区块链拷贝给 Bob。Bob 通过验证签名,就知道 Alice 在骗人。如果 Alice 修改了所有 Block 9 以后的 Block相当于修改了完整的一个链条且修改了所有的签名。Bob 只需要核对其中几个版本和其他人,或者和中心服务的签名的区别就知道 Alice 在欺诈。
刚才有一个设计,就是有一个中心平台供 Bob 下载。如果中心平台修改了数据。那么 Bob 会马上发现存在本地的和自己相关的数据与中心平台不一致。这样 Bob 就会联合其他用户一起抵制中心平台。
所以结论是,区块链一旦写入就不能修改,这样可以防止很多欺诈行为。
解决并发问题
假设全球有几十亿人都在下单。那么每次下单,需要创建新的一个 Block。这种情况会导致最后面的 Block开很多分支。
这个时候你会发现,这里有同步问题对不对? 最傻的方案就是用锁解决,比如用一个集中式的办法,去接收所有的请求,这样就又回到中心化的设计。
还有一个高明的办法,就是允许商家开分支。 用户和苹果店订合同,苹果店独立做一个分支,把用户的合同连起来。
这样苹果店自己先维护自己的 Block-Chain等待合适的时机再去合并到主分支上。 如果有合同合并不进去,比如余额不足,那再作废这个合同(不发货了)。
这里请你思考这样一种处理方式:如果全世界每天有 1000 亿笔订单要处理,那么可以先拆分成 100 个区域,每个区域是 10W 家店。这样最终每家店的平均并发量在 10000 单。 然后可以考虑每过多长时间,比如 10s进行一次逐级合并。
这样,整体每个节点的压力就不是很大了。
总结
在这一讲,我们主要学习了一些比锁更加有趣的处理方式, 其实还有很多方式,你可以去思考。并发问题也不仅仅是要解决并发问题,并发还伴随着一致性、可用性、欺诈及吞吐量等。一名优秀的架构师是需要储备多个维度的知识,所以还是我常常跟你强调的,知识在于积累,绝非朝夕之功。
另外,我想告诉你的是,其实大厂并不是只招收处理过并发场景的工程师。作为一名资深面试官,我愿意给任何人机会,前提是你的方案打动了我。而设计方案的能力,是可以学习的。你要多思考,多查资料,多整理总结,这样久而久之,就有公司愿意让你做架构了。
那么通过这节课的学习,你现在可以尝试来回答本节关联的面试题目:除了上锁还有哪些并发控制方法?
【解析】 这个问题比较发散,这一讲我们介绍了基于乐观锁的版本控制,还介绍了区块链技术。另外还有一个名词,并不属于操作系统课程范畴,我也简单给你介绍下。处理并发还可以考虑 Lock-Free 数据结构。比如 Lock-Free 队列,是基于 cas 指令实现的,允许多个线程使用这个队列。再比如 ThreadLocal让每个线程访问不同的资源旨在用空间换时间也是避免锁的一种方案。