2021-04-22

1.Java线程的状态

Java线程分为初始化,就绪,运行,阻塞,等待,终止,超时等待等状态,其中状态迁移需重点描述

2.进程和线程的区别,进程间如何通讯,线程间如何通讯

进程和线程的区别:

  1. 线程共享同一块地址空间和所有可用数据
  2. 线程比进程更轻量级,创建撤销快10~100倍,上下文切换也快的多
  3. 多线程OS中,进程不是一个可执行的实体

进程通讯

  1. 管道/匿名管道(pipe)(内存):管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据,管道一端的进程顺序的将数据写入缓冲区,另一端的进程则顺序的读出数据。

  2. 有名管道(FIFO)(磁盘):匿名管道是单向的,只能在有亲缘关系的进程间通信;有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。

  3. 信号(Signal):如SIGQUIT:程序退出信号。程序运行过程中,按Ctrl+\键将产生该信号。硬件来源:用户按键输入Ctrl+C退出、硬件异常如无效的存储访问等;软件终止:终止进程信号、其他进程调用kill函数、软件异常产生信号。

  4. 消息(Message)队列:消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。与管道等不同的是:消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。

  5. 共享内存(share memory):使得多个进程可以可以直接读写同一块内存空间

  6. 信号量(semaphore):信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。

     //p操作:申请一个单位资源,进程进入
     wait(S){
      while(s<=0)//如果没有资源则会循环等待;
         S-- ;
     }
     //v操作(signal):释放一个单位资源,进程出来
     signal(S){
     S++ ;
     }
    
  7. 套接字(socket):一是流套接字,流套接字在域中通过TCP/IP连接实现;二个是数据报套接字,通常是通过UDP/IP协议实现的;三是原始套接字,原始套接字允许对较低层次的协议直接访问,比如IP、 ICMP协议

JAVA线程通讯

  1. Volatile关键
  2. synchronized关键字
  3. 等待/通知机制
  4. Wait/Notify机制
  5. 管道输入/输出流
  6. Thread.join()
  7. ThreadLocal

3. HashMap的数据结构是什么?如何实现的?HashTable,ConcurrentHashMap的区别

数据结构:数组+链表形式 ,1.8相对1.7 在链表到达一定长度时会进化成红黑树
如何实现:

//size:KV数量,capacity:桶的数量,loadFactor:size/capacity,衡量满的程度,threshold=capacity*loadFactor,hashmap的值大于该值会执行resize操作
//node数据结构
static class Node implements Map.Entry {
    final int hash;
    final K key;
    V value;
    Node next;
}
//put操作
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node[] tab; Node p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;                --如桶数组为空,新建数组,初始化属性
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);   --如对应桶为空,新建链表
    else {
        Node e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;                                  --如对应桶的头结点key值与key相等,直接替换
        else if (p instanceof TreeNode)
            e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st        --如某次链表遍历数量大于TREEIFY_THRESHOLD:8-1=7,则该链表进化成红黑树,待细研究
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;                          --遍历链表,如该结点与key相等,直接退出遍历
                p = e;                          
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;                    --替换对应node value,如存在,直接替换,如不存在,插入链表尾节点
            afterNodeAccess(e);                     --LinkedHashMap将访问过的节点移到双链表尾部
            return oldValue;
        }
    }
    ++modCount;                                     --FailedFast机制,并发过程中判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了Map
    if (++size > threshold)
        resize();                                   --如果大于负载因子,则扩容
    afterNodeInsertion(evict);                      --LinkedHashMap插入节点尾部,移除双链表头结点&Hash表对应节点
    return null;
}
//get操作,注:LinkedHashMap直接覆盖
final Node getNode(int hash, Object key) {
    Node[] tab; Node first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
//resize:若kv数量>ThreadHold,直接重建hash表

区别:hashtable支持同步,到syschronized全部到方法维度,性能较差;ConcurrentHashMap采取桶的方式,每次访问只锁对应的hash桶,当前采用CAS实现。

Concurrent源码解析

//PUT操作
//sizeCtl是控制标识符:
//负数代表正在进行初始化或扩容操作 ,其中-1代表正在初始化 ,-N 表示有N-1个线程正在进行扩容操作
//正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,类似于扩容阈值。它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。实际容量>=sizeCtl,则扩容
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node[] tab = table;;) {       //类似于while(true),死循环,直到插入成功 
        Node f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();              //设置sizeCtl为初始化中,并新建数组
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node(hash, key, value, null)))        //CAS初始化链表节点
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);     //检测到正在扩容,协助其扩容?
        else {
            V oldVal = null;
            synchronized (f) {              //锁定链表,解析来与hashmap类似
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node p;
                        binCount = 2;
                        if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);                                     //检测数量,并进行扩容
    return null;
}

4. 限流算法

http://dockone.io/article/10569

4.1 计数限流

实现:系统能同时处理100个请求,保存一个计数器,处理了一个请求,计数器加一,一个请求处理完毕之后计数器减一
缺点: 如限定5分钟最多处理2000请求,结果前一秒内涌入1500请求,突发的流量导致系统冲垮

4.2 固定窗口限流

实现:计数器每过一个时间窗口就重置
缺点:系统每秒允许 100 个请求,时间窗口是01s,0.55s处涌入100请求,过了1s时间窗口计数清零,1.05s时又涌入100请求;实际窗口的计数未超阈值,但是0.55s1.05s涌入200请求,无法接受

4.3 滑动窗口限流

实现:记录每次请求以及时间,使用类似redis zrangeByScore操作截取当前时间往前1s的请求数量,如超过拒绝,未超过允许通过
缺点:与固定窗口类似,只是它的粒度更小,如在5ms内打满阈值,无法接受

4.4 漏桶(漏斗)算法

实现:水滴持续滴入漏桶中,底部定速流出。
设计思想:存在桶中的请求是原本就应该被系统处理的,是系统对外界宣称的预期,不应该被丢失
优缺点: 但当流量爆发时,漏桶算法的业务服务器感受到的是一样的速率变化
代码:

boolean tryAcquire(){
    long now=currentTimeMillis();//获取当前时间
    long cousumeWater=(now-lastInjectTime)*rate//当前时间减去上次注水的时间*流出的速率=流出的水量
    long leftWater=max(0,leftWater-consumerWater)//之前桶的水量-这段时间流出的水量
    if(leftWater+1<=capacity){  //水桶内的水量+此次注入的一滴水是否不大于桶的大小
        lastInjectTime=now; //重置注水的时间
        leftWater++;
        return true;
    }else{
        return false;
    }
}

4.4 令牌桶算法

实现:令牌桶是定速地往桶里塞入令牌,然后请求只有拿到了令牌才能通过,之后再被服务器处理。
设计思想:缓冲思想中放在缓冲区域的请求仅仅是对意外状况的尽量优化,并没有任何强制要求这些请求可以被处理
优缺点: 但当流量爆发时,令牌桶算法会使业务服务器直接承担这种流量
代码:

boolean tryAcquire(String key){
    long now=currentTimeMillis();//获取当前时间
    long generatedToken=(now-lastAcquireTime)*rate//当前时间减去上次取令牌的时间*放入令牌的苏略
    long leftToken=min(capacity,leftToken+generatedToken)//之前桶内的令牌数+这段时间放入的令牌数
    if(leftToken>=1){
        lastAcquireTime=now;//重置取令牌时间
        leftToken--;//令牌数减一
        return true;
    }else{
        return false;
    }
}

5.SELECT,POLL,EPOLL的区别

6.Cookie和Session的区别

https://segmentfault.com/a/1190000017831088

  • Cooike和Session的交互
  • token方案
  • 分布式会话方案
  • JWT原理

7.索引有什么用?如何建索引?

从B+树角度阐述,重点讲B+树适合建索引的场景,延伸讲二叉数,AVL,红黑树,B树(即B-),B+树的优缺点

8.ArrayList是如何实现的,ArrayList和LinedList的区别?ArrayList如何实现扩容。

数据与链表的区别,扩容:容量*2,System.Copy进行扩容

9.equals方法实现

默认Object.equal用于判断引用是否相等,复写equals用于判断值是否相等,通过hashCode和equals延伸出hashmap和hashset等

10.面向对象

封装,继承,多态(重载与重写)
继承:Java子类继承父类,是在子类的内部隐式的持有一个super引用,super引用指向一个父类的实例对象,当调用子类的方法时,会先在子类的内部寻找方法,找不到则通过super引用找父类的方法。
多态:

11.线程状态,BLOCKED和WAITING有什么区别

BLOCKED:加入同步队列,需要当前占据锁的线程释放锁,调用park方案进行唤醒
WAITING:加入等待队列,需要协同线程调用SIGNAL或SIGNAL-ALL进行唤醒

12.JVM如何加载字节码文件

12.1 类加载过程

  • 加载:
    • 通过类的全限定名获取类的二进制流(JAR,EAR,APPLET,动态代理等)
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
    • 内存中生成这个类的Class对象
  • 验证:确保Class文件包含的信息符合当前虚拟机要求,不会危害虚拟机安全,如JDK版本是否符合,引用的类是否能访问(private?)
  • 准备:为类变量分配内存并设置类变量初始值(static修饰的变量),不会赋值只会初始化值
  • 解析:常量池内的符号引用(字面量)替换为直接引用(指针)
  • 初始化:执行类构造器()方法,()由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)合并产生的,收集顺序按照源文件出现的顺序
  • PS:虚拟机保证()方法在多线程环境中被正确加锁同步,即多个线程同时初始化,只有一个线程能执行(用于双重检查单例)
  • 使用:
  • 卸载:

12.2 类加载器(双亲委派模型)

双亲委派模型:类加载器收到类加载请求,首先不会自己加载,将请求委托父类加载器加载,直至顶层,只有父加载器无法加载,子加载器才会尝试自己加载

  • 启动类加载器(Bootstrap ClassLoarder)(C++实现):加载存放/lib目录或者-Xbootclasspath指定
  • 扩展类加载器(Extension CLassLoader):加载/lib/ext目录或java.ext.dirs指定路径
  • 应用程序类加载器(Application ClassLoader):用户类路径(ClassPath),默认类加载器
  • 自定义类加载器:

12.3 对象创建

对象存储布局:对象头Mark Word(哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等),实例数据,对齐填充

  • 遇到new指令,检查指令参数是否能定位到类的符号引用,并检查类是否被加载、解析和初始化过,如没有,启动类加载过程
  • 为新生对象分配内存(指针碰撞和空闲列表),对象所需内存大小在类加载完成后即可完全确定
  • 将分配到的内存空间都初始化为零值
  • 对对象进行必要的设置(对象头),如对象是哪个类的实例,对象的哈希吗,对象的GC分代年龄
  • 执行init方法(invoke special)

13.JVM GC,GC算法。

13.1 对象已死吗

  • 引用计数法:无法解决循环引用
  • 可达性分析算法:GC ROOTS对象作为起始点,从上往下进行搜索

13.2 引用

  • 强引用:如Object obj=new Object(),只要强引用还存在,垃圾收集器用于不会回收引用的对象
  • 软引用:软引用关联的对象,在系统将要发生内存溢出之前,将会把这些对象纳入回收范围中进行第二次回收;如回收后还没有足够的内存,内存溢出
  • 弱引用:垃圾回收器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
  • 虚引用:对象是否有虚引用存在,完全不会对生命周期造成影响;唯一的目的是对象被收集器回收时收到一个通知
  • WeakHashMap,Reference?

13.3 垃圾收集算法

  • 标记-清除算法:效率问题,标记和清除两个过程效率都不搞;空间问题,产生大量不连续的内存碎片;不会操作内存,保证用户线程不受影响
  • 复制算法:空间问题,内存缩小为原来的一版;效率问题,对象存活率较高,大量复制操作,效率将会变低
  • 标记-整理算法:
  • 分代收集算法:
    • 新生代:一个较大的Eden空间,两块较小的Survivor空间,Eden:Survivor=8:1,回收时,将Eden和Survivor的对象一次性复制到另外一块Survivor空间上
    • 老生代:如新生代复制时Survivor空间不够,进行FULL GC内存担保,一般采用标记-整理或标记-清理

13.4 垃圾收集器

[图片上传失败...(image-43520-1619094005828)]

  • Serial(复制):新生代垃圾收集时,暂停所有工作线程(Stop The World),单线程进行垃圾收集
  • Serial Old(标记-整理):老年代垃圾收集时,暂停所有工作线程(Stop The World),单线程进行垃圾收集
  • ParNew(复制):新生代垃圾收集时,暂停所有工作线程(Stop The World),多线程进行垃圾收集
  • Parallel Old(标记-整理):老年代垃圾收集时,暂停所有工作线程(Stop The World),多线程进行垃圾收集
  • Parallel Scavenge(复制):
    • 目标:达成可控制的吞吐量,吞吐量即CPU用于运行用户代码的时间与CPU总消耗时间的比值,即=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
    • 实现:虚拟机根据当前系统的运行情况收集性能监控信息,动态调整已提供最合适的停顿时间或者最大的吞吐量,此调节方式成为GC自适应的调节策略。
    • 关键参数:
      • XX:MaxGCPanseMillis:最大垃圾收集停顿时间,以牺牲吞吐量和新生代空间换取的;如新生代从500MB调节至300MB,垃圾收集更加频繁,原来10S收集一次、停顿100ms,现在变成5s收集一次、停顿70ms,停顿时间确实下降,但吞吐量也降下来了
      • XX:GCTimeRatio:吞吐量大小,垃圾收集时间占总时间的比率,相当于吞吐量的倒数
  • CMS(标记-清除)
    • 目标:获取最短回收停顿时间
    • 优点:并发收集;低延迟
    • 缺点:
      • 产生内存碎片;
      • 本身占用线程,降低吞吐量;
      • CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure" 失败而导致另一次Full GC的产生,一般需预留一部分内存保证清理阶段用户产生得新对象有地方可以放
    • 过程:
      • 初始标记:StopTheWorld,仅标记出GCRoots能直接关联得对象,速度快
      • 并发标记:并发如4个线程(3个用户线程,1个并发标记线程),从GC Roots的直接关联对象开始遍历整个对象图的过程,耗时长
      • 重新标记:StopTheWorld,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,速度快
      • 并发清除(浮动垃圾出现阶段):并发如4个线程(3个用户线程,1个并发标记线程),从GC Roots的直接关联对象开始遍历整个对象图的过程,耗时长
  • G1
    • 优点:
      • 并行与并发
      • 分代收集
      • 空间整合:基于标记-整理,不会产生内存碎片
      • 可预测得停顿,将Java堆划分未多个大小相等的独立区域,新生代和老年代不再物理隔离,他们都是一部分Region的集合,回收时根据允许的收集时间,优先回收价值最大的Region,因为每个Region可控制大小,因此停顿时间相对可预测
    • 过程:
      • 初始标记:与CMS类似
      • 并发标记:与CMS类似
      • 最终标记:StopTheWorld,修正并发标记期间因用户程序继续运作二导致标记产生变动的那一部分标记记录
      • 筛选回收:StopTheWorld,并行执行。对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来执行回收计划。因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅提升手机效率。

14.什么情况会出现Full GC,什么情况会出现yong GC。

见13.3

15.JVM内存模型

  • 内存模型
    • 线程:
    • 工作内存:线程的工作内存保存该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)必须在工作内存中进行,不能直接读取主内存的变量;不同线程之间也无法访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存完成
    • 主内存:所有变量都存储在主内存中
  • 内存交互操作:
    • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态
    • unlock(解锁):作用于主内存的变量,一个处于锁定状态的变量释放出来
    • read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
    • load(载入):作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中
    • use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,虚拟机遇到需使用变量值字节码指令都会执行该操作
    • assign(赋值):作用于工作内存的变量,把执行引擎接收的值赋给工作内存的变量,虚拟机遇到需赋值变量值字节码指令都会执行该操作
    • store(存储):作用于工作内存的变量,把工作内存中变量值传递给主内存,以便随后的
    • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

16.Java运行时数据区

  • 运行结构
    • 程序计数器:线程私有,用于确定某时刻线程要执行的指令
    • Java虚拟机栈:线程私有,每个方法执行时创建一个栈帧,栈帧存储局部变量表、操作数栈、动态链接、方法出口等信息
    • 本地方法栈:类似虚拟机栈,主要用于Native方法
    • Java堆:各个线程共享的区域,存放对象实例,几乎所有对象实例都在这里分配内存
    • 方法区:各个线程共享的区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等;包含运行时常量池
    • 直接内存:非虚拟机内存的一部分

17.事务的实现原理

从ACID阐述,重点将隔离性,MVVC

18. MySQL锁

https://juejin.cn/post/6878884451162521613#heading-19

  • 表级锁:资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。
  • 行锁:
    • 其加锁粒度最小,但加锁的开销也最大。有可能会出现死锁的情况。
    • SQL 使用主键索引来查询,则只需要在 id = 49 这个主键索引上加上写锁
    • SQL 则使用二级索引来查询,使用二级索引需要在二级索引和主键索引上各加一把锁。
  • 页锁:页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录
  • 共享锁:LOCK IN SHARE MODE。如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
  • 排他锁:FOR UPDATE,如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。
  • 乐观锁:乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。
    • 数据版本,为数据增加的一个版本标识。
    • 当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。
    • 当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。
  • 悲观锁:悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。
    • 在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking);
    • 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。 具体响应方式由开发者根据实际需要决定;
    • 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
    • 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。
  • 意向共享锁/意向排他锁
    • 问题:
      • 事务A锁住了表中的一行,让这一行只能读,不能写。如果事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。数据库需要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁。
      • step1:判断表是否已被其他事务用表锁锁表;step2:判断表中的每一行是否已被行锁锁住。
      • step2,这样的判断方法效率实在不高,因为需要遍历整个表
    • 意向锁:在意向锁存在的情况下,事务A必须先申请表的意向共享锁,成功后再申请一行的行锁。step2:发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。
  • 记录锁(Record Lock): InnoDB 加锁原理中的锁就是记录锁,只锁住 id = 49 或者 name = 'Tom' 这一条记录。
    • 当 SQL 语句无法使用索引时,会进行全表扫描,这个时候 MySQL 会给整张表的所有数据行加记录锁,再由 MySQL Server 层进行过滤。但是,在 MySQL Server 层进行过滤的时候,如果发现不满足 WHERE 条件,会释放对应记录的锁。
  • 间隙锁(Gap Lock):
    • 定义
      • 当我们使用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,InnoDB 也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。
      • 间隙锁在 InnoDB 的唯一作用就是防止其它事务的插入操作,以此来达到防止幻读的发生,所以间隙锁不分什么共享锁与排他锁。
    • 使用
      • 在唯一索引上,对于指定查询某一条记录的加锁语句,如果该记录不存在,会产生记录锁(行锁)和间隙锁,如果记录存在,则只会产生记录锁(行锁);
      • 在唯一索引上,对于查找某一范围内的查询语句,会产生间隙锁。
      • 在普通索引列上,不管是何种查询,只要加锁,都会产生间隙锁,这跟唯一索引不一样;因为普通索引相同key会再插入数据
      • 在普通索引跟唯一索引中,数据间隙的分析,数据行是优先根据普通普通索引排序,再根据唯一索引排序。
  • 临键锁(Next-key Locks)
    • 是记录锁(行锁)与间隙锁的组合
    • 如上,Next-Key Lock锁定记录和之前的间隙,Gap Lock锁定唯一的间隙
    • http://blog.sina.com.cn/s/blog_a1e9c7910102vnrj.html
    • 锁选择
      4、 锁选择
      • 如果更新条件没有走索引,例如执行”update from t1 set v2=0 where v2=5;” ,此时会进行全表扫描,扫表的时候,要阻止其他任何的更新操作,所以上升为表锁。
      • 如果更新条件为索引字段,但是并非唯一索引(包括主键索引),例如执行“update from t1 set v2=0 where v1=9;” 那么此时更新会使用Next-Key Lock。使用Next-Key Lock的原因:
        首先要保证在符合条件的记录上加上排他锁,会锁定当前非唯一索引和对应的主键索引的值;还要保证锁定的区间不能插入新的数据。
      • 如果更新条件为唯一索引,则使用Record Lock(记录锁)。
      • 如果更新条件为唯一索引,但key不存在,则使用Record Lock(记录锁)+间隙锁
  • 插入意向锁(Insert Intention Locks)
    • 插入意向锁是一种特殊的间隙锁(简称II GAP)表示插入的意向,只有在 INSERT 的时候才会有这个锁。
  • 自增锁(Auto-inc Locks)

19.索引

回表、索引下推、覆盖索引

你可能感兴趣的:(2021-04-22)