面试题
面试题
待整理
Java 基础
Java 有哪些循环语句
for 循环
for (初始化语句; 循环条件; 迭代语句) { // 循环体 }
while 循环
while (循环条件) { // 循环体 }
do-while 循环
do { // 循环体 } while (循环条件);
增强 for 循环(foreach 循环)
for (元素类型 元素变量 : 遍历对象) { // 循环体 }
Java 集合
![](https://img.sherry4869.com/blog/it/java/interview/img_2.png)
List
ArrayList:动态数组实现,支持快速随机访问
LinkedList:双向链表实现,适用于频繁插入和删除操作
Vector:类似于 ArrayList,但是是线程安全的,通常不推荐使用,可以使用 ArrayList 替代
Set
HashSet:基于哈希表实现,不保证元素的顺序
LinkedHashSet:具有可预知迭代顺序的 HashSet
TreeSet:基于红黑树实现,按照自然顺序或者指定比较器排序
Map
HashMap:基于哈希表实现,无序
LinkedHashMap:具有可预知迭代顺序的 HashMap
TreeMap:基于红黑树实现,按照自然顺序或者指定比较器排序
Queue
PriorityQueue:非阻塞、非线程安全、无边界,支持优先级队列实现类。
ConcurrentLinkedQueue:非阻塞、线程安全、无边界,基于链接节点的队列实现类
ArrayBlockingQueue:阻塞、线程安全、有边界,创建的时候指定大小,一旦创建容量不可改变实现类,默认是不保证线程的公平性,不允许向队列中插入 null 元素
LinkedBlockingQueue:阻塞、线程安全、可选有边界,一个由链表结构组成的可选有界阻塞队列实现类,如果未指定容量,那么容量将等于 Integer.MAX_VALUE
PriorityBlockingQueue:阻塞、线程安全、无边界,支持优先级排序的无边界阻塞队列实现类
DelayQueue:阻塞、线程安全、无边界,使用优先级队列实现的无界阻塞队列实现类,只有在延迟期满时才能从中提取元素
SynchronousQueue:阻塞、线程安全、无数据队列,不存储元素、没有内部容量的阻塞队列实现类
LinkedBlockingDeque:阻塞、线程安全、无边界,由链表结构组成的可选范围双向阻塞队列实现类,如果未指定容量,那么容量将等于 Integer.MAX_VALUE
Deque
- ArrayDeque:双端队列的数组实现
ArrayList和LinkedList的区别
底层数据结构
ArrayList
使用动态数组实现。它在内存中是一个连续的存储块,可以通过索引直接访问元素LinkedList
使用双向链表实现。每个元素都包含一个指向前一个元素和一个指向后一个元素的引用
访问效率
ArrayList
的访问效率比较高,因为它可以通过索引直接访问元素,时间复杂度为 O(1)LinkedList
的访问效率相对较低,因为要从链表头或尾开始沿着链表逐步遍历,时间复杂度为 O(n)
插入/删除效率
ArrayList
在中间或尾部的插入/删除操作可能比较耗时,因为需要移动元素,时间复杂度为 O(n)LinkedList
在中间或尾部的插入/删除操作相对较快,因为只需要调整节点的引用,时间复杂度为 O(1)
迭代效率
ArrayList
在迭代方面效率更高,因为可以通过迭代器直接访问数组中的元素LinkedList
在迭代时需要按照链表结构一个个地遍历节点
适用场景
ArrayList
适合随机访问和读取,对于频繁读取而很少修改的场景LinkedList
适合频繁插入和删除的场景,特别是在中间或尾部进行插入/删除操作
HashMap的内存结构
HashMap
是 Java 中常用的数据结构之一,它实现了基于键-值对的映射。HashMap
的内存结构主要包含以下几个重要的部分:
数组桶(Buckets)
HashMap
内部维护一个数组,称为桶(buckets)或数组桶。桶的数量由HashMap
的容量决定桶中的每个位置存储一个链表或红黑树,用于解决哈希冲突(多个键映射到同一个桶的情况)
节点(Node)或条目(Entry)
在
HashMap
的桶中,每个元素都是一个节点或条目,用于存储键值对在 Java 8 之前,节点是单向链表的形式。在 Java 8 及之后,当链表长度超过一定阈值(>=8)时,链表会转换为红黑树,以提高查找效率
哈希码(Hash Code)
每个键对象都有一个哈希码,
HashMap
使用哈希码来确定键在桶数组中的位置哈希码通过
hashCode()
方法生成
键-值对的映射关系:每个节点中存储了一个键-值对。键通过哈希码映射到桶的位置,然后通过链表或红黑树解决冲突
负载因子(Load Factor)
负载因子是指在哈希表中允许的最大元素数目与实际桶数之间的比率
当负载因子超过某个阈值时,
HashMap
将会进行扩容,即增加桶的数量,以保持哈希表的性能
容量(Capacity):容量是桶数组的大小,
HashMap
在初始化时设置初始容量,并在需要时通过扩容进行调整
Java 8 之前和 Java 8 及以后的 HashMap
内部实现有一些区别,主要集中在解决哈希冲突的方式以及性能的优化上
Java 8 之前的 HashMap
内存结构:
基于链表的解决冲突
每个桶中的元素是一个链表,用于存储哈希冲突的键值对
当多个键映射到同一个桶时,它们会以链表的形式存储在该桶中
扩容时的重新哈希
当
HashMap
达到负载因子(默认为 0.75)时,会触发扩容操作扩容时,桶的数量会翻倍,并且所有的键值对需要重新计算哈希值并分配到新的桶中
Java 8 及以后的 HashMap
内存结构:
链表和红黑树的转换
在 Java 8 中,引入了红黑树来优化链表,提高查找效率。当链表长度达到一定阈值时,链表会被转换为红黑树
红黑树对于大型桶中的键值对查找操作具有更好的性能,时间复杂度降为O(log n)
TreeNode 节点:在红黑树中,每个节点都是 TreeNode 的实例,而不是之前的 Node 实例
优化的扩容操作
扩容时,Java 8 及以后的
HashMap
会在原有的基础上进行迁移,而不是像之前版本那样简单地翻倍迁移过程中,如果链表长度小于等于 8,将保持链表结构,否则将链表转换为红黑树
数组的初始化方式:在 Java 8 中,
HashMap
的桶数组采用的是懒初始化方式,即在第一次添加元素时才会创建桶数组
ConcurrentHashMap的加锁力度
Java 8 之前的 ConcurrentHashMap
:
Segment
级别的锁:Java 7的
ConcurrentHashMap
使用分段锁机制,即将整个哈希表分成多个段,每个段有一个独立的锁每个线程在对哈希表进行操作时,只需要锁住对应的段,而不是整个哈希表
锁的粒度相对较大:尽管采用了分段锁,但是在某些高并发场景下,仍可能出现较大的锁竞争,因为每个线程在进行操作时需要锁住整个段
Java 8 及以后的 ConcurrentHashMap
基于 CAS 操作的改进
Java 8 引入了一种全新的实现方式,使用了 CAS(Compare and Swap)操作,将分段锁替换为了一种更细粒度的锁结构
取消了
Segment
,而是使用更轻量级的Node、TreeBin等数据结构
Node 和 TreeBin 的引入
引入了
Node
和TreeBin
等数据结构,使得ConcurrentHashMap
能够更灵活地适应不同的并发情境当哈希冲突较少时,使用
Node
,而在冲突较多时,会将链表转换为红黑树(TreeBin)以提高查询效率
更细粒度的锁:基于 CAS 操作的改进使得锁的粒度更小,进一步减小了锁的竞争范围,提高了并发性能
改进的扩容机制:在扩容时,Java 8 及之后版本的
ConcurrentHashMap
可以在进行扩容时,避免锁住整个哈希表,而是只锁住部分桶
TreeMap的有序性
TreeMap
是 Java 中的一种有序映射,它实现了 SortedMap
接口,而 SortedMap
接口扩展了 Map
接口,具有按键的自然顺序或者通过比较器进行排序的特性。因此 TreeMap
是有序的
红黑树结构:
TreeMap
内部采用红黑树(Red-Black Tree)的数据结构。红黑树是一种自平衡二叉搜索树,确保了元素的有序性自然顺序或比较器:在构造
TreeMap
对象时,可以选择使用自然顺序(元素必须实现Comparable
接口)或者指定一个比较器(实现Comparator
接口)按键的比较:
TreeMap
通过比较键的顺序来维护元素的有序性。如果使用自然顺序,则元素的键必须是Comparable
类型;如果使用比较器,则根据比较器的规则进行排序遍历顺序:当你遍历
TreeMap
的键时,它们将按照升序顺序排列。这是因为红黑树的特性确保了每个节点的左子树包含的键都小于当前节点的键,而右子树包含的键都大于当前节点的键
import java.util.TreeMap;
public class TreeMapExample {
public static void main(String[] args) {
// 创建一个 TreeMap
TreeMap<Integer, String> treeMap = new TreeMap<>();
// 插入元素
treeMap.put(3, "Three");
treeMap.put(1, "One");
treeMap.put(2, "Two");
// 遍历 TreeMap,按键升序
for (Integer key : treeMap.keySet()) {
System.out.println(key + ": " + treeMap.get(key));
}
}
}
在这个示例中,插入了三个键值对,但在遍历时,它们会按照键的升序顺序输出,即 1、2、3。这反映了 TreeMap
的有序性
开发中为什么要用到接口
抽象和实现分离:接口提供了一种将抽象与实现分离的机制。通过定义接口,可以明确规定类应该提供哪些方法,但无需实现这些方法的具体细节。这有助于降低代码的耦合度,使系统更易于维护和扩展
多态性:接口允许通过多态性实现代码的灵活性。通过接口,可以在不考虑实际实现的情况下,使用不同类的对象,提高代码的灵活性和可复用性
实现多继承:接口支持多继承,一个类可以实现多个接口。这种机制在需要继承多个不同功能的情况下非常有用,避免了 Java 中单继承的限制
接口的动态代理:接口可以与动态代理结合使用,实现一些横切关注点(cross-cutting concerns),如事务管理、日志记录等。这种机制使得代码更易于维护和扩展
约定编程:接口提供了一种约定编程的机制,定义了类应该实现的方法,使得开发者在设计和实现类时有一个明确的契约。这有助于提高代码的一致性和可读性
代码组织和模块化:接口有助于代码的组织和模块化。通过将相关的方法定义在接口中,可以更清晰地组织代码结构,提高系统的可维护性
抽象类和接口的区别
继承
抽象类:使用关键字
abstract
定义的类,可以包含抽象方法和具体方法。一个类只能继承一个抽象类接口:使用关键字
interface
定义的接口,只包含抽象方法和常量。一个类可以实现多个接口
构造函数
抽象类:可以有构造函数,且构造函数可以有参数和方法体
接口:不能有构造函数,因为接口不能被实例化
字段
抽象类:可以有实例变量
接口:只能包含
public static final
类型的变量,即常量
方法实现
抽象类:可以有抽象方法和具体方法。子类继承抽象类时,可以选择性地覆盖(实现)抽象方法,也可以继续使用具体方法
接口:只能包含抽象方法,实现接口的类必须提供对所有抽象方法的实现
访问修饰符
抽象类:可以有访问修饰符,可以是
public
、protected
、default
或private
接口:默认是
public
,且不允许使用其他修饰符
多线程
线程池的核心参数及结构
ThreadPoolExecutor:
corePoolSize(核心线程数)
corePoolSize
是线程池中的核心线程数,它表示在没有任务需要执行时,线程池保持的最小线程数量。这些线程会一直存在,即使它们是空闲的核心线程在执行任务时不会被回收,除非设置了允许核心线程超时的情况(allowCoreThreadTimeOut)
maximumPoolSize(最大线程数)
maximumPoolSize
表示线程池中允许的最大线程数量。当队列满了且核心线程数已满时,线程池会创建新的线程,但不会超过最大线程数超过核心线程数的线程在任务执行完成后会被回收,以保持线程数不超过最小和最大线程数之间
queue(等待队列)
任务等待队列用于存储已经提交但尚未执行的任务。
ThreadPoolExecutor
提供了多种类型的队列,如ArrayBlockingQueue
、LinkedBlockingQueue
等任务在队列中等待执行,直到线程池的线程可用。当任务数超过核心线程数时,多余的任务将放入等待队列
keepAliveTime(线程存活时间)
keepAliveTime
表示空闲线程的最大存活时间。当线程池中的线程数量超过核心线程数,并且有空闲线程时,这些空闲线程会根据存活时间进行回收回收的条件是,线程在
keepAliveTime
时间内没有被使用,可以通过allowCoreThreadTimeOut
参数来控制是否对核心线程应用相同的规则
handler(拒绝策略)
handler
定义了当线程池和等待队列都满了,无法继续接受新任务时的处理策略一些常见地拒绝策略有:
AbortPolicy:默认策略,直接抛出
RejectedExecutionException
异常CallerRunsPolicy:由调用线程处理该任务
DiscardPolicy:直接丢弃任务,不抛出异常
DiscardOldestPolicy:丢弃等待队列中最旧的任务,然后尝试重新提交新任务
Synchronized和ReentrantLock
关键字
Synchronized
是 Java 内置的关键字,提供了隐式的锁机制。当一个线程进入一个被Synchronized
修饰的代码块时,它会自动获得锁,执行完代码块后会释放锁ReentrantLock
是一个基于java.util.concurrent
包的显式锁,提供了更灵活地锁定机制。它允许可重入性,即一个线程可以多次获取同一把锁
等待可不可以中断
Synchronized
是没有办法去设置锁的一个超时等待时间ReentrantLock
可以控制锁的等待时间
锁的获取方式
Synchronized
锁是悲观锁,它在获取锁的过程中可能会阻塞ReentrantLock
提供了更灵活的获取锁的方式,包括可中断、可超时、非阻塞的获取锁
条件等待
ReentrantLock
提供了Condition
对象,允许线程在特定条件下等待或唤醒其他线程。这在一些复杂的同步场景中很有用
CopyOnWrite容器
CopyOnWrite
容器是 Java 并发包中的一种并发集合,用于在多线程环境下提供安全的读操作,同时也提供一定程度的写操作支持。它的主要特点是在对容器进行修改(添加、修改、删除元素)时,会创建一个新的容器的拷贝,而不是直接在原有容器上进行操作。这种机制保证了在读取操作时不需要加锁,从而提高了读取的性能,但写操作仍然是相对较慢的
如果 CopyOnWrite
容器在并发的时候发生了两次写操作,难道是开两个对应的写副本吗?
对应的写副本是会被加锁的,如果 CopyOnWrite
容器发生了并发写,在同一时间只有一个写操作需要等待另外一个写操作完成之后才可以进入下一次写的过程。因此 CopyOnWrite
容器的写操作本质上是串行的,而读操作是可以做并行的
Volatile关键字
Volatile
是 Java 中的关键字,用于修饰实例变量。它的主要作用是保证变量的可见性和禁止指令重排序。Volatile
关键字解决了多线程环境下某些问题,特别是在一个线程写入数据,而另一个线程读取数据的情况下
我们经常会有一些单例的书写,在单例书写的多线程版本当中,我们是需要确保对应的 static
变量上是加上了 Volatile
关键字的。为什么需要 Volatile
关键字?
本质上是因为线程所读取的变量是处于内存当中的,而做变量的写操作是会把内存当中的数据发送到 CPU 的指令寄存器上,在指令寄存器上去做写操作,写完之后再同步会对应的内存当中。那每个线程读取变量的一个栈地址来说的话,内存的区域是不同的。因此如果说变量不设置 Volatile
关键字,每个线程看到的内存区域往往是属于自己栈空间的副本,没有办法在多线程环境下一个写操作立马同步到另外一个线程可见的状态。因此多线程变量的修改是需要加 Volatile
关键字的
同时,我们对应的 Volatile
关键字也可以去防止指令重排。来确保我们读操作在代码维度必定发生在写操作之后去做流程的控制
多线程中的异常处理
捕获异常:在多线程代码中使用
try-catch
块捕获异常,确保异常不会跨越线程边界,即每个线程独立处理自己的异常try { // 多线程代码 } catch (Exception e) { // 异常处理 }
日志记录:使用合适的日志框架记录异常信息,这有助于更好地理解问题和排查错误
try { // 多线程代码 } catch (Exception e) { logger.error("Exception in thread: " + Thread.currentThread().getName(), e); }
线程组:在创建线程时,可以将线程加入一个线程组。线程组可以帮助收集线程的未捕获异常
ThreadGroup threadGroup = new ThreadGroup("MyThreadGroup"); Thread myThread = new Thread(threadGroup, () -> { // 多线程代码 });
使用线程异常处理器(UncaughtExceptionHandler):Java 提供了 UncaughtExceptionHandler 接口,可以用来处理那些未被捕获的异常。你可以给所有线程设置统一的异常处理器,也可以给每个线程设置特定的异常处理器
Thread myThread = new Thread(() -> { // 多线程代码 }); myThread.setUncaughtExceptionHandler((thread, throwable) -> { // 自定义异常处理逻辑 });
Future
和Callable
: 如果你使用Future
和Callable
进行多线程编程,可以通过Future
的get
方法捕获异常ExecutorService executorService = Executors.newFixedThreadPool(5); Future<?> future = executorService.submit(() -> { // 多线程代码 }); try { future.get(); // 获取结果,此处会抛出异常 } catch (Exception e) { // 处理异常 }
IO
BIO(Blocking I/O),NIO(Non-blocking I/O),AIO(Asynchronous I/O)是关于 I/O 模型的概念,它们描述了不同的方式来处理输入和输出操作。这些模型在网络编程中经常被提及,特别是在 Java 中,它们对应于不同的 Java I/O(java.io)和 NIO(java.nio)包的特性
BIO
阻塞 I/O 模型:在传统的阻塞 I/O 模型中,每个连接都需要独立的线程来处理,当有大量连接时,会导致线程数量的急剧增加,严重影响系统性能
工作原理:当一个线程在进行 I/O 操作时(例如读取数据),该线程会阻塞,直到数据准备好,或者超时,或者发生其他可使操作继续的事件。期间,该线程不能执行其他任务
Java 示例:Socket
和 ServerSocket
是典型的阻塞 I/O 类
// 服务器端
ServerSocket serverSocket = new ServerSocket(8080);
Socket clientSocket = serverSocket.accept(); // 阻塞等待客户端连接
// 客户端
Socket socket = new Socket("localhost", 8080);
InputStream inputStream = socket.getInputStream();
int data = inputStream.read(); // 阻塞等待数据
NIO
非阻塞 I/O 模型:NIO 引入了选择器(Selector)和通道(Channel)的概念,使得一个线程可以处理多个连接。相对于 BIO,NIO 更为灵活,适用于高并发的场景
工作原理:一个线程通过选择器监视多个通道的 I/O 事件,当某个通道有数据可读/可写时,线程会处理该通道,而不是等待
Java 示例:Selector
、ServerSocketChannel
、SocketChannel
是典型的 NIO 类
// 服务器端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 客户端
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080));
socketChannel.configureBlocking(false);
selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_READ);
AIO
异步 I/O 模型:AIO 引入了异步通道的概念,使得一个线程在处理多个连接时,无需等待 I/O 操作完成,而是通过回调方式得知操作的完成状态
工作原理:当一个异步操作开始时,线程不会等待其完成,而是继续执行其他操作。当操作完成时,系统会通知线程,或者调用注册的回调函数
Java 示例:AsynchronousServerSocketChannel
和 AsynchronousSocketChannel
是典型的 AIO 类
// 服务器端
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel result, Void attachment) {
// 处理连接成功
}
@Override
public void failed(Throwable exc, Void attachment) {
// 处理连接失败
}
});
// 客户端
AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080), null, new CompletionHandler<Void, Void>() {
@Override
public void completed(Void result, Void attachment) {
// 处理连接成功
}
@Override
public void failed(Throwable exc, Void attachment) {
// 处理连接失败
}
});
Java 中级
交易一致性问题
重复支付
支付系统因为各种原因重复回调
考察点:幂等方法
![](https://img.sherry4869.com/blog/it/java/interview/img.png)
伪防重操作:
begin transaction
1. select * from order_info where id = "20201020"
2. java 代码判断 status == '初始' 执行 3 否则直接返回 rollback
3. update order_info set status = '成功' where id = "20201020"
commit
防重幂等操作:
悲观锁
begin transaction
1. select * from order_info where id = "20201020" for update 加 record lock
2. java 代码判断 status == '初始' 执行 3 否则直接返回 rollback
3. update order_info set status = '成功' where id = "20201020"
commit
乐观锁
begin transaction
1. select * from order_info where id = "20201020"
2. java 代码判断 status == '初始' 执行 3 否则直接返回 rollback
3. update order_info set status = '成功' where id = "20201020" and status == '初始' 判断影响行数
commit
超时退问题
如何回滚内容
回滚失败如何解决
重复回滚如何预防
考察点:分布式事务、流水号应用、重试方法
分布式原理
CAP
CAP 定理(CAP theorem)又被称作布鲁尔定理(Brewer's theorem),是加州大学伯克利分校的计算机科学家埃里克·布鲁尔(Eric Brewer)在 2000 年的 ACM PODC 上提出的一个猜想。对于设计分布式系统的架构师来说,CAP 是必须掌握的理论
在一个分布式系统中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲
C 一致性(Consistency):等同于所有节点访问同一份最新的数据副本。在某个写操作完成之后的任何读操作都必须返回该写操作写入的值,或者再之后的写操作写入的值。即各个数据备份的数据内容要保持一致且都为最新数据,强调的是数据正确
A 可用性(Availability):等同于每次请求都能获取到非错误的响应,但是不保证获取的数据为最新数据。非故障的节点(不能宕机)在合理的时间内返回合理地响应(不是错误和超时的响应)。对访问本系统的客户的一种承诺:我一定会给你返回数据,但不保证数据最新,强调的是不出错
P 分区容忍性(Partition Tolerance):等同于系统在网络分区或消息丢失时,仍能继续运行。由于分布式系统通过网络进行通信,网络是不可靠的。当任意数量的消息丢失或延迟到达时,系统仍会继续提供服务,不会挂掉。对访问本系统的客户端的一种承诺:系统会一直运行,哪怕是给用户返回一个响应超时的错误提示信息,或者是旧的数据。强调的是不挂掉
如果从服务器设置为只读模式,那么它在复制完数据之前是不能被写入的,但是可以被读取。如果从服务器设置为读写模式,那么它在复制完数据之前既可以被写入也可以被读取,但是可能会造成数据不一致的问题
网络分区或故障会导致分布式系统中的节点之间无法通信或通信延迟,这就会影响系统的一致性和可用性。例如:如果一个节点向 CA 申请证书,但是网络分区导致 CA 无法收到申请或者无法及时回复,那么这个节点就无法获取证书,也就无法访问其他服务。这就降低了系统的可用性;另一方面,如果一个节点向 CA 申请撤销证书,但是网络分区导致其他节点无法及时更新证书状态,那么这些节点就可能接收到已经失效的证书,这就破坏了系统数据的一致性
假设有一个网上银行系统,它使用 CA 架构来保证用户和服务器之间的安全通信。用户需要向 CA 申请数字证书,然后用这个证书来登录银行网站进行转账、查询等操作。如果发生网络分区或故障,可能会出现以下影响系统的一致性和可用性的情况:
用户无法向 CA 申请或更新证书,导致无法登录或操作
CA 无法向用户或服务器发送证书撤销列表(CRL),导致无法及时识别已经失效的证书
用户或服务器收到了过期或被篡改的证书,导致信息泄露或被攻击
总结:分区容忍性(P)是分布式系统的基本要求,因为网络分区是一种不可避免的错误,如果不考虑分区容忍性(P),那么 CA 架构的系统就相当于传统的单机数据库,无法实现数据的水平扩展
CP 架构是指在 CAP 架构中,选择保证一致性和分区容错性的系统。这种系统牺牲了可用性,即在网络分区或故障的情况下,可能无法对外提供服务。CP 架构适合对数据一致性要求高的场景:例如银行转账、电商订单等
一个典型的 CP 架构的例子是 Zookeeper,它作为微服务注册中心时,保证了数据的一致性,但是在网络故障时可能无法正常工作
如下图所示,为了保证数据一致性,当发生网络分区现象后,N1 节点上的数据已经更新到 y,但由于 N1 和 N2 之间的复制通道中断,数据 y 无法同步到 N2,N2 节点上的数据还是 x。这时客户端 C 访问 N2 时,N2 需要返回 Error,提示客户端 C:“系统故障中”,如果要用户访问系统时就绝对要返回一致的数据话,只能给用户返回错误信息,这种处理方式违背了可用性的要求,因此 CAP 三者只能满足 CP
![](https://img.sherry4869.com/blog/it/java/intermediate/sharding-sphere/architecture/img_2.png)
AP 架构是指在分布式系统中,优先保证可用性和分区容错性,而牺牲一致性的架构。AP 架构适合那些对数据一致性要求不高,但对服务可用性要求高的场景,例如社交网络等。AP 架构的优点是可以提高系统的可扩展性、容错性和响应速度,缺点是可能导致数据不一致或丢失
AP 架构的例子是 Eureka,它是一个基于 REST 的服务注册和发现组件,用于构建微服务架构。Eureka 在设计时就放弃了强一致性,而选择了最终一致性,这样可以提高系统的可用性和容错性。Eureka 还提供了自我保护机制,当网络分区发生时,它不会剔除注册表中的服务实例,而是保持当前状态,以防止误删
如下图所示,为了保证可用性,当发生分区现象后,N1 节点上的数据已经更新到 y,但由于 N1 和 N2 之间的复制通道中断,数据 y 无法同步到 N2,N2 节点上的数据还是 x。这时客户端 C 访问 N2 时,N2 将当前自己拥有的数据 x 返回给客户端 C 了,而实际上当前最新的数据已经是 y 了,这就不满足一致性的要求了,因此 CAP 三者只能满足 AP。注意:这里 N2 节点返回 x,虽然不是一个“正确”的结果,但是一个“合理”的结果,因为 x 是旧的数据,并不是一个错乱的值,只是不是最新的数据而已
![](https://img.sherry4869.com/blog/it/java/intermediate/sharding-sphere/architecture/img_3.png)
Base
BASE 理论是由 eBay 工程师提出,是对可用性和一致性的权衡。BASE 是由 Basically Available(基本可用),Soft State(软状态),和 Eventually Consistent(最终一致性)三个短语的缩写。BASE 是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于 CAP 定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)
举例来说,电商网站的购物车功能就可以使用 BASE 理论来实现。用户在不同的设备上添加或删除购物车中的商品时,可能会看到不同的结果,但这并不影响用户的基本使用体验。购物车中的数据会在后台进行异步同步,最终保证用户在结算时能够看到正确的商品信息
基本可用(Basically Available):指的是在分布式系统出现故障或者网络延迟时,系统仍然可以提供基本的服务功能,但是可能会降低部分性能或者舍弃部分非核心功能。例如,在电商网站中,如果出现了服务器故障或者网络拥塞,那么用户仍然可以浏览商品和下单购买,但是可能会无法查看评论或者推荐等非核心功能
软状态(Soft state):指的是在分布式系统中允许数据存在中间状态,并且该中间状态不会影响系统整体可用性。例如在电商网站中,如果用户修改了收货地址或者取消了订单,那么这些数据可能会在不同的副本之间存在不一致的情况(即软状态),但是这些不一致并不会影响用户继续使用网站的其他功能。再例如微信发朋友圈,只有我自己看到,其余人隔了 1 分钟后才看到
最终一致性(Eventually consistent):指的是在分布式系统中,在经过一段时间或者经过一定条件后,所有副本之间能够达到数据一致的状态。例如,在电商网站中,如果用户修改了收货地址或者取消了订单(即软状态),那么这些数据最终会通过某种机制(如异步通知、定时同步等)使得所有副本都更新为最新的值(即最终一致性)
分布式事务
分布式事务会采取以下 4 中不同的类型来确保事务的最终一致性
二阶段提交:保证了强一致性和分区容错性
异步确保型:保证了可用性和分区容错性,最终一致性
事务型消息:保证了可用性和分区容错性,最终一致性
TCC 型:保证了可用性和分区容错性,最终一致性
强一致性保证
强一致性是指分布式系统中所有节点在同一时间看到的数据是相同的,也就是说,更新操作成功并返回客户端后,所有节点的数据完全一致。这种一致性模式要求复制是同步的,即主库在写入成功后需要等待从库的确认,确保从库已经收到写入操作
强一致性可以保证数据的准确性和可靠性,但是会降低系统的可用性和吞吐量,因为每次写入操作都需要等待所有节点的响应。强一致性适合对数据敏感度高、实时性要求强的场景,例如银行转账、电商支付等
两阶段提交、三阶段提交和 Paxos 算法都是分布式系统中实现强一致性的协议,它们的主要区别如下:
两阶段提交(2PC)的流程分为两个阶段:第一阶段是协调者向所有参与者发送事务内容,并等待参与者的响应;第二阶段是协调者根据参与者的反馈决定是否执行事务提交,并通知所有参与者
一个例子是:假设有一个分布式系统,其中有一个协调者节点 C 和三个参与者节点 A、B、D 要执行一个转账事务。那么 2PC 的流程如下:
第一阶段:C 向 A、B、D 发送转账事务的内容,并询问它们是否可以执行该事务。A、B、D 在收到请求后,各自执行转账操作,并将结果返回给 C
第二阶段:C 在收到所有节点的响应后,如果都是成功的,则向 A、B、D 发送提交消息,让它们正式提交转账操作;如果有任何一个失败的,则向 A、B、D 发送回滚消息,让它们撤销转账操作
优点:简单易实现,保证了强一致性
缺点:同步阻塞,性能低;单点故障,协调者挂了会导致参与者处于不确定状态;网络分区时无法保证一致性
场景:适用于对数据一致性要求高、事务执行时间短、参与者数量少的场合
三阶段提交(3PC)的流程分为三个阶段:第一阶段是协调者向所有参与者发送事务内容,并等待参与者的响应;第二阶段是协调者在收到所有参与者的同意后,再次向所有参与者发送预提交消息,并等待参与者的确认;第三阶段是协调者根据参与者的反馈决定是否执行事务提交,并通知所有参与者
一个例子是:假设有一个分布式系统,其中有一个协调者节点 C 和三个参与者节点 A、B、D 要执行一个转账事务。那么 3PC 的流程如下:
第一阶段:C 向 A、B、D 发送转账事务的内容,并询问它们是否可以执行该事务。A、B、D 在收到请求后,各自准备好转账操作,并将结果返回给 C
第二阶段:C 在收到所有节点的同意后,向 A、B、D 发送预提交消息,并等待它们的确认。A、B、D 在收到预提交消息后,锁定资源并准备提交或回滚操作,并将确认结果返回给 C
第三阶段:C 在收到所有节点的确认后,如果都是成功的,则向 A、B、D 发送提交消息,让它们正式提交转账操作;如果有任何一个失败或超时的,则向 A、B、D 发送回滚消息,让它们撤销转账操作
优点:相比 2PC,减少了同步阻塞时间,提高了性能;避免了单点故障导致的不确定状态
缺点:仍然存在网络分区时无法保证一致性的问题
场景:适用于对数据一致性要求高、事务执行时间短、参与者数量少且网络稳定的场合
Paxos 算法是一种基于消息传递和投票机制来达成共识(即选择某个值) 的分布式一致性算法。可以在存在部分节点故障或消息丢失的情况下达成一致。Paxos 算法涉及三种角色:提议人(Proposer)、接受人(Acceptor)和学习人(Learner)。Paxos 算法可以分为两个过程:Prepare 过程和 Accept 过程
一个例子是:在某个分布式系统中有五个节点 P1-P5,在这些节点中要选出领导人(Leader)。那么 Paxos 算法可以描述如下:
Prepare 过程
P1 作为提议人发起一个编号为 N 的提案,向其他节点发送 Prepare(N) 消息,请求它们承认自己是领导人
其他节点收到 Prepare(N) 消息后,如果 N 大于它们之前看到的任何提案编号,则回复 Promise(N),表示承诺不再接受任何编号小于 N 的提案;否则,拒绝回复
P1 收到多数节点的 Promise(N) 消息后,进入 Accept 过程;否则放弃或重新发起一个新的提案
Accept 过程
P1 向所有承诺了自己的节点发送 Accept(N,V) 消息,其中 V 是自己作为领导人的值
其他节点收到 Accept(N,V) 消息后,如果没有承诺过比 N 更大的提案编号,则回复 Accepted(N,V),表示接受了该提案;否则,拒绝回复
P1 收到多数节点的 Accepted(N,V) 消息后,完成共识,并通知所有学习者该值;否则放弃或重新发起一个新的提案
如果有多个提议人,那么可能会出现冲突的情况,即不同的提议人发起了不同编号或者不同值的提案。Paxos 算法通过以下机制来解决这个问题:
提议人在发起提案时,必须保证每次使用的编号都是唯一且递增的
接受人在回复 Promise 或 Accepted 消息时,必须遵守以下规则:
如果收到了编号更大的 Prepare 消息,那么拒绝回复之前承诺过的任何编号更小的 Accept 消息
如果收到了编号相同但值不同的 Accept 消息,那么只接受第一个收到的 Accept 消息
提议人在收到多数节点的 Promise 消息后,必须选择其中最大编号且已经被接受过的值作为自己提案的值;如果没有这样的值则可以任意选择一个值
优点:容错性强,可以在任意多数节点正常工作时保证一致性;可扩展性好,可以动态增加或删除节点
缺点:复杂难懂,实现困难;效率低,在网络延迟大或消息冲突多时需要多次重试才能达成共识
场景:适用于对数据一致性要求高、事务执行时间长、参与者数量多且网络不稳定的场合
弱一致性
弱一致性是指系统中的某个数据被更新后,后续对该数据的读取操作可能得到更新后的值,也可能是更新前的值。弱一致性根据不同的业务场景,又可以分解为更细分的模型,不同一致性模型又有不同的应用场景。以下是一些常见的弱一致性细分模型
指的是一个进程从一个副本中写入数据后,再从同一个副本中读取数据,必须能够读到刚才写入的数据。它保证了一个用户在提交了一个更新后,可以立刻读到自己的更新。它不会对其他用户的写入做出承诺,其他用户的更新可能稍等才会看到
例如在社交网络中,当你修改了个人资料或发表了动态后,你希望能够立即看到自己做出的改变。但其他人可能需要等待几秒或几分钟才能看到你更新后的信息
还有一个关于数据库中使用读写一致性的例子是 MySQL 的主从复制。在这种架构中,主数据库负责写入操作,从数据库负责读取操作。主数据库会将写入的数据同步到从数据库,从而保证数据的一致性。但是如果用户在主数据库上更新了数据,然后立刻去从数据库上读取,可能会发现数据还没有同步过来。这就是弱一致性的问题
为了解决这个问题,可以使用读写一致性的模型,让用户在提交了更新后,可以立刻从主数据库上读取自己的更新。这样就保证了用户自己视角的数据一致性。读写一致性的缺点是会影响系统性能和资源消耗,因为每次写入都需要通知所有节点或者中间件来保证读取最新数据
读写一致性的两种常见方案:
一种是让所有请求(读写)都走一遍日志复制(Log replication)这样可以保证所有节点都有相同的数据,但是会降低系统性能和可用性
另一种是使用 ReadIndex 的方法。ReadIndex 是一种实现线性一致读的机制,它的原理是这样的:
当主数据库收到一个读请求时,它会记录当前已提交的索引为 ReadIndex,并将这个 ReadIndex 发送给从数据库
从数据库收到 ReadIndex 后,会回复主数据库自己是否已经应用了这个索引
主数据库等待多数从数据库的回复,如果都已经应用了 ReadIndex ,那么主数据库就认为自己还是有效的主数据库,并且可以处理读请求
主数据库等待自己的应用索引达到 ReadIndex ,然后从状态机中读取最新数据,并返回给客户端
这样就保证了客户端可以读到最新的数据,并且不会受到主从切换的影响。这种模型可以保证每个副本都有最新的数据,但是也会增加网络通信和同步的开销,适用于对实时性要求高,需要保持用户自己视角的数据一致性的场景,以及实时性要求较高的场景,如电商库存、订单
单调读的具体使用场景是指在分布式系统中,需要保证同一个用户或进程在多次读取数据时,不会遇到时光倒流的情况
一个例子是邮件系统,当用户收到了一封邮件后,就不能再看到这封邮件之前收到的邮件
如果一个用户先从一个副本中读取了某个值,然后又从另一个副本中读取了同一个值,那么他应该看到相同或更新的值,而不是更旧的值。这样可以保证用户看到的数据是单调递增的,不会出现逻辑上的错误或混乱。例如,在电商网站中,当你浏览了某个商品后,你希望在下次浏览时能够看到相同或更高的价格;但如果价格降低了,你可能会感觉被欺骗了
实现单调读一致性的一种方式是确保每个客户端(或进程)都只从一个副本读取数据,这样就可以避免读到不同副本上的不同版本的数据。另一种方式是在每次写入数据时,都附上一个时间戳或者递增的序号,然后在每次读取数据时都检查这个标识符是否比之前读到得更大,如果不是则拒绝接受这个数据
单调读的优点是保证了数据的顺序和逻辑性,缺点是需要记录每个节点的读取历史和版本号
这种模型可以保证每个进程看到的数据是单调递增的,但是也会导致不同进程看到不同版本的数据,适用于对顺序要求高的场景,比如消息队列、日志等。这样可以创建很多从库,并将读请求分散到所有的从库上去,减小主库的负载,并允许向最近的节点发送读请求。但是这只适用于异步复制,如果尝试同步复制,则单个节点故障将使整个系统无法写入
单调写一致性是一种以客户端为中心的一致性模型,它要求同一个客户端(或进程)的写操作在所有副本上都以同样的顺序执行,即保证客户端的写操作是串行的
举例来说,如果一个用户在游戏中杀死了两个怪物,分别获得了 100 和 200 经验值,那么他在任何副本上查看自己的经验值时,都应该是 300 或者更高。如果不满足单调写一致性,有可能出现用户在某个副本上看到自己的经验值只有 100 或者 200 ,这会影响用户的体验和信任
实现单调写一致性的一种方式是确保每个客户端(或进程)都有一个唯一的标识符,例如时间戳或者递增的序号,然后在每次写入数据时都附上这个标识符。这样所有副本就可以根据这个标识符来判断数据的更新顺序,并且拒绝接受比当前版本更旧的数据
单调写的优点是可以避免时光倒流的现象,即先前写入较新的数据,后续读取不会得到更旧的数据。单调写也可以保证 FIFO 一致性,即先发生的写操作先被其他客户端看到。缺点是可能会降低系统的吞吐量和可用性,因为需要在多个副本之间同步数据和顺序。单调写也不能保证强一致性或因果一致性,即不同客户端之间可能看到不同版本或顺序的数据
单调写一致性适用于那些对数据更新顺序敏感,但不需要实时反馈给其他用户的场景。例如在社交网络中,用户修改了自己的个人信息或者发布了新的动态,他希望能够立即看到自己的修改或者发布结果,但不介意其他用户稍后才能看到
会话一致性是一种分布式系统的一致性模型,它保证同一个会话内,后面的请求一定能够看到此前更新所产生版本的数据或者比这个版本更新的数据。也就是说执行更新操作之后,客户端能够在同一个会话中始终读取到该数据项的最新值
会话一致性适用于需要保证“读己之所写”的场景,例如在线购物、社交网络、电子邮件等这些场景中,客户端对自己刚刚执行的操作有实时反馈的需求,但不要求其他客户端也能立即看到最新数据
会话一致性可以通过以下几种方式实现:
使用客户端存储方式:Cookie、JWT 等来做登录状态验证
使用公共 Session 存储服务:Redis 或者 Memcached 等存储 Session
使用 nginx-sticky-module 来保持会话
使用会话令牌来跟踪数据版本
指的是如果两个操作之间存在因果关系(例如先写后读),那么所有进程都必须按照因果关系来观察它们;如果两个操作之间没有因果关系(例如并发写),那么所有进程可以以任意顺序来观察它们
因果一致性是指捕获系统中操作之间的因果关系的模型,并保证每个过程可以以共同的因果顺序观察那些因果相关的操作。也就是说,如果一个操作 A 在因果上先于另一个操作 B ,那么所有节点都应该按照 A -> B 的顺序看到这两个操作。但如果两个操作没有因果关系,也就是并发的,那么不同节点可以看到不同的顺序
举个例子,如果用户 A 在社交网络上发了一条动态,然后用户 B 对这条动态进行了评论。那么这两个操作就有因果关系,用户 A 的动态必须先于用户 B 的评论被其他用户看到。但如果用户 C 和用户 D 同时对这条动态进行了评论,那么这两个评论就没有因果关系,不同用户可以看到不同的评论顺序
因果一致性的实现方法是在每个数据对象上附加一个版本向量,记录该对象被哪些节点修改过,以及每个节点对该对象的最新版本号。这样就可以根据因果关系来判断哪些数据更新应该被可见,哪些应该被隐藏
这种模型可以保证有依赖关系的操作被正确执行,但是也会牺牲部分并发性能,适用于对逻辑正确性要求高,不需要全局写入顺序,但需要保持用户之间交互的顺序的场景,比如社交网络,论坛等
MySQL
varchar(100)
能存储多少个中文
对于 MySQL 5.0 以下的版本,varchar(100)
是以字节为单位的。如果使用 UTF-8 编码,每个中文字符通常占用 3 个字节,所以 varchar(100)
大约可以存储 33 个中文字符。但是,这可能会根据您的具体编码设置而变化。例如,如果您使用的是 GBK 编码,那么每个中文字符可能只占用 2 个字节。在这种情况下,varchar(100)
可以存储 50 个中文字符
对于 MySQL 5.0 及以上的版本,varchar(n)
表示 n 个字符,无论是汉字还是英文,MySQL 都能存入 n 个字符。这是因为在这些版本中,varchar
的长度是以字符为单位的,而不是字节。所以,如果你使用的是 UTF-8 编码,varchar(100)
可以存储 100 个中文字符
varchar
的最大长度是多少
在 MySQL 中,一行数据的最大长度是 65535 个字节。这个长度是所有列共享的,也就是说,所有列的长度加起来不能超过 65535 个字节。因此,虽然理论上 varchar
类型的最大长度可以达到 65535 个字节,但实际上,由于需要考虑到其他列的长度,以及每个列的元数据(如是否为空,长度等)所占用的空间,所以每个 varchar
字段的长度不可能达到 65535 个字节
索引
主键索引
定义主键
- 主键是一个唯一标识表中每一行的列或列的组合。在 MySQL 中,可以在表的创建时或之后使用
ALTER TABLE
语句添加主键
CREATE TABLE example ( id INT PRIMARY KEY, name VARCHAR(50) );
上面的例子中,
id
列被定义为主键- 主键是一个唯一标识表中每一行的列或列的组合。在 MySQL 中,可以在表的创建时或之后使用
主键索引的特点
主键索引是一种唯一性索引,确保表中的每个主键值都是唯一的
主键索引默认为聚簇索引,这意味着数据行的物理存储顺序与主键的逻辑顺序一致
性能优势
快速查找:由于主键是唯一的,通过主键索引可以快速准确定位一行数据
聚簇索引:主键索引的聚簇特性意味着相邻的主键值在物理上也是相邻存储的,从而提高了范围查询和范围扫描的性能
主键的选择
主键通常选择具有唯一性且不会频繁变更的列,例如自增长的整数列
可以使用多列作为主键,这称为复合主键
注意事项
主键列不允许有 NULL 值
主键索引对于频繁地插入和更新操作可能会影响性能,因为插入和更新时需要维护索引结构
网络编程
HTTP 和 HTTPS 的区别
HTTP(超文本传输协议)和HTTPS(安全超文本传输协议)是用于在网络上传输数据的两种不同协议。以下是它们的主要区别:
安全性
HTTP:是一种不安全的协议,数据在传输过程中是未加密的。这意味着敏感信息可能被中间人攻击截取或窃听
HTTPS:通过使用 TLS/SSL 协议对数据进行加密,提供了更高的安全性。这使得数据在传输过程中变得更难被窃听或篡改
端口
HTTP:默认使用端口 80
HTTPS:默认使用端口 443
证书
HTTP:不需要使用证书
HTTPS:需要在服务器端配置 SSL 证书,用于验证服务器的身份。这通常是由可信任的第三方证书颁发机构(CA)签发的
搜索引擎排名
HTTP:在搜索引擎排名中可能会受到一定的影响,因为搜索引擎更倾向于显示安全的网站
HTTPS:被认为更安全,有助于提高搜索引擎排名