分布式理论 | 青训营笔记

83 阅读7分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 11 天

前言

在听完课后,结合网上查找到的知识,对分布式理论有了初步的认识,学员手册上的课后问题也有了自己的最优解。现将问题及解答分享如下,供大家参考。

为什么TCP采用三次握手?而不是两次和四次?

TCP 的可靠连接是靠 seq( sequence numbers 序列号)来达成的。

所以说,建立可靠连接,需要对客户端和服务器的起始序列号达成共识。TCP建立链接的过程如下:

为什么不能两次握手?| 两次握手演绎

第一步:

客户端发送一个起始序列号seq = x的 报文段给服务器。

第二步:

服务器端返回向客户端发送确认号 ack = x+1,表示对客户端的起始序列号x 表示确认,

并告诉客户端,他的起始序列号是 seq = y.

所以,为什么不能两次握手?

因为这种情况下,只有服务器对客户端的起始序列号做了确认,但客户端却没有对服务器的起始序列号做确认,不能保证传输的可靠性。

为什么不是四次握手?| 四次握手演绎

第一步:

客户端A 发送同步信号SYN + A的初始序列号 seq = x

第二步:

服务器端B 确认收到A的同步信号,并记录A的初试序列号到本地,

并向A发送确认信息 ack =x+1。

第三步:

B发送同步信号SYN + B的初始序列号 seq 给客户端A。

第四步

A确认收到B的同步信号,并记录 B'的初试序列号到本地,并向B发送确认信息 ack = y + 1.

很显然,第二,三步可以合并,只需要三次握手,可以提高连接的速度与效率。


总结:
TCP链接之所以可靠,是因为其链接是面向字节的。

在通信的过程中,协议会给每个字节一个序分配一个序号。三步握手的过程,主要是为了互相确认双方的起始序列号。

如果只进行两次握手, 客户端发送链接请求及起始序列号seq = x, 收到 服务器端的起始序列号seq = y及对客户端序列号seq= x 的确认。此时,双方就 客户端的起始序列号达成了共识。

此时,并没有对服务器的起始序列号达成共识,所以就需要进行第三次握手。对B的起始序列号达成共识,不能保证通信的可靠。

如果进行四次握手,在四次握手的过程中,可以把第二、三步合并,这样可以提高连接的速度与效率。

什么场景适合乐观锁?什么场景适合悲观锁?

悲观锁和乐观锁的区别

image.png

场景

什么时候使用乐观锁?

资源提交冲突,其他使用方需要重新读取资源,会增加读的次数,但是可以面对高并发场景,前提是如果出现提交失败,用户是可以接受的。因此一般乐观锁只用在高并发、多读少写的场景。

其中:GIT,SVN,CVS等代码版本控制管理器,就是一个乐观锁使用很好的场景,例如:A、B程序员,同时从SVN服务器上下载了code.html文件,当A完成提交后,此时B再提交,那么会报版本冲突,此时需要B进行版本处理合并后,再提交到服务器。这其实就是乐观锁的实现全过程。如果此时使用的是悲观锁,那么意味者所有程序员都必须一个一个等待操作提交完,才能访问文件,这是难以接受的。

什么时候使用悲观锁?

一旦通过悲观锁锁定一个资源,那么其他需要操作该资源的使用方,只能等待直到锁被释放,好处在于可以减少并发,但是当并发量非常大的时候,由于锁消耗资源,并且可能锁定时间过长,容易导致系统性能下降,资源消耗严重。因此一般我们可以在并发量不是很大,并且出现并发情况导致的异常用户和系统都很难以接受的情况下,会选择悲观锁进行。

示例

乐观锁

版本号控制

1, start transaction  
2, first_version = get_cur_version() // 获取当前数据版本
3, update_data(version+=1)           // 更新操作版本号+1
4, cur_version = get_cur_version()   // 提交更新时,获取版本号
5, if first_version == cur_version   // 比较提交时的版本号与第一次获取的版本号,如果一致,那么认为资源是最新的,可以更新
     then commit 
  else rollback or raise exception  // 否则回滚或者抛出异常

时间戳控制

1, start transaction
2, first_timestamp = get_cur_timestamp()
3, update_data(timestamp=get_sys_cur_timestamp)
4, if first_timestamp = get_cur_timestamp()
      then commit 
   else rollback or raise Exception

悲观锁

悲观锁的实现一般都是通过锁机制来实现的,锁可以简单理解为资源的访问的入口。如果要对一个具有锁属性的资源执行访问时,在更新操作时,需要持锁权才能进行操作,但是往往这种操作可以保证数据的一致性和完整性。例如数据库表的行锁。

RAFT协议中该如何解决Stale读的问题?

解决方案:
引入一个新的概念, region leader。region leader 是一个逻辑上的概念, 任意时刻对于某一个 region 来说, 一定只拥有一个 region leader, 每个 region leader 在任期之内尝试每隔 t 时间间隔, 在 raft group 内部更新一下 region leader 的 lease. 所有的读写请求都必须通过 region leader 完成, 但是值得注意的是, region leader 和 raft leader 可能不是一个节点,当 region leader 和 raft leader 不重合的时候,region leader 会将请求转发给当前的 raft leader,当网络出现分区时,会出现以下几种情况:

  • region leader 落在多数派,老 raft leader 在多数派这边
  • region leader 落在多数派,老 raft leader 在少数派这边
  • region leader 落在少数派,老 raft leader 在多数派这边
  • region leader 落在少数派,老 raft leader 在少数派这边

对于第一种情况,region leader 的 lease 不会过期,因为 region leader 的心跳仍然能更新到多数派的节点上,老的 raft leader 仍然能同步到大多数节点上,少数派这边也不会选举出新的 leader, 这种情况下不会出现 stale read。

image.png

第二种情况,就是开篇提到会出现 stale read 的典型情况,老的 raft leader 被分到了少数派这边,多数派这边选举出了新的 raft leader ,如果此时的 region leader 在多数派这边。

image.png

因为所有的读写请求都会找到 region leader 进行,即使在原来没有出现网络分区的情况下,客户端的请求也都是要走 node 1 ,经由 node 1 转发给 node 5,客户端不会直接访问 node 5,所以此时即使网络出现分区,新 leader 也正好在多数派这边,读写直接就打到 node 1 上,皆大欢喜,没有 stale read。

第三种情况,region leader 落在少数派这边,老 raft leader 在多数派这边,这种情况客户端的请求找到 region leader,他发现的无法联系到 leader(因为在少数派这边没有办法选举出新的 leader),请求会失败,直到本次 region leader 的 lease 过期,同时新的 region leader 会在多数派那边产生(因为新的 region leader 需要尝试走一遍 raft 流程)。因为老的 region leader 没办法成功的写入,所以也不会出现 stale read。但是付出的代价是在 region leader lease 期间的系统的可用性。

第四种情况和第三种情况类似,多数派这边会产生新的 raft leader 和 region leader。

总体来说,这种方法牺牲了一定的可用性(在脑裂时部分客户端的可用性)换取了一致性的保证。