主页 > imtoken正版 > 以太坊 2.0 的致命缺陷

以太坊 2.0 的致命缺陷

imtoken正版 2023-12-17 05:13:07

本文字数:4600字

阅读时间:15分钟

继比特币减半之后,币圈正在酝酿另一件大事,那就是以太坊的升级。 在我开始谈论以太坊 2.0 之前,让我问一个问题。

假设某银行总准备金为1万元,在A市和B市分别有一台独立的ATM机。 一个人在A市的ATM机上取了5000元,同时另一个人也在B市取了5000元,请问你银行里还剩多少钱?

这是一道非常简单的数学题。 相信大家都能给出正确答案。 银行总共有10000元,A和B分别取了5000元,所以总和是10000元。 10000-10000=0。 所以银行还剩0元。

这道题对我们来说很容易,但是对计算机来说就没那么容易了。 比如我们要写一个应用来实现上面的功能。 难点在于我们如何保证取款过程中银行的数据是实时同步的。 因为如果不同步的话,B在操作ATM的时候,他读到的准备金数额可能还是10000,还没有扣除A取的5000,这显然是乱七八糟的! 我用Java程序模拟一下,你就明白了。

让我们首先定义一个名为 Bank 的简单类。 其中,只有一个自定义变量是balance,用来指代账户的余额。 操作函数只有两个,分别是withdraw with withdraw和query balance getBalance。

public class Bank{   //银行余额   private int balance;
public Bank(int balance){ this.balance = balance; }
//用户提款 public void withdraw (int value) { try { Thread.sleep(300); //0.3秒的模拟时延 } catch (InterruptedException e) { e.printStackTrace(); } this.balance -= value; }
//查询当前余额 public int getBalance(){ return this.balance; }
}

接下来是用于演示的主程序:

public class Demo {
public static void main(String args[]) throws InterruptedException { Bank bank = new Bank(10000); //银行的初始余额 Runnable Atm1 = () -> { bank.withdraw(5000); System.out.println("A 提款 5000"); };
Runnable Atm2 = () -> { bank.withdraw(5000); System.out.println("B 提款 5000"); };
Thread A = new Thread(Atm1); //提款人A的操作线程 Thread B = new Thread(Atm2);//提款人B的操作线程 A.start();//A开始提款 B.start();//B开始提款 A.join();//等待A操作结束 B.join();//等待B操作结束
//显示余额 System.out.println("银行余额:"+bank.getBalance()); }}

在这个程序中,我们设置了初始余额为10000。然后我们模拟了两个操作线程A和B,两者几乎同时进行了取款操作,都拿了5000元。 我们来看看程序的运行结果:

以太坊转到自己的账户_批量查询以太坊账户余额_sitejianshu.com 以太坊账户创建

但是问题是B筹到5000后,银行没有余额,所以应该显示0。但是这里还是5000,这就是问题所在! 有趣的是,如果你反复运行这个程序,你会发现每次的结果可能都不一样。 有时显示0,有时显示5000。这种现象在计算机中有一个典型的术语叫做race condition。 意味着有多个计算机线程在竞争同一个资源,导致数据更新无序。 在我们的例子中,A 和 B 通过不同的 ATM 开了两个取款操作线程。 两个线程都将修改银行余额。 在这种情况下,B相当于抢走了A的数据修改权,导致新更新的数据立即被B覆盖。

那么为什么会这样呢? 这是由于计算机 CPU 的特殊架构。 计算机的任何一条指令都需要知道它的操作对象是谁,它的值是什么,否则这条指令是没有意义的。 在哪里可以找到这个操作对象? CPU中有一个叫做寄存器的部件负责存储这些信息。 任何一条指令都需要访问这个寄存器来获取其操作数的值,这样指令才能完整的执行。 比如下图中的AX就是CPU的一个寄存器。 里面可以存放一个16位的二进制数。

sitejianshu.com 以太坊账户创建_批量查询以太坊账户余额_以太坊转到自己的账户

就拿我们的例子来说,withdraw是一条取款指令,它的操作对象是银行余额。 这个余额的价值是多少? 这是在寄存器中查找。

得到这个值后,指令开始执行。 在这个过程中,它会修改寄存器的内容来完成数据的更新。 这就是我们的银行余额更新的方式。 这时候如果有新的指令进来获取当前余额,我们就可以回到那个寄存器去寻找答案。

但问题是我们的计算机并不是每单位时间只执行一条指令。 在许多情况下,多条指令同时运行。 不然怎么能一边听音乐一边上网呢? 所以为了实现“并行运行”,我们的CPU引入了多线程管理机制。 就是将这些指令封装在不同的线程中,通过合理的调度,并行运行多个程序。 例如,我们可以使用一个线程来执行取款(withdraw)命令,同时可以分配另一个线程来查询当前余额(getBalance),如下图所示:

批量查询以太坊账户余额_sitejianshu.com 以太坊账户创建_以太坊转到自己的账户

查询操作不影响寄存器的状态,所以两个线程可以相安无事,但是如果这时候也引入第三个线程来执行取款操作,事情就会变得很困难。 因为它可能抢夺和线程1同一个寄存器的资源,如下图,线程1和线程3同时更新寄存器的状态。 很有可能线程3执行的时候,线程1还没有来得及更新balance的值,所以它读取到的值还是更新前的值。 ,即余额=10000。 而当线程1结束运行时批量查询以太坊账户余额,虽然余额更新为5000,但也无济于事,因为线程3已经在运行了。 所以线程3对寄存器的更新还是按照原来的旧值:10000,导致最终余额还是5000。(10000-5000=5000)

以太坊转到自己的账户_批量查询以太坊账户余额_sitejianshu.com 以太坊账户创建

所以为了避免这种情况,我们必须要保证这个寄存器的状态在多线程运行中是同步的。 虽然他们共享同一个数据资源,但必须有一个在前。 以上面的情况为例,我们要保证当线程1操作寄存器时,其他线程不能访问到。 只有线程1结束后,线程3才能运行。 这样各个线程读取的寄存器数据就同步了。

为了实现多线程之间的同步,我们的CPU引入了“保护锁”(lock)机制。 它是为这个共享寄存器资源标记一个“锁存”状态。 任何线程都可以在访问寄存器时“锁定”它。 这样其他线程无法访问,只能乖乖等待。 只有当当前线程执行完毕后,这个“保护锁”才会被释放。 然后剩下的线程会被自动唤醒,开始访问这个寄存器资源。 像我们的例子,线程1在访问寄存器的时候可以“锁定”,线程3会被强制等待。 线程1执行完后,余额会更新为10000-5000=5000。 然后寄存器释放保护锁,线程3被唤醒,开始访问变量balance。 类似地,然后将其放在保护锁上。 此时它得到的值是5000,它执行完后,余额会更新为5000-5000=0。 如下:

sitejianshu.com 以太坊账户创建_批量查询以太坊账户余额_以太坊转到自己的账户

相应的,我们的Java源码只需要修改如下,就可以实现这套“保护锁”机制:

public class Bank{   //银行余额  private int balance;  private final ReentrantLock lock = new ReentrantLock();  …….   //用户提款   public void withdraw (int value)  {       lock.lock(); //加上保护锁       try {           Thread.sleep(300); //0.3秒的模拟时延       } catch (InterruptedException e) {           e.printStackTrace();       }       this.balance -= value;       lock.unlock(); //释放保护锁   }}

运行结果如下:

批量查询以太坊账户余额_sitejianshu.com 以太坊账户创建_以太坊转到自己的账户

根据这个操作的结果可以看到,在A和B进行了两次提款后,我们的银行余额已经正确更新为0了。

所以我们可以看到虽然每个线程都是独立的,但是整个线程的调度是集中的。 CPU就像一个大脑。 它必须合理地为不同的线程分配资源,并安排执行的顺序,以保证数据的同步。 所以大脑必须知道哪些寄存器被锁定,哪些线程正在访问它们,哪些线程正在等待。 也就是说,它有一个“上帝视角”,可以实时监控每个线程和每个寄存器的状态。

单台电脑的程序运行是这样的,但是如果我们再大一点,多台电脑的节点部署也是这样。 以淘宝为例,双11要处理几千万的交易请求,一台服务器肯定不够用。 它必须部署多个服务器节点,然后通过负载均衡器将这些请求平均分配到每个服务器。 如下所示:

以太坊转到自己的账户_sitejianshu.com 以太坊账户创建_批量查询以太坊账户余额

但是不管有多少服务器节点,不管有多少请求,最终都是访问同一个数据库! 这一点非常重要。 因为只有这样才能引入“保护锁”机制,在事务中锁定对应的数据库表,保证读写的同步。 比如现在天猫店有10个香奈儿包包,20折,100人抢购。 当第一个人下订单并开始交易时,数据库必须将其他购买请求放入等待队列。 . 这将确保下一个人看到 9 个包裹而不是原来的 10 个。

因此,淘宝的节点部署虽然是分布式架构,但本质上是集中式的。 这种中心化体现在节点线程和数据库解决方案的监控和调度上。 也就是说淘宝服务器背后有一个控制中心,可以实时检测各个节点的状态,这些节点访问同一个数据库。 正是这种中心化的架构,让这么多的节点在保证数据同步的情况下,能够并行处理这么多的请求。

但是公链的分布式架构就完全不一样了,因为它本质上是去中心化的,每个节点都是独立作战的。 所以它没有控制面板来控制所有节点的状态。 其次,它没有一个集中的数据库供这些节点访问,而是每个节点独立配置自己的数据库。 因此,会同时存在多个账本。 我们只能引入投票机制来确定一个最终的账本,间接实现节点间的同步。 比特币通过算力投票,选出最长的区块链作为最终账本。 虽然不同节点会产生多个区块链账本,造成拜占庭将军问题,但比特币算法是行得通的。 因为是单链结构,单位时间内只能产生一个区块。 虽然不同的节点可以同时广播区块,但是比特币的挖矿机制保证了这个区块的唯一性。 所以比特币本质上是一个单线程的数据库读写操作。

以太坊没有问题,因为它和比特币一样是单链结构,采用POW共识。 但是升级到2.0之后,问题就非常大了。 因为以太坊 2.0 引入了一种称为“分片”的机制。 简单来说,就是借鉴了淘宝的负载均衡器(Load Balancer)机制——设置多个节点,批量处理不同的请求。 比如现在有10000笔交易请求,我让A节点处理5000笔,B节点处理剩下的5000笔,这样速度会更快。 我承认初衷是好的,但实际上行不通。 根据以太坊2.0的介绍,它首先引入了一条名为Beacon的主链,负责记录所有交易的状态,相当于账本的核心。 然后它将整个节点网络划分成不同的区域,每个区域作为一个分片,相当于一个Load Balancer。 每个分片处理不同的交易请求,最终分别记录在主链上。 如下所示:

以太坊转到自己的账户_批量查询以太坊账户余额_sitejianshu.com 以太坊账户创建

说到这里你可能有点懵,我换个方式解释一下,你就会明白了。 只要看过我之前的介绍,应该对多线程同步有一个简单的了解。 以太坊 2.0 也有类似的架构。 Beacon链可以理解为一个中心数据库,每个分片相当于一个独立的线程。 每个线程广播的块是不同的。 比如分片1的区块包含的交易顺序是1到3000,分片2的区块是3000到6000。所以以太坊2.0相当于一个多线程的数据库读写操作。 这与比特币有着根本的不同。

如果多个线程操作同一个数据库,很容易出现数据不同步的问题,所以正确的做法是在每个线程执行的过程中给数据库加一把保护锁,防止其他线程访问它同时。 所以我们的情况也是一样的,就是每个分片在更新Beacon链的时候,都要在主链上加一个“保护锁”,从而强制其他分片进入等待队列。 V神也确实考虑到了这一点,打算引入这种“保护锁”机制。 但错误的是,这个 Beacon 链并不是唯一的中央数据库。

我们要知道以太坊是公链,公链是去中心化的! 所以每个挖矿节点都有自己的Beacon链。 所以这里的“锁”就是加在Beacon链上的锁。 这个锁的状态显然没有和其他节点同步,所以剩下的分片节点还是会继续访问主链。 这时候,不同分片之间就会出现我前面提到的竞争条件(race condition)。 shard 1的更新可能会被shard 2覆盖,如下图:

sitejianshu.com 以太坊账户创建_以太坊转到自己的账户_批量查询以太坊账户余额

由此可见,去中心化架构中的保护锁无疑是没有用的。 而且,以太坊 2.0 还允许分片之间进行读写操作,这同样会暴露出多线程同步问题。

那么有人可能会问,我们能不能把这条Beacon链的latch状态同步到其他分片上呢? 这里还涉及另一个投票问题。 因为如果每个分片节点都从自己的角度出发,它看到的主链状态是不一样的。 比如上图中的节点1已经锁定了自己记录的主链,但是节点2并不这么认为。 因为它没有看到锁。 所以节点1认为有锁,节点2认为没有锁。 拜占庭将军问题又出现了批量查询以太坊账户余额,只能投票决定了。 但是以太坊 2.0 使用的是 POS,不再是 POW,所以选择最长链的共识是没有意义的。 因为区块的产生是没有成本的,只要拿到记账权,就可以一次广播多个区块,所以最长的链不能代表最多的共识。 这时候计票就会变得更加复杂。 即使共识算法能够正确统计票数并确定节点 1 获胜,也意味着分片 2 的区块将同时被丢弃。 这个时候分片有什么意义呢? 如果要保留分片2的块,就必须把这个线程放到等待队列中。 但问题是你没有一个可以全局为不同线程分配资源的控制面板,你甚至不知道每个线程的状态。 那么这是否又回到了过去的中心化方式呢?

基于以上分析,我可以得出结论,以太坊2.0上线后,必然会出现大量数据不同步的问题。 不仅分片不同步,每个节点的 Beacon 链也不同步。 公链不同于淘宝。 如果淘宝出现数据不同步的问题,我最多只能修改数据库或者重启服务器。 但是在公链上不同步会导致矿工撕裂造成分叉,这是一个非常严重的问题。