首页 > 编程语言 >Java 面试题 11 - 分布式系统常见问题

Java 面试题 11 - 分布式系统常见问题

时间:2022-10-07 00:12:10浏览次数:62  
标签:面试题 常见问题 Redis 事务 线程 key 分布式系统 ID 分布式

分布式 ID 的实现

分布式 ID 需要满足哪些需求?

基本需求:

  • 全局唯一
  • 高性能:生成速度快,对本地资源消耗小。
  • 高可用:生成分布式 ID 的 服务 要保证高可用性。
  • 方便易用:使用方便、快速接入。

此外,一个好的分布式 ID 还应该保证:

  • 安全:ID 中不包含敏感信息。
  • 有序递增:可以通过 ID 进行排序。
  • 有具体的业务含义:可以让定位问题更透明化。
  • 独立部署:分布式系统专门有一个发号器服务,用来生成分布式 ID。

1️⃣ UUID

Universally Unique Identifier,通用唯一识别码。由 32 位 16 进制数字构成,可以基于当前时间戳产生(通过当前时间、随机数、本机 MAC 地址计算得出),也可以基于随机数或者“名字”产生。Java 自带的 UUID 可以基于随机数和基于名字两种方式产生:

UUID uuid = UUID.randomUUID();
// 根据指定的字节数组产生
byte[] nbyte = {10, 20, 30};
UUID uuidFromBytes = UUID.nameUUIDFromBytes(nbyte);

UUID 太长而不易于存储;无序,如果作为数据库主键(InnoDB)会严重影响性能。(因为 InnoDB 采用聚集索引)

2️⃣ 用数据库生成

用自增 ID 来表示分布式 ID,另外,将各个分库中同一个业务表的自增 ID 设为 不同的起始值,同时设置 固定的步长,步长等于分库的数量。比如 A 库产生 1, 3, 5...,B 库产生 2, 4, 6, ...

优势是 仅依赖于数据库自身,不需要其他资源,并且 ID 单调递增;缺点是 过于依赖数据库,当数据库异常时整个系统就不可用;另外主从集群下,数据一致性有时难以保证ID 发号性能被单台 MySQL 的读写性能限制;

3️⃣ 用 Redis 实现

通过 incrincrby 这样的自增原子命令来实现,Redis 单线程的特点可以保证生成的 ID 唯一且有序。同样可以采用集群方式满足高并发的业务需求,这时也要设置不同的起始值和固定的步长。

4️⃣ snowflake 算法

用 64 位整数表示一个 ID,将这些整数分成四部分,每个部分代表不同的含义:

时间戳部分可以表示 \(2^{41}\) 个数,每个数代表某个毫秒,一共可以表示的时间大概是 69 年。

优缺点:

  • 雪花算法生成 ID 的 性能高,且 ID 单调递增,不依赖第三方系统,以服务的方式部署稳定性高。
  • 但是 强依赖机器时钟,如果机器上 时钟回拨,会导致 发号重复

分布式锁的实现

为什么要有分布式锁?

我们在系统中修改已有数据时,需要先读取,然后进行修改保存,由于修改和保存不是原子操作,在并发场景下,部分对数据的操作可能会丢失。

在单服务器系统我们常用本地锁(ReentrantLock,synchronized)来避免并发带来的问题,然而,当服务采用集群方式部署时,本地锁无法在多个服务器之间生效,这时候保证数据的一致性就需要分布式锁来实现。

分布式锁需要具备以下特征:

  • 互斥性:同一时刻锁只能被一个线程持有。
  • 超时释放:可以避免死锁。
  • 可重入:进入临界区之后,可以直接使用已经获取到的锁(临界区中调用的方法也需要当前锁),而不需要再次请求锁。
  • 高性能、高可用。

Redis 分布式锁

1️⃣ 基于 Redis 单节点的分布式锁

使用 SETNX 命令

  • 加锁:SETNX key value 命令只在 key 不存在时才设置这个键值对,key 唯一标识一个锁,可以按照业务 需要锁定的资源 ID 来命名。
  • 解锁:DEL key,删除键值对,从而其他线程可以获取该锁。
  • 锁超时:EXPIRE key timeout:设置过期时间,保证即使发生异常导致锁没有被显式释放时,锁也可以在 超时后自动释放,避免死锁。
if (setnx(key, 1) == 1) { // 加锁
  expire(key, 30); // 设置过期时间
  try {
    // 执行业务逻辑
  } finally {
    del(key); // 解锁
  }
}

这种方式存在一些问题:

  • setnxexpire 操作是 非原子性 的,如果在获取锁后发生异常,导致 expire 命令没有执行,那么就永远不会解锁。解决方法是使用 set key value nx ex timeout 命令,它将 setnxexpire 两个命令变成一个原子命令。
  • set k v nx ex t 仍不能彻底解决分布式锁超时问题:
    • 锁被提前释放:假设线程 A 在加锁和解锁之间的逻辑执行的时间过长,超出了锁的过期时间,这时锁会被自动释放,线程 B 就可以获取这把锁,但此时线程 A 的逻辑还没有执行完,这就导致临界区代码 不能严格串行执行
    • 锁被误删:在上面情形中,A 执行完后,它会以为自己还持有锁,所以会继续执行 DEL 命令释放锁,如果此时线程 B 在临界区的逻辑还没有执行完,线程 A 实际上释放了线程 B 的锁。

为了避免以上情况,建议 不要在执行时间过长的场景中使用 Redis 分布式锁,同时一个比较安全的做法是 在执行 DEL 解锁之前对锁进行判断,验证当前锁的持有者是否是自己。具体方法就是:在加锁时将 value 设置为一个唯一的随机数,释放锁时先判断随机数是否一致,然后再执行释放操作,这样可以确保不会错误地释放其他线程持有的锁。但判断 value 和删除 key 这两个操作并不是原子性的,所以这个地方需要 使用 Lua 脚本 进行处理,它可以保证连续多个指令的原子性执行。

使用 Redisson 的分布式锁

上面的方法只是解决了一个线程释放其他线程持有的锁的问题,并没有解决 锁超时而自动提前释放 的问题。为此可以利用锁的可重入特性,让获得锁的线程开启一个定时器的守护线程,每 timeout/3 的时间执行一次,去检查该锁是否还存在,存在则对锁的过期时间重新设置为 timeout,即 利用守护线程对锁进行续期,防止锁由于过期而提前释放。

开源框架 Redisson 实现了这个守护线程,且支持 Redis 集群部署。

2️⃣ 基于 Redis 集群实现的分布式锁 Redlock

上面加锁时只作用在一个 Redis 节点上,在主从集群环境下,主节点获取到锁后,在数据同步期间发生故障转移,导致锁数据丢失,其他线程仍可以获取到锁,不满足锁的互斥性。

Redlock 算法就是为了解决这个问题,主要思想是 多节点部署 Redis,有效防止单点故障:

假设有 N 个节点,这些节点 相互独立(不存在主从关系),客户端需要依次尝试从这些节点处获取锁(执行 setnx key value,其中所用的 key 都相同,但 value 各不相同),如果成功获取锁(在超时之前得到响应),则记录下获取锁所用时间,当从 N/2+1 个节点都获取到锁,并且总用时没有超出锁的过期时间时,才认为加锁成功。 如果获取锁失败,客户端应该在 所有 Redis 实例上进行解锁(使用 Lua 脚本),因为有的 Redis 实例可能已经执行了 setnx key value,只是因为网络原因未能向客户端成功发送响应。

分布式事务

CAP:

  • Consistency:分布式系统的所有数据备份,在同一时刻是否为同样的值。
  • Availability:能够及时处理请求,不会一直等待。
  • Partition tolerance:能容忍网络分区,即在网络断开时,被分隔的节点仍能正常提供服务。

BASE 原则:

  • Basically Available:系统故障时,允许损失部分可用性。
  • Soft state:允许数据存在中间状态,即允许系统在不同节点之间进行数据同步时出现延时。
  • Eventually consistent:最终一致性,系统中所有数据副本在经过一段时间的同步后,最终能够达到一致的状态。

BASE 是对 CAP 中一致性和可用性权衡的结果,核心思想是 牺牲强一致性而获得可用性,并允许数据在一段时间内不一致,只要最终达到一致即可。

分布式事务的解决方案

基于 ACID 的事务是 刚性事务,基于 BASE 的事务叫做 柔性事务

刚性事务实现方案—— XA 协议

XA 协议是一个基于数据库层面的分布式事务协议,存在一个统揽全局的 事务管理器,每个节点有一个 本地资源管理器,后者一般由数据库实现。事务管理器是全局的调度者,负责对各个本地资源管理器发出提交或者回滚的命令。

两阶段提交 2PC

所谓两阶段是指:

  • 在第一阶段(准备阶段),事务管理器(协调者)向所有资源管理器(参与者)发送 准备请求,参与者收到后会向协调者返回 prepared 或者 no,前者表示可以执行事务提交,后者表示回滚。
  • 在第二阶段(提交阶段),协调者根据参与者的反馈做抉择,如果所有反馈都是 prepared,那么会向所有参与者发送 commit 消息;否则,发送 abort 消息。参与者根据消息类型执行提交或回滚操作。

存在的问题:

  • 同步阻塞:当资源管理器都共同依赖于某一资源时,如果某个参与者出现通信超时,其他参与者就会出现阻塞。
  • 单点故障:一旦事务管理器故障,整个系统都不可用。
  • 数据不一致:在第二阶段,如果事务管理器只给一部分资源管理器发送了 commit 命令,网络就发生了异常,那么就会只有这部分事务参与者提交事务,导致系统数据不一致。

另外,2PC 只能用在两个数据库之间,因为只有数据库实现了 XA 协议。

柔性事务

  • TCC

    • Try 阶段:检查资源和 预留资源,比如在订单操作中,检查剩余库存是否够用,并预留一定量货物供本次事务使用。

    • Confirm 阶段:确认执行业务,在预留资源的基础上进行操作。

    • Cancel 阶段:如果有一个业务 预留资源失败,则取消 Try 阶段预留的所有资源。

  • SagaSaga 由一系列本地事务构成,每一个本地事务执行完成后,会发布一条 消息 或者一个 事件 来触发 Saga 中下一个本地事务的执行。如果一个本地事务因为某些要求无法满足而失败,Saga 会为该事务之前成功提交的所有事务执行 补偿操作

标签:面试题,常见问题,Redis,事务,线程,key,分布式系统,ID,分布式
From: https://www.cnblogs.com/lzh1995/p/16758889.html

相关文章

  • Java 面试题 09 - 计算机网络
    TCP&UDPTCP和UDP的区别有什么?TCP面向连接,UDP无连接。TCP提供可靠的传输,在传递数据之前,需要通过三次握手建立连接,在传递数据时,有确认、窗口、重传、拥塞机......
  • Java 面试题 08 - 计算机网络
    进程什么是系统调用?根据进程访问资源的特点,可以把进程的运行状态分为两个级别:用户态:只能读取用户程序的数据;内核态:可以访问几乎一切资源。用户程序基本都运行在用户......
  • 2022 HTTP面试题都在这里
    HTTP常见面试题Http与Https的区别:Http与Https的区别:HTTP的URL以http://开头,而HTTPS的URL以https://开头HTTP是不安全的,而HTTPS是安全的HTTP标准端口是80,而HTTPS......
  • Java 面试题 05 - Spring Boot
    Spring是什么?是一个轻量级的控制反转和面向切面的容器框架。控制反转(IOC):一个对象所依赖的其他对象的创建,不由这个对象负责,而是由容器负责,容器会在对象初始化时就将所......
  • Java 面试题 06 - MySQL
    事务事务是逻辑上的一组操作,要么都执行,要么都不执行。事务的四个特性(ACID):原子性:事务不允许分割,要么全部完成,要么完全不执行。一致性:逻辑上的正确性,即这组操作的结果是......
  • Java 面试题 02 - IO
    select、poll、epoll缓存IO数据传输过程中,会先被拷贝到内核的缓冲区中,然后再从缓冲区拷贝到应用程序的地址空间。这些拷贝操作的开销是很大的。阻塞/非阻塞vs同步......
  • Java 面试题 01 - Java 基础
    基础概念JDK、JRE、JVM的区别?JDK是Java开发工具包,包含了Java的开发工具(编译工具javac.exe和打包工具jar.exe等)和JRE。JRE是Java运行环境,提供了库、JVM......
  • MySQL面试题(二)
    11、列对比运算符是什么?在SELECT 语句 的列比较中使用=,<>,<=,<,>=,>,<<,>>,<=>,AND,&nbs***bsp;或 LIKE 运算符。12、  BLOB 和 TEXT 有什么区别?BLOB 是一个二进......
  • 第一道面试题 第一道困难题解答记录
    输入一个奇数n,输出一个由*构成的n阶实心菱形。输入格式一个奇数n。输出格式输出一个由*构成的n阶实心菱形。具体格式参照输出样例。数据范围1≤n≤99输......
  • MySQL面试题(一)
    1、MySQL中有哪几种锁?1、表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。2、行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并......