4 minute read

八股

简单记录一下

10.28

  • java中只有值传递
    • 传递的即使是对象,实际上也是一份拷贝,能交换数组内元素位置是因为引用参数拷贝了实际参数的地址,直接操作地址
  • 序列化:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式
  • 反序列化:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程
    • 序列化和反序列化发生在TCP/IP模型的应用层
  • 反射让我们可以在运行时分析类以及执行类中方法,是各种框架的灵魂
    • 优点:可以访问private修饰的方法和变量,运行时加载更灵活
    • 缺点:性能开销较大,破坏了OOP的思想

10.29

  • 乐观锁和悲观锁
    • 乐观锁倾向认为系统的共享资源很少被同时访问,只需要在提交时进行检查,无需加锁等待,但是在写冲突频繁发生时大量重写会导致cpu占用过大
    • 悲观锁倾向认为系统很有可能发生共享资源访问问题,所有共享资源在使用时都必须上锁,高并发下容易导致频繁切换上下文开销大,影响性能,以及引发死锁问题
    • 一般来说,乐观锁常用于多读少写场景,悲观锁用于多写少读场景
  • 乐观锁实现?
    • 版本号检查:在数据库中加入version字段,读出数据时也读出ver值,在写回时再取出ver值比较,如果ver值变化就拒绝写操作,防止值覆盖问题
    • CAS算法:在cpu中有一个原子操作,用于更新内存中变量的值,CAS每次更新时先获取内存中变量的值作为预期值,然后开始原子操作:比较当前内存中变量的值和预期值是否相等,是则更新,这样可以防止别的线程更改过变量值
  • Java中CAS的实现?
    • 调用Unsafe类中native关键字的本地方法,实现原子的操作
  • CAS的问题?
    • ABA问题:如果读取的时候值是A,在当前线程阻塞和等待时变量值先后被修改成B和A,当调度到当前线程时会认为值没有被修改过
      • 解决方法:增加时间戳或者版本号机制
    • 自旋操作开销大,更新失败时会循环重试,会造成cpu开销大
      • 解决方法:引入JVM的pause指令,更新失败就暂时把线程阻塞起来
    • 只对单个变量有效
      • 解决方法:JDK新增了检查引用对象值的方法
  • volatile关键字
    • 可见性:要求线程对该变量的读和写都需要在主存中进行,防止线程的本地内存造成读写问题
    • 有序性:在内存中加入屏障,禁止对变量读写语句进行排序优化

10.30

  • 线程池参数

    • 已经实现过简单的线程池就很好理解了,核心线程数,最大线程数,任务队列(阻塞队列实现),辅助线程TTL,拒绝策略
  • 常见的拒绝策略?

    • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理
    • ThreadPoolExecutor.CallerRunsPolicy: 调用执行被抛弃的任务的线程重新execute,如果原线程关闭就抛弃当前任务
    • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉
    • ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求
  • 线程池的原理?

    public class MiniThreadPool {
      
        public final BlockingDeque<Runnable> blockingDeque;
        private final int CORE_POOL_SIZE;
        private final int MAX_POOL_SIZE;
        private final long TIME_OUT;
        private final TimeUnit TIME_UNIT;
        private final RejectHandle rejectHandle;
      
        public MiniThreadPool(int corePoolSize, int maxPoolSize, long timeout, TimeUnit timeUnit,
                              BlockingDeque<Runnable> blockingDeque, RejectHandle rejectHandle) {
            this.CORE_POOL_SIZE = corePoolSize;
            this.MAX_POOL_SIZE = maxPoolSize;
            this.TIME_OUT = timeout;
            this.TIME_UNIT = timeUnit;
            this.blockingDeque = blockingDeque;
            this.rejectHandle = rejectHandle;
        }
      
        List<Thread> coreThreads = new ArrayList<>();
        List<Thread> supportThreads = new ArrayList<>();
      
        public void execute(Runnable command) {
            if(coreThreads.size() < CORE_POOL_SIZE) {
                Thread thread = new CoreThread();
                coreThreads.add(thread);
                thread.start();
            }
            if(blockingDeque.offer(command)) {
                return;
            }
            if(coreThreads.size() + supportThreads.size() < MAX_POOL_SIZE) {
                Thread thread = new SupportThread();
                supportThreads.add(thread);
                thread.start();
            }
            if(!blockingDeque.offer(command)) {
                rejectHandle.reject(command,this);
            }
        }
      
        class CoreThread extends Thread {
            @Override
            public void run() {
                while (true) {
                    try {
                        Runnable command = blockingDeque.take();
                        command.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
      
        class SupportThread extends Thread {
            @Override
            public void run() {
                while (true) {
                    try {
                        Runnable command = blockingDeque.poll(TIME_OUT, TIME_UNIT);
                        if(command == null) {
                            break;
                        }
                        command.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println("support thread done");
            }
        }
    }
    
    • 已经实现过了,我的理解是线程池的核心对象是待执行的命令(Runnable)和执行者(Thread),命令用阻塞队列存储,可以在队列为空时阻塞线程,避免空等导致cpu资源浪费;执行者分为核心线程和辅助线程,核心线程只要被创建就不会被销毁,属于常驻线程,辅助线程在当前线程数小于任务队列大小时被创建,当任务结束后在TTL到期时自动结束
  • 几个对比

    • Runnable vs Callable :Runnable不返回值和抛出异常,Callable可以
    • Execute vs Submit :execute() 方法用于提交不需要返回值的任务。通常用于执行 Runnable 任务,无法判断任务是否被线程池成功执行。submit() 方法用于提交需要返回值的任务。可以提交 RunnableCallable 任务
    • shutDown vs shutDownNow:前者关闭线程池后会让队列内所有的任务执行完,后者会直接关闭线程池

10.31

  • 定时任务实现?

    public class ScheduleService {
      
        ExecutorService executorService = Executors.newFixedThreadPool(8);
      
        Trigger trigger = new Trigger();
      
        void schedule(Runnable command, long delay) {
            Job job = new Job();
            job.setTask(command);
            job.setStartTime(System.currentTimeMillis() + delay);
            job.setDelay(delay);
            trigger.jobs.offer(job);
            trigger.wakeUp();
        }
      
        class Trigger  {
            PriorityBlockingQueue<Job> jobs = new PriorityBlockingQueue<>();
            Thread thread = new Thread(() -> {
                while (true) {
                    while (jobs.isEmpty()) {
                        LockSupport.park();
                    }
                    Job latestJob = jobs.peek();
                    if(latestJob.getStartTime() < System.currentTimeMillis()) {
                        latestJob = jobs.poll();
                        executorService.submit(latestJob.getTask());
                        Job nextJob = new Job();
                        nextJob.setStartTime(System.currentTimeMillis() + latestJob.getDelay());
                        nextJob.setTask(latestJob.getTask());
                        nextJob.setDelay(latestJob.getDelay());
                        jobs.offer(nextJob);
                    }else {
                        LockSupport.parkUntil(latestJob.getStartTime());
                    }
      
                }
            });
      
            {
                thread.start();
            }
      
            void wakeUp(){
                LockSupport.unpark(thread);
            }
        }
    }
    
    • 整体的思路其实很像操作系统中的进程调度部分,这个demo采用的是短开始时间优先策略,用PriorityBlockingQueue存储我们的Job,并且添加一个触发器,触发器每次都会拿出队列中开始时间最近的任务,如果当前时间已经大于开始时间,就马上开始执行任务,否则调用LockSupport中的parkUntil方法阻塞当前线程,直到到达开始时间。
    • 每次处理完一个任务,都重新计算下一次执行此任务的开始时间,再把这个任务扔进优先阻塞队列
  • 其他的定时任务实现?

    • Redis和MQ都提供类似的定时消息功能,Spring也有提供@Schedule注解,实现定时任务功能,还有一些开源项目

11.6

颓废了小一周,接着背吧

  • BigDemical

    • 引入原因:计算机底层的浮点数会造成精度丢失,因为二进制和十进制之间的转换会产生无穷小数,对无穷小数的截断引发精度丢失

    • 用例:

      BigDecimal a = new BigDecimal("1.0");
      BigDecimal b = new BigDecimal("0.9");
      System.out.println(a.add(b));// 1.9
      System.out.println(a.subtract(b));// 0.1
      System.out.println(a.multiply(b));// 0.90
      System.out.println(a.divide(b));// 无法除尽,抛出 ArithmeticException 异常
      System.out.println(a.divide(b, 2, RoundingMode.HALF_UP));// 1.11
      
  • JMM(Java Memory Model)

    • JMM是Java对于并发编程的一套内存模型规范,抽象了线程和主内存之间的关系,保证了JVM运行的跨平台性
  • 并发编程的三个特性

    • 原子性:对于一段指令,要么不做,要么不被打断的做完,可以依靠synchornized,Lock等锁实现
    • 可见性:一个线程修改了共享变量,那么别的线程读取到的都应该是修改后的变量
    • 有序性:执行顺序必须按照指定顺序来,要防止编译器的优化重排序(如volatile)
  • 可重入指的是什么?
    • 假设一个线程有两个加锁的方法A和B,A内部调用了B,如果锁是不可重入的就会引发死锁问题,所以需要ReentrantLock
  • Atomic原子类
    • 原子类的操作都依赖于CAS操作,是一种乐观锁实现
    • 除了基本数据类型以外,引用数据类型要用AtomicReference<T>来实现

11.7

  • 虚假唤醒机制
    • JUC中的LockSupport类提供了park()和unpark()的线程阻塞和唤醒方案,但是存在虚假唤醒现象,即唤醒可能不是被LockSupport.unpark()执行的,而是被内核错误唤醒或者cpu中断唤醒,所以在程序中需要对park()操作进行条件自旋,防止虚假唤醒
    • 一些想法:https://github.com/Aucannot/BubbleThink/issues/13
  • 公平锁 非公平锁
    • 公平锁的锁分配严格按照先来后到,非公平锁中后来的线程可能直接抢占到锁
  • 一个类CLH队列锁实现:

    public class ILock {
      
        AtomicReference<Node> head = new AtomicReference<>(new Node());
        AtomicReference<Node> tail = new AtomicReference<>(head.get());
      
        AtomicBoolean flag = new AtomicBoolean(false);
        Thread owner;
      
        public void lock(){
            // 尝试获得锁,成功后返回
            if(flag.compareAndSet(false,true)){
                owner = Thread.currentThread();
                System.out.println(owner.getName() + "直接获得了锁");
                return;
            }
            // 如果没有成功,就把当前线程插入尾节点
            Node cur = new Node();
            cur.thread = Thread.currentThread();
            while (true){
                Node curTail = tail.get();
                if(tail.compareAndSet(curTail, cur)){
                    cur.pre = curTail;
                    curTail.nxt = cur;
                    System.out.println(Thread.currentThread().getName() + "加入链表");
                    break;
                }
            }
            while (true){
                // 被唤醒时执行的操作,如果if判断成功说明抢占到锁了,否则就认为是虚假唤醒继续阻塞
                // if条件表示 当前线程是链表中第一个节点,并且抢占到锁
                if(cur.pre == head.get() && flag.compareAndSet(false,true)){
                    owner = Thread.currentThread();
                    System.out.println(owner.getName() + "被唤醒,并且获得了锁");
                    head.set(cur);
                    cur.pre.nxt = null;
                    cur.pre = null;
                    return;
                }
                LockSupport.park();
            }
        }
      
        public void unlock(){
            if(owner == null || owner != Thread.currentThread()){
                throw new RuntimeException("当前线程未持有锁");
            }
      
            Node headNode = head.get();
            Node nxtNode = headNode.nxt;
            flag.set(false);
            if(nxtNode != null){
                LockSupport.unpark(nxtNode.thread);
                System.out.println(nxtNode.thread.getName() + "被unpark唤醒");
            }
      
        }
      
        class Node{
            Node pre;
            Node nxt;
            Thread thread;
        }
    }
    

11.16

  • 读扩散,写扩散?
    • 以消息发布模型为例,读扩散就是用户发布消息时,把消息写进在他的发件箱里,关注他的用户要读取时,再进行数据查询。这种模式的缺点是如果某用户关注了很多人,一次性发出大量的查询请求,服务器查询压力大。
    • 写扩散就是在用户发布消息时,把消息写进关注他的用户的收件箱内,这样关注他的用户读消息的时候就不需要再经过这个用户查询了,但是缺点是如果某用户粉丝太多,写入的开销也很大。
    • 折中做法:站在发件人这一段,如果是个普通的人,那么我们采用写扩散的方式,直接把数据写入到他的粉丝中去,因为普通的人他的粉丝关注量比较小,所以这样做没有压力,如果是大V,那么他是直接将数据先写入到一份到发件箱里边去,然后再直接写一份到活跃粉丝收件箱里边去,现在站在收件人这端来看,如果是活跃粉丝,那么大V和普通的人发的都会直接写入到自己收件箱里边来,而如果是普通的粉丝,由于他们上线不是很频繁,所以等他们上线时,再从发件箱里边去拉信息

11.18

  • 四种引用:
    • 强引用:我们常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
    • 软引用:使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
    • 弱引用:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
    • 虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
  • ThreadLocal
    • 主要用法:在多线程环境中,存取当前线程的变量副本
    • ThreadLocalMap:由每个线程私有,用于存取ThreadLocal保存的值,ThreadLocal作为key,set的值作为value
    • ThreadLocal继承了WeakReference,发生gc之后key值为空,map中会存在stale key,需要遇到remove(),set(),get()方法中的懒清理才会回收,否则会有内存泄漏
  • ArrayList
    • 三个构造函数:
      • 1)空参构造:使用默认容量为10的数组
      • 2)指定容量:按照传入的容量新建数组
      • 3)传入Collections:先判断非空,再进行数组拷贝
    • 扩容机制:每次扩容都是原容量的1.5倍,计算时用了位运算(oldCapacity«1),速度快,性能瓶颈在数组拷贝的O(n)
  • LinkList
    • 本质双向链表,只有在首尾插入元素的时候才是O(1),别的都是O(n)

11.20

  • 网页访问的全过程?
    • 输入URL
    • 本地DNS服务器通过迭代查询,得到URL的目标ip地址和端口
    • 发起TCP连接请求
    • 连接成功后,发起HTTP请求网页内容
    • 根据网页HTML中的资源地址,再请求图片音频等内容
  • SMTP/POP3/IMAP?
    • SMTP为客户端向邮件服务器和邮件服务器之间传输邮件的协议
    • POP3/IMAP为邮件服务器向客户端发送邮件的协议
  • HTTP vs HTTPS
    • HTTP是基于TCP连接的无状态超文本传输协议
    • HTTPS是基于HTTP增加了SSL/TLS安全措施的HTTP,保证了数据传输的安全
  • TLS
    • 非对称加密:分为公钥和私钥,如RSA,依赖的原理是大整数分解问题,计算φ(n)困难
    • 对称加密:共用同一个密钥,需要安全保存密钥,加解密同步效率高
    • 安全证书:CA颁发给服务器,客户端接收服务器数据时可以校验,防止中间人攻击
    • 数字水印:散列加密校验

11.21

  • 双亲委派机制
    • 子类加载器在需要加载类的时候会先请求父加载器,并且依次向上委托,只有当父加载器找不到类的时候才会尝试自己加载(本质递归)
    • 这么设计的目的:1)安全 2)避免重复加载导致的性能问题
  • TCP三次握手四次挥手
    • 三次握手:确认双方的发送能力和接受能力
      • C -> S:发送SYN,确认C发送能力正常
      • S -> C:发送SYN+ACK,确认S接受能力正常,发送能力正常
      • C -> S:发送ACK,确认C接受能力正常
    • 四次挥手:确认双方都传输完,结束连接
      • C -> S:发送FIN,表示C传输完毕,请求关闭发送连接
      • S -> C:发送ACK,表示S知道,关闭监听连接
      • S -> C:发送FIN,表示S传输完毕,请求关闭发送连接
      • C -> S:发送ACK,表示C知道,关闭监听连接
    • 这么做是因为TCP是全双工协议,需要确认双方能互相通信
  • GET和POST的区别?
    • 语义:GET一般是获取或者查询资源,POST一般是创建或者修改资源
    • GET是幂等的,可以通过缓存的方式提高效率,而POST不是幂等的

12.12

  • B树和B+树的区别?
    • B树相比于二叉平衡树,是一种矮胖的树(分叉多),节点中同时存储了索引和数据
    • B+树是对B树的改进,它的中间节点不再存储数据,所有数据全部存储在叶子节点中,这样保证了中间节点的轻量,也就是说同一个内存页中有更多的节点,Cache Miss带来的影响大大减小,同时由于B+树结构扁平,这样我们可以通过最少的IO次数来查询到目标数据。
    • B+树的叶子节点通过双向指针形成链表,更加方便数据的范围查询,符合关系型数据库的设计理念
  • 红黑树的设计思想?
    • 红黑树的产生来自于二叉搜索树和AVL树的折中,二叉搜索树容易因为插入顺序导致树的平衡性被破坏,查询效率退化到logN,而AVL树能保证树的结构平衡,但是调整次数过多,在写频繁场景下性能差。
    • 红黑树通过结构设计保证:从根到叶子的最长路径,不会超过最短路径的 2 倍,折中了上述两种树的性能。
  • GMP模型:
    • G: (Goroutine) 协程,go定义的轻量级并发执行体,包含了线程执行的上下文,在未被调度时不占用内存
    • M: (Machine) 操作系统内核级线程,真正执行任务的载体,需要绑定一个P才能工作
    • P: (Processer) 上下文调度器,维护一个本地运行队列(存放可运行的goroutine),绑定的M从运行队列中拿到任务执行
  • GMP 调度的四大核心策略

    A. 工作窃取 (Work Stealing)

    当一个 P 闲下来时,它不会让 M 休息。

    • 如果 P 的本地队列空了,它会先尝试从全局队列拿 G。
    • 如果全局队列也空了,它会随机挑选另一个 P,从它的本地队列里偷走一半的 G 来执行。
    • 目的: 实现了负载均衡,避免有的核忙死,有的核闲死。

    B. 切换机制 (Hand-off / P-separation)

    当 M 被阻塞时(例如正在进行系统调用 syscall),P 会抛弃它。

    • 如果是短时间的系统调用,P 会自旋等待。
    • 如果是长时间阻塞(如文件 I/O),P 会与当前的 M 断开(Detaching)。P 会去寻找一个新的 M(或者新建一个 M)来继续执行队列里剩下的 G。
    • 被阻塞的旧 M 等待系统调用结束后,会尝试找一个空闲的 P 挂载,找不到就把 G 扔回全局队列,自己去睡觉(线程休眠)。
    • 目的: 保证 CPU 资源不被阻塞的线程浪费,始终有线程在干活。

    C. 抢占式调度 (Preemption)

    防止某个 G 长期霸占 CPU。

    • 在 Go 1.14 之前,如果一个 G 写了个 for {} 死循环,它会一直占用 M,导致同个 P 上的其他 G 饿死。
    • 现在的 Go 引入了基于信号的异步抢占。后台有一个监控线程 sysmon,如果发现某个 G 执行超过 10ms,就会强制把它踢下来,放到全局队列尾部,让其他 G 有机会执行。

    D. 局部性原则 (Locality)

    • 新创建的 G 优先放入当前 P 的本地队列。这样数据大概率还在 CPU 缓存(Cache)中,执行效率更高。

12.14

  • 消息队列的作用

    • 异步处理:在服务层引入消息队列可以进行与数据库I/O操作的解耦,将任务扔给MQ后先返回数据,提高用户体验,比如先返回“订单正在处理中”,等到消息队列中的任务执行完成后再响应操作结果。
    • 削峰/限流:将客户端发出的请求先放到消息队列中,后端再根据自己的能力进行消费,解耦后可以防止大量请求打垮服务器。
    • 降低耦合度:如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。
  • Raft协议

    • Raft 协议 是一种分布式一致性算法 (Distributed Consensus Algorithm)

      它的核心目标非常简单:让一组计算机(节点)在部分节点故障或网络不通的情况下,依然能对某个“数据记录”达成一致。

    • 协议中节点被分为Leader, Follower, Candidate

    • 核心机制

      • 1)选举:如果某个Follower一段时间没有收到Leader的心跳信息,等待随机时间(150ms-300ms)后就认为Leader挂了,则他作为Candidate发起投票,请求当选下一个Leader,对于其他收到请求的节点,如果Candidate的日志不比自己旧则投赞成票,一个Candidate收到N/2的赞成票就立刻当选
      • 2)日志复制:一个Client发起更新请求给Leader时,Leader先把命令追加到日志中但不执行,而是先广播给所有Follower,Follower收到后加入自己的日志,并且返回收到。只有半数以上的节点回复收到,Leader才会执行命令,并且在下一次心跳中告知Follower,让它们也执行更新操作

1.25

  • MySQL的事务隔离级别有哪些?

    • 读未提交:其他事务可以读到一个事务未提交的数据,发生回滚时会导致脏读
    • 读已提交:其他事务可以读到一个事务已提交的数据,避免了脏读,但会出现不可重复读 (Non-repeatable Read)。即在同一个事务中,多次读取同一行数据,结果可能不同(因为在两次读取之间,另一个事务修改并提交了该数据)。
    • 可重复读:保证在同一个事务中,多次读取同样的数据结果是一致的。即使其他事务在期间修改并提交了数据,当前事务读取到的依然是事务开始时的数据状态。理论上会出现幻读 (Phantom Read)。幻读是指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围时,会发现有新的“幻影”行。(InnoDB默认级别)
    • 串行化:强制事务串行执行。它会在读取的每一行数据上都加锁,导致大量的锁竞争。并发情况下,性能极差。
  • Redis 的有序集合底层为什么要用跳表

    • 跳表的结构适合做区间查找ZRANGE
    • 跳表相比于平衡树,进行写操作时变动更小,性能更好
    • 跳表相比于红黑树,内存占用更少,符合Redis作为内存存储的要求
  • Redis 持久化

    • 持久化:把内存中的数据写进磁盘中

    • RDB(Redis Database):Redis 默认的持久化方式。它会在指定的时间间隔内,将内存中的数据集快照写入磁盘。如 save 60 1000,表示60秒内有1000个改动则触发,也可以手动触发(save,bgsave)

      • save : 同步保存操作,会阻塞 Redis 主线程;
      • bgsave : fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。
    • AOF(Append Only File):以日志的形式记录服务器处理的每一个写、删除操作(查询操作不记录)。

      • 优点:

      • 数据更安全: 默认每秒同步一次,最多只丢失一秒的数据。
      • 可读性强: AOF 文件是纯文本的 Redis 协议格式。如果你误执行了 FLUSHALL,只要在 rewrite 发生前停止服务器,删掉 AOF 文件末尾的 FLUSHALL 命令,重启即可恢复。

      • 缺点

      • 文件体积大: 同一份数据,AOF 文件通常比 RDB 文件大。
      • 恢复速度慢: 重启时需要一条条重放命令,速度远慢于 RDB。
    • 混合模式:AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头,形成这样的结构[RDB 格式数据] + [AOF 增量日志]。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。

    • RDB 比 AOF 优秀的地方

      • RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小,适合做数据的备份,灾难恢复。AOF 文件存储的是每一次写命令,类似于 MySQL 的 binlog 日志,通常会比 RDB 文件大很多。当 AOF 变得太大时,Redis 能够在后台自动重写 AOF。新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。不过, Redis 7.0 版本之前,如果在重写期间有写入命令,AOF 可能会使用大量内存,重写期间到达的所有写入命令都会写入磁盘两次。
      • 使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。而 AOF 则需要依次执行每个写命令,速度非常慢。也就是说,与 AOF 相比,恢复大数据集的时候,RDB 速度更快。

      AOF 比 RDB 优秀的地方

      • RDB 的数据安全性不如 AOF,没有办法实时或者秒级持久化数据。生成 RDB 文件的过程是比较繁重的, 虽然 BGSAVE 子进程写入 RDB 文件的工作不会阻塞主线程,但会对机器的 CPU 资源和内存资源产生影响,严重的情况下甚至会直接把 Redis 服务干宕机。AOF 支持秒级数据丢失(取决 fsync 策略,如果是 everysec,最多丢失 1 秒的数据),仅仅是追加命令到 AOF 文件,操作轻量。
      • RDB 文件是以特定的二进制格式保存的,并且在 Redis 版本演进中有多个版本的 RDB,所以存在老版本的 Redis 服务不兼容新版本的 RDB 格式的问题。
      • AOF 以一种易于理解和解析的格式包含所有操作的日志。你可以轻松地导出 AOF 文件进行分析,你也可以直接操作 AOF 文件来解决一些问题。比如,如果执行FLUSHALL命令意外地刷新了所有内容后,只要 AOF 文件没有被重写,删除最新命令并重启即可恢复之前的状态。

Tags:

Categories:

Updated: