分布式相关面试

Catalogue
  1. 1. 分布式锁
    1. 1.1. 基于 redisson 做分布式锁
    2. 1.2. 基于 ZooKeeper 做分布式锁
  2. 2. 分布式限流
  3. 3. 分布式ID生成
    1. 3.1. 雪花算法snowflake
    2. 3.2. 利⽤redis的incr原⼦性操作⾃增
  4. 4. 分布式session
    1. 4.1. 基于redis和JWT
    2. 4.2. 基于redis和Spring Session
  5. 5. 接口幂等性
  6. 6. 参考资料

分布式锁

基于 redisson 做分布式锁

Redlock算法: Redlock 是 Redis 的作者 antirez 给出的集群模式的 Redis 分布式锁,它基于 N 个完全独立的 Redis 节点(通常情况下 N 可以设置成 5)。算法的步骤如下:

  1. 客户端获取当前时间,以毫秒为单位。
  2. 客户端尝试获取 N 个节点的锁,N 个节点以相同的 key 和 value 获取锁。客户端需要设置接口访问超时,接口超时时间需要远远小于锁超时时间,比如锁自动释放的时间是 10s,那么接口超时大概设置 5-50ms。这样可以在有 redis 节点宕机后,访问该节点时能尽快超时,而减小锁的正常使用。
  3. 客户端计算在获得锁的时候花费了多少时间,方法是用当前时间减去在步骤一获取的时间,只有客户端获得了超过 3 个节点的锁,而且获取锁的时间小于锁的超时时间,客户端才获得了分布式锁。
  4. 客户端获取的锁的时间为设置的锁超时时间减去步骤三计算出的获取锁花费时间。
  5. 如果客户端获取锁失败了,客户端会依次删除所有的锁。

使用 Redlock 算法,可以保证在挂掉最多 2 个节点的时候,分布式锁服务仍然能工作,这相比之前的数据库锁和缓存锁大大提高了可用性,由于 redis 的高效性能,分布式缓存锁性能并不比数据库锁差。
但是失效时间设置多长时间为好?如何设置的失效时间太短,方法没等执行完,锁就自动释放了,那么就会产生并发问题。如果设置的时间太长,其他获取锁的线程就可能要平白的多等一段时间。

redisson 是 redis 官方的分布式锁组件。GitHub 地址:https://github.com/redisson/redisson

上面的这个问题,失效时间设置多长时间为好?这个问题在 redisson 的做法是:每获得一个锁时,只设置一个很短的超时时间,同时起一个线程在每次快要到超时时间时去刷新锁的超时时间。在释放锁的同时结束这个线程。

基于 ZooKeeper 做分布式锁

  • zk 一般由多个节点构成,一般节点为单数,采用 zab 一致性协议,属于CAP中的CP。因此可以将 zk 看成一个单点结构,对其修改数据后其内部自动将所有节点数据进行修改而后才提供查询服务。
  • zk 的数据以目录树的形式,每个目录称为 znode, znode 中可存储数据(一般不超过 1M),还可以在其中增加子节点。
  • 子节点有三种类型。序列化节点,每在该节点下增加一个节点自动给该节点的名称上自增。临时节点,一旦创建这个 znode 的客户端与服务器失去联系,这个 znode 也将自动删除。最后就是普通节点。
  • Watch 机制,client 可以监控每个节点的变化,当产生变化会给 client 产生一个事件。

实现分布式锁原理:利用临时节点与 watch 机制。每个锁占用一个普通节点 /lock,当需要获取锁时在 /lock 目录下创建一个临时节点,创建成功则表示获取锁成功,失败则 watch/lock 节点,有删除操作后再去争锁。临时节点好处在于当进程挂掉后能自动上锁的节点自动删除即取消锁。
缺点:所有取锁失败的进程都监听父节点,很容易发生羊群效应,即当释放锁后所有等待进程一起来创建节点,并发量很大。

优化:上锁改为创建临时有序节点,每个上锁的节点均能创建节点成功,只是其序号不同。只有序号最小的可以拥有锁,如果这个节点序号不是最小的则 watch 序号比本身小的前一个节点 (公平锁)。实现步骤:

  1. 在 /lock 节点下创建一个有序临时节点 (EPHEMERAL_SEQUENTIAL)。
  2. 判断创建的节点序号是否最小,如果是最小则获取锁成功。不是则取锁失败,然后 watch 序号比本身小的前一个节点。
  3. 当取锁失败,设置 watch 后则等待 watch 事件到来后,再次判断是否序号最小。
  4. 取锁成功则执行代码,最后释放锁(删除该节点)。

优点:有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。
缺点:性能上可能并没有缓存服务那么高,因为每次在创建锁和释放锁的过程中,都要动态创建、销毁临时节点来实现锁功能。ZK 中创建和删除节点只能通过 Leader 服务器来执行,然后将数据同步到所有的 Follower 机器上。还需要对 ZK的原理有所了解。

分布式限流

分布式ID生成

雪花算法snowflake

它⽣成64位的⼆进制正整数,通常各位如下:

  • 1位标识符:始终是0
  • 41位时间戳:41位时间截不是存储当前时间的时间截,⽽是存储时间截的差值(当前时间截 - 开始时间截 )得到的值,这⾥的 的开始时间截,⼀般是我们的id⽣成器开始使⽤的时间,由我们程序来指定的
  • 10位机器标识码:可以部署在1024个节点,如果机器分机房(IDC)部署,这10位可以由 5位机房ID + 5位机器ID 组成
  • 12位序列:毫秒内的计数,12位的计数顺序号⽀持每个节点每毫秒(同⼀机器,同⼀时间截)产⽣4096个ID序号

优点:

  • 此⽅案每秒能够产⽣409.6万个ID,性能快
  • 时间戳在⾼位,⾃增序列在低位,整个ID是趋势递增的,按照时间有序递增
  • 灵活度⾼,可以根据业务需求,调整bit位的划分,满⾜不同的需求

缺点:依赖机器的时钟,如果服务器时钟回拨,会导致重复ID⽣成 在分布式场景中,服务器时钟回拨会经常遇到,⼀般存在10ms之间的回拨;⼩伙伴们就说这点10ms,很短可以不考虑吧。但此算 法就是建⽴在毫秒级别的⽣成⽅案,⼀旦回拨,就很有可能存在重复ID。

利⽤redis的incr原⼦性操作⾃增

⼀般算法为: 年份 + 当天距当年第多少天 + 天数 + ⼩时 + redis⾃增

优点:有序递增,可读性强
缺点:占⽤带宽,每次要向redis进⾏请求

ID安全性的问题:Redis⽅案中,⽤⼾是可以预测下⼀个ID号是多少,因为算法是递增的。 这样的话竞争对⼿第⼀天中午12点下个订单,就可以看到平台的订单ID是多少,第⼆天中午12点再下⼀单,⼜平台订单ID到多少。 这样就可以猜到平台1天能产⽣多少订单了,这个是绝对不允许的,公司绝密啊。

分布式session

基于redis和JWT

使⽤ JWT Token 储存⽤⼾⾝份,然后再从数据库或者 cache 中获取其他的信息。这样⽆论请求分配到哪个服务器都⽆所谓。

基于redis和Spring Session

给 sping session 配置基于 redis 来存储 session 数据,然后配置了⼀个 spring session 的过滤 器,这样的话,session 相关操作都会交给 spring session 来管了。接着在代码中,就⽤原⽣的 session 操作,就是直接基于 spring sesion 从 redis 中获取数据了。实

接口幂等性

参考资料