HashMap的扩容

MySQL优化

Redis和MongoDB

一、HashMap的扩容

1.1 底层数据结构

数组 + 链表(或红黑树)

/**
 * 数组
 */
transient Node<K,V>[] table;

/**
 * 链表结构
 */
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

/**
 * 红黑树结构
 */
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
    ...

链表超过8个转为红黑树,红黑树小于6个转为链表

1.2 初始化

HashMap 的构造方法有如下 4 种

/**
 * 构造方法 1
 *
 * 通过 指定的 initialCapacity 和 loadFactor 实例化一个空的 HashMap 对象
 */
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

/**
 * 构造方法 2
 *
 * 通过指定的 initialCapacity 和 默认的 loadFactor(0.75) 实例化一个空的 HashMap 对象
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * 构造方法 3
 *
 * 通过默认的 initialCapacity 和 默认的 loadFactor(0.75) 实例化一个空的 HashMap 对象
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
 *
 * 构造方法 4
 * 通过指定的 Map 对象实例化一个 HashMap 对象
 */
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

使用方式 1 实例化 HashMap 的时候,table 并未进行初始化,那 table 是何时进行初始化的了 ?
table是在第一次 put 的时候,调用 resize 方法进行 table 的初始化(懒初始化)

	//put方法
    public V put(K key, V value) {
		//调用putval方法
        return putVal(hash(key), key, value, false, true);
    }
	
	//putval方法
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
			   boolean evict) {
	Node<K,V>[] tab; Node<K,V> p; int n, i;
	if ((tab = table) == null || (n = tab.length) == 0)
		//调用resize()方法初始化
		n = (tab = resize()).length;
		
	//resize方法
	final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;                    // 第一次 put 的时候,table = null
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // oldCap = 0
    int oldThr = threshold;                        // threshold=0, oldThr = 0
    int newCap, newThr = 0;
    if (oldCap > 0) {    // 条件不满足,往下走
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults 走到这里,进行默认初始化
        newCap = DEFAULT_INITIAL_CAPACITY;    // DEFAULT_INITIAL_CAPACITY = 1 << 4 = 16, newCap = 16;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);    // newThr = 0.75 * 16 = 12;
    }
    if (newThr == 0) {    // 条件不满足
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;        // threshold = 12; 重置阀值为12
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];     // 初始化 newTab, length = 16;
    table = newTab;            // table 初始化完成, length = 16;
    if (oldTab != null) {    // 此时条件不满足,后续扩容的时候,走此if分支 将数组元素复制到新数组
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;    // 新数组
}

初始化的 table.length 是多少、阀值(threshold)是多少,实际能容下多少元素?

  1. 默认情况下,table.length = 16; 指定了 initialCapacity 的情况放到问题 5 中分析
  2. 默认情况下,threshold = 12; 指定了 initialCapacity 的情况放到问题 5 中分析
  3. 默认情况下,能存放 12 个元素,当存放第 13 个元素后进行扩容

1.3 扩容

put源码如下

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> 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<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)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
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)             // 当size(已存放元素个数) > thrshold(阀值),调用resize()进行扩容
        resize();
    afterNodeInsertion(evict);
    return null;
}



final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;                    // 此时的 table != null,oldTab 指向旧的 table
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // oldCap = table.length; 第一次扩容时是 16
    int oldThr = threshold;                        // threshold=12, oldThr = 12;
    int newCap, newThr = 0;
    if (oldCap > 0) {    // 条件满足,走此分支
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&    // oldCap左移一位; newCap = 16 << 1 = 32;
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold            // newThr = 12 << 1 = 24;
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;    // DEFAULT_INITIAL_CAPACITY = 1 << 4 = 16, newCap = 16;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {    // 条件不满足
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;        // threshold = newThr = 24; 重置阀值为 24
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];     // 初始化 newTab, length = 32;
    table = newTab;            // table 指向 newTab, length = 32;
    if (oldTab != null) {    // 扩容后,将 oldTab(旧table) 中的元素移到 newTab(新table)中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;        // 
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

什么时候触发扩容,扩容之后的 table.length、阀值各是多少?

  1. 当 size > threshold 的时候进行扩容
  2. 扩容之后的 table.length = 旧 table.length * 2,
  3. 扩容之后的 threshold = 旧 threshold * 2

1.4 容量是2的次幂

table是一个数组,下标是int类型。通过key算出一个int类型的hash值

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  // 这里的处理,有兴趣的可以琢磨下;能够减少碰撞
}

然后通过h&(length-1)得到索引,length是2的次幂,-1后的二进制数都是1,与hash值进行&运算能够保留原hash值的n个低位。
加上在计算hash值的时候,h>>>16已经得到了hashCode的高16位,所以高位地位都参与了运算,增加了散列性,减少了碰撞。

1.5 初始化指定容量

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
		//调用tableSizeFor方法
        this.threshold = tableSizeFor(initialCapacity);
    }
	
	//调用tableSizeFor方法
	/**
	 * Returns a power of two size for the given target capacity.
	 * 返回 >= cap 最小的 2^n
	 * cap = 10, 则返回 2^4 = 16;
	 * cap = 5, 则返回 2^3 = 8;
	 */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
		//如果传入的容量不是2的次幂,tableSizeFor会算出大于等于容量值的2次幂作为新容量,然后阈值按新容量*负载因子来算。
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

Map map = new HashMap(1000); 当我们存入多少个元素时会触发map的扩容?
此时的 table.length = 2^10 = 1024; threshold = 1024 * 0.75 = 768; 所以存入第 769 个元素时进行扩容

1.6 加载因子

为什么加载因子的默认值是 0.75,并且不推荐我们修改?

  1. 如果loadFactor太小,那么map中的table需要不断的扩容,扩容是个耗时的过程
  2. 如果loadFactor太大,那么map中table放满了也不不会扩容,导致冲突越来越多,解决冲突而起的链表越来越长,效率越来越低
  3. 0.75 这是一个折中的值,是一个比较理想的值

1.7 put流程

image.png

二、MySQL优化

2.1 基本概念

逻辑架构

image.png

  1. 第一层:客户端通过连接服务,将要执行的sql指令传输过来
  2. 第二层:服务器解析并优化sql,生成最终的执行计划并执行
  3. 第三层:存储引擎,负责数据的储存和提取
共享锁/独占锁
  1. 数据库通过锁机制来解决并发场景-共享锁(读锁)和排他锁(写锁)。
  2. 读锁是不阻塞的,多个客户端可以在同一时刻读取同一个资源。
  3. 写锁是排他的,并且会阻塞其他的读锁和写锁。简单提下乐观锁和悲观锁。
乐观锁/悲观锁
  1. 乐观锁,通常用于数据竞争不激烈的场景,多读少写,通过版本号和时间戳实现。
  2. 悲观锁,通常用于数据竞争激烈的场景,每次操作都会锁定数据。
锁策略
  1. 表锁,锁定整张表,开销最小,但是会加剧锁竞争。
  2. 行锁,锁定行级别,开销最大,但是可以最大程度的支持并发。
  3. MySql的存储引擎的真实实现不是简单的行级锁,一般都是实现了多版本并发控制(MVCC)。MVCC是行级锁的变种,多数情况下避免了加锁操作,开销更低。MVCC是通过保存数据的某个时间点快照实现的。
事务隔离级别
事务

事务保证一组原子性的操作,要么全部成功,要么全部失败。一旦失败,回滚之前的所有操作。MySql采用自动提交,如果不是显式的开启一个事务,则每个查询都作为一个事务。

隔离级别

隔离级别控制了一个事务中的修改,哪些在事务内和事务间是可见的

  1. 未提交读(Read UnCommitted),事务中的修改,即使没提交对其他事务也是可见的。事务可能读取未提交的数据,造成脏读。
  2. 提交读(Read Committed),一个事务开始时,只能看见已提交的事务所做的修改。事务未提交之前,所做的修改对其他事务是不可见的。也叫不可重复读,同一个事务多次读取同样记录可能不同。
  3. 可重复读(RepeatTable Read),同一个事务中多次读取同样的记录结果时结果相同。
  4. 可串行化(Serializable),最高隔离级别,强制事务串行执行。
存储引擎
  1. InnoDB引擎,最重要,使用最广泛的存储引擎。被用来设计处理大量短期事务,具有高性能和自动崩溃恢复的特性。
  2. MyISAM引擎,不支持事务和行级锁,崩溃后无法安全恢复。

2.2 创建时的优化

Schema
  1. 尽量使用对应的数据类型。比如,不要用字符串类型保存时间,用整型保存IP。
  2. 选择更小的数据类型。能用TinyInt不用Int。
  3. 标识列(identifier column),建议使用整型,不推荐字符串类型,占用更多空间,而且计算速度比整型慢。
  4. 不推荐ORM系统自动生成的Schema,通常具有不注重数据类型,使用很大的VarChar类型,索引利用不合理等问题。
  5. 真实场景混用范式和反范式。冗余高查询效率高,插入更新效率低;冗余低插入更新效率高,查询效率低。
  6. 创建完全的独立的汇总表\缓存表,定时生成数据,用于用户耗时时间长的操作。对于精确度要求高的汇总操作,可以采用 历史结果+最新记录的结果 来达到快速查询的目的。
  7. 数据迁移,表升级的过程中可以使用影子表的方式,通过修改原表的表名,达到保存历史数据,同时不影响新表使用的目的。
索引

索引包含一个或多个列的值。MySql只能高效的利用索引的最左前缀列。

优势
  1. 索引大大减少了服务器需要扫描的数据量。
  2. 索引可以帮助服务器避免排序和临时表
  3. 索引可以将随机IO变为顺序IO
B-Tree索引

B-Tree 每个叶子节点都包含指向下一个叶子节点的指针,从而方便叶子节点的遍历

  1. B-Tree索引适用于全键值,键值范围,键前缀查找,支持排序。
  2. 如果不是按照索引的最左列开始查询,则无法使用索引。
  3. 不能跳过索引中的列。如果使用第一列和第三列索引,则只能使用第一列索引。
  4. 如果查询中有个范围查询,则其右边的所有列都无法使用索引优化查询。
哈希索引

只有精确匹配索引的所有列,查询才有效。

  1. 存储引擎会对所有的索引列计算一个哈希码,哈希索引将所有的哈希码存储在索引中,并保存指向每个数据行的指针。
  2. 无法用于排序
  3. 不支持部分匹配
  4. 只支持等值查询如=,IN(),不支持 < >
优化
  1. 注意每种索引的适用范围和适用限制。
  2. 索引的列如果是表达式的一部分或者是函数的参数,则失效。
  3. 针对特别长的字符串,可以使用前缀索引,根据索引的选择性选择合适的前缀长度。
  4. 使用多列索引的时候,可以通过 AND 和 OR 语法连接。
  5. 重复索引没必要,如(A,B)和(A)重复。
  6. 索引在where条件查询和group by语法查询的时候特别有效。
  7. 将范围查询放在条件查询的最后,防止范围查询导致的右边索引失效的问题。
  8. 索引最好不要选择过长的字符串,而且索引列也不宜为null。

2.3 查询时优化

性能指标
  1. 响应时间 (服务时间,排队时间)
  2. 扫描的行
  3. 返回的行
优化
  1. 避免查询无关的列,如使用Select * 返回所有的列。
  2. 切分查询。将一个对服务器压力较大的任务,分解到一个较长的时间中,并分多次执行。如要删除一万条数据,可以分10次执行,每次执行完成后暂停一段时间,再继续执行。过程中可以释放服务器资源给其他任务。
  3. 分解关联查询。将多表关联查询的一次查询,分解成对单表的多次查询。可以减少锁竞争,查询本身的查询效率也比较高。因为MySql的连接和断开都是轻量级的操作,不会由于查询拆分为多次,造成效率问题。
  4. 注意count的操作只能统计不为null的列,所以统计总的行数使用count(*)。
  5. group by 按照标识列分组效率高,分组结果不宜出行分组列之外的列。
  6. 关联查询延迟关联,可以根据查询条件先缩小各自要查询的范围,再关联。
  7. Limit分页优化。可以根据索引覆盖扫描,再根据索引列关联自身查询其他列。
    	SELECT id,NAME,age
    	From student s1 
    		INNER JOIN ( SELECT id FROM student ORDER BY age LIMIT 50,5 ) AS s2 ON s1.id = s2.id
    
  8. Union查询默认去重,如果不是业务必须,建议使用效率更高的Union All
  9. 条件中的字段类型和表结构类型不一致,mysql会自动加转换函数,导致索引作为函数中的参数失效。
  10. like查询前面部分未输入,以%开头无法命中索引。

2.4 explain分析

image.png

select_type
  1. simple(表示简单的select,没有union和子查询)
  2. primary(有子查询,最外面的select查询就是primary)
  3. union(union中的第二个或随后的select查询,不依赖外部查询结果)
  4. dependent union(union中的第二个或随后的select查询,依赖外部查询结果)
type
  1. system(表仅有一行(=系统表),这是const连接类型的一个特例)
  2. const(常量查询)
  3. ref(非唯一索引访问,只有普通索引)
  4. eq_ref(使用唯一索引或组件查询)
  5. all(全表查询)
  6. index(根据索引查询全表)
  7. range(范围查询)
possible_keys

表中可能帮助查询的索引

key

选择使用的索引

key_len

使用的索引长度

rows

扫描的行数,越大越不好

extra
  1. Only index(信息从索引中检索出,比扫描表快)
  2. where used(使用where限制)
  3. Using filesort (可能在内存或磁盘排序)
  4. Using temporary(对查询结果排序时使用临时表)

三、Redis和MongoDB

大家一般称之为Redis缓存、MongoDB数据库。

  1. Redis主要把数据存储在内存中,其“缓存”的性质远大于其“数据存储“的性质,其中数据的增删改查也只是像变量操作一样简单;
  2. MongoDB是一个“存储数据”的系统,增删改查可以添加很多条件,就像SQL数据库一样灵活

image.png

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议