mbers" aria-hidden="true" style="counter-reset:line-number 0">

但是这样做,不也使得使用 HashMap 时的 TreeNode 多了两个没有必要的引用吗?这不也是一种空间的浪费吗?

//AN_Xml:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//AN_Xml:  //略
//AN_Xml:
//AN_Xml:}
//AN_Xml:

对于这个问题,引用作者的一段注释,作者们认为在良好的 hashCode 算法时,HashMap 转红黑树的概率不大。就算转为红黑树变为树节点,也可能会因为移除或者扩容将 TreeNode 变为 Node,所以 TreeNode 的使用概率不算很大,对于这一点资源空间的浪费是可以接受的。

//AN_Xml:
Because TreeNodes are about twice the size of regular nodes, we
//AN_Xml:use them only when bins contain enough nodes to warrant use
//AN_Xml:(see TREEIFY_THRESHOLD). And when they become too small (due to
//AN_Xml:removal or resizing) they are converted back to plain bins.  In
//AN_Xml:usages with well-distributed user hashCodes, tree bins are
//AN_Xml:rarely used.  Ideally, under random hashCodes, the frequency of
//AN_Xml:nodes in bins follows a Poisson distribution
//AN_Xml:

构造方法

//AN_Xml:

LinkedHashMap 构造方法有 4 个实现也比较简单,直接调用父类即 HashMap 的构造方法完成初始化。

//AN_Xml:
public LinkedHashMap() {
//AN_Xml:    super();
//AN_Xml:    accessOrder = false;
//AN_Xml:}
//AN_Xml:
//AN_Xml:public LinkedHashMap(int initialCapacity) {
//AN_Xml:    super(initialCapacity);
//AN_Xml:    accessOrder = false;
//AN_Xml:}
//AN_Xml:
//AN_Xml:public LinkedHashMap(int initialCapacity, float loadFactor) {
//AN_Xml:    super(initialCapacity, loadFactor);
//AN_Xml:    accessOrder = false;
//AN_Xml:}
//AN_Xml:
//AN_Xml:public LinkedHashMap(int initialCapacity,
//AN_Xml:    float loadFactor,
//AN_Xml:    boolean accessOrder) {
//AN_Xml:    super(initialCapacity, loadFactor);
//AN_Xml:    this.accessOrder = accessOrder;
//AN_Xml:}
//AN_Xml:

我们上面也提到了,默认情况下 accessOrder 为 false,如果我们要让 LinkedHashMap 实现键值对按照访问顺序排序(即将最近未访问的元素排在链表首部、最近访问的元素移动到链表尾部),需要调用第 4 个构造方法将 accessOrder 设置为 true。

//AN_Xml:

get 方法

//AN_Xml:

get 方法是 LinkedHashMap 增删改查操作中唯一一个重写的方法, accessOrder 为 true 的情况下, 它会在元素查询完成之后,将当前访问的元素移到链表的末尾。

//AN_Xml:
public V get(Object key) {
//AN_Xml:     Node < K, V > e;
//AN_Xml:     //获取key的键值对,若为空直接返回
//AN_Xml:     if ((e = getNode(hash(key), key)) == null)
//AN_Xml:         return null;
//AN_Xml:     //若accessOrder为true,则调用afterNodeAccess将当前元素移到链表末尾
//AN_Xml:     if (accessOrder)
//AN_Xml:         afterNodeAccess(e);
//AN_Xml:     //返回键值对的值
//AN_Xml:     return e.value;
//AN_Xml: }
//AN_Xml:

从源码可以看出,get 的执行步骤非常简单:

//AN_Xml:
    //AN_Xml:
  1. 调用父类即 HashMapgetNode 获取键值对,若为空则直接返回。
  2. //AN_Xml:
  3. 判断 accessOrder 是否为 true,若为 true 则说明需要保证 LinkedHashMap 的链表访问有序性,执行步骤 3。
  4. //AN_Xml:
  5. 调用 LinkedHashMap 重写的 afterNodeAccess 将当前元素添加到链表末尾。
  6. //AN_Xml:
//AN_Xml:

关键点在于 afterNodeAccess 方法的实现,这个方法负责将元素移动到链表末尾。

//AN_Xml:
void afterNodeAccess(Node < K, V > e) { // move node to last
//AN_Xml:    LinkedHashMap.Entry < K, V > last;
//AN_Xml:    //如果accessOrder 且当前节点不为链表尾节点
//AN_Xml:    if (accessOrder && (last = tail) != e) {
//AN_Xml:
//AN_Xml:        //获取当前节点、以及前驱节点和后继节点
//AN_Xml:        LinkedHashMap.Entry < K, V > p =
//AN_Xml:            (LinkedHashMap.Entry < K, V > ) e, b = p.before, a = p.after;
//AN_Xml:
//AN_Xml:        //将当前节点的后继节点指针指向空,使其和后继节点断开联系
//AN_Xml:        p.after = null;
//AN_Xml:
//AN_Xml:        //如果前驱节点为空,则说明当前节点是链表的首节点,故将后继节点设置为首节点
//AN_Xml:        if (b == null)
//AN_Xml:            head = a;
//AN_Xml:        else
//AN_Xml:            //如果前驱节点不为空,则让前驱节点指向后继节点
//AN_Xml:            b.after = a;
//AN_Xml:
//AN_Xml:        //如果后继节点不为空,则让后继节点指向前驱节点
//AN_Xml:        if (a != null)
//AN_Xml:            a.before = b;
//AN_Xml:        else
//AN_Xml:            //如果后继节点为空,则说明当前节点在链表最末尾,直接让last 指向前驱节点,这个 else其实 没有意义,因为最开头if已经确保了p不是尾结点了,自然after不会是null
//AN_Xml:            last = b;
//AN_Xml:
//AN_Xml:        //如果last为空,则说明当前链表只有一个节点p,则将head指向p
//AN_Xml:        if (last == null)
//AN_Xml:            head = p;
//AN_Xml:        else {
//AN_Xml:            //反之让p的前驱指针指向尾节点,再让尾节点的前驱指针指向p
//AN_Xml:            p.before = last;
//AN_Xml:            last.after = p;
//AN_Xml:        }
//AN_Xml:        //tail指向p,自此将节点p移动到链表末尾
//AN_Xml:        tail = p;
//AN_Xml:
//AN_Xml:        ++modCount;
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

从源码可以看出, afterNodeAccess 方法完成了下面这些操作:

//AN_Xml:
    //AN_Xml:
  1. 如果 accessOrder 为 true 且链表尾部不为当前节点 p,我们则需要将当前节点移到链表尾部。
  2. //AN_Xml:
  3. 获取当前节点 p、以及它的前驱节点 b 和后继节点 a。
  4. //AN_Xml:
  5. 将当前节点 p 的后继指针设置为 null,使其和后继节点 p 断开联系。
  6. //AN_Xml:
  7. 尝试将前驱节点指向后继节点,若前驱节点为空,则说明当前节点 p 就是链表首节点,故直接将后继节点 a 设置为首节点,随后我们再将 p 追加到 a 的末尾。
  8. //AN_Xml:
  9. 再尝试让后继节点 a 指向前驱节点 b。
  10. //AN_Xml:
  11. 上述操作让前驱节点和后继节点完成关联,并将当前节点 p 独立出来,这一步则是将当前节点 p 追加到链表末端,如果链表末端为空,则说明当前链表只有一个节点 p,所以直接让 head 指向 p 即可。
  12. //AN_Xml:
  13. 上述操作已经将 p 成功到达链表末端,最后我们将 tail 指针即指向链表末端的指针指向 p 即可。
  14. //AN_Xml:
//AN_Xml:

可以结合这张图理解,展示了 key 为 13 的元素被移动到了链表尾部。

//AN_Xml:

LinkedHashMap 移动元素 13 到链表尾部

//AN_Xml:

看不太懂也没关系,知道这个方法的作用就够了,后续有时间再慢慢消化。

//AN_Xml:

newNode——新节点尾插链表

//AN_Xml:

上文介绍了 afterNodeAccess 如何将已存在的节点移动到链表尾部,那么新插入的节点是如何被添加到链表中的呢?

//AN_Xml:

答案在于 LinkedHashMap 重写了 HashMapnewNode 方法。当 HashMap 插入新键值对时,会调用 newNode 创建节点对象,LinkedHashMap 在重写的方法中不仅创建了 Entry 节点,还额外调用了 linkNodeLast 将其链接到双向链表的尾部:

//AN_Xml:
// HashMap 的 newNode 是普通实现
//AN_Xml:Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
//AN_Xml:    return new Node<>(hash, key, value, next);
//AN_Xml:}
//AN_Xml:
//AN_Xml:// LinkedHashMap 重写 newNode,额外调用 linkNodeLast
//AN_Xml:Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
//AN_Xml:    LinkedHashMap.Entry<K,V> p =
//AN_Xml:        new LinkedHashMap.Entry<>(hash, key, value, e);
//AN_Xml:    linkNodeLast(p);  // 关键:将新节点链接到链表尾部
//AN_Xml:    return p;
//AN_Xml:}
//AN_Xml:

linkNodeLast 方法的实现如下:

//AN_Xml:
// 将节点链接到双向链表尾部
//AN_Xml:private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
//AN_Xml:    LinkedHashMap.Entry<K,V> last = tail;
//AN_Xml:    tail = p;  // tail 指向新节点
//AN_Xml:    if (last == null)
//AN_Xml:        head = p;  // 链表为空,head 也指向新节点
//AN_Xml:    else {
//AN_Xml:        p.before = last;  // 新节点的前驱指向原尾节点
//AN_Xml:        last.after = p;   // 原尾节点的后继指向新节点
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

这就是 LinkedHashMap 实现插入有序的核心机制:每次插入新节点时,通过重写 newNode 并调用 linkNodeLast,将新节点追加到双向链表尾部。这样遍历时从头节点 head 开始沿着 after 指针遍历,就能按插入顺序获取所有元素。

//AN_Xml:

同理,LinkedHashMap 也重写了 newTreeNode 方法,确保树节点插入时同样会被链接到链表尾部:

//AN_Xml:
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
//AN_Xml:    TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
//AN_Xml:    linkNodeLast(p);
//AN_Xml:    return p;
//AN_Xml:}
//AN_Xml:

remove 方法后置操作——afterNodeRemoval

//AN_Xml:

LinkedHashMap 并没有对 remove 方法进行重写,而是直接继承 HashMapremove 方法,为了保证键值对移除后双向链表中的节点也会同步被移除,LinkedHashMap 重写了 HashMap 的空实现方法 afterNodeRemoval

//AN_Xml:
final Node<K,V> removeNode(int hash, Object key, Object value,
//AN_Xml:                               boolean matchValue, boolean movable) {
//AN_Xml:        //略
//AN_Xml:            if (node != null && (!matchValue || (v = node.value) == value ||
//AN_Xml:                                 (value != null && value.equals(v)))) {
//AN_Xml:                if (node instanceof TreeNode)
//AN_Xml:                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//AN_Xml:                else if (node == p)
//AN_Xml:                    tab[index] = node.next;
//AN_Xml:                else
//AN_Xml:                    p.next = node.next;
//AN_Xml:                ++modCount;
//AN_Xml:                --size;
//AN_Xml:                //HashMap的removeNode完成元素移除后会调用afterNodeRemoval进行移除后置操作
//AN_Xml:                afterNodeRemoval(node);
//AN_Xml:                return node;
//AN_Xml:            }
//AN_Xml:        }
//AN_Xml:        return null;
//AN_Xml:    }
//AN_Xml://空实现
//AN_Xml:void afterNodeRemoval(Node<K,V> p) { }
//AN_Xml:

我们可以看到从 HashMap 继承来的 remove 方法内部调用的 removeNode 方法将节点从 bucket 删除后,调用了 afterNodeRemoval

//AN_Xml:
void afterNodeRemoval(Node<K,V> e) { // unlink
//AN_Xml:
//AN_Xml:    //获取当前节点p、以及e的前驱节点b和后继节点a
//AN_Xml:        LinkedHashMap.Entry<K,V> p =
//AN_Xml:            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//AN_Xml:    //将p的前驱和后继指针都设置为null,使其和前驱、后继节点断开联系
//AN_Xml:        p.before = p.after = null;
//AN_Xml:
//AN_Xml:    //如果前驱节点为空,则说明当前节点p是链表首节点,让head指针指向后继节点a即可
//AN_Xml:        if (b == null)
//AN_Xml:            head = a;
//AN_Xml:        else
//AN_Xml:        //如果前驱节点b不为空,则让b直接指向后继节点a
//AN_Xml:            b.after = a;
//AN_Xml:
//AN_Xml:    //如果后继节点为空,则说明当前节点p在链表末端,所以直接让tail指针指向前驱节点a即可
//AN_Xml:        if (a == null)
//AN_Xml:            tail = b;
//AN_Xml:        else
//AN_Xml:        //反之后继节点的前驱指针直接指向前驱节点
//AN_Xml:            a.before = b;
//AN_Xml:    }
//AN_Xml:

从源码可以看出, afterNodeRemoval 方法的整体操作就是让当前节点 p 和前驱节点、后继节点断开联系,等待 gc 回收,整体步骤为:

//AN_Xml:
    //AN_Xml:
  1. 获取当前节点 p、以及 p 的前驱节点 b 和后继节点 a。
  2. //AN_Xml:
  3. 让当前节点 p 和其前驱、后继节点断开联系。
  4. //AN_Xml:
  5. 尝试让前驱节点 b 指向后继节点 a,若 b 为空则说明当前节点 p 在链表首部,我们直接将 head 指向后继节点 a 即可。
  6. //AN_Xml:
  7. 尝试让后继节点 a 指向前驱节点 b,若 a 为空则说明当前节点 p 在链表末端,所以直接让 tail 指针指向前驱节点 b 即可。
  8. //AN_Xml:
//AN_Xml:

可以结合这张图理解,展示了 key 为 13 的元素被删除,也就是从链表中移除了这个元素。

//AN_Xml:

LinkedHashMap 删除元素 13

//AN_Xml:

看不太懂也没关系,知道这个方法的作用就够了,后续有时间再慢慢消化。

//AN_Xml:

put 方法后置操作——afterNodeInsertion

//AN_Xml:

同样的 LinkedHashMap 并没有实现插入方法,而是直接继承 HashMap 的所有插入方法交由用户使用,但为了维护双向链表访问的有序性,它做了这样两件事:

//AN_Xml:
    //AN_Xml:
  1. 重写 afterNodeAccess(上文提到过),如果当前被插入的 key 已存在与 map 中,因为 LinkedHashMap 的插入操作会将新节点追加至链表末尾,所以对于存在的 key 则调用 afterNodeAccess 将其放到链表末端。
  2. //AN_Xml:
  3. 重写了 HashMapafterNodeInsertion 方法,当 removeEldestEntry 返回 true 时,会将链表首节点移除。
  4. //AN_Xml:
//AN_Xml:

这一点我们可以在 HashMap 的插入操作核心方法 putVal 中看到。

//AN_Xml:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
//AN_Xml:                   boolean evict) {
//AN_Xml:          //略
//AN_Xml:            if (e != null) { // existing mapping for key
//AN_Xml:                V oldValue = e.value;
//AN_Xml:                if (!onlyIfAbsent || oldValue == null)
//AN_Xml:                    e.value = value;
//AN_Xml:                 //如果当前的key在map中存在,则调用afterNodeAccess
//AN_Xml:                afterNodeAccess(e);
//AN_Xml:                return oldValue;
//AN_Xml:            }
//AN_Xml:        }
//AN_Xml:        ++modCount;
//AN_Xml:        if (++size > threshold)
//AN_Xml:            resize();
//AN_Xml:         //调用插入后置方法,该方法被LinkedHashMap重写
//AN_Xml:        afterNodeInsertion(evict);
//AN_Xml:        return null;
//AN_Xml:    }
//AN_Xml:

上述步骤的源码上文已经解释过了,所以这里我们着重了解一下 afterNodeInsertion 的工作流程,假设我们的重写了 removeEldestEntry,当链表 size 超过 capacity 时,就返回 true。

//AN_Xml:
/**
//AN_Xml: * 判断size超过容量时返回true,告知LinkedHashMap移除最老的缓存项(即链表的第一个元素)
//AN_Xml: */
//AN_Xml:protected boolean removeEldestEntry(Map.Entry < K, V > eldest) {
//AN_Xml:    return size() > capacity;
//AN_Xml:}
//AN_Xml:

以下图为例,假设笔者最后新插入了一个不存在的节点 19,假设 capacity 为 4,所以 removeEldestEntry 返回 true,我们要将链表首节点移除。

//AN_Xml:

LinkedHashMap 中插入新元素 19

//AN_Xml:

移除的步骤很简单,查看链表首节点是否存在,若存在则断开首节点和后继节点的关系,并让首节点指针指向下一节点,所以 head 指针指向了 12,节点 10 成为没有任何引用指向的空对象,等待 GC。

//AN_Xml:

LinkedHashMap 中插入新元素 19

//AN_Xml:
void afterNodeInsertion(boolean evict) { // possibly remove eldest
//AN_Xml:        LinkedHashMap.Entry<K,V> first;
//AN_Xml:        //如果evict为true且队首元素不为空以及removeEldestEntry返回true,则说明我们需要最老的元素(即在链表首部的元素)移除。
//AN_Xml:        if (evict && (first = head) != null && removeEldestEntry(first)) {
//AN_Xml:          //获取链表首部的键值对的key
//AN_Xml:            K key = first.key;
//AN_Xml:            //调用removeNode将元素从HashMap的bucket中移除,并和LinkedHashMap的双向链表断开,等待gc回收
//AN_Xml:            removeNode(hash(key), key, null, false, true);
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:

从源码可以看出, afterNodeInsertion 方法完成了下面这些操作:

//AN_Xml:
    //AN_Xml:
  1. 判断 eldest 是否为 true,只有为 true 才能说明可能需要将最年长的键值对(即链表首部的元素)进行移除,具体是否具体要进行移除,还得确定链表是否为空((first = head) != null),以及 removeEldestEntry 方法是否返回 true,只有这两个方法返回 true 才能确定当前链表不为空,且链表需要进行移除操作了。
  2. //AN_Xml:
  3. 获取链表第一个元素的 key。
  4. //AN_Xml:
  5. 调用 HashMapremoveNode 方法,该方法我们上文提到过,它会将节点从 HashMap 的 bucket 中移除,并且 LinkedHashMap 还重写了 removeNode 中的 afterNodeRemoval 方法,所以这一步将通过调用 removeNode 将元素从 HashMap 的 bucket 中移除,并和 LinkedHashMap 的双向链表断开,等待 gc 回收。
  6. //AN_Xml:
//AN_Xml:

LinkedHashMap 和 HashMap 遍历性能比较

//AN_Xml:

LinkedHashMap 维护了一个双向链表来记录数据插入的顺序,因此在迭代遍历生成的迭代器的时候,是按照双向链表的路径进行遍历的。这一点相比于 HashMap 那种遍历整个 bucket 的方式来说,高效许多。

//AN_Xml:

这一点我们可以从两者的迭代器中得以印证,先来看看 HashMap 的迭代器,可以看到 HashMap 迭代键值对时会用到一个 nextNode 方法,该方法会返回 next 指向的下一个元素,并会从 next 开始遍历 bucket 找到下一个 bucket 中不为空的元素 Node。

//AN_Xml:
 final class EntryIterator extends HashIterator
//AN_Xml: implements Iterator < Map.Entry < K, V >> {
//AN_Xml:     public final Map.Entry < K,
//AN_Xml:     V > next() {
//AN_Xml:         return nextNode();
//AN_Xml:     }
//AN_Xml: }
//AN_Xml:
//AN_Xml: //获取下一个Node
//AN_Xml: final Node < K, V > nextNode() {
//AN_Xml:     Node < K, V > [] t;
//AN_Xml:     //获取下一个元素next
//AN_Xml:     Node < K, V > e = next;
//AN_Xml:     if (modCount != expectedModCount)
//AN_Xml:         throw new ConcurrentModificationException();
//AN_Xml:     if (e == null)
//AN_Xml:         throw new NoSuchElementException();
//AN_Xml:     //将next指向bucket中下一个不为空的Node
//AN_Xml:     if ((next = (current = e).next) == null && (t = table) != null) {
//AN_Xml:         do {} while (index < t.length && (next = t[index++]) == null);
//AN_Xml:     }
//AN_Xml:     return e;
//AN_Xml: }
//AN_Xml:

相比之下 LinkedHashMap 的迭代器则是直接使用通过 after 指针快速定位到当前节点的后继节点,简洁高效许多。

//AN_Xml:
 final class LinkedEntryIterator extends LinkedHashIterator
//AN_Xml: implements Iterator < Map.Entry < K, V >> {
//AN_Xml:     public final Map.Entry < K,
//AN_Xml:     V > next() {
//AN_Xml:         return nextNode();
//AN_Xml:     }
//AN_Xml: }
//AN_Xml: //获取下一个Node
//AN_Xml: final LinkedHashMap.Entry < K, V > nextNode() {
//AN_Xml:     //获取下一个节点next
//AN_Xml:     LinkedHashMap.Entry < K, V > e = next;
//AN_Xml:     if (modCount != expectedModCount)
//AN_Xml:         throw new ConcurrentModificationException();
//AN_Xml:     if (e == null)
//AN_Xml:         throw new NoSuchElementException();
//AN_Xml:     //current 指针指向当前节点
//AN_Xml:     current = e;
//AN_Xml:     //next直接当前节点的after指针快速定位到下一个节点
//AN_Xml:     next = e.after;
//AN_Xml:     return e;
//AN_Xml: }
//AN_Xml:

为了验证笔者所说的观点,笔者对这两个容器进行了压测,测试插入 1000w 和迭代 1000w 条数据的耗时,代码如下:

//AN_Xml:
int count = 1000_0000;
//AN_Xml:Map<Integer, Integer> hashMap = new HashMap<>();
//AN_Xml:Map<Integer, Integer> linkedHashMap = new LinkedHashMap<>();
//AN_Xml:
//AN_Xml:long start, end;
//AN_Xml:
//AN_Xml:start = System.currentTimeMillis();
//AN_Xml:for (int i = 0; i < count; i++) {
//AN_Xml:    hashMap.put(ThreadLocalRandom.current().nextInt(1, count), ThreadLocalRandom.current().nextInt(0, count));
//AN_Xml:}
//AN_Xml:end = System.currentTimeMillis();
//AN_Xml:System.out.println("map time putVal: " + (end - start));
//AN_Xml:
//AN_Xml:start = System.currentTimeMillis();
//AN_Xml:for (int i = 0; i < count; i++) {
//AN_Xml:    linkedHashMap.put(ThreadLocalRandom.current().nextInt(1, count), ThreadLocalRandom.current().nextInt(0, count));
//AN_Xml:}
//AN_Xml:end = System.currentTimeMillis();
//AN_Xml:System.out.println("linkedHashMap putVal time: " + (end - start));
//AN_Xml:
//AN_Xml:start = System.currentTimeMillis();
//AN_Xml:long num = 0;
//AN_Xml:for (Integer v : hashMap.values()) {
//AN_Xml:    num = num + v;
//AN_Xml:}
//AN_Xml:end = System.currentTimeMillis();
//AN_Xml:System.out.println("map get time: " + (end - start));
//AN_Xml:
//AN_Xml:start = System.currentTimeMillis();
//AN_Xml:for (Integer v : linkedHashMap.values()) {
//AN_Xml:    num = num + v;
//AN_Xml:}
//AN_Xml:end = System.currentTimeMillis();
//AN_Xml:System.out.println("linkedHashMap get time: " + (end - start));
//AN_Xml:System.out.println(num);
//AN_Xml:

从输出结果来看,因为 LinkedHashMap 需要维护双向链表的缘故,插入元素相较于 HashMap 会更耗时,但是有了双向链表明确的前后节点关系,迭代效率相对于前者高效了许多。不过,总体来说却别不大,毕竟数据量这么庞大。

//AN_Xml:
map time putVal: 5880
//AN_Xml:linkedHashMap putVal time: 7567
//AN_Xml:map get time: 143
//AN_Xml:linkedHashMap get time: 67
//AN_Xml:63208969074998
//AN_Xml:

LinkedHashMap 常见面试题

//AN_Xml:

什么是 LinkedHashMap?

//AN_Xml:

LinkedHashMap 是 Java 集合框架中 HashMap 的一个子类,它继承了 HashMap 的所有属性和方法,并且在 HashMap 的基础重写了 afterNodeRemovalafterNodeInsertionafterNodeAccess 方法。使之拥有顺序插入和访问有序的特性。

//AN_Xml:

LinkedHashMap 如何按照插入顺序迭代元素?

//AN_Xml:

LinkedHashMap 按照插入顺序迭代元素是它的默认行为。LinkedHashMap 内部维护了一个双向链表,用于记录元素的插入顺序。因此,当使用迭代器迭代元素时,元素的顺序与它们最初插入的顺序相同。

//AN_Xml:

LinkedHashMap 如何按照访问顺序迭代元素?

//AN_Xml:

LinkedHashMap 可以通过构造函数中的 accessOrder 参数指定按照访问顺序迭代元素。当 accessOrder 为 true 时,每次访问一个元素时,该元素会被移动到链表的末尾,因此下次访问该元素时,它就会成为链表中的最后一个元素,从而实现按照访问顺序迭代元素。

//AN_Xml:

LinkedHashMap 如何实现 LRU 缓存?

//AN_Xml:

accessOrder 设置为 true 并重写 removeEldestEntry 方法当链表大小超过容量时返回 true,使得每次访问一个元素时,该元素会被移动到链表的末尾。一旦插入操作让 removeEldestEntry 返回 true 时,视为缓存已满,LinkedHashMap 就会将链表首元素移除,由此我们就能实现一个 LRU 缓存。

//AN_Xml:

LinkedHashMap 和 HashMap 有什么区别?

//AN_Xml:

LinkedHashMapHashMap 都是 Java 集合框架中的 Map 接口的实现类。它们的最大区别在于迭代元素的顺序。HashMap 迭代元素的顺序是不确定的,而 LinkedHashMap 提供了按照插入顺序或访问顺序迭代元素的功能。此外,LinkedHashMap 内部维护了一个双向链表,用于记录元素的插入顺序或访问顺序,而 HashMap 则没有这个链表。因此,LinkedHashMap 的插入性能可能会比 HashMap 略低,但它提供了更多的功能并且迭代效率相较于 HashMap 更加高效。

//AN_Xml:

参考文献

//AN_Xml: //AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]> //AN_Xml: //AN_Xml: //AN_Xml: //AN_Xml: DelayQueue 源码分析 //AN_Xml: https://javaguide.cn/java/collection/delayqueue-source-code.html //AN_Xml: https://javaguide.cn/java/collection/delayqueue-source-code.html //AN_Xml: DelayQueue 源码分析 //AN_Xml: DelayQueue源码深度解析:详解延迟队列实现原理、Delayed接口使用、延时任务调度、订单超时取消等应用场景、基于PriorityQueue的线程安全设计。 //AN_Xml: Java //AN_Xml: Fri, 30 Jun 2023 11:46:59 GMT //AN_Xml: DelayQueue 简介 //AN_Xml:

DelayQueue 是 JUC 包(java.util.concurrent)为我们提供的延迟队列,用于实现延时任务比如订单下单 15 分钟未支付直接取消。它是 BlockingQueue 的一种,底层是一个基于 PriorityQueue 实现的一个无界队列,是线程安全的。关于PriorityQueue可以参考笔者编写的这篇文章:PriorityQueue 源码分析

//AN_Xml:

BlockingQueue 的实现类

//AN_Xml:

DelayQueue 中存放的元素必须实现 Delayed 接口,并且需要重写 getDelay()方法(计算是否到期)。

//AN_Xml:
public interface Delayed extends Comparable<Delayed> {
//AN_Xml:    long getDelay(TimeUnit unit);
//AN_Xml:}
//AN_Xml:

默认情况下, DelayQueue 会按照到期时间升序编排任务。只有当元素过期时(getDelay()方法返回值小于等于 0),才能从队列中取出。

//AN_Xml:

DelayQueue 发展史

//AN_Xml:
    //AN_Xml:
  • DelayQueue 最早是在 Java 5 中引入的,作为 java.util.concurrent 包中的一部分,用于支持基于时间的任务调度和缓存过期删除等场景,该版本仅仅支持延迟功能的实现,还未解决线程安全问题。
  • //AN_Xml:
  • 在 Java 6 中,DelayQueue 的实现进行了优化,通过使用 ReentrantLockCondition 解决线程安全及线程间交互的效率,提高了其性能和可靠性。
  • //AN_Xml:
  • 在 Java 7 中,DelayQueue 的实现进行了进一步的优化,通过使用 CAS 操作实现元素的添加和移除操作,提高了其并发操作性能。
  • //AN_Xml:
  • 在 Java 8 中,DelayQueue 的实现没有进行重大变化,但是在 java.time 包中引入了新的时间类,如 DurationInstant,使得使用 DelayQueue 进行基于时间的调度更加方便和灵活。
  • //AN_Xml:
  • 在 Java 9 中,DelayQueue 的实现进行了一些微小的改进,主要是对代码进行了一些优化和精简。
  • //AN_Xml:
//AN_Xml:

总的来说,DelayQueue 的发展史主要是通过优化其实现方式和提高其性能和可靠性,使其更加适用于基于时间的调度和缓存过期删除等场景。

//AN_Xml:

DelayQueue 常见使用场景示例

//AN_Xml:

我们这里希望任务可以按照我们预期的时间执行,例如提交 3 个任务,分别要求 1s、2s、3s 后执行,即使是乱序添加,1s 后要求 1s 执行的任务会准时执行。

//AN_Xml:

延迟任务

//AN_Xml:

对此我们可以使用 DelayQueue 来实现,所以我们首先需要继承 Delayed 实现 DelayedTask,实现 getDelay 方法以及优先级比较 compareTo

//AN_Xml:
/**
//AN_Xml: * 延迟任务
//AN_Xml: */
//AN_Xml:public class DelayedTask implements Delayed {
//AN_Xml:    /**
//AN_Xml:     * 任务到期时间
//AN_Xml:     */
//AN_Xml:    private long executeTime;
//AN_Xml:    /**
//AN_Xml:     * 任务
//AN_Xml:     */
//AN_Xml:    private Runnable task;
//AN_Xml:
//AN_Xml:    public DelayedTask(long delay, Runnable task) {
//AN_Xml:        this.executeTime = System.currentTimeMillis() + delay;
//AN_Xml:        this.task = task;
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    /**
//AN_Xml:     * 查看当前任务还有多久到期
//AN_Xml:     * @param unit
//AN_Xml:     * @return
//AN_Xml:     */
//AN_Xml:    @Override
//AN_Xml:    public long getDelay(TimeUnit unit) {
//AN_Xml:        return unit.convert(executeTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    /**
//AN_Xml:     * 延迟队列需要到期时间升序入队,所以我们需要实现compareTo进行到期时间比较
//AN_Xml:     * @param o
//AN_Xml:     * @return
//AN_Xml:     */
//AN_Xml:    @Override
//AN_Xml:    public int compareTo(Delayed o) {
//AN_Xml:        return Long.compare(this.executeTime, ((DelayedTask) o).executeTime);
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    public void execute() {
//AN_Xml:        task.run();
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

完成任务的封装之后,使用就很简单了,设置好多久到期然后将任务提交到延迟队列中即可。

//AN_Xml:
// 创建延迟队列,并添加任务
//AN_Xml:DelayQueue < DelayedTask > delayQueue = new DelayQueue < > ();
//AN_Xml:
//AN_Xml://分别添加1s、2s、3s到期的任务
//AN_Xml:delayQueue.add(new DelayedTask(2000, () -> System.out.println("Task 2")));
//AN_Xml:delayQueue.add(new DelayedTask(1000, () -> System.out.println("Task 1")));
//AN_Xml:delayQueue.add(new DelayedTask(3000, () -> System.out.println("Task 3")));
//AN_Xml:
//AN_Xml:// 取出任务并执行
//AN_Xml:while (!delayQueue.isEmpty()) {
//AN_Xml:  //阻塞获取最先到期的任务
//AN_Xml:  DelayedTask task = delayQueue.take();
//AN_Xml:  if (task != null) {
//AN_Xml:    task.execute();
//AN_Xml:  }
//AN_Xml:}
//AN_Xml:

从输出结果可以看出,即使笔者先提到 2s 到期的任务,1s 到期的任务 Task1 还是优先执行的。

//AN_Xml:
Task 1
//AN_Xml:Task 2
//AN_Xml:Task 3
//AN_Xml:

DelayQueue 源码解析

//AN_Xml:

这里以 JDK1.8 为例,分析一下 DelayQueue 的底层核心源码。

//AN_Xml:

DelayQueue 的类定义如下:

//AN_Xml:
public class DelayQueue<E extends Delayed> extends AbstractQueue<E> implements BlockingQueue<E>
//AN_Xml:{
//AN_Xml:  //...
//AN_Xml:}
//AN_Xml:

DelayQueue 继承了 AbstractQueue 类,实现了 BlockingQueue 接口。

//AN_Xml:

DelayQueue类图

//AN_Xml:

核心成员变量

//AN_Xml:

DelayQueue 的 4 个核心成员变量如下:

//AN_Xml:
//可重入锁,实现线程安全的关键
//AN_Xml:private final transient ReentrantLock lock = new ReentrantLock();
//AN_Xml://延迟队列底层存储数据的集合,确保元素按照到期时间升序排列
//AN_Xml:private final PriorityQueue<E> q = new PriorityQueue<E>();
//AN_Xml:
//AN_Xml://指向准备执行优先级最高的线程
//AN_Xml:private Thread leader = null;
//AN_Xml://实现多线程之间等待唤醒的交互
//AN_Xml:private final Condition available = lock.newCondition();
//AN_Xml:
    //AN_Xml:
  • lock : 我们都知道 DelayQueue 存取是线程安全的,所以为了保证存取元素时线程安全,我们就需要在存取时上锁,而 DelayQueue 就是基于 ReentrantLock 独占锁确保存取操作的线程安全。
  • //AN_Xml:
  • q : 延迟队列要求元素按照到期时间进行升序排列,所以元素添加时势必需要进行优先级排序,所以 DelayQueue 底层元素的存取都是通过这个优先队列 PriorityQueue 的成员变量 q 来管理的。
  • //AN_Xml:
  • leader : 延迟队列的任务只有到期之后才会执行,对于没有到期的任务只有等待,为了确保优先级最高的任务到期后可以即刻被执行,设计者就用 leader 来管理延迟任务,只有 leader 所指向的线程才具备定时等待任务到期执行的权限,而其他那些优先级低的任务只能无限期等待,直到 leader 线程执行完手头的延迟任务后唤醒它。
  • //AN_Xml:
  • available : 上文讲述 leader 线程时提到的等待唤醒操作的交互就是通过 available 实现的,假如线程 1 尝试在空的 DelayQueue 获取任务时,available 就会将其放入等待队列中。直到有一个线程添加一个延迟任务后通过 availablesignal 方法将其唤醒。
  • //AN_Xml:
//AN_Xml:

构造方法

//AN_Xml:

相较于其他的并发容器,延迟队列的构造方法比较简单,它只有两个构造方法,因为所有成员变量在类加载时都已经初始完成了,所以默认构造方法什么也没做。还有一个传入 Collection 对象的构造方法,它会将调用 addAll()方法将集合元素存到优先队列 q 中。

//AN_Xml:
public DelayQueue() {}
//AN_Xml:
//AN_Xml:public DelayQueue(Collection<? extends E> c) {
//AN_Xml:    this.addAll(c);
//AN_Xml:}
//AN_Xml:

添加元素

//AN_Xml:

DelayQueue 添加元素的方法无论是 addput 还是 offer,本质上就是调用一下 offer ,所以了解延迟队列的添加逻辑我们只需阅读 offer 方法即可。

//AN_Xml:

offer 方法的整体逻辑为:

//AN_Xml:
    //AN_Xml:
  1. 尝试获取 lock
  2. //AN_Xml:
  3. 如果上锁成功,则调 qoffer 方法将元素存放到优先队列中。
  4. //AN_Xml:
  5. 调用 peek 方法看看当前队首元素是否就是本次入队的元素,如果是则说明当前这个元素是即将到期的任务(即优先级最高的元素),于是将 leader 设置为空,通知因为队列为空时调用 take 等方法导致阻塞的线程来争抢元素。
  6. //AN_Xml:
  7. 上述步骤执行完成,释放 lock
  8. //AN_Xml:
  9. 返回 true。
  10. //AN_Xml:
//AN_Xml:

源码如下,笔者已详细注释,读者可自行参阅:

//AN_Xml:
public boolean offer(E e) {
//AN_Xml:    //尝试获取lock
//AN_Xml:    final ReentrantLock lock = this.lock;
//AN_Xml:    lock.lock();
//AN_Xml:    try {
//AN_Xml:        //如果上锁成功,则调q的offer方法将元素存放到优先队列中
//AN_Xml:        q.offer(e);
//AN_Xml:        //调用peek方法看看当前队首元素是否就是本次入队的元素,如果是则说明当前这个元素是即将到期的任务(即优先级最高的元素)
//AN_Xml:        if (q.peek() == e) {
//AN_Xml:            //将leader设置为空,通知调用取元素方法而阻塞的线程来争抢这个任务
//AN_Xml:            leader = null;
//AN_Xml:            available.signal();
//AN_Xml:        }
//AN_Xml:        return true;
//AN_Xml:    } finally {
//AN_Xml:        //上述步骤执行完成,释放lock
//AN_Xml:        lock.unlock();
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

获取元素

//AN_Xml:

DelayQueue 中获取元素的方式分为阻塞式和非阻塞式,先来看看逻辑比较复杂的阻塞式获取元素方法 take,为了让读者可以更直观的了解阻塞式获取元素的全流程,笔者将以 3 个线程并发获取元素为例讲述 take 的工作流程。

//AN_Xml:
//AN_Xml:

想要理解下面的内容,需要用到 AQS 相关的知识,推荐阅读下面这两篇文章:

//AN_Xml: //AN_Xml:
//AN_Xml:

1、首先, 3 个线程会尝试获取可重入锁 lock,假设我们现在有 3 个线程分别是 t1、t2、t3,随后 t1 得到了锁,而 t2、t3 没有抢到锁,故将这两个线程存入等待队列中。

//AN_Xml:

//AN_Xml:

2、紧接着 t1 开始进行元素获取的逻辑。

//AN_Xml:

3、线程 t1 首先会查看 DelayQueue 队列首元素是否为空。

//AN_Xml:

4、如果元素为空,则说明当前队列没有任何元素,故 t1 就会被阻塞存到 conditionWaiter 这个队列中。

//AN_Xml:

//AN_Xml:

注意,调用 await 之后 t1 就会释放 lcok 锁,假如 DelayQueue 持续为空,那么 t2、t3 也会像 t1 一样执行相同的逻辑并进入 conditionWaiter 队列中。

//AN_Xml:

//AN_Xml:

如果元素不为空,则判断当前任务是否到期,如果元素到期,则直接返回出去。如果元素未到期,则判断当前 leader 线程(DelayQueue 中唯一一个可以等待并获取元素的线程引用)是否为空,若不为空,则说明当前 leader 正在等待执行一个优先级比当前元素还高的元素到期,故当前线程 t1 只能调用 await 进入无限期等待,等到 leader 取得元素后唤醒。反之,若 leader 线程为空,则将当前线程设置为 leader 并进入有限期等待,到期后取出元素并返回。

//AN_Xml:

自此我们阻塞式获取元素的逻辑都已完成后,源码如下,读者可自行参阅:

//AN_Xml:
public E take() throws InterruptedException {
//AN_Xml:    // 尝试获取可重入锁,将底层AQS的state设置为1,并设置为独占锁
//AN_Xml:    final ReentrantLock lock = this.lock;
//AN_Xml:    lock.lockInterruptibly();
//AN_Xml:    try {
//AN_Xml:        for (;;) {
//AN_Xml:            //查看队列第一个元素
//AN_Xml:            E first = q.peek();
//AN_Xml:            //若为空,则将当前线程放入ConditionObject的等待队列中,并将底层AQS的state设置为0,表示释放锁并进入无限期等待
//AN_Xml:            if (first == null)
//AN_Xml:                available.await();
//AN_Xml:            else {
//AN_Xml:                //若元素不为空,则查看当前元素多久到期
//AN_Xml:                long delay = first.getDelay(NANOSECONDS);
//AN_Xml:                //如果小于0则说明已到期直接返回出去
//AN_Xml:                if (delay <= 0)
//AN_Xml:                    return q.poll();
//AN_Xml:                //如果大于0则说明任务还没到期,首先需要释放对这个元素的引用
//AN_Xml:                first = null; // don't retain ref while waiting
//AN_Xml:                //判断leader是否为空,如果不为空,则说明正有线程作为leader并等待一个任务到期,则当前线程进入无限期等待
//AN_Xml:                if (leader != null)
//AN_Xml:                    available.await();
//AN_Xml:                else {
//AN_Xml:                    //反之将我们的线程成为leader
//AN_Xml:                    Thread thisThread = Thread.currentThread();
//AN_Xml:                    leader = thisThread;
//AN_Xml:                    try {
//AN_Xml:                        //并进入有限期等待
//AN_Xml:                        available.awaitNanos(delay);
//AN_Xml:                    } finally {
//AN_Xml:                        //等待任务到期时,释放leader引用,进入下一次循环将任务return出去
//AN_Xml:                        if (leader == thisThread)
//AN_Xml:                            leader = null;
//AN_Xml:                    }
//AN_Xml:                }
//AN_Xml:            }
//AN_Xml:        }
//AN_Xml:    } finally {
//AN_Xml:        // 收尾逻辑:当leader为null,并且队列中有任务时,唤醒等待的获取元素的线程。
//AN_Xml:        if (leader == null && q.peek() != null)
//AN_Xml:            available.signal();
//AN_Xml:        //释放锁
//AN_Xml:        lock.unlock();
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

我们再来看看非阻塞的获取元素方法 poll ,逻辑比较简单,整体步骤如下:

//AN_Xml:
    //AN_Xml:
  1. 尝试获取可重入锁。
  2. //AN_Xml:
  3. 查看队列第一个元素,判断元素是否为空。
  4. //AN_Xml:
  5. 若元素为空,或者元素未到期,则直接返回空。
  6. //AN_Xml:
  7. 若元素不为空且到期了,直接调用 poll 返回出去。
  8. //AN_Xml:
  9. 释放可重入锁 lock
  10. //AN_Xml:
//AN_Xml:

源码如下,读者可自行参阅源码及注释:

//AN_Xml:
public E poll() {
//AN_Xml:    //尝试获取可重入锁
//AN_Xml:    final ReentrantLock lock = this.lock;
//AN_Xml:    lock.lock();
//AN_Xml:    try {
//AN_Xml:        //查看队列第一个元素,判断元素是否为空
//AN_Xml:        E first = q.peek();
//AN_Xml:
//AN_Xml:        //若元素为空,或者元素未到期,则直接返回空
//AN_Xml:        if (first == null || first.getDelay(NANOSECONDS) > 0)
//AN_Xml:            return null;
//AN_Xml:        else
//AN_Xml:            //若元素不为空且到期了,直接调用poll返回出去
//AN_Xml:            return q.poll();
//AN_Xml:    } finally {
//AN_Xml:        //释放可重入锁lock
//AN_Xml:        lock.unlock();
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

查看元素

//AN_Xml:

上文获取元素时都会调用到 peek 方法,peek 顾名思义仅仅窥探一下队列中的元素,它的步骤就 4 步:

//AN_Xml:
    //AN_Xml:
  1. 上锁。
  2. //AN_Xml:
  3. 调用优先队列 q 的 peek 方法查看索引 0 位置的元素。
  4. //AN_Xml:
  5. 释放锁。
  6. //AN_Xml:
  7. 将元素返回出去。
  8. //AN_Xml:
//AN_Xml:
public E peek() {
//AN_Xml:    final ReentrantLock lock = this.lock;
//AN_Xml:    lock.lock();
//AN_Xml:    try {
//AN_Xml:        return q.peek();
//AN_Xml:    } finally {
//AN_Xml:        lock.unlock();
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

DelayQueue 常见面试题

//AN_Xml:

DelayQueue 的实现原理是什么?

//AN_Xml:

DelayQueue 底层是使用优先队列 PriorityQueue 来存储元素,而 PriorityQueue 采用二叉小顶堆的思想确保值小的元素排在最前面,这就使得 DelayQueue 对于延迟任务优先级的管理就变得十分方便了。同时 DelayQueue 为了保证线程安全还用到了可重入锁 ReentrantLock,确保单位时间内只有一个线程可以操作延迟队列。最后,为了实现多线程之间等待和唤醒的交互效率,DelayQueue 还用到了 Condition,通过 Conditionawaitsignal 方法完成多线程之间的等待唤醒。

//AN_Xml:

DelayQueue 的实现是否线程安全?

//AN_Xml:

DelayQueue 的实现是线程安全的,它通过 ReentrantLock 实现了互斥访问和 Condition 实现了线程间的等待和唤醒操作,可以保证多线程环境下的安全性和可靠性。

//AN_Xml:

DelayQueue 的使用场景有哪些?

//AN_Xml:

DelayQueue 通常用于实现定时任务调度和缓存过期删除等场景。在定时任务调度中,需要将需要执行的任务封装成延迟任务对象,并将其添加到 DelayQueue 中,DelayQueue 会自动按照剩余延迟时间进行升序排序(默认情况),以保证任务能够按照时间先后顺序执行。对于缓存过期这个场景而言,在数据被缓存到内存之后,我们可以将缓存的 key 封装成一个延迟的删除任务,并将其添加到 DelayQueue 中,当数据过期时,拿到这个任务的 key,将这个 key 从内存中移除。

//AN_Xml:

DelayQueue 中 Delayed 接口的作用是什么?

//AN_Xml:

Delayed 接口定义了元素的剩余延迟时间(getDelay)和元素之间的比较规则(该接口继承了 Comparable 接口)。若希望元素能够存放到 DelayQueue 中,就必须实现 Delayed 接口的 getDelay() 方法和 compareTo() 方法,否则 DelayQueue 无法得知当前任务剩余时长和任务优先级的比较。

//AN_Xml:

DelayQueue 和 Timer/TimerTask 的区别是什么?

//AN_Xml:

DelayQueueTimer/TimerTask 都可以用于实现定时任务调度,但是它们的实现方式不同。DelayQueue 是基于优先级队列和堆排序算法实现的,可以实现多个任务按照时间先后顺序执行;而 Timer/TimerTask 是基于单线程实现的,只能按照任务的执行顺序依次执行,如果某个任务执行时间过长,会影响其他任务的执行。另外,DelayQueue 还支持动态添加和移除任务,而 Timer/TimerTask 只能在创建时指定任务。

//AN_Xml:

参考文献

//AN_Xml: //AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 常见加密算法总结 //AN_Xml: https://javaguide.cn/system-design/security/encryption-algorithms.html //AN_Xml: https://javaguide.cn/system-design/security/encryption-algorithms.html //AN_Xml: 常见加密算法总结 //AN_Xml: 常见加密算法详解,涵盖AES、RSA等对称与非对称加密算法及MD5、SHA等哈希算法的原理与应用场景。 //AN_Xml: 系统设计 //AN_Xml: Tue, 27 Jun 2023 12:05:39 GMT //AN_Xml: 加密算法是一种用数学方法对数据进行变换的技术,目的是保护数据的安全,防止被未经授权的人读取或修改。加密算法可以分为三大类:对称加密算法、非对称加密算法和哈希算法(也叫摘要算法)。

//AN_Xml:

日常开发中常见的需要用到加密算法的场景:

//AN_Xml:
    //AN_Xml:
  1. 保存在数据库中的密码需要加盐之后使用哈希算法(比如 BCrypt)进行加密。
  2. //AN_Xml:
  3. 保存在数据库中的银行卡号、身份号这类敏感数据需要使用对称加密算法(比如 AES)保存。
  4. //AN_Xml:
  5. 网络传输的敏感数据比如银行卡号、身份号需要用 HTTPS + 非对称加密算法(如 RSA)来保证传输数据的安全性。
  6. //AN_Xml:
  7. ……
  8. //AN_Xml:
//AN_Xml:

ps: 严格上来说,哈希算法其实不属于加密算法,只是可以用到某些加密场景中(例如密码加密),两者可以看作是并列关系。加密算法通常指的是可以将明文转换为密文,并且能够通过某种方式(如密钥)再将密文还原为明文的算法。而哈希算法是一种单向过程,它将输入信息转换成一个固定长度的、看似随机的哈希值,但这个过程是不可逆的,也就是说,不能从哈希值还原出原始信息。

//AN_Xml:

哈希算法

//AN_Xml:

哈希算法也叫散列函数或摘要算法,它的作用是对任意长度的数据生成一个固定长度的唯一标识,也叫哈希值、散列值或消息摘要(后文统称为哈希值)。

//AN_Xml:

哈希算法效果演示

//AN_Xml:

哈希算法的是不可逆的,你无法通过哈希之后的值再得到原值。

//AN_Xml:

哈希值的作用是可以用来验证数据的完整性和一致性。

//AN_Xml:

举两个实际的例子:

//AN_Xml:
    //AN_Xml:
  • 保存密码到数据库时使用哈希算法进行加密,可以通过比较用户输入密码的哈希值和数据库保存的哈希值是否一致,来判断密码是否正确。
  • //AN_Xml:
  • 我们下载一个文件时,可以通过比较文件的哈希值和官方提供的哈希值是否一致,来判断文件是否被篡改或损坏;
  • //AN_Xml:
//AN_Xml:

这种算法的特点是不可逆:

//AN_Xml:
    //AN_Xml:
  • 不能从哈希值还原出原始数据。
  • //AN_Xml:
  • 原始数据的任何改变都会导致哈希值的巨大变化。
  • //AN_Xml:
//AN_Xml:

哈希算法可以简单分为两类:

//AN_Xml:
    //AN_Xml:
  1. 加密哈希算法:安全性较高的哈希算法,它可以提供一定的数据完整性保护和数据防篡改能力,能够抵御一定的攻击手段,安全性相对较高,但性能较差,适用于对安全性要求较高的场景。例如 SHA2、SHA3、SM3、RIPEMD-160、BLAKE2、SipHash 等等。
  2. //AN_Xml:
  3. 非加密哈希算法:安全性相对较低的哈希算法,易受到暴力破解、冲突攻击等攻击手段的影响,但性能较高,适用于对安全性没有要求的业务场景。例如 CRC32、MurMurHash3、SipHash 等等。
  4. //AN_Xml:
//AN_Xml:

除了这两种之外,还有一些特殊的哈希算法,例如安全性更高的慢哈希算法

//AN_Xml:

常见的哈希算法有:

//AN_Xml:
    //AN_Xml:
  • MD(Message Digest,消息摘要算法):MD2、MD4、MD5 等,已经不被推荐使用。
  • //AN_Xml:
  • SHA(Secure Hash Algorithm,安全哈希算法):SHA-1 系列安全性低,SHA2,SHA3 系列安全性较高。
  • //AN_Xml:
  • 国密算法:例如 SM2、SM3、SM4,其中 SM2 为非对称加密算法,SM4 为对称加密算法,SM3 为哈希算法(安全性及效率和 SHA-256 相当,但更适合国内的应用环境)。
  • //AN_Xml:
  • Bcrypt(密码哈希算法):基于 Blowfish 加密算法的密码哈希算法,专门为密码加密而设计,安全性高,属于慢哈希算法。
  • //AN_Xml:
  • MAC(Message Authentication Code,消息认证码算法):HMAC 是一种基于哈希的 MAC,可以与任何安全的哈希算法结合使用,例如 SHA-256。
  • //AN_Xml:
  • CRC:(Cyclic Redundancy Check,循环冗余校验):CRC32 是一种 CRC 算法,它的特点是生成 32 位的校验值,通常用于数据完整性校验、文件校验等场景。
  • //AN_Xml:
  • SipHash:加密哈希算法,它的设计目的是在速度和安全性之间达到一个平衡,用于防御哈希泛洪 DoS 攻击。Rust 默认使用 SipHash 作为哈希算法,从 Redis4.0 开始,哈希算法被替换为 SipHash。
  • //AN_Xml:
  • MurMurHash:经典快速的非加密哈希算法,目前最新的版本是 MurMurHash3,可以生成 32 位或者 128 位哈希值;
  • //AN_Xml:
  • ……
  • //AN_Xml:
//AN_Xml:

哈希算法一般是不需要密钥的,但也存在部分特殊哈希算法需要密钥。例如,MAC 和 SipHash 就是一种基于密钥的哈希算法,它在哈希算法的基础上增加了一个密钥,使得只有知道密钥的人才能验证数据的完整性和来源。

//AN_Xml:

MD

//AN_Xml:

MD 算法有多个版本,包括 MD2、MD4、MD5 等,其中 MD5 是最常用的版本,它可以生成一个 128 位(16 字节)的哈希值。从安全性上说:MD5 > MD4 > MD2。除了这些版本,还有一些基于 MD4 或 MD5 改进的算法,如 RIPEMD、HAVAL 等。

//AN_Xml:

即使是最安全 MD 算法 MD5 也存在被破解的风险,攻击者可以通过暴力破解或彩虹表攻击等方式,找到与原始数据相同的哈希值,从而破解数据。

//AN_Xml:

为了增加破解难度,通常可以选择加盐。盐(Salt)在密码学中,是指通过在密码任意固定位置插入特定的字符串,让哈希后的结果和使用原始密码的哈希结果不相符,这种过程称之为“加盐”。

//AN_Xml:

加盐之后就安全了吗?并不一定,这只是增加了破解难度,不代表无法破解。而且,MD5 算法本身就存在弱碰撞(Collision)问题,即多个不同的输入产生相同的 MD5 值。

//AN_Xml:

因此,MD 算法已经不被推荐使用,建议使用更安全的哈希算法比如 SHA-2、Bcrypt。

//AN_Xml:

Java 提供了对 MD 算法系列的支持,包括 MD2、MD5。

//AN_Xml:

MD5 代码示例(未加盐):

//AN_Xml:
String originalString = "Java学习 + 面试指南:javaguide.cn";
//AN_Xml:// 创建MD5摘要对象
//AN_Xml:MessageDigest messageDigest = MessageDigest.getInstance("MD5");
//AN_Xml:messageDigest.update(originalString.getBytes(StandardCharsets.UTF_8));
//AN_Xml:// 计算哈希值
//AN_Xml:byte[] result = messageDigest.digest();
//AN_Xml:// 将哈希值转换为十六进制字符串
//AN_Xml:String hexString = new HexBinaryAdapter().marshal(result);
//AN_Xml:System.out.println("Original String: " + originalString);
//AN_Xml:System.out.println("MD5 Hash: " + hexString.toLowerCase());
//AN_Xml:

输出:

//AN_Xml:
Original String: Java学习 + 面试指南:javaguide.cn
//AN_Xml:MD5 Hash: fb246796f5b1b60d4d0268c817c608fa
//AN_Xml:

SHA

//AN_Xml:

SHA(Secure Hash Algorithm)系列算法是一组密码哈希算法,用于将任意长度的数据映射为固定长度的哈希值。SHA 系列算法由美国国家安全局(NSA)于 1993 年设计,目前共有 SHA-1、SHA-2、SHA-3 三种版本。

//AN_Xml:

SHA-1 算法将任意长度的数据映射为 160 位的哈希值。然而,SHA-1 算法存在一些严重的缺陷,比如安全性低,容易受到碰撞攻击和长度扩展攻击。因此,SHA-1 算法已经不再被推荐使用。 SHA-2 家族(如 SHA-256、SHA-384、SHA-512 等)和 SHA-3 系列是 SHA-1 算法的替代方案,它们都提供了更高的安全性和更长的哈希值长度。

//AN_Xml:

SHA-2 家族是在 SHA-1 算法的基础上改进而来的,它们采用了更复杂的运算过程和更多的轮次,使得攻击者更难以通过预计算或巧合找到碰撞。

//AN_Xml:

为了寻找一种更安全和更先进的密码哈希算法,美国国家标准与技术研究院(National Institute of Standards and Technology,简称 NIST)在 2007 年公开征集 SHA-3 的候选算法。NIST 一共收到了 64 个算法方案,经过多轮的评估和筛选,最终在 2012 年宣布 Keccak 算法胜出,成为 SHA-3 的标准算法(SHA-3 与 SHA-2 算法没有直接的关系)。 Keccak 算法具有与 MD 和 SHA-1/2 完全不同的设计思路,即海绵结构(Sponge Construction),使得传统攻击方法无法直接应用于 SHA-3 的攻击中(能够抵抗目前已知的所有攻击方式包括碰撞攻击、长度扩展攻击、差分攻击等)。

//AN_Xml:

由于 SHA-2 算法还没有出现重大的安全漏洞,而且在软件中的效率更高,所以大多数人还是倾向于使用 SHA-2 算法。

//AN_Xml:

相比 MD5 算法,SHA-2 算法之所以更强,主要有两个原因:

//AN_Xml:
    //AN_Xml:
  • 哈希值长度更长:例如 SHA-256 算法的哈希值长度为 256 位,而 MD5 算法的哈希值长度为 128 位,这就提高了攻击者暴力破解或者彩虹表攻击的难度。
  • //AN_Xml:
  • 更强的碰撞抗性:SHA 算法采用了更复杂的运算过程和更多的轮次,使得攻击者更难以通过预计算或巧合找到碰撞。目前还没有找到任何两个不同的数据,它们的 SHA-256 哈希值相同。
  • //AN_Xml:
//AN_Xml:

当然,SHA-2 也不是绝对安全的,也有被暴力破解或者彩虹表攻击的风险,所以,在实际的应用中,加盐还是必不可少的。

//AN_Xml:

Java 提供了对 SHA 算法系列的支持,包括 SHA-1、SHA-256、SHA-384 和 SHA-512。

//AN_Xml:

SHA-256 代码示例(未加盐):

//AN_Xml:
String originalString = "Java学习 + 面试指南:javaguide.cn";
//AN_Xml:// 创建SHA-256摘要对象
//AN_Xml:MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
//AN_Xml:messageDigest.update(originalString.getBytes());
//AN_Xml:// 计算哈希值
//AN_Xml:byte[] result = messageDigest.digest();
//AN_Xml:// 将哈希值转换为十六进制字符串
//AN_Xml:String hexString = new HexBinaryAdapter().marshal(result);
//AN_Xml:System.out.println("Original String: " + originalString);
//AN_Xml:System.out.println("SHA-256 Hash: " + hexString.toLowerCase());
//AN_Xml:

输出:

//AN_Xml:
Original String: Java学习 + 面试指南:javaguide.cn
//AN_Xml:SHA-256 Hash: 184eb7e1d7fb002444098c9bde3403c6f6722c93ecfac242c0e35cd9ed3b41cd
//AN_Xml:

Bcrypt

//AN_Xml:

Bcrypt 算法是一种基于 Blowfish 加密算法的密码哈希算法,专门为密码加密而设计,安全性高。

//AN_Xml:

由于 Bcrypt 采用了 salt(盐) 和 cost(成本) 两种机制,它可以有效地防止彩虹表攻击和暴力破解攻击,从而保证密码的安全性。salt 是一个随机生成的字符串,用于和密码混合,增加密码的复杂度和唯一性。cost 是一个数值参数,用于控制 Bcrypt 算法的迭代次数,增加密码哈希的计算时间和资源消耗。

//AN_Xml:

Bcrypt 算法可以根据实际情况进行调整加密的复杂度,可以设置不同的 cost 值和 salt 值,从而满足不同的安全需求,灵活性很高。

//AN_Xml:

Java 应用程序的安全框架 Spring Security 支持多种密码编码器,其中 BCryptPasswordEncoder 是官方推荐的一种,它使用 BCrypt 算法对用户的密码进行加密存储。

//AN_Xml:
@Bean
//AN_Xml:public PasswordEncoder passwordEncoder(){
//AN_Xml:    return new BCryptPasswordEncoder();
//AN_Xml:}
//AN_Xml:

对称加密

//AN_Xml:

对称加密算法是指加密和解密使用同一个密钥的算法,也叫共享密钥加密算法。

//AN_Xml:

对称加密

//AN_Xml:

常见的对称加密算法有 DES、3DES、AES 等。

//AN_Xml:

DES 和 3DES

//AN_Xml:

DES(Data Encryption Standard)使用 64 位的密钥(有效秘钥长度为 56 位,8 位奇偶校验位)和 64 位的明文进行加密。

//AN_Xml:

虽然 DES 一次只能加密 64 位,但我们只需要把明文划分成 64 位一组的块,就可以实现任意长度明文的加密。如果明文长度不是 64 位的倍数,必须进行填充,常用的模式有 PKCS5Padding, PKCS7Padding, NOPADDING。

//AN_Xml:

DES 加密算法的基本思想是将 64 位的明文分成两半,然后对每一半进行多轮的变换,最后再合并成 64 位的密文。这些变换包括置换、异或、选择、移位等操作,每一轮都使用了一个子密钥,而这些子密钥都是由同一个 56 位的主密钥生成的。DES 加密算法总共进行了 16 轮变换,最后再进行一次逆置换,得到最终的密文。

//AN_Xml:

DES(Data Encryption Standard)

//AN_Xml:

这是一个经典的对称加密算法,但也有明显的缺陷,即 56 位的密钥安全性不足,已被证实可以在短时间内破解。

//AN_Xml:

为了提高 DES 算法的安全性,人们提出了一些变种或者替代方案,例如 3DES(Triple DES)。

//AN_Xml:

3DES(Triple DES)是 DES 向 AES 过渡的加密算法,它使用 2 个或者 3 个 56 位的密钥对数据进行三次加密。3DES 相当于是对每个数据块应用三次 DES 的对称加密算法。

//AN_Xml:

为了兼容普通的 DES,3DES 并没有直接使用 加密->加密->加密 的方式,而是采用了加密->解密->加密 的方式。当三种密钥均相同时,前两步相互抵消,相当于仅实现了一次加密,因此可实现对普通 DES 加密算法的兼容。3DES 比 DES 更为安全,但其处理速度不高。

//AN_Xml:

AES

//AN_Xml:

AES(Advanced Encryption Standard)算法是一种更先进的对称密钥加密算法,它使用 128 位、192 位或 256 位的密钥对数据进行加密或解密,密钥越长,安全性越高。

//AN_Xml:

AES 也是一种分组(或者叫块)密码,分组长度只能是 128 位,也就是说,每个分组为 16 个字节。AES 加密算法有多种工作模式(mode of operation),如:ECB、CBC、OFB、CFB、CTR、XTS、OCB、GCM(目前使用最广泛的模式)。不同的模式参数和加密流程不同,但是核心仍然是 AES 算法。

//AN_Xml:

和 DES 类似,对于不是 128 位倍数的明文需要进行填充,常用的填充模式有 PKCS5Padding, PKCS7Padding, NOPADDING。不过,AES-GCM 是流加密算法,可以对任意长度的明文进行加密,所以对应的填充模式为 NoPadding,即无需填充。

//AN_Xml:

AES 的速度比 3DES 快,而且更安全。

//AN_Xml:

AES(Advanced Encryption Standard)

//AN_Xml:

DES 算法和 AES 算法简单对比(图片来自于:RSA vs. AES Encryption: Key Differences Explained):

//AN_Xml:

DES 和 AES 对比

//AN_Xml:

基于 Java 实现 AES 算法代码示例:

//AN_Xml:
private static final String AES_ALGORITHM = "AES";
//AN_Xml:// AES密钥
//AN_Xml:private static final String AES_SECRET_KEY = "4128D9CDAC7E2F82951CBAF7FDFE675B";
//AN_Xml:// AES加密模式为GCM,填充方式为NoPadding
//AN_Xml:// AES-GCM 是流加密(Stream cipher)算法,所以对应的填充模式为 NoPadding,即无需填充。
//AN_Xml:private static final String AES_TRANSFORMATION = "AES/GCM/NoPadding";
//AN_Xml:// 加密器
//AN_Xml:private static Cipher encryptionCipher;
//AN_Xml:// 解密器
//AN_Xml:private static Cipher decryptionCipher;
//AN_Xml:
//AN_Xml:/**
//AN_Xml: * 完成一些初始化工作
//AN_Xml: */
//AN_Xml:public static void init() throws Exception {
//AN_Xml:    // 将AES密钥转换为SecretKeySpec对象
//AN_Xml:    SecretKeySpec secretKeySpec = new SecretKeySpec(AES_SECRET_KEY.getBytes(), AES_ALGORITHM);
//AN_Xml:    // 使用指定的AES加密模式和填充方式获取对应的加密器并初始化
//AN_Xml:    encryptionCipher = Cipher.getInstance(AES_TRANSFORMATION);
//AN_Xml:    encryptionCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
//AN_Xml:    // 使用指定的AES加密模式和填充方式获取对应的解密器并初始化
//AN_Xml:    decryptionCipher = Cipher.getInstance(AES_TRANSFORMATION);
//AN_Xml:    decryptionCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new GCMParameterSpec(128, encryptionCipher.getIV()));
//AN_Xml:}
//AN_Xml:
//AN_Xml:/**
//AN_Xml: * 加密
//AN_Xml: */
//AN_Xml:public static String encrypt(String data) throws Exception {
//AN_Xml:    byte[] dataInBytes = data.getBytes();
//AN_Xml:    // 加密数据
//AN_Xml:    byte[] encryptedBytes = encryptionCipher.doFinal(dataInBytes);
//AN_Xml:    return Base64.getEncoder().encodeToString(encryptedBytes);
//AN_Xml:}
//AN_Xml:
//AN_Xml:/**
//AN_Xml: * 解密
//AN_Xml: */
//AN_Xml:public static String decrypt(String encryptedData) throws Exception {
//AN_Xml:    byte[] dataInBytes = Base64.getDecoder().decode(encryptedData);
//AN_Xml:    // 解密数据
//AN_Xml:    byte[] decryptedBytes = decryptionCipher.doFinal(dataInBytes);
//AN_Xml:    return new String(decryptedBytes, StandardCharsets.UTF_8);
//AN_Xml:}
//AN_Xml:
//AN_Xml:public static void main(String[] args) throws Exception {
//AN_Xml:    String originalString = "Java学习 + 面试指南:javaguide.cn";
//AN_Xml:    init();
//AN_Xml:    String encryptedData = encrypt(originalString);
//AN_Xml:    String decryptedData = decrypt(encryptedData);
//AN_Xml:    System.out.println("Original String: " + originalString);
//AN_Xml:    System.out.println("AES Encrypted Data : " + encryptedData);
//AN_Xml:    System.out.println("AES Decrypted Data : " + decryptedData);
//AN_Xml:}
//AN_Xml:

输出:

//AN_Xml:
Original String: Java学习 + 面试指南:javaguide.cn
//AN_Xml:AES Encrypted Data : E1qTkK91suBqToag7WCyoFP9uK5hR1nSfM6p+oBlYj71bFiIVnk5TsQRT+zpjv8stha7oyKi3jQ=
//AN_Xml:AES Decrypted Data : Java学习 + 面试指南:javaguide.cn
//AN_Xml:

非对称加密

//AN_Xml:

非对称加密算法是指加密和解密使用不同的密钥的算法,也叫公开密钥加密算法。这两个密钥互不相同,一个称为公钥,另一个称为私钥。公钥可以公开给任何人使用,私钥则要保密。

//AN_Xml:

如果用公钥加密数据,只能用对应的私钥解密(加密);如果用私钥加密数据,只能用对应的公钥解密(签名)。这样就可以实现数据的安全传输和身份认证。

//AN_Xml:

非对称加密

//AN_Xml:

常见的非对称加密算法有 RSA、DSA、ECC 等。

//AN_Xml:

RSA

//AN_Xml:

RSA(Rivest–Shamir–Adleman algorithm)算法是一种基于大数分解的困难性的非对称加密算法,它需要选择两个大素数作为私钥的一部分,然后计算出它们的乘积作为公钥的一部分(寻求两个大素数比较简单,而将它们的乘积进行因式分解却极其困难)。RSA 算法原理的详细介绍,可以参考这篇文章:你真的了解 RSA 加密算法吗? - 小傅哥

//AN_Xml:

RSA 算法的安全性依赖于大数分解的难度,目前已经有 512 位和 768 位的 RSA 公钥被成功分解,因此建议使用 2048 位或以上的密钥长度。

//AN_Xml:

RSA 算法的优点是简单易用,可以用于数据加密和数字签名;缺点是运算速度慢,不适合大量数据的加密。

//AN_Xml:

RSA 算法是是目前应用最广泛的非对称加密算法,像 SSL/TLS、SSH 等协议中就用到了 RSA 算法。

//AN_Xml:

HTTPS 证书签名算法中带RSA 加密的SHA-256

//AN_Xml:

基于 Java 实现 RSA 算法代码示例:

//AN_Xml:
private static final String RSA_ALGORITHM = "RSA";
//AN_Xml:
//AN_Xml:/**
//AN_Xml: * 生成RSA密钥对
//AN_Xml: */
//AN_Xml:public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
//AN_Xml:    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM);
//AN_Xml:    // 密钥大小为2048位
//AN_Xml:    keyPairGenerator.initialize(2048);
//AN_Xml:    return keyPairGenerator.generateKeyPair();
//AN_Xml:}
//AN_Xml:
//AN_Xml:/**
//AN_Xml: * 使用公钥加密数据
//AN_Xml: */
//AN_Xml:public static String encrypt(String data, PublicKey publicKey) throws Exception {
//AN_Xml:    Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
//AN_Xml:    cipher.init(Cipher.ENCRYPT_MODE, publicKey);
//AN_Xml:    byte[] encryptedData = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
//AN_Xml:    return Base64.getEncoder().encodeToString(encryptedData);
//AN_Xml:}
//AN_Xml:
//AN_Xml:/**
//AN_Xml: * 使用私钥解密数据
//AN_Xml: */
//AN_Xml:public static String decrypt(String encryptedData, PrivateKey privateKey) throws Exception {
//AN_Xml:    byte[] decodedData = Base64.getDecoder().decode(encryptedData);
//AN_Xml:    Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
//AN_Xml:    cipher.init(Cipher.DECRYPT_MODE, privateKey);
//AN_Xml:    byte[] decryptedData = cipher.doFinal(decodedData);
//AN_Xml:    return new String(decryptedData, StandardCharsets.UTF_8);
//AN_Xml:}
//AN_Xml:
//AN_Xml:public static void main(String[] args) throws Exception {
//AN_Xml:    KeyPair keyPair = generateKeyPair();
//AN_Xml:    PublicKey publicKey = keyPair.getPublic();
//AN_Xml:    PrivateKey privateKey = keyPair.getPrivate();
//AN_Xml:    String originalString = "Java学习 + 面试指南:javaguide.cn";
//AN_Xml:    String encryptedData = encrypt(originalString, publicKey);
//AN_Xml:    String decryptedData = decrypt(encryptedData, privateKey);
//AN_Xml:    System.out.println("Original String: " + originalString);
//AN_Xml:    System.out.println("RSA Encrypted Data : " + encryptedData);
//AN_Xml:    System.out.println("RSA Decrypted Data : " + decryptedData);
//AN_Xml:}
//AN_Xml:

输出:

//AN_Xml:
Original String: Java学习 + 面试指南:javaguide.cn
//AN_Xml:RSA Encrypted Data : T9ey/CEPUAhZm4UJjuVNIg8RPd1fQ32S9w6+rvOKxmuMumkJY2daFfWuCn8A73Mk5bL6TigOJI0GHfKOt/W2x968qLM3pBGCcPX17n4pR43f32IIIz9iPdgF/INOqDxP5ZAtCDvTiuzcSgDHXqiBSK5TDjtj7xoGjfudYAXICa8pWitnqDgJYoo2J0F8mKzxoi8D8eLE455MEx8ZT1s7FUD/z7/H8CfShLRbO9zq/zFI06TXn123ufg+F4lDaq/5jaIxGVEUB/NFeX4N6OZCFHtAV32mw71BYUadzI9TgvkkUr1rSKmQ0icNhnRdKedJokGUh8g9QQ768KERu92Ibg==
//AN_Xml:RSA Decrypted Data : Java学习 + 面试指南:javaguide.cn
//AN_Xml:

DSA

//AN_Xml:

DSA(Digital Signature Algorithm)算法是一种基于离散对数的困难性的非对称加密算法,它需要选择一个素数 q 和一个 q 的倍数 p 作为私钥的一部分,然后计算出一个模 p 的原根 g 和一个模 q 的整数 y 作为公钥的一部分。DSA 算法的安全性依赖于离散对数的难度,目前已经有 1024 位的 DSA 公钥被成功破解,因此建议使用 2048 位或以上的密钥长度。

//AN_Xml:

DSA 算法的优点是数字签名速度快,适合生成数字证书;缺点是不能用于数据加密,且签名过程需要随机数。

//AN_Xml:

DSA 算法签名过程:

//AN_Xml:
    //AN_Xml:
  1. 使用消息摘要算法对要发送的数据进行加密,生成一个信息摘要,也就是一个短的、唯一的、不可逆的数据表示。
  2. //AN_Xml:
  3. 发送方用自己的 DSA 私钥对信息摘要再进行加密,形成一个数字签名,也就是一个可以证明数据来源和完整性的数据附加。
  4. //AN_Xml:
  5. 将原始数据和数字签名一起通过互联网传送给接收方。
  6. //AN_Xml:
  7. 接收方用发送方的公钥对数字签名进行解密,得到信息摘要。同时,接收方也用消息摘要算法对收到的原始数据进行加密,得到另一个信息摘要。接收方将两个信息摘要进行比较,如果两者一致,则说明在传送过程中数据没有被篡改或损坏;否则,则说明数据已经失去了安全性和保密性。
  8. //AN_Xml:
//AN_Xml:

DSA 算法签名过程

//AN_Xml:

总结

//AN_Xml:

这篇文章介绍了三种加密算法:哈希算法、对称加密算法和非对称加密算法。

//AN_Xml:
    //AN_Xml:
  • 哈希算法是一种用数学方法对数据生成一个固定长度的唯一标识的技术,可以用来验证数据的完整性和一致性,常见的哈希算法有 MD、SHA、MAC 等。
  • //AN_Xml:
  • 对称加密算法是一种加密和解密使用同一个密钥的算法,可以用来保护数据的安全性和保密性,常见的对称加密算法有 DES、3DES、AES 等。
  • //AN_Xml:
  • 非对称加密算法是一种加密和解密使用不同的密钥的算法,可以用来实现数据的安全传输和身份认证,常见的非对称加密算法有 RSA、DSA、ECC 等。
  • //AN_Xml:
//AN_Xml:

参考

//AN_Xml: //AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Java NIO 核心知识总结 //AN_Xml: https://javaguide.cn/java/io/nio-basis.html //AN_Xml: https://javaguide.cn/java/io/nio-basis.html //AN_Xml: Java NIO 核心知识总结 //AN_Xml: Java NIO核心知识全面总结:详解Channel通道、Buffer缓冲区、Selector选择器三大核心组件、非阻塞IO实现、零拷贝技术、与传统IO性能对比。 //AN_Xml: Java //AN_Xml: Mon, 26 Jun 2023 15:16:09 GMT //AN_Xml: 在学习 NIO 之前,需要先了解一下计算机 I/O 模型的基础理论知识。还不了解的话,可以参考我写的这篇文章:Java IO 模型详解

//AN_Xml:

NIO 简介

//AN_Xml:

在传统的 Java I/O 模型(BIO)中,I/O 操作是以阻塞的方式进行的。也就是说,当一个线程执行一个 I/O 操作时,它会被阻塞直到操作完成。这种阻塞模型在处理多个并发连接时可能会导致性能瓶颈,因为需要为每个连接创建一个线程,而线程的创建和切换都是有开销的。

//AN_Xml:

为了解决这个问题,在 Java1.4 版本引入了一种新的 I/O 模型 — NIO (New IO,也称为 Non-blocking IO) 。NIO 弥补了同步阻塞 I/O 的不足,它在标准 Java 代码中提供了非阻塞、面向缓冲、基于通道的 I/O,可以使用少量的线程来处理多个连接,大大提高了 I/O 效率和并发。

//AN_Xml:

下图是 BIO、NIO 和 AIO 处理客户端请求的简单对比图(关于 AIO 的介绍,可以看我写的这篇文章:Java IO 模型详解,不是重点,了解即可)。

//AN_Xml:

BIO、NIO 和 AIO 对比

//AN_Xml:

⚠️需要注意:使用 NIO 并不一定意味着高性能,它的性能优势主要体现在高并发和高延迟的网络环境下。当连接数较少、并发程度较低或者网络传输速度较快时,NIO 的性能并不一定优于传统的 BIO 。

//AN_Xml:

NIO 核心组件

//AN_Xml:

NIO 主要包括以下三个核心组件:

//AN_Xml:
    //AN_Xml:
  • Buffer(缓冲区):NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。
  • //AN_Xml:
  • Channel(通道):Channel 是一个双向的、可读可写的数据传输通道,NIO 通过 Channel 来实现数据的输入输出。通道是一个抽象的概念,它可以代表文件、套接字或者其他数据源之间的连接。
  • //AN_Xml:
  • Selector(选择器):允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到 Selector 上,由 Selector 来分配线程来处理事件。
  • //AN_Xml:
//AN_Xml:

三者的关系如下图所示(暂时不理解没关系,后文会详细介绍):

//AN_Xml:

Buffer、Channel和Selector三者之间的关系

//AN_Xml:

下面详细介绍一下这三个组件。

//AN_Xml:

Buffer(缓冲区)

//AN_Xml:

在传统的 BIO 中,数据的读写是面向流的, 分为字节流和字符流。

//AN_Xml:

在 Java 1.4 的 NIO 库中,所有数据都是用缓冲区处理的,这是新库和之前的 BIO 的一个重要区别,有点类似于 BIO 中的缓冲流。NIO 在读取数据时,它是直接读到缓冲区中的。在写入数据时,写入到缓冲区中。 使用 NIO 在读写数据时,都是通过缓冲区进行操作。

//AN_Xml:

Buffer 的子类如下图所示。其中,最常用的是 ByteBuffer,它可以用来存储和操作字节数据。

//AN_Xml:

Buffer 的子类

//AN_Xml:

你可以将 Buffer 理解为一个数组,IntBufferFloatBufferCharBuffer 等分别对应 int[]float[]char[] 等。

//AN_Xml:

为了更清晰地认识缓冲区,我们来简单看看Buffer 类中定义的四个成员变量:

//AN_Xml:
public abstract class Buffer {
//AN_Xml:    // Invariants: mark <= position <= limit <= capacity
//AN_Xml:    private int mark = -1;
//AN_Xml:    private int position = 0;
//AN_Xml:    private int limit;
//AN_Xml:    private int capacity;
//AN_Xml:}
//AN_Xml:

这四个成员变量的具体含义如下:

//AN_Xml:
    //AN_Xml:
  1. 容量(capacity):Buffer可以存储的最大数据量,Buffer创建时设置且不可改变;
  2. //AN_Xml:
  3. 界限(limit):Buffer 中可以读/写数据的边界。写模式下,limit 代表最多能写入的数据,一般等于 capacity(可以通过limit(int newLimit)方法设置);读模式下,limit 等于 Buffer 中实际写入的数据大小。
  4. //AN_Xml:
  5. 位置(position):下一个可以被读写的数据的位置(索引)。从写操作模式到读操作模式切换的时候(flip),position 都会归零,这样就可以从头开始读写了。
  6. //AN_Xml:
  7. 标记(mark):Buffer允许将位置直接定位到该标记处,这是一个可选属性;
  8. //AN_Xml:
//AN_Xml:

并且,上述变量满足如下的关系:0 <= mark <= position <= limit <= capacity

//AN_Xml:

另外,Buffer 有读模式和写模式这两种模式,分别用于从 Buffer 中读取数据或者向 Buffer 中写入数据。Buffer 被创建之后默认是写模式,调用 flip() 可以切换到读模式。如果要再次切换回写模式,可以调用 clear() 或者 compact() 方法。

//AN_Xml:

position 、limit 和 capacity 之前的关系

//AN_Xml:

position 、limit 和 capacity 之前的关系

//AN_Xml:

Buffer 对象不能通过 new 调用构造方法创建对象 ,只能通过静态方法实例化 Buffer

//AN_Xml:

这里以 ByteBuffer为例进行介绍:

//AN_Xml:
// 分配堆内存
//AN_Xml:public static ByteBuffer allocate(int capacity);
//AN_Xml:// 分配直接内存
//AN_Xml:public static ByteBuffer allocateDirect(int capacity);
//AN_Xml:

Buffer 最核心的两个方法:

//AN_Xml:
    //AN_Xml:
  1. get : 读取缓冲区的数据
  2. //AN_Xml:
  3. put :向缓冲区写入数据
  4. //AN_Xml:
//AN_Xml:

除上述两个方法之外,其他的重要方法:

//AN_Xml:
    //AN_Xml:
  • flip :将缓冲区从写模式切换到读模式,它会将 limit 的值设置为当前 position 的值,将 position 的值设置为 0。
  • //AN_Xml:
  • clear: 清空缓冲区,将缓冲区从读模式切换到写模式,并将 position 的值设置为 0,将 limit 的值设置为 capacity 的值。
  • //AN_Xml:
  • ……
  • //AN_Xml:
//AN_Xml:

Buffer 中数据变化的过程:

//AN_Xml:
import java.nio.*;
//AN_Xml:
//AN_Xml:public class CharBufferDemo {
//AN_Xml:    public static void main(String[] args) {
//AN_Xml:        // 分配一个容量为8的CharBuffer
//AN_Xml:        CharBuffer buffer = CharBuffer.allocate(8);
//AN_Xml:        System.out.println("初始状态:");
//AN_Xml:        printState(buffer);
//AN_Xml:
//AN_Xml:        // 向buffer写入3个字符
//AN_Xml:        buffer.put('a').put('b').put('c');
//AN_Xml:        System.out.println("写入3个字符后的状态:");
//AN_Xml:        printState(buffer);
//AN_Xml:
//AN_Xml:        // 调用flip()方法,准备读取buffer中的数据,将 position 置 0,limit 的置 3
//AN_Xml:        buffer.flip();
//AN_Xml:        System.out.println("调用flip()方法后的状态:");
//AN_Xml:        printState(buffer);
//AN_Xml:
//AN_Xml:        // 读取字符
//AN_Xml:        while (buffer.hasRemaining()) {
//AN_Xml:            System.out.print(buffer.get());
//AN_Xml:        }
//AN_Xml:
//AN_Xml:        // 调用clear()方法,清空缓冲区,将 position 的值置为 0,将 limit 的值置为 capacity 的值
//AN_Xml:        buffer.clear();
//AN_Xml:        System.out.println("调用clear()方法后的状态:");
//AN_Xml:        printState(buffer);
//AN_Xml:
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    // 打印buffer的capacity、limit、position、mark的位置
//AN_Xml:    private static void printState(CharBuffer buffer) {
//AN_Xml:        System.out.print("capacity: " + buffer.capacity());
//AN_Xml:        System.out.print(", limit: " + buffer.limit());
//AN_Xml:        System.out.print(", position: " + buffer.position());
//AN_Xml:        System.out.print(", mark 开始读取的字符: " + buffer.mark());
//AN_Xml:        System.out.println("\n");
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

输出:

//AN_Xml:
初始状态:
//AN_Xml:capacity: 8, limit: 8, position: 0
//AN_Xml:
//AN_Xml:写入3个字符后的状态:
//AN_Xml:capacity: 8, limit: 8, position: 3
//AN_Xml:
//AN_Xml:准备读取buffer中的数据!
//AN_Xml:
//AN_Xml:调用flip()方法后的状态:
//AN_Xml:capacity: 8, limit: 3, position: 0
//AN_Xml:
//AN_Xml:读取到的数据:abc
//AN_Xml:
//AN_Xml:调用clear()方法后的状态:
//AN_Xml:capacity: 8, limit: 8, position: 0
//AN_Xml:

为了帮助理解,我绘制了一张图片展示 capacitylimitposition每一阶段的变化。

//AN_Xml:

capacity、limit和position每一阶段的变化

//AN_Xml:

Channel(通道)

//AN_Xml:

Channel 是一个通道,它建立了与数据源(如文件、网络套接字等)之间的连接。我们可以利用它来读取和写入数据,就像打开了一条自来水管,让数据在 Channel 中自由流动。

//AN_Xml:

BIO 中的流是单向的,分为各种 InputStream(输入流)和 OutputStream(输出流),数据只是在一个方向上传输。通道与流的不同之处在于通道是双向的,它可以用于读、写或者同时用于读写。

//AN_Xml:

Channel 与前面介绍的 Buffer 打交道,读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。

//AN_Xml:

Channel 和 Buffer之间的关系

//AN_Xml:

另外,因为 Channel 是全双工的,所以它可以比流更好地映射底层操作系统的 API。特别是在 UNIX 网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。

//AN_Xml:

Channel 的子类如下图所示。

//AN_Xml:

Channel 的子类

//AN_Xml:

其中,最常用的是以下几种类型的通道:

//AN_Xml:
    //AN_Xml:
  • FileChannel:文件访问通道;
  • //AN_Xml:
  • SocketChannelServerSocketChannel:TCP 通信通道;
  • //AN_Xml:
  • DatagramChannel:UDP 通信通道;
  • //AN_Xml:
//AN_Xml:

Channel继承关系图

//AN_Xml:

Channel 最核心的两个方法:

//AN_Xml:
    //AN_Xml:
  1. read :读取数据并写入到 Buffer 中。
  2. //AN_Xml:
  3. write :将 Buffer 中的数据写入到 Channel 中。
  4. //AN_Xml:
//AN_Xml:

这里我们以 FileChannel 为例演示一下是读取文件数据的。

//AN_Xml:
RandomAccessFile reader = new RandomAccessFile("/Users/guide/Documents/test_read.in", "r");
//AN_Xml:FileChannel channel = reader.getChannel();
//AN_Xml:ByteBuffer buffer = ByteBuffer.allocate(1024);
//AN_Xml:channel.read(buffer);
//AN_Xml:

Selector(选择器)

//AN_Xml:

Selector(选择器) 是 NIO 中的一个关键组件,它允许一个线程处理多个 Channel。Selector 是基于事件驱动的 I/O 多路复用模型,主要运作原理是:通过 Selector 注册通道的事件,Selector 会不断地轮询注册在其上的 Channel。当事件发生时,比如:某个 Channel 上面有新的 TCP 连接接入、读和写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来。Selector 会将相关的 Channel 加入到就绪集合中。通过 SelectionKey 可以获取就绪 Channel 的集合,然后对这些就绪的 Channel 进行相应的 I/O 操作。

//AN_Xml:

Selector 选择器工作示意图

//AN_Xml:

一个多路复用器 Selector 可以同时轮询多个 Channel,由于 JDK 使用了 epoll() 代替传统的 select 实现,所以它并没有最大连接句柄 1024/2048 的限制。这也就意味着只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。

//AN_Xml:

Selector 可以监听以下四种事件类型:

//AN_Xml:
    //AN_Xml:
  1. SelectionKey.OP_ACCEPT:表示通道接受连接的事件,这通常用于 ServerSocketChannel
  2. //AN_Xml:
  3. SelectionKey.OP_CONNECT:表示通道完成连接的事件,这通常用于 SocketChannel
  4. //AN_Xml:
  5. SelectionKey.OP_READ:表示通道准备好进行读取的事件,即有数据可读。
  6. //AN_Xml:
  7. SelectionKey.OP_WRITE:表示通道准备好进行写入的事件,即可以写入数据。
  8. //AN_Xml:
//AN_Xml:

Selector是抽象类,可以通过调用此类的 open() 静态方法来创建 Selector 实例。Selector 可以同时监控多个 SelectableChannelIO 状况,是非阻塞 IO 的核心。

//AN_Xml:

一个 Selector 实例有三个 SelectionKey 集合:

//AN_Xml:
    //AN_Xml:
  1. 所有的 SelectionKey 集合:代表了注册在该 Selector 上的 Channel,这个集合可以通过 keys() 方法返回。
  2. //AN_Xml:
  3. 被选择的 SelectionKey 集合:代表了所有可通过 select() 方法获取的、需要进行 IO 处理的 Channel,这个集合可以通过 selectedKeys() 返回。
  4. //AN_Xml:
  5. 被取消的 SelectionKey 集合:代表了所有被取消注册关系的 Channel,在下一次执行 select() 方法时,这些 Channel 对应的 SelectionKey 会被彻底删除,程序通常无须直接访问该集合,也没有暴露访问的方法。
  6. //AN_Xml:
//AN_Xml:

简单演示一下如何遍历被选择的 SelectionKey 集合并进行处理:

//AN_Xml:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
//AN_Xml:Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
//AN_Xml:while (keyIterator.hasNext()) {
//AN_Xml:    SelectionKey key = keyIterator.next();
//AN_Xml:    if (key != null) {
//AN_Xml:        if (key.isAcceptable()) {
//AN_Xml:            // ServerSocketChannel 接收了一个新连接
//AN_Xml:        } else if (key.isConnectable()) {
//AN_Xml:            // 表示一个新连接建立
//AN_Xml:        } else if (key.isReadable()) {
//AN_Xml:            // Channel 有准备好的数据,可以读取
//AN_Xml:        } else if (key.isWritable()) {
//AN_Xml:            // Channel 有空闲的 Buffer,可以写入数据
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:    keyIterator.remove();
//AN_Xml:}
//AN_Xml:

Selector 还提供了一系列和 select() 相关的方法:

//AN_Xml:
    //AN_Xml:
  • int select():监控所有注册的 Channel,当它们中间有需要处理的 IO 操作时,该方法返回,并将对应的 SelectionKey 加入被选择的 SelectionKey 集合中,该方法返回这些 Channel 的数量。
  • //AN_Xml:
  • int select(long timeout):可以设置超时时长的 select() 操作。
  • //AN_Xml:
  • int selectNow():执行一个立即返回的 select() 操作,相对于无参数的 select() 方法而言,该方法不会阻塞线程。
  • //AN_Xml:
  • Selector wakeup():使一个还未返回的 select() 方法立刻返回。
  • //AN_Xml:
  • ……
  • //AN_Xml:
//AN_Xml:

使用 Selector 实现网络读写的简单示例:

//AN_Xml:
import java.io.IOException;
//AN_Xml:import java.net.InetSocketAddress;
//AN_Xml:import java.nio.ByteBuffer;
//AN_Xml:import java.nio.channels.SelectionKey;
//AN_Xml:import java.nio.channels.Selector;
//AN_Xml:import java.nio.channels.ServerSocketChannel;
//AN_Xml:import java.nio.channels.SocketChannel;
//AN_Xml:import java.util.Iterator;
//AN_Xml:import java.util.Set;
//AN_Xml:
//AN_Xml:public class NioSelectorExample {
//AN_Xml:
//AN_Xml:  public static void main(String[] args) {
//AN_Xml:    try {
//AN_Xml:      ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//AN_Xml:      serverSocketChannel.configureBlocking(false);
//AN_Xml:      serverSocketChannel.socket().bind(new InetSocketAddress(8080));
//AN_Xml:
//AN_Xml:      Selector selector = Selector.open();
//AN_Xml:      // 将 ServerSocketChannel 注册到 Selector 并监听 OP_ACCEPT 事件
//AN_Xml:      serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//AN_Xml:
//AN_Xml:      while (true) {
//AN_Xml:        int readyChannels = selector.select();
//AN_Xml:
//AN_Xml:        if (readyChannels == 0) {
//AN_Xml:          continue;
//AN_Xml:        }
//AN_Xml:
//AN_Xml:        Set<SelectionKey> selectedKeys = selector.selectedKeys();
//AN_Xml:        Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
//AN_Xml:
//AN_Xml:        while (keyIterator.hasNext()) {
//AN_Xml:          SelectionKey key = keyIterator.next();
//AN_Xml:
//AN_Xml:          if (key.isAcceptable()) {
//AN_Xml:            // 处理连接事件
//AN_Xml:            ServerSocketChannel server = (ServerSocketChannel) key.channel();
//AN_Xml:            SocketChannel client = server.accept();
//AN_Xml:            client.configureBlocking(false);
//AN_Xml:
//AN_Xml:            // 将客户端通道注册到 Selector 并监听 OP_READ 事件
//AN_Xml:            client.register(selector, SelectionKey.OP_READ);
//AN_Xml:          } else if (key.isReadable()) {
//AN_Xml:            // 处理读事件
//AN_Xml:            SocketChannel client = (SocketChannel) key.channel();
//AN_Xml:            ByteBuffer buffer = ByteBuffer.allocate(1024);
//AN_Xml:            int bytesRead = client.read(buffer);
//AN_Xml:
//AN_Xml:            if (bytesRead > 0) {
//AN_Xml:              buffer.flip();
//AN_Xml:              System.out.println("收到数据:" +new String(buffer.array(), 0, bytesRead));
//AN_Xml:              // 将客户端通道注册到 Selector 并监听 OP_WRITE 事件
//AN_Xml:              client.register(selector, SelectionKey.OP_WRITE);
//AN_Xml:            } else if (bytesRead < 0) {
//AN_Xml:              // 客户端断开连接
//AN_Xml:              client.close();
//AN_Xml:            }
//AN_Xml:          } else if (key.isWritable()) {
//AN_Xml:            // 处理写事件
//AN_Xml:            SocketChannel client = (SocketChannel) key.channel();
//AN_Xml:            ByteBuffer buffer = ByteBuffer.wrap("Hello, Client!".getBytes());
//AN_Xml:            client.write(buffer);
//AN_Xml:
//AN_Xml:            // 将客户端通道注册到 Selector 并监听 OP_READ 事件
//AN_Xml:            client.register(selector, SelectionKey.OP_READ);
//AN_Xml:          }
//AN_Xml:
//AN_Xml:          keyIterator.remove();
//AN_Xml:        }
//AN_Xml:      }
//AN_Xml:    } catch (IOException e) {
//AN_Xml:      e.printStackTrace();
//AN_Xml:    }
//AN_Xml:  }
//AN_Xml:}
//AN_Xml:

在示例中,我们创建了一个简单的服务器,监听 8080 端口,使用 Selector 处理连接、读取和写入事件。当接收到客户端的数据时,服务器将读取数据并将其打印到控制台,然后向客户端回复 "Hello, Client!"。

//AN_Xml:

NIO 零拷贝

//AN_Xml:

零拷贝是提升 IO 操作性能的一个常用手段,像 ActiveMQ、Kafka 、RocketMQ、QMQ、Netty 等顶级开源项目都用到了零拷贝。

//AN_Xml:

零拷贝是指计算机执行 IO 操作时,CPU 不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及 CPU 的拷贝时间。也就是说,零拷贝主要解决操作系统在处理 I/O 操作时频繁复制数据的问题。零拷贝的常见实现技术有: mmap+writesendfilesendfile + DMA gather copy

//AN_Xml:

下图展示了各种零拷贝技术的对比图:

//AN_Xml:

| | CPU 拷贝 | DMA 拷贝 | 系统调用 | 上下文切换 |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: ArrayBlockingQueue 源码分析 //AN_Xml: https://javaguide.cn/java/collection/arrayblockingqueue-source-code.html //AN_Xml: https://javaguide.cn/java/collection/arrayblockingqueue-source-code.html //AN_Xml: ArrayBlockingQueue 源码分析 //AN_Xml: ArrayBlockingQueue源码深度解析:详解有界阻塞队列实现、生产者消费者模式应用、ReentrantLock+Condition并发控制、线程池工作队列机制。 //AN_Xml: Java //AN_Xml: Wed, 21 Jun 2023 05:03:13 GMT //AN_Xml: 阻塞队列简介 //AN_Xml:

阻塞队列的历史

//AN_Xml:

Java 阻塞队列的历史可以追溯到 JDK1.5 版本,当时 Java 平台增加了 java.util.concurrent,即我们常说的 JUC 包,其中包含了各种并发流程控制工具、并发容器、原子类等。这其中自然也包含了我们这篇文章所讨论的阻塞队列。

//AN_Xml:

为了解决高并发场景下多线程之间数据共享的问题,JDK1.5 版本中出现了 ArrayBlockingQueueLinkedBlockingQueue,它们是带有生产者-消费者模式实现的并发容器。其中,ArrayBlockingQueue 是有界队列,即添加的元素达到上限之后,再次添加就会被阻塞或者抛出异常。而 LinkedBlockingQueue 则由链表构成的队列,正是因为链表的特性,所以 LinkedBlockingQueue 在添加元素上并不会向 ArrayBlockingQueue 那样有着较多的约束,所以 LinkedBlockingQueue 设置队列是否有界是可选的(注意这里的无界并不是指可以添加任意数量的元素,而是说队列的大小默认为 Integer.MAX_VALUE,近乎于无限大)。

//AN_Xml:

随着 Java 的不断发展,JDK 后续的几个版本又对阻塞队列进行了不少的更新和完善:

//AN_Xml:
    //AN_Xml:
  1. JDK1.6 版本:增加 SynchronousQueue,一个不存储元素的阻塞队列。
  2. //AN_Xml:
  3. JDK1.7 版本:增加 TransferQueue,一个支持更多操作的阻塞队列。
  4. //AN_Xml:
  5. JDK1.8 版本:增加 DelayQueue,一个支持延迟获取元素的阻塞队列。
  6. //AN_Xml:
//AN_Xml:

阻塞队列的思想

//AN_Xml:

阻塞队列就是典型的生产者-消费者模型,它可以做到以下几点:

//AN_Xml:
    //AN_Xml:
  1. 当阻塞队列数据为空时,所有的消费者线程都会被阻塞,等待队列非空。
  2. //AN_Xml:
  3. 当生产者往队列里填充数据后,队列就会通知消费者队列非空,消费者此时就可以进来消费。
  4. //AN_Xml:
  5. 当阻塞队列因为消费者消费过慢或者生产者存放元素过快导致队列填满时无法容纳新元素时,生产者就会被阻塞,等待队列非满时继续存放元素。
  6. //AN_Xml:
  7. 当消费者从队列中消费一个元素之后,队列就会通知生产者队列非满,生产者可以继续填充数据了。
  8. //AN_Xml:
//AN_Xml:

总结一下:阻塞队列就说基于非空和非满两个条件实现生产者和消费者之间的交互,尽管这些交互流程和等待通知的机制实现非常复杂,好在 Doug Lea 的操刀之下已将阻塞队列的细节屏蔽,我们只需调用 puttakeofferpoll 等 API 即可实现多线程之间的生产和消费。

//AN_Xml:

这也使得阻塞队列在多线程开发中有着广泛的运用,最常见的例子无非是我们的线程池,从源码中我们就能看出当核心线程无法及时处理任务时,这些任务都会扔到 workQueue 中。

//AN_Xml:
public ThreadPoolExecutor(int corePoolSize,
//AN_Xml:                            int maximumPoolSize,
//AN_Xml:                            long keepAliveTime,
//AN_Xml:                            TimeUnit unit,
//AN_Xml:                            BlockingQueue<Runnable> workQueue,
//AN_Xml:                            ThreadFactory threadFactory,
//AN_Xml:                            RejectedExecutionHandler handler) {// ...}
//AN_Xml:

ArrayBlockingQueue 常见方法及测试

//AN_Xml:

简单了解了阻塞队列的历史之后,我们就开始重点讨论本篇文章所要介绍的并发容器——ArrayBlockingQueue。为了后续更加深入的了解 ArrayBlockingQueue,我们不妨基于下面几个实例了解以下 ArrayBlockingQueue 的使用。

//AN_Xml:

先看看第一个例子,我们这里会用两个线程分别模拟生产者和消费者,生产者生产完会使用 put 方法生产 10 个元素给消费者进行消费,当队列元素达到我们设置的上限 5 时,put 方法就会阻塞。
//AN_Xml:同理消费者也会通过 take 方法消费元素,当队列为空时,take 方法就会阻塞消费者线程。这里笔者为了保证消费者能够在消费完 10 个元素后及时退出。便通过倒计时门闩,来控制消费者结束,生产者在这里只会生产 10 个元素。当消费者将 10 个元素消费完成之后,按下倒计时门闩,所有线程都会停止。

//AN_Xml:
public class ProducerConsumerExample {
//AN_Xml:
//AN_Xml:    public static void main(String[] args) throws InterruptedException {
//AN_Xml:
//AN_Xml:        // 创建一个大小为 5 的 ArrayBlockingQueue
//AN_Xml:        ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
//AN_Xml:
//AN_Xml:        // 创建生产者线程
//AN_Xml:        Thread producer = new Thread(() -> {
//AN_Xml:            try {
//AN_Xml:                for (int i = 1; i <= 10; i++) {
//AN_Xml:                    // 向队列中添加元素,如果队列已满则阻塞等待
//AN_Xml:                    queue.put(i);
//AN_Xml:                    System.out.println("生产者添加元素:" + i);
//AN_Xml:                }
//AN_Xml:            } catch (InterruptedException e) {
//AN_Xml:                e.printStackTrace();
//AN_Xml:            }
//AN_Xml:
//AN_Xml:        });
//AN_Xml:
//AN_Xml:        CountDownLatch countDownLatch = new CountDownLatch(1);
//AN_Xml:
//AN_Xml:        // 创建消费者线程
//AN_Xml:        Thread consumer = new Thread(() -> {
//AN_Xml:            try {
//AN_Xml:                int count = 0;
//AN_Xml:                while (true) {
//AN_Xml:
//AN_Xml:                    // 从队列中取出元素,如果队列为空则阻塞等待
//AN_Xml:                    int element = queue.take();
//AN_Xml:                    System.out.println("消费者取出元素:" + element);
//AN_Xml:                    ++count;
//AN_Xml:                    if (count == 10) {
//AN_Xml:                        break;
//AN_Xml:                    }
//AN_Xml:                }
//AN_Xml:
//AN_Xml:                countDownLatch.countDown();
//AN_Xml:            } catch (InterruptedException e) {
//AN_Xml:                e.printStackTrace();
//AN_Xml:            }
//AN_Xml:
//AN_Xml:        });
//AN_Xml:
//AN_Xml:        // 启动线程
//AN_Xml:        producer.start();
//AN_Xml:        consumer.start();
//AN_Xml:
//AN_Xml:        // 等待线程结束
//AN_Xml:        producer.join();
//AN_Xml:        consumer.join();
//AN_Xml:
//AN_Xml:        countDownLatch.await();
//AN_Xml:
//AN_Xml:        producer.interrupt();
//AN_Xml:        consumer.interrupt();
//AN_Xml:    }
//AN_Xml:
//AN_Xml:}
//AN_Xml:

代码输出结果如下,可以看到只有生产者往队列中投放元素之后消费者才能消费,这也就意味着当队列中没有数据的时消费者就会阻塞,等待队列非空再继续消费。

//AN_Xml:
生产者添加元素:1
//AN_Xml:生产者添加元素:2
//AN_Xml:消费者取出元素:1
//AN_Xml:消费者取出元素:2
//AN_Xml:生产者添加元素:3
//AN_Xml:消费者取出元素:3
//AN_Xml:生产者添加元素:4
//AN_Xml:生产者添加元素:5
//AN_Xml:消费者取出元素:4
//AN_Xml:生产者添加元素:6
//AN_Xml:消费者取出元素:5
//AN_Xml:生产者添加元素:7
//AN_Xml:生产者添加元素:8
//AN_Xml:生产者添加元素:9
//AN_Xml:生产者添加元素:10
//AN_Xml:消费者取出元素:6
//AN_Xml:消费者取出元素:7
//AN_Xml:消费者取出元素:8
//AN_Xml:消费者取出元素:9
//AN_Xml:消费者取出元素:10
//AN_Xml:

了解了 puttake 这两个会阻塞的存和取方法之后,我我们再来看看阻塞队列中非阻塞的入队和出队方法 offerpoll

//AN_Xml:

如下所示,我们设置了一个大小为 3 的阻塞队列,我们会尝试在队列用 offer 方法存放 4 个元素,然后再从队列中用 poll 尝试取 4 次。

//AN_Xml:
public class OfferPollExample {
//AN_Xml:
//AN_Xml:    public static void main(String[] args) {
//AN_Xml:        // 创建一个大小为 3 的 ArrayBlockingQueue
//AN_Xml:        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
//AN_Xml:
//AN_Xml:        // 向队列中添加元素
//AN_Xml:        System.out.println(queue.offer("A"));
//AN_Xml:        System.out.println(queue.offer("B"));
//AN_Xml:        System.out.println(queue.offer("C"));
//AN_Xml:
//AN_Xml:        // 尝试向队列中添加元素,但队列已满,返回 false
//AN_Xml:        System.out.println(queue.offer("D"));
//AN_Xml:
//AN_Xml:        // 从队列中取出元素
//AN_Xml:        System.out.println(queue.poll());
//AN_Xml:        System.out.println(queue.poll());
//AN_Xml:        System.out.println(queue.poll());
//AN_Xml:
//AN_Xml:        // 尝试从队列中取出元素,但队列已空,返回 null
//AN_Xml:        System.out.println(queue.poll());
//AN_Xml:    }
//AN_Xml:
//AN_Xml:}
//AN_Xml:

最终代码的输出结果如下,可以看到因为队列的大小为 3 的缘故,我们前 3 次存放到队列的结果为 true,第 4 次存放时,由于队列已满,所以存放结果返回 false。这也是为什么我们后续的 poll 方法只得到了 3 个元素的值。

//AN_Xml:
true
//AN_Xml:true
//AN_Xml:true
//AN_Xml:false
//AN_Xml:A
//AN_Xml:B
//AN_Xml:C
//AN_Xml:null
//AN_Xml:

了解了阻塞存取和非阻塞存取,我们再来看看阻塞队列的一个比较特殊的操作,某些场景下,我们希望能够一次性将阻塞队列的结果存到列表中再进行批量操作,我们就可以使用阻塞队列的 drainTo 方法,这个方法会一次性将队列中所有元素存放到列表,如果队列中有元素,且成功存到 list 中则 drainTo 会返回本次转移到 list 中的元素数,反之若队列为空,drainTo 则直接返回 0。

//AN_Xml:
public class DrainToExample {
//AN_Xml:
//AN_Xml:    public static void main(String[] args) {
//AN_Xml:        // 创建一个大小为 5 的 ArrayBlockingQueue
//AN_Xml:        ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
//AN_Xml:
//AN_Xml:        // 向队列中添加元素
//AN_Xml:        queue.add(1);
//AN_Xml:        queue.add(2);
//AN_Xml:        queue.add(3);
//AN_Xml:        queue.add(4);
//AN_Xml:        queue.add(5);
//AN_Xml:
//AN_Xml:        // 创建一个 List,用于存储从队列中取出的元素
//AN_Xml:        List<Integer> list = new ArrayList<>();
//AN_Xml:
//AN_Xml:        // 从队列中取出所有元素,并添加到 List 中
//AN_Xml:        queue.drainTo(list);
//AN_Xml:
//AN_Xml:        // 输出 List 中的元素
//AN_Xml:        System.out.println(list);
//AN_Xml:    }
//AN_Xml:
//AN_Xml:}
//AN_Xml:

代码输出结果如下

//AN_Xml:
[1, 2, 3, 4, 5]
//AN_Xml:

ArrayBlockingQueue 源码分析

//AN_Xml:

自此我们对阻塞队列的使用有了基本的印象,接下来我们就可以进一步了解一下 ArrayBlockingQueue 的工作机制了。

//AN_Xml:

整体设计

//AN_Xml:

在了解 ArrayBlockingQueue 的具体细节之前,我们先来看看 ArrayBlockingQueue 的类图。

//AN_Xml:

ArrayBlockingQueue 类图

//AN_Xml:

从图中我们可以看出,ArrayBlockingQueue 实现了阻塞队列 BlockingQueue 这个接口,不难猜出通过实现 BlockingQueue 这个接口之后,ArrayBlockingQueue 就拥有了阻塞队列那些常见的操作行为。

//AN_Xml:

同时, ArrayBlockingQueue 还继承了 AbstractQueue 这个抽象类,这个继承了 AbstractCollectionQueue 的抽象类,从抽象类的特定和语义我们也可以猜出,这个继承关系使得 ArrayBlockingQueue 拥有了队列的常见操作。

//AN_Xml:

所以我们是否可以得出这样一个结论,通过继承 AbstractQueue 获得队列所有的操作模板,其实现的入队和出队操作的整体框架。然后 ArrayBlockingQueue 通过实现 BlockingQueue 获取到阻塞队列的常见操作并将这些操作实现,填充到 AbstractQueue 模板方法的细节中,由此 ArrayBlockingQueue 成为一个完整的阻塞队列。

//AN_Xml:

为了印证这一点,我们到源码中一探究竟。首先我们先来看看 AbstractQueue,从类的继承关系我们可以大致得出,它通过 AbstractCollection 获得了集合的常见操作方法,然后通过 Queue 接口获得了队列的特性。

//AN_Xml:
public abstract class AbstractQueue<E>
//AN_Xml:    extends AbstractCollection<E>
//AN_Xml:    implements Queue<E> {
//AN_Xml:       //...
//AN_Xml:}
//AN_Xml:

对于集合的操作无非是增删改查,所以我们不妨从添加方法入手,从源码中我们可以看到,它实现了 AbstractCollectionadd 方法,其内部逻辑如下:

//AN_Xml:
    //AN_Xml:
  1. 调用继承 Queue 接口得来的 offer 方法,如果 offer 成功则返回 true
  2. //AN_Xml:
  3. 如果 offer 失败,即代表当前元素入队失败直接抛异常。
  4. //AN_Xml:
//AN_Xml:
public boolean add(E e) {
//AN_Xml:  if (offer(e))
//AN_Xml:      return true;
//AN_Xml:  else
//AN_Xml:      throw new IllegalStateException("Queue full");
//AN_Xml:}
//AN_Xml:

AbstractQueue 中并没有对 Queueoffer 的实现,很明显这样做的目的是定义好了 add 的核心逻辑,将 offer 的细节交由其子类即我们的 ArrayBlockingQueue 实现。

//AN_Xml:

到此,我们对于抽象类 AbstractQueue 的分析就结束了,我们继续看看 ArrayBlockingQueue 中实现的另一个重要接口 BlockingQueue

//AN_Xml:

点开 BlockingQueue 之后,我们可以看到这个接口同样继承了 Queue 接口,这就意味着它也具备了队列所拥有的所有行为。同时,它还定义了自己所需要实现的方法。

//AN_Xml:
public interface BlockingQueue<E> extends Queue<E> {
//AN_Xml:
//AN_Xml:     //元素入队成功返回true,反之则会抛出异常IllegalStateException
//AN_Xml:    boolean add(E e);
//AN_Xml:
//AN_Xml:     //元素入队成功返回true,反之返回false
//AN_Xml:    boolean offer(E e);
//AN_Xml:
//AN_Xml:     //元素入队成功则直接返回,如果队列已满元素不可入队则将线程阻塞,因为阻塞期间可能会被打断,所以这里方法签名抛出了InterruptedException
//AN_Xml:    void put(E e) throws InterruptedException;
//AN_Xml:
//AN_Xml:   //和上一个方法一样,只不过队列满时只会阻塞单位为unit,时间为timeout的时长,如果在等待时长内没有入队成功则直接返回false。
//AN_Xml:    boolean offer(E e, long timeout, TimeUnit unit)
//AN_Xml:        throws InterruptedException;
//AN_Xml:
//AN_Xml:    //从队头取出一个元素,如果队列为空则阻塞等待,因为会阻塞线程的缘故,所以该方法可能会被打断,所以签名定义了InterruptedException
//AN_Xml:    E take() throws InterruptedException;
//AN_Xml:
//AN_Xml:      //取出队头的元素并返回,如果当前队列为空则阻塞等待timeout且单位为unit的时长,如果这个时间段没有元素则直接返回null。
//AN_Xml:    E poll(long timeout, TimeUnit unit)
//AN_Xml:        throws InterruptedException;
//AN_Xml:
//AN_Xml:      //获取队列剩余元素个数
//AN_Xml:    int remainingCapacity();
//AN_Xml:
//AN_Xml:     //删除我们指定的对象,如果成功返回true,反之返回false。
//AN_Xml:    boolean remove(Object o);
//AN_Xml:
//AN_Xml:    //判断队列中是否包含指定元素
//AN_Xml:    public boolean contains(Object o);
//AN_Xml:
//AN_Xml:     //将队列中的元素全部存到指定的集合中
//AN_Xml:    int drainTo(Collection<? super E> c);
//AN_Xml:
//AN_Xml:    //转移maxElements个元素到集合中
//AN_Xml:    int drainTo(Collection<? super E> c, int maxElements);
//AN_Xml:}
//AN_Xml:

了解了 BlockingQueue 的常见操作后,我们就知道了 ArrayBlockingQueue 通过实现 BlockingQueue 的方法并重写后,填充到 AbstractQueue 的方法上,由此我们便知道了上文中 AbstractQueueadd 方法的 offer 方法是哪里是实现的了。

//AN_Xml:
public boolean add(E e) {
//AN_Xml:  //AbstractQueue的offer来自下层的ArrayBlockingQueue从BlockingQueue实现并重写的offer方法
//AN_Xml:  if (offer(e))
//AN_Xml:      return true;
//AN_Xml:  else
//AN_Xml:      throw new IllegalStateException("Queue full");
//AN_Xml:}
//AN_Xml:

初始化

//AN_Xml:

了解 ArrayBlockingQueue 的细节前,我们不妨先看看其构造函数,了解一下其初始化过程。从源码中我们可以看出 ArrayBlockingQueue 有 3 个构造方法,而最核心的构造方法就是下方这一个。

//AN_Xml:
// capacity 表示队列初始容量,fair 表示 锁的公平性
//AN_Xml:public ArrayBlockingQueue(int capacity, boolean fair) {
//AN_Xml:  //如果设置的队列大小小于0,则直接抛出IllegalArgumentException
//AN_Xml:  if (capacity <= 0)
//AN_Xml:      throw new IllegalArgumentException();
//AN_Xml:  //初始化一个数组用于存放队列的元素
//AN_Xml:  this.items = new Object[capacity];
//AN_Xml:  //创建阻塞队列流程控制的锁
//AN_Xml:  lock = new ReentrantLock(fair);
//AN_Xml:  //用lock锁创建两个条件控制队列生产和消费
//AN_Xml:  notEmpty = lock.newCondition();
//AN_Xml:  notFull =  lock.newCondition();
//AN_Xml:}
//AN_Xml:

这个构造方法里面有两个比较核心的成员变量 notEmpty(非空) 和 notFull (非满) ,需要我们格外留意,它们是实现生产者和消费者有序工作的关键所在,这一点笔者会在后续的源码解析中详细说明,这里我们只需初步了解一下阻塞队列的构造即可。

//AN_Xml:

另外两个构造方法都是基于上述的构造方法,默认情况下,我们会使用下面这个构造方法,该构造方法就意味着 ArrayBlockingQueue 用的是非公平锁,即各个生产者或者消费者线程收到通知后,对于锁的争抢是随机的。

//AN_Xml:
 public ArrayBlockingQueue(int capacity) {
//AN_Xml:        this(capacity, false);
//AN_Xml:    }
//AN_Xml:

还有一个不怎么常用的构造方法,在初始化容量和锁的非公平性之后,它还提供了一个 Collection 参数,从源码中不难看出这个构造方法是将外部传入的集合的元素在初始化时直接存放到阻塞队列中。

//AN_Xml:
public ArrayBlockingQueue(int capacity, boolean fair,
//AN_Xml:                              Collection<? extends E> c) {
//AN_Xml:  //初始化容量和锁的公平性
//AN_Xml:  this(capacity, fair);
//AN_Xml:
//AN_Xml:  final ReentrantLock lock = this.lock;
//AN_Xml:  //上锁并将c中的元素存放到ArrayBlockingQueue底层的数组中
//AN_Xml:  lock.lock();
//AN_Xml:  try {
//AN_Xml:      int i = 0;
//AN_Xml:      try {
//AN_Xml:                //遍历并添加元素到数组中
//AN_Xml:          for (E e : c) {
//AN_Xml:              checkNotNull(e);
//AN_Xml:              items[i++] = e;
//AN_Xml:          }
//AN_Xml:      } catch (ArrayIndexOutOfBoundsException ex) {
//AN_Xml:          throw new IllegalArgumentException();
//AN_Xml:      }
//AN_Xml:      //记录当前队列容量
//AN_Xml:      count = i;
//AN_Xml:                      //更新下一次put或者offer或用add方法添加到队列底层数组的位置
//AN_Xml:      putIndex = (i == capacity) ? 0 : i;
//AN_Xml:  } finally {
//AN_Xml:      //完成遍历后释放锁
//AN_Xml:      lock.unlock();
//AN_Xml:  }
//AN_Xml:}
//AN_Xml:

阻塞式获取和新增元素

//AN_Xml:

ArrayBlockingQueue 阻塞式获取和新增元素对应的就是生产者-消费者模型,虽然它也支持非阻塞式获取和新增元素(例如 poll()offer(E e) 方法,后文会介绍到),但一般不会使用。

//AN_Xml:

ArrayBlockingQueue 阻塞式获取和新增元素的方法为:

//AN_Xml:
    //AN_Xml:
  • put(E e):将元素插入队列中,如果队列已满,则该方法会一直阻塞,直到队列有空间可用或者线程被中断。
  • //AN_Xml:
  • take() :获取并移除队列头部的元素,如果队列为空,则该方法会一直阻塞,直到队列非空或者线程被中断。
  • //AN_Xml:
//AN_Xml:

这两个方法实现的关键就是在于两个条件对象 notEmpty(非空) 和 notFull (非满),这个我们在上文的构造方法中有提到。

//AN_Xml:

接下来笔者就通过两张图让大家了解一下这两个条件是如何在阻塞队列中运用的。

//AN_Xml:

ArrayBlockingQueue 非空条件

//AN_Xml:

假设我们的代码消费者先启动,当它发现队列中没有数据,那么非空条件就会将这个线程挂起,即等待条件非空时挂起。然后 CPU 执行权到达生产者,生产者发现队列中可以存放数据,于是将数据存放进去,通知此时条件非空,此时消费者就会被唤醒到队列中使用 take 等方法获取值了。

//AN_Xml:

ArrayBlockingQueue 非满条件

//AN_Xml:

随后的执行中,生产者生产速度远远大于消费者消费速度,于是生产者将队列塞满后再次尝试将数据存入队列,发现队列已满,于是阻塞队列就将当前线程挂起,等待非满。然后消费者拿着 CPU 执行权进行消费,于是队列可以存放新数据了,发出一个非满的通知,此时挂起的生产者就会等待 CPU 执行权到来时再次尝试将数据存到队列中。

//AN_Xml:

简单了解阻塞队列的基于两个条件的交互流程之后,我们不妨看看 puttake 方法的源码。

//AN_Xml:
public void put(E e) throws InterruptedException {
//AN_Xml:    //确保插入的元素不为null
//AN_Xml:    checkNotNull(e);
//AN_Xml:    //加锁
//AN_Xml:    final ReentrantLock lock = this.lock;
//AN_Xml:    //这里使用lockInterruptibly()方法而不是lock()方法是为了能够响应中断操作,如果在等待获取锁的过程中被打断则该方法会抛出InterruptedException异常。
//AN_Xml:    lock.lockInterruptibly();
//AN_Xml:    try {
//AN_Xml:            //如果count等数组长度则说明队列已满,当前线程将被挂起放到AQS队列中,等待队列非满时插入(非满条件)。
//AN_Xml:       //在等待期间,锁会被释放,其他线程可以继续对队列进行操作。
//AN_Xml:        while (count == items.length)
//AN_Xml:            notFull.await();
//AN_Xml:           //如果队列可以存放元素,则调用enqueue将元素入队
//AN_Xml:        enqueue(e);
//AN_Xml:    } finally {
//AN_Xml:        //释放锁
//AN_Xml:        lock.unlock();
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

put方法内部调用了 enqueue 方法来实现元素入队,我们继续深入查看一下 enqueue 方法的实现细节:

//AN_Xml:
private void enqueue(E x) {
//AN_Xml:   //获取队列底层的数组
//AN_Xml:    final Object[] items = this.items;
//AN_Xml:    //将putindex位置的值设置为我们传入的x
//AN_Xml:    items[putIndex] = x;
//AN_Xml:    //更新putindex,如果putindex等于数组长度,则更新为0
//AN_Xml:    if (++putIndex == items.length)
//AN_Xml:        putIndex = 0;
//AN_Xml:    //队列长度+1
//AN_Xml:    count++;
//AN_Xml:    //通知队列非空,那些因为获取元素而阻塞的线程可以继续工作了
//AN_Xml:    notEmpty.signal();
//AN_Xml:}
//AN_Xml:

从源码中可以看到入队操作的逻辑就是在数组中追加一个新元素,整体执行步骤为:

//AN_Xml:
    //AN_Xml:
  1. 获取 ArrayBlockingQueue 底层的数组 items
  2. //AN_Xml:
  3. 将元素存到 putIndex 位置。
  4. //AN_Xml:
  5. 更新 putIndex 到下一个位置,如果 putIndex 等于队列长度,则说明 putIndex 已经到达数组末尾了,下一次插入则需要 0 开始。(ArrayBlockingQueue 用到了循环队列的思想,即从头到尾循环复用一个数组)
  6. //AN_Xml:
  7. 更新 count 的值,表示当前队列长度+1。
  8. //AN_Xml:
  9. 调用 notEmpty.signal() 通知队列非空,消费者可以从队列中获取值了。
  10. //AN_Xml:
//AN_Xml:

自此我们了解了 put 方法的流程,为了更加完整的了解 ArrayBlockingQueue 关于生产者-消费者模型的设计,我们继续看看阻塞获取队列元素的 take 方法。

//AN_Xml:
public E take() throws InterruptedException {
//AN_Xml:       //获取锁
//AN_Xml:     final ReentrantLock lock = this.lock;
//AN_Xml:     lock.lockInterruptibly();
//AN_Xml:     try {
//AN_Xml:             //如果队列中元素个数为0,则将当前线程打断并存入AQS队列中,等待队列非空时获取并移除元素(非空条件)
//AN_Xml:         while (count == 0)
//AN_Xml:             notEmpty.await();
//AN_Xml:            //如果队列不为空则调用dequeue获取元素
//AN_Xml:         return dequeue();
//AN_Xml:     } finally {
//AN_Xml:          //释放锁
//AN_Xml:         lock.unlock();
//AN_Xml:     }
//AN_Xml:}
//AN_Xml:

理解了 put 方法再看take 方法就很简单了,其核心逻辑和put 方法正好是相反的,比如put 方法在队列满的时候等待队列非满时插入元素(非满条件),而take 方法等待队列非空时获取并移除元素(非空条件)。

//AN_Xml:

take方法内部调用了 dequeue 方法来实现元素出队,其核心逻辑和 enqueue 方法也是相反的。

//AN_Xml:
private E dequeue() {
//AN_Xml:  //获取阻塞队列底层的数组
//AN_Xml:  final Object[] items = this.items;
//AN_Xml:  @SuppressWarnings("unchecked")
//AN_Xml:  //从队列中获取takeIndex位置的元素
//AN_Xml:  E x = (E) items[takeIndex];
//AN_Xml:  //将takeIndex置空
//AN_Xml:  items[takeIndex] = null;
//AN_Xml:  //takeIndex向后挪动,如果等于数组长度则更新为0
//AN_Xml:  if (++takeIndex == items.length)
//AN_Xml:      takeIndex = 0;
//AN_Xml:  //队列长度减1
//AN_Xml:  count--;
//AN_Xml:  if (itrs != null)
//AN_Xml:      itrs.elementDequeued();
//AN_Xml:  //通知那些被打断的线程当前队列状态非满,可以继续存放元素
//AN_Xml:  notFull.signal();
//AN_Xml:  return x;
//AN_Xml:}
//AN_Xml:

由于dequeue 方法(出队)和上面介绍的 enqueue 方法(入队)的步骤大致类似,这里就不重复介绍了。

//AN_Xml:

为了帮助理解,我专门画了一张图来展示 notEmpty(非空) 和 notFull (非满)这两个条件对象是如何控制 ArrayBlockingQueue 的存和取的。

//AN_Xml:

ArrayBlockingQueue 非空非满

//AN_Xml:
    //AN_Xml:
  • 消费者:当消费者从队列中 take 或者 poll 等操作取出一个元素之后,就会通知队列非满,此时那些等待非满的生产者就会被唤醒等待获取 CPU 时间片进行入队操作。
  • //AN_Xml:
  • 生产者:当生产者将元素存到队列中后,就会触发通知队列非空,此时消费者就会被唤醒等待 CPU 时间片尝试获取元素。如此往复,两个条件对象就构成一个环路,控制着多线程之间的存和取。
  • //AN_Xml:
//AN_Xml:

非阻塞式获取和新增元素

//AN_Xml:

ArrayBlockingQueue 非阻塞式获取和新增元素的方法为:

//AN_Xml:
    //AN_Xml:
  • offer(E e):将元素插入队列尾部。如果队列已满,则该方法会直接返回 false,不会等待并阻塞线程。
  • //AN_Xml:
  • poll():获取并移除队列头部的元素,如果队列为空,则该方法会直接返回 null,不会等待并阻塞线程。
  • //AN_Xml:
  • add(E e):将元素插入队列尾部。如果队列已满则会抛出 IllegalStateException 异常,底层基于 offer(E e) 方法。
  • //AN_Xml:
  • remove():移除队列头部的元素,如果队列为空则会抛出 NoSuchElementException 异常,底层基于 poll()
  • //AN_Xml:
  • peek():获取但不移除队列头部的元素,如果队列为空,则该方法会直接返回 null,不会等待并阻塞线程。
  • //AN_Xml:
//AN_Xml:

先来看看 offer 方法,逻辑和 put 差不多,唯一的区别就是入队失败时不会阻塞当前线程,而是直接返回 false

//AN_Xml:
public boolean offer(E e) {
//AN_Xml:        //确保插入的元素不为null
//AN_Xml:        checkNotNull(e);
//AN_Xml:        //获取锁
//AN_Xml:        final ReentrantLock lock = this.lock;
//AN_Xml:        lock.lock();
//AN_Xml:        try {
//AN_Xml:             //队列已满直接返回false
//AN_Xml:            if (count == items.length)
//AN_Xml:                return false;
//AN_Xml:            else {
//AN_Xml:                //反之将元素入队并直接返回true
//AN_Xml:                enqueue(e);
//AN_Xml:                return true;
//AN_Xml:            }
//AN_Xml:        } finally {
//AN_Xml:            //释放锁
//AN_Xml:            lock.unlock();
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:

poll 方法同理,获取元素失败也是直接返回空,并不会阻塞获取元素的线程。

//AN_Xml:
public E poll() {
//AN_Xml:        final ReentrantLock lock = this.lock;
//AN_Xml:        //上锁
//AN_Xml:        lock.lock();
//AN_Xml:        try {
//AN_Xml:            //如果队列为空直接返回null,反之出队返回元素值
//AN_Xml:            return (count == 0) ? null : dequeue();
//AN_Xml:        } finally {
//AN_Xml:            lock.unlock();
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:

add 方法其实就是对于 offer 做了一层封装,如下代码所示,可以看到 add 会调用没有规定时间的 offer,如果入队失败则直接抛异常。

//AN_Xml:
public boolean add(E e) {
//AN_Xml:        return super.add(e);
//AN_Xml:    }
//AN_Xml:
//AN_Xml:
//AN_Xml:public boolean add(E e) {
//AN_Xml:        //调用offer方法如果失败直接抛出异常
//AN_Xml:        if (offer(e))
//AN_Xml:            return true;
//AN_Xml:        else
//AN_Xml:            throw new IllegalStateException("Queue full");
//AN_Xml:    }
//AN_Xml:

remove 方法同理,调用 poll,如果返回 null 则说明队列没有元素,直接抛出异常。

//AN_Xml:
public E remove() {
//AN_Xml:        E x = poll();
//AN_Xml:        if (x != null)
//AN_Xml:            return x;
//AN_Xml:        else
//AN_Xml:            throw new NoSuchElementException();
//AN_Xml:    }
//AN_Xml:

peek() 方法的逻辑也很简单,内部调用了 itemAt 方法。

//AN_Xml:
public E peek() {
//AN_Xml:        //加锁
//AN_Xml:        final ReentrantLock lock = this.lock;
//AN_Xml:        lock.lock();
//AN_Xml:        try {
//AN_Xml:            //当队列为空时返回 null
//AN_Xml:            return itemAt(takeIndex);
//AN_Xml:        } finally {
//AN_Xml:            //释放锁
//AN_Xml:            lock.unlock();
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:
//AN_Xml://返回队列中指定位置的元素
//AN_Xml:@SuppressWarnings("unchecked")
//AN_Xml:final E itemAt(int i) {
//AN_Xml:    return (E) items[i];
//AN_Xml:}
//AN_Xml:

指定超时时间内阻塞式获取和新增元素

//AN_Xml:

offer(E e)poll() 非阻塞获取和新增元素的基础上,设计者提供了带有等待时间的 offer(E e, long timeout, TimeUnit unit)poll(long timeout, TimeUnit unit) ,用于在指定的超时时间内阻塞式地添加和获取元素。

//AN_Xml:
 public boolean offer(E e, long timeout, TimeUnit unit)
//AN_Xml:        throws InterruptedException {
//AN_Xml:
//AN_Xml:        checkNotNull(e);
//AN_Xml:        long nanos = unit.toNanos(timeout);
//AN_Xml:        final ReentrantLock lock = this.lock;
//AN_Xml:        lock.lockInterruptibly();
//AN_Xml:        try {
//AN_Xml:        //队列已满,进入循环
//AN_Xml:            while (count == items.length) {
//AN_Xml:            //时间到了队列还是满的,则直接返回false
//AN_Xml:                if (nanos <= 0)
//AN_Xml:                    return false;
//AN_Xml:                 //阻塞nanos时间,等待非满
//AN_Xml:                nanos = notFull.awaitNanos(nanos);
//AN_Xml:            }
//AN_Xml:            enqueue(e);
//AN_Xml:            return true;
//AN_Xml:        } finally {
//AN_Xml:            lock.unlock();
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:

可以看到,带有超时时间的 offer 方法在队列已满的情况下,会等待用户所传的时间段,如果规定时间内还不能存放元素则直接返回 false

//AN_Xml:
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
//AN_Xml:        long nanos = unit.toNanos(timeout);
//AN_Xml:        final ReentrantLock lock = this.lock;
//AN_Xml:        lock.lockInterruptibly();
//AN_Xml:        try {
//AN_Xml:          //队列为空,循环等待,若时间到还是空的,则直接返回null
//AN_Xml:            while (count == 0) {
//AN_Xml:                if (nanos <= 0)
//AN_Xml:                    return null;
//AN_Xml:                nanos = notEmpty.awaitNanos(nanos);
//AN_Xml:            }
//AN_Xml:            return dequeue();
//AN_Xml:        } finally {
//AN_Xml:            lock.unlock();
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:

同理,带有超时时间的 poll 也一样,队列为空则在规定时间内等待,若时间到了还是空的,则直接返回 null。

//AN_Xml:

判断元素是否存在

//AN_Xml:

ArrayBlockingQueue 提供了 contains(Object o) 来判断指定元素是否存在于队列中。

//AN_Xml:
public boolean contains(Object o) {
//AN_Xml:    //若目标元素为空,则直接返回 false
//AN_Xml:    if (o == null) return false;
//AN_Xml:    //获取当前队列的元素数组
//AN_Xml:    final Object[] items = this.items;
//AN_Xml:    //加锁
//AN_Xml:    final ReentrantLock lock = this.lock;
//AN_Xml:    lock.lock();
//AN_Xml:    try {
//AN_Xml:        // 如果队列非空
//AN_Xml:        if (count > 0) {
//AN_Xml:            final int putIndex = this.putIndex;
//AN_Xml:            //从队列头部开始遍历
//AN_Xml:            int i = takeIndex;
//AN_Xml:            do {
//AN_Xml:                if (o.equals(items[i]))
//AN_Xml:                    return true;
//AN_Xml:                if (++i == items.length)
//AN_Xml:                    i = 0;
//AN_Xml:            } while (i != putIndex);
//AN_Xml:        }
//AN_Xml:        return false;
//AN_Xml:    } finally {
//AN_Xml:        //释放锁
//AN_Xml:        lock.unlock();
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

ArrayBlockingQueue 获取和新增元素的方法对比

//AN_Xml:

为了帮助理解 ArrayBlockingQueue ,我们再来对比一下上面提到的这些获取和新增元素的方法。

//AN_Xml:

新增元素:

//AN_Xml:

| 方法 | 队列满时处理方式 | 方法返回值 |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: CopyOnWriteArrayList 源码分析 //AN_Xml: https://javaguide.cn/java/collection/copyonwritearraylist-source-code.html //AN_Xml: https://javaguide.cn/java/collection/copyonwritearraylist-source-code.html //AN_Xml: CopyOnWriteArrayList 源码分析 //AN_Xml: CopyOnWriteArrayList源码深度解析:详解写时复制COW机制、适用读多写少场景、线程安全List实现、快照一致性保证及内存开销权衡。 //AN_Xml: Java //AN_Xml: Thu, 08 Jun 2023 12:34:44 GMT //AN_Xml: CopyOnWriteArrayList 简介 //AN_Xml:

在 JDK1.5 之前,如果想要使用并发安全的 List 只能选择 Vector。而 Vector 是一种老旧的集合,已经被淘汰。Vector 对于增删改查等方法基本都加了 synchronized,这种方式虽然能够保证同步,但这相当于对整个 Vector 加上了一把大锁,使得每个方法执行的时候都要去获得锁,导致性能非常低下。

//AN_Xml:

JDK1.5 引入了 Java.util.concurrent(JUC)包,其中提供了很多线程安全且并发性能良好的容器,其中唯一的线程安全 List 实现就是 CopyOnWriteArrayList 。关于java.util.concurrent 包下常见并发容器的总结,可以看我写的这篇文章:Java 常见并发容器总结

//AN_Xml:

CopyOnWriteArrayList 到底有什么厉害之处?

//AN_Xml:

对于大部分业务场景来说,读取操作往往是远大于写入操作的。由于读取操作不会对原有数据进行修改,因此,对于每次读取都进行加锁其实是一种资源浪费。相比之下,我们应该允许多个线程同时访问 List 的内部数据,毕竟对于读取操作来说是安全的。

//AN_Xml:

这种思路与 ReentrantReadWriteLock 读写锁的设计思想非常类似,即读读不互斥、读写互斥、写写互斥(只有读读不互斥)。CopyOnWriteArrayList 更进一步地实现了这一思想。为了将读操作性能发挥到极致,CopyOnWriteArrayList 中的读取操作是完全无需加锁的。更加厉害的是,写入操作也不会阻塞读取操作,只有写写才会互斥。这样一来,读操作的性能就可以大幅度提升。

//AN_Xml:

CopyOnWriteArrayList 线程安全的核心在于其采用了 写时复制(Copy-On-Write) 的策略,从 CopyOnWriteArrayList 的名字就能看出了。

//AN_Xml:

Copy-On-Write 的思想是什么?

//AN_Xml:

CopyOnWriteArrayList名字中的“Copy-On-Write”即写时复制,简称 COW。

//AN_Xml:

下面是维基百科对 Copy-On-Write 的介绍,介绍的挺不错:

//AN_Xml:
//AN_Xml:

写入时复制(英语:Copy-on-write,简称 COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

//AN_Xml:
//AN_Xml:

这里再以 CopyOnWriteArrayList为例介绍:当需要修改( addsetremove 等操作) CopyOnWriteArrayList 的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。

//AN_Xml:

可以看出,写时复制机制非常适合读多写少的并发场景,能够极大地提高系统的并发性能。

//AN_Xml:

不过,写时复制机制并不是银弹,其依然存在一些缺点,下面列举几点:

//AN_Xml:
    //AN_Xml:
  1. 内存占用:每次写操作都需要复制一份原始数据,会占用额外的内存空间,在数据量比较大的情况下,可能会导致内存资源不足。
  2. //AN_Xml:
  3. 写操作开销:每一次写操作都需要复制一份原始数据,然后再进行修改和替换,所以写操作的开销相对较大,在写入比较频繁的场景下,性能可能会受到影响。
  4. //AN_Xml:
  5. 数据一致性问题:修改操作不会立即反映到最终结果中,还需要等待复制完成,这可能会导致一定的数据一致性问题。
  6. //AN_Xml:
  7. ……
  8. //AN_Xml:
//AN_Xml:

CopyOnWriteArrayList 源码分析

//AN_Xml:

这里以 JDK1.8 为例,分析一下 CopyOnWriteArrayList 的底层核心源码。

//AN_Xml:

CopyOnWriteArrayList 的类定义如下:

//AN_Xml:
public class CopyOnWriteArrayList<E>
//AN_Xml:extends Object
//AN_Xml:implements List<E>, RandomAccess, Cloneable, Serializable
//AN_Xml:{
//AN_Xml:  //...
//AN_Xml:}
//AN_Xml:

CopyOnWriteArrayList 实现了以下接口:

//AN_Xml:
    //AN_Xml:
  • List : 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。
  • //AN_Xml:
  • RandomAccess :这是一个标志接口,表明实现这个接口的 List 集合是支持 快速随机访问 的。
  • //AN_Xml:
  • Cloneable :表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。
  • //AN_Xml:
  • Serializable : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。
  • //AN_Xml:
//AN_Xml:

CopyOnWriteArrayList 类图

//AN_Xml:

初始化

//AN_Xml:

CopyOnWriteArrayList 中有一个无参构造函数和两个有参构造函数。

//AN_Xml:
// 创建一个空的 CopyOnWriteArrayList
//AN_Xml:public CopyOnWriteArrayList() {
//AN_Xml:    setArray(new Object[0]);
//AN_Xml:}
//AN_Xml:
//AN_Xml:// 按照集合的迭代器返回的顺序创建一个包含指定集合元素的 CopyOnWriteArrayList
//AN_Xml:public CopyOnWriteArrayList(Collection<? extends E> c) {
//AN_Xml:    Object[] elements;
//AN_Xml:    if (c.getClass() == CopyOnWriteArrayList.class)
//AN_Xml:        elements = ((CopyOnWriteArrayList<?>)c).getArray();
//AN_Xml:    else {
//AN_Xml:        elements = c.toArray();
//AN_Xml:        // c.toArray might (incorrectly) not return Object[] (see 6260652)
//AN_Xml:        if (elements.getClass() != Object[].class)
//AN_Xml:            elements = Arrays.copyOf(elements, elements.length, Object[].class);
//AN_Xml:    }
//AN_Xml:    setArray(elements);
//AN_Xml:}
//AN_Xml:
//AN_Xml:// 创建一个包含指定数组的副本的列表
//AN_Xml:public CopyOnWriteArrayList(E[] toCopyIn) {
//AN_Xml:    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
//AN_Xml:}
//AN_Xml:

插入元素

//AN_Xml:

CopyOnWriteArrayListadd()方法有三个版本:

//AN_Xml:
    //AN_Xml:
  • add(E e):在 CopyOnWriteArrayList 的尾部插入元素。
  • //AN_Xml:
  • add(int index, E element):在 CopyOnWriteArrayList 的指定位置插入元素。
  • //AN_Xml:
  • addIfAbsent(E e):如果指定元素不存在,那么添加该元素。如果成功添加元素则返回 true。
  • //AN_Xml:
//AN_Xml:

这里以add(E e)为例进行介绍:

//AN_Xml:
// 插入元素到 CopyOnWriteArrayList 的尾部
//AN_Xml:public boolean add(E e) {
//AN_Xml:    final ReentrantLock lock = this.lock;
//AN_Xml:    // 加锁
//AN_Xml:    lock.lock();
//AN_Xml:    try {
//AN_Xml:        // 获取原来的数组
//AN_Xml:        Object[] elements = getArray();
//AN_Xml:        // 原来数组的长度
//AN_Xml:        int len = elements.length;
//AN_Xml:        // 创建一个长度+1的新数组,并将原来数组的元素复制给新数组
//AN_Xml:        Object[] newElements = Arrays.copyOf(elements, len + 1);
//AN_Xml:        // 元素放在新数组末尾
//AN_Xml:        newElements[len] = e;
//AN_Xml:        // array指向新数组
//AN_Xml:        setArray(newElements);
//AN_Xml:        return true;
//AN_Xml:    } finally {
//AN_Xml:        // 解锁
//AN_Xml:        lock.unlock();
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

从上面的源码可以看出:

//AN_Xml:
    //AN_Xml:
  • add方法内部用到了 ReentrantLock 加锁,保证了同步,避免了多线程写的时候会复制出多个副本出来。锁被修饰保证了锁的内存地址肯定不会被修改,并且,释放锁的逻辑放在 finally 中,可以保证锁能被释放。
  • //AN_Xml:
  • CopyOnWriteArrayList 通过复制底层数组的方式实现写操作,即先创建一个新的数组来容纳新添加的元素,然后在新数组中进行写操作,最后将新数组赋值给底层数组的引用,替换掉旧的数组。这也就证明了我们前面说的:CopyOnWriteArrayList 线程安全的核心在于其采用了 写时复制(Copy-On-Write) 的策略。
  • //AN_Xml:
  • 每次写操作都需要通过 Arrays.copyOf 复制底层数组,时间复杂度是 O(n) 的,且会占用额外的内存空间。因此,CopyOnWriteArrayList 适用于读多写少的场景,在写操作不频繁且内存资源充足的情况下,可以提升系统的性能表现。
  • //AN_Xml:
  • CopyOnWriteArrayList 中并没有类似于 ArrayListgrow() 方法扩容的操作。
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

Arrays.copyOf 方法的时间复杂度是 O(n),其中 n 表示需要复制的数组长度。因为这个方法的实现原理是先创建一个新的数组,然后将源数组中的数据复制到新数组中,最后返回新数组。这个方法会复制整个数组,因此其时间复杂度与数组长度成正比,即 O(n)。值得注意的是,由于底层调用了系统级别的拷贝指令,因此在实际应用中这个方法的性能表现比较优秀,但是也需要注意控制复制的数据量,避免出现内存占用过高的情况。

//AN_Xml:
//AN_Xml:

读取元素

//AN_Xml:

CopyOnWriteArrayList 的读取操作是基于内部数组 array 并没有发生实际的修改,因此在读取操作时不需要进行同步控制和锁操作,可以保证数据的安全性。这种机制下,多个线程可以同时读取列表中的元素。

//AN_Xml:
// 底层数组,只能通过getArray和setArray方法访问
//AN_Xml:private transient volatile Object[] array;
//AN_Xml:
//AN_Xml:public E get(int index) {
//AN_Xml:    return get(getArray(), index);
//AN_Xml:}
//AN_Xml:
//AN_Xml:final Object[] getArray() {
//AN_Xml:    return array;
//AN_Xml:}
//AN_Xml:
//AN_Xml:private E get(Object[] a, int index) {
//AN_Xml:    return (E) a[index];
//AN_Xml:}
//AN_Xml:

不过,get方法是弱一致性的,在某些情况下可能读到旧的元素值。

//AN_Xml:

get(int index)方法是分两步进行的:

//AN_Xml:
    //AN_Xml:
  1. 通过getArray()获取当前数组的引用;
  2. //AN_Xml:
  3. 直接从数组中获取下标为 index 的元素。
  4. //AN_Xml:
//AN_Xml:

这个过程并没有加锁,所以在并发环境下可能出现如下情况:

//AN_Xml:
    //AN_Xml:
  1. 线程 1 调用get(int index)方法获取值,内部通过getArray()方法获取到了 array 属性值;
  2. //AN_Xml:
  3. 线程 2 调用CopyOnWriteArrayListaddsetremove 等修改方法时,内部通过setArray方法修改了array属性的值;
  4. //AN_Xml:
  5. 线程 1 还是从旧的 array 数组中取值。
  6. //AN_Xml:
//AN_Xml:

获取列表中元素的个数

//AN_Xml:
public int size() {
//AN_Xml:    return getArray().length;
//AN_Xml:}
//AN_Xml:

CopyOnWriteArrayList中的array数组每次复制都刚好能够容纳下所有元素,并不像ArrayList那样会预留一定的空间。因此,CopyOnWriteArrayList中并没有size属性CopyOnWriteArrayList的底层数组的长度就是元素个数,因此size()方法只要返回数组长度就可以了。

//AN_Xml:

删除元素

//AN_Xml:

CopyOnWriteArrayList删除元素相关的方法一共有 4 个:

//AN_Xml:
    //AN_Xml:
  1. remove(int index):移除此列表中指定位置上的元素。将任何后续元素向左移动(从它们的索引中减去 1)。
  2. //AN_Xml:
  3. boolean remove(Object o):删除此列表中首次出现的指定元素,如果不存在该元素则返回 false。
  4. //AN_Xml:
  5. boolean removeAll(Collection<?> c):从此列表中删除指定集合中包含的所有元素。
  6. //AN_Xml:
  7. void clear():移除此列表中的所有元素。
  8. //AN_Xml:
//AN_Xml:

这里以remove(int index)为例进行介绍:

//AN_Xml:
public E remove(int index) {
//AN_Xml:    // 获取可重入锁
//AN_Xml:    final ReentrantLock lock = this.lock;
//AN_Xml:    // 加锁
//AN_Xml:    lock.lock();
//AN_Xml:    try {
//AN_Xml:         //获取当前array数组
//AN_Xml:        Object[] elements = getArray();
//AN_Xml:        // 获取当前array长度
//AN_Xml:        int len = elements.length;
//AN_Xml:        //获取指定索引的元素(旧值)
//AN_Xml:        E oldValue = get(elements, index);
//AN_Xml:        int numMoved = len - index - 1;
//AN_Xml:        // 判断删除的是否是最后一个元素
//AN_Xml:        if (numMoved == 0)
//AN_Xml:             // 如果删除的是最后一个元素,直接复制该元素前的所有元素到新的数组
//AN_Xml:            setArray(Arrays.copyOf(elements, len - 1));
//AN_Xml:        else {
//AN_Xml:            // 分段复制,将index前的元素和index+1后的元素复制到新数组
//AN_Xml:            // 新数组长度为旧数组长度-1
//AN_Xml:            Object[] newElements = new Object[len - 1];
//AN_Xml:            System.arraycopy(elements, 0, newElements, 0, index);
//AN_Xml:            System.arraycopy(elements, index + 1, newElements, index,
//AN_Xml:                             numMoved);
//AN_Xml:            //将新数组赋值给array引用
//AN_Xml:            setArray(newElements);
//AN_Xml:        }
//AN_Xml:        return oldValue;
//AN_Xml:    } finally {
//AN_Xml:         // 解锁
//AN_Xml:        lock.unlock();
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

判断元素是否存在

//AN_Xml:

CopyOnWriteArrayList提供了两个用于判断指定元素是否在列表中的方法:

//AN_Xml:
    //AN_Xml:
  • contains(Object o):判断是否包含指定元素。
  • //AN_Xml:
  • containsAll(Collection<?> c):判断是否保证指定集合的全部元素。
  • //AN_Xml:
//AN_Xml:
// 判断是否包含指定元素
//AN_Xml:public boolean contains(Object o) {
//AN_Xml:    //获取当前array数组
//AN_Xml:    Object[] elements = getArray();
//AN_Xml:    //调用index尝试查找指定元素,如果返回值大于等于0,则返回true,否则返回false
//AN_Xml:    return indexOf(o, elements, 0, elements.length) >= 0;
//AN_Xml:}
//AN_Xml:
//AN_Xml:// 判断是否保证指定集合的全部元素
//AN_Xml:public boolean containsAll(Collection<?> c) {
//AN_Xml:    //获取当前array数组
//AN_Xml:    Object[] elements = getArray();
//AN_Xml:    //获取数组长度
//AN_Xml:    int len = elements.length;
//AN_Xml:    //遍历指定集合
//AN_Xml:    for (Object e : c) {
//AN_Xml:        //循环调用indexOf方法判断,只要有一个没有包含就直接返回false
//AN_Xml:        if (indexOf(e, elements, 0, len) < 0)
//AN_Xml:            return false;
//AN_Xml:    }
//AN_Xml:    //最后表示全部包含或者制定集合为空集合,那么返回true
//AN_Xml:    return true;
//AN_Xml:}
//AN_Xml:

CopyOnWriteArrayList 常用方法测试

//AN_Xml:

代码:

//AN_Xml:
// 创建一个 CopyOnWriteArrayList 对象
//AN_Xml:CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
//AN_Xml:
//AN_Xml:// 向列表中添加元素
//AN_Xml:list.add("Java");
//AN_Xml:list.add("Python");
//AN_Xml:list.add("C++");
//AN_Xml:System.out.println("初始列表:" + list);
//AN_Xml:
//AN_Xml:// 使用 get 方法获取指定位置的元素
//AN_Xml:System.out.println("列表第二个元素为:" + list.get(1));
//AN_Xml:
//AN_Xml:// 使用 remove 方法删除指定元素
//AN_Xml:boolean result = list.remove("C++");
//AN_Xml:System.out.println("删除结果:" + result);
//AN_Xml:System.out.println("列表删除元素后为:" + list);
//AN_Xml:
//AN_Xml:// 使用 set 方法更新指定位置的元素
//AN_Xml:list.set(1, "Golang");
//AN_Xml:System.out.println("列表更新后为:" + list);
//AN_Xml:
//AN_Xml:// 使用 add 方法在指定位置插入元素
//AN_Xml:list.add(0, "PHP");
//AN_Xml:System.out.println("列表插入元素后为:" + list);
//AN_Xml:
//AN_Xml:// 使用 size 方法获取列表大小
//AN_Xml:System.out.println("列表大小为:" + list.size());
//AN_Xml:
//AN_Xml:// 使用 removeAll 方法删除指定集合中所有出现的元素
//AN_Xml:result = list.removeAll(List.of("Java", "Golang"));
//AN_Xml:System.out.println("批量删除结果:" + result);
//AN_Xml:System.out.println("列表批量删除元素后为:" + list);
//AN_Xml:
//AN_Xml:// 使用 clear 方法清空列表中所有元素
//AN_Xml:list.clear();
//AN_Xml:System.out.println("列表清空后为:" + list);
//AN_Xml:

输出:

//AN_Xml:
列表更新后为:[Java, Golang]
//AN_Xml:列表插入元素后为:[PHP, Java, Golang]
//AN_Xml:列表大小为:3
//AN_Xml:批量删除结果:true
//AN_Xml:列表批量删除元素后为:[PHP]
//AN_Xml:列表清空后为:[]
//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: LinkedList 源码分析 //AN_Xml: https://javaguide.cn/java/collection/linkedlist-source-code.html //AN_Xml: https://javaguide.cn/java/collection/linkedlist-source-code.html //AN_Xml: LinkedList 源码分析 //AN_Xml: LinkedList源码深度解析:剖析双向链表结构、Deque接口实现、头尾插入删除O(1)时间复杂度、与ArrayList性能对比及适用场景。 //AN_Xml: Java //AN_Xml: Wed, 07 Jun 2023 05:18:39 GMT //AN_Xml: 《SpringAI 智能面试平台+RAG 知识库》

//AN_Xml:

LinkedList 简介

//AN_Xml:

LinkedList 是一个基于双向链表实现的集合类,经常被拿来和 ArrayList 做比较。关于 LinkedListArrayList的详细对比,我们 Java 集合常见面试题总结(上)有详细介绍到。

//AN_Xml:

双向链表

//AN_Xml:

不过,我们在项目中一般是不会使用到 LinkedList 的,需要用到 LinkedList 的场景几乎都可以使用 ArrayList 来代替,并且,性能通常会更好!就连 LinkedList 的作者约书亚 · 布洛克(Josh Bloch)自己都说从来不会使用 LinkedList

//AN_Xml:

//AN_Xml:

另外,不要下意识地认为 LinkedList 作为链表就最适合元素增删的场景。我在上面也说了,LinkedList 仅仅在头尾插入或者删除元素的时候时间复杂度近似 O(1),其他情况增删元素的平均时间复杂度都是 O(n) 。

//AN_Xml:

LinkedList 插入和删除元素的时间复杂度?

//AN_Xml:
    //AN_Xml:
  • 头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
  • //AN_Xml:
  • 尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
  • //AN_Xml:
  • 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,不过由于有头尾指针,可以从较近的指针出发,因此需要遍历平均 n/4 个元素,时间复杂度为 O(n)。
  • //AN_Xml:
//AN_Xml:

LinkedList 为什么不能实现 RandomAccess 接口?

//AN_Xml:

RandomAccess 是一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素)。由于 LinkedList 底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现 RandomAccess 接口。

//AN_Xml:

LinkedList 源码分析

//AN_Xml:

这里以 JDK1.8 为例,分析一下 LinkedList 的底层核心源码。

//AN_Xml:

LinkedList 的类定义如下:

//AN_Xml:
public class LinkedList<E>
//AN_Xml:    extends AbstractSequentialList<E>
//AN_Xml:    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
//AN_Xml:{
//AN_Xml:  //...
//AN_Xml:}
//AN_Xml:

LinkedList 继承了 AbstractSequentialList ,而 AbstractSequentialList 又继承于 AbstractList

//AN_Xml:

阅读过 ArrayList 的源码我们就知道,ArrayList 同样继承了 AbstractList , 所以 LinkedList 会有大部分方法和 ArrayList 相似。

//AN_Xml:

LinkedList 实现了以下接口:

//AN_Xml:
    //AN_Xml:
  • List : 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。
  • //AN_Xml:
  • Deque :继承自 Queue 接口,具有双端队列的特性,支持从两端插入和删除元素,方便实现栈和队列等数据结构。需要注意,Deque 的发音为 "deck" [dɛk],这个大部分人都会读错。
  • //AN_Xml:
  • Cloneable :表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。
  • //AN_Xml:
  • Serializable : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。
  • //AN_Xml:
//AN_Xml:

LinkedList 类图

//AN_Xml:

LinkedList 中的元素是通过 Node 定义的:

//AN_Xml:
private static class Node<E> {
//AN_Xml:    E item;// 节点值
//AN_Xml:    Node<E> next; // 指向的下一个节点(后继节点)
//AN_Xml:    Node<E> prev; // 指向的前一个节点(前驱结点)
//AN_Xml:
//AN_Xml:    // 初始化参数顺序分别是:前驱结点、本身节点值、后继节点
//AN_Xml:    Node(Node<E> prev, E element, Node<E> next) {
//AN_Xml:        this.item = element;
//AN_Xml:        this.next = next;
//AN_Xml:        this.prev = prev;
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

初始化

//AN_Xml:

LinkedList 中有一个无参构造函数和一个有参构造函数。

//AN_Xml:
// 创建一个空的链表对象
//AN_Xml:public LinkedList() {
//AN_Xml:}
//AN_Xml:
//AN_Xml:// 接收一个集合类型作为参数,会创建一个与传入集合相同元素的链表对象
//AN_Xml:public LinkedList(Collection<? extends E> c) {
//AN_Xml:    this();
//AN_Xml:    addAll(c);
//AN_Xml:}
//AN_Xml:

插入元素

//AN_Xml:

LinkedList 除了实现了 List 接口相关方法,还实现了 Deque 接口的很多方法,所以我们有很多种方式插入元素。

//AN_Xml:

我们这里以 List 接口中相关的插入方法为例进行源码讲解,对应的是add() 方法。

//AN_Xml:

add() 方法有两个版本:

//AN_Xml:
    //AN_Xml:
  • add(E e):用于在 LinkedList 的尾部插入元素,即将新元素作为链表的最后一个元素,时间复杂度为 O(1)。
  • //AN_Xml:
  • add(int index, E element):用于在指定位置插入元素。这种插入方式需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/4 个元素,时间复杂度为 O(n)。
  • //AN_Xml:
//AN_Xml:
// 在链表尾部插入元素
//AN_Xml:public boolean add(E e) {
//AN_Xml:    linkLast(e);
//AN_Xml:    return true;
//AN_Xml:}
//AN_Xml:
//AN_Xml:// 在链表指定位置插入元素
//AN_Xml:public void add(int index, E element) {
//AN_Xml:    // 下标越界检查
//AN_Xml:    checkPositionIndex(index);
//AN_Xml:
//AN_Xml:    // 判断 index 是不是链表尾部位置
//AN_Xml:    if (index == size)
//AN_Xml:        // 如果是就直接调用 linkLast 方法将元素节点插入链表尾部即可
//AN_Xml:        linkLast(element);
//AN_Xml:    else
//AN_Xml:        // 如果不是则调用 linkBefore 方法将其插入指定元素之前
//AN_Xml:        linkBefore(element, node(index));
//AN_Xml:}
//AN_Xml:
//AN_Xml:// 将元素节点插入到链表尾部
//AN_Xml:void linkLast(E e) {
//AN_Xml:    // 将最后一个元素赋值(引用传递)给节点 l
//AN_Xml:    final Node<E> l = last;
//AN_Xml:    // 创建节点,并指定节点前驱为链表尾节点 last,后继引用为空
//AN_Xml:    final Node<E> newNode = new Node<>(l, e, null);
//AN_Xml:    // 将 last 引用指向新节点
//AN_Xml:    last = newNode;
//AN_Xml:    // 判断尾节点是否为空
//AN_Xml:    // 如果 l 是null 意味着这是第一次添加元素
//AN_Xml:    if (l == null)
//AN_Xml:        // 如果是第一次添加,将first赋值为新节点,此时链表只有一个元素
//AN_Xml:        first = newNode;
//AN_Xml:    else
//AN_Xml:        // 如果不是第一次添加,将新节点赋值给l(添加前的最后一个元素)的next
//AN_Xml:        l.next = newNode;
//AN_Xml:    size++;
//AN_Xml:    modCount++;
//AN_Xml:}
//AN_Xml:
//AN_Xml:// 在指定元素之前插入元素
//AN_Xml:void linkBefore(E e, Node<E> succ) {
//AN_Xml:    // assert succ != null;断言 succ不为 null
//AN_Xml:    // 定义一个节点元素保存 succ 的 prev 引用,也就是它的前一节点信息
//AN_Xml:    final Node<E> pred = succ.prev;
//AN_Xml:    // 初始化节点,并指明前驱和后继节点
//AN_Xml:    final Node<E> newNode = new Node<>(pred, e, succ);
//AN_Xml:    // 将 succ 节点前驱引用 prev 指向新节点
//AN_Xml:    succ.prev = newNode;
//AN_Xml:    // 判断前驱节点是否为空,为空表示 succ 是第一个节点
//AN_Xml:    if (pred == null)
//AN_Xml:        // 新节点成为第一个节点
//AN_Xml:        first = newNode;
//AN_Xml:    else
//AN_Xml:        // succ 节点前驱的后继引用指向新节点
//AN_Xml:        pred.next = newNode;
//AN_Xml:    size++;
//AN_Xml:    modCount++;
//AN_Xml:}
//AN_Xml:

获取元素

//AN_Xml:

LinkedList获取元素相关的方法一共有 3 个:

//AN_Xml:
    //AN_Xml:
  1. getFirst():获取链表的第一个元素。
  2. //AN_Xml:
  3. getLast():获取链表的最后一个元素。
  4. //AN_Xml:
  5. get(int index):获取链表指定位置的元素。
  6. //AN_Xml:
//AN_Xml:
// 获取链表的第一个元素
//AN_Xml:public E getFirst() {
//AN_Xml:    final Node<E> f = first;
//AN_Xml:    if (f == null)
//AN_Xml:        throw new NoSuchElementException();
//AN_Xml:    return f.item;
//AN_Xml:}
//AN_Xml:
//AN_Xml:// 获取链表的最后一个元素
//AN_Xml:public E getLast() {
//AN_Xml:    final Node<E> l = last;
//AN_Xml:    if (l == null)
//AN_Xml:        throw new NoSuchElementException();
//AN_Xml:    return l.item;
//AN_Xml:}
//AN_Xml:
//AN_Xml:// 获取链表指定位置的元素
//AN_Xml:public E get(int index) {
//AN_Xml:  // 下标越界检查,如果越界就抛异常
//AN_Xml:  checkElementIndex(index);
//AN_Xml:  // 返回链表中对应下标的元素
//AN_Xml:  return node(index).item;
//AN_Xml:}
//AN_Xml:

这里的核心在于 node(int index) 这个方法:

//AN_Xml:
// 返回指定下标的非空节点
//AN_Xml:Node<E> node(int index) {
//AN_Xml:    // 断言下标未越界
//AN_Xml:    // assert isElementIndex(index);
//AN_Xml:    // 如果index小于size的二分之一  从前开始查找(向后查找)  反之向前查找
//AN_Xml:    if (index < (size >> 1)) {
//AN_Xml:        Node<E> x = first;
//AN_Xml:        // 遍历,循环向后查找,直至 i == index
//AN_Xml:        for (int i = 0; i < index; i++)
//AN_Xml:            x = x.next;
//AN_Xml:        return x;
//AN_Xml:    } else {
//AN_Xml:        Node<E> x = last;
//AN_Xml:        for (int i = size - 1; i > index; i--)
//AN_Xml:            x = x.prev;
//AN_Xml:        return x;
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

get(int index)remove(int index) 等方法内部都调用了该方法来获取对应的节点。

//AN_Xml:

从这个方法的源码可以看出,该方法通过比较索引值与链表 size 的一半大小来确定从链表头还是尾开始遍历。如果索引值小于 size 的一半,就从链表头开始遍历,反之从链表尾开始遍历。这样可以在较短的时间内找到目标节点,充分利用了双向链表的特性来提高效率。

//AN_Xml:

删除元素

//AN_Xml:

LinkedList删除元素相关的方法一共有 5 个:

//AN_Xml:
    //AN_Xml:
  1. removeFirst():删除并返回链表的第一个元素。
  2. //AN_Xml:
  3. removeLast():删除并返回链表的最后一个元素。
  4. //AN_Xml:
  5. remove(E e):删除链表中首次出现的指定元素,如果不存在该元素则返回 false。
  6. //AN_Xml:
  7. remove(int index):删除指定索引处的元素,并返回该元素的值。
  8. //AN_Xml:
  9. void clear():移除此链表中的所有元素。
  10. //AN_Xml:
//AN_Xml:
// 删除并返回链表的第一个元素
//AN_Xml:public E removeFirst() {
//AN_Xml:    final Node<E> f = first;
//AN_Xml:    if (f == null)
//AN_Xml:        throw new NoSuchElementException();
//AN_Xml:    return unlinkFirst(f);
//AN_Xml:}
//AN_Xml:
//AN_Xml:// 删除并返回链表的最后一个元素
//AN_Xml:public E removeLast() {
//AN_Xml:    final Node<E> l = last;
//AN_Xml:    if (l == null)
//AN_Xml:        throw new NoSuchElementException();
//AN_Xml:    return unlinkLast(l);
//AN_Xml:}
//AN_Xml:
//AN_Xml:// 删除链表中首次出现的指定元素,如果不存在该元素则返回 false
//AN_Xml:public boolean remove(Object o) {
//AN_Xml:    // 如果指定元素为 null,遍历链表找到第一个为 null 的元素进行删除
//AN_Xml:    if (o == null) {
//AN_Xml:        for (Node<E> x = first; x != null; x = x.next) {
//AN_Xml:            if (x.item == null) {
//AN_Xml:                unlink(x);
//AN_Xml:                return true;
//AN_Xml:            }
//AN_Xml:        }
//AN_Xml:    } else {
//AN_Xml:        // 如果不为 null ,遍历链表找到要删除的节点
//AN_Xml:        for (Node<E> x = first; x != null; x = x.next) {
//AN_Xml:            if (o.equals(x.item)) {
//AN_Xml:                unlink(x);
//AN_Xml:                return true;
//AN_Xml:            }
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:    return false;
//AN_Xml:}
//AN_Xml:
//AN_Xml:// 删除链表指定位置的元素
//AN_Xml:public E remove(int index) {
//AN_Xml:    // 下标越界检查,如果越界就抛异常
//AN_Xml:    checkElementIndex(index);
//AN_Xml:    return unlink(node(index));
//AN_Xml:}
//AN_Xml:

这里的核心在于 unlink(Node<E> x) 这个方法:

//AN_Xml:
E unlink(Node<E> x) {
//AN_Xml:    // 断言 x 不为 null
//AN_Xml:    // assert x != null;
//AN_Xml:    // 获取当前节点(也就是待删除节点)的元素
//AN_Xml:    final E element = x.item;
//AN_Xml:    // 获取当前节点的下一个节点
//AN_Xml:    final Node<E> next = x.next;
//AN_Xml:    // 获取当前节点的前一个节点
//AN_Xml:    final Node<E> prev = x.prev;
//AN_Xml:
//AN_Xml:    // 如果前一个节点为空,则说明当前节点是头节点
//AN_Xml:    if (prev == null) {
//AN_Xml:        // 直接让链表头指向当前节点的下一个节点
//AN_Xml:        first = next;
//AN_Xml:    } else { // 如果前一个节点不为空
//AN_Xml:        // 将前一个节点的 next 指针指向当前节点的下一个节点
//AN_Xml:        prev.next = next;
//AN_Xml:        // 将当前节点的 prev 指针置为 null,,方便 GC 回收
//AN_Xml:        x.prev = null;
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    // 如果下一个节点为空,则说明当前节点是尾节点
//AN_Xml:    if (next == null) {
//AN_Xml:        // 直接让链表尾指向当前节点的前一个节点
//AN_Xml:        last = prev;
//AN_Xml:    } else { // 如果下一个节点不为空
//AN_Xml:        // 将下一个节点的 prev 指针指向当前节点的前一个节点
//AN_Xml:        next.prev = prev;
//AN_Xml:        // 将当前节点的 next 指针置为 null,方便 GC 回收
//AN_Xml:        x.next = null;
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    // 将当前节点元素置为 null,方便 GC 回收
//AN_Xml:    x.item = null;
//AN_Xml:    size--;
//AN_Xml:    modCount++;
//AN_Xml:    return element;
//AN_Xml:}
//AN_Xml:

unlink() 方法的逻辑如下:

//AN_Xml:
    //AN_Xml:
  1. 首先获取待删除节点 x 的前驱和后继节点;
  2. //AN_Xml:
  3. 判断待删除节点是否为头节点或尾节点: //AN_Xml:
      //AN_Xml:
    • 如果 x 是头节点,则将 first 指向 x 的后继节点 next
    • //AN_Xml:
    • 如果 x 是尾节点,则将 last 指向 x 的前驱节点 prev
    • //AN_Xml:
    • 如果 x 不是头节点也不是尾节点,执行下一步操作
    • //AN_Xml:
    //AN_Xml:
  4. //AN_Xml:
  5. 将待删除节点 x 的前驱的后继指向待删除节点的后继 next,断开 x 和 x.prev 之间的链接;
  6. //AN_Xml:
  7. 将待删除节点 x 的后继的前驱指向待删除节点的前驱 prev,断开 x 和 x.next 之间的链接;
  8. //AN_Xml:
  9. 将待删除节点 x 的元素置空,修改链表长度。
  10. //AN_Xml:
//AN_Xml:

可以参考下图理解(图源:LinkedList 源码分析(JDK 1.8)):

//AN_Xml:

unlink 方法逻辑

//AN_Xml:

遍历链表

//AN_Xml:

推荐使用for-each 循环来遍历 LinkedList 中的元素, for-each 循环最终会转换成迭代器形式。

//AN_Xml:
LinkedList<String> list = new LinkedList<>();
//AN_Xml:list.add("apple");
//AN_Xml:list.add("banana");
//AN_Xml:list.add("pear");
//AN_Xml:
//AN_Xml:for (String fruit : list) {
//AN_Xml:    System.out.println(fruit);
//AN_Xml:}
//AN_Xml:

LinkedList 的遍历的核心就是它的迭代器的实现。

//AN_Xml:
// 双向迭代器
//AN_Xml:private class ListItr implements ListIterator<E> {
//AN_Xml:    // 表示上一次调用 next() 或 previous() 方法时经过的节点;
//AN_Xml:    private Node<E> lastReturned;
//AN_Xml:    // 表示下一个要遍历的节点;
//AN_Xml:    private Node<E> next;
//AN_Xml:    // 表示下一个要遍历的节点的下标,也就是当前节点的后继节点的下标;
//AN_Xml:    private int nextIndex;
//AN_Xml:    // 表示当前遍历期望的修改计数值,用于和 LinkedList 的 modCount 比较,判断链表是否被其他线程修改过。
//AN_Xml:    private int expectedModCount = modCount;
//AN_Xml:    …………
//AN_Xml:}
//AN_Xml:

下面我们对迭代器 ListItr 中的核心方法进行详细介绍。

//AN_Xml:

我们先来看下从头到尾方向的迭代:

//AN_Xml:
// 判断还有没有下一个节点
//AN_Xml:public boolean hasNext() {
//AN_Xml:    // 判断下一个节点的下标是否小于链表的大小,如果是则表示还有下一个元素可以遍历
//AN_Xml:    return nextIndex < size;
//AN_Xml:}
//AN_Xml:// 获取下一个节点
//AN_Xml:public E next() {
//AN_Xml:    // 检查在迭代过程中链表是否被修改过
//AN_Xml:    checkForComodification();
//AN_Xml:    // 判断是否还有下一个节点可以遍历,如果没有则抛出 NoSuchElementException 异常
//AN_Xml:    if (!hasNext())
//AN_Xml:        throw new NoSuchElementException();
//AN_Xml:    // 将 lastReturned 指向当前节点
//AN_Xml:    lastReturned = next;
//AN_Xml:    // 将 next 指向下一个节点
//AN_Xml:    next = next.next;
//AN_Xml:    nextIndex++;
//AN_Xml:    return lastReturned.item;
//AN_Xml:}
//AN_Xml:

再来看一下从尾到头方向的迭代:

//AN_Xml:
// 判断是否还有前一个节点
//AN_Xml:public boolean hasPrevious() {
//AN_Xml:    return nextIndex > 0;
//AN_Xml:}
//AN_Xml:
//AN_Xml:// 获取前一个节点
//AN_Xml:public E previous() {
//AN_Xml:    // 检查是否在迭代过程中链表被修改
//AN_Xml:    checkForComodification();
//AN_Xml:    // 如果没有前一个节点,则抛出异常
//AN_Xml:    if (!hasPrevious())
//AN_Xml:        throw new NoSuchElementException();
//AN_Xml:    // 将 lastReturned 和 next 指针指向上一个节点
//AN_Xml:    lastReturned = next = (next == null) ? last : next.prev;
//AN_Xml:    nextIndex--;
//AN_Xml:    return lastReturned.item;
//AN_Xml:}
//AN_Xml:

如果需要删除或插入元素,也可以使用迭代器进行操作。

//AN_Xml:
LinkedList<String> list = new LinkedList<>();
//AN_Xml:list.add("apple");
//AN_Xml:list.add(null);
//AN_Xml:list.add("banana");
//AN_Xml:
//AN_Xml://  Collection 接口的 removeIf 方法底层依然是基于迭代器
//AN_Xml:list.removeIf(Objects::isNull);
//AN_Xml:
//AN_Xml:for (String fruit : list) {
//AN_Xml:    System.out.println(fruit);
//AN_Xml:}
//AN_Xml:

迭代器对应的移除元素的方法如下:

//AN_Xml:
// 从列表中删除上次被返回的元素
//AN_Xml:public void remove() {
//AN_Xml:    // 检查是否在迭代过程中链表被修改
//AN_Xml:    checkForComodification();
//AN_Xml:    // 如果上次返回的节点为空,则抛出异常
//AN_Xml:    if (lastReturned == null)
//AN_Xml:        throw new IllegalStateException();
//AN_Xml:
//AN_Xml:    // 获取当前节点的下一个节点
//AN_Xml:    Node<E> lastNext = lastReturned.next;
//AN_Xml:    // 从链表中删除上次返回的节点
//AN_Xml:    unlink(lastReturned);
//AN_Xml:    // 修改指针
//AN_Xml:    if (next == lastReturned)
//AN_Xml:        next = lastNext;
//AN_Xml:    else
//AN_Xml:        nextIndex--;
//AN_Xml:    // 将上次返回的节点引用置为 null,方便 GC 回收
//AN_Xml:    lastReturned = null;
//AN_Xml:    expectedModCount++;
//AN_Xml:}
//AN_Xml:

LinkedList 常用方法测试

//AN_Xml:

代码:

//AN_Xml:
// 创建 LinkedList 对象
//AN_Xml:LinkedList<String> list = new LinkedList<>();
//AN_Xml:
//AN_Xml:// 添加元素到链表末尾
//AN_Xml:list.add("apple");
//AN_Xml:list.add("banana");
//AN_Xml:list.add("pear");
//AN_Xml:System.out.println("链表内容:" + list);
//AN_Xml:
//AN_Xml:// 在指定位置插入元素
//AN_Xml:list.add(1, "orange");
//AN_Xml:System.out.println("链表内容:" + list);
//AN_Xml:
//AN_Xml:// 获取指定位置的元素
//AN_Xml:String fruit = list.get(2);
//AN_Xml:System.out.println("索引为 2 的元素:" + fruit);
//AN_Xml:
//AN_Xml:// 修改指定位置的元素
//AN_Xml:list.set(3, "grape");
//AN_Xml:System.out.println("链表内容:" + list);
//AN_Xml:
//AN_Xml:// 删除指定位置的元素
//AN_Xml:list.remove(0);
//AN_Xml:System.out.println("链表内容:" + list);
//AN_Xml:
//AN_Xml:// 删除第一个出现的指定元素
//AN_Xml:list.remove("banana");
//AN_Xml:System.out.println("链表内容:" + list);
//AN_Xml:
//AN_Xml:// 获取链表的长度
//AN_Xml:int size = list.size();
//AN_Xml:System.out.println("链表长度:" + size);
//AN_Xml:
//AN_Xml:// 清空链表
//AN_Xml:list.clear();
//AN_Xml:System.out.println("清空后的链表:" + list);
//AN_Xml:

输出:

//AN_Xml:
索引为 2 的元素:banana
//AN_Xml:链表内容:[apple, orange, banana, grape]
//AN_Xml:链表内容:[orange, banana, grape]
//AN_Xml:链表内容:[orange, grape]
//AN_Xml:链表长度:2
//AN_Xml:清空后的链表:[]
//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 工作五年之后,对技术和业务的思考 //AN_Xml: https://javaguide.cn/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.html //AN_Xml: https://javaguide.cn/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.html //AN_Xml: 工作五年之后,对技术和业务的思考 //AN_Xml: 工作五年之后,对技术和业务的思考:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。 //AN_Xml: 技术文章精选集 //AN_Xml: Sun, 04 Jun 2023 16:54:06 GMT //AN_Xml: //AN_Xml:

推荐语:这是我在两年前看到的一篇对我触动比较深的文章。确实要学会适应变化,并积累能力。积累解决问题的能力,优化思考方式,拓宽自己的认知。

//AN_Xml:

原文地址: https://mp.weixin.qq.com/s/CTbEdi0F4-qFoJT05kNlXA

//AN_Xml: //AN_Xml:

苦海无边,回头无岸。

//AN_Xml:

01 前言

//AN_Xml:

晃晃悠悠的,在互联网行业工作了五年,默然回首,你看哪里像灯火阑珊处?

//AN_Xml:

初入职场,大部分程序员会觉得苦学技术,以后会顺风顺水升职加薪,这样的想法没有错,但是不算全面,五年后你会不会继续做技术写代码这是核心问题。

//AN_Xml:

初入职场,会觉得努力加班可以不断提升能力,可以学到技术的公司就算薪水低点也可以接受,但是五年之后会认为加班都是在不断挤压自己的上升空间,薪水低是人生的天花板。

//AN_Xml:

这里想说的关键问题就是:初入职场的认知和想法大部分不会再适用于五年后的认知。

//AN_Xml:

工作五年之后面临的最大压力就是选择:职场天花板,技术能力天花板,薪水天花板,三十岁天花板。

//AN_Xml:

如何面对这些问题,是大部分程序员都在思考和纠结的。做选择的唯一参考点就是:利益最大化,这里可以理解为职场更好的升职加薪,顺风顺水。

//AN_Xml:

五年,变化最大不是工作经验,能力积累,而是心态,清楚的知道现实和理想之间是存在巨大的差距。

//AN_Xml:

02 学会适应变化,并积累能力

//AN_Xml:

回首自己的职场五年,最认可的一句话就是:学会适应变化,并积累能力。

//AN_Xml:

变化的就是,五年的时间技术框架更新迭代,开发工具的变迁,公司环境队友的更换,甚至是不同城市的流浪,想着能把肉体和灵魂安放在一处,有句很经典的话就是:唯一不变的就是变化本身。

//AN_Xml:

要积累的是:解决问题的能力,思考方式,拓宽认知。

//AN_Xml:

这种很难直白的描述,属于个人认知的范畴,不同的人有不一样的看法,所以只能站在大众化的角度去思考。

//AN_Xml:

首先聊聊技术,大部分小白级别的,都希望自己的技术能力不断提高,争取做到架构师级别,但是站在当前的互联网环境中,这种想法实现难度还是偏高,这里既不是打击也不是为了抬杠。

//AN_Xml:

可以观察一下现状,技术团队大的 20-30 人,小的 10-15 人,能有一个架构师去专门管理底层框架都是少有现象。

//AN_Xml:

这个问题的原因很多,首先架构师的成本过高,环境架构也不是需要经常升级,说的难听点可能框架比项目生命周期更高。

//AN_Xml:

所以大部分公司的大部分业务,基于现有大部分成熟的开源框架都可以解决,这也就导致架构师这个角色通常由项目主管代替或者级别较高的开发直接负责,这就是现实情况。

//AN_Xml:

这就导致技术框架的选择思路就是:只选对的。即这方面的人才多,开源解决方案多,以此降低技术方面对公司业务发展的影响。

//AN_Xml:

那为什么还要不断学习和积累技术能力?如果没有这个能力,程序员岗位可能根本走不了五年之久,需要用技术深度积累不断解决工作中的各种问题,用技术的广度提升自己实现业务需求的认知边界,这是安放肉体的根本保障。

//AN_Xml:

这就是导致很多五年以后的程序员压力陡然升高的原因,走向管理岗的另一个壁垒就是业务思维和认知。

//AN_Xml:

03 提高业务能力的积累

//AN_Xml:

程序员该不该用心研究业务,这个问题真的没有纠结的必要,只要不是纯技术型的公司,都需要面对业务。

//AN_Xml:

不管技术、运营、产品、管理层,都是在面向业务工作。

//AN_Xml:

从自己职场轨迹来看,五年变化最大就是解决业务问题的能力,职场之初面对很多业务场景都不知道如何下手,到几年之后设计业务的解决方案。

//AN_Xml:

这是大部分程序员在职场前五年跳槽就能涨薪的根本原因,面对业务场景,基于积累的经验和现有的开源工具,能快速给出合理的解决思路和实现过程。

//AN_Xml:

工作五年可能对技术底层的清晰程度都没有初入职场的小白清楚,但是写的程序却可以避开很多坑坑洼洼,对于业务的审视也是很细节全面。

//AN_Xml:

解决业务能力的积累,对于技术视野的宽度需求更甚,比如职场初期对于海量数据的处理束手无策,但是在工作几年之后见识数据行业的技术栈,真的就是技术选型的视野问题。

//AN_Xml:

什么是衡量技术能力的标准?站在一个共识的角度上看:系统的架构与代码设计能适应业务的不断变化和各种需求。

//AN_Xml:

相对比与技术,业务的变化更加快速频繁,高级工程师或者架构师之所以薪资高,这些角色一方面能适应业务的迭代,并且在工作中具有一定前瞻性,会考虑业务变化的情况下代码复用逻辑,这样的能力是需要一定的技术视野和业务思维的沉淀。

//AN_Xml:

所以职场中:业务能说的井井有条,代码能写的明明白白,得到机会的可能性更大。

//AN_Xml:

04 不同的阶段技术和业务的平衡和选择

//AN_Xml:

从理性的角度看技术和业务两个方面,能让大部分人职场走的平稳顺利,但是不同的阶段对两者的平衡和选择是不一样的。

//AN_Xml:

在思考如何选择的时候,可以参考二八原则的逻辑,即在任何一组东西中,最重要的只占其中一小部分,约 20%,其余 80%尽管是多数,却是次要的,因此又称二八定律。

//AN_Xml:

个人真的非常喜欢这个原则,大部分人都不是天才,所以很难三心二意同时做好几件事情,在同一时间段内应该集中精力做好一件事件。

//AN_Xml:

但是单纯的二八原则模式可能不适应大部分职场初期的人,因为初期要学习很多内容,如何在职场生存:专业能力,职场关系,为人处世,产品设计等等。

//AN_Xml:

当然这些东西不是都要用心刻意学习,但是合理安排二二六原则或其他组合是更明智的,首先是专业能力要重点练习,其次可以根据自己的兴趣合理选择一到两个方面去慢慢了解,例如产品,运营,运维,数据等,毕竟三五年以后会不会继续写代码很难说,多给自己留个机会总是有备无患。

//AN_Xml:

在职场初期,基本都是从技术角度去思考问题,如何快速提升自己的编码能力,在公司能稳定是首要目标,因此大部分时间都是在做基础编码和学习规范,这时可能 90%的心思都是放在基础编码上,另外 10%会学习环境架构。

//AN_Xml:

最多一到两年,就会开始独立负责模块需求开发,需要自己设计整个代码思路,这里业务就会进入视野,要懂得业务上下游关联关系,学会思考如何设计代码结构,才能在需求变动的情况下代码改动较少,这个时候可能就会放 20%的心思在业务方面,30%学习架构方式。

//AN_Xml:

三到五年这个时间段,是解决问题能力提升最快的时候,因为这个阶段的程序员基本都是在开发核心业务链路,例如交易、支付、结算、智能商业等模块,需要对业务整体有较清晰的把握能力,不然就是给自己挖坑,这个阶段要对业务流付出大量心血思考。

//AN_Xml:

越是核心的业务线,越是容易爆发各种问题,如果在日常工作中不花心思处理各种细节问题,半夜异常自动的消息和邮件总是容易让人憔悴。

//AN_Xml:

所以努力学习技术是提升自己,培养自己的业务认知也同样重要,个人认为这二者的分量平分秋色,只是需要在合适的阶段做出合理的权重划分。

//AN_Xml:

05 学会在职场做选择和生存

//AN_Xml:

基于技术能力和业务思维,学会在职场做选择和生存,这些是职场前五年一路走来的最大体会。

//AN_Xml:

不管是技术还是业务,这两个概念依旧是个很大的命题,不容易把握,所以学会理清这两个方面能力中的公共模块是关键。

//AN_Xml:

不管技术还是业务,都不可能从一家公司完全复制到另一家公司,但是可以把一家公司的技术框架,业务解决方案学会,并且带到另一家公司,例如技术领域内的架构、设计、流程、数据管理,业务领域内的思考方式、产品逻辑、分析等,这些是核心能力并且是大部分公司人才招聘的要求,所以这些才是工作中需要重点积累的。

//AN_Xml:

人的精力是有限的,而且面对三十这个天花板,各种事件也会接连而至,在职场中学会合理安排时间并不断提升核心能力,这样才能保证自己的竞争力。

//AN_Xml:

职场就像苦海无边,回首望去可能也没有岸边停泊,但是要具有换船的能力或者有个小木筏也就大差不差了。

//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml:
//AN_Xml: //AN_Xml: 使用建议 //AN_Xml: https://javaguide.cn/javaguide/use-suggestion.html //AN_Xml: https://javaguide.cn/javaguide/use-suggestion.html //AN_Xml: 使用建议 //AN_Xml: JavaGuide使用建议,讲解如何高效利用本站内容进行Java学习与面试准备的方法指南。 //AN_Xml: 走近项目 //AN_Xml: Mon, 22 May 2023 02:13:07 GMT //AN_Xml: 对于不准备面试的同学来说 ,本文档倾向于给你提供一个比较详细的学习路径,目录清晰,让你对于 Java 整体的知识体系有一个清晰认识。你可以跟着视频、书籍或者官方文档学习完某个知识点之后,然后来这里找对应的总结,帮助你更好地掌握对应的知识点。甚至说,你在有编程基础的情况下,想要学习某个知识点的话,可以直接看我的总结,这样学习效率会非常高。

//AN_Xml:

对于准备面试的同学来说 ,本文档涵盖 Java 程序员所需要掌握的核心知识的常见面试问题总结。

//AN_Xml:

大部分人看 JavaGuide 应该都是为了准备技术八股文。那如何才能更高效地准备技术八股文?

//AN_Xml:

对于技术八股文来说,尽量不要死记硬背,这种方式非常枯燥且对自身能力提升有限!但是!想要一点不背是不太现实的,只是说要结合实际应用场景和实战来理解记忆。

//AN_Xml:

我一直觉得面试八股文最好是和实际应用场景和实战相结合。很多同学现在的方向都错了,上来就是直接背八股文,硬生生学成了文科,那当然无趣了。

//AN_Xml:

举个例子:你的项目中需要用到 Redis 来做缓存,你对照着官网简单了解并实践了简单使用 Redis 之后,你去看了 Redis 对应的八股文。你发现 Redis 可以用来做限流、分布式锁,于是你去在项目中实践了一下并掌握了对应的八股文。紧接着,你又发现 Redis 内存不够用的情况下,还能使用 Redis Cluster 来解决,于是你就又去实践了一下并掌握了对应的八股文。

//AN_Xml:

而且, 面试中有水平的面试官都是根据你的项目经历来顺带着问一些技术八股文

//AN_Xml:

举个例子:你的项目用到了消息队列,那面试官可能就会问你:为什么使用消息队列?项目中什么模块用到了消息队列?如何保证消息不丢失?如何保证消息的顺序性?(结合你使用的具体的消息队列来准备)……。

//AN_Xml:

一定要记住你的主要目标是理解和记关键词,而不是像背课文一样一字一句地记下来!

//AN_Xml:

另外,记录博客或者用自己的理解把对应的知识点讲给别人听也是一个不错的选择。

//AN_Xml:

最后,准备技术面试的同学一定要定期复习(自测的方式非常好),不然确实会遗忘的。

//AN_Xml:]]>
//AN_Xml:
//AN_Xml: //AN_Xml: 十年大厂成长之路 //AN_Xml: https://javaguide.cn/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.html //AN_Xml: https://javaguide.cn/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.html //AN_Xml: 十年大厂成长之路 //AN_Xml: 十年大厂成长之路:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。 //AN_Xml: 技术文章精选集 //AN_Xml: Mon, 15 May 2023 09:49:43 GMT //AN_Xml: //AN_Xml:

推荐语:这篇文章的作者有着丰富的工作经验,曾在大厂工作了 12 年。结合自己走过的弯路和接触过的优秀技术人,他总结出了一些对于个人成长具有普遍指导意义的经验和特质。

//AN_Xml:

原文地址: https://mp.weixin.qq.com/s/vIIRxznpRr5yd6IVyNUW2w

//AN_Xml: //AN_Xml:

最近这段时间,有好几个年轻的同学和我聊到自己的迷茫。其中有关于技术成长的、有关于晋升的、有关于择业的。我很高兴他们愿意听我这个“过来人”分享自己的经验。

//AN_Xml:

我自己毕业后进入大厂,在大厂工作 12 年,我说的内容都来自于我自己或者身边人的真实情况。尤其,我会把 【我自己走过的弯路】【我看到过的优秀技术人的特质】 相结合来给出建议。

//AN_Xml:

这些内容我觉得具有普遍的指导意义,所以决定做个整理分享出来。我相信,无论你在大厂还是小厂,如果你相信这些建议,或早或晚他们会帮助到你。

//AN_Xml:

我自己工作 12 年,走了些弯路,所以我就来讲讲,“在一个技术人 10 年的发展过程中,应该注意些什么”。我们把内容分为两块:

//AN_Xml:
    //AN_Xml:
  1. 十年技术路怎么走
  2. //AN_Xml:
  3. 一些重要选择
  4. //AN_Xml:
//AN_Xml:

01 十年技术路怎么走

//AN_Xml:

【1-2 年】=> 从“菜鸟”到“职业”

//AN_Xml:

应届生刚进入到工作时,会有各种不适应。比如写好的代码会被反复打回、和团队老司机讨论技术问题会有一堆问号、不敢提问和质疑、碰到问题一个人使劲死磕等等。

//AN_Xml:

简单来说就是,即使日以继夜地埋头苦干,最后也无法顺利的开展工作。

//AN_Xml:

这个阶段最重要的几个点:

//AN_Xml:

【多看多模仿】:比如写代码的时候,不要就像在学校完成书本作业那样只关心功能是否正确,还要关心模块的设计、异常的处理、代码的可读性等等。在你还没有了解这些内容的精髓之前,也要照猫画虎地模仿起来,慢慢地你就会越来越明白真实世界的代码是怎么写的,以及为什么要这么写。

//AN_Xml:

做技术方案的时候也是同理,技术文档的要求你也许并不理解,但你可以先参考已有文档写起来。

//AN_Xml:

【脸皮厚一点】:不懂就问,你是新人大家都是理解的。你做的各种方案也可以多找老司机们 review,不要怕被看笑话。

//AN_Xml:

【关注工作方式】:比如发现需求在计划时间完不成就要尽快报风险、及时做好工作内容的汇报(例如周报)、开会后确定会议结论和 todo 项、承诺时间就要尽力完成、严格遵循公司的要求(例如发布规范、权限规范等)

//AN_Xml:

一般来说,工作 2 年后,你就应该成为一个职业人。老板可以相信任何工作交到你的手里,不会出现“意外”(例如一个重要需求明天要上线了,突然被告知上不了)。

//AN_Xml:

【3-4 年】=> 从“职业”到“尖兵”

//AN_Xml:

工作两年后,对业务以及现有系统的了解已经到达了一定的程度,技术同学会开始承担更有难度的技术挑战。

//AN_Xml:

例如需要将性能提升到某一个水位、例如需要对某一个重要模块进行重构、例如有个重要的项目需要协同 N 个团队一起完成。

//AN_Xml:

可见,上述的这些技术问题,难度都已经远远超过一个普通的需求。解决这些问题需要有一定的技术能力,同时也需要具备更高的协同能力。

//AN_Xml:

这个阶段最重要的几个点:

//AN_Xml:

【技术能力提升】:无论是公司内还是公司外的技术内容,都要多做主动的学习。基本上这个阶段的技术难题都集中在【性能】【稳定性】和【扩展性】上,而这些内容在业界都是有成型的方法论的。

//AN_Xml:

【主人翁精神】:技术难题除了技术方案设计及落地外,背后还有一系列的其他工作。例如上线后对效果的观测、重点项目对于上下游改造和风险的了解程度、对于整个技改后续的计划(二期、三期的优化思路)等。

//AN_Xml:

在工作四年后,基本上你成为了团队的一、二号技术位。很多技术难题即使不是你来落地,也是由你来决定方案。你会做调研、会做方案对比、会考虑整个技改的生命周期。

//AN_Xml:

【5-7 年】=> 从“尖兵”到“专家”

//AN_Xml:

技术尖兵重点在于解决某一个具体的技术难题或者重点项目。而下一步的发展方向,就是能够承担起来一整个“业务板块”,也就是“领域技术专家”。

//AN_Xml:

想要承担一整个“业务板块”需要 【对业务领域有深刻的理解,同时基于这些理解来规划技术的发展方向】

//AN_Xml:

拿支付做个例子。简单的支付功能其实很容易完成,只要处理好和双联(网联和银联)的接口调用(成功、失败、异常)即可。但在很多背景下,支付没有那么简单。

//AN_Xml:

例如,支付是一个用户敏感型操作,非常强调用户体验,如何能兼顾体验和接口的不稳定?支付接口还需要承担费用,同步和异步的接口费用不同,如何能够降本?支付接口往往还有限额等。这一系列问题的背后涉及到很多技术的设计,包括异步化、补偿设计、资金流设计、最终一致性设计等等。

//AN_Xml:

这个阶段最重要的几个点:

//AN_Xml:

【深入理解行业及趋势】:密切关注行业的各种变化(新鲜的玩法、政策的变动、竞对的策略、科技等外在因素的影响等等),和业务同学加强沟通。

//AN_Xml:

【深入了解行业解决方案】:充分对标已有的国内外技术方案,做深入学习和尝试,评估建设及运维成本,结合业务趋势制定计划。

//AN_Xml:

【8-10 年】=> 从“专家”到“TL”

//AN_Xml:

其实很多时候,如果能做到专家,基本也是一个 TL 的角色了,但这并不代表正在执行 TL 的职责。

//AN_Xml:

专家虽然已经可以做到“为业务发展而规划好技术发展”,但问题是要怎么落地呢?显然,靠一个人的力量是不可能完成建设的。所以,这里的 TL 更多强调的不是“领导”这个职位,而是 【通过聚合一个团队的力量来实施技术规划】

//AN_Xml:

所以,这里的 TL 需要具备【团队技术培养】【合理分配资源】【确认工作优先级】【激励与奖惩】等各种能力。

//AN_Xml:

这个阶段最重要的几个点:

//AN_Xml:

【学习管理学】:这里的管理学当然不是指 PUA,而是指如何在每个同学都有各自诉求的现实背景下,让个人目标和团队目标相结合,产生向前发展的动力。

//AN_Xml:

【始终扎根技术】:很多时候,工作重心偏向管理以后,就会荒废技术。但事实是,一个优秀的领导永远是一个优秀的技术人。参与一起讨论技术方案并给予指导、不断扩展自己的技术宽度、保持对技术的好奇心,这些是让一个技术领导持续拥有向心力的关键。

//AN_Xml:

02 一些重要选择

//AN_Xml:

下面来聊聊在十年间我们可能会碰到的一些重要选择。这些都是真实的血与泪的教训。

//AN_Xml:

我该不该转岗?

//AN_Xml:

大厂都有转岗的机制。转岗可以帮助员工寻找自己感兴趣的方向,也可以帮助新型团队招募有即战力的同学。

//AN_Xml:

转岗看似只是在公司内部变动,但你需要谨慎决定。

//AN_Xml:

本人转岗过多次。虽然还在同一家公司,但转岗等同于换工作。无论是领域沉淀、工作内容、信任关系、协作关系都是从零开始。

//AN_Xml:

针对转岗我的建议是:**如果你是想要拓宽自己的技术广度,也就是抱着提升技术能力的想法,我觉得可以转岗。但如果你想要晋升,不建议你转岗。**晋升需要在一个领域的持续积淀和在一个团队信任感的持续建立。

//AN_Xml:

当然,转岗可能还有其他原因,例如家庭原因、身体原因等,这个不展开讨论了。

//AN_Xml:

我该不该跳槽?

//AN_Xml:

跳槽和转岗一样,往往有很多因素造成,不能一概而论,我仅以几个场景来说:

//AN_Xml:

【晋升失败】:扪心自问,如果你觉得自己确实还不够格,那你就踏踏实实继续努力。如果你觉得评委有失偏颇,你可以尝试去外面面试一下,让市场来给你答案。

//AN_Xml:

【成长局限】:觉得自己做的事情没有挑战,无法成长。你可以和老板聊一下,有可能是因为你没有看到其中的挑战,也有可能老板没有意识到你的“野心”。

//AN_Xml:

【氛围不适】:一般来自于新入职或者领导更换,这种情况下不适是正常的。我的建议是,如果一个环境是“对事不对人”的,那就可以留下来,努力去适应,这种不适应只是做事方式不同导致的。但如果这个环境是“对人不对事”的话,走吧。

//AN_Xml:

跳槽该找怎样的工作?

//AN_Xml:

我们跳槽的时候往往会同时面试好几家公司。行情好的时候,往往可以收到多家 offer,那么我们要如何选择呢?

//AN_Xml:

考虑一个 offer 往往有这几点:【公司品牌】【薪资待遇】【职级职称】【技术背景】。每个同学其实都有自己的诉求,所以无论做什么选择都没有对错之分。

//AN_Xml:

我的一个建议是:你要关注新岗位的空间,这个空间是有希望满足你的期待的

//AN_Xml:

比如,你想成为架构师,那新岗位是否有足够的技术挑战来帮助你提升技术能力,而不仅仅是疲于奔命地应付需求?

//AN_Xml:

比如,你想往技术管理发展,那新岗位是否有带人的机会?是否有足够的问题需要搭建团队来解决?

//AN_Xml:

比如,你想扎根在某个领域持续发展(例如电商、游戏),那新岗位是不是延续这个领域,并且可以碰到更多这个领域的问题?

//AN_Xml:

当然,如果薪资实在高到无法拒绝,以上参考可以忽略!

//AN_Xml:

结语

//AN_Xml:

以上就是我对互联网从业技术人员十年成长之路的心得,希望在你困惑和关键选择的时候可以帮助到你。如果我的只言片语能够在未来的某个时间帮助到你哪怕一点,那将是我莫大的荣幸。

//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml:
//AN_Xml: //AN_Xml: 32条总结教你提升职场经验 //AN_Xml: https://javaguide.cn/high-quality-technical-articles/work/32-tips-improving-career.html //AN_Xml: https://javaguide.cn/high-quality-technical-articles/work/32-tips-improving-career.html //AN_Xml: 32条总结教你提升职场经验 //AN_Xml: 32条总结教你提升职场经验:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。 //AN_Xml: 技术文章精选集 //AN_Xml: Mon, 15 May 2023 09:49:43 GMT //AN_Xml: //AN_Xml:

推荐语:阿里开发者的一篇职场经验的分享。

//AN_Xml:

原文地址: https://mp.weixin.qq.com/s/6BkbGekSRTadm9j7XUL13g

//AN_Xml: //AN_Xml:

成长的捷径

//AN_Xml:
    //AN_Xml:
  • 入职伊始谦逊的态度是好的,但不要把“我是新人”作为心理安全线;
  • //AN_Xml:
  • 写一篇技术博客大概需要两周左右,但可能是最快的成长方式;
  • //AN_Xml:
  • 一定要读两本书:金字塔原理、高效能人士的七个习惯(这本书名字像成功学,实际讲的是如何塑造性格);
  • //AN_Xml:
  • 多问是什么、为什么,追本溯源把问题解决掉,试图绕过的问题永远会在下个路口等着你;
  • //AN_Xml:
  • 不要沉迷于忙碌带来的虚假安全感中,目标的确定和追逐才是最真实的安全;
  • //AN_Xml:
  • 不用过于计较一时的得失,在公平的环境中,吃亏是福不是鸡汤;
  • //AN_Xml:
  • 思维和技能不要受限于前端、后端、测试等角色,把自己定位成业务域问题的终结者;
  • //AN_Xml:
  • 好奇和热爱是成长最大的捷径,长期主义者会认同自己的工作价值,甚至要高于组织当下给的认同(KPI)。
  • //AN_Xml:
//AN_Xml:

功夫在日常

//AN_Xml:
    //AN_Xml:
  • 每行代码要代表自己当下的最高水平,你觉得无所谓的小细节,有可能就是在晋升场上伤害你的暗箭;
  • //AN_Xml:
  • 双周报不是工作日志流水账,不要被时间推着走,最起码要知道下次双周报里会有什么(小目标驱动);
  • //AN_Xml:
  • 觉得日常都是琐碎工作、不技术、给师兄打杂等,可以尝试对手头事情做一下分类,想象成每个分类都是个小格子,这些格子连起来的终点就是自己的目标,这样每天不再是机械的做需求,而是有规划的填格子、为目标努力,甚至会给自己加需求,因为自己看清楚了要去哪里;
  • //AN_Xml:
  • 日常的言行举止是能力的显微镜,大部分人可能意识不到,自己的强大和虚弱是那么的明显,不要无谓的试图掩盖,更不存在蒙混过关。
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

最后一条大概意思就是有时候我们会在意自己在聚光灯下(述职、晋升、周报、汇报等)的表现,以为大家会根据这个评价自己。实际上日常是怎么完成业务需求、帮助身边同学、创造价值的,才是大家评价自己的依据,而且每个人是什么样的特质,合作过三次的伙伴就可以精准评价,在聚光灯下的表演只能骗自己。

//AN_Xml:
//AN_Xml:

学会被管理

//AN_Xml:
//AN_Xml:

上级、主管是泛指,开发对口的 PD 主管等也在范围内。

//AN_Xml:
//AN_Xml:
    //AN_Xml:
  • //AN_Xml:

    不要传播负面情绪,不要总是抱怨;

    //AN_Xml:
  • //AN_Xml:
  • //AN_Xml:

    对上级不卑不亢更容易获得尊重,但不要当众反驳对方观点,分歧私下沟通;

    //AN_Xml:
  • //AN_Xml:
  • //AN_Xml:

    好好做向上管理,尤其是对齐预期,沟通绩效出现 Surprise 双方其实都有责任,但倒霉的是自己;

    //AN_Xml:
  • //AN_Xml:
  • //AN_Xml:

    尽量站在主管角度想问题:

    //AN_Xml:
  • //AN_Xml:
  • //AN_Xml:
      //AN_Xml:
    • 这样能理解很多过去感觉匪夷所思的决策;
    • //AN_Xml:
    • 不要在意谁执行、功劳是谁的等,为团队分忧赢得主管信任的重要性远远高于这些;
    • //AN_Xml:
    • 不要把这个原则理解为唯上,这种最让人不齿。
    • //AN_Xml:
    //AN_Xml:
  • //AN_Xml:
//AN_Xml:

思维转换

//AN_Xml:
    //AN_Xml:
  • 定义问题是个高阶能力,尽早形成 发现问题->定义问题->解决问题->消灭问题 的思维闭环;
  • //AN_Xml:
  • 定事情价值导向,做事情结果导向,讲事情问题导向;
  • //AN_Xml:
  • 讲不清楚,大概率不是因为自己是实干型,而是没想清楚,在晋升场更加明显;
  • //AN_Xml:
  • 当一个人擅长解决某一场景的问题的时候,时间越久也许越离不开这个场景(被人贴上一个标签很难,撕掉一个标签更难)。
  • //AN_Xml:
//AN_Xml:

要栓住情绪

//AN_Xml:
    //AN_Xml:
  • 学会控制情绪,没人会认真听一个愤怒的人在说什么;
  • //AN_Xml:
  • 再委屈、再愤怒也要保持理智,不要让自己成为需要被哄着的那种人;
  • //AN_Xml:
  • 足够自信的人才会坦率的承认自己的问题,很多时候我们被激怒了,只是因为对方指出了自己藏在深处的自卑;
  • //AN_Xml:
  • 伤害我们最深的既不是别人的所作所为,也不是自己犯的错误,而是我们对错误的回应。
  • //AN_Xml:
//AN_Xml:

成为 Leader

//AN_Xml:
//AN_Xml:

Manager 有下属,Leader 有追随者,管理者不需要很多,但人人都可以是 Leader。

//AN_Xml:
//AN_Xml:
    //AN_Xml:
  • 让你信服、愿意追随的人不是职务上的 Manager,而是在帮助自己的那个人,自己想服众的话道理一样;
  • //AN_Xml:
  • 不要轻易对人做负面评价,片面认知下的评价可能不准确,不经意的传播更是会给对方带来极大的困扰;
  • //AN_Xml:
  • Leader 如果不认同公司的使命、愿景、价值观,会过的特别痛苦;
  • //AN_Xml:
  • 困难时候不要否定自己的队友,多给及时、正向的反馈;
  • //AN_Xml:
  • 船长最重要的事情不是造船,而是激发水手对大海的向往;
  • //AN_Xml:
  • Leader 的天然职责是让团队活下去,唯一的途径是实现上级、老板、公司经营者的目标,越是艰难的时候越明显;
  • //AN_Xml:
  • Leader 的重要职责是识别团队需要被做的事情,并坚定信念,使众人行,越是艰难的时候越要坚定;
  • //AN_Xml:
  • Leader 应该让自己遇到的每个人都感觉自己很重要、被需要。
  • //AN_Xml:
//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml:
//AN_Xml: //AN_Xml: JVM线上问题排查和性能调优案例 //AN_Xml: https://javaguide.cn/java/jvm/jvm-in-action.html //AN_Xml: https://javaguide.cn/java/jvm/jvm-in-action.html //AN_Xml: JVM线上问题排查和性能调优案例 //AN_Xml: 汇集 JVM 在生产中的问题排查与优化案例,涵盖内存与 GC、工具使用等。 //AN_Xml: Java //AN_Xml: Wed, 10 May 2023 08:36:34 GMT //AN_Xml: JVM 线上问题排查和性能调优也是面试常问的一个问题,尤其是社招中大厂的面试。

//AN_Xml:

这篇文章,我会分享一些我看到的相关的案例。

//AN_Xml:

下面是正文。

//AN_Xml:

一次线上 OOM 问题分析 - 艾小仙 - 2023

//AN_Xml:
    //AN_Xml:
  • 现象:线上某个服务有接口非常慢,通过监控链路查看发现,中间的 GAP 时间非常大,实际接口并没有消耗很多时间,并且在那段时间里有很多这样的请求。
  • //AN_Xml:
  • 分析:使用 JDK 自带的jvisualvm分析 dump 文件(MAT 也能分析)。
  • //AN_Xml:
  • 建议:对于 SQL 语句,如果监测到没有where条件的全表查询应该默认增加一个合适的limit作为限制,防止这种问题拖垮整个系统
  • //AN_Xml:
  • 资料实战案例:记一次 dump 文件分析历程转载 - HeapDump - 2022
  • //AN_Xml:
//AN_Xml:

生产事故-记一次特殊的 OOM 排查 - 程语有云 - 2023

//AN_Xml:
    //AN_Xml:
  • 现象:网络没有问题的情况下,系统某开放接口从 2023 年 3 月 10 日 14 时许开始无法访问和使用。
  • //AN_Xml:
  • 临时解决办法:紧急回滚至上一稳定版本。
  • //AN_Xml:
  • 分析:使用 MAT (Memory Analyzer Tool)工具分析 dump 文件。
  • //AN_Xml:
  • 建议:正常情况下,-Xmn参数(控制 Young 区的大小)总是应当小于-Xmx参数(控制堆内存的最大大小),否则就会触发 OOM 错误。
  • //AN_Xml:
  • 资料最重要的 JVM 参数总结 - JavaGuide - 2023
  • //AN_Xml:
//AN_Xml:

一次大量 JVM Native 内存泄露的排查分析(64M 问题) - 掘金 - 2022

//AN_Xml: //AN_Xml:

YGC 问题排查,又让我涨姿势了! - IT 人的职场进阶 - 2021

//AN_Xml:
    //AN_Xml:
  • 现象:广告服务在新版本上线后,收到了大量的服务超时告警。
  • //AN_Xml:
  • 分析:使用 MAT (Memory Analyzer Tool) 工具分析 dump 文件。
  • //AN_Xml:
  • 建议:学会 YGC(Young GC) 问题的排查思路,掌握 YGC 的相关知识点。
  • //AN_Xml:
//AN_Xml:

听说 JVM 性能优化很难?今天我小试了一把! - 陈树义 - 2021

//AN_Xml:

通过观察 GC 频率和停顿时间,来进行 JVM 内存空间调整,使其达到最合理的状态。调整过程记得小步快跑,避免内存剧烈波动影响线上服务。 这其实是最为简单的一种 JVM 性能调优方式了,可以算是粗调吧。

//AN_Xml:

你们要的线上 GC 问题案例来啦 - 编了个程 - 2021

//AN_Xml:
    //AN_Xml:
  • 案例 1:使用 guava cache 的时候,没有设置最大缓存数量和弱引用,导致频繁触发 Young GC
  • //AN_Xml:
  • 案例 2: 对于一个查询和排序分页的 SQL,同时这个 SQL 需要 join 多张表,在分库分表下,直接调用 SQL 性能很差。于是,查单表,再在内存排序分页,用了一个 List 来保存数据,而有些数据量大,造成了这个现象。
  • //AN_Xml:
//AN_Xml:

Java 中 9 种常见的 CMS GC 问题分析与解决 - 美团技术团 - 2020

//AN_Xml:

这篇文章共 2w+ 字,详细介绍了 GC 基础,总结了 CMS GC 的一些常见问题分析与解决办法。

//AN_Xml:

给祖传系统做了点 GC 调优,暂停时间降低了 90% - 京东云技术团队 - 2023

//AN_Xml:

这篇文章提到了一个在规则引擎系统中遇到的 GC(垃圾回收)问题,主要表现为系统在启动后发生了一次较长的 Young GC(年轻代垃圾回收)导致性能下降。经过分析,问题的核心在于动态对象年龄判定机制,它导致了过早的对象晋升,引起了长时间的垃圾回收。

//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml:
//AN_Xml: //AN_Xml: 分布式ID设计实战指南 //AN_Xml: https://javaguide.cn/distributed-system/distributed-id-design.html //AN_Xml: https://javaguide.cn/distributed-system/distributed-id-design.html //AN_Xml: 分布式ID设计实战指南 //AN_Xml: 分布式ID设计实战指南,结合订单系统、一码付、优惠券等业务场景讲解分布式ID的设计要点、技术选型及不同场景下的ID生成策略。 //AN_Xml: 分布式 //AN_Xml: Fri, 05 May 2023 02:23:30 GMT //AN_Xml: //AN_Xml:

提示

//AN_Xml:

看到百度 Geek 说的一篇结合具体场景聊分布式 ID 设计的文章,感觉挺不错的。于是,我将这篇文章的部分内容整理到了这里。原文传送门:分布式 ID 生成服务的技术原理和项目实战

//AN_Xml: //AN_Xml:

网上绝大多数的分布式 ID 生成服务,一般着重于技术原理剖析,很少见到根据具体的业务场景去选型 ID 生成服务的文章。

//AN_Xml:

本文结合一些使用场景,进一步探讨业务场景中对 ID 有哪些具体的要求。

//AN_Xml:

场景一:订单系统

//AN_Xml:

我们在商场买东西一码付二维码,下单生成的订单号,使用到的优惠券码,联合商品兑换券码,这些是在网上购物经常使用到的单号,那么为什么有些单号那么长,有些只有几位数?有些单号一看就知道年月日的信息,有些却看不出任何意义?下面展开分析下订单系统中不同场景的 id 服务的具体实现。

//AN_Xml:

1、一码付

//AN_Xml:

我们常见的一码付,指的是一个二维码可以使用支付宝或者微信进行扫码支付。

//AN_Xml:

二维码的本质是一个字符串。聚合码的本质就是一个链接地址。用户使用支付宝微信直接扫一个码付钱,不用担心拿支付宝扫了微信的收款码或者用微信扫了支付宝的收款码,这极大减少了用户扫码支付的时间。

//AN_Xml:

实现原理是当客户用 APP 扫码后,网站后台就会判断客户的扫码环境。(微信、支付宝、QQ 钱包、京东支付、云闪付等)。

//AN_Xml:

判断扫码环境的原理就是根据打开链接浏览器的 HTTP header。任何浏览器打开 http 链接时,请求的 header 都会有 User-Agent(UA、用户代理)信息。

//AN_Xml:

UA 是一个特殊字符串头,服务器依次可以识别出客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等很多信息。

//AN_Xml:

各渠道对应支付产品的名称不一样,一定要仔细看各支付产品的 API 介绍。

//AN_Xml:
    //AN_Xml:
  1. 微信支付:JSAPI 支付支付
  2. //AN_Xml:
  3. 支付宝:手机网站支付
  4. //AN_Xml:
  5. QQ 钱包:公众号支付
  6. //AN_Xml:
//AN_Xml:

其本质均为在 APP 内置浏览器中实现 HTML5 支付。

//AN_Xml:

文库会员支付示例

//AN_Xml:

文库的研发同学在这个思路上,做了优化迭代。动态生成一码付的二维码预先绑定用户所选的商品信息和价格,根据用户所选的商品动态更新。这样不仅支持一码多平台调起支付,而且不用用户选择商品输入金额,即可完成订单支付的功能,很丝滑。用户在真正扫码后,服务端才通过前端获取用户 UID,结合二维码绑定的商品信息,真正的生成订单,发送支付信息到第三方(qq、微信、支付宝),第三方生成支付订单推给用户设备,从而调起支付。

//AN_Xml:

区别于固定的一码付,在文库的应用中,使用到了动态二维码,二维码本质是一个短网址,ID 服务提供短网址的唯一标志参数。唯一的短网址映射的 ID 绑定了商品的订单信息,技术和业务的深度结合,缩短了支付流程,提升用户的支付体验。

//AN_Xml:

2、订单号

//AN_Xml:

订单号在实际的业务过程中作为一个订单的唯一标识码存在,一般实现以下业务场景:

//AN_Xml:
    //AN_Xml:
  1. 用户订单遇到问题,需要找客服进行协助;
  2. //AN_Xml:
  3. 对订单进行操作,如线下收款,订单核销;
  4. //AN_Xml:
  5. 下单,改单,成单,退单,售后等系统内部的订单流程处理和跟进。
  6. //AN_Xml:
//AN_Xml:

很多时候搜索订单相关信息的时候都是以订单 ID 作为唯一标识符,这是由于订单号的生成规则的唯一性决定的。从技术角度看,除了 ID 服务必要的特性之外,在订单号的设计上需要体现几个特性:

//AN_Xml:

(1)信息安全

//AN_Xml:

编号不能透露公司的运营情况,比如日销、公司流水号等信息,以及商业信息和用户手机号,身份证等隐私信息。并且不能有明显的整体规律(可以有局部规律),任意修改一个字符就能查询到另一个订单信息,这也是不允许的。

//AN_Xml:

类比于我们高考时候的考生编号的生成规则,一定不能是连号的,否则只需要根据顺序往下查询就能搜索到别的考生的成绩,这是绝对不可允许。

//AN_Xml:

(2)部分可读

//AN_Xml:

位数要便于操作,因此要求订单号的位数适中,且局部有规律。这样可以方便在订单异常,或者退货时客服查询。

//AN_Xml:

过长的订单号或易读性差的订单号会导致客服输入困难且易错率较高,影响用户体验的售后体验。因此在实际的业务场景中,订单号的设计通常都会适当携带一些允许公开的对使用场景有帮助的信息,如时间,星期,类型等等,这个主要根据所涉及的编号对应的使用场景来。

//AN_Xml:

而且像时间、星期这些自增长的属于作为订单号的设计的一部分元素,有助于解决业务累积而导致的订单号重复的问题。

//AN_Xml:

(3)查询效率

//AN_Xml:

常见的电商平台订单号大多是纯数字组成,兼具可读性的同时,int 类型相对 varchar 类型的查询效率更高,对在线业务更加友好。

//AN_Xml:

3、优惠券和兑换券

//AN_Xml:

优惠券、兑换券是运营推广最常用的促销工具之一,合理使用它们,可以让买家得到实惠,商家提升商品销量。常见场景有:

//AN_Xml:
    //AN_Xml:
  1. 在文库购买【文库 VIP+QQ 音乐年卡】联合商品,支付成功后会得到 QQ 音乐年卡的兑换码,可以去 QQ 音乐 App 兑换音乐会员年卡;
  2. //AN_Xml:
  3. 疫情期间,部分地方政府发放的消费券;
  4. //AN_Xml:
  5. 瓶装饮料经常会出现输入优惠编码兑换奖品。
  6. //AN_Xml:
//AN_Xml:

优惠编码兑换奖品

//AN_Xml:

从技术角度看,有些场景适合 ID 即时生成,比如电商平台购物领取的优惠券,只需要在用户领取时分配优惠券信息即可。有些线上线下结合的场景,比如疫情优惠券,瓶盖开奖,京东卡,超市卡这种,则需要预先生成,预先生成的券码具备以下特性:

//AN_Xml:

1.预先生成,在活动正式开始前提供出来进行活动预热;

//AN_Xml:

2.优惠券体量大,以万为单位,通常在 10 万级别以上;

//AN_Xml:

3.不可破解、仿制券码;

//AN_Xml:

4.支持用后核销;

//AN_Xml:

5.优惠券、兑换券属于广撒网的策略,所以利用率低,也就不适合使用数据库进行存储 (占空间,有效的数据又少)

//AN_Xml:

设计思路上,需要设计一种有效的兑换码生成策略,支持预先生成,支持校验,内容简洁,生成的兑换码都具有唯一性,那么这种策略就是一种特殊的编解码策略,按照约定的编解码规则支撑上述需求。

//AN_Xml:

既然是一种编解码规则,那么需要约定编码空间(也就是用户看到的组成兑换码的字符),编码空间由字符 a-z,A-Z,数字 0-9 组成,为了增强兑换码的可识别度,剔除大写字母 O 以及 I,可用字符如下所示,共 60 个字符:

//AN_Xml:

abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXZY0123456789

//AN_Xml:

之前说过,兑换码要求尽可能简洁,那么设计时就需要考虑兑换码的字符数,假设上限为 12 位,而字符空间有 60 位,那么可以表示的空间范围为 60^12=130606940160000000000000(也就是可以 12 位的兑换码可以生成天量,应该够运营同学挥霍了),转换成 2 进制:

//AN_Xml:

1001000100000000101110011001101101110011000000000000000000000(61 位)

//AN_Xml:

兑换码组成成分分析

//AN_Xml:

兑换码可以预先生成,并且不需要额外的存储空间保存这些信息,每一个优惠方案都有独立的一组兑换码(指运营同学组织的每一场运营活动都有不同的兑换码,不能混合使用, 例如双 11 兑换码不能使用在双 12 活动上),每个兑换码有自己的编号,防止重复,为了保证兑换码的有效性,对兑换码的数据需要进行校验,当前兑换码的数据组成如下所示:

//AN_Xml:

优惠方案 ID + 兑换码序列号 i + 校验码

//AN_Xml:

编码方案

//AN_Xml:
    //AN_Xml:
  1. 兑换码序列号 i,代表当前兑换码是当前活动中第 i 个兑换码,兑换码序列号的空间范围决定了优惠活动可以发行的兑换码数目,当前采用 30 位 bit 位表示,可表示范围:1073741824(10 亿个券码)。
  2. //AN_Xml:
  3. 优惠方案 ID, 代表当前优惠方案的 ID 号,优惠方案的空间范围决定了可以组织的优惠活动次数,当前采用 15 位表示,可以表示范围:32768(考虑到运营活动的频率,以及 ID 的初始值 10000,15 位足够,365 天每天有运营活动,可以使用 54 年)。
  4. //AN_Xml:
  5. 校验码,校验兑换码是否有效,主要为了快捷的校验兑换码信息的是否正确,其次可以起到填充数据的目的,增强数据的散列性,使用 13 位表示校验位,其中分为两部分,前 6 位和后 7 位。
  6. //AN_Xml:
//AN_Xml:

深耕业务还会有区分通用券和单独券的情况,分别具备以下特点,技术实现需要因地制宜地思考。

//AN_Xml:
    //AN_Xml:
  1. 通用券:多个玩家都可以输入兑换,然后有总量限制,期限限制。
  2. //AN_Xml:
  3. 单独券:运营同学可以在后台设置兑换码的奖励物品、期限、个数,然后由后台生成兑换码的列表,兑换之后核销。
  4. //AN_Xml:
//AN_Xml:

场景二:Tracing

//AN_Xml:

1、日志跟踪

//AN_Xml:

在分布式服务架构下,一个 Web 请求从网关流入,有可能会调用多个服务对请求进行处理,拿到最终结果。这个过程中每个服务之间的通信又是单独的网络请求,无论请求经过的哪个服务出了故障或者处理过慢都会对前端造成影响。

//AN_Xml:

处理一个 Web 请求要调用的多个服务,为了能更方便的查询哪个环节的服务出现了问题,现在常用的解决方案是为整个系统引入分布式链路跟踪。

//AN_Xml:

在分布式链路跟踪

//AN_Xml:

在分布式链路跟踪中有两个重要的概念:跟踪(trace)和 跨度( span)。trace 是请求在分布式系统中的整个链路视图,span 则代表整个链路中不同服务内部的视图,span 组合在一起就是整个 trace 的视图。

//AN_Xml:

在整个请求的调用链中,请求会一直携带 traceid 往下游服务传递,每个服务内部也会生成自己的 spanid 用于生成自己的内部调用视图,并和 traceid 一起传递给下游服务。

//AN_Xml:

2、TraceId 生成规则

//AN_Xml:

这种场景下,生成的 ID 除了要求唯一之外,还要求生成的效率高、吞吐量大。traceid 需要具备接入层的服务器实例自主生成的能力,如果每个 trace 中的 ID 都需要请求公共的 ID 服务生成,纯纯的浪费网络带宽资源。且会阻塞用户请求向下游传递,响应耗时上升,增加了没必要的风险。所以需要服务器实例最好可以自行计算 tracid,spanid,避免依赖外部服务。

//AN_Xml:

产生规则:服务器 IP + ID 产生的时间 + 自增序列 + 当前进程号 ,比如:

//AN_Xml:

0ad1348f1403169275002100356696

//AN_Xml:

前 8 位 0ad1348f 即产生 TraceId 的机器的 IP,这是一个十六进制的数字,每两位代表 IP 中的一段,我们把这个数字,按每两位转成 10 进制即可得到常见的 IP 地址表示方式 10.209.52.143,您也可以根据这个规律来查找到请求经过的第一个服务器。

//AN_Xml:

后面的 13 位 1403169275002 是产生 TraceId 的时间。之后的 4 位 1003 是一个自增的序列,从 1000 涨到 9000,到达 9000 后回到 1000 再开始往上涨。最后的 5 位 56696 是当前的进程 ID,为了防止单机多进程出现 TraceId 冲突的情况,所以在 TraceId 末尾添加了当前的进程 ID。

//AN_Xml:

3、SpanId 生成规则

//AN_Xml:

span 是层的意思,比如在第一个实例算是第一层, 请求代理或者分流到下一个实例处理,就是第二层,以此类推。通过层,SpanId 代表本次调用在整个调用链路树中的位置。

//AN_Xml:

假设一个 服务器实例 A 接收了一次用户请求,代表是整个调用的根节点,那么 A 层处理这次请求产生的非服务调用日志记录 spanid 的值都是 0,A 层需要通过 RPC 依次调用 B、C、D 三个服务器实例,那么在 A 的日志中,SpanId 分别是 0.1,0.2 和 0.3,在 B、C、D 中,SpanId 也分别是 0.1,0.2 和 0.3;如果 C 系统在处理请求的时候又调用了 E,F 两个服务器实例,那么 C 系统中对应的 spanid 是 0.2.1 和 0.2.2,E、F 两个系统对应的日志也是 0.2.1 和 0.2.2。

//AN_Xml:

根据上面的描述可以知道,如果把一次调用中所有的 SpanId 收集起来,可以组成一棵完整的链路树。

//AN_Xml:

spanid 的生成本质:在跨层传递透传的同时,控制大小版本号的自增来实现的。

//AN_Xml:

场景三:短网址

//AN_Xml:

短网址主要功能包括网址缩短与还原两大功能。相对于长网址,短网址可以更方便地在电子邮件,社交网络,微博和手机上传播,例如原来很长的网址通过短网址服务即可生成相应的短网址,避免折行或超出字符限制。

//AN_Xml:

短网址作用

//AN_Xml:

常用的 ID 生成服务比如:MySQL ID 自增、 Redis 键自增、号段模式,生成的 ID 都是一串数字。短网址服务把客户的长网址转换成短网址,

//AN_Xml:

实际是在 dwz.cn 域名后面拼接新产生的数字类型 ID,直接用数字 ID,网址长度也有些长,服务可以通过数字 ID 转更高进制的方式压缩长度。这种算法在短网址的技术实现上越来越多了起来,它可以进一步压缩网址长度。转进制的压缩算法在生活中有广泛的应用场景,举例:

//AN_Xml: //AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Spring Cloud Gateway面试题总结 //AN_Xml: https://javaguide.cn/distributed-system/spring-cloud-gateway-questions.html //AN_Xml: https://javaguide.cn/distributed-system/spring-cloud-gateway-questions.html //AN_Xml: Spring Cloud Gateway面试题总结 //AN_Xml: Spring Cloud Gateway核心原理详解,包括路由配置、Predicate断言、Filter过滤器机制、限流熔断、工作流程等常见面试题与实践要点。 //AN_Xml: 分布式 //AN_Xml: Thu, 04 May 2023 10:53:24 GMT //AN_Xml: //AN_Xml:

本文重构完善自6000 字 | 16 图 | 深入理解 Spring Cloud Gateway 的原理 - 悟空聊架构这篇文章。

//AN_Xml: //AN_Xml:

什么是 Spring Cloud Gateway?

//AN_Xml:

Spring Cloud Gateway 属于 Spring Cloud 生态系统中的网关,其诞生的目标主要是为了替代 Zuul 1.x。Zuul 1.x 基于 Servlet 阻塞 I/O 架构,在高并发场景下性能有限。而 Zuul 2.x 虽然采用了 Netty 非阻塞架构,但 Spring Cloud 官方并未正式集成 Zuul 2.x。Spring Cloud Gateway 起步要比 Zuul 2.x 更早。

//AN_Xml:

为了提升网关的性能,Spring Cloud Gateway 基于 Spring WebFlux 。Spring WebFlux 使用 Reactor 库来实现响应式编程模型,底层基于 Netty 实现同步非阻塞的 I/O。

//AN_Xml:

//AN_Xml:

Spring Cloud Gateway 不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,限流。

//AN_Xml:

Spring Cloud Gateway 和 Zuul 2.x 的差别不大,也是通过过滤器来处理请求。不过,目前更加推荐使用 Spring Cloud Gateway 而非 Zuul,Spring Cloud 生态对其支持更加友好。

//AN_Xml: //AN_Xml:

Spring Cloud Gateway 的工作流程?

//AN_Xml:

Spring Cloud Gateway 的工作流程如下图所示:

//AN_Xml:

Spring Cloud Gateway 的工作流程

//AN_Xml:

这是 Spring 官方博客中的一张图,原文地址:https://spring.io/blog/2022/08/26/creating-a-custom-spring-cloud-gateway-filter

//AN_Xml:

具体的流程分析:

//AN_Xml:
    //AN_Xml:
  1. 路由判断:客户端的请求到达网关后,先经过 Gateway Handler Mapping 处理,这里面会做断言(Predicate)判断,看下符合哪个路由规则,这个路由映射后端的某个服务。
  2. //AN_Xml:
  3. 请求过滤:然后请求到达 Gateway Web Handler,这里面有很多过滤器,组成过滤器链(Filter Chain),这些过滤器可以对请求进行拦截和修改,比如添加请求头、参数校验等等,有点像净化污水。然后将请求转发到实际的后端服务。这些过滤器逻辑上可以称作 Pre-Filters,Pre 可以理解为“在...之前”。
  4. //AN_Xml:
  5. 服务处理:后端服务会对请求进行处理。
  6. //AN_Xml:
  7. 响应过滤:后端处理完结果后,返回给 Gateway 的过滤器再次做处理,逻辑上可以称作 Post-Filters,Post 可以理解为“在...之后”。
  8. //AN_Xml:
  9. 响应返回:响应经过过滤处理后,返回给客户端。
  10. //AN_Xml:
//AN_Xml:

总结:客户端的请求先通过匹配规则找到合适的路由,就能映射到具体的服务。然后请求经过过滤器处理后转发给具体的服务,服务处理后,再次经过过滤器处理,最后返回给客户端。

//AN_Xml:

Spring Cloud Gateway 的断言是什么?

//AN_Xml:

断言(Predicate)这个词听起来极其深奥,它是一种编程术语,我们生活中根本就不会用它。说白了它就是对一个表达式进行 if 判断,结果为真或假,如果为真则做这件事,否则做那件事。

//AN_Xml:

在 Gateway 中,如果客户端发送的请求满足了断言的条件,则映射到指定的路由器,就能转发到指定的服务上进行处理。

//AN_Xml:

断言配置的示例如下,配置了两个路由规则,有一个 predicates 断言配置,当请求 url 中包含 api/thirdparty,就匹配到了第一个路由 route_thirdparty

//AN_Xml:

断言配置示例

//AN_Xml:

常见的路由断言规则如下图所示:

//AN_Xml:

Spring Cloud GateWay 路由断言规则

//AN_Xml:

Spring Cloud Gateway 的路由和断言是什么关系?

//AN_Xml:

Route 路由和 Predicate 断言的对应关系如下::

//AN_Xml:

路由和断言的对应关系

//AN_Xml:
    //AN_Xml:
  • 一对多:一个路由规则可以包含多个断言。如上图中路由 Route1 配置了三个断言 Predicate。
  • //AN_Xml:
  • 同时满足:如果一个路由规则中有多个断言,则需要同时满足才能匹配。如上图中路由 Route2 配置了两个断言,客户端发送的请求必须同时满足这两个断言,才能匹配路由 Route2。
  • //AN_Xml:
  • 第一个匹配成功:如果一个请求可以匹配多个路由,则映射第一个匹配成功的路由。如上图所示,客户端发送的请求满足 Route3 和 Route4 的断言,但是 Route3 的配置在配置文件中靠前,所以只会匹配 Route3。
  • //AN_Xml:
//AN_Xml:

Spring Cloud Gateway 如何实现动态路由?

//AN_Xml:

在使用 Spring Cloud Gateway 的时候,官方文档提供的方案总是基于配置文件或代码配置的方式。

//AN_Xml:

Spring Cloud Gateway 作为微服务的入口,需要尽量避免重启,而现在配置更改需要重启服务不能满足实际生产过程中的动态刷新、实时变更的业务需求,所以我们需要在 Spring Cloud Gateway 运行时动态配置网关。

//AN_Xml:

实现动态路由的方式有很多种,其中一种推荐的方式是基于 Nacos 注册中心来做。 Spring Cloud Gateway 可以从注册中心获取服务的元数据(例如服务名称、路径等),然后根据这些信息自动生成路由规则。这样,当你添加、移除或更新服务实例时,网关会自动感知并相应地调整路由规则,无需手动维护路由配置。

//AN_Xml:

其实这些复杂的步骤并不需要我们手动实现,通过 Nacos Server 和 Spring Cloud Alibaba Nacos Config 即可实现配置的动态变更,官方文档地址:https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-config

//AN_Xml:

Spring Cloud Gateway 的过滤器有哪些?

//AN_Xml:

过滤器 Filter 按照请求和响应可以分为两种:

//AN_Xml:
    //AN_Xml:
  • Pre 类型:在请求被转发到微服务之前,对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等操作。
  • //AN_Xml:
  • Post 类型:微服务处理完请求后,返回响应给网关,网关可以再次进行处理,例如修改响应内容或响应头、日志输出、流量监控等。
  • //AN_Xml:
//AN_Xml:

另外一种分类是按照过滤器 Filter 作用的范围进行划分:

//AN_Xml:
    //AN_Xml:
  • GatewayFilter:局部过滤器,应用在单个路由或一组路由上的过滤器。标红色表示比较常用的过滤器。
  • //AN_Xml:
  • GlobalFilter:全局过滤器,应用在所有路由上的过滤器。
  • //AN_Xml:
//AN_Xml:

局部过滤器

//AN_Xml:

常见的局部过滤器如下图所示:

//AN_Xml:

//AN_Xml:

具体怎么用呢?这里有个示例,如果 URL 匹配成功,则去掉 URL 中的 “api”。

//AN_Xml:
filters: #过滤器
//AN_Xml:  - RewritePath=/api/(?<segment>.*),/$\{segment} # 将跳转路径中包含的 “api” 替换成空
//AN_Xml:

当然我们也可以自定义过滤器,本篇不做展开。

//AN_Xml:

全局过滤器

//AN_Xml:

常见的全局过滤器如下图所示:

//AN_Xml:

//AN_Xml:

全局过滤器最常见的用法是进行负载均衡。配置如下所示:

//AN_Xml:
spring:
//AN_Xml:  cloud:
//AN_Xml:    gateway:
//AN_Xml:      routes:
//AN_Xml:        - id: route_member # 第三方微服务路由规则
//AN_Xml:          uri: lb://passjava-member # 负载均衡,将请求转发到注册中心注册的 passjava-member 服务
//AN_Xml:          predicates: # 断言
//AN_Xml:            - Path=/api/member/** # 如果前端请求路径包含 api/member,则应用这条路由规则
//AN_Xml:          filters: #过滤器
//AN_Xml:            - RewritePath=/api/(?<segment>.*),/$\{segment} # 将跳转路径中包含的api替换成空
//AN_Xml:

这里有个关键字 lb,用到了全局过滤器 LoadBalancerClientFilter,当匹配到这个路由后,会将请求转发到 passjava-member 服务,且支持负载均衡转发,也就是先将 passjava-member 解析成实际的微服务的 host 和 port,然后再转发给实际的微服务。

//AN_Xml:

Spring Cloud Gateway 支持限流吗?

//AN_Xml:

Spring Cloud Gateway 自带了限流过滤器,对应的接口是 RateLimiterRateLimiter 接口只有一个实现类 RedisRateLimiter (基于 Redis + Lua 实现的限流),提供的限流功能比较简易且不易使用。

//AN_Xml:

从 Sentinel 1.6.0 版本开始,Sentinel 引入了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:route 维度和自定义 API 维度。也就是说,Spring Cloud Gateway 可以结合 Sentinel 实现更强大的网关流量控制。

//AN_Xml:

Spring Cloud Gateway 如何自定义全局异常处理?

//AN_Xml:

在 SpringBoot 项目中,我们捕获全局异常只需要在项目中配置 @RestControllerAdvice@ExceptionHandler就可以了。不过,这种方式在 Spring Cloud Gateway 下不适用。

//AN_Xml:

Spring Cloud Gateway 提供了多种全局处理的方式,比较常用的一种是实现ErrorWebExceptionHandler并重写其中的handle方法。

//AN_Xml:
@Order(-1)
//AN_Xml:@Component
//AN_Xml:@RequiredArgsConstructor
//AN_Xml:public class GlobalErrorWebExceptionHandler implements ErrorWebExceptionHandler {
//AN_Xml:    private final ObjectMapper objectMapper;
//AN_Xml:
//AN_Xml:    @Override
//AN_Xml:    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
//AN_Xml:    // ...
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

参考

//AN_Xml: //AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Disruptor常见问题总结 //AN_Xml: https://javaguide.cn/high-performance/message-queue/disruptor-questions.html //AN_Xml: https://javaguide.cn/high-performance/message-queue/disruptor-questions.html //AN_Xml: Disruptor常见问题总结 //AN_Xml: 本文总结 Disruptor 高性能内存队列的核心知识与面试要点,涵盖 Disruptor 架构(RingBuffer/Sequencer/WaitStrategy)、高性能原理(无锁设计/缓存行填充/预分配内存)、与 ArrayBlockingQueue 对比、生产者消费者模式等,助力 Disruptor 学习与面试。 //AN_Xml: 高性能 //AN_Xml: Thu, 04 May 2023 10:53:24 GMT //AN_Xml: Disruptor 是一个相对冷门一些的知识点,不过,如果你的项目经历中用到了 Disruptor 的话,那面试中就很可能会被问到。

//AN_Xml:

一位球友之前投稿的面经(社招)中就涉及一些 Disruptor 的问题,文章传送门:圆梦!顺利拿到字节、淘宝、拼多多等大厂 offer!

//AN_Xml:

//AN_Xml:

这篇文章可以看作是对 Disruptor 做的一个简单总结,每个问题都不会扯太深入,主要针对面试或者速览 Disruptor。

//AN_Xml:

Disruptor 是什么?

//AN_Xml:

Disruptor 是一个开源的高性能内存队列,诞生初衷是为了解决内存队列的性能和内存安全问题,由英国外汇交易公司 LMAX 开发。

//AN_Xml:

根据 Disruptor 官方介绍,基于 Disruptor 开发的系统 LMAX(新的零售金融交易平台),单线程就能支撑每秒 600 万订单。Martin Fowler 在 2011 年写的一篇文章 The LMAX Architecture 中专门介绍过这个 LMAX 系统的架构,感兴趣的可以看看这篇文章。。

//AN_Xml:

LMAX 公司 2010 年在 QCon 演讲后,Disruptor 获得了业界关注,并获得了 2011 年的 Oracle 官方的 Duke's Choice Awards(Duke 选择大奖)。

//AN_Xml:

//AN_Xml:
//AN_Xml:

“Duke 选择大奖”旨在表彰过去一年里全球个人或公司开发的、最具影响力的 Java 技术应用,由甲骨文公司主办。含金量非常高!

//AN_Xml:
//AN_Xml:

我专门找到了 Oracle 官方当年颁布获得 Duke's Choice Awards 项目的那篇文章(文章地址:https://blogs.oracle.com/java/post/and-the-winners-arethe-dukes-choice-award) 。从文中可以看出,同年获得此大奖荣誉的还有大名鼎鼎的 Netty、JRebel 等项目。

//AN_Xml:

2011 年的 Oracle 官方的 Duke's Choice Awards

//AN_Xml:

Disruptor 提供的功能优点类似于 Kafka、RocketMQ 这类分布式队列,不过,其作为范围是 JVM(内存)。

//AN_Xml: //AN_Xml:

关于如何在 Spring Boot 项目中使用 Disruptor,可以看这篇文章:Spring Boot + Disruptor 实战入门

//AN_Xml:

为什么要用 Disruptor?

//AN_Xml:

Disruptor 主要解决了 JDK 内置线程安全队列的性能和内存安全问题。

//AN_Xml:

JDK 中常见的线程安全的队列如下

//AN_Xml:

| 队列名字 | 锁 | 是否有界 |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: NAT 协议详解(网络层) //AN_Xml: https://javaguide.cn/cs-basics/network/nat.html //AN_Xml: https://javaguide.cn/cs-basics/network/nat.html //AN_Xml: NAT 协议详解(网络层) //AN_Xml: 解析 NAT 的地址转换与端口映射机制,结合 LAN/WAN 通信与转换表,理解家庭与企业网络的实践细节。 //AN_Xml: 计算机基础 //AN_Xml: Sun, 30 Apr 2023 08:44:12 GMT //AN_Xml: 应用场景 //AN_Xml:

NAT 协议(Network Address Translation) 的应用场景如同它的名称——网络地址转换,应用于内部网到外部网的地址转换过程中。具体地说,在一个小的子网(局域网,Local Area Network,LAN)内,各主机使用的是同一个 LAN 下的 IP 地址,但在该 LAN 以外,在广域网(Wide Area Network,WAN)中,需要一个统一的 IP 地址来标识该 LAN 在整个 Internet 上的位置。

//AN_Xml:

这个场景其实不难理解。随着一个个小型办公室、家庭办公室(Small Office, Home Office, SOHO)的出现,为了管理这些 SOHO,一个个子网被设计出来,从而在整个 Internet 中的主机数量将非常庞大。如果每个主机都有一个“绝对唯一”的 IP 地址,那么 IPv4 地址的表达能力可能很快达到上限($2^{32}$)。因此,实际上,SOHO 子网中的 IP 地址是“相对的”,这在一定程度上也缓解了 IPv4 地址的分配压力。

//AN_Xml:

SOHO 子网的“代理人”,也就是和外界的窗口,通常由路由器扮演。路由器的 LAN 一侧管理着一个小子网,而它的 WAN 接口才是真正参与到 Internet 中的接口,也就有一个“绝对唯一的地址”。NAT 协议,正是在 LAN 中的主机在与 LAN 外界通信时,起到了地址转换的关键作用。

//AN_Xml:

细节

//AN_Xml:

NAT 协议

//AN_Xml:

假设当前场景如上图。中间是一个路由器,它的右侧组织了一个 LAN,网络号为10.0.0/24。LAN 侧接口的 IP 地址为10.0.0.4,并且该子网内有至少三台主机,分别是10.0.0.110.0.0.210.0.0.3。路由器的左侧连接的是 WAN,WAN 侧接口的 IP 地址为138.76.29.7

//AN_Xml:

首先,针对以上信息,我们有如下事实需要说明:

//AN_Xml:
    //AN_Xml:
  1. 路由器右侧子网的网络地址为 10.0.0.0/24(网络前缀 24 位,主机号占 8 位),三台主机地址以及路由器的 LAN 侧接口地址,均由 DHCP 协议规定。而且,该 DHCP 运行在路由器内部(路由器自维护一个小 DHCP 服务器),从而为子网内提供 DHCP 服务。
  2. //AN_Xml:
  3. 路由器的 WAN 侧接口地址同样由 DHCP 协议规定,但该地址是路由器从 ISP(网络服务提供商)处获得,也就是该 DHCP 通常运行在路由器所在区域的 DHCP 服务器上。
  4. //AN_Xml:
//AN_Xml:

现在,路由器内部还运行着 NAT 协议,从而为 LAN-WAN 间通信提供地址转换服务。为此,一个很重要的结构是 NAT 转换表。为了说明 NAT 的运行细节,假设有以下请求发生:

//AN_Xml:
    //AN_Xml:
  1. 主机10.0.0.1向 IP 地址为128.119.40.186的 Web 服务器(端口 80)发送了 HTTP 请求(如请求页面)。此时,主机10.0.0.1将随机指派一个端口,如3345,作为本次请求的源端口号,将该请求发送到路由器中(目的地址将是128.119.40.186,但会先到达10.0.0.4)。
  2. //AN_Xml:
  3. 10.0.0.4即路由器的 LAN 接口收到10.0.0.1的请求。路由器将为该请求指派一个新的源端口号,如5001,并将请求报文发送给 WAN 接口138.76.29.7。同时,在 NAT 转换表中记录一条转换记录138.76.29.7:5001——10.0.0.1:3345
  4. //AN_Xml:
  5. 请求报文到达 WAN 接口,继续向目的主机128.119.40.186发送。
  6. //AN_Xml:
//AN_Xml:

之后,将会有如下响应发生:

//AN_Xml:
    //AN_Xml:
  1. 主机128.119.40.186收到请求,构造响应报文,并将其发送给目的地138.76.29.7:5001
  2. //AN_Xml:
  3. 响应报文到达路由器的 WAN 接口。路由器查询 NAT 转换表,发现138.76.29.7:5001在转换表中有记录,从而将其目的地址和目的端口转换成为10.0.0.1:3345,再发送到10.0.0.4上。
  4. //AN_Xml:
  5. 被转换的响应报文到达路由器的 LAN 接口,继而被转发至目的地10.0.0.1
  6. //AN_Xml:
//AN_Xml:

LAN-WAN 间通信提供地址转换

//AN_Xml:

🐛 修正(参见:issue#2009):上图第四步的 Dest 值应该为 10.0.0.1:3345 而不是~~138.76.29.7:5001~~,这里笔误了。

//AN_Xml:

划重点

//AN_Xml:

针对以上过程,有以下几个重点需要强调:

//AN_Xml:
    //AN_Xml:
  1. 当请求报文到达路由器,并被指定了新端口号时,由于端口号有 16 位,因此,通常来说,一个路由器管理的 LAN 中的最大主机数 $≈65500$($2^{16}$ 的地址空间),但通常 SOHO 子网内不会有如此多的主机数量。
  2. //AN_Xml:
  3. 对于目的服务器来说,从来不知道“到底是哪个主机给我发送的请求”,它只知道是来自138.76.29.7:5001的路由器转发的请求。因此,可以说,路由器在 WAN 和 LAN 之间起到了屏蔽作用,所有内部主机发送到外部的报文,都具有同一个 IP 地址(不同的端口号),所有外部发送到内部的报文,也都只有一个目的地(不同端口号),是经过了 NAT 转换后,外部报文才得以正确地送达内部主机。
  4. //AN_Xml:
  5. 在报文穿过路由器,发生 NAT 转换时,如果 LAN 主机 IP 已经在 NAT 转换表中注册过了,则不需要路由器新指派端口,而是直接按照转换记录穿过路由器。同理,外部报文发送至内部时也如此。
  6. //AN_Xml:
//AN_Xml:

总结 NAT 协议的特点,有以下几点:

//AN_Xml:
    //AN_Xml:
  1. NAT 协议通过对 WAN 屏蔽 LAN,有效地缓解了 IPv4 地址分配压力。
  2. //AN_Xml:
  3. LAN 主机 IP 地址的变更,无需通告 WAN。
  4. //AN_Xml:
  5. WAN 的 ISP 变更接口地址时,无需通告 LAN 内主机。
  6. //AN_Xml:
  7. LAN 主机对 WAN 不可见,不可直接寻址,可以保证一定程度的安全性。
  8. //AN_Xml:
//AN_Xml:

然而,NAT 协议由于其独特性,存在着一些争议。比如,可能你已经注意到了,NAT 协议在 LAN 以外,标识一个内部主机时,使用的是端口号,因为 IP 地址都是相同的。这种将端口号作为主机寻址的行为,可能会引发一些误会。此外,路由器作为网络层的设备,修改了传输层的分组内容(修改了源 IP 地址和端口号),同样是不规范的行为。但是,尽管如此,NAT 协议作为 IPv4 时代的产物,极大地方便了一些本来棘手的问题,一直被沿用至今。

//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 程序员简历编写指南 //AN_Xml: https://javaguide.cn/interview-preparation/resume-guide.html //AN_Xml: https://javaguide.cn/interview-preparation/resume-guide.html //AN_Xml: 程序员简历编写指南 //AN_Xml: 程序员简历编写指南:从筛选逻辑出发讲清简历结构、项目经历与技能描述写法,提供简历模板与避坑建议,帮助你提高简历通过率并让面试官更好地深挖你的亮点。 //AN_Xml: 面试准备 //AN_Xml: Fri, 28 Apr 2023 13:38:12 GMT //AN_Xml: //AN_Xml:

友情提示

//AN_Xml:

本文节选自 《Java 面试指北》。这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。

//AN_Xml: //AN_Xml:

前言

//AN_Xml:

一份好的简历可以在整个申请面试以及面试过程中起到非常重要的作用。

//AN_Xml:

为什么说简历很重要呢? 我们可以从下面几点来说:

//AN_Xml:

1、简历就像是我们的一个门面一样,它在很大程度上决定了是否能够获得面试机会。

//AN_Xml:
    //AN_Xml:
  • 假如你是网申,你的简历必然会经过 HR 的筛选,一张简历 HR 可能也就花费 10 秒钟左右看一下,然后决定你能否进入面试。
  • //AN_Xml:
  • 假如你是内推,如果你的简历没有什么优势的话,就算是内推你的人再用心,也无能为力。
  • //AN_Xml:
//AN_Xml:

另外,就算你通过了第一轮的筛选获得面试机会,后面的面试中,面试官也会根据你的简历来判断你究竟是否值得他花费很多时间去面试。

//AN_Xml:

2、简历上的内容很大程度上决定了面试官提问的侧重点。

//AN_Xml:
    //AN_Xml:
  • 一般情况下你的简历上注明你会的东西才会被问到(Java 基础、集合、并发、MySQL、Redis 、Spring、Spring Boot 这些算是每个人必问的),比如写了你熟练使用 Redis,那面试官就很大概率会问你 Redis 的一些问题,再比如你写了你在项目中使用了消息队列,那面试官大概率问很多消息队列相关的问题。
  • //AN_Xml:
  • 技能熟练度在很大程度上也决定了面试官提问的深度。
  • //AN_Xml:
//AN_Xml:

在不夸大自己能力的情况下,写出一份好的简历也是一项很棒的能力。一般情况下,技术能力和学习能力比较厉害的,写出来的简历也比较棒!

//AN_Xml:

简历模板

//AN_Xml:

简历的样式真的非常非常重要!!!如果你的简历样式丑到没朋友的话,面试官真的没有看下去的欲望。一天处理上百份的简历的痛苦,你不懂!

//AN_Xml:

我这里的话,推荐大家使用 Markdown 语法写简历,然后再将 Markdown 格式转换为 PDF 格式后进行简历投递。如果你对 Markdown 语法不太了解的话,可以花半个小时简单看一下 Markdown 语法说明: http://www.markdown.cn/

//AN_Xml:

下面是我收集的一些还不错的简历模板:

//AN_Xml: //AN_Xml:

上面这些简历模板大多是只有 1 页内容,很难展现足够的信息量。如果你不是顶级大牛(比如 ACM 大赛获奖)的话,我建议还是尽可能多写一点可以突出你自己能力的内容(校招生 2 页之内,社招生 3 页之内,记得精炼语言,不要过多废话)。

//AN_Xml:

再总结几点 简历排版的注意事项

//AN_Xml:
    //AN_Xml:
  • 尽量简洁,不要太花里胡哨。
  • //AN_Xml:
  • 技术名词最好规范大小写比较好,比如 java->Java ,spring boot -> Spring Boot 。这个虽然有些面试官不会介意,但是很多面试官都会在意这个细节的。
  • //AN_Xml:
  • 中文和数字英文之间加上空格的话看起来会舒服一点。
  • //AN_Xml:
//AN_Xml:

另外,知识星球里还有真实的简历模板可供参考,地址:https://t.zsxq.com/12ypxGNzU (需加入知识星球获取)。

//AN_Xml:

//AN_Xml:

简历内容

//AN_Xml:

个人信息

//AN_Xml:
    //AN_Xml:
  • 最基本的 :姓名(身份证上的那个)、年龄、电话、籍贯、联系方式、邮箱地址
  • //AN_Xml:
  • 潜在加分项 : Github 地址、博客地址(如果技术博客和 Github 上没有什么内容的话,就不要写了)
  • //AN_Xml:
//AN_Xml:

示例:

//AN_Xml:

//AN_Xml:

简历要不要放照片呢? 很多人写简历的时候都有这个问题。

//AN_Xml:

其实放不放都行,影响不大,完全不用在意这个问题。除非,你投递的岗位明确要求要放照片。 不过,如果要放的话,不要放生活照,还是应该放正规一些的照片比如证件照。

//AN_Xml:

求职意向

//AN_Xml:

你想要应聘什么岗位,希望在什么城市。另外,你也可以将求职意向放到个人信息这块写。

//AN_Xml:

示例:

//AN_Xml:

//AN_Xml:

教育经历

//AN_Xml:

教育经历也不可或缺。通过教育经历的介绍,你要确保能让面试官就可以知道你的学历、专业、毕业学校以及毕业的日期。

//AN_Xml:

示例:

//AN_Xml:
//AN_Xml:

北京理工大学 硕士,软件工程 2019.09 - 2022.01
//AN_Xml:湖南大学 学士,应用化学 2015.09 ~ 2019.06

//AN_Xml:
//AN_Xml:

专业技能

//AN_Xml:

先问一下你自己会什么,然后看看你意向的公司需要什么。一般 HR 可能并不太懂技术,所以他在筛选简历的时候可能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你可以花几天时间学习一下,然后在简历上可以写上自己了解这个技能。

//AN_Xml:

下面是一份最新的 Java 后端开发技能清单,你可以根据自身情况以及岗位招聘要求做动态调整,核心思想就是尽可能满足岗位招聘的所有技能要求。

//AN_Xml:

Java 后端技能模板

//AN_Xml:

我这里再单独放一个我看过的某位同学的技能介绍,我们来找找问题。

//AN_Xml:

//AN_Xml:

上图中的技能介绍存在的问题:

//AN_Xml:
    //AN_Xml:
  • 技术名词最好规范大小写比较好,比如 java->Java ,spring boot -> Spring Boot 。这个虽然有些面试官不会介意,但是很多面试官都会在意这个细节的。
  • //AN_Xml:
  • 技能介绍太杂,没有亮点。不需要全才,某个领域做得好就行了!
  • //AN_Xml:
  • 对 Java 后台开发的部分技能比如 Spring Boot 的熟悉度仅仅为了解,无法满足企业的要求。
  • //AN_Xml:
//AN_Xml:

实习经历/工作经历(重要)

//AN_Xml:

工作经历针对社招,实习经历针对校招。

//AN_Xml:

工作经历建议采用时间倒序的方式来介绍。实习经历和工作经历都需要简单突出介绍自己在职期间主要做了什么。

//AN_Xml:

示例:

//AN_Xml:
//AN_Xml:

XXX 公司 (201X 年 X 月 ~ 201X 年 X 月 )

//AN_Xml:
    //AN_Xml:
  • 职位:Java 后端开发工程师
  • //AN_Xml:
  • 工作内容:主要负责 XXX
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

项目经历(重要)

//AN_Xml:

简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。

//AN_Xml:

很多求职者的项目经历介绍都会面临过于啰嗦、过于简单、没突出亮点等问题。

//AN_Xml:

项目经历介绍模板如下:

//AN_Xml:
//AN_Xml:

项目名称(字号要大一些)

//AN_Xml:

2017-05~2018-06 淘宝 Java 后端开发工程师

//AN_Xml:
    //AN_Xml:
  • 项目描述 : 简单描述项目是做什么的。
  • //AN_Xml:
  • 技术栈 :用了什么技术(如 Spring Boot + MySQL + Redis + Mybatis-plus + Spring Security + Oauth2)
  • //AN_Xml:
  • 工作内容/个人职责 : 简单描述自己做了什么,解决了什么问题,带来了什么实质性的改善。突出自己的能力,不要过于平淡的叙述。
  • //AN_Xml:
  • 个人收获(可选) : 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用。通常是可以不用写个人收获的,因为你在个人职责介绍中写的东西已经表明了自己的主要收获。
  • //AN_Xml:
  • 项目成果(可选) :简单描述这个项目取得了什么成绩。
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

1、项目经历应该突出自己做了什么,简单概括项目基本情况。

//AN_Xml:

项目介绍尽量压缩在两行之内,不需要介绍太多,但也不要随便几个字就介绍完了。

//AN_Xml:

另外,个人收获和项目成果都是可选的,如果选择写的话,也不要花费太多篇幅,记住你的重点是介绍工作内容/个人职责。

//AN_Xml:

2、技术架构直接写技术名词就行,不要再介绍技术是干嘛的了,没意义,属于无效介绍。

//AN_Xml:

//AN_Xml:

3、尽量减少纯业务的个人职责介绍,对于面试不太友好。尽量再多挖掘一些亮点(6~8 条个人职责介绍差不多了,做好筛选),最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目优化了某个模块的性能。

//AN_Xml:

即使不是你做的功能模块或者解决的问题,你只要搞懂吃透了就能拿来自己用,适当润色即可!

//AN_Xml:

像性能优化方向上的亮点面试之前也比较容易准备,但也不要都是性能优化相关的,这种也算是一个极端。

//AN_Xml:

另外,技术优化取得的成果尽量要量化一下:

//AN_Xml:
    //AN_Xml:
  • 使用 xxx 技术解决了 xxx 问题,系统 QPS 从 xxx 提高到了 xxx。
  • //AN_Xml:
  • 使用 xxx 技术了优化了 xxx 接口,系统 QPS 从 xxx 提高到了 xxx。
  • //AN_Xml:
  • 使用 xxx 技术解决了 xxx 问题,查询速度优化了 xxx,系统 QPS 达到 10w+。
  • //AN_Xml:
  • 使用 xxx 技术优化了 xxx 模块,响应时间从 2s 降低到 0.2s。
  • //AN_Xml:
  • ……
  • //AN_Xml:
//AN_Xml:

个人职责介绍示例(这里只是举例,不要照搬,结合自己项目经历自己去写,不然面试的时候容易被问倒) :

//AN_Xml:
    //AN_Xml:
  • 基于 Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权,使用 RBAC 权限模型实现动态权限控制。
  • //AN_Xml:
  • 参与项目订单模块的开发,负责订单创建、删除、查询等功能,基于 Spring 状态机实现订单状态流转。
  • //AN_Xml:
  • 商品和订单搜索场景引入 Elasticsearch,并且实现了相关商品推荐以及搜索提示功能。
  • //AN_Xml:
  • 整合 Canal + RabbitMQ 将 MySQL 增量数据(如商品、订单数据)同步到 Elasticsearch。
  • //AN_Xml:
  • 利用 RabbitMQ 官方提供的延迟队列插件实现延时任务场景比如订单超时自动取消、优惠券过期提醒、退款处理。
  • //AN_Xml:
  • 消息推送系统引入 RabbitMQ 实现异步处理、削峰填谷和服务解耦,最高推送速度 10w/s,单日最大消息量 2000 万。
  • //AN_Xml:
  • 使用 MAT 工具分析 dump 文件解决了广告服务新版本上线后导致大量的服务超时告警的问题。
  • //AN_Xml:
  • 排查并解决扣费模块由于扣费父任务和反作弊子任务使用同一个线程池导致的死锁问题。
  • //AN_Xml:
  • 基于 EasyExcel 实现广告投放数据的导入导出,通过 MyBatis 批处理插入数据,基于任务表实现异步。
  • //AN_Xml:
  • 负责用户统计模块的开发,使用 CompletableFuture 并行加载后台用户统计模块的数据信息,平均相应时间从 3.5s 降低到 1s。
  • //AN_Xml:
  • 基于 Sentinel 对核心场景(如用户登入注册、收货地址查询等)进行限流、降级,保护系统,提升用户体验。
  • //AN_Xml:
  • 热门数据(如首页、热门博客)使用 Redis+Caffeine 两级缓存,解决了缓存击穿和穿透问题,查询速度毫秒级,QPS 30w+。
  • //AN_Xml:
  • 使用 CompletableFuture 优化购物车查询模块,对获取用户信息、商品详情、优惠券信息等异步 RPC 调用进行编排,响应时间从 2s 降低为 0.2s。
  • //AN_Xml:
  • 搭建 EasyMock 服务,用于模拟第三方平台接口,方便了在网络隔离情况下的接口对接工作。
  • //AN_Xml:
  • 基于 SkyWalking + Elasticsearch 搭建分布式链路追踪系统实现全链路监控。
  • //AN_Xml:
//AN_Xml:

4、如果你觉得你的项目技术比较落后的话,可以自己私下进行改进。重要的是让项目比较有亮点,通过什么方式就无所谓了。

//AN_Xml:

项目经历这部分对于简历来说非常重要,《Java 面试指北》的面试准备篇有好几篇关于优化项目经历的文章,建议你仔细阅读一下,应该会对你有帮助。

//AN_Xml:

//AN_Xml:

5、避免个人职责介绍都是围绕一个技术点来写,非常不可取。

//AN_Xml:

//AN_Xml:

6、避免模糊性描述,介绍要具体(技术+场景+效果),也要注意精简语言(避免堆砌技术词,省略不必要的描述)。

//AN_Xml:

//AN_Xml:

荣誉奖项(可选)

//AN_Xml:

如果你有含金量比较高的竞赛(比如 ACM、阿里的天池大赛)的获奖经历的话,荣誉奖项这块内容一定要写一下!并且,你还可以将荣誉奖项这块内容适当往前放,放在一个更加显眼的位置。

//AN_Xml:

校园经历(可选)

//AN_Xml:

如果有比较亮眼的校园经历的话就简单写一下,没有就不写!

//AN_Xml:

个人评价

//AN_Xml:

个人评价就是对自己的解读,一定要用简洁的语言突出自己的特点和优势,避免废话! 像勤奋、吃苦这些比较虚的东西就不要扯了,面试官看着这种个人评价就烦。

//AN_Xml:

我们可以从下面几个角度来写个人评价:

//AN_Xml:
    //AN_Xml:
  • 文档编写能力、学习能力、沟通能力、团队协作能力
  • //AN_Xml:
  • 对待工作的态度以及个人的责任心
  • //AN_Xml:
  • 能承受的工作压力以及对待困难的态度
  • //AN_Xml:
  • 对技术的追求、对代码质量的追求
  • //AN_Xml:
  • 分布式、高并发系统开发或维护经验
  • //AN_Xml:
//AN_Xml:

列举 3 个实际的例子:

//AN_Xml:
    //AN_Xml:
  • 学习能力较强,大三参加国家软件设计大赛的时候快速上手 Python 写了一个可配置化的爬虫系统。
  • //AN_Xml:
  • 具有团队协作精神,大三参加国家软件设计大赛的时候协调项目组内 5 名开发同学,并对编码遇到困难的同学提供帮助,最终顺利在 1 个月的时间完成项目的核心功能。
  • //AN_Xml:
  • 项目经验丰富,在校期间主导过多个企业级项目的开发。
  • //AN_Xml:
//AN_Xml:

STAR 法则和 FAB 法则

//AN_Xml:

STAR 法则(Situation Task Action Result)

//AN_Xml:

相信大家一定听说过 STAR 法则。对于面试,你可以将这个法则用在自己的简历以及和面试官沟通交流的过程中。

//AN_Xml:

STAR 法则由下面 4 个单词组成(STAR 法则的名字就是由它们的首字母组成):

//AN_Xml:
    //AN_Xml:
  • Situation: 情景。 事情是在什么情况下发生的?
  • //AN_Xml:
  • Task: 任务。你的任务是什么?
  • //AN_Xml:
  • Action: 行动。你做了什么?
  • //AN_Xml:
  • Result: 结果。最终的结果怎样?
  • //AN_Xml:
//AN_Xml:

FAB 法则(Feature Advantage Benefit)

//AN_Xml:

除了 STAR 法则,你还需要了解在销售行业经常用到的一个叫做 FAB 的法则。

//AN_Xml:

FAB 法则由下面 3 个单词组成(FAB 法则的名字就是由它们的首字母组成):

//AN_Xml:
    //AN_Xml:
  • Feature: 你的特征/优势是什么?
  • //AN_Xml:
  • Advantage: 比别人好在哪些地方;
  • //AN_Xml:
  • Benefit: 如果雇佣你,招聘方会得到什么好处。
  • //AN_Xml:
//AN_Xml:

简单来说,FAB 法则主要是让你的面试官知道你的优势和你能为公司带来的价值。

//AN_Xml:

建议

//AN_Xml:

避免页数过多

//AN_Xml:

精简表述,突出亮点。校招简历建议不要超过 2 页,社招简历建议不要超过 3 页。如果内容过多的话,不需要非把内容压缩到一页,保持排版干净整洁就可以了。

//AN_Xml:

看了几千份简历,有少部分同学的简历页数都接近 10 页了,让我头皮发麻。

//AN_Xml:

简历页数过多

//AN_Xml:

避免语义模糊

//AN_Xml:

尽量避免主观表述,少一点语义模糊的形容词。表述要简洁明了,简历结构要清晰。

//AN_Xml:

举例:

//AN_Xml:
    //AN_Xml:
  • 不好的表述:我在团队中扮演了很重要的角色。
  • //AN_Xml:
  • 好的表述:我作为后端技术负责人,领导团队完成后端项目的设计与开发。
  • //AN_Xml:
//AN_Xml:

注意简历样式

//AN_Xml:

简历样式同样很重要,一定要注意!不必追求花里胡哨,但要尽量保证结构清晰且易于阅读。

//AN_Xml:

其他

//AN_Xml:
    //AN_Xml:
  • 一定要使用 PDF 格式投递,不要使用 Word 或者其他格式投递。这是最基本的!
  • //AN_Xml:
  • 不会的东西就不要写在简历上了。注意简历真实性,适当润色没有问题。
  • //AN_Xml:
  • 工作经历建议采用时间倒序的方式来介绍,实习经历建议将最有价值的放在最前面。
  • //AN_Xml:
  • 将自己的项目经历完美的展示出来非常重要,重点是突出自己做了什么(挖掘亮点),而不是介绍项目是做什么的。
  • //AN_Xml:
  • 项目经历建议以时间倒序排序,另外项目经历不在于多(精选 2~3 即可),而在于有亮点。
  • //AN_Xml:
  • 准备面试的过程中应该将你写在简历上的东西作为重点,尤其是项目经历上和技能介绍上的。
  • //AN_Xml:
  • 面试和工作是两回事,聪明的人会把面试官往自己擅长的领域领,其他人则被面试官牵着鼻子走。虽说面试和工作是两回事,但是你要想要获得自己满意的 offer ,你自身的实力必须要强。
  • //AN_Xml:
//AN_Xml:

简历修改

//AN_Xml:

到目前为止,我至少帮助 6000+ 位球友提供了免费的简历修改服务。由于个人精力有限,修改简历仅限加入星球的读者,需要帮看简历的话,可以加入 JavaGuide 官方知识星球(点击链接查看详细介绍)。

//AN_Xml:

img

//AN_Xml:

虽然收费只有培训班/训练营的百分之一,但是知识星球里的内容质量更高,提供的服务也更全面,非常适合准备 Java 面试和学习 Java 的同学。

//AN_Xml:

下面是星球提供的部分服务(点击下方图片即可获取知识星球的详细介绍):

//AN_Xml:

星球服务

//AN_Xml:

这里再提供一份限时专属优惠卷:

//AN_Xml:

知识星球30元优惠卷

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 数据冷热分离详解 //AN_Xml: https://javaguide.cn/high-performance/data-cold-hot-separation.html //AN_Xml: https://javaguide.cn/high-performance/data-cold-hot-separation.html //AN_Xml: 数据冷热分离详解 //AN_Xml: 本文详解数据冷热分离的核心原理与实践方案,涵盖冷热数据判定策略、多级分层设计、数据迁移一致性保障、冷数据查询优化、存储选型(HBase/TiDB/对象存储),以及订单/日志/内容系统的典型落地案例。 //AN_Xml: 高性能 //AN_Xml: Fri, 28 Apr 2023 11:27:56 GMT //AN_Xml: JavaGuide官方知识星球

//AN_Xml:

什么是数据冷热分离?

//AN_Xml:

数据冷热分离是指根据数据的访问频率业务重要性,将数据划分为冷数据和热数据,并分别存储在不同性能和成本的存储介质中的架构策略。

//AN_Xml:

这种架构的核心目标有三个:

//AN_Xml:
    //AN_Xml:
  1. 提升查询性能:热数据存储在高性能介质(如 SSD、内存)中,保障核心业务的响应速度。
  2. //AN_Xml:
  3. 降低存储成本:冷数据迁移至低成本介质(如 HDD、对象存储),大幅削减存储开支。
  4. //AN_Xml:
  5. 满足合规要求:部分行业(如金融、医疗)要求数据长期归档,冷热分离可兼顾合规与成本。
  6. //AN_Xml:
//AN_Xml:

冷数据和热数据

//AN_Xml:

热数据是指被频繁访问和修改、且需要快速响应的数据;冷数据是指访问频率极低、对当前业务价值较小、但需要长期保留的数据。

//AN_Xml:

冷热数据的区分方法主要有两种:

//AN_Xml:
    //AN_Xml:
  1. 时间维度区分:按照数据的创建时间、更新时间或过期时间划分。例如,订单系统将一段时间前(如 90 天或 1 年)的订单数据标记为冷数据。该方法适用于数据访问频率与时间强相关的场景,实现简单、成本低。
  2. //AN_Xml:
  3. 访问频率区分:将高频访问的数据视为热数据,低频访问的数据视为冷数据。例如,内容系统将浏览量低于阈值的文章标记为冷数据。该方法需要额外记录访问频率,适用于访问频率与数据本身特性强相关的场景。
  4. //AN_Xml:
//AN_Xml:

如何选择区分策略?

//AN_Xml:
    //AN_Xml:
  • 若业务数据天然具有时效性(如订单、日志、账单),优先选择时间维度,实现成本最低。
  • //AN_Xml:
  • 若数据价值与时间无关(如文章、商品、用户画像),需结合访问频率进行判定。
  • //AN_Xml:
  • 实际项目中,可将两者结合使用:以时间维度为主、访问频率为辅,覆盖更多业务场景。
  • //AN_Xml:
//AN_Xml:

冷热分离的多级分层策略

//AN_Xml:

实际落地时,"冷"与"热"往往不是非此即彼的二分法,而是渐进式多级分层

//AN_Xml:

| 层级 | 数据特性 | 判定规则示例 | 存储策略 |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 深度分页介绍及优化建议 //AN_Xml: https://javaguide.cn/high-performance/deep-pagination-optimization.html //AN_Xml: https://javaguide.cn/high-performance/deep-pagination-optimization.html //AN_Xml: 深度分页介绍及优化建议 //AN_Xml: 深度分页是指查询偏移量过大导致性能下降的场景,本文详解深度分页产生的原因及四种优化方案:范围查询、子查询优化、INNER JOIN 延迟关联、覆盖索引,并分析各方案的适用场景与优缺点。 //AN_Xml: 高性能 //AN_Xml: Fri, 28 Apr 2023 11:27:56 GMT //AN_Xml: JavaGuide官方知识星球

//AN_Xml:

什么是深度分页?怎么导致的?

//AN_Xml:

查询偏移量过大的场景我们称为深度分页,这会导致查询性能较低,例如:

//AN_Xml:
# MySQL 在无法利用索引的情况下跳过1000000条记录后,再获取10条记录
//AN_Xml:SELECT * FROM t_order ORDER BY id LIMIT 1000000, 10
//AN_Xml:

当查询偏移量过大时,MySQL 的查询优化器可能会选择全表扫描而不是利用索引来优化查询。

//AN_Xml:

深度分页变慢的根本原因在于 MySQL 的执行机制:对于 LIMIT offset, N,MySQL 并非直接跳到 offset 处,而是必须从头扫描 offset + N 条记录。如果查询依赖二级索引且不满足覆盖索引,这意味着 MySQL 需要对前 offset 条记录执行毫无意义的回表查询(产生海量的随机 I/O),最后再将这些辛苦查出的数据丢弃。即便优化器最终因代价过高退化为全表扫描,顺序扫描百万行的成本依然巨大。

//AN_Xml:

深度分页问题

//AN_Xml:

不同机器上这个查询偏移量过大的临界点可能不同,取决于多个因素,包括硬件配置(如 CPU 性能、磁盘速度)、表的大小、索引的类型和统计信息等。

//AN_Xml:

转全表扫描的临界点

//AN_Xml:

MySQL 的查询优化器采用基于成本的策略来选择最优的查询执行计划。它会根据 CPU 和 I/O 的成本来决定是否使用索引扫描或全表扫描。如果优化器认为全表扫描的成本更低,它就会放弃使用索引。不过,即使偏移量很大,如果查询中使用了覆盖索引(covering index),MySQL 仍然可能会使用索引,避免回表操作。

//AN_Xml:

深度分页优化建议

//AN_Xml:
//AN_Xml:

本文基于 MySQL 8.0 + InnoDB 存储引擎,不同版本优化器行为可能存在差异。

//AN_Xml:
//AN_Xml:

范围查询(游标分页)

//AN_Xml:

通过记录上一页最后一条记录的 ID,使用 WHERE id > last_id LIMIT n 获取下一页数据:

//AN_Xml:
# 通过记录上次查询结果的最后一条记录的 ID 进行下一页的查询
//AN_Xml:SELECT * FROM t_order WHERE id > 100000 ORDER BY id LIMIT 10
//AN_Xml:

游标分页的核心优势不依赖 ID 的连续性。MySQL 只需要在 B+ 树上定位到 last_id 的位置,然后顺序向后读取 n 条记录即可,中间是否有断层(如 ID 被删除)完全不影响结果的准确性和性能。

//AN_Xml:

这种方式的限制:

//AN_Xml:
    //AN_Xml:
  1. 不支持跳页:无法直接跳转到第 N 页,只能逐页向后(或向前)翻页。
  2. //AN_Xml:
  3. 排序字段受限:如果查询需要按照其他字段(如创建时间)排序而非 ID 排序,需使用联合游标 (sort_field, id) 保证唯一性和顺序。
  4. //AN_Xml:
  5. 并发场景:当分页查询期间有新数据插入或删除时,可能出现: //AN_Xml:
      //AN_Xml:
    • 数据遗漏:查询第二页时,有新数据插入到第一页范围内,导致该数据被"挤"到第二页,但第二页查询已基于旧的最后 ID 跳过它。
    • //AN_Xml:
    • 数据重复:查询第二页时,第一页末尾有数据被删除,原第二页的第一条数据"升"到第一页末尾,导致第二页查询再次返回它。
    • //AN_Xml:
    //AN_Xml:
  6. //AN_Xml:
//AN_Xml:

子查询

//AN_Xml:

我们先查询出 limit 第一个参数对应的主键值,再根据这个主键值再去过滤并 limit,这样效率会更快一些。

//AN_Xml:

阿里巴巴《Java 开发手册》中也有对应的描述:

//AN_Xml:
//AN_Xml:

利用延迟关联或者子查询优化超多分页场景。

//AN_Xml:

//AN_Xml:
//AN_Xml:
-- 先通过子查询在主键索引上进行偏移,快速找到起始ID
//AN_Xml:SELECT * FROM t_order
//AN_Xml:WHERE id >= (
//AN_Xml:    SELECT id FROM t_order ORDER BY id LIMIT 1000000, 1
//AN_Xml:) ORDER BY id LIMIT 10;
//AN_Xml:

工作原理:

//AN_Xml:
    //AN_Xml:
  1. 子查询 (SELECT id FROM t_order ORDER BY id LIMIT 1000000, 1) 利用主键索引扫描并跳过前 1000000 条记录,返回第 1000001 条记录的主键值。
  2. //AN_Xml:
  3. 主查询 SELECT * FROM t_order WHERE id >= ... ORDER BY id LIMIT 10 以该主键为起点,获取后续 10 条完整记录。
  4. //AN_Xml:
//AN_Xml:

不过,某些情况下子查询可能会产生临时表,影响性能,因此在复杂查询中建议优先考虑延迟关联。

//AN_Xml:
//AN_Xml:

复杂过滤场景:在包含复杂过滤条件的分页场景中(如 WHERE status = 1 ORDER BY id LIMIT 1000000, 10),符合条件的 ID 往往是离散的。此时子查询的优势更加明显:通过在子查询中利用联合索引(如 (status, id))实现覆盖索引扫描,可以高效地跳过前 100 万条符合条件的记录,定位到目标 ID 后,主查询只需回表 10 次。

//AN_Xml:
//AN_Xml:

当然,我们也可以利用子查询先去获取目标分页的 ID 集合,然后再根据 ID 集合获取内容,但这种写法非常繁琐,不如使用 INNER JOIN 延迟关联。

//AN_Xml:

延迟关联

//AN_Xml:

延迟关联与子查询的优化思路类似,都是通过将 LIMIT 操作转移到主键索引树上,减少回表次数。相比直接使用子查询,延迟关联通过 INNER JOIN 将子查询结果集成到主查询中,避免了子查询可能产生的临时表。在执行 INNER JOIN 时,MySQL 优化器能够利用索引进行高效的连接操作(如索引扫描或其他优化策略),因此在深度分页场景下,性能通常优于直接使用子查询。

//AN_Xml:
-- 使用 INNER JOIN 进行延迟关联
//AN_Xml:SELECT t1.*
//AN_Xml:FROM t_order t1
//AN_Xml:INNER JOIN (
//AN_Xml:    -- 这里的子查询可以利用覆盖索引,性能极高
//AN_Xml:    SELECT id FROM t_order ORDER BY id LIMIT 1000000, 10
//AN_Xml:) t2 ON t1.id = t2.id
//AN_Xml:ORDER BY t1.id;
//AN_Xml:

工作原理:

//AN_Xml:
    //AN_Xml:
  1. 子查询 (SELECT id FROM t_order ORDER BY id LIMIT 1000000, 10) 利用主键索引扫描并跳过前 1000000 条记录,返回目标分页的 10 条记录的 ID。
  2. //AN_Xml:
  3. 通过 INNER JOIN 将子查询结果与主表 t_order 关联,获取完整的记录数据。
  4. //AN_Xml:
//AN_Xml:

除了使用 INNER JOIN 之外,还可以使用逗号连接子查询。

//AN_Xml:
-- 使用逗号进行延迟关联
//AN_Xml:SELECT t1.* FROM t_order t1,
//AN_Xml:(SELECT id FROM t_order ORDER BY id LIMIT 1000000, 10) t2
//AN_Xml:WHERE t1.id = t2.id
//AN_Xml:ORDER BY t1.id;
//AN_Xml:

注意: 虽然逗号连接子查询也能实现类似的效果,但为了代码可读性和可维护性,建议使用更规范的 INNER JOIN 语法。

//AN_Xml:

覆盖索引

//AN_Xml:

索引中已经包含了所有需要获取的字段的查询方式称为覆盖索引。

//AN_Xml:

覆盖索引的好处:

//AN_Xml:
    //AN_Xml:
  • 避免 InnoDB 表进行索引的二次查询,也就是回表操作:InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询(回表),减少了 IO 操作,提升了查询效率。
  • //AN_Xml:
  • 减少回表带来的随机 IO:通过覆盖索引直接返回数据,避免了根据二级索引的主键值回表查询聚簇索引的随机 IO 操作。回表时每次按主键值查找聚簇索引,本质上是随机 IO。
  • //AN_Xml:
//AN_Xml:

假设建立了 (code, type) 联合索引,下面的查询即可使用覆盖索引:

//AN_Xml:
# 在 InnoDB 中,辅助索引天然包含主键 id
//AN_Xml:# 如果只需要查询 id, code, type 这三列,只需建立 (code, type) 的联合索引即可实现覆盖
//AN_Xml:SELECT id, code, type FROM t_order
//AN_Xml:ORDER BY code
//AN_Xml:LIMIT 1000000, 10;
//AN_Xml:

⚠️注意:

//AN_Xml:
    //AN_Xml:
  • 当查询的结果集占表的总行数的很大一部分时,MySQL 查询优化器可能选择放弃使用索引,自动转换为全表扫描。
  • //AN_Xml:
  • 虽然可以使用 FORCE INDEX 强制查询优化器走索引,但这种方式可能会导致查询优化器无法选择更优的执行计划,效果并不总是理想。
  • //AN_Xml:
//AN_Xml:

生产落地建议

//AN_Xml:

监控与告警

//AN_Xml:
    //AN_Xml:
  • 慢查询监控:监控慢查询日志中 LIMIT 偏移量过大的 SQL,及时发现问题。
  • //AN_Xml:
  • 阈值告警:设置 long_query_time 阈值捕获深度分页查询。
  • //AN_Xml:
  • 执行计划检查:使用 EXPLAIN 定期检查关键分页 SQL 的执行计划,确保优化器按预期使用索引。
  • //AN_Xml:
//AN_Xml:

常见误区

//AN_Xml:

| 误区 | 事实 |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 面试太紧张怎么办? //AN_Xml: https://javaguide.cn/interview-preparation/how-to-handle-interview-nerves.html //AN_Xml: https://javaguide.cn/interview-preparation/how-to-handle-interview-nerves.html //AN_Xml: 面试太紧张怎么办? //AN_Xml: 面试太紧张影响发挥怎么办?从心态调整、提前准备到模拟面试与表达训练,提供一套可落地的方法,帮助你降低焦虑、提升临场表现,更稳定地通过技术面试。 //AN_Xml: 面试准备 //AN_Xml: Fri, 28 Apr 2023 11:27:56 GMT //AN_Xml: JavaGuide官方知识星球

//AN_Xml:

很多小伙伴在第一次技术面试时都会感到紧张甚至害怕,遇到稍微刁钻的问题大脑就一片空白,面试结束后还会有种“懵懵的”感觉。我也经历过类似的状况,对这种手心出汗、语无伦次的窘境深有体会。

//AN_Xml:

其实,紧张是非常正常的生理和心理反应——它代表你对这次机会的重视,也源于人类对未知结果的天然担忧。但如果任由过度紧张蔓延,绝对会大幅折损你的临场发挥水平。

//AN_Xml:

下面,我将结合自己的实战经验,从心态重塑、战术准备、临场应对、面后复盘四个维度,分享一套可落地的“抗紧张”指南。

//AN_Xml:

试着接受紧张情绪,调整心态

//AN_Xml:

首先要明白,紧张是正常情绪,特别是初次或前几次面试时,多少都会有点忐忑。不要过分排斥这种情绪,可以适当地“拥抱”它:

//AN_Xml:
    //AN_Xml:
  • 搞清楚面试的本质:面试本质上是一场与面试官的深入交流,是一个双向选择的过程。面试失败并不意味着你的价值和努力被否定,而可能只是因为你与目标岗位暂时不匹配,或者仅仅是一次 KPI 面试,这家公司可能压根就没有真正的招聘需求。失败的原因也可能是某些知识点、项目经验或表达方式未能充分展现出你的能力。即便这次面试未通过,也不妨碍你继续尝试其他公司,完全不慌!
  • //AN_Xml:
  • 不要害怕面试官:很多求职者平时和同学朋友交流沟通的蛮好,一到面试就害怕了。面试官和求职者双方是平等的,以后说不定就是同事关系。也不要觉得面试官就很厉害,实际上,面试官的水平也参差不齐。他们提出的问题,可能自己也没有完全理解。
  • //AN_Xml:
  • 给自己积极的心理暗示:告诉自己“有点紧张没关系,这只能让我更专注,心跳加快是我在给自己打气,我一定可以回答的很好!”。
  • //AN_Xml:
//AN_Xml:

提前准备,减少不确定性

//AN_Xml:

不确定性越多,越容易紧张。 如果你能够在面试前做充分的准备,很多“未知”就会消失,紧张情绪自然会减轻很多。

//AN_Xml:

认真准备技术面试

//AN_Xml:
    //AN_Xml:
  • 优先梳理核心知识点:比如计算基础、数据库、Java 基础、Java 集合、并发编程、SpringBoot(这里以 Java 后端方向为例)等。如果时间不够,可以分轻重缓急,有重点地复习。如果你想要系统准备 Java 后端面试但又不知道如何开始的,可以参考 Java 后端面试通关计划(后端通用)
  • //AN_Xml:
  • 精心准备项目经历:认真思考你简历上最重要的项目(面试以前两个项目为主,尤其是第一个),它们的技术难点、业务逻辑、架构设计,以及可能被面试官深挖的点。把你的思考总结成可能出现的面试问题,并尝试回答。
  • //AN_Xml:
//AN_Xml:

模拟面试和自测

//AN_Xml:
    //AN_Xml:
  • 约朋友或同学互相提问:以真实的面试场景来进行演练,并及时对回答进行诊断和反馈。
  • //AN_Xml:
  • 线上练习:直接利用 AI 来进行模拟面试即可,免费且高效。把自己的简历投喂给它,让它根据你的简历,尤其是项目经历生成面试问题。
  • //AN_Xml:
  • 面经:平时可以多看一些前辈整理的面经,尤其是目标岗位或目标公司的面经,总结高频考点和常见问题。
  • //AN_Xml:
  • 技术面试题自测:在 《Java 面试指北》 的 「技术面试题自测篇」 ,我总结了 Java 面试中最重要的知识点的最常见的面试题并按照面试提问的方式展现出来。其中,每一个问题都有提示和重要程度说明,非常适合用来自测。
  • //AN_Xml:
//AN_Xml:

《Java 面试指北》 的 「技术面试题自测篇」概览:

//AN_Xml:

技术面试题自测篇

//AN_Xml:

多表达

//AN_Xml:

平时要多说,多表达出来,不要只是在心里面想,不然真正面试的时候会发现想的和说的不太一样。

//AN_Xml:

我前面推荐的模拟面试和自测,有一部分原因就是为了能够多多表达。

//AN_Xml:

多面试

//AN_Xml:
    //AN_Xml:
  • 先小厂后大厂:可以先去一些规模较小或者对你来说压力没那么大的公司试试手,积累一些实战经验,增加一些信心;等熟悉了面试流程、能够更从容地回答问题后,再去挑战自己心仪的大厂或热门公司。
  • //AN_Xml:
  • 积累“失败经验”:不要怕被拒,有些时候被拒绝却能从中学到更多。多复盘,多思考到底是哪个环节出了问题,再用更好的状态迎接下一次面试。
  • //AN_Xml:
//AN_Xml:

保证休息

//AN_Xml:
    //AN_Xml:
  • 留出充裕时间:面试前尽量不要排太多事情,保证自己能有个好状态去参加面试。
  • //AN_Xml:
  • 保证休息:充足睡眠有助于情绪稳定,也能让你在面试时更清晰地思考问题。
  • //AN_Xml:
//AN_Xml:

遇到不会的问题不要慌

//AN_Xml:

一场面试,不太可能面试官提的每一个问题你都能轻松应对,除非这场面试非常简单。

//AN_Xml:

在面试过程中,遇到不会的问题,首先要做的是快速回顾自己过往的知识,看是否能找到突破口。如果实在没有思路的话,可以真诚地向面试要一些提示比如谈谈你对这个问题的理解以及困惑点。一定不要觉得向面试官要提示很可耻,只要沟通没问题,这其实是很正常的。最怕的就是自己不会,还乱回答一通,这样会让面试官觉得你技术态度有问题。

//AN_Xml:

面试结束后的复盘

//AN_Xml:

很多人关注面试前的准备,却忽略了面试后的复盘,这一步真的非常非常非常重要:

//AN_Xml:
    //AN_Xml:
  1. 记录面试中的问题:无论回答得好坏,都把它们写下来。如果问到了一些没想过的问题,可以认真思考并在面试后补上答案。
  2. //AN_Xml:
  3. 反思自己的表现:有没有遇到卡壳的地方?是知识没准备到还是过于紧张导致表达混乱?下次如何改进?
  4. //AN_Xml:
  5. 持续完善自己的“面试题库”:把新的问题补充进去,不断拓展自己的知识面,也逐步降低对未知问题的恐惧感。
  6. //AN_Xml:
//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 校招没有实习经历怎么办?实习经历怎么写? //AN_Xml: https://javaguide.cn/interview-preparation/internship-experience.html //AN_Xml: https://javaguide.cn/interview-preparation/internship-experience.html //AN_Xml: 校招没有实习经历怎么办?实习经历怎么写? //AN_Xml: 校招没有实习经历也能上岸:从补强项目经验、持续优化简历到系统准备技术面试,给出可执行的提升路径与注意事项,帮助你在没有大厂实习的情况下提高面试通过率。 //AN_Xml: 面试准备 //AN_Xml: Fri, 28 Apr 2023 11:27:56 GMT //AN_Xml: JavaGuide官方知识星球

//AN_Xml:

由于目前的面试太卷,对于犹豫是否要找实习的同学来说,个人建议不论是本科生还是研究生都应该在参加校招面试之前,争取一下不错的实习机会,尤其是大厂的实习机会,日常实习或者暑期实习都可以。当然,如果大厂实习面不上,中小厂实习也是可以接受的。

//AN_Xml:

不过,现在的实习是真难找,这两年有非常多的同学没有找到实习,有一部分甚至是 211/985 名校的同学。实习难找是一方面原因,国内很多学校的导师压根不放实习,这也是很棘手的问题。

//AN_Xml:

没有实习经历怎么办?

//AN_Xml:

如果实在是找不到合适的实习的话,那也没办法,我们应该多花时间去把下面这三件事情给做好:

//AN_Xml:
    //AN_Xml:
  1. 补强项目经历
  2. //AN_Xml:
  3. 持续完善简历
  4. //AN_Xml:
  5. 准备技术面试
  6. //AN_Xml:
//AN_Xml:

补强项目经历

//AN_Xml:

校招没有实习经历的话,找工作比较吃亏(没办法,太卷了),需要在项目经历部分多发力弥补一下。

//AN_Xml:

建议你尽全力地去补强自己的项目经历,完善现有的项目或者去做更有亮点的项目,尽可能地通过项目经历去弥补一些。

//AN_Xml:

你面试中的重点就是你的项目经历涉及到的知识点,如果你的项目经历比较简单的话,面试官直接不知道问啥了。另外,你的项目经历中不涉及的知识点,但在技能介绍中提到的知识点也很大概率会被问到。像 Redis 这种基本是面试 Java 后端岗位必备的技能,我觉得大部分面试官应该都会问。

//AN_Xml:

推荐阅读一下网站的这篇文章:项目经验指南

//AN_Xml:

完善简历

//AN_Xml:

一定一定一定要重视简历啊!建议至少花 2~3 天时间来专门完善自己的简历。并且,后续还要持续完善。

//AN_Xml:

对于面试官来说,筛选简历的时候会比较看重下面这些维度:

//AN_Xml:
    //AN_Xml:
  1. 实习/工作经历:看你是否有不错的实习经历,大厂且与面试岗位相关的实习/工作经历最佳。
  2. //AN_Xml:
  3. 获奖经历:如果有含金量比较高(知名度较高的赛事比如 ACM、阿里云天池)的获奖经历的话,也是加分点,尤其是对于校招来说,这类求职者属于是很多大厂争抢的对象(但不是说获奖了就能进大厂,还是要面试表现还可以)。对于社招来说,获奖经历作用相对较小,通常会更看重过往的工作经历和项目经验。
  4. //AN_Xml:
  5. 项目经验:项目经验对于面试来说非常重要,面试官会重点关注,同时也是有水平的面试提问的重点。
  6. //AN_Xml:
  7. 技能匹配度:看你的技能是否满足岗位的需求。在投递简历之前,一定要确认一下自己的技能介绍中是否缺少一些你要投递的对应岗位的技能要求。
  8. //AN_Xml:
  9. 学历:相对其他行业来说,程序员求职面试对于学历的包容度还是比较高的,只要你在其他方面有过人之出的话,也是可以弥补一下学历的缺陷的。你要知道,很多行业比如律师、金融,学历就是敲门砖,学历没达到要求,直接面试机会都没有。不过,由于现在面试越来越卷,一些大厂、国企和研究所也开始卡学历了,很多岗位都要求 211/985,甚至必须需要硕士学历。总之,学历很难改变,学校较差的话,就投递那些对学历没有明确要求的公司即可,努力提升自己的其他方面的硬实力。
  10. //AN_Xml:
//AN_Xml:

对于大部分求职者来说,实习/工作经历、项目经验、技能匹配度更重要一些。不过,不排除一些公司会因为学历卡人。

//AN_Xml:

详细的程序员简历编写指南可以参考这篇文章:程序员简历编写指南(重要)

//AN_Xml:

准备技术面试

//AN_Xml:

面试之前一定要提前准备一下常见的面试题也就是八股文:

//AN_Xml:
    //AN_Xml:
  • 自己面试中可能涉及哪些知识点、那些知识点是重点。
  • //AN_Xml:
  • 面试中哪些问题会被经常问到、面试中自己该如何回答。(强烈不推荐死记硬背,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!)
  • //AN_Xml:
//AN_Xml:

不同类型的公司对于技能的要求侧重点是不同的比如腾讯、字节可能更重视计算机基础比如网络、操作系统这方面的内容。阿里、美团这种可能更重视你的项目经历、实战能力。

//AN_Xml:

一定不要抱着一种思想,觉得八股文或者基础问题的考查意义不大。如果你抱着这种思想复习的话,那效果可能不会太好。实际上,个人认为还是很有意义的,八股文或者基础性的知识在日常开发中也会需要经常用到。例如,线程池这块的拒绝策略、核心参数配置什么的,如果你不了解,实际项目中使用线程池可能就用的不是很明白,容易出现问题。而且,其实这种基础性的问题是最容易准备的,像各种底层原理、系统设计、场景题以及深挖你的项目这类才是最难的!

//AN_Xml:

八股文资料首推我的 《Java 面试指北》JavaGuide 。里面不仅仅是原创八股文,还有很多对实际开发有帮助的干货。除了我的资料之外,你还可以去网上找一些其他的优质的文章、视频来看。

//AN_Xml:

如果你想要系统准备 Java 后端面试但又不知道如何开始的,可以参考 Java 后端面试通关计划(后端通用)

//AN_Xml:

实习经历在简历上一般怎么写比较出彩?

//AN_Xml:

实习经历的描述一定要避免空谈,尽量列举出你在实习期间取得的成就和具体贡献,使用具体的数据和指标来量化你的工作成果。

//AN_Xml:

示例(这里假设项目细节放在实习经历这里介绍,你也可以选择将实习经历参与的项目放到项目经历中):

//AN_Xml:
    //AN_Xml:
  1. 负责订单模块核心流程开发,实现订单状态的精确流转,并保障与库存、支付等模块的数据一致性。
  2. //AN_Xml:
  3. 负责行为风控黑名单看板的开发,支持查看拉黑用户、批量拉黑以及取消拉黑。
  4. //AN_Xml:
  5. 基于 Redisson + AOP 封装限流组件,实现对核心接口(如付费、课程搜索)的限流,有效防止恶意请求冲击。
  6. //AN_Xml:
  7. 优化用户统计模块性能,利用 CompletableFuture 并行加载多维度数据(如用户增长、课程活跃度),,平均相应时间从 3.5s 降低到 1s。
  8. //AN_Xml:
  9. 封装通用数据脱敏组件,通过自定义 Jackson 注解实现对手机号、邮箱等敏感信息的自动、无侵入式脱敏。
  10. //AN_Xml:
  11. 优化文件上传模块,基于 MinIO 实现了文件的分片上传、断点续传以及极速秒传功能。
  12. //AN_Xml:
  13. 排查并解决扣费模块由于扣费父任务和反作弊子任务使用同一个线程池导致的死锁问题,通过线程池隔离策略根除该隐患。
  14. //AN_Xml:
  15. 实习期间独立负责 7 个功能需求与 3 个线上问题修复,代码均一次性通过评审与测试。
  16. //AN_Xml:
//AN_Xml:

下面是星球一位球友分享的实习经历介绍,整体写的还是非常不错的:

//AN_Xml:

实习经历模板

//AN_Xml:

📌关于实习经历这块再多提一点:很多同学实习期间可能接触不到什么实际的开发任务,大部分时间可能都是在熟悉和维护项目。

//AN_Xml:

对于这种情况,应对思路是一套组合拳:首先,你肯定是要和 mentor 沟通继续争取做一些有价值的工作,这样你的实习经历才更有价值,简历上自然就能够有东西可写。记得找一个 mentor 不那么忙的时候沟通,放低姿态,真诚一些,表明自己现有的工作已经认真完成,想要承担更多责任的意愿。其次,不管是否能够争取到这种机会,你都要自己有意识地寻找项目中适合自己研究的功能点(比如同组其他实习生干的活),进行深度挖掘。重点关注以下几个方面:

//AN_Xml:
    //AN_Xml:
  1. 这个功能是干嘛的? 它解决了什么业务痛点?给哪个业务方用的?整个流程是怎样的?
  2. //AN_Xml:
  3. 它是怎么实现的? 用了哪些关键技术、框架或者设计模式?核心代码的逻辑是怎样的?
  4. //AN_Xml:
  5. 为什么要这么设计? 当初设计的时候有没有别的方案?现在这个方案好在哪,又有什么潜在的坑?如果让你来做,你会怎么设计?
  6. //AN_Xml:
//AN_Xml:

只要你把具体的功能点彻底搞懂,那就可以在简历上合理包装成自己的成果。除了功能点开发之外,也可以包装一些合适的问题排查解决经历,这样能够体现你解决问题的能力。 面试时也不用太担心自己“露馅”,只要你选择的内容不属于那些显然不会交给实习生完成的高难度任务,并且能清晰地讲明白,就不会有问题。

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Java 学习路线(最新版,4w+字) //AN_Xml: https://javaguide.cn/interview-preparation/java-roadmap.html //AN_Xml: https://javaguide.cn/interview-preparation/java-roadmap.html //AN_Xml: Java 学习路线(最新版,4w+字) //AN_Xml: Java学习路线最新版:结合当下 Java 后端招聘要求,提供从基础到进阶的系统学习路径与资料建议,覆盖Java核心、数据库、缓存、中间件、框架与面试重点,帮助高效规划与提速上岸。 //AN_Xml: 面试准备 //AN_Xml: Fri, 28 Apr 2023 11:27:56 GMT //AN_Xml: JavaGuide官方知识星球

//AN_Xml:
//AN_Xml:

重要说明

//AN_Xml:

本学习路线保持年度系统性修订,严格同步 Java 技术生态与招聘市场的最新动态,确保内容时效性与前瞻性

//AN_Xml:
//AN_Xml:

历时一个月精心打磨,笔者基于当下 Java 后端开发岗位招聘的最新要求,对既有学习路线进行了全面升级。本次升级涵盖技术栈增删、学习路径优化、配套学习资源更新等维度,力争构建出更符合 Java 开发者成长曲线的知识体系。

//AN_Xml:

亮色板概览:

//AN_Xml:

Java 学习路线 PDF 概览 - 亮色板

//AN_Xml:

暗色板概览:

//AN_Xml:

Java 学习路线 PDF 概览 - 暗色版

//AN_Xml:

这可能是你见过的最用心、最全面的 Java 后端学习路线。这份学习路线共包含 4w+ 字,但你完全不用担心内容过多而学不完。我会根据学习难度,划分出适合找小厂工作必学的内容,以及适合逐步提升 Java 后端开发能力的学习路径。

//AN_Xml:

Java 学习路线图

//AN_Xml:

对于初学者,你可以按照这篇文章推荐的学习路线和资料进行系统性的学习;对于有经验的开发者,你可以根据这篇文章更一步地深入学习 Java 后端开发,提升个人竞争力。

//AN_Xml:

在看这份学习路线的过程中,建议搭配 Java 面试重点总结(重要),可以让你在学习过程中更有目的性。

//AN_Xml:

由于这份学习路线内容太多,因此我将其整理成了 PDF 版本(共 55 页),方便大家阅读。这份 PDF 有黑夜和白天两种阅读版本,满足大家的不同需求。

//AN_Xml:

这份学习路线的获取方法很简单:直接在公众号「JavaGuide」后台回复“路线”即可获取。

//AN_Xml:

JavaGuide 官方公众号

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 《后端面试高频系统设计&场景题》 //AN_Xml: https://javaguide.cn/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.html //AN_Xml: https://javaguide.cn/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.html //AN_Xml: 《后端面试高频系统设计&场景题》 //AN_Xml: 后端面试高频系统设计与场景题专栏,涵盖秒杀系统、短链系统、海量数据处理等30+道经典面试题解析。 //AN_Xml: 知识星球 //AN_Xml: Fri, 28 Apr 2023 09:48:34 GMT //AN_Xml: 介绍 //AN_Xml:

《后端面试高频系统设计&场景题》 是我的知识星球的一个内部小册,系统性地总结了后端面试中高频出现的系统设计案例和场景题。

//AN_Xml:

为什么你需要这份小册?

//AN_Xml:

近年来,国内技术面试"越来越卷"。越来越多的公司(阿里、美团、字节、腾讯等)开始在面试中考察 系统设计场景问题,以此来更全面地考察求职者的综合能力——不论是校招还是社招。

//AN_Xml:
//AN_Xml:

很多同学八股文背得滚瓜烂熟,但一遇到"如何设计一个秒杀系统?"这类开放性问题就懵了。

//AN_Xml:
//AN_Xml:

系统设计和场景题的考察特点

//AN_Xml:
    //AN_Xml:
  • ✅ 没有标准答案,重点考察思维过程和架构能力
  • //AN_Xml:
  • ✅ 考察对高并发、高可用、分布式等技术的综合运用
  • //AN_Xml:
  • ✅ 考察解决实际问题的能力和工程经验
  • //AN_Xml:
  • ⚠️ 正常面试不会全是场景题,一般会穿插 1-2 道来考察你
  • //AN_Xml:
//AN_Xml:

于是,《后端面试高频系统设计&场景题》 小册就诞生了!

//AN_Xml:

这份小册能带给你什么?

//AN_Xml:

1. 面试加分项

//AN_Xml:

系统设计和场景题回答得好,面试官会对你印象非常好!这类问题稍微准备就能脱颖而出。

//AN_Xml:

2. 提升系统设计思维

//AN_Xml:

即使不是准备面试,这份小册也能帮助你建立系统设计的思维框架,提升解决实际问题的能力。

//AN_Xml:

3. 实战落地参考

//AN_Xml:

涉及到的很多案例都可以直接用到自己的项目上,比如:

//AN_Xml:
    //AN_Xml:
  • 第三方授权登录(微信/QQ 登录)
  • //AN_Xml:
  • Redis 实现延时任务的正确方式
  • //AN_Xml:
  • 动态线程池的设计与实现
  • //AN_Xml:
  • 分布式锁的多种实现方案
  • //AN_Xml:
//AN_Xml:

内容概览

//AN_Xml:

📐 系统设计案例

//AN_Xml:

| 主题 | 核心知识点 |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: PriorityQueue 源码分析(付费) //AN_Xml: https://javaguide.cn/java/collection/priorityqueue-source-code.html //AN_Xml: https://javaguide.cn/java/collection/priorityqueue-source-code.html //AN_Xml: PriorityQueue 源码分析(付费) //AN_Xml: PriorityQueue源码深度解析:详解基于二叉堆的优先队列实现、堆化siftUp/siftDown操作、Comparator自定义排序、动态扩容机制。 //AN_Xml: Java //AN_Xml: Fri, 28 Apr 2023 09:48:34 GMT //AN_Xml: PriorityQueue 源码分析 为我的知识星球(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了《Java 必读源码系列》中。

//AN_Xml:

PriorityQueue 源码分析

//AN_Xml:

《Java 必读源码系列》(点击链接即可查看详细介绍)的部分内容展示如下。

//AN_Xml:

《Java 必读源码系列》

//AN_Xml:

为了帮助更多同学准备 Java 面试以及学习 Java ,我创建了一个纯粹的Java 面试知识星球。虽然收费只有培训班/训练营的百分之一,但是知识星球里的内容质量更高,提供的服务也更全面,非常适合准备 Java 面试和学习 Java 的同学。

//AN_Xml:

欢迎准备 Java 面试以及学习 Java 的同学加入我的 知识星球,干货非常多,学习氛围也很不错!收费虽然是白菜价,但星球里的内容或许比你参加上万的培训班质量还要高。

//AN_Xml:

下面是星球提供的部分服务(点击下方图片即可获取知识星球的详细介绍):

//AN_Xml:

星球服务

//AN_Xml:

我有自己的原则,不割韭菜,用心做内容,真心希望帮助到你!

//AN_Xml:

如果你感兴趣的话,不妨花 3 分钟左右看看星球的详细介绍:JavaGuide 知识星球详细介绍

//AN_Xml:

这里再送一个 30 元的星球专属优惠券,数量有限(价格即将上调。老用户续费半价 ,微信扫码即可续费)!

//AN_Xml:

知识星球30元优惠卷

//AN_Xml:

进入星球之后,记得查看 星球使用指南 (一定要看!!!) 和 星球优质主题汇总

//AN_Xml:

无任何套路,无任何潜在收费项。用心做内容,不割韭菜!

//AN_Xml:

不过, 一定要确定需要再进 。并且, 三天之内觉得内容不满意可以全额退款

//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Spring Boot核心源码解读(付费) //AN_Xml: https://javaguide.cn/system-design/framework/spring/springboot-source-code.html //AN_Xml: https://javaguide.cn/system-design/framework/spring/springboot-source-code.html //AN_Xml: Spring Boot核心源码解读(付费) //AN_Xml: Spring Boot核心源码深度解读,涵盖启动流程、自动配置机制、条件注解及SpringApplication源码分析。 //AN_Xml: 框架 //AN_Xml: Fri, 28 Apr 2023 09:48:34 GMT //AN_Xml: Spring Boot 核心源码解读 为我的知识星球(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了《Java 必读源码系列》中。

//AN_Xml:

Spring Boot核心源码解读

//AN_Xml:

《Java 必读源码系列》(点击链接即可查看详细介绍)的部分内容展示如下。

//AN_Xml:

《Java 必读源码系列》

//AN_Xml:

为了帮助更多同学准备 Java 面试以及学习 Java ,我创建了一个纯粹的Java 面试知识星球。虽然收费只有培训班/训练营的百分之一,但是知识星球里的内容质量更高,提供的服务也更全面,非常适合准备 Java 面试和学习 Java 的同学。

//AN_Xml:

欢迎准备 Java 面试以及学习 Java 的同学加入我的 知识星球,干货非常多,学习氛围也很不错!收费虽然是白菜价,但星球里的内容或许比你参加上万的培训班质量还要高。

//AN_Xml:

下面是星球提供的部分服务(点击下方图片即可获取知识星球的详细介绍):

//AN_Xml:

星球服务

//AN_Xml:

我有自己的原则,不割韭菜,用心做内容,真心希望帮助到你!

//AN_Xml:

如果你感兴趣的话,不妨花 3 分钟左右看看星球的详细介绍:JavaGuide 知识星球详细介绍

//AN_Xml:

这里再送一个 30 元的星球专属优惠券,数量有限(价格即将上调。老用户续费半价 ,微信扫码即可续费)!

//AN_Xml:

知识星球30元优惠卷

//AN_Xml:

进入星球之后,记得查看 星球使用指南 (一定要看!!!) 和 星球优质主题汇总

//AN_Xml:

无任何套路,无任何潜在收费项。用心做内容,不割韭菜!

//AN_Xml:

不过, 一定要确定需要再进 。并且, 三天之内觉得内容不满意可以全额退款

//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Java 后端面试通关计划(涵盖后端通用体系) //AN_Xml: https://javaguide.cn/interview-preparation/backend-interview-plan.html //AN_Xml: https://javaguide.cn/interview-preparation/backend-interview-plan.html //AN_Xml: Java 后端面试通关计划(涵盖后端通用体系) //AN_Xml: Java 后端面试通关计划:严格按照面试考察真实优先级编排,涵盖项目经历、Java核心、MySQL/Redis、框架、系统设计、计算机基础、分布式与JVM,适合校招/社招准备。 //AN_Xml: 面试准备 //AN_Xml: Sat, 22 Apr 2023 02:34:42 GMT //AN_Xml: 本计划严格按照面试考察的真实优先级进行编排,顺序为:
//AN_Xml:「 项目经历与简历深挖 → Java核心/MySQL/Redis → 框架应用 → 系统设计与场景题 → 计算机基础 → 分布式/高并发 → JVM」

//AN_Xml:

每一阶段都对应了本站具体的精选文章,方便你按图索骥,逐个击破。

//AN_Xml:
    //AN_Xml:
  • 建议总周期:4~8 周(请根据目标公司是中小厂还是大厂,以及自身的脱产时间灵活压缩或拉长)。
  • //AN_Xml:
  • 适用人群:准备秋招/春招的计算机专业学生,以及 0-5 年经验准备跳槽的 Java 开发者。
  • //AN_Xml:
  • 面试突击:下文中推荐的技术文章以 JavaGuide 为主,非常全面且详细,如果突击面试,可以选择阅读 JavaGuide 面试突击版 中对应的文章。
  • //AN_Xml:
//AN_Xml:

计划总览

//AN_Xml:

| 阶段 | 建议时长 | 核心产出 | 自测标准 |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Java后端面试重点总结 //AN_Xml: https://javaguide.cn/interview-preparation/key-points-of-interview.html //AN_Xml: https://javaguide.cn/interview-preparation/key-points-of-interview.html //AN_Xml: Java后端面试重点总结 //AN_Xml: Java后端面试重点总结:梳理校招/社招高频考点与复习优先级,覆盖Java基础、集合、并发、MySQL、Redis、Spring/Spring Boot、JVM与项目经验准备,帮你抓重点高效备战。 //AN_Xml: 面试准备 //AN_Xml: Sat, 22 Apr 2023 02:34:42 GMT //AN_Xml: JavaGuide官方知识星球

//AN_Xml:
//AN_Xml:

友情提示

//AN_Xml:

本文节选自 《Java 面试指北》。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。

//AN_Xml:
//AN_Xml:

Java 后端面试哪些知识点是重点?

//AN_Xml:

准备面试的时候,具体哪些知识点是重点呢?如何把握重点?

//AN_Xml:

先看下面这张全局图(后续会详细解读):

//AN_Xml:

Java 后端面试重点

//AN_Xml:

给你几点靠谱的建议:

//AN_Xml:
    //AN_Xml:
  1. Java 基础、集合、并发、MySQL、Redis 、Spring、Spring Boot 这些 Java 后端开发必备的知识点(MySQL + Redis >= Java > Spring + Spring Boot)。大厂以及中小厂的面试问的比较多的就是这些知识点。Spring 和 Spring Boot 这俩框架类的知识点相对前面的知识点来说重要性要稍低一些,但一般面试也会问一些,尤其是中小厂。并发知识一般中大厂提问更多也更难,尤其是大厂喜欢深挖底层,很容易把人问倒。计算机基础相关的内容会在下面提到。
  2. //AN_Xml:
  3. 你的项目经历涉及到的知识点是重中之重,有水平的面试官都是会根据你的项目经历来问的。举个例子,你的项目经历使用了 Redis 来做限流,那 Redis 相关的八股文(比如 Redis 常见数据结构)以及限流相关的八股文(比如常见的限流算法)你就应该多花更多心思来搞懂吃透!你把项目经历上的知识点吃透之后,再把你简历上哪些写熟练掌握的技术给吃透,最后再去花时间准备其他知识点。
  4. //AN_Xml:
  5. 针对自身找工作的需求,你又可以适当地调整复习的重点。像中小厂一般问计算机基础比较少一些,有些大厂比如字节比较重视计算机基础尤其是算法。这样的话,如果你的目标是中小厂的话,计算机基础就准备面试来说不是那么重要了。如果复习时间不够的话,可以暂时先放放,腾出时间给其他重要的知识点。
  6. //AN_Xml:
  7. 一般校招的面试不会强制要求你会分布式/微服务、高并发的知识(不排除个别岗位有这方面的硬性要求),所以到底要不要掌握还是要看你个人当前的实际情况。如果你会这方面的知识的话,对面试相对来说还是会更有利一些(想要让项目经历有亮点,还是得会一些性能优化的知识。性能优化的知识这也算是高并发知识的一个小分支了)。如果你的技能介绍或者项目经历涉及到分布式/微服务、高并发的知识,那建议你尽量也要抽时间去认真准备一下,面试中很可能会被问到,尤其是项目经历用到的时候。不过,也还是主要准备写在简历上的那些知识点就好。
  8. //AN_Xml:
  9. JVM 相关的知识点,一般是大厂(例如美团、阿里)和一些不错的中厂(例如携程、顺丰、招银网络)才会问到,面试国企、差一点的中厂和小厂就没必要准备了。JVM 面试中比较常问的是 Java 内存区域JVM 垃圾回收类加载器和双亲委派模型 以及 JVM 调优和问题排查(我之前分享过一些常见的线上问题案例,里面就有 JVM 相关的)。
  10. //AN_Xml:
  11. 不同的大厂面试侧重点也会不同。比如说你要去阿里这种公司的话,项目和八股文就是重点,阿里笔试一般会有代码题,进入面试后就很少问代码题了,但是对原理性的问题问的比较深,经常会问一些你对技术的思考。再比如说你要面试字节这种公司,那计算机基础,尤其是算法是重点,字节的面试十分注重代码功底,有时候开始面试就会直接甩给你一道代码题,写出来再谈别的。也会问面试八股文,以及项目,不过,相对来说要少很多。
  12. //AN_Xml:
  13. 多去找一些面经看看,尤其你目标公司或者类似公司对应岗位的面经。这样可以实现针对性的复习,还能顺便自测一波,检查一下自己的掌握情况。
  14. //AN_Xml:
//AN_Xml:

看似 Java 后端八股文很多,实际把复习范围一缩小,重要的东西就是那些。考虑到时间问题,你不可能连一些比较冷门的知识点也给准备了。这没必要,主要精力先放在那些重要的知识点即可。

//AN_Xml:

如何更高效地准备八股文?

//AN_Xml: //AN_Xml:

对于技术八股文来说,尽量不要死记硬背,这种方式非常枯燥且对自身能力提升有限!但是!想要一点不背是不太现实的,只是说要结合实际应用场景和实战来理解记忆。

//AN_Xml:

我一直觉得面试八股文最好是和实际应用场景和实战相结合。很多同学现在的方向都错了,上来就是直接背八股文,硬生生学成了文科,那当然无趣了。

//AN_Xml:

举个例子:你的项目中需要用到 Redis 来做缓存,你对照着官网简单了解并实践了简单使用 Redis 之后,你去看了 Redis 对应的八股文。你发现 Redis 可以用来做限流、分布式锁,于是你去在项目中实践了一下并掌握了对应的八股文。紧接着,你又发现 Redis 内存不够用的情况下,还能使用 Redis Cluster 来解决,于是你就又去实践了一下并掌握了对应的八股文。

//AN_Xml:

一定要记住你的主要目标是理解和记关键词,而不是像背课文一样一字一句地记下来,这样毫无意义!效率最低,对自身帮助也最小!

//AN_Xml:

还要注意适当“投机取巧”,不要单纯死记八股,有些技术方案的实现有很多种,例如分布式 ID、分布式锁、幂等设计,想要完全记住所有方案不太现实,你就重点记忆你项目的实现方案以及选择该种实现方案的原因就好了。当然,其他方案还是建议你简单了解一下,不然也没办法和你选择的方案进行对比。

//AN_Xml:

想要检测自己是否搞懂或者加深印象,记录博客或者用自己的理解把对应的知识点讲给别人听也是一个不错的选择。

//AN_Xml:

另外,准备八股文的过程中,强烈建议你花个几个小时去根据你的简历(主要是项目经历部分)思考一下哪些地方可能被深挖,然后把你自己的思考以面试问题的形式体现出来。面试之后,你还要根据当下的面试情况复盘一波,对之前自己整理的面试问题进行完善补充。这个过程对于个人进一步熟悉自己的简历(尤其是项目经历)部分,非常非常有用。这些问题你也一定要多花一些时间搞懂吃透,能够流畅地表达出来。面试问题可以参考 Java 面试常见问题总结(2024 最新版),记得根据自己项目经历去深入拓展即可!

//AN_Xml:

最后,准备技术面试的同学一定要定期复习(自测的方式非常好),不然确实会遗忘的。

//AN_Xml:

详细面试准备计划(后端通用)

//AN_Xml:

Java 后端面试重点和详细准备计划

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 计算机网络常见面试题总结(下) //AN_Xml: https://javaguide.cn/cs-basics/network/other-network-questions2.html //AN_Xml: https://javaguide.cn/cs-basics/network/other-network-questions2.html //AN_Xml: 计算机网络常见面试题总结(下) //AN_Xml: 最新计算机网络高频面试题总结(下):TCP/UDP深度对比、三次握手四次挥手、HTTP/3 QUIC优化、IPv6优势、NAT/ARP详解,附表格+⭐️重点标注,一文掌握传输层&网络层核心考点,快速通关后端技术面试! //AN_Xml: 计算机基础 //AN_Xml: Thu, 13 Apr 2023 11:00:22 GMT //AN_Xml: 《SpringAI 智能面试平台+RAG 知识库》

//AN_Xml:

下篇主要是传输层和网络层相关的内容。

//AN_Xml:

TCP 与 UDP

//AN_Xml:

⭐️TCP 与 UDP 的区别(重要)

//AN_Xml:
    //AN_Xml:
  1. 是否面向连接: //AN_Xml:
      //AN_Xml:
    • TCP 是面向连接的。在传输数据之前,必须先通过“三次握手”建立连接;数据传输完成后,还需要通过“四次挥手”来释放连接。这保证了双方都准备好通信。
    • //AN_Xml:
    • UDP 是无连接的。发送数据前不需要建立任何连接,直接把数据包(数据报)扔出去。
    • //AN_Xml:
    //AN_Xml:
  2. //AN_Xml:
  3. 是否是可靠传输: //AN_Xml:
      //AN_Xml:
    • TCP 提供可靠的数据传输服务。它通过序列号、确认应答 (ACK)、超时重传、流量控制、拥塞控制等一系列机制,来确保数据能够无差错、不丢失、不重复且按顺序地到达目的地。
    • //AN_Xml:
    • UDP 提供不可靠的传输。它尽最大努力交付 (best-effort delivery),但不保证数据一定能到达,也不保证到达的顺序,更不会自动重传。收到报文后,接收方也不会主动发确认。
    • //AN_Xml:
    //AN_Xml:
  4. //AN_Xml:
  5. 是否有状态: //AN_Xml:
      //AN_Xml:
    • TCP 是有状态的。因为要保证可靠性,TCP 需要在连接的两端维护连接状态信息,比如序列号、窗口大小、哪些数据发出去了、哪些收到了确认等。
    • //AN_Xml:
    • UDP 是无状态的。它不维护连接状态,发送方发出数据后就不再关心它是否到达以及如何到达,因此开销更小(这很“渣男”!)。
    • //AN_Xml:
    //AN_Xml:
  6. //AN_Xml:
  7. 传输效率: //AN_Xml:
      //AN_Xml:
    • TCP 因为需要建立连接、发送确认、处理重传等,其开销较大,传输效率相对较低。
    • //AN_Xml:
    • UDP 结构简单,没有复杂的控制机制,开销小,传输效率更高,速度更快。
    • //AN_Xml:
    //AN_Xml:
  8. //AN_Xml:
  9. 传输形式: //AN_Xml:
      //AN_Xml:
    • TCP 是面向字节流 (Byte Stream) 的。它将应用程序交付的数据视为一连串无结构的字节流,可能会对数据进行拆分或合并。
    • //AN_Xml:
    • UDP 是面向报文 (Message Oriented) 的。应用程序交给 UDP 多大的数据块,UDP 就照样发送,既不拆分也不合并,保留了应用程序消息的边界。
    • //AN_Xml:
    //AN_Xml:
  10. //AN_Xml:
  11. 首部开销: //AN_Xml:
      //AN_Xml:
    • TCP 的头部至少需要 20 字节,如果包含选项字段,最多可达 60 字节。
    • //AN_Xml:
    • UDP 的头部非常简单,固定只有 8 字节。
    • //AN_Xml:
    //AN_Xml:
  12. //AN_Xml:
  13. 是否提供广播或多播服务: //AN_Xml:
      //AN_Xml:
    • TCP 只支持点对点 (Point-to-Point) 的单播通信。
    • //AN_Xml:
    • UDP 支持一对一 (单播)、一对多 (多播/Multicast) 和一对所有 (广播/Broadcast) 的通信方式。
    • //AN_Xml:
    //AN_Xml:
  14. //AN_Xml:
  15. ……
  16. //AN_Xml:
//AN_Xml:

为了更直观地对比,可以看下面这个表格:

//AN_Xml:

| 特性 | TCP | UDP |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: DNS 域名系统详解(应用层) //AN_Xml: https://javaguide.cn/cs-basics/network/dns.html //AN_Xml: https://javaguide.cn/cs-basics/network/dns.html //AN_Xml: DNS 域名系统详解(应用层) //AN_Xml: 详解 DNS 的层次结构与解析流程,覆盖递归/迭代、缓存与权威服务器,明确应用层端口与性能优化要点。 //AN_Xml: 计算机基础 //AN_Xml: Tue, 11 Apr 2023 08:54:54 GMT //AN_Xml: DNS(Domain Name System)域名管理系统,是当用户使用浏览器访问网址之后,使用的第一个重要协议。DNS 要解决的是域名和 IP 地址的映射问题

//AN_Xml:

DNS:域名系统

//AN_Xml:

在实际使用中,有一种情况下,浏览器是可以不必动用 DNS 就可以获知域名和 IP 地址的映射的。浏览器在本地会维护一个hosts列表,一般来说浏览器要先查看要访问的域名是否在hosts列表中,如果有的话,直接提取对应的 IP 地址记录,就好了。如果本地hosts列表内没有域名-IP 对应记录的话,那么 DNS 就闪亮登场了。

//AN_Xml:

目前 DNS 的设计采用的是分布式、层次数据库结构,DNS 是应用层协议,通常基于 UDP 协议,端口为 53。当响应数据超过 UDP 报文长度限制(512 字节,EDNS0 可扩展至更大)或进行区域传送(Zone Transfer)时,会改用 TCP 协议以保证数据完整性。

//AN_Xml:

TCP/IP 各层协议概览

//AN_Xml:

DNS 服务器

//AN_Xml:

DNS 服务器自底向上可以依次分为以下几个层级(所有 DNS 服务器都属于以下四个类别之一):

//AN_Xml:
    //AN_Xml:
  • 根 DNS 服务器。根 DNS 服务器提供 TLD 服务器的 IP 地址。目前世界上只有 13 组根服务器,我国境内目前仍没有根服务器。
  • //AN_Xml:
  • 顶级域 DNS 服务器(TLD 服务器)。顶级域是指域名的后缀,如comorgnetedu等。国家也有自己的顶级域,如ukfrca。TLD 服务器提供了权威 DNS 服务器的 IP 地址。
  • //AN_Xml:
  • 权威 DNS 服务器。在因特网上具有公共可访问主机的每个组织机构必须提供公共可访问的 DNS 记录,这些记录将这些主机的名字映射为 IP 地址。
  • //AN_Xml:
  • 本地 DNS 服务器。每个 ISP(互联网服务提供商)都有一个自己的本地 DNS 服务器。当主机发出 DNS 请求时,该请求被发往本地 DNS 服务器,它起着代理的作用,并将该请求转发到 DNS 层次结构中。严格说来,不属于 DNS 层级结构。
  • //AN_Xml:
//AN_Xml:

世界上真的只有 13 台根服务器吗? 这是一个流传已久的技术误解。如果你在网上搜索,仍能看到许多陈旧文章宣称“全球仅有 13 台根服务器,且全部由美国控制”。

//AN_Xml:

事实并非如此。

//AN_Xml:

最初在设计 DNS(域名系统)架构时,受限于早期 IPv4 数据包的大小限制(UDP 报文需控制在 512 字节以内),预留给根服务器地址的空间确实只够容纳 13 个 IP 地址,每个 IP 地址对应一个不同的根 DNS 服务器。这 13 个地址分别被命名为 a.root-servers.netm.root-servers.net

//AN_Xml:

虽然逻辑上只有 13 个 IP 地址,但随着互联网规模的爆发,物理上的“单一服务器”早已无法承载全球的查询压力。为了提升 DNS 的可靠性、安全性和响应速度,技术人员引入了 IP 任播(Anycast) 技术。

//AN_Xml:

通过任播技术,每一个逻辑 IP 地址背后都可以对应成百上千台分布在全球各地的物理服务器。当你发起查询请求时,互联网路由协议(BGP)会自动将请求引导至地理位置或网络路径上离你最近的那台物理实例。

//AN_Xml:

截止到 2023 年底,全球根服务器物理实例总数已超过 1700 台。根据 Root-Servers.org 的最新实时监测数据,到 2026 年,全球根服务器物理实例已突破 1900+ 台,并正向 2000 台大关迈进。

//AN_Xml:

Root-Servers.org

//AN_Xml:

DNS 工作流程

//AN_Xml:

以下图为例,介绍 DNS 的查询解析过程。DNS 的查询解析过程分为两种模式:

//AN_Xml:
    //AN_Xml:
  • 迭代
  • //AN_Xml:
  • 递归
  • //AN_Xml:
//AN_Xml:

下图是实践中常采用的方式,从请求主机到本地 DNS 服务器的查询是递归的,其余的查询时迭代的。

//AN_Xml:

//AN_Xml:

现在,主机cis.poly.edu想知道gaia.cs.umass.edu的 IP 地址。假设主机cis.poly.edu的本地 DNS 服务器为dns.poly.edu,并且gaia.cs.umass.edu的权威 DNS 服务器为dns.cs.umass.edu

//AN_Xml:
    //AN_Xml:
  1. 首先,主机cis.poly.edu向本地 DNS 服务器dns.poly.edu发送一个 DNS 请求,该查询报文包含被转换的域名gaia.cs.umass.edu
  2. //AN_Xml:
  3. 本地 DNS 服务器dns.poly.edu检查本机缓存,发现并无记录,也不知道gaia.cs.umass.edu的 IP 地址该在何处,不得不向根服务器发送请求。
  4. //AN_Xml:
  5. 根服务器注意到请求报文中含有edu顶级域,因此告诉本地 DNS,你可以向edu的 TLD DNS 发送请求,因为目标域名的 IP 地址很可能在那里。
  6. //AN_Xml:
  7. 本地 DNS 获取到了edu的 TLD DNS 服务器地址,向其发送请求,询问gaia.cs.umass.edu的 IP 地址。
  8. //AN_Xml:
  9. edu的 TLD DNS 服务器仍不清楚请求域名的 IP 地址,但是它注意到该域名有umass.edu前缀,因此返回告知本地 DNS,umass.edu的权威服务器可能记录了目标域名的 IP 地址。
  10. //AN_Xml:
  11. 这一次,本地 DNS 将请求发送给权威 DNS 服务器dns.cs.umass.edu
  12. //AN_Xml:
  13. 终于,由于gaia.cs.umass.edu向权威 DNS 服务器备案过,在这里有它的 IP 地址记录,权威 DNS 成功地将 IP 地址返回给本地 DNS。
  14. //AN_Xml:
  15. 最后,本地 DNS 获取到了目标域名的 IP 地址,将其返回给请求主机。
  16. //AN_Xml:
//AN_Xml:

除了迭代式查询,还有一种递归式查询如下图,具体过程和上述类似,只是顺序有所不同。

//AN_Xml:

//AN_Xml:

另外,DNS 的缓存位于本地 DNS 服务器。由于全世界的根服务器甚少,只有 600 多台,分为 13 组,且顶级域的数量也在一个可数的范围内,因此本地 DNS 通常已经缓存了很多 TLD DNS 服务器,所以在实际查找过程中,无需访问根服务器。根服务器通常是被跳过的,不请求的。这样可以提高 DNS 查询的效率和速度,减少对根服务器和 TLD 服务器的负担。

//AN_Xml:

DNS 报文格式

//AN_Xml:

DNS 的报文格式如下图所示:

//AN_Xml:

//AN_Xml:

DNS 报文分为查询和回答报文,两种形式的报文结构相同。

//AN_Xml:
    //AN_Xml:
  • 标识符。16 比特,用于标识该查询。这个标识符会被复制到对查询的回答报文中,以便让客户用它来匹配发送的请求和接收到的回答。
  • //AN_Xml:
  • 标志。1 比特的”查询/回答“标识位,0表示查询报文,1表示回答报文;1 比特的”权威的“标志位(当某 DNS 服务器是所请求名字的权威 DNS 服务器时,且是回答报文,使用”权威的“标志);1 比特的”希望递归“标志位,显式地要求执行递归查询;1 比特的”递归可用“标志位,用于回答报文中,表示 DNS 服务器支持递归查询。
  • //AN_Xml:
  • 问题数、回答 RR 数、权威 RR 数、附加 RR 数。分别指示了后面 4 类数据区域出现的数量。
  • //AN_Xml:
  • 问题区域。包含正在被查询的主机名字,以及正被询问的问题类型。
  • //AN_Xml:
  • 回答区域。包含了对最初请求的名字的资源记录。在回答报文的回答区域中可以包含多条 RR,因此一个主机名能够有多个 IP 地址。
  • //AN_Xml:
  • 权威区域。包含了其他权威服务器的记录。
  • //AN_Xml:
  • 附加区域。包含了其他有帮助的记录。
  • //AN_Xml:
//AN_Xml:

DNS 记录

//AN_Xml:

DNS 服务器在响应查询时,需要查询自己的数据库,数据库中的条目被称为 资源记录(Resource Record,RR) 。RR 提供了主机名到 IP 地址的映射。RR 是一个包含了Name, Value, Type, TTL四个字段的四元组。

//AN_Xml:

//AN_Xml:

TTL是该记录的生存时间,它决定了资源记录应当从缓存中删除的时间。

//AN_Xml:

NameValue字段的取值取决于Type

//AN_Xml:

//AN_Xml:
    //AN_Xml:
  • 如果Type=A,则Name是主机名信息,Value 是该主机名对应的 IP 地址。这样的 RR 记录了一条主机名到 IP 地址的映射。
  • //AN_Xml:
  • 如果 Type=AAAA (与 A 记录非常相似),唯一的区别是 A 记录使用的是 IPv4,而 AAAA 记录使用的是 IPv6。
  • //AN_Xml:
  • 如果Type=CNAME (Canonical Name Record,真实名称记录) ,则Value是别名为Name的主机对应的规范主机名。Value值才是规范主机名。CNAME 记录将一个主机名映射到另一个主机名。CNAME 记录用于为现有的 A 记录创建别名。下文有示例。
  • //AN_Xml:
  • 如果Type=NS,则Name是个域,而Value是个知道如何获得该域中主机 IP 地址的权威 DNS 服务器的主机名。通常这样的 RR 是由 TLD 服务器发布的。
  • //AN_Xml:
  • 如果Type=MX ,则Value是个别名为Name的邮件服务器的规范主机名。既然有了 MX 记录,那么邮件服务器可以和其他服务器使用相同的别名。为了获得邮件服务器的规范主机名,需要请求 MX 记录;为了获得其他服务器的规范主机名,需要请求 CNAME 记录。
  • //AN_Xml:
//AN_Xml:

CNAME记录总是指向另一则域名,而非 IP 地址。假设有下述 DNS zone:

//AN_Xml:
NAME                    TYPE   VALUE
//AN_Xml:
]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 坚持写技术博客六年了! //AN_Xml: https://javaguide.cn/about-the-author/writing-technology-blog-six-years.html //AN_Xml: https://javaguide.cn/about-the-author/writing-technology-blog-six-years.html //AN_Xml: 坚持写技术博客六年了! //AN_Xml: 坚持写技术博客六年的心得分享,写博客的好处、如何坚持下去、写哪些方向的博客、实用写作技巧等经验总结。 //AN_Xml: 走近作者 //AN_Xml: Sun, 09 Apr 2023 12:42:14 GMT //AN_Xml: 坚持写技术博客已经有六年了,也算是一个小小的里程碑了。

//AN_Xml:

一开始,我写技术博客就是简单地总结自己课堂上学习的课程比如网络、操作系统。渐渐地,我开始撰写一些更为系统化的知识点详解和面试常见问题总结。

//AN_Xml:

JavaGuide 首页

//AN_Xml:

许多人都想写技术博客,但却不清楚这对他们有何好处。有些人开始写技术博客,却不知道如何坚持下去,也不知道该写些什么。这篇文章我会认真聊聊我对记录技术博客的一些看法和心得,或许可以帮助你解决这些问题。

//AN_Xml:

写技术博客有哪些好处?

//AN_Xml:

学习效果更好,加深知识点的认识

//AN_Xml:

费曼学习法 大家应该已经比较清楚了,这是一个经过实践证明非常有效的学习方式。费曼学习法的命名源自 Richard Feynman,这位物理学家曾获得过诺贝尔物理学奖,也曾参与过曼哈顿计划。

//AN_Xml:

所谓费曼学习法,就是当你学习了一个新知识之后,想象自己是一个老师:用最简单、最浅显直白的话复述、表达复杂深奥的知识,最好不要使用行业术语,让非行业内的人也能听懂。为了达到这种效果,最好想象你是在给一个 80 多岁或 8 岁的小孩子上课,甚至他们都能听懂。

//AN_Xml:

教授别人学习效果最好

//AN_Xml:

看书、看视频这类都属于是被动学习,学习效果比较差。费曼学习方法属于主动学习,学习效果非常好。

//AN_Xml:

写技术博客实际就是教别人的一种方式。 不过,记录技术博客的时候是可以有专业术语(除非你的文章群体是非技术人员),只是你需要用自己的话表述出来,尽量让别人一看就懂。切忌照搬书籍或者直接复制粘贴其他人的总结!

//AN_Xml:

如果我们被动的学习某个知识点,可能大部分时候都是仅仅满足自己能够会用的层面,你并不会深究其原理,甚至很多关键概念都没搞懂。

//AN_Xml:

如果你是要将你所学到的知识总结成一篇博客的话,一定会加深你对这个知识点的思考。很多时候,你为了将一个知识点讲清楚,你回去查阅很多资料,甚至需要查看很多源码,这些细小的积累在潜移默化中加深了你对这个知识点的认识。

//AN_Xml:

甚至,我还经常会遇到这种情况:写博客的过程中,自己突然意识到自己对于某个知识点的理解存在错误。

//AN_Xml:

写博客本身就是一个对自己学习到的知识进行总结、回顾、思考的过程。记录博客也是对于自己学习历程的一种记录。随着时间的流逝、年龄的增长,这又何尝不是一笔宝贵的精神财富呢?

//AN_Xml:

知识星球的一位球友还提到写技术博客有助于完善自己的知识体系:

//AN_Xml:

写技术博客有助于完善自己的知识体系

//AN_Xml:

帮助别人的同时获得成就感

//AN_Xml:

就像我们程序员希望自己的产品能够得到大家的认可和喜欢一样。我们写技术博客在某一方面当然也是为了能够得到别人的认可。

//AN_Xml:

当你写的东西对别人产生帮助的时候,你会产生成就感和幸福感。

//AN_Xml:

读者的认可

//AN_Xml:

这种成就感和幸福感会作为 正向反馈 ,继续激励你写博客。

//AN_Xml:

但是,即使受到很多读者的赞赏,也要保持谦虚学习的太多。人外有人,比你技术更厉害的读者多了去,一定要虚心学习!

//AN_Xml:

当然,你可以可能会受到很多非议。可能会有很多人说你写的文章没有深度,还可能会有很多人说你闲的蛋疼,你写的东西网上/书上都有。

//AN_Xml:

坦然对待这些非议,做好自己,走好自己的路就好!用行动自证!

//AN_Xml:

可能会有额外的收入

//AN_Xml:

写博客可能还会为你带来经济收入。输出价值的同时,还能够有合理的经济收入,这是最好的状态!

//AN_Xml:

为什么说是可能呢? 因为就目前来看,大部分人还是很难短期通过写博客有收入。我也不建议大家一开始写博客就奔着赚钱的目的,这样功利性太强了,效果可能反而不好。就比如说你坚持了写了半年发现赚不到钱,那你可能就会坚持不下去了。

//AN_Xml:

我自己从大二开始写博客,大三下学期开始将自己的文章发布到公众号上,一直到大四下学期,才通过写博客赚到属于自己的第一笔钱。

//AN_Xml:

第一笔钱是通过微信公众号接某培训机构的推广获得的。没记错的话,当时通过这个推广为自己带来了大约 500 元的收入。虽然这不是很多,但对于还在上大学的我来说,这笔钱非常宝贵。那时我才知道,原来写作真的可以赚钱,这也让我更有动力去分享自己的写作。可惜的是,在接了两次这家培训机构的广告之后,它就倒闭了。

//AN_Xml:

之后,很长一段时间我都没有接到过广告。直到网易的课程合作找上门,一篇文章 1000 元,每个月接近一篇,发了接近两年,这也算是我在大学期间比较稳定的一份收入来源了。

//AN_Xml:

网易的课程合作

//AN_Xml:

老粉应该大部分都是通过 JavaGuide 这个项目认识我的,这是我在大三开始准备秋招面试时创建的一个项目。没想到这个项目竟然火了一把,一度霸占了 GitHub 榜单。可能当时国内这类开源文档教程类项目太少了,所以这个项目受欢迎程度非常高。

//AN_Xml:

JavaGuide Star 趋势

//AN_Xml:

项目火了之后,有一个国内比较大的云服务公司找到我,说是要赞助 JavaGuide 这个项目。我既惊又喜,担心别人是骗子,反复确认合同之后,最终确定以每月 1000 元的费用在我的项目首页加上对方公司的 banner。

//AN_Xml:

随着时间的推移,以及自己后来写了一些比较受欢迎、比较受众的文章,我的博客知名度也有所提升,通过写博客的收入也增加了不少。

//AN_Xml:

增加个人影响力

//AN_Xml:

写技术博客是一种展示自己技术水平和经验的方式,能够让更多的人了解你的专业领域知识和技能。持续分享优质的技术文章,一定能够在技术领域增加个人影响力,这一点是毋庸置疑的。

//AN_Xml:

有了个人影响力之后,不论是对你后面找工作,还是搞付费知识分享或者出书,都非常有帮助。

//AN_Xml:

拿我自己来说,已经很多知名出版社的编辑找过我,协商出一本的书的事情。这种机会应该也是很多人梦寐以求的。不过,我都一一拒绝了,因为觉得自己远远没有达到能够写书的水平。

//AN_Xml:

电子工业出版社编辑邀约出书

//AN_Xml:

其实不出书最主要的原因还是自己嫌麻烦,整个流程的事情太多了。我自己又是比较佛系随性的人,平时也不想把时间都留给工作。

//AN_Xml:

怎样才能坚持写技术博客?

//AN_Xml:

不可否认,人都是有懒性的,这是人的本性。我们需要一个目标/动力来 Push 一下自己。

//AN_Xml:

就技术写作而言,你的目标可以以技术文章的数量为标准,比如:

//AN_Xml:
    //AN_Xml:
  • 一年写多少篇技术文章。我个人觉得一年的范围还是太长了,不太容易定一个比较合适的目标。
  • //AN_Xml:
  • 每月输出一篇高质量的技术文章。这个相对容易实现一些,每月一篇,一年也有十二篇了,也很不错了。
  • //AN_Xml:
//AN_Xml:

不过,以技术文章的数量为目标有点功利化,文章的质量同样很重要。一篇高质量的技术文可能需要花费一周甚至半个月的业余时间才能写完。一定要避免自己刻意追求数量,而忽略质量,迷失技术写作的本心。

//AN_Xml:

我个人给自己定的目标是:每个月至少写一篇原创技术文章或者认真修改完善过去写的三篇技术文章 (像开源项目推荐、开源项目学习、个人经验分享、面经分享等等类型的文章不会被记入)。

//AN_Xml:

我的目标对我来说比较容易完成,因此不会出现为了完成目标而应付任务的情况。在我状态比较好,工作也不是很忙的时候,还会经常超额完成任务。下图是我今年 3 月份完成的任务(任务管理工具:Microsoft To-Do)。除了 gossip 协议是去年写的之外,其他都是 3 月份完成的。

//AN_Xml:

//AN_Xml:

如果觉得以文章数量为标准过于功利的话,也可以比较随性地按照自己的节奏来写作。不过,一般这种情况下,你很可能过段时间就忘了还有这件事,开始慢慢抵触写博客。

//AN_Xml:

写完一篇技术文章之后,我们不光要同步到自己的博客,还要分发到国内一些常见的技术社区比如博客园、掘金。分发到其他平台的原因是获得关注进而收获正向反馈(动力来源之一)与建议,这是技术写作能坚持下去的非常重要的一步,一定要重视!!!

//AN_Xml:

说实话,当你写完一篇自认为还不错的文章的幸福感和成就感还是有的。但是,让自己去做这件事情还是比较痛苦的。 就好比你让自己出去玩很简单,为了达到这个目的,你可以有各种借口。但是,想要自己老老实实学习,还是需要某个外力来督促自己的。

//AN_Xml:

写哪些方向的博客比较好?

//AN_Xml:

通常来说,写下面这些方向的博客会比较好:

//AN_Xml:
    //AN_Xml:
  1. 详细讲解某个知识点:一定要有自己的思考而不是东拼西凑。不仅要介绍知识点的基本概念和原理,还需要适当结合实际案例和应用场景进行举例说明。
  2. //AN_Xml:
  3. 问题排查/性能优化经历:需要详细描述清楚具体的场景以及解决办法。一定要有足够的细节描述,包括出现问题的具体场景、问题的根本原因、解决问题的思路和具体步骤等等。同时,要注重实践性和可操作性,帮助读者更好地学习理解。
  4. //AN_Xml:
  5. 源码阅读记录:从一个功能点出发描述其底层源码实现,谈谈你从源码中学到了什么。
  6. //AN_Xml:
//AN_Xml:

最重要的是一定要重视 Markdown 规范,不然内容再好也会显得不专业。

//AN_Xml:

详见 Markdown 规范 (很重要,尽量按照规范来,对你工作中写文档会非常有帮助)

//AN_Xml:

有没有什么写作技巧分享?

//AN_Xml:

句子不要过长

//AN_Xml:

句子不要过长,尽量使用短句(但也不要太短),这样读者更容易阅读和理解。

//AN_Xml:

尽量让文章更加生动有趣

//AN_Xml:

尽量让文章更加生动有趣,比如你可以适当举一些形象的例子、用一些有趣的段子、歇后语或者网络热词。

//AN_Xml:

不过,这个也主要看你的文章风格。

//AN_Xml:

使用简单明了的语言

//AN_Xml:

避免使用阅读者可能无法理解的行话或复杂语言。

//AN_Xml:

注重清晰度和说服力,保持简单。简单的写作是有说服力的,一个五句话的好论点会比一百句话的精彩论点更能打动人。为什么格言、箴言这类文字容易让人接受,与简洁、直白也有些关系。

//AN_Xml:

使用视觉效果

//AN_Xml:

图表、图像等视觉效果可以让朴素的文本内容更容易理解。记得在适当的地方使用视觉效果来增强你的文章的表现力。

//AN_Xml:

//AN_Xml:

技术文章配图色彩要鲜明

//AN_Xml:

下面是同样内容的两张图,都是通过 drawio 画的,小伙伴们更喜欢哪一张呢?

//AN_Xml:

我相信大部分小伙伴都会选择后面一个色彩更鲜明的!

//AN_Xml:

色彩的调整不过花费了我不到 30s 的时间,带来的阅读体验的上升却是非常之大!

//AN_Xml:

//AN_Xml:

确定你的读者

//AN_Xml:

写作之前,思考一下你的文章的主要受众全体是谁。受众群体确定之后,你可以根据受众的需求和理解水平调整你的写作风格和内容难易程度。

//AN_Xml:

审查和修改

//AN_Xml:

在发表之前一定要审查和修改你的文章。这将帮助你发现错误、澄清任何令人困惑的信息并提高文档的整体质量。

//AN_Xml:

好文是改出来的,切记!!!

//AN_Xml:

总结

//AN_Xml:

总的来说,写技术博客是一件利己利彼的事情。你可能会从中收获到很多东西,你写的东西也可能对别人也有很大的帮助。但是,写技术博客还是比较耗费自己时间的,你需要和工作以及生活做好权衡。

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 操作系统常见面试题总结(下) //AN_Xml: https://javaguide.cn/cs-basics/operating-system/operating-system-basic-questions-02.html //AN_Xml: https://javaguide.cn/cs-basics/operating-system/operating-system-basic-questions-02.html //AN_Xml: 操作系统常见面试题总结(下) //AN_Xml: 最新操作系统高频面试题总结(下):虚拟内存映射、内存碎片/伙伴系统、TLB+页缺失处理、分页分段对比、页面置换算法详解、文件系统&磁盘调度,附图表+⭐️重点标注,一文掌握OS内存/文件考点,快速通关后端面试! //AN_Xml: 计算机基础 //AN_Xml: Sun, 09 Apr 2023 12:09:19 GMT //AN_Xml: 《SpringAI 智能面试平台+RAG 知识库》

//AN_Xml:

内存管理

//AN_Xml:

内存管理主要做了什么?

//AN_Xml:

内存管理主要做的事情

//AN_Xml:

操作系统的内存管理非常重要,主要负责下面这些事情:

//AN_Xml:
    //AN_Xml:
  • 内存的分配与回收:对进程所需的内存进行分配和释放,malloc 函数:申请内存,free 函数:释放内存。
  • //AN_Xml:
  • 地址转换:将程序中的虚拟地址转换成内存中的物理地址。
  • //AN_Xml:
  • 内存扩充:当系统没有足够的内存时,利用虚拟内存技术或自动覆盖技术,从逻辑上扩充内存。
  • //AN_Xml:
  • 内存映射:将一个文件直接映射到进程的进程空间中,这样可以通过内存指针用读写内存的办法直接存取文件内容,速度更快。
  • //AN_Xml:
  • 内存优化:通过调整内存分配策略和回收算法来优化内存使用效率。
  • //AN_Xml:
  • 内存安全:保证进程之间使用内存互不干扰,避免一些恶意程序通过修改内存来破坏系统的安全性。
  • //AN_Xml:
  • ……
  • //AN_Xml:
//AN_Xml:

什么是内存碎片?

//AN_Xml:

内存碎片是由内存的申请和释放产生的,通常分为下面两种:

//AN_Xml:
    //AN_Xml:
  • 内部内存碎片(Internal Memory Fragmentation,简称为内存碎片):已经分配给进程使用但未被使用的内存。导致内部内存碎片的主要原因是,当采用固定比例比如 2 的幂次方进行内存分配时,进程所分配的内存可能会比其实际所需要的大。举个例子,一个进程只需要 65 字节的内存,但为其分配了 128(2^7) 大小的内存,那 63 字节的内存就成为了内部内存碎片。
  • //AN_Xml:
  • 外部内存碎片(External Memory Fragmentation,简称为外部碎片):由于未分配的连续内存区域太小,以至于不能满足任意进程所需要的内存分配请求,这些小片段且不连续的内存空间被称为外部碎片。也就是说,外部内存碎片指的是那些并未分配给进程但又不能使用的内存。我们后面介绍的分段机制就会导致外部内存碎片。
  • //AN_Xml:
//AN_Xml:

内存碎片

//AN_Xml:

内存碎片会导致内存利用率下降,如何减少内存碎片是内存管理要非常重视的一件事情。

//AN_Xml:

常见的内存管理方式有哪些?

//AN_Xml:

内存管理方式可以简单分为下面两种:

//AN_Xml:
    //AN_Xml:
  • 连续内存管理:为一个用户程序分配一个连续的内存空间,内存利用率一般不高。
  • //AN_Xml:
  • 非连续内存管理:允许一个程序使用的内存分布在离散或者说不相邻的内存中,相对更加灵活一些。
  • //AN_Xml:
//AN_Xml:

连续内存管理

//AN_Xml:

块式管理 是早期计算机操作系统的一种连续内存管理方式,存在严重的内存碎片问题。块式管理会将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间,我们称之为内部内存碎片。除了内部内存碎片之外,由于两个内存块之间可能还会有外部内存碎片,这些不连续的外部内存碎片由于太小了无法再进行分配。

//AN_Xml:

在 Linux 系统中,连续内存管理采用了 伙伴系统(Buddy System)算法 来实现,这是一种经典的连续内存分配算法,可以有效解决外部内存碎片的问题。伙伴系统的主要思想是将内存按 2 的幂次划分(每一块内存大小都是 2 的幂次比如 2^6=64 KB),并将相邻的内存块组合成一对伙伴(注意:必须是相邻的才是伙伴)。

//AN_Xml:

当进行内存分配时,伙伴系统会尝试找到大小最合适的内存块。如果找到的内存块过大,就将其一分为二,分成两个大小相等的伙伴块。如果还是大的话,就继续切分,直到到达合适的大小为止。

//AN_Xml:

假设两块相邻的内存块都被释放,系统会将这两个内存块合并,进而形成一个更大的内存块,以便后续的内存分配。这样就可以减少内存碎片的问题,提高内存利用率。

//AN_Xml:

伙伴系统(Buddy System)内存管理

//AN_Xml:

虽然解决了外部内存碎片的问题,但伙伴系统仍然存在内存利用率不高的问题(内部内存碎片)。这主要是因为伙伴系统只能分配大小为 2^n 的内存块,因此当需要分配的内存大小不是 2^n 的整数倍时,会浪费一定的内存空间。举个例子:如果要分配 65 大小的内存快,依然需要分配 2^7=128 大小的内存块。

//AN_Xml:

伙伴系统内存浪费问题

//AN_Xml:

对于内部内存碎片的问题,Linux 采用 SLAB 进行解决。由于这部分内容不是本篇文章的重点,这里就不详细介绍了。

//AN_Xml:

非连续内存管理

//AN_Xml:

非连续内存管理存在下面 3 种方式:

//AN_Xml:
    //AN_Xml:
  • 段式管理:以段(一段连续的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。
  • //AN_Xml:
  • 页式管理:把物理内存分为连续等长的物理页,应用程序的虚拟地址空间也被划分为连续等长的虚拟页,是现代操作系统广泛使用的一种内存管理方式。
  • //AN_Xml:
  • 段页式管理机制:结合了段式管理和页式管理的一种内存管理机制,把物理内存先分成若干段,每个段又继续分成若干大小相等的页。
  • //AN_Xml:
//AN_Xml:

虚拟内存

//AN_Xml:

什么是虚拟内存?有什么用?

//AN_Xml:

虚拟内存(Virtual Memory) 是计算机系统内存管理非常重要的一个技术,本质上来说它只是逻辑存在的,是一个假想出来的内存空间,主要作用是作为进程访问主存(物理内存)的桥梁并简化内存管理。

//AN_Xml:

虚拟内存作为进程访问主存的桥梁

//AN_Xml:

总结来说,虚拟内存主要提供了下面这些能力:

//AN_Xml:
    //AN_Xml:
  • 隔离进程:物理内存通过虚拟地址空间访问,虚拟地址空间与进程一一对应。每个进程都认为自己拥有了整个物理内存,进程之间彼此隔离,一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。
  • //AN_Xml:
  • 提升物理内存利用率:有了虚拟地址空间后,操作系统只需要将进程当前正在使用的部分数据或指令加载入物理内存。
  • //AN_Xml:
  • 简化内存管理:进程都有一个一致且私有的虚拟地址空间,程序员不用和真正的物理内存打交道,而是借助虚拟地址空间访问物理内存,从而简化了内存管理。
  • //AN_Xml:
  • 多个进程共享物理内存:进程在运行过程中,会加载许多操作系统的动态库。这些库对于每个进程而言都是公用的,它们在内存中实际只会加载一份,这部分称为共享内存。
  • //AN_Xml:
  • 提高内存使用安全性:控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统的安全性。
  • //AN_Xml:
  • 提供更大的可使用内存空间:可以让程序拥有超过系统物理内存大小的可用内存空间。这是因为当物理内存不够用时,可以利用磁盘充当,将物理内存页(通常大小为 4 KB)保存到磁盘文件(会影响读写速度),数据或代码页会根据需要在物理内存与磁盘之间移动。
  • //AN_Xml:
//AN_Xml:

没有虚拟内存有什么问题?

//AN_Xml:

如果没有虚拟内存的话,程序直接访问和操作的都是物理内存,看似少了一层中介,但多了很多问题。

//AN_Xml:

具体有什么问题呢? 这里举几个例子说明(参考虚拟内存提供的能力回答这个问题):

//AN_Xml:
    //AN_Xml:
  1. 用户程序可以访问任意物理内存,可能会不小心操作到系统运行必需的内存,进而造成操作系统崩溃,严重影响系统的安全。
  2. //AN_Xml:
  3. 同时运行多个程序容易崩溃。比如你想同时运行一个微信和一个 QQ 音乐,微信在运行的时候给内存地址 1xxx 赋值后,QQ 音乐也同样给内存地址 1xxx 赋值,那么 QQ 音乐对内存的赋值就会覆盖微信之前所赋的值,这就可能会造成微信这个程序会崩溃。
  4. //AN_Xml:
  5. 程序运行过程中使用的所有数据或指令都要载入物理内存,根据局部性原理,其中很大一部分可能都不会用到,白白占用了宝贵的物理内存资源。
  6. //AN_Xml:
  7. ……
  8. //AN_Xml:
//AN_Xml:

什么是虚拟地址和物理地址?

//AN_Xml:

物理地址(Physical Address) 是真正的物理内存中地址,更具体点来说是内存地址寄存器中的地址。程序中访问的内存地址不是物理地址,而是 虚拟地址(Virtual Address)

//AN_Xml:

也就是说,我们编程开发的时候实际就是在和虚拟地址打交道。比如在 C 语言中,指针里面存储的数值就可以理解成为内存里的一个地址,这个地址也就是我们说的虚拟地址。

//AN_Xml:

操作系统一般通过 CPU 芯片中的一个重要组件 MMU(Memory Management Unit,内存管理单元) 将虚拟地址转换为物理地址,这个过程被称为 地址翻译/地址转换(Address Translation)

//AN_Xml:

地址翻译过程

//AN_Xml:

通过 MMU 将虚拟地址转换为物理地址后,再通过总线传到物理内存设备,进而完成相应的物理内存读写请求。

//AN_Xml:

MMU 将虚拟地址翻译为物理地址的主要机制有两种: 分段机制分页机制

//AN_Xml:

什么是虚拟地址空间和物理地址空间?

//AN_Xml:
    //AN_Xml:
  • 虚拟地址空间是虚拟地址的集合,是虚拟内存的范围。每一个进程都有一个一致且私有的虚拟地址空间。
  • //AN_Xml:
  • 物理地址空间是物理地址的集合,是物理内存的范围。
  • //AN_Xml:
//AN_Xml:

虚拟地址与物理内存地址是如何映射的?

//AN_Xml:

MMU 将虚拟地址翻译为物理地址的主要机制有 3 种:

//AN_Xml:
    //AN_Xml:
  1. 分段机制
  2. //AN_Xml:
  3. 分页机制
  4. //AN_Xml:
  5. 段页机制
  6. //AN_Xml:
//AN_Xml:

其中,现代操作系统广泛采用分页机制,需要重点关注!

//AN_Xml:

分段机制

//AN_Xml:

分段机制(Segmentation) 以段(一段 连续 的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。

//AN_Xml:

段表有什么用?地址翻译过程是怎样的?

//AN_Xml:

分段管理通过 段表(Segment Table) 映射虚拟地址和物理地址。

//AN_Xml:

分段机制下的虚拟地址由两部分组成:

//AN_Xml:
    //AN_Xml:
  • 段号:标识着该虚拟地址属于整个虚拟地址空间中的哪一个段。
  • //AN_Xml:
  • 段内偏移量:相对于该段起始地址的偏移量。
  • //AN_Xml:
//AN_Xml:

具体的地址翻译过程如下:

//AN_Xml:
    //AN_Xml:
  1. MMU 首先解析得到虚拟地址中的段号;
  2. //AN_Xml:
  3. 通过段号去该应用程序的段表中取出对应的段信息(找到对应的段表项);
  4. //AN_Xml:
  5. 从段信息中取出该段的起始地址(物理地址)加上虚拟地址中的段内偏移量得到最终的物理地址。
  6. //AN_Xml:
//AN_Xml:

分段机制下的地址翻译过程

//AN_Xml:

段表中还存有诸如段长(可用于检查虚拟地址是否超出合法范围)、段类型(该段的类型,例如代码段、数据段等)等信息。

//AN_Xml:

通过段号一定要找到对应的段表项吗?得到最终的物理地址后对应的物理内存一定存在吗?

//AN_Xml:

不一定。段表项可能并不存在:

//AN_Xml:
    //AN_Xml:
  • 段表项被删除:软件错误、软件恶意行为等情况可能会导致段表项被删除。
  • //AN_Xml:
  • 段表项还未创建:如果系统内存不足或者无法分配到连续的物理内存块就会导致段表项无法被创建。
  • //AN_Xml:
//AN_Xml:

分段机制为什么会导致内存外部碎片?

//AN_Xml:

分段机制容易出现外部内存碎片,即在段与段之间留下碎片空间(不足以映射给虚拟地址空间中的段)。从而造成物理内存资源利用率的降低。

//AN_Xml:

举个例子:假设可用物理内存为 5G 的系统使用分段机制分配内存。现在有 4 个进程,每个进程的内存占用情况如下:

//AN_Xml:
    //AN_Xml:
  • 进程 1:0~1G(第 1 段)
  • //AN_Xml:
  • 进程 2:1~3G(第 2 段)
  • //AN_Xml:
  • 进程 3:3~4.5G(第 3 段)
  • //AN_Xml:
  • 进程 4:4.5~5G(第 4 段)
  • //AN_Xml:
//AN_Xml:

此时,我们关闭了进程 1 和进程 4,则第 1 段和第 4 段的内存会被释放,空闲物理内存还有 1.5G。由于这 1.5G 物理内存并不是连续的,导致没办法将空闲的物理内存分配给一个需要 1.5G 物理内存的进程。

//AN_Xml:

分段机制导致外部内存碎片

//AN_Xml:

分页机制

//AN_Xml:

分页机制(Paging) 把主存(物理内存)分为连续等长的物理页,应用程序的虚拟地址空间划也被分为连续等长的虚拟页。现代操作系统广泛采用分页机制。

//AN_Xml:

注意:这里的页是连续等长的,不同于分段机制下不同长度的段。

//AN_Xml:

在分页机制下,应用程序虚拟地址空间中的任意虚拟页可以被映射到物理内存中的任意物理页上,因此可以实现物理内存资源的离散分配。分页机制按照固定页大小分配物理内存,使得物理内存资源易于管理,可有效避免分段机制中外部内存碎片的问题。

//AN_Xml:

页表有什么用?地址翻译过程是怎样的?

//AN_Xml:

分页管理通过 页表(Page Table) 映射虚拟地址和物理地址。我这里画了一张基于单级页表进行地址翻译的示意图。

//AN_Xml:

单级页表

//AN_Xml:

在分页机制下,每个进程都会有一个对应的页表。

//AN_Xml:

分页机制下的虚拟地址由两部分组成:

//AN_Xml:
    //AN_Xml:
  • 页号:通过虚拟页号可以从页表中取出对应的物理页号;
  • //AN_Xml:
  • 页内偏移量:物理页起始地址+页内偏移量=物理内存地址。
  • //AN_Xml:
//AN_Xml:

具体的地址翻译过程如下:

//AN_Xml:
    //AN_Xml:
  1. MMU 首先解析得到虚拟地址中的虚拟页号;
  2. //AN_Xml:
  3. 通过虚拟页号去该应用程序的页表中取出对应的物理页号(找到对应的页表项);
  4. //AN_Xml:
  5. 用该物理页号对应的物理页起始地址(物理地址)加上虚拟地址中的页内偏移量得到最终的物理地址。
  6. //AN_Xml:
//AN_Xml:

分页机制下的地址翻译过程

//AN_Xml:

页表中还存有诸如访问标志(标识该页面有没有被访问过)、脏数据标识位等信息。

//AN_Xml:

通过虚拟页号一定要找到对应的物理页号吗?找到了物理页号得到最终的物理地址后对应的物理页一定存在吗?

//AN_Xml:

不一定!可能会存在 页缺失 。也就是说,物理内存中没有对应的物理页或者物理内存中有对应的物理页但虚拟页还未和物理页建立映射(对应的页表项不存在)。关于页缺失的内容,后面会详细介绍到。

//AN_Xml:

单级页表有什么问题?为什么需要多级页表?

//AN_Xml:

以 32 位的环境为例,虚拟地址空间范围共有 2^32(4G)。假设 一个页的大小是 2^12(4KB),那页表项共有 4G / 4K = 2^20 个。每个页表项为一个地址,占用 4 字节,(2^20 * 2^2) / (1024 * 1024)= 4MB。也就是说一个程序啥都不干,页表大小就得占用 4M。

//AN_Xml:

系统运行的应用程序多起来的话,页表的开销还是非常大的。而且,绝大部分应用程序可能只能用到页表中的几项,其他的白白浪费了。

//AN_Xml:

为了解决这个问题,操作系统引入了 多级页表 ,多级页表对应多个页表,每个页表与前一个页表相关联。32 位系统一般为二级页表,64 位系统一般为四级页表。

//AN_Xml:

这里以二级页表为例进行介绍:二级列表分为一级页表和二级页表。一级页表共有 1024 个页表项,一级页表又关联二级页表,二级页表同样共有 1024 个页表项。二级页表中的一级页表项是一对多的关系,二级页表按需加载(只会用到很少一部分二级页表),进而节省空间占用。

//AN_Xml:

假设只需要 2 个二级页表,那两级页表的内存占用情况为: 4KB(一级页表占用) + 4KB * 2(二级页表占用) = 12 KB。

//AN_Xml:

多级页表

//AN_Xml:

多级页表属于时间换空间的典型场景,利用增加页表查询的次数减少页表占用的空间。

//AN_Xml:

TLB 有什么用?使用 TLB 之后的地址翻译流程是怎样的?

//AN_Xml:

为了提高虚拟地址到物理地址的转换速度,操作系统在 页表方案 基础之上引入了 转址旁路缓存(Translation Lookaside Buffer,TLB,也被称为快表)

//AN_Xml:

加入 TLB 之后的地址翻译

//AN_Xml:

在主流的 AArch64 和 x86-64 体系结构下,TLB 属于 (Memory Management Unit,内存管理单元) 内部的单元,本质上就是一块高速缓存(Cache),缓存了虚拟页号到物理页号的映射关系,你可以将其简单看作是存储着键(虚拟页号)值(物理页号)对的哈希表。

//AN_Xml:

使用 TLB 之后的地址翻译流程是这样的:

//AN_Xml:
    //AN_Xml:
  1. 用虚拟地址中的虚拟页号作为 key 去 TLB 中查询;
  2. //AN_Xml:
  3. 如果能查到对应的物理页的话,就不用再查询页表了,这种情况称为 TLB 命中(TLB hit)。
  4. //AN_Xml:
  5. 如果不能查到对应的物理页的话,还是需要去查询主存中的页表,同时将页表中的该映射表项添加到 TLB 中,这种情况称为 TLB 未命中(TLB miss)。
  6. //AN_Xml:
  7. 当 TLB 填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。
  8. //AN_Xml:
//AN_Xml:

使用 TLB 之后的地址翻译流程

//AN_Xml:

由于页表也在主存中,因此在没有 TLB 之前,每次读写内存数据时 CPU 要访问两次主存。有了 TLB 之后,对于存在于 TLB 中的页表数据只需要访问一次主存即可。

//AN_Xml:

TLB 的设计思想非常简单,但命中率往往非常高,效果很好。这就是因为被频繁访问的页就是其中的很小一部分。

//AN_Xml:

看完了之后你会发现快表和我们平时经常在开发系统中使用的缓存(比如 Redis)很像,的确是这样的,操作系统中的很多思想、很多经典的算法,你都可以在我们日常开发使用的各种工具或者框架中找到它们的影子。

//AN_Xml:

换页机制有什么用?

//AN_Xml:

换页机制的思想是当物理内存不够用的时候,操作系统选择将一些物理页的内容放到磁盘上去,等要用到的时候再将它们读取到物理内存中。也就是说,换页机制利用磁盘这种较低廉的存储设备扩展的物理内存。

//AN_Xml:

这也就解释了一个日常使用电脑常见的问题:为什么操作系统中所有进程运行所需的物理内存即使比真实的物理内存要大一些,这些进程也是可以正常运行的,只是运行速度会变慢。

//AN_Xml:

这同样是一种时间换空间的策略,你用 CPU 的计算时间,页的调入调出花费的时间,换来了一个虚拟的更大的物理内存空间来支持程序的运行。

//AN_Xml:

什么是页缺失?

//AN_Xml:

根据维基百科:

//AN_Xml:
//AN_Xml:

页缺失(Page Fault,又名硬错误、硬中断、分页错误、寻页缺失、缺页中断、页故障等)指的是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由 MMU 所发出的中断。

//AN_Xml:
//AN_Xml:

常见的页缺失有下面这两种:

//AN_Xml:
    //AN_Xml:
  • 硬性页缺失(Hard Page Fault):物理内存中没有对应的物理页。于是,Page Fault Handler 会指示 CPU 从已经打开的磁盘文件中读取相应的内容到物理内存,而后交由 MMU 建立相应的虚拟页和物理页的映射关系。
  • //AN_Xml:
  • 软性页缺失(Soft Page Fault):物理内存中有对应的物理页,但虚拟页还未和物理页建立映射。于是,Page Fault Handler 会指示 MMU 建立相应的虚拟页和物理页的映射关系。
  • //AN_Xml:
//AN_Xml:

发生上面这两种缺页错误的时候,应用程序访问的是有效的物理内存,只是出现了物理页缺失或者虚拟页和物理页的映射关系未建立的问题。如果应用程序访问的是无效的物理内存的话,还会出现 无效缺页错误(Invalid Page Fault)

//AN_Xml:

常见的页面置换算法有哪些?

//AN_Xml:

当发生硬性页缺失时,如果物理内存中没有空闲的物理页面可用的话。操作系统就必须将物理内存中的一个物理页淘汰出去,这样就可以腾出空间来加载新的页面了。

//AN_Xml:

用来选择淘汰哪一个物理页的规则叫做 页面置换算法 ,我们可以把页面置换算法看成是淘汰物物理页的规则。

//AN_Xml:

页缺失太频繁的发生会非常影响性能,一个好的页面置换算法应该是可以减少页缺失出现的次数。

//AN_Xml:

常见的页面置换算法有下面这 5 种(其他还有很多页面置换算法都是基于这些算法改进得来的):

//AN_Xml:

常见的页面置换算法

//AN_Xml:
    //AN_Xml:
  1. 最佳页面置换算法(OPT,Optimal):优先选择淘汰的页面是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若干页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现,只是理论最优的页面置换算法,可以作为衡量其他置换算法优劣的标准。
  2. //AN_Xml:
  3. 先进先出页面置换算法(FIFO,First In First Out) : 最简单的一种页面置换算法,总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。该算法易于实现和理解,一般只需要通过一个 FIFO 队列即可满足需求。不过,它的性能并不是很好。
  4. //AN_Xml:
  5. 最近最久未使用页面置换算法(LRU ,Least Recently Used):LRU 算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。LRU 算法是根据各页之前的访问情况来实现,因此是易于实现的。OPT 算法是根据各页未来的访问情况来实现,因此是不可实现的。
  6. //AN_Xml:
  7. 最少使用页面置换算法(LFU,Least Frequently Used) : 和 LRU 算法比较像,不过该置换算法选择的是之前一段时间内使用最少的页面作为淘汰页。
  8. //AN_Xml:
  9. 时钟页面置换算法(Clock):可以认为是一种最近未使用算法,即逐出的页面都是最近没有使用的那个。
  10. //AN_Xml:
//AN_Xml:

FIFO 页面置换算法性能为何不好?

//AN_Xml:

主要原因主要有二:

//AN_Xml:
    //AN_Xml:
  1. 经常访问或者需要长期存在的页面会被频繁调入调出:较早调入的页往往是经常被访问或者需要长期存在的页,这些页会被反复调入和调出。
  2. //AN_Xml:
  3. 存在 Belady 现象:被置换的页面并不是进程不会访问的,有时就会出现分配的页面数增多但缺页率反而提高的异常现象。出现该异常的原因是因为 FIFO 算法只考虑了页面进入内存的顺序,而没有考虑页面访问的频率和紧迫性。
  4. //AN_Xml:
//AN_Xml:

哪一种页面置换算法实际用的比较多?

//AN_Xml:

LRU 算法是实际使用中应用的比较多,也被认为是最接近 OPT 的页面置换算法。

//AN_Xml:

不过,需要注意的是,实际应用中这些算法会被做一些改进,就比如 InnoDB Buffer Pool( InnoDB 缓冲池,MySQL 数据库中用于管理缓存页面的机制)就改进了传统的 LRU 算法,使用了一种称为"Adaptive LRU"的算法(同时结合了 LRU 和 LFU 算法的思想)。

//AN_Xml:

分页机制和分段机制有哪些共同点和区别?

//AN_Xml:

共同点

//AN_Xml:
    //AN_Xml:
  • 都是非连续内存管理的方式。
  • //AN_Xml:
  • 都采用了地址映射的方法,将虚拟地址映射到物理地址,以实现对内存的管理和保护。
  • //AN_Xml:
//AN_Xml:

区别

//AN_Xml:
    //AN_Xml:
  • 分页机制以页面为单位进行内存管理,而分段机制以段为单位进行内存管理。页的大小是固定的,由操作系统决定,通常为 2 的幂次方。而段的大小不固定,取决于我们当前运行的程序。
  • //AN_Xml:
  • 页是物理单位,即操作系统将物理内存划分成固定大小的页面,每个页面的大小通常是 2 的幂次方,例如 4KB、8KB 等等。而段则是逻辑单位,是为了满足程序对内存空间的逻辑需求而设计的,通常根据程序中数据和代码的逻辑结构来划分。
  • //AN_Xml:
  • 分段机制容易出现外部内存碎片,即在段与段之间留下碎片空间(不足以映射给虚拟地址空间中的段)。分页机制解决了外部内存碎片的问题,但仍然可能会出现内部内存碎片。
  • //AN_Xml:
  • 分页机制采用了页表来完成虚拟地址到物理地址的映射,页表通过一级页表和二级页表来实现多级映射;而分段机制则采用了段表来完成虚拟地址到物理地址的映射,每个段表项中记录了该段的起始地址和长度信息。
  • //AN_Xml:
  • 分页机制对程序没有任何要求,程序只需要按照虚拟地址进行访问即可;而分段机制需要程序员将程序分为多个段,并且显式地使用段寄存器来访问不同的段。
  • //AN_Xml:
//AN_Xml:

段页机制

//AN_Xml:

结合了段式管理和页式管理的一种内存管理机制。程序视角中,内存被划分为多个逻辑段,每个逻辑段进一步被划分为固定大小的页。

//AN_Xml:

在段页式机制下,地址翻译的过程分为两个步骤:

//AN_Xml:
    //AN_Xml:
  1. 段式地址映射(虚拟地址 → 线性地址): //AN_Xml:
      //AN_Xml:
    • 虚拟地址 = 段选择符(段号)+ 段内偏移。
    • //AN_Xml:
    • 根据段号查段表,找到段基址,加上段内偏移得到线性地址。
    • //AN_Xml:
    //AN_Xml:
  2. //AN_Xml:
  3. 页式地址映射(线性地址 → 物理地址): //AN_Xml:
      //AN_Xml:
    • 线性地址 = 页号 + 页内偏移。
    • //AN_Xml:
    • 根据页号查页表,找到物理页框号,加上页内偏移得到物理地址。
    • //AN_Xml:
    //AN_Xml:
  4. //AN_Xml:
//AN_Xml:

局部性原理

//AN_Xml:

要想更好地理解虚拟内存技术,必须要知道计算机中著名的 局部性原理(Locality Principle)。另外,局部性原理既适用于程序结构,也适用于数据结构,是非常重要的一个概念。

//AN_Xml:

局部性原理是指在程序执行过程中,数据和指令的访问存在一定的空间和时间上的局部性特点。其中,时间局部性是指一个数据项或指令在一段时间内被反复使用的特点,空间局部性是指一个数据项或指令在一段时间内与其相邻的数据项或指令被反复使用的特点。

//AN_Xml:

在分页机制中,页表的作用是将虚拟地址转换为物理地址,从而完成内存访问。在这个过程中,局部性原理的作用体现在两个方面:

//AN_Xml:
    //AN_Xml:
  • 时间局部性:由于程序中存在一定的循环或者重复操作,因此会反复访问同一个页或一些特定的页,这就体现了时间局部性的特点。为了利用时间局部性,分页机制中通常采用缓存机制来提高页面的命中率,即将最近访问过的一些页放入缓存中,如果下一次访问的页已经在缓存中,就不需要再次访问内存,而是直接从缓存中读取。
  • //AN_Xml:
  • 空间局部性:由于程序中数据和指令的访问通常是具有一定的空间连续性的,因此当访问某个页时,往往会顺带访问其相邻的一些页。为了利用空间局部性,分页机制中通常采用预取技术来预先将相邻的一些页读入内存缓存中,以便在未来访问时能够直接使用,从而提高访问速度。
  • //AN_Xml:
//AN_Xml:

总之,局部性原理是计算机体系结构设计的重要原则之一,也是许多优化算法的基础。在分页机制中,利用时间局部性和空间局部性,采用缓存和预取技术,可以提高页面的命中率,从而提高内存访问效率

//AN_Xml:

文件系统

//AN_Xml:

文件系统主要做了什么?

//AN_Xml:

文件系统主要负责管理和组织计算机存储设备上的文件和目录,其功能包括以下几个方面:

//AN_Xml:
    //AN_Xml:
  1. 存储管理:将文件数据存储到物理存储介质中,并且管理空间分配,以确保每个文件都有足够的空间存储,并避免文件之间发生冲突。
  2. //AN_Xml:
  3. 文件管理:文件的创建、删除、移动、重命名、压缩、加密、共享等等。
  4. //AN_Xml:
  5. 目录管理:目录的创建、删除、移动、重命名等等。
  6. //AN_Xml:
  7. 文件访问控制:管理不同用户或进程对文件的访问权限,以确保用户只能访问其被授权访问的文件,以保证文件的安全性和保密性。
  8. //AN_Xml:
//AN_Xml:

硬链接和软链接有什么区别?

//AN_Xml:

在 Linux/类 Unix 系统上,文件链接(File Link)是一种特殊的文件类型,可以在文件系统中指向另一个文件。常见的文件链接类型有两种:

//AN_Xml:

1、硬链接(Hard Link)

//AN_Xml:
    //AN_Xml:
  • 在 Linux/类 Unix 文件系统中,每个文件和目录都有一个唯一的索引节点(inode)号,用来标识该文件或目录。硬链接通过 inode 节点号建立连接,硬链接和源文件的 inode 节点号相同,两者对文件系统来说是完全平等的(可以看作是互为硬链接,源头是同一份文件),删除其中任何一个对另外一个没有影响,可以通过给文件设置硬链接文件来防止重要文件被误删。
  • //AN_Xml:
  • 只有删除了源文件和所有对应的硬链接文件,该文件才会被真正删除。
  • //AN_Xml:
  • 硬链接具有一些限制,不能对目录以及不存在的文件创建硬链接,并且,硬链接也不能跨越文件系统。
  • //AN_Xml:
  • ln 命令用于创建硬链接。
  • //AN_Xml:
//AN_Xml:

2、软链接(Symbolic Link 或 Symlink)

//AN_Xml:
    //AN_Xml:
  • 软链接和源文件的 inode 节点号不同,而是指向一个文件路径。
  • //AN_Xml:
  • 源文件删除后,软链接依然存在,但是指向的是一个无效的文件路径。
  • //AN_Xml:
  • 软连接类似于 Windows 系统中的快捷方式。
  • //AN_Xml:
  • 不同于硬链接,可以对目录或者不存在的文件创建软链接,并且,软链接可以跨越文件系统。
  • //AN_Xml:
  • ln -s 命令用于创建软链接。
  • //AN_Xml:
//AN_Xml:

硬链接为什么不能跨文件系统?

//AN_Xml:

我们之前提到过,硬链接是通过 inode 节点号建立连接的,而硬链接和源文件共享相同的 inode 节点号。

//AN_Xml:

然而,每个文件系统都有自己的独立 inode 表,且每个 inode 表只维护该文件系统内的 inode。如果在不同的文件系统之间创建硬链接,可能会导致 inode 节点号冲突的问题,即目标文件的 inode 节点号已经在该文件系统中被使用。

//AN_Xml:

提高文件系统性能的方式有哪些?

//AN_Xml:
    //AN_Xml:
  • 优化硬件:使用高速硬件设备(如 SSD、NVMe)替代传统的机械硬盘,使用 RAID(Redundant Array of Independent Disks)等技术提高磁盘性能。
  • //AN_Xml:
  • 选择合适的文件系统选型:不同的文件系统具有不同的特性,对于不同的应用场景选择合适的文件系统可以提高系统性能。
  • //AN_Xml:
  • 运用缓存:访问磁盘的效率比较低,可以运用缓存来减少磁盘的访问次数。不过,需要注意缓存命中率,缓存命中率过低的话,效果太差。
  • //AN_Xml:
  • 避免磁盘过度使用:注意磁盘的使用率,避免将磁盘用满,尽量留一些剩余空间,以免对文件系统的性能产生负面影响。
  • //AN_Xml:
  • 对磁盘进行合理的分区:合理的磁盘分区方案,能够使文件系统在不同的区域存储文件,从而减少文件碎片,提高文件读写性能。
  • //AN_Xml:
//AN_Xml:

常见的磁盘调度算法有哪些?

//AN_Xml:

磁盘调度算法是操作系统中对磁盘访问请求进行排序和调度的算法,其目的是提高磁盘的访问效率。

//AN_Xml:

一次磁盘读写操作的时间由磁盘寻道/寻找时间、延迟时间和传输时间决定。磁盘调度算法可以通过改变到达磁盘请求的处理顺序,减少磁盘寻道时间和延迟时间。

//AN_Xml:

常见的磁盘调度算法有下面这 6 种(其他还有很多磁盘调度算法都是基于这些算法改进得来的):

//AN_Xml:

常见的磁盘调度算法

//AN_Xml:
    //AN_Xml:
  1. 先来先服务算法(First-Come First-Served,FCFS):按照请求到达磁盘调度器的顺序进行处理,先到达的请求的先被服务。FCFS 算法实现起来比较简单,不存在算法开销。不过,由于没有考虑磁头移动的路径和方向,平均寻道时间较长。同时,该算法容易出现饥饿问题,即一些后到的磁盘请求可能需要等待很长时间才能得到服务。
  2. //AN_Xml:
  3. 最短寻道时间优先算法(Shortest Seek Time First,SSTF):也被称为最佳服务优先(Shortest Service Time First,SSTF)算法,优先选择距离当前磁头位置最近的请求进行服务。SSTF 算法能够最小化磁头的寻道时间,但容易出现饥饿问题,即磁头附近的请求不断被服务,远离磁头的请求长时间得不到响应。实际应用中,需要优化一下该算法的实现,避免出现饥饿问题。
  4. //AN_Xml:
  5. 扫描算法(SCAN):也被称为电梯(Elevator)算法,基本思想和电梯非常类似。磁头沿着一个方向扫描磁盘,如果经过的磁道有请求就处理,直到到达磁盘的边界,然后改变移动方向,依此往复。SCAN 算法能够保证所有的请求得到服务,解决了饥饿问题。但是,如果磁头从一个方向刚扫描完,请求才到的话。这个请求就需要等到磁头从相反方向过来之后才能得到处理。
  6. //AN_Xml:
  7. 循环扫描算法(Circular Scan,C-SCAN):SCAN 算法的变体,只在磁盘的一侧进行扫描,并且只按照一个方向扫描,直到到达磁盘边界,然后回到磁盘起点,重新开始循环。
  8. //AN_Xml:
  9. 边扫描边观察算法(LOOK):SCAN 算法中磁头到了磁盘的边界才改变移动方向,这样可能会做很多无用功,因为磁头移动方向上可能已经没有请求需要处理了。LOOK 算法对 SCAN 算法进行了改进,如果磁头移动方向上已经没有别的请求,就可以立即改变磁头移动方向,依此往复。也就是边扫描边观察指定方向上还有无请求,因此叫 LOOK。
  10. //AN_Xml:
  11. 均衡循环扫描算法(C-LOOK):C-SCAN 只有到达磁盘边界时才能改变磁头移动方向,并且磁头返回时也需要返回到磁盘起点,这样可能会做很多无用功。C-LOOK 算法对 C-SCAN 算法进行了改进,如果磁头移动的方向上已经没有磁道访问请求了,就可以立即让磁头返回,并且磁头只需要返回到有磁道访问请求的位置即可。
  12. //AN_Xml:
//AN_Xml:

参考

//AN_Xml: //AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Java 20 新特性概览 //AN_Xml: https://javaguide.cn/java/new-features/java20.html //AN_Xml: https://javaguide.cn/java/new-features/java20.html //AN_Xml: Java 20 新特性概览 //AN_Xml: 总结 JDK 20 的语言与并发改动,延续虚拟线程与模式匹配相关增强。 //AN_Xml: Java //AN_Xml: Mon, 27 Mar 2023 07:25:00 GMT //AN_Xml: JDK 20 于 2023 年 3 月 21 日发布,非长期支持版本。

//AN_Xml:

根据开发计划,下一个 LTS 版本就是将于 2023 年 9 月发布的 JDK 21。

//AN_Xml:

JDK 20 共有 7 个新特性,这篇文章会挑选其中较为重要的一些新特性进行详细介绍:

//AN_Xml: //AN_Xml:

下图是从 JDK 8 到 JDK 25 每个版本的更新带来的新特性数量和更新时间:

//AN_Xml:

 JDK 8 到 JDK 25 每个版本的更新带来的新特性数量和更新时间

//AN_Xml:

JEP 429: Scoped Values(作用域值,第一次孵化)

//AN_Xml:

作用域值(Scoped Values)它可以在线程内和线程间共享不可变的数据,优于线程局部变量,尤其是在使用大量虚拟线程时。

//AN_Xml:
final static ScopedValue<...> V = new ScopedValue<>();
//AN_Xml:
//AN_Xml:// In some method
//AN_Xml:ScopedValue.where(V, <value>)
//AN_Xml:           .run(() -> { ... V.get() ... call methods ... });
//AN_Xml:
//AN_Xml:// In a method called directly or indirectly from the lambda expression
//AN_Xml:... V.get() ...
//AN_Xml:

作用域值允许在大型程序中的组件之间安全有效地共享数据,而无需求助于方法参数。

//AN_Xml:

关于作用域值的详细介绍,推荐阅读作用域值常见问题解答这篇文章。

//AN_Xml:

JEP 432: Record Patterns(记录模式,第二次预览)

//AN_Xml:

记录模式(Record Patterns) 可对 record 的值进行解构,也就是更方便地从记录类(Record Class)中提取数据。并且,还可以嵌套记录模式和类型模式结合使用,以实现强大的、声明性的和可组合的数据导航和处理形式。

//AN_Xml:

记录模式不能单独使用,而是要与 instanceof 或 switch 模式匹配一同使用。

//AN_Xml:

先以 instanceof 为例简单演示一下。

//AN_Xml:

简单定义一个记录类:

//AN_Xml:
record Shape(String type, long unit){}
//AN_Xml:

没有记录模式之前:

//AN_Xml:
Shape circle = new Shape("Circle", 10);
//AN_Xml:if (circle instanceof Shape shape) {
//AN_Xml:
//AN_Xml:  System.out.println("Area of " + shape.type() + " is : " + Math.PI * Math.pow(shape.unit(), 2));
//AN_Xml:}
//AN_Xml:

有了记录模式之后:

//AN_Xml:
Shape circle = new Shape("Circle", 10);
//AN_Xml:if (circle instanceof Shape(String type, long unit)) {
//AN_Xml:  System.out.println("Area of " + type + " is : " + Math.PI * Math.pow(unit, 2));
//AN_Xml:}
//AN_Xml:

再看看记录模式与 switch 的配合使用。

//AN_Xml:

定义一些类:

//AN_Xml:
interface Shape {}
//AN_Xml:record Circle(double radius) implements Shape { }
//AN_Xml:record Square(double side) implements Shape { }
//AN_Xml:record Rectangle(double length, double width) implements Shape { }
//AN_Xml:

没有记录模式之前:

//AN_Xml:
Shape shape = new Circle(10);
//AN_Xml:switch (shape) {
//AN_Xml:    case Circle c:
//AN_Xml:        System.out.println("The shape is Circle with area: " + Math.PI * c.radius() * c.radius());
//AN_Xml:        break;
//AN_Xml:
//AN_Xml:    case Square s:
//AN_Xml:        System.out.println("The shape is Square with area: " + s.side() * s.side());
//AN_Xml:        break;
//AN_Xml:
//AN_Xml:    case Rectangle r:
//AN_Xml:        System.out.println("The shape is Rectangle with area: " + r.length() * r.width());
//AN_Xml:        break;
//AN_Xml:
//AN_Xml:    default:
//AN_Xml:        System.out.println("Unknown Shape");
//AN_Xml:        break;
//AN_Xml:}
//AN_Xml:

有了记录模式之后:

//AN_Xml:
Shape shape = new Circle(10);
//AN_Xml:switch(shape) {
//AN_Xml:
//AN_Xml:  case Circle(double radius):
//AN_Xml:    System.out.println("The shape is Circle with area: " + Math.PI * radius * radius);
//AN_Xml:    break;
//AN_Xml:
//AN_Xml:  case Square(double side):
//AN_Xml:    System.out.println("The shape is Square with area: " + side * side);
//AN_Xml:    break;
//AN_Xml:
//AN_Xml:  case Rectangle(double length, double width):
//AN_Xml:    System.out.println("The shape is Rectangle with area: " + length * width);
//AN_Xml:    break;
//AN_Xml:
//AN_Xml:  default:
//AN_Xml:    System.out.println("Unknown Shape");
//AN_Xml:    break;
//AN_Xml:}
//AN_Xml:

记录模式可以避免不必要的转换,使得代码更简洁易读。而且,用了记录模式后不必再担心 null 或者 NullPointerException,代码更安全可靠。

//AN_Xml:

记录模式在 Java 19 进行了第一次预览, 由 JEP 405 提出。JDK 20 中是第二次预览,由 JEP 432 提出。这次的改进包括:

//AN_Xml:
    //AN_Xml:
  • 添加对通用记录模式类型参数推断的支持,
  • //AN_Xml:
  • 添加对记录模式的支持以出现在增强语句的标题中for
  • //AN_Xml:
  • 删除对命名记录模式的支持。
  • //AN_Xml:
//AN_Xml:

注意:不要把记录模式和 JDK16 正式引入的记录类搞混了。

//AN_Xml:

JEP 433: Pattern Matching for switch(switch 模式匹配,第四次预览)

//AN_Xml:

正如 instanceof 一样, switch 也紧跟着增加了类型匹配自动转换功能。

//AN_Xml:

instanceof 代码示例:

//AN_Xml:
// Old code
//AN_Xml:if (o instanceof String) {
//AN_Xml:    String s = (String)o;
//AN_Xml:    ... use s ...
//AN_Xml:}
//AN_Xml:
//AN_Xml:// New code
//AN_Xml:if (o instanceof String s) {
//AN_Xml:    ... use s ...
//AN_Xml:}
//AN_Xml:

switch 代码示例:

//AN_Xml:
// Old code
//AN_Xml:static String formatter(Object o) {
//AN_Xml:    String formatted = "unknown";
//AN_Xml:    if (o instanceof Integer i) {
//AN_Xml:        formatted = String.format("int %d", i);
//AN_Xml:    } else if (o instanceof Long l) {
//AN_Xml:        formatted = String.format("long %d", l);
//AN_Xml:    } else if (o instanceof Double d) {
//AN_Xml:        formatted = String.format("double %f", d);
//AN_Xml:    } else if (o instanceof String s) {
//AN_Xml:        formatted = String.format("String %s", s);
//AN_Xml:    }
//AN_Xml:    return formatted;
//AN_Xml:}
//AN_Xml:
//AN_Xml:// New code
//AN_Xml:static String formatterPatternSwitch(Object o) {
//AN_Xml:    return switch (o) {
//AN_Xml:        case Integer i -> String.format("int %d", i);
//AN_Xml:        case Long l    -> String.format("long %d", l);
//AN_Xml:        case Double d  -> String.format("double %f", d);
//AN_Xml:        case String s  -> String.format("String %s", s);
//AN_Xml:        default        -> o.toString();
//AN_Xml:    };
//AN_Xml:}
//AN_Xml:

switch 模式匹配分别在 Java17、Java18、Java19 中进行了预览,Java20 是第四次预览了。每一次的预览基本都会有一些小改进,这里就不细提了。

//AN_Xml:

JEP 434: Foreign Function & Memory API(外部函数和内存 API,第二次预览)

//AN_Xml:

Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。

//AN_Xml:

外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 JEP 412 提出。Java 18 中进行了第二次孵化,由JEP 419 提出。Java 19 中是第一次预览,由 JEP 424 提出。

//AN_Xml:

JDK 20 中是第二次预览,由 JEP 434 提出,这次的改进包括:

//AN_Xml:
    //AN_Xml:
  • MemorySegmentMemoryAddress 抽象的统一
  • //AN_Xml:
  • 增强的 MemoryLayout 层次结构
  • //AN_Xml:
  • MemorySession拆分为ArenaSegmentScope,以促进跨维护边界的段共享。
  • //AN_Xml:
//AN_Xml:

Java 19 新特性概览 中,我有详细介绍到外部函数和内存 API,这里就不再做额外的介绍了。

//AN_Xml:

JEP 436: Virtual Threads(虚拟线程,第二次预览)

//AN_Xml:

虚拟线程(Virtual Thread)是 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP),由 JVM 调度。许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。

//AN_Xml:

在引入虚拟线程之前,java.lang.Thread 包已经支持所谓的平台线程,也就是没有虚拟线程之前,我们一直使用的线程。JVM 调度程序通过平台线程(载体线程)来管理虚拟线程,一个平台线程可以在不同的时间执行不同的虚拟线程(多个虚拟线程挂载在一个平台线程上),当虚拟线程被阻塞或等待时,平台线程可以切换到执行另一个虚拟线程。

//AN_Xml:

虚拟线程、平台线程和系统内核线程的关系图如下所示(图源:How to Use Java 19 Virtual Threads):

//AN_Xml:

虚拟线程、平台线程和系统内核线程的关系

//AN_Xml:

关于平台线程和系统内核线程的对应关系多提一点:在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个平台线程对应一个系统内核线程。Solaris 系统是一个特例,HotSpot VM 在 Solaris 上支持多对多和一对一。具体可以参考 R 大的回答: JVM 中的线程模型是用户级的么?

//AN_Xml:

相比较于平台线程来说,虚拟线程是廉价且轻量级的,使用完后立即被销毁,因此它们不需要被重用或池化,每个任务可以有自己专属的虚拟线程来运行。虚拟线程暂停和恢复来实现线程之间的切换,避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂,可以有效减少编写、维护和观察高吞吐量并发应用程序的工作量。

//AN_Xml:

虚拟线程在其他多线程语言中已经被证实是十分有用的,比如 Go 中的 Goroutine、Erlang 中的进程。

//AN_Xml:

知乎有一个关于 Java 19 虚拟线程的讨论,感兴趣的可以去看看:https://www.zhihu.com/question/536743167

//AN_Xml:

Java 虚拟线程的详细解读和原理可以看下面这几篇文章:

//AN_Xml: //AN_Xml:

虚拟线程在 Java 19 中进行了第一次预览,由JEP 425提出。JDK 20 中是第二次预览,做了一些细微变化,这里就不细提了。

//AN_Xml:

最后,我们来看一下四种创建虚拟线程的方法:

//AN_Xml:
// 1、通过 Thread.ofVirtual() 创建
//AN_Xml:Runnable fn = () -> {
//AN_Xml:  // your code here
//AN_Xml:};
//AN_Xml:
//AN_Xml:Thread thread = Thread.ofVirtual(fn)
//AN_Xml:                      .start();
//AN_Xml:
//AN_Xml:// 2、通过 Thread.startVirtualThread() 创建
//AN_Xml:Thread thread = Thread.startVirtualThread(() -> {
//AN_Xml:  // your code here
//AN_Xml:});
//AN_Xml:
//AN_Xml:// 3、通过 Executors.newVirtualThreadPerTaskExecutor() 创建
//AN_Xml:var executorService = Executors.newVirtualThreadPerTaskExecutor();
//AN_Xml:
//AN_Xml:executorService.submit(() -> {
//AN_Xml:  // your code here
//AN_Xml:});
//AN_Xml:
//AN_Xml:class CustomThread implements Runnable {
//AN_Xml:  @Override
//AN_Xml:  public void run() {
//AN_Xml:    System.out.println("CustomThread run");
//AN_Xml:  }
//AN_Xml:}
//AN_Xml:
//AN_Xml://4、通过 ThreadFactory 创建
//AN_Xml:CustomThread customThread = new CustomThread();
//AN_Xml:// 获取线程工厂类
//AN_Xml:ThreadFactory factory = Thread.ofVirtual().factory();
//AN_Xml:// 创建虚拟线程
//AN_Xml:Thread thread = factory.newThread(customThread);
//AN_Xml:// 启动线程
//AN_Xml:thread.start();
//AN_Xml:

通过上述列举的 4 种创建虚拟线程的方式可以看出,官方为了降低虚拟线程的门槛,尽力复用原有的 Thread 线程类,这样可以平滑的过渡到虚拟线程的使用。

//AN_Xml:

JEP 437: Structured Concurrency(结构化并发,第二次孵化)

//AN_Xml:

Java 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代java.util.concurrent,目前处于孵化器阶段。

//AN_Xml:

结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。

//AN_Xml:

结构化并发的基本 API 是StructuredTaskScopeStructuredTaskScope 支持将任务拆分为多个并发子任务,在它们自己的线程中执行,并且子任务必须在主任务继续之前完成。

//AN_Xml:

StructuredTaskScope 的基本用法如下:

//AN_Xml:
    try (var scope = new StructuredTaskScope<Object>()) {
//AN_Xml:        // 使用fork方法派生线程来执行子任务
//AN_Xml:        Future<Integer> future1 = scope.fork(task1);
//AN_Xml:        Future<String> future2 = scope.fork(task2);
//AN_Xml:        // 等待线程完成
//AN_Xml:        scope.join();
//AN_Xml:        // 结果的处理可能包括处理或重新抛出异常
//AN_Xml:        ... process results/exceptions ...
//AN_Xml:    } // close
//AN_Xml:

结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。

//AN_Xml:

JDK 20 中对结构化并发唯一变化是更新为支持在任务范围内创建的线程StructuredTaskScope继承范围值 这简化了跨线程共享不可变数据,详见JEP 429

//AN_Xml:

JEP 438: Vector API(向量 API,第五次孵化)

//AN_Xml:

向量计算由对向量的一系列操作组成。向量 API 用来表达向量计算,该计算可以在运行时可靠地编译为支持的 CPU 架构上的最佳向量指令,从而实现优于等效标量计算的性能。

//AN_Xml:

向量 API 的目标是为用户提供简洁易用且与平台无关的表达范围广泛的向量计算。

//AN_Xml:

向量(Vector) API 最初由 JEP 338 提出,并作为孵化 API集成到 Java 16 中。第二轮孵化由 JEP 414 提出并集成到 Java 17 中,第三轮孵化由 JEP 417 提出并集成到 Java 18 中,第四轮由 JEP 426 提出并集成到了 Java 19 中。

//AN_Xml:

Java20 的这次孵化基本没有改变向量 API ,只是进行了一些错误修复和性能增强,详见 JEP 438

//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 从校招入职腾讯的四年工作总结 //AN_Xml: https://javaguide.cn/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.html //AN_Xml: https://javaguide.cn/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.html //AN_Xml: 从校招入职腾讯的四年工作总结 //AN_Xml: 从校招入职腾讯的四年工作总结:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。 //AN_Xml: 技术文章精选集 //AN_Xml: Fri, 24 Mar 2023 11:23:46 GMT //AN_Xml: 程序员是一个流动性很大的职业,经常会有新面孔的到来,也经常会有老面孔的离开,有主动离开的,也有被动离职的。

//AN_Xml:

再加上这几年卷得厉害,做的事更多了,拿到的却更少了,互联网好像也没有那么香了。

//AN_Xml:

人来人往,变动无常的状态,其实也早已习惯。

//AN_Xml:

打工人的唯一出路,无外乎精进自己的专业技能,提升自己的核心竞争力,这样无论有什么变动,走到哪里,都能有口饭吃。

//AN_Xml:

今天分享一位博主,校招入职腾讯,工作四年后,离开的故事。

//AN_Xml:

至于为什么离开,我也不清楚,可能是有其他更好的选择,或者是觉得当前的工作对自己的提升有限。

//AN_Xml:

下文中的“我”,指这位作者本人。

//AN_Xml:
//AN_Xml:

原文地址:https://zhuanlan.zhihu.com/p/602517682

//AN_Xml:
//AN_Xml:

研究生毕业后, 一直在腾讯工作,不知不觉就过了四年。个人本身没有刻意总结的习惯,以前只顾着往前奔跑了,忘了停下来思考总结。记得看过一个职业规划文档,说的三年一个阶段,五年一个阶段的说法,现在恰巧是四年,同时又从腾讯离开,该做一个总结了。

//AN_Xml:

先对自己这四年做一个简单的评价吧:个人认为,没有完全的浪费和辜负这四年的光阴。为何要这么说了?因为我发现和别人对比,好像意义不大,比我混的好的人很多;比我混的差的人也不少。说到底,我只是一个普普通通的人,才不惊人,技不压众,接受自己的平凡,然后看自己做的,是否让自己满意就好。

//AN_Xml:

下面具体谈几点吧,我主要想聊下工作,绩效,EPC,嫡系看法,最后再谈下收获。

//AN_Xml:

工作情况

//AN_Xml:

我在腾讯内部没有转过岗,但是做过的项目也还是比较丰富的,包括:BUGLY、分布式调用链(Huskie)、众包系统(SOHO),EPC 度量系统。其中一些是对外的,一些是内部系统,可能有些大家不知道。还是比较感谢这些项目经历,既有纯业务的系统,也有偏框架的系统,让我学到了不少知识。

//AN_Xml:

接下来,简单介绍一下每个项目吧,毕竟每一个项目都付出了很多心血的:

//AN_Xml:

BUGLY,这是一个终端 Crash 联网上报的系统,很多 APP 都接入了。Huskie,这是一个基于 zipkin 搭建的分布式调用链跟踪项目。SOHO,这是一个众包系统,主要是将数据标准和语音采集任务众包出去,让人家做。EPC 度量系统,这是研发效能度量系统,主要是度量研发效能情况的。这里我谈一下对于业务开发的理解和认识,很多人可能都跟我最开始一样,有一个疑惑,整天做业务开发如何成长?换句话说,就是说整天做 CRUD,如何成长?我开始也有这样的疑惑,后来我转变了观念。

//AN_Xml:

我觉得对于系统的复杂度,可以粗略的分为技术复杂度和业务复杂度,对于业务系统,就是业务复杂度高一些,对于框架系统就是技术复杂度偏高一些。解决这两种复杂度,都具有很大的挑战。

//AN_Xml:

此前做过的众包系统,就是各种业务逻辑,搞过去,搞过来,其实这就是业务复杂度高。为了解决这个问题,我们开始探索和实践领域驱动(DDD),确实带来了一些帮助,不至于系统那么混乱了。同时,我觉得这个过程中,自己对于 DDD 的感悟,对于我后来的项目系统划分和设计以及开发都带来了帮助。

//AN_Xml:

当然 DDD 不是银弹,我也不是吹嘘它有多好,只是了解了它后,有时候设计和开发时,能换一种思路。

//AN_Xml:

可以发现,其实平时咱们做业务,想做好,其实也没那么容易,如果可以多探索多实践,将一些好的方法或思想或架构引入进来,与个人和业务都会有有帮助。

//AN_Xml:

绩效情况

//AN_Xml:

我在腾讯工作四年,腾讯半年考核一次,一共考核八次,回想了下,四年来的绩效情况为:三星,三星,五星,三星,五星,四星,四星,三星。统计一下, 四五星占比刚好一半。

//AN_Xml:

//AN_Xml:

PS:还好以前有奖杯,不然一点念想都没了。(现在腾讯似乎不发了)

//AN_Xml:

印象比较深的是两次五星获得经历。第一次五星是工作的第二年,那一年是在做众包项目,因为项目本身难度不大,因此我把一些精力投入到了团队的基础建设中,帮团队搭建了 java 以及 golang 的项目脚手架,又做了几次中心技术分享,最终 Leader 觉得我表现比较突出,因此给了我五星。看来,主动一些,与个人与团队都是有好处的,最终也能获得一些回报。

//AN_Xml:

第二次五星,就是与 EPC 有关了。说一个搞笑的事,我也是后来才知道的,项目初期,总监去汇报时,给老板演示系统,加载了很久指标才刷出来,总监很不好意思的说正在优化;过了一段时间,又去汇报演示,结果又很尴尬的刷了很久才出来,总监无赖表示还是在优化。没想到,自己曾经让总监这么丢脸,哈哈。好吧,说一下结果,最终,我自己写了一个查询引擎替换了 Mondrian,之后再也没有出现那种尴尬的情况了。随之而来,也给了好绩效鼓励。做 EPC 度量项目,我觉得自己成长很大,比如抗压能力,当你从零到一搭建一个系统时,会有一个先扛住再优化的过程,此外如果你的项目很重要,尤其是数据相关,那么任何一点问题,都可能让你神经紧绷,得想尽办法降低风险和故障。此外,另一个不同的感受就是,以前得项目,我大多是开发者,而这个系统,我是 Owner 负责人,当你 Owner 一个系统时,你得时刻负责,同时还需要思考系统的规划和方向,此外还需要分配好需求和把控进度,角色体验跟以前完全不一样。

//AN_Xml:

谈谈 EPC

//AN_Xml:

很多人都骂 EPC,或者笑 EPC,作为度量平台核心开发者之一,我来谈谈客观的看法。

//AN_Xml:

其实 EPC 初衷是好的,希望通过全方位多维度的研效指标,来度量研发效能各环节的质量,进而反推业务,提升研发效能。然而,最终在实践的过程中,才发现,客观条件并不支持(工具还没建设好);此外,一味的追求指标数据,使得下面的人想方设法让指标好看,最终违背了初衷。

//AN_Xml:

为什么,说 EPC 好了,其实如果你仔细了解下 EPC,你就会发现,他是一套相当完善且比较先进的指标度量体系。覆盖了需求,代码,缺陷,测试,持续集成,运营部署各个环节。

//AN_Xml:

此外,这个过程中,虽然一些人和一些业务做弊,但绝大多数业务还是做出了改变的,比如微视那边的人反馈是,以前的代码写的跟屎一样,当有了 EPC 后,代码质量好了很多。虽然最后微视还是亡了,但是大厦将倾,EPC 是救不了的,亡了也更不能怪 EPC。

//AN_Xml:

谈谈嫡系

//AN_Xml:

大家都说腾讯,嫡系文化盛行。但其实我觉得在那个公司都一样吧。这也符合事物的基本规律,人们只相信自己信任并熟悉的人。作为领导,你难道会把把重要的事情交给自己不熟悉的人吗?

//AN_Xml:

其实我也不知道我算不算嫡系,脉脉上有人问过”怎么知道自己算不算嫡系”,下面有一个回答,我觉得很妙:如果你不知道你是不是嫡系,那你就不是。哈哈,这么说来,我可能不是。

//AN_Xml:

但另一方面,后来我负责了团队内很重要的事情,应该是中心内都算很重要的事,我独自负责一个方向,直接向总监汇报,似乎又有点像。

//AN_Xml:

网上也有其他说法,一针见血,是不是嫡系,就看钱到不到位,这么说也有道理。我在 7 级时,就发了股票,自我感觉,还是不错的。我当时以为不出意外的话,我以后的钱途和发展是不是就会一帆风顺。不出意外就出了意外,第二年,EPC 不达预期,部门总经理和总监都被换了,中心来了一个新的总监。

//AN_Xml:

好吧,又要重新建立信任了。再到后来,是不是嫡系已经不重要了,因为大环境不好,又加上裁员,大家主动的被动的差不多都走了。

//AN_Xml:

总结一下,嫡系的存在,其实情有可原。怎么样成为嫡系了?其实我也不知道。不过,我觉得,与其思考怎么成为嫡系,不如思考怎么展现自己的价值和能力,当别人发现你的价值和能力了,那自然更多的机会就会给予你,有了机会,只要把握住了,那就有更多的福利了。

//AN_Xml:

再谈收获

//AN_Xml:

收获,什么叫做收获了?个人觉得无论是外在的物质,技能,职级;还是内在的感悟,认识,都算收获。

//AN_Xml:

先说一些可量化的吧,我觉得有:

//AN_Xml:
    //AN_Xml:
  • 级别上,升上了九级,高级工程师。虽然大家都在说腾讯职级缩水,但是有没有高工的能力自己其实是知道的,我个人感觉,通过我这几年的努力,我算是达到了我当时认为的我需要在高工时达到的状态;
  • //AN_Xml:
  • 绩效上,自我评价,个人不是一个特别卷的人,或者说不会为了卷而卷。但是,如果我认定我应该把它做好得,我的 Owner 意识,以及负责态度,我觉得还是可以的。最终在腾讯四年的绩效也还算过的去。再谈一些其他软技能方面:
  • //AN_Xml:
//AN_Xml:

1、文档能力

//AN_Xml:

作为程序员,文档能力其实是一项很重要的能力。其实我也没觉得自己文档能力有多好,但是前后两任总监,都说我的文档不错,那看来,我可能在平均水准之上。

//AN_Xml:

2、明确方向

//AN_Xml:

最后,说一个更虚的,但是我觉得最有价值的收获: 我逐渐明确了,或者确定了以后的方向和路,那就是走数据开发。

//AN_Xml:

其实,找到并确定一个目标很难,身边有清晰目标和方向的人很少,大多数是迷茫的。

//AN_Xml:

前一段时间,跟人聊天,谈到职业规划,说是可以从两个角度思考:

//AN_Xml:
    //AN_Xml:
  • 选一个业务方向,比如电商,广告,不断地积累业务领域知识和业务相关技能,随着经验的不断积累,最终你就是这个领域的专家。
  • //AN_Xml:
  • 深入一个技术方向,不断钻研底层技术知识,这样就有希望成为此技术专家。坦白来说,虽然我深入研究并实践过领域驱动设计,也用来建模和解决了一些复杂业务问题,但是发自内心的,我其实更喜欢钻研技术,同时,我又对大数据很感兴趣。因此,我决定了,以后的方向,就做数据相关的工作。
  • //AN_Xml:
//AN_Xml:

腾讯的四年,是我的第一份工作经历,认识了很多厉害的人,学到了很多。最后自己主动离开,也算走的体面(即使损失了大礼包),还是感谢腾讯。

//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Redis持久化机制详解 //AN_Xml: https://javaguide.cn/database/redis/redis-persistence.html //AN_Xml: https://javaguide.cn/database/redis/redis-persistence.html //AN_Xml: Redis持久化机制详解 //AN_Xml: 深入解析Redis三种持久化机制RDB快照、AOF日志和混合持久化的工作原理、配置方法和优缺点对比,帮助你选择适合业务场景的持久化策略。 //AN_Xml: 数据库 //AN_Xml: Thu, 23 Mar 2023 13:09:30 GMT //AN_Xml: 使用缓存的时候,我们经常需要对内存中的数据进行持久化也就是将内存中的数据写入到硬盘中。大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了做数据同步(比如 Redis 集群的主从节点通过 RDB 文件同步数据)。

//AN_Xml:

Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式:

//AN_Xml:
    //AN_Xml:
  • 快照(snapshotting,RDB)
  • //AN_Xml:
  • 只追加文件(append-only file, AOF)
  • //AN_Xml:
  • RDB 和 AOF 的混合持久化(Redis 4.0 新增)
  • //AN_Xml:
//AN_Xml:

官方文档地址:https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/

//AN_Xml:

//AN_Xml:

本文基于 Redis 7.0+ 版本。不同版本的持久化机制有重要差异,使用前请确认你的 Redis 版本:

//AN_Xml:

| 版本 | 持久化默认方式 | 重要特性 |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Redis常见阻塞原因总结 //AN_Xml: https://javaguide.cn/database/redis/redis-common-blocking-problems-summary.html //AN_Xml: https://javaguide.cn/database/redis/redis-common-blocking-problems-summary.html //AN_Xml: Redis常见阻塞原因总结 //AN_Xml: 全面总结Redis常见的阻塞原因,包括O(n)复杂度命令、bigkey操作、AOF日志刷盘、RDB快照创建、主从同步等场景,帮助你排查和预防Redis性能问题。 //AN_Xml: 数据库 //AN_Xml: Thu, 23 Mar 2023 10:25:39 GMT //AN_Xml: JavaGuide官方知识星球

//AN_Xml:
//AN_Xml:

本文整理完善自:https://mp.weixin.qq.com/s/0Nqfq_eQrUb12QH6eBbHXA ,作者:阿 Q 说代码

//AN_Xml:
//AN_Xml:

这篇文章会详细总结一下可能导致 Redis 阻塞的情况,这些情况也是影响 Redis 性能的关键因素,使用 Redis 的时候应该格外注意!

//AN_Xml:

O(n) 命令

//AN_Xml:

Redis 中的大部分命令都是 O(1)时间复杂度,但也有少部分 O(n) 时间复杂度的命令,例如:

//AN_Xml:
    //AN_Xml:
  • KEYS *:会返回所有符合规则的 key。
  • //AN_Xml:
  • HGETALL:会返回一个 Hash 中所有的键值对。
  • //AN_Xml:
  • LRANGE:会返回 List 中指定范围内的元素。
  • //AN_Xml:
  • SMEMBERS:返回 Set 中的所有元素。
  • //AN_Xml:
  • SINTER/SUNION/SDIFF:计算多个 Set 的交集/并集/差集。
  • //AN_Xml:
  • ……
  • //AN_Xml:
//AN_Xml:

由于这些命令时间复杂度是 O(n),有时候也会全表扫描,随着 n 的增大,执行耗时也会越长,从而导致客户端阻塞。不过, 这些命令并不是一定不能使用,但是需要明确 N 的值。另外,有遍历的需求可以使用 HSCANSSCANZSCAN 代替。

//AN_Xml:

除了这些 O(n)时间复杂度的命令可能会导致阻塞之外, 还有一些时间复杂度可能在 O(N) 以上的命令,例如:

//AN_Xml:
    //AN_Xml:
  • ZRANGE/ZREVRANGE:返回指定 Sorted Set 中指定排名范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 为返回的元素数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。
  • //AN_Xml:
  • ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 被删除元素的数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。
  • //AN_Xml:
  • ……
  • //AN_Xml:
//AN_Xml:

SAVE 创建 RDB 快照

//AN_Xml:

Redis 提供了两个命令来生成 RDB 快照文件:

//AN_Xml:
    //AN_Xml:
  • save : 同步保存操作,会阻塞 Redis 主线程;
  • //AN_Xml:
  • bgsave : fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。
  • //AN_Xml:
//AN_Xml:

默认情况下,Redis 默认配置会使用 bgsave 命令。如果手动使用 save 命令生成 RDB 快照文件的话,就会阻塞主线程。

//AN_Xml:

AOF

//AN_Xml:

AOF 日志记录阻塞

//AN_Xml:

Redis AOF 持久化机制是在执行完命令之后再记录日志,这和关系型数据库(如 MySQL)通常都是执行命令之前记录日志(方便故障恢复)不同。

//AN_Xml:

AOF 记录日志过程

//AN_Xml:

为什么是在执行完命令之后记录日志呢?

//AN_Xml:
    //AN_Xml:
  • 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查;
  • //AN_Xml:
  • 在命令执行完之后再记录,不会阻塞当前的命令执行。
  • //AN_Xml:
//AN_Xml:

这样也带来了风险(我在前面介绍 AOF 持久化的时候也提到过):

//AN_Xml:
    //AN_Xml:
  • 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失;
  • //AN_Xml:
  • 可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)
  • //AN_Xml:
//AN_Xml:

AOF 刷盘阻塞

//AN_Xml:

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再根据 appendfsync 配置来决定何时将其同步到硬盘中的 AOF 文件。

//AN_Xml:

在 Redis 的配置文件中存在三种不同的 AOF 持久化方式( fsync策略),它们分别是:

//AN_Xml:
    //AN_Xml:
  1. appendfsync always:主线程调用 write 执行写操作后,后台线程( aof_fsync 线程)立即会调用 fsync 函数同步 AOF 文件(刷盘),fsync 完成后线程返回,这样会严重降低 Redis 的性能(write + fsync)。
  2. //AN_Xml:
  3. appendfsync everysec:主线程调用 write 执行写操作后立即返回,由后台线程( aof_fsync 线程)每秒钟调用 fsync 函数(系统调用)同步一次 AOF 文件(write+fsyncfsync间隔为 1 秒)
  4. //AN_Xml:
  5. appendfsync no:主线程调用 write 执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(write但不fsyncfsync 的时机由操作系统决定)。
  6. //AN_Xml:
//AN_Xml:

当后台线程( aof_fsync 线程)调用 fsync 函数同步 AOF 文件时,需要等待,直到写入完成。当磁盘压力太大的时候,会导致 fsync 操作发生阻塞,主线程调用 write 函数时也会被阻塞。fsync 完成后,主线程执行 write 才能成功返回。

//AN_Xml:

关于 AOF 工作流程的详细介绍可以查看:Redis 持久化机制详解,有助于理解 AOF 刷盘阻塞。

//AN_Xml:

AOF 重写阻塞

//AN_Xml:
    //AN_Xml:
  1. fork 出一条子线程来将文件重写,在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子线程创建新 AOF 文件期间,记录服务器执行的所有写命令。
  2. //AN_Xml:
  3. 当子线程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。
  4. //AN_Xml:
  5. 最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。
  6. //AN_Xml:
//AN_Xml:

阻塞就是出现在第 2 步的过程中,将缓冲区中新数据写到新文件的过程中会产生阻塞

//AN_Xml:

相关阅读:Redis AOF 重写阻塞问题分析

//AN_Xml:

大 Key

//AN_Xml:

如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准:

//AN_Xml:
    //AN_Xml:
  • string 类型的 value 超过 1MB
  • //AN_Xml:
  • 复合类型(列表、哈希、集合、有序集合等)的 value 包含的元素超过 5000 个(对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。
  • //AN_Xml:
//AN_Xml:

大 key 造成的阻塞问题如下:

//AN_Xml:
    //AN_Xml:
  • 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
  • //AN_Xml:
  • 引发网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
  • //AN_Xml:
  • 阻塞工作线程:如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
  • //AN_Xml:
//AN_Xml:

查找大 key

//AN_Xml:

当我们在使用 Redis 自带的 --bigkeys 参数查找大 key 时,最好选择在从节点上执行该命令,因为主节点上执行时,会阻塞主节点。

//AN_Xml:
    //AN_Xml:
  • //AN_Xml:

    我们还可以使用 SCAN 命令来查找大 key;

    //AN_Xml:
  • //AN_Xml:
  • //AN_Xml:

    通过分析 RDB 文件来找出 big key,这种方案的前提是 Redis 采用的是 RDB 持久化。网上有现成的工具:

    //AN_Xml:
  • //AN_Xml:
  • //AN_Xml:
      //AN_Xml:
    • redis-rdb-tools:Python 语言写的用来分析 Redis 的 RDB 快照文件用的工具
    • //AN_Xml:
    • rdb_bigkeys:Go 语言写的用来分析 Redis 的 RDB 快照文件用的工具,性能更好。
    • //AN_Xml:
    //AN_Xml:
  • //AN_Xml:
//AN_Xml:

删除大 key

//AN_Xml:

删除操作的本质是要释放键值对占用的内存空间。

//AN_Xml:

释放内存只是第一步,为了更加高效地管理内存空间,在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程本身需要一定时间,而且会阻塞当前释放内存的应用程序。

//AN_Xml:

所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞,如果主线程发生了阻塞,其他所有请求可能都会超时,超时越来越多,会造成 Redis 连接耗尽,产生各种异常。

//AN_Xml:

删除大 key 时建议采用分批次删除和异步删除的方式进行。

//AN_Xml:

清空数据库

//AN_Xml:

清空数据库和上面 bigkey 删除也是同样道理,flushdbflushall 也涉及到删除和释放所有的键值对,也是 Redis 的阻塞点。

//AN_Xml:

集群扩容

//AN_Xml:

Redis 集群可以进行节点的动态扩容缩容,这一过程目前还处于半自动状态,需要人工介入。

//AN_Xml:

在扩缩容的时候,需要进行数据迁移。而 Redis 为了保证迁移的一致性,迁移所有操作都是同步操作。

//AN_Xml:

执行迁移时,两端的 Redis 均会进入时长不等的阻塞状态,对于小 Key,该时间可以忽略不计,但如果一旦 Key 的内存使用过大,严重的时候会触发集群内的故障转移,造成不必要的切换。

//AN_Xml:

Swap(内存交换)

//AN_Xml:

什么是 Swap? Swap 直译过来是交换的意思,Linux 中的 Swap 常被称为内存交换或者交换分区。类似于 Windows 中的虚拟内存,就是当内存不足的时候,把一部分硬盘空间虚拟成内存使用,从而解决内存容量不足的情况。因此,Swap 分区的作用就是牺牲硬盘,增加内存,解决 VPS 内存不够用或者爆满的问题。

//AN_Xml:

Swap 对于 Redis 来说是非常致命的,Redis 保证高性能的一个重要前提是所有的数据在内存中。如果操作系统把 Redis 使用的部分内存换出硬盘,由于内存与硬盘的读写速度差几个数量级,会导致发生交换后的 Redis 性能急剧下降。

//AN_Xml:

识别 Redis 发生 Swap 的检查方法如下:

//AN_Xml:

1、查询 Redis 进程号

//AN_Xml:
redis-cli -p 6383 info server | grep process_id
//AN_Xml:process_id: 4476
//AN_Xml:

2、根据进程号查询内存交换信息

//AN_Xml:
cat /proc/4476/smaps | grep Swap
//AN_Xml:Swap: 0kB
//AN_Xml:Swap: 0kB
//AN_Xml:Swap: 4kB
//AN_Xml:Swap: 0kB
//AN_Xml:Swap: 0kB
//AN_Xml:.....
//AN_Xml:

如果交换量都是 0KB 或者个别的是 4KB,则正常。

//AN_Xml:

预防内存交换的方法:

//AN_Xml:
    //AN_Xml:
  • 保证机器充足的可用内存
  • //AN_Xml:
  • 确保所有 Redis 实例设置最大可用内存(maxmemory),防止极端情况 Redis 内存不可控的增长
  • //AN_Xml:
  • 降低系统使用 swap 优先级,如echo 10 > /proc/sys/vm/swappiness
  • //AN_Xml:
//AN_Xml:

CPU 竞争

//AN_Xml:

Redis 是典型的 CPU 密集型应用,不建议和其他多核 CPU 密集型服务部署在一起。当其他进程过度消耗 CPU 时,将严重影响 Redis 的吞吐量。

//AN_Xml:

可以通过redis-cli --stat获取当前 Redis 使用情况。通过top命令获取进程对 CPU 的利用率等信息 通过info commandstats统计信息分析出命令不合理开销时间,查看是否是因为高算法复杂度或者过度的内存优化问题。

//AN_Xml:

网络问题

//AN_Xml:

连接拒绝、网络延迟,网卡软中断等网络问题也可能会导致 Redis 阻塞。

//AN_Xml:

参考

//AN_Xml: //AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: MySQL查询缓存详解 //AN_Xml: https://javaguide.cn/database/mysql/mysql-query-cache.html //AN_Xml: https://javaguide.cn/database/mysql/mysql-query-cache.html //AN_Xml: MySQL查询缓存详解 //AN_Xml: 深入解析MySQL查询缓存的工作原理、配置管理及其优缺点,分析为什么MySQL 8.0移除了查询缓存功能,以及生产环境中的最佳实践建议。 //AN_Xml: 数据库 //AN_Xml: Thu, 16 Mar 2023 03:33:45 GMT //AN_Xml: 缓存是一个有效且实用的系统性能优化手段,无论是操作系统,还是各类应用软件与 Web 服务,均广泛采用了缓存机制。

//AN_Xml:

然而,有经验的 DBA 都建议生产环境中把 MySQL 自带的 Query Cache(查询缓存)给关掉。而且,从 MySQL 5.7.20 开始,就已经默认弃用查询缓存了。在 MySQL 8.0 及之后,更是直接删除了查询缓存的功能。

//AN_Xml:

这又是为什么呢?查询缓存真就这么鸡肋么?

//AN_Xml:

带着如下几个问题,我们正式进入本文。

//AN_Xml:
    //AN_Xml:
  • MySQL 查询缓存是什么?适用范围?
  • //AN_Xml:
  • MySQL 缓存规则是什么?
  • //AN_Xml:
  • MySQL 缓存的优缺点是什么?
  • //AN_Xml:
  • MySQL 缓存对性能有什么影响?
  • //AN_Xml:
//AN_Xml:

MySQL 查询缓存介绍

//AN_Xml:

MySQL 体系架构如下图所示:

//AN_Xml:

//AN_Xml:

为了提高完全相同的查询语句的响应速度,MySQL Server 会对查询语句进行 Hash 计算得到一个 Hash 值。MySQL Server 不会对 SQL 做任何处理,SQL 必须完全一致 Hash 值才会一样。得到 Hash 值之后,通过该 Hash 值到查询缓存中匹配该查询的结果。

//AN_Xml:
    //AN_Xml:
  • 如果匹配(命中),则将查询的结果集直接返回给客户端,不必再解析、执行查询。
  • //AN_Xml:
  • 如果没有匹配(未命中),则将 Hash 值和结果集保存在查询缓存中,以便以后使用。
  • //AN_Xml:
//AN_Xml:

也就是说,一个查询语句(select)到了 MySQL Server 之后,会先到查询缓存看看,如果曾经执行过的话,就直接返回结果集给客户端。

//AN_Xml:

//AN_Xml:

MySQL 查询缓存管理和配置

//AN_Xml:

通过 show variables like '%query_cache%'命令可以查看查询缓存相关的信息。

//AN_Xml:

8.0 版本之前的话,打印的信息可能是下面这样的:

//AN_Xml:
mysql> show variables like '%query_cache%';
//AN_Xml:+
//AN_Xml:
]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 程序员的技术成长战略 //AN_Xml: https://javaguide.cn/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.html //AN_Xml: https://javaguide.cn/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.html //AN_Xml: 程序员的技术成长战略 //AN_Xml: 程序员的技术成长战略:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。 //AN_Xml: 技术文章精选集 //AN_Xml: Thu, 23 Feb 2023 04:45:00 GMT //AN_Xml: //AN_Xml:

推荐语:波波老师的一篇文章,写的非常好,不光是对技术成长有帮助,其他领域也是同样适用的!建议反复阅读,形成一套自己的技术成长策略。

//AN_Xml:

原文地址: https://mp.weixin.qq.com/s/YrN8T67s801-MRo01lCHXA

//AN_Xml: //AN_Xml:

1. 前言

//AN_Xml:

在波波的微信技术交流群里头,经常有学员问关于技术人该如何学习成长的问题,虽然是微信交流,但我依然可以感受到小伙伴们焦虑的心情。

//AN_Xml:

技术人为啥焦虑? 恕我直言,说白了是胆识不足格局太小。胆就是胆量,焦虑的人一般对未来的不确定性怀有恐惧。识就是见识,焦虑的人一般看不清楚周围世界,也看不清自己和适合自己的道路。格局也称志向,容易焦虑的人通常视野窄志向小。如果从战略和管理的视角来看,就是对自己和周围世界的认知不足,没有一个清晰和长期的学习成长战略,也没有可执行的阶段性目标计划+严格的执行。

//AN_Xml:

因为问此类问题的学员很多,让我感觉有点烦了,为了避免重复回答,所以我专门总结梳理了这篇长文,试图统一来回答这类问题。如果后面还有学员问类似问题,我会引导他们来读这篇文章,然后让他们用三个月、一年甚至更长的时间,去思考和回答这样一个问题:你的技术成长战略究竟是什么? 如果你想清楚了这个问题,有清晰和可落地的答案,那么恭喜你,你只需按部就班执行就好,根本无需焦虑,你实现自己的战略目标并做出成就只是一个时间问题;否则,你仍然需要通过不断磨炼+思考,务必去搞清楚这个人生的大问题!!!

//AN_Xml:

下面我们来看一些行业技术大牛是怎么做的。

//AN_Xml:

二. 跟技术大牛学成长战略

//AN_Xml:

我们知道软件设计是有设计模式(Design Pattern)的,其实技术人的成长也是有成长模式(Growth Pattern)的。波波经常在 Linkedin 上看一些技术大牛的成长履历,探究其中的成长模式,从而启发制定自己的技术成长战略。

//AN_Xml:

当然,很少有技术大牛会清晰地告诉你他们的技术成长战略,以及每一年的细分落地计划。但是,这并不妨碍我们通过他们的过往履历和产出成果,去溯源他们的技术成长战略。实际上, 越是牛逼的技术人,他们的技术成长战略和路径越是清晰,我们越容易从中探究出一些成功的模式。

//AN_Xml:

2.1 系统性能专家案例

//AN_Xml:

国内的开发者大都热衷于系统性能优化,有些人甚至三句话离不开高性能/高并发,但真正能深入这个领域,做到专家级水平的却寥寥无几。

//AN_Xml:

我这边要特别介绍的这个技术大牛叫 Brendan Gregg ,他是系统性能领域经典书《System Performance: Enterprise and the Cloud》(中文版《性能之巅:洞悉系统、企业和云计算》)的作者,也是著名的性能分析利器火焰图(Flame Graph)的作者。

//AN_Xml:

Brendan Gregg 之前是 Netflix 公司的高级性能架构师,在 Netflix 工作近 7 年。2022 年 4 月,他离开了 Netflix 去了 Intel,担任院士职位。

//AN_Xml:

//AN_Xml:

总体上,他已经在系统性能领域深耕超过 10 年,Brendan Gregg 的过往履历可以在 linkedin 上看到。在这 10 年间,除了书籍以外,Brendan Gregg 还产出了超过上百份和系统性能相关的技术文档,演讲视频/ppt,还有各种工具软件,相关内容都整整齐齐地分享在他的技术博客上,可以说他是一个非常高产的技术大牛。

//AN_Xml:

性能工具

//AN_Xml:

上图来自 Brendan Gregg 的新书《BPF Performance Tools: Linux System and Application Observability》。从这个图可以看出,Brendan Gregg 对系统性能领域的掌握程度,已经深挖到了硬件、操作系统和应用的每一个角落,可以说是 360 度无死角,整个计算机系统对他来说几乎都是透明的。波波认为,Brendan Gregg 是名副其实的,世界级的,系统性能领域的大神级人物。

//AN_Xml:

2.2 从开源到企业案例

//AN_Xml:

我要分享的第二个技术大牛是 Jay Kreps,他是知名的开源消息中间件 Kafka 的创始人/架构师,也是 Confluent 公司的联合创始人和 CEO,Confluent 公司是围绕 Kafka 开发企业级产品和服务的技术公司。

//AN_Xml:

Jay Kreps 的 Linkedin 的履历上我们可以看出,Jay Kreps 之前在 Linkedin 工作了 7 年多(2007.6 ~ 2014. 9),从高级工程师、工程主管,一直做到首席资深工程师。Kafka 大致是在 2010 年,Jay Kreps 在 Linkedin 发起的一个项目,解决 Linkedin 内部的大数据采集、存储和消费问题。之后,他和他的团队一直专注 Kafka 的打磨,开源(2011 年初)和社区生态的建设。

//AN_Xml:

到 2014 年底,Kafka 在社区已经非常成功,有了一个比较大的用户群,于是 Jay Kreps 就和几个早期作者一起离开了 Linkedin,成立了Confluent 公司,开始了 Kafka 和周边产品的企业化服务道路。今年(2020.4 月),Confluent 公司已经获得 E 轮 2.5 亿美金融资,公司估值达到 45 亿美金。从 Kafka 诞生到现在,Jay Kreps 差不多在这个产品和公司上投入了整整 10 年。

//AN_Xml:

Confluent创始人三人组

//AN_Xml:

上图是 Confluent 创始人三人组,一个非常有意思的组合,一个中国人(左),一个印度人(右),中间的 Jay Kreps 是美国人。

//AN_Xml:

我之所以对 Kafka 和 Jay Kreps 的印象特别深刻,是因为在 2012 年下半年,我在携程框架部也是专门搞大数据采集的,我还开发过一套功能类似 Kafka 的 Log Collector + Agent 产品。我记得同时期有不止 4 个同类型的开源产品:Facebook Scribe、Apache Chukwa、Apache Flume 和 Apache Kafka。现在回头看,只有 Kafka 走到现在发展得最好,这个和创始人的专注和持续投入是分不开的,当然背后和几个创始人的技术大格局也是分不开的。

//AN_Xml:

当年我对战略性思维几乎没有概念,还处在什么技术都想学、认为各种项目做得越多越牛的阶段。搞了半年的数据采集以后,我就掉头搞其它“更有趣的”项目去了(从这个事情的侧面,也可以看出我当年的技术格局是很小的)。中间我陆续关注过 Jay 的一些创业动向,但是没想到他能把 Confluent 公司发展到目前这个规模。现在回想,其实在十年前,Jay Kreps 对自己的技术成长就有比较明确的战略性思考,也具有大的技术格局和成事的一些必要特质。Jay Kreps 和 Kafka 给我上了一堂生动的技术战略和实践课。

//AN_Xml:

2.3 技术媒体大 V 案例

//AN_Xml:

介绍到这里,有些同学可能会反驳说:波波你讲的这些大牛都是学历背景好,功底扎实起点高,所以他们才更能成功。其实不然,这里我再要介绍一位技术媒体界的大 V 叫 Brad Traversy,大家可以看他的 Linkedin 简历,背景很一般,学历差不多是一个非正规的社区大学(相当于大专),没有正规大厂工作经历,有限几份工作一直是在做网站外包。

//AN_Xml:

//AN_Xml:

但是!!!Brad Traversy 目前是技术媒体领域的一个大 V,当前他在 Youtube 上的频道有 138 万多的订阅量,10 年累计输出 Web 开发和编程相关教学视频超过 800 个。Brad Traversy 也是 Udemy 上的一个成功讲师,目前已经在 Udemy 上累计输出课程 19 门,购课学生数量近 42 万。

//AN_Xml:

//AN_Xml:

Brad Traversy 目前是自由职业者,他的 Youtube 广告+Udemy 课程的收入相当不错。

//AN_Xml:

就是这样一位技术媒体大 V,你很难想象,在年轻的时候,贴在他身上的标签是:不良少年,酗酒,抽烟,吸毒,纹身,进监狱。。。直

//AN_Xml:

到结婚后的第一个孩子诞生,他才开始担起责任做出改变,然后凭借对技术的一腔热情,开始在 Youtube 平台上持续输出免费课程。从此他找到了适合自己的战略目标,然后人生开始发生各种积极的变化。。。如果大家对 Brad Traversy 的过往经历感兴趣,推荐观看他在 Youtube 上的自述视频《My Struggles & Success》

//AN_Xml:

My Struggles & Success

//AN_Xml:

我粗略浏览了Brad Traversy 在 Youtube 上的所有视频,10 年总计输出 800+视频,平均每年 80+。第一个视频提交于 2010 年 8 月,刚开始几年几乎没有订阅量,2017 年 1 月订阅量才到 50k,这中间差不多隔了 6 年。2017.10 月订阅量猛增到 200k,2018 年 3 月订阅量到 300k。当前 2021.1 月,订阅量达到 138 万。可以认为从 2017 开始,也就是在积累了 6 ~ 7 年后,他的订阅量开始出现拐点。如果把这些数据画出来,将会是一条非常漂亮的复利曲线

//AN_Xml:

2.4 案例小结

//AN_Xml:

Brendan Gregg,Jay Kreps 和 Brad Traversy 三个人走的技术路线各不相同,但是他们的成功具有共性或者说模式:

//AN_Xml:

1、找到了适合自己的长期战略目标。

//AN_Xml:
    //AN_Xml:
  • Brendan Gregg: 成为系统性能领域顶级专家
  • //AN_Xml:
  • Jay Kreps:开创基于 Kafka 开源消息队列的企业服务公司,并将公司做到上市
  • //AN_Xml:
  • Brad Traversy: 成为技术媒体领域大 V 和课程讲师,并以此作为自己的职业
  • //AN_Xml:
//AN_Xml:

2、专注深耕一个(或有限几个相关的)细分领域(Niche),保持定力,不随便切换领域。

//AN_Xml:
    //AN_Xml:
  • Brendan Gregg:系统性能领域
  • //AN_Xml:
  • Jay Kreps: 消息中间件/实时计算领域+创业
  • //AN_Xml:
  • Brad Traversy: 技术媒体/教学领域,方向 Web 开发 + 编程语言
  • //AN_Xml:
//AN_Xml:

3、长期投入,三人都持续投入了 10 年。

//AN_Xml:

4、年度细分计划+持续可量化的价值产出(Persistent & Measurable Value Output)。

//AN_Xml:
    //AN_Xml:
  • Brendan Gregg:除公司日常工作产出以外,每年有超过 10 份以上的技术文档和演讲视频产出,平均每年有 2.5 个开源工具产出。十年共产出书籍 2 本,其中《System Performance》已经更新到第二版。
  • //AN_Xml:
  • Jay Kreps:总体有开源产品+公司产出,1 本书产出,每年有 Kafka 和周边产品发版若干。
  • //AN_Xml:
  • Brad Traversy: 每年有 Youtube 免费视频产出(平均每年 80+)+Udemy 收费视频课产出(平均每年 1.5 门)。
  • //AN_Xml:
//AN_Xml:

5、以终为始是牛人和普通人的一大区别。

//AN_Xml:

普通人通常走一步算一步,很少长远规划。牛人通 常是先有远大目标,然后采用倒推法,将大目标细化到每年/月/周的详细落地计划。Brendan Gregg,Jay Kreps 和 Brad Traversy 三人都是以终为始的典型。

//AN_Xml:

以终为始

//AN_Xml:

上面总结了几位技术大牛的成长模式,其中一个重点就是:这些大牛的成长都是通过 持续有价值产出(Persistent Valuable Output) 来驱动的。持续产出为啥如此重要,这个还要从下面的学习金字塔说起。

//AN_Xml:

三、学习金字塔和刻意训练

//AN_Xml:

学习金字塔

//AN_Xml:

学习金字塔是美国缅因州国家训练实验室的研究成果,它认为:

//AN_Xml:
//AN_Xml:
    //AN_Xml:
  1. 我们平时上课听讲之后,学习内容平均留存率大致只有 5%左右;
  2. //AN_Xml:
  3. 书本阅读的平均留存率大致只有 10%左右;
  4. //AN_Xml:
  5. 学习配上视听效果的课程,平均留存率大致在 20%左右;
  6. //AN_Xml:
  7. 老师实际动手做实验演示后的平均留存率大致在 30%左右;
  8. //AN_Xml:
  9. 小组讨论(尤其是辩论后)的平均留存率可以达到 50%左右;
  10. //AN_Xml:
  11. 在实践中实际应用所学之后,平均留存率可以达到 75%左右;
  12. //AN_Xml:
  13. 在实践的基础上,再把所学梳理出来,转而再传授给他人后,平均留存率可以达到 90%左右。
  14. //AN_Xml:
//AN_Xml:
//AN_Xml:

上面列出的 7 种学习方法,前四种称为 被动学习 ,后三种称为 主动学习

//AN_Xml:

拿学游泳做个类比,被动学习相当于你看别人游泳,而主动学习则是你自己要下水去游。我们知道游泳或者跑步之类的运动是要燃烧身体卡路里的,这样才能达到锻炼身体和长肌肉的效果(肌肉是卡路里燃烧的结果)。如果你只是看别人游泳,自己不实际去游,是不会长肌肉的。同样的,主动学习也是要燃烧脑部卡路里的,这样才能达到训练大脑和长脑部“肌肉”的效果。

//AN_Xml:

我们也知道,燃烧身体的卡路里,通常会让人感觉不舒适,如果燃烧身体卡路里会让人感觉舒适的话,估计这个世界上应该不会有胖子这类人。同样,燃烧脑部卡路里也会让人感觉不适、紧张、出汗或语无伦次,如果燃烧脑部卡路里会让人感觉舒适的话,估计这个世界上人人都很聪明,人人都能发挥最大潜能。当然,这些不舒适是短期的,长期会使你更健康和聪明。波波一直认为, 人与人之间的先天身体其实都差不多,但是后天身体素质和能力有差异,这些差异,很大程度是由后天对身体和大脑的训练质量、频度和强度所造成的。

//AN_Xml:

明白这个道理之后,心智成熟和自律的人就会对自己进行持续地 刻意训练 。这个刻意训练包括对身体的训练,比如波波现在每天坚持跑步 3km,走 3km,每天做 60 个仰卧起坐,5 分钟平板撑等等,每天保持让身体燃烧一定量的卡路里。刻意训练也包括对大脑的训练,比如波波现在每天做项目写代码 coding(训练脑+手),平均每天在 B 站上输出十分钟免费视频(训练脑+口头表达),另外有定期总结输出公众号文章(训练脑+文字表达),还有每天打半小时左右的平衡球(下图)或古墓丽影游戏(训练小脑+手),每天保持让大脑燃烧一定量的卡路里,并保持一定强度(适度不适感)。

//AN_Xml:

平衡球游戏

//AN_Xml:

关于刻意训练的专业原理和方法论,推荐看书籍《刻意练习》。

//AN_Xml:

刻意练习

//AN_Xml:

注意,如果你平时从来不做举重锻炼的,那么某天突然做举重会很不适应甚至受伤。脑部训练也是一样的,如果你从来没有做过视频输出,那么刚开始做会很不适应,做出来的视频质量会很差。不过没有关系,任何训练都是一个循序渐进,不断强化的过程。等大脑相关区域的"肌肉"长出来以后,会逐步进入正循环,后面会越来越顺畅,相关"肌肉"会越来越发达。所以,和健身一样,健脑也不能遇到困难就放弃,需要循序渐进(Incremental)+持续地(Persistent)刻意训练。

//AN_Xml:

理解了学习金字塔和刻意训练以后,现在再来看 Brendan Gregg,Jay Kreps 和 Brad Traversy 这些大牛的做法,他们的学习成长都是建立在持续有价值产出的基础上的,这些产出都是刻意训练+燃烧脑部卡路里的成果。他们的产出要么是建立在实践基础上的产出,例如 Jay Kreps 的 Kafka 开源项目和 Confluent 公司;要么是在实践的基础上,再整理传授给其他人的产出,例如,Brendan Greeg 的技术演讲 ppt/视频,书籍,还有 Brad Traversy 的教学视频等等。换句话说,他们一直在学习金字塔的 5 ~ 7 层主动和高效地学习。并且,他们的学习产出还可以获得用户使用,有客户价值(Customer Value),有用户就有反馈和度量。记住,有反馈和度量的学习,也称闭环学习,它是能够不断改进提升的;反之,没有反馈和度量的学习,无法改进提升。

//AN_Xml:

现在,你也应该明白,晒个书单秀个技能图谱很简单,读个书上个课也不难。但是要你给出 5 ~ 10 年的总体技术成长战略,再基于这个战略给出每年的细分落地计划(尤其是产出计划),然后再严格按计划执行,这的确是很难的事情。这需要大量的实践训练+深度思考,要燃烧大量的脑部卡路里!但这是上天设置的进化法则,成长为真正的技术大牛如同成长为一流的运动员,是需要通过燃烧与之相匹配量的卡路里来交换的。成长为真正的技术大牛,也是需要通过产出与之匹配的社会价值来交换的,只有这样社会才能正常进化。你推进了社会进化,社会才会回馈你。如果不是这样,社会就无法正常进化。

//AN_Xml:

四、战略思维的诞生

//AN_Xml:

思考周期和机会点

//AN_Xml:

一般毕业生刚进入企业工作的时候,思考大都是以天/星期/月为单位的,基本上都是今天学个什么技术,明天学个什么语言,很少会去思考一年甚至更长的目标。这是个眼前漆黑看不到的懵懂时期,捕捉到机会点的能力和概率都非常小。

//AN_Xml:

工作了三年以后,悟性好的人通常会以一年为思考周期,制定和实施一些年度计划。这个时期是相信天赋和比拼能力的阶段,可以捕捉到一些小机会。

//AN_Xml:

工作了五年以后,一些悟性好的人会产生出一定的胆识和眼光,他们会以 3 ~ 5 年为周期来制定和实施计划,开始主动布局去捕捉一些中型机会点。

//AN_Xml:

工作了十年以后,悟性高的人会看到模式和规则变化,例如看出行业发展模式,还有人才的成长模式等,于是开始诞生出战略性思维。然后他们会以 5 ~ 10 年为周期来制定和实施自己的战略计划,开始主动布局去捕捉一些中大机会点。Brendan Gregg,Jay Kreps 和 Brad Traversy 都是属于这个阶段的人。

//AN_Xml:

当然还有很少一些更牛的时代精英,他们能够看透时代和人性,他们的思考是以一生甚至更长时间为单位的,这些超人不在本文讨论范围内。

//AN_Xml:

五、建议

//AN_Xml:

1、以 5 ~ 10 年为周期去布局谋划你的战略。

//AN_Xml:

现在大学生毕业的年龄一般在 22 ~ 23 岁,那么在工作了十年后,也就是在你 32 ~ 33 岁的时候,你也差不多看了十年了,应该对自己和周围的世界(你的行业和领域)有一个比较深刻的领悟了。如果你到这个年纪还懵懵懂懂,今天抓东明天抓西,那么只能说你的胆识格局是相当的低。在当前 IT 行业竞争这么激烈的情况下,到 35 岁被下岗可能就在眼前了。

//AN_Xml:

有了战略性思考,你应该以 5 ~ 10 年为周期去布局谋划你的战略。以 Brendan Gregg,Jay Kreps 和 Brad Traversy 这些大牛为例,人生若真的要干点成就出来,投入周期一般都要十年的。从 33 岁开始,你大致有 3 个十年,因为到 60 岁以后,一般人都老眼昏花干不了大事了。如果你悟性差一点,到 40 岁才开始规划,那么你大致还有 2 个十年。如果你规划好了,这 2 ~ 3 个十年可以成就不小的事业。否则,你很可能一生都成就不了什么事业,或者一直在帮助别人成就别人的事业。

//AN_Xml:

2、专注自己的精力。

//AN_Xml:

考虑到人生能干事业的时间也就是 2 ~ 3 个十年,你会发现人生其实很短暂,这时候你会把精力都投入到实现你的十年战略上去,没有时间再浪费在比如网上的闲聊和扯皮争论上去。

//AN_Xml:

3、细分落地计划尤其是产出计划。

//AN_Xml:

有了十年战略方向,下一步是每年的细分落地计划,尤其是产出计划。这些计划主要应该工作在学习金字塔的 5/6/7 层。产出应该是刻意训练+燃烧卡路里的结果,每天让身体和大脑都保持燃烧一定量的卡路里

//AN_Xml:

4、产出有价值的东西形成正反馈。

//AN_Xml:

产出应该有客户价值,自己能学习(自己成长进化),对别人还有用(推动社会成长进化),这样可以得到用户回馈和度量,形成一个闭环,可以持续改进和提升你的学习。

//AN_Xml:

5、少即是多。

//AN_Xml:

深耕一个(或有限几个相关的)领域。所有细分计划应该紧密围绕你的战略展开。克制内心欲望,不要贪多和分心,不要被喧嚣的世界所迷惑。

//AN_Xml:

6、战略方向+细分计划都要写下来,定期 review 优化。

//AN_Xml:

7、要有定力,持续努力。

//AN_Xml:

曲则全、枉则直,战略实现是不可能直线的。战略方向和细分计划通常要按需调整,尤其在早期,但是最终要收敛。如果老是变不收敛,就是缺乏战略定力,是个必须思考和解决的大问题。

//AN_Xml:

别人的成长战略可以参考,但是不要刻意去模仿,你有你自己的颜色,你应该成为独一无二的你

//AN_Xml:

战略方向和细分计划明确了,接下来就是按部就班执行,十年如一日铁打不动。

//AN_Xml:

8、慢就是快。

//AN_Xml:

战略目标的实现也和种树一样是生长出来的,需要时间耐心栽培,记住**慢就是快。**焦虑纠结的时候,像念经一样默念王阳明《传习录》中的教诲:

//AN_Xml:
//AN_Xml:

立志用功,如种树然。方其根芽,犹未有干;及其有干,尚未有枝;枝而后叶,叶而后花实。初种根时,只管栽培灌溉。勿作枝想,勿作花想,勿作实想。悬想何益?但不忘栽培之功,怕没有枝叶花实?

//AN_Xml:

译文:

//AN_Xml:

实现战略目标,就像种树一样。刚开始只是一个小根芽,树干还没有长出来;树干长出来了,枝叶才能慢慢长出来;树枝长出来,然后才能开花和结果。刚开始种树的时候,只管栽培灌溉,别老是纠结枝什么时候长出来,花什么时候开,果实什么时候结出来。纠结有什么好处呢?只要你坚持投入栽培,还怕没有枝叶花实吗?

//AN_Xml:
//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 网络攻击常见手段总结 //AN_Xml: https://javaguide.cn/cs-basics/network/network-attack-means.html //AN_Xml: https://javaguide.cn/cs-basics/network/network-attack-means.html //AN_Xml: 网络攻击常见手段总结 //AN_Xml: 总结常见 TCP/IP 攻击与防护思路,覆盖 DDoS、IP/ARP 欺骗、中间人等手段,强调工程防护实践。 //AN_Xml: 计算机基础 //AN_Xml: Sun, 19 Feb 2023 09:28:45 GMT //AN_Xml: //AN_Xml:

本文整理完善自TCP/IP 常见攻击手段 - 暖蓝笔记 - 2021这篇文章。

//AN_Xml: //AN_Xml:

这篇文章的内容主要是介绍 TCP/IP 常见攻击手段,尤其是 DDoS 攻击,也会补充一些其他的常见网络攻击手段。

//AN_Xml:

IP 欺骗

//AN_Xml:

IP 是什么?

//AN_Xml:

在网络中,所有的设备都会分配一个地址。这个地址就仿佛小蓝的家地址「多少号多少室」,这个号就是分配给整个子网的,「」对应的号码即分配给子网中计算机的,这就是网络中的地址。「号」对应的号码为网络号,「」对应的号码为主机号,这个地址的整体就是 IP 地址

//AN_Xml:

通过 IP 地址我们能知道什么?

//AN_Xml:

通过 IP 地址,我们就可以知道判断访问对象服务器的位置,从而将消息发送到服务器。一般发送者发出的消息首先经过子网的集线器,转发到最近的路由器,然后根据路由位置访问下一个路由器的位置,直到终点

//AN_Xml:

IP 头部格式 :

//AN_Xml:

//AN_Xml:

IP 欺骗技术是什么?

//AN_Xml:

骗呗,拐骗,诱骗!

//AN_Xml:

IP 欺骗技术就是伪造某台主机的 IP 地址的技术。通过 IP 地址的伪装使得某台主机能够伪装另外的一台主机,而这台主机往往具有某种特权或者被另外的主机所信任。

//AN_Xml:

假设现在有一个合法用户 (1.1.1.1) 已经同服务器建立正常的连接,攻击者构造攻击的 TCP 数据,伪装自己的 IP 为 1.1.1.1,并向服务器发送一个带有 RST 位的 TCP 数据段。服务器接收到这样的数据后,认为从 1.1.1.1 发送的连接有错误,就会清空缓冲区中建立好的连接。

//AN_Xml:

这时,如果合法用户 1.1.1.1 再发送合法数据,服务器就已经没有这样的连接了,该用户就必须从新开始建立连接。攻击时,伪造大量的 IP 地址,向目标发送 RST 数据,使服务器不对合法用户服务。虽然 IP 地址欺骗攻击有着相当难度,但我们应该清醒地意识到,这种攻击非常广泛,入侵往往从这种攻击开始。

//AN_Xml:

IP 欺骗 DDoS 攻击

//AN_Xml:

如何缓解 IP 欺骗?

//AN_Xml:

虽然无法预防 IP 欺骗,但可以采取措施来阻止伪造数据包渗透网络。入口过滤 是防范欺骗的一种极为常见的防御措施,如 BCP38(通用最佳实践文档)所示。入口过滤是一种数据包过滤形式,通常在网络边缘设备上实施,用于检查传入的 IP 数据包并确定其源标头。如果这些数据包的源标头与其来源不匹配或者看上去很可疑,则拒绝这些数据包。一些网络还实施出口过滤,检查退出网络的 IP 数据包,确保这些数据包具有合法源标头,以防止网络内部用户使用 IP 欺骗技术发起出站恶意攻击。

//AN_Xml:

SYN Flood(洪水)

//AN_Xml:

SYN Flood 是什么?

//AN_Xml:

SYN Flood 是互联网上最原始、最经典的 DDoS(Distributed Denial of Service,分布式拒绝服务)攻击之一,旨在耗尽可用服务器资源,致使服务器无法传输合法流量

//AN_Xml:

SYN Flood 利用了 TCP 协议的三次握手机制,攻击者通常利用工具或者控制僵尸主机向服务器发送海量的变源 IP 地址或变源端口的 TCP SYN 报文,服务器响应了这些报文后就会生成大量的半连接,当系统资源被耗尽后,服务器将无法提供正常的服务。
//AN_Xml:增加服务器性能,提供更多的连接能力对于 SYN Flood 的海量报文来说杯水车薪,防御 SYN Flood 的关键在于判断哪些连接请求来自于真实源,屏蔽非真实源的请求以保障正常的业务请求能得到服务。

//AN_Xml:

//AN_Xml:

TCP SYN Flood 攻击原理是什么?

//AN_Xml:

TCP SYN Flood 攻击利用的是 TCP 的三次握手(SYN -> SYN/ACK -> ACK),假设连接发起方是 A,连接接受方是 B,即 B 在某个端口(Port)上监听 A 发出的连接请求,过程如下图所示,左边是 A,右边是 B。

//AN_Xml:

//AN_Xml:

A 首先发送 SYN(Synchronization)消息给 B,要求 B 做好接收数据的准备;B 收到后反馈 SYN-ACK(Synchronization-Acknowledgement) 消息给 A,这个消息的目的有两个:

//AN_Xml:
    //AN_Xml:
  • 向 A 确认已做好接收数据的准备,
  • //AN_Xml:
  • 同时要求 A 也做好接收数据的准备,此时 B 已向 A 确认好接收状态,并等待 A 的确认,连接处于半开状态(Half-Open),顾名思义只开了一半;A 收到后再次发送 ACK (Acknowledgement) 消息给 B,向 B 确认也做好了接收数据的准备,至此三次握手完成,「连接」就建立了,
  • //AN_Xml:
//AN_Xml:

大家注意到没有,最关键的一点在于双方是否都按对方的要求进入了可以接收消息的状态。而这个状态的确认主要是双方将要使用的消息序号(SequenceNum),TCP 为保证消息按发送顺序抵达接收方的上层应用,需要用消息序号来标记消息的发送先后顺序的。

//AN_Xml:

TCP是「双工」(Duplex)连接,同时支持双向通信,也就是双方同时可向对方发送消息,其中 SYNSYN-ACK 消息开启了 A→B 的单向通信通道(B 获知了 A 的消息序号);SYN-ACKACK 消息开启了 B→A 单向通信通道(A 获知了 B 的消息序号)。

//AN_Xml:

上面讨论的是双方在诚实守信,正常情况下的通信。

//AN_Xml:

但实际情况是,网络可能不稳定会丢包,使握手消息不能抵达对方,也可能是对方故意不按规矩来,故意延迟或不发送握手确认消息。

//AN_Xml:

假设 B 通过某 TCP 端口提供服务,B 在收到 A 的 SYN 消息时,积极的反馈了 SYN-ACK 消息,使连接进入半开状态,因为 B 不确定自己发给 A 的 SYN-ACK 消息或 A 反馈的 ACK 消息是否会丢在半路,所以会给每个待完成的半开连接都设一个Timer,如果超过时间还没有收到 A 的 ACK 消息,则重新发送一次 SYN-ACK 消息给 A,直到重试超过一定次数时才会放弃。

//AN_Xml:

图片

//AN_Xml:

B 为帮助 A 能顺利连接,需要分配内核资源维护半开连接,那么当 B 面临海量的连接 A 时,如上图所示,SYN Flood 攻击就形成了。攻击方 A 可以控制肉鸡向 B 发送大量 SYN 消息但不响应 ACK 消息,或者干脆伪造 SYN 消息中的 Source IP,使 B 反馈的 SYN-ACK 消息石沉大海,导致 B 被大量注定不能完成的半开连接占据,直到资源耗尽,停止响应正常的连接请求。

//AN_Xml:

SYN Flood 的常见形式有哪些?

//AN_Xml:

恶意用户可通过三种不同方式发起 SYN Flood 攻击

//AN_Xml:
    //AN_Xml:
  1. 直接攻击: 不伪造 IP 地址的 SYN 洪水攻击称为直接攻击。在此类攻击中,攻击者完全不屏蔽其 IP 地址。由于攻击者使用具有真实 IP 地址的单一源设备发起攻击,因此很容易发现并清理攻击者。为使目标机器呈现半开状态,黑客将阻止个人机器对服务器的 SYN-ACK 数据包做出响应。为此,通常采用以下两种方式实现:部署防火墙规则,阻止除 SYN 数据包以外的各类传出数据包;或者,对传入的所有 SYN-ACK 数据包进行过滤,防止其到达恶意用户机器。实际上,这种方法很少使用(即便使用过也不多见),因为此类攻击相当容易缓解 – 只需阻止每个恶意系统的 IP 地址。哪怕攻击者使用僵尸网络(如 Mirai 僵尸网络),通常也不会刻意屏蔽受感染设备的 IP。
  2. //AN_Xml:
  3. 欺骗攻击: 恶意用户还可以伪造其发送的各个 SYN 数据包的 IP 地址,以便阻止缓解措施并加大身份暴露难度。虽然数据包可能经过伪装,但还是可以通过这些数据包追根溯源。此类检测工作很难开展,但并非不可实现;特别是,如果 Internet 服务提供商 (ISP) 愿意提供帮助,则更容易实现。
  4. //AN_Xml:
  5. 分布式攻击(DDoS): 如果使用僵尸网络发起攻击,则追溯攻击源头的可能性很低。随着混淆级别的攀升,攻击者可能还会命令每台分布式设备伪造其发送数据包的 IP 地址。哪怕攻击者使用僵尸网络(如 Mirai 僵尸网络),通常也不会刻意屏蔽受感染设备的 IP。
  6. //AN_Xml:
//AN_Xml:

如何缓解 SYN Flood?

//AN_Xml:

扩展积压工作队列

//AN_Xml:

目标设备安装的每个操作系统都允许具有一定数量的半开连接。若要响应大量 SYN 数据包,一种方法是增加操作系统允许的最大半开连接数目。为成功扩展最大积压工作,系统必须额外预留内存资源以处理各类新请求。如果系统没有足够的内存,无法应对增加的积压工作队列规模,将对系统性能产生负面影响,但仍然好过拒绝服务。

//AN_Xml:

回收最先创建的 TCP 半开连接

//AN_Xml:

另一种缓解策略是在填充积压工作后覆盖最先创建的半开连接。这项策略要求完全建立合法连接的时间低于恶意 SYN 数据包填充积压工作的时间。当攻击量增加或积压工作规模小于实际需求时,这项特定的防御措施将不奏效。

//AN_Xml:

SYN Cookie

//AN_Xml:

此策略要求服务器创建 Cookie。为避免在填充积压工作时断开连接,服务器使用 SYN-ACK 数据包响应每一项连接请求,而后从积压工作中删除 SYN 请求,同时从内存中删除请求,保证端口保持打开状态并做好重新建立连接的准备。如果连接是合法请求并且已将最后一个 ACK 数据包从客户端机器发回服务器,服务器将重建(存在一些限制)SYN 积压工作队列条目。虽然这项缓解措施势必会丢失一些 TCP 连接信息,但好过因此导致对合法用户发起拒绝服务攻击。

//AN_Xml:

UDP Flood(洪水)

//AN_Xml:

UDP Flood 是什么?

//AN_Xml:

UDP Flood 也是一种拒绝服务攻击,将大量的用户数据报协议(UDP)数据包发送到目标服务器,目的是压倒该设备的处理和响应能力。防火墙保护目标服务器也可能因 UDP 泛滥而耗尽,从而导致对合法流量的拒绝服务。

//AN_Xml:

UDP Flood 攻击原理是什么?

//AN_Xml:

UDP Flood 主要通过利用服务器响应发送到其中一个端口的 UDP 数据包所采取的步骤。在正常情况下,当服务器在特定端口接收到 UDP 数据包时,会经过两个步骤:

//AN_Xml:
    //AN_Xml:
  • 服务器首先检查是否正在运行正在侦听指定端口的请求的程序。
  • //AN_Xml:
  • 如果没有程序在该端口接收数据包,则服务器使用 ICMP(ping)数据包进行响应,以通知发送方目的地不可达。
  • //AN_Xml:
//AN_Xml:

举个例子。假设今天要联系酒店的小蓝,酒店客服接到电话后先查看房间的列表来确保小蓝在客房内,随后转接给小蓝。

//AN_Xml:

首先,接待员接收到呼叫者要求连接到特定房间的电话。接待员然后需要查看所有房间的清单,以确保客人在房间中可用,并愿意接听电话。碰巧的是,此时如果突然间所有的电话线同时亮起来,那么他们就会很快就变得不堪重负了。

//AN_Xml:

当服务器接收到每个新的 UDP 数据包时,它将通过步骤来处理请求,并利用该过程中的服务器资源。发送 UDP 报文时,每个报文将包含源设备的 IP 地址。在这种类型的 DDoS 攻击期间,攻击者通常不会使用自己的真实 IP 地址,而是会欺骗 UDP 数据包的源 IP 地址,从而阻止攻击者的真实位置被暴露并潜在地饱和来自目标的响应数据包服务器。

//AN_Xml:

由于目标服务器利用资源检查并响应每个接收到的 UDP 数据包的结果,当接收到大量 UDP 数据包时,目标的资源可能会迅速耗尽,导致对正常流量的拒绝服务。

//AN_Xml:

//AN_Xml:

如何缓解 UDP Flooding?

//AN_Xml:

大多数操作系统部分限制了 ICMP 报文的响应速率,以中断需要 ICMP 响应的 DDoS 攻击。这种缓解的一个缺点是在攻击过程中,合法的数据包也可能被过滤。如果 UDP Flood 的容量足够高以使目标服务器的防火墙的状态表饱和,则在服务器级别发生的任何缓解都将不足以应对目标设备上游的瓶颈。

//AN_Xml:

HTTP Flood(洪水)

//AN_Xml:

HTTP Flood 是什么?

//AN_Xml:

HTTP Flood 是一种大规模的 DDoS(Distributed Denial of Service,分布式拒绝服务)攻击,旨在利用 HTTP 请求使目标服务器不堪重负。目标因请求而达到饱和,且无法响应正常流量后,将出现拒绝服务,拒绝来自实际用户的其他请求。

//AN_Xml:

HTTP 洪水攻击

//AN_Xml:

HTTP Flood 的攻击原理是什么?

//AN_Xml:

HTTP 洪水攻击是“第 7 层”DDoS 攻击的一种。第 7 层是 OSI 模型的应用程序层,指的是 HTTP 等互联网协议。HTTP 是基于浏览器的互联网请求的基础,通常用于加载网页或通过互联网发送表单内容。缓解应用程序层攻击特别复杂,因为恶意流量和正常流量很难区分。

//AN_Xml:

为了获得最大效率,恶意行为者通常会利用或创建僵尸网络,以最大程度地扩大攻击的影响。通过利用感染了恶意软件的多台设备,攻击者可以发起大量攻击流量来进行攻击。

//AN_Xml:

HTTP 洪水攻击有两种:

//AN_Xml:
    //AN_Xml:
  • HTTP GET 攻击:在这种攻击形式下,多台计算机或其他设备相互协调,向目标服务器发送对图像、文件或其他资产的多个请求。当目标被传入的请求和响应所淹没时,来自正常流量源的其他请求将被拒绝服务。
  • //AN_Xml:
  • HTTP POST 攻击:一般而言,在网站上提交表单时,服务器必须处理传入的请求并将数据推送到持久层(通常是数据库)。与发送 POST 请求所需的处理能力和带宽相比,处理表单数据和运行必要数据库命令的过程相对密集。这种攻击利用相对资源消耗的差异,直接向目标服务器发送许多 POST 请求,直到目标服务器的容量饱和并拒绝服务为止。
  • //AN_Xml:
//AN_Xml:

如何防护 HTTP Flood?

//AN_Xml:

如前所述,缓解第 7 层攻击非常复杂,而且通常要从多方面进行。一种方法是对发出请求的设备实施质询,以测试它是否是机器人,这与在线创建帐户时常用的 CAPTCHA 测试非常相似。通过提出 JavaScript 计算挑战之类的要求,可以缓解许多攻击。

//AN_Xml:

其他阻止 HTTP 洪水攻击的途径包括使用 Web 应用程序防火墙 (WAF)、管理 IP 信誉数据库以跟踪和有选择地阻止恶意流量,以及由工程师进行动态分析。Cloudflare 具有超过 2000 万个互联网设备的规模优势,能够分析来自各种来源的流量并通过快速更新的 WAF 规则和其他防护策略来缓解潜在的攻击,从而消除应用程序层 DDoS 流量。

//AN_Xml:

DNS Flood(洪水)

//AN_Xml:

DNS Flood 是什么?

//AN_Xml:

域名系统(DNS)服务器是互联网的“电话簿“;互联网设备通过这些服务器来查找特定 Web 服务器以便访问互联网内容。DNS Flood 攻击是一种分布式拒绝服务(DDoS)攻击,攻击者用大量流量淹没某个域的 DNS 服务器,以尝试中断该域的 DNS 解析。如果用户无法找到电话簿,就无法查找到用于调用特定资源的地址。通过中断 DNS 解析,DNS Flood 攻击将破坏网站、API 或 Web 应用程序响应合法流量的能力。很难将 DNS Flood 攻击与正常的大流量区分开来,因为这些大规模流量往往来自多个唯一地址,查询该域的真实记录,模仿合法流量。

//AN_Xml:

DNS Flood 的攻击原理是什么?

//AN_Xml:

//AN_Xml:

域名系统的功能是将易于记忆的名称(例如 example.com)转换成难以记住的网站服务器地址(例如 192.168.0.1),因此成功攻击 DNS 基础设施将导致大多数人无法使用互联网。DNS Flood 攻击是一种相对较新的基于 DNS 的攻击,这种攻击是在高带宽物联网(IoT)僵尸网络(如 Mirai)兴起后激增的。DNS Flood 攻击使用 IP 摄像头、DVR 盒和其他 IoT 设备的高带宽连接直接淹没主要提供商的 DNS 服务器。来自 IoT 设备的大量请求淹没 DNS 提供商的服务,阻止合法用户访问提供商的 DNS 服务器。

//AN_Xml:

DNS Flood 攻击不同于 DNS 放大攻击。与 DNS Flood 攻击不同,DNS 放大攻击反射并放大不安全 DNS 服务器的流量,以便隐藏攻击的源头并提高攻击的有效性。DNS 放大攻击使用连接带宽较小的设备向不安全的 DNS 服务器发送无数请求。这些设备对非常大的 DNS 记录发出小型请求,但在发出请求时,攻击者伪造返回地址为目标受害者。这种放大效果让攻击者能借助有限的攻击资源来破坏较大的目标。

//AN_Xml:

如何防护 DNS Flood?

//AN_Xml:

DNS Flood 对传统上基于放大的攻击方法做出了改变。借助轻易获得的高带宽僵尸网络,攻击者现能针对大型组织发动攻击。除非被破坏的 IoT 设备得以更新或替换,否则抵御这些攻击的唯一方法是使用一个超大型、高度分布式的 DNS 系统,以便实时监测、吸收和阻止攻击流量。

//AN_Xml:

TCP 重置攻击

//AN_Xml:

TCP 重置攻击中,攻击者通过向通信的一方或双方发送伪造的消息,告诉它们立即断开连接,从而使通信双方连接中断。正常情况下,如果客户端收发现到达的报文段对于相关连接而言是不正确的,TCP 就会发送一个重置报文段,从而导致 TCP 连接的快速拆卸。

//AN_Xml:

TCP 重置攻击利用这一机制,通过向通信方发送伪造的重置报文段,欺骗通信双方提前关闭 TCP 连接。如果伪造的重置报文段完全逼真,接收者就会认为它有效,并关闭 TCP 连接,防止连接被用来进一步交换信息。服务端可以创建一个新的 TCP 连接来恢复通信,但仍然可能会被攻击者重置连接。万幸的是,攻击者需要一定的时间来组装和发送伪造的报文,所以一般情况下这种攻击只对长连接有杀伤力,对于短连接而言,你还没攻击呢,人家已经完成了信息交换。

//AN_Xml:

从某种意义上来说,伪造 TCP 报文段是很容易的,因为 TCP/IP 都没有任何内置的方法来验证服务端的身份。有些特殊的 IP 扩展协议(例如 IPSec)确实可以验证身份,但并没有被广泛使用。客户端只能接收报文段,并在可能的情况下使用更高级别的协议(如 TLS)来验证服务端的身份。但这个方法对 TCP 重置包并不适用,因为 TCP 重置包是 TCP 协议本身的一部分,无法使用更高级别的协议进行验证。

//AN_Xml:

模拟攻击

//AN_Xml:
//AN_Xml:

以下实验是在 OSX 系统中完成的,其他系统请自行测试。

//AN_Xml:
//AN_Xml:

现在来总结一下伪造一个 TCP 重置报文要做哪些事情:

//AN_Xml:
    //AN_Xml:
  • 嗅探通信双方的交换信息。
  • //AN_Xml:
  • 截获一个 ACK 标志位置位 1 的报文段,并读取其 ACK 号。
  • //AN_Xml:
  • 伪造一个 TCP 重置报文段(RST 标志位置为 1),其序列号等于上面截获的报文的 ACK 号。这只是理想情况下的方案,假设信息交换的速度不是很快。大多数情况下为了增加成功率,可以连续发送序列号不同的重置报文。
  • //AN_Xml:
  • 将伪造的重置报文发送给通信的一方或双方,时其中断连接。
  • //AN_Xml:
//AN_Xml:

为了实验简单,我们可以使用本地计算机通过 localhost 与自己通信,然后对自己进行 TCP 重置攻击。需要以下几个步骤:

//AN_Xml:
    //AN_Xml:
  • 在两个终端之间建立一个 TCP 连接。
  • //AN_Xml:
  • 编写一个能嗅探通信双方数据的攻击程序。
  • //AN_Xml:
  • 修改攻击程序,伪造并发送重置报文。
  • //AN_Xml:
//AN_Xml:

下面正式开始实验。

//AN_Xml:
//AN_Xml:

建立 TCP 连接

//AN_Xml:
//AN_Xml:

可以使用 netcat 工具来建立 TCP 连接,这个工具很多操作系统都预装了。打开第一个终端窗口,运行以下命令:

//AN_Xml:
nc -nvl 8000
//AN_Xml:

这个命令会启动一个 TCP 服务,监听端口为 8000。接着再打开第二个终端窗口,运行以下命令:

//AN_Xml:
nc 127.0.0.1 8000
//AN_Xml:

该命令会尝试与上面的服务建立连接,在其中一个窗口输入一些字符,就会通过 TCP 连接发送给另一个窗口并打印出来。

//AN_Xml:

//AN_Xml:
//AN_Xml:

嗅探流量

//AN_Xml:
//AN_Xml:

编写一个攻击程序,使用 Python 网络库 scapy 来读取两个终端窗口之间交换的数据,并将其打印到终端上。代码比较长,下面为一部份,完整代码后台回复 TCP 攻击,代码的核心是调用 scapy 的嗅探方法:

//AN_Xml:

//AN_Xml:

这段代码告诉 scapylo0 网络接口上嗅探数据包,并记录所有 TCP 连接的详细信息。

//AN_Xml:
    //AN_Xml:
  • iface : 告诉 scapy 在 lo0(localhost)网络接口上进行监听。
  • //AN_Xml:
  • lfilter : 这是个过滤器,告诉 scapy 忽略所有不属于指定的 TCP 连接(通信双方皆为 localhost,且端口号为 8000)的数据包。
  • //AN_Xml:
  • prn : scapy 通过这个函数来操作所有符合 lfilter 规则的数据包。上面的例子只是将数据包打印到终端,下文将会修改函数来伪造重置报文。
  • //AN_Xml:
  • count : scapy 函数返回之前需要嗅探的数据包数量。
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

发送伪造的重置报文

//AN_Xml:
//AN_Xml:

下面开始修改程序,发送伪造的 TCP 重置报文来进行 TCP 重置攻击。根据上面的解读,只需要修改 prn 函数就行了,让其检查数据包,提取必要参数,并利用这些参数来伪造 TCP 重置报文并发送。

//AN_Xml:

例如,假设该程序截获了一个从(src_ip, src_port)发往 (dst_ip, dst_port)的报文段,该报文段的 ACK 标志位已置为 1,ACK 号为 100,000。攻击程序接下来要做的是:

//AN_Xml:
    //AN_Xml:
  • 由于伪造的数据包是对截获的数据包的响应,所以伪造数据包的源 IP/Port 应该是截获数据包的目的 IP/Port,反之亦然。
  • //AN_Xml:
  • 将伪造数据包的 RST 标志位置为 1,以表示这是一个重置报文。
  • //AN_Xml:
  • 将伪造数据包的序列号设置为截获数据包的 ACK 号,因为这是发送方期望收到的下一个序列号。
  • //AN_Xml:
  • 调用 scapysend 方法,将伪造的数据包发送给截获数据包的发送方。
  • //AN_Xml:
//AN_Xml:

对于我的程序而言,只需将这一行取消注释,并注释这一行的上面一行,就可以全面攻击了。按照步骤 1 的方法设置 TCP 连接,打开第三个窗口运行攻击程序,然后在 TCP 连接的其中一个终端输入一些字符串,你会发现 TCP 连接被中断了!

//AN_Xml:
//AN_Xml:

进一步实验

//AN_Xml:
//AN_Xml:
    //AN_Xml:
  1. 可以继续使用攻击程序进行实验,将伪造数据包的序列号加减 1 看看会发生什么,是不是确实需要和截获数据包的 ACK 号完全相同。
  2. //AN_Xml:
  3. 打开 Wireshark,监听 lo0 网络接口,并使用过滤器 ip.src == 127.0.0.1 && ip.dst == 127.0.0.1 && tcp.port == 8000 来过滤无关数据。你可以看到 TCP 连接的所有细节。
  4. //AN_Xml:
  5. 在连接上更快速地发送数据流,使攻击更难执行。
  6. //AN_Xml:
//AN_Xml:

中间人攻击

//AN_Xml:

猪八戒要向小蓝表白,于是写了一封信给小蓝,结果第三者小黑拦截到了这封信,把这封信进行了篡改,于是乎在他们之间进行搞破坏行动。这个马文才就是中间人,实施的就是中间人攻击。好我们继续聊聊什么是中间人攻击。

//AN_Xml:

什么是中间人?

//AN_Xml:

攻击中间人攻击英文名叫 Man-in-the-MiddleAttack,简称「MITM 攻击」。指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方 直接对话,但事实上整个会话都被攻击者完全控制。我们画一张图:

//AN_Xml:

图片

//AN_Xml:

从这张图可以看到,中间人其实就是攻击者。通过这种原理,有很多实现的用途,比如说,你在手机上浏览不健康网站的时候,手机就会提示你,此网站可能含有病毒,是否继续访问还是做其他的操作等等。

//AN_Xml:

中间人攻击的原理是什么?

//AN_Xml:

举个例子,我和公司签了一个一份劳动合同,一人一份合同。不晓得哪个可能改了合同内容,不知道真假了,怎么搞?只好找专业的机构来鉴定,自然就要花钱。

//AN_Xml:

在安全领域有句话:我们没有办法杜绝网络犯罪,只好想办法提高网络犯罪的成本。既然没法杜绝这种情况,那我们就想办法提高作案的成本,今天我们就简单了解下基本的网络安全知识,也是面试中的高频面试题了。

//AN_Xml:

为了避免双方说活不算数的情况,双方引入第三家机构,将合同原文给可信任的第三方机构,只要这个机构不监守自盗,合同就相对安全。

//AN_Xml:

如果第三方机构内部不严格或容易出现纰漏?

//AN_Xml:

虽然我们将合同原文给第三方机构了,为了防止内部人员的更改,需要采取什么措施呢

//AN_Xml:

一种可行的办法是引入 摘要算法 。即合同和摘要一起,为了简单的理解摘要。大家可以想象这个摘要为一个函数,这个函数对原文进行了加密,会产生一个唯一的散列值,一旦原文发生一点点变化,那么这个散列值将会变化。

//AN_Xml:

有哪些常用的摘要算法呢?

//AN_Xml:

目前比较常用的加密算法有消息摘要算法和安全散列算法(SHA)。MD5 是将任意长度的文章转化为一个 128 位的散列值,可是在 2004 年,MD5 被证实了容易发生碰撞,即两篇原文产生相同的摘要。这样的话相当于直接给黑客一个后门,轻松伪造摘要。

//AN_Xml:

所以在大部分的情况下都会选择 SHA 算法

//AN_Xml:

出现内鬼了怎么办?

//AN_Xml:

看似很安全的场面了,理论上来说杜绝了篡改合同的做法。主要某个员工同时具有修改合同和摘要的权利,那搞事儿就是时间的问题了,毕竟没哪个系统可以完全的杜绝员工接触敏感信息,除非敏感信息都不存在。所以能不能考虑将合同和摘要分开存储呢

//AN_Xml:

那如何确保员工不会修改合同呢?

//AN_Xml:

这确实蛮难的,不过办法总比困难多。我们将合同放在双方手中,摘要放在第三方机构,篡改难度进一步加大

//AN_Xml:

那么员工万一和某个用户串通好了呢?

//AN_Xml:

看来放在第三方的机构还是不好使,同样存在不小风险。所以还需要寻找新的方案,这就出现了 数字签名和证书

//AN_Xml:

数字证书和签名有什么用?

//AN_Xml:

同样的,举个例子。Sum 和 Mike 两个人签合同。Sum 首先用 SHA 算法计算合同的摘要,然后用自己私钥将摘要加密,得到数字签名。Sum 将合同原文、签名,以及公钥三者都交给 Mike

//AN_Xml:

//AN_Xml:

如果 Sum 想要证明合同是 Mike 的,那么就要使用 Mike 的公钥,将这个签名解密得到摘要 x,然后 Mike 计算原文的 sha 摘要 Y,随后对比 x 和 y,如果两者相等,就认为数据没有被篡改

//AN_Xml:

在这样的过程中,Mike 是不能更改 Sum 的合同,因为要修改合同不仅仅要修改原文还要修改摘要,修改摘要需要提供 Mike 的私钥,私钥即 Sum 独有的密码,公钥即 Sum 公布给他人使用的密码

//AN_Xml:

总之,公钥加密的数据只能私钥可以解密。私钥加密的数据只有公钥可以解密,这就是 非对称加密

//AN_Xml:

隐私保护?不是吓唬大家,信息是透明的兄 die,不过尽量去维护个人的隐私吧,今天学习对称加密和非对称加密。

//AN_Xml:

大家先读读这个字"钥",是读"yao",我以前也是,其实读"yue"

//AN_Xml:

什么是对称加密?

//AN_Xml:

对称加密,顾名思义,加密方与解密方使用同一钥匙(秘钥)。具体一些就是,发送方通过使用相应的加密算法和秘钥,对将要发送的信息进行加密;对于接收方而言,使用解密算法和相同的秘钥解锁信息,从而有能力阅读信息。

//AN_Xml:

图片

//AN_Xml:

常见的对称加密算法有哪些?

//AN_Xml:

DES

//AN_Xml:

DES 使用的密钥表面上是 64 位的,然而只有其中的 56 位被实际用于算法,其余 8 位可以被用于奇偶校验,并在算法中被丢弃。因此,DES 的有效密钥长度为 56 位,通常称 DES 的密钥长度为 56 位。假设秘钥为 56 位,采用暴力破 Jie 的方式,其秘钥个数为 2 的 56 次方,那么每纳秒执行一次解密所需要的时间差不多 1 年的样子。当然,没人这么干。DES 现在已经不是一种安全的加密方法,主要因为它使用的 56 位密钥过短。

//AN_Xml:

//AN_Xml:

IDEA

//AN_Xml:

国际数据加密算法(International Data Encryption Algorithm)。秘钥长度 128 位,优点没有专利的限制。

//AN_Xml:

AES

//AN_Xml:

当 DES 被破解以后,没过多久推出了 AES 算法,提供了三种长度供选择,128 位、192 位和 256,为了保证性能不受太大的影响,选择 128 即可。

//AN_Xml:

SM1 和 SM4

//AN_Xml:

之前几种都是国外的,我们国内自行研究了国密 SM1SM4。其中 S 都属于国家标准,算法公开。优点就是国家的大力支持和认可

//AN_Xml:

总结

//AN_Xml:

//AN_Xml:

常见的非对称加密算法有哪些?

//AN_Xml:

在对称加密中,发送方与接收方使用相同的秘钥。那么在非对称加密中则是发送方与接收方使用的不同的秘钥。其主要解决的问题是防止在秘钥协商的过程中发生泄漏。比如在对称加密中,小蓝将需要发送的消息加密,然后告诉你密码是 123balala,ok,对于其他人而言,很容易就能劫持到密码是 123balala。那么在非对称的情况下,小蓝告诉所有人密码是 123balala,对于中间人而言,拿到也没用,因为没有私钥。所以,非对称密钥其实主要解决了密钥分发的难题。如下图

//AN_Xml:

//AN_Xml:

其实我们经常都在使用非对称加密,比如使用多台服务器搭建大数据平台 hadoop,为了方便多台机器设置免密登录,是不是就会涉及到秘钥分发。再比如搭建 docker 集群也会使用相关非对称加密算法。

//AN_Xml:

常见的非对称加密算法:

//AN_Xml:
    //AN_Xml:
  • //AN_Xml:

    RSA(RSA 加密算法,RSA Algorithm):安全性基于大整数分解的计算难度,应用广泛,兼容性好。缺点是性能相对较慢,且密钥越长(如 2048/4096 位)安全性越高,但运算开销也随之增大。

    //AN_Xml:
  • //AN_Xml:
  • //AN_Xml:

    ECC:基于椭圆曲线提出。是目前加密强度最高的非对称加密算法

    //AN_Xml:
  • //AN_Xml:
  • //AN_Xml:

    SM2:同样基于椭圆曲线问题设计。最大优势就是国家认可和大力支持。

    //AN_Xml:
  • //AN_Xml:
//AN_Xml:

总结:

//AN_Xml:

//AN_Xml:

常见的散列算法有哪些?

//AN_Xml:

这个大家应该更加熟悉了,比如我们平常使用的 MD5 校验,在很多时候,我并不是拿来进行加密,而是用来获得唯一性 ID。在做系统的过程中,存储用户的各种密码信息,通常都会通过散列算法,最终存储其散列值。

//AN_Xml:

MD5(不推荐)

//AN_Xml:

MD5 可以用来生成一个 128 位的消息摘要,它是目前应用比较普遍的散列算法,具体的应用场景你可以自行  参阅。虽然,因为算法的缺陷,它的唯一性已经被破解了,但是大部分场景下,这并不会构成安全问题。但是,如果不是长度受限(32 个字符),我还是不推荐你继续使用 MD5 的。

//AN_Xml:

SHA

//AN_Xml:

安全散列算法。SHA 包括SHA-1SHA-2SHA-3三个版本。该算法的基本思想是:接收一段明文数据,通过不可逆的方式将其转换为固定长度的密文。简单来说,SHA 将输入数据(即预映射或消息)转化为固定长度、较短的输出值,称为散列值(或信息摘要、信息认证码)。SHA-1 已被证明不够安全,因此逐渐被 SHA-2 取代,而 SHA-3 则作为 SHA 系列的最新版本,采用不同的结构(Keccak 算法)提供更高的安全性和灵活性。

//AN_Xml:

SM3

//AN_Xml:

国密算法SM3。加密强度和 SHA-256 算法 相差不多。主要是受到了国家的支持。

//AN_Xml:

总结

//AN_Xml:

图片

//AN_Xml:

大部分情况下使用对称加密,具有比较不错的安全性。如果需要分布式进行秘钥分发,考虑非对称。如果不需要可逆计算则散列算法。 因为这段时间有这方面需求,就看了一些这方面的资料,入坑信息安全,就怕以后洗发水都不用买。谢谢大家查看!

//AN_Xml:

第三方机构和证书机制有什么用?

//AN_Xml:

问题还有,此时如果 Sum 否认给过 Mike 的公钥和合同,不久 gg 了

//AN_Xml:

所以需要 Sum 过的话做过的事儿需要足够的信誉,这就引入了 第三方机构和证书机制

//AN_Xml:

证书之所以会有信用,是因为证书的签发方拥有信用。所以如果 Sum 想让 Mike 承认自己的公钥,Sum 不会直接将公钥给 Mike ,而是提供由第三方机构,含有公钥的证书。如果 Mike 也信任这个机构,法律都认可,那 ik,信任关系成立

//AN_Xml:

//AN_Xml:

如上图所示,Sum 将自己的申请提交给机构,产生证书的原文。机构用自己的私钥签名 Sum 的申请原文(先根据原文内容计算摘要,再用私钥加密),得到带有签名信息的证书。Mike 拿到带签名信息的证书,通过第三方机构的公钥进行解密,获得 Sum 证书的摘要、证书的原文。有了 Sum 证书的摘要和原文,Mike 就可以进行验签。验签通过,Mike 就可以确认 Sum 的证书的确是第三方机构签发的。

//AN_Xml:

用上面这样一个机制,合同的双方都无法否认合同。这个解决方案的核心在于需要第三方信用服务机构提供信用背书。这里产生了一个最基础的信任链,如果第三方机构的信任崩溃,比如被黑客攻破,那整条信任链条也就断裂了

//AN_Xml:

为了让这个信任条更加稳固,就需要环环相扣,打造更长的信任链,避免单点信任风险

//AN_Xml:

//AN_Xml:

上图中,由信誉最好的根证书机构提供根证书,然后根证书机构去签发二级机构的证书;二级机构去签发三级机构的证书;最后有由三级机构去签发 Sum 证书。

//AN_Xml:

如果要验证 Sum 证书的合法性,就需要用三级机构证书中的公钥去解密 Sum 证书的数字签名。

//AN_Xml:

如果要验证三级机构证书的合法性,就需要用二级机构的证书去解密三级机构证书的数字签名。

//AN_Xml:

如果要验证二级结构证书的合法性,就需要用根证书去解密。

//AN_Xml:

以上,就构成了一个相对长一些的信任链。如果其中一方想要作弊是非常困难的,除非链条中的所有机构同时联合起来,进行欺诈。

//AN_Xml:

中间人攻击如何避免?

//AN_Xml:

既然知道了中间人攻击的原理也知道了他的危险,现在我们看看如何避免。相信我们都遇到过下面这种状况:

//AN_Xml:

//AN_Xml:

出现这个界面的很多情况下,都是遇到了中间人攻击的现象,需要对安全证书进行及时地监测。而且大名鼎鼎的 github 网站,也曾遭遇过中间人攻击:

//AN_Xml:

想要避免中间人攻击的方法目前主要有两个:

//AN_Xml:
    //AN_Xml:
  • 客户端不要轻易相信证书:因为这些证书极有可能是中间人。
  • //AN_Xml:
  • App 可以提前预埋证书在本地:意思是我们本地提前有一些证书,这样其他证书就不能再起作用了。
  • //AN_Xml:
//AN_Xml:

DDOS

//AN_Xml:

通过上面的描述,总之即好多种攻击都是 DDOS 攻击,所以简单总结下这个攻击相关内容。

//AN_Xml:

其实,像全球互联网各大公司,均遭受过大量的 DDoS

//AN_Xml:

2018 年,GitHub 在一瞬间遭到高达 1.35Tbps 的带宽攻击。这次 DDoS 攻击几乎可以堪称是互联网有史以来规模最大、威力最大的 DDoS 攻击了。在 GitHub 遭到攻击后,仅仅一周后,DDoS 攻击又开始对 Google、亚马逊甚至 Pornhub 等网站进行了 DDoS 攻击。后续的 DDoS 攻击带宽最高也达到了 1Tbps。

//AN_Xml:

DDoS 攻击究竟是什么?

//AN_Xml:

DDos 全名 Distributed Denial of Service,翻译成中文就是分布式拒绝服务。指的是处于不同位置的多个攻击者同时向一个或数个目标发动攻击,是一种分布的、协同的大规模攻击方式。单一的 DoS 攻击一般是采用一对一方式的,它利用网络协议和操作系统的一些缺陷,采用欺骗和伪装的策略来进行网络攻击,使网站服务器充斥大量要求回复的信息,消耗网络带宽或系统资源,导致网络或系统不胜负荷以至于瘫痪而停止提供正常的网络服务。

//AN_Xml:
//AN_Xml:

举个例子

//AN_Xml:
//AN_Xml:

我开了一家有五十个座位的重庆火锅店,由于用料上等,童叟无欺。平时门庭若市,生意特别红火,而对面二狗家的火锅店却无人问津。二狗为了对付我,想了一个办法,叫了五十个人来我的火锅店坐着却不点菜,让别的客人无法吃饭。

//AN_Xml:

上面这个例子讲的就是典型的 DDoS 攻击,一般来说是指攻击者利用“肉鸡”对目标网站在较短的时间内发起大量请求,大规模消耗目标网站的主机资源,让它无法正常服务。在线游戏、互联网金融等领域是 DDoS 攻击的高发行业。

//AN_Xml:

攻击方式很多,比如 ICMP FloodUDP FloodNTP FloodSYN FloodCC 攻击DNS Query Flood等等。

//AN_Xml:

如何应对 DDoS 攻击?

//AN_Xml:

高防服务器

//AN_Xml:

还是拿开的重庆火锅店举例,高防服务器就是我给重庆火锅店增加了两名保安,这两名保安可以让保护店铺不受流氓骚扰,并且还会定期在店铺周围巡逻防止流氓骚扰。

//AN_Xml:

高防服务器主要是指能独立硬防御 50Gbps 以上的服务器,能够帮助网站拒绝服务攻击,定期扫描网络主节点等,这东西是不错,就是贵~

//AN_Xml:

黑名单

//AN_Xml:

面对火锅店里面的流氓,我一怒之下将他们拍照入档,并禁止他们踏入店铺,但是有的时候遇到长得像的人也会禁止他进入店铺。这个就是设置黑名单,此方法秉承的就是“错杀一千,也不放一百”的原则,会封锁正常流量,影响到正常业务。

//AN_Xml:

DDoS 清洗

//AN_Xml:

DDos 清洗,就是我发现客人进店几分钟以后,但是一直不点餐,我就把他踢出店里。

//AN_Xml:

DDoS 清洗会对用户请求数据进行实时监控,及时发现 DOS 攻击等异常流量,在不影响正常业务开展的情况下清洗掉这些异常流量。

//AN_Xml:

CDN 加速

//AN_Xml:

CDN 加速,我们可以这么理解:为了减少流氓骚扰,我干脆将火锅店开到了线上,承接外卖服务,这样流氓找不到店在哪里,也耍不来流氓了。

//AN_Xml:

在现实中,CDN 服务将网站访问流量分配到了各个节点中,这样一方面隐藏网站的真实 IP,另一方面即使遭遇 DDoS 攻击,也可以将流量分散到各个节点中,防止源站崩溃。

//AN_Xml:

参考

//AN_Xml: //AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: SQL常见面试题总结(1) //AN_Xml: https://javaguide.cn/database/sql/sql-questions-01.html //AN_Xml: https://javaguide.cn/database/sql/sql-questions-01.html //AN_Xml: SQL常见面试题总结(1) //AN_Xml: SQL常见面试题总结第一篇,涵盖SELECT检索数据、WHERE条件过滤、ORDER BY排序、DISTINCT去重、LIMIT分页等基础查询操作及牛客真题解析。 //AN_Xml: 数据库 //AN_Xml: Fri, 17 Feb 2023 05:40:22 GMT //AN_Xml: //AN_Xml:

题目来源于:牛客题霸 - SQL 必知必会

//AN_Xml: //AN_Xml:

检索数据

//AN_Xml:

SELECT 用于从数据库中查询数据。

//AN_Xml:

从 Customers 表中检索所有的 ID

//AN_Xml:

现有表 Customers 如下:

//AN_Xml:

| cust_id |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: SQL语法基础知识总结 //AN_Xml: https://javaguide.cn/database/sql/sql-syntax-summary.html //AN_Xml: https://javaguide.cn/database/sql/sql-syntax-summary.html //AN_Xml: SQL语法基础知识总结 //AN_Xml: SQL语法基础知识总结,系统讲解DDL数据定义、DML数据操作、DQL数据查询、DCL数据控制语言,涵盖表操作、约束、索引、事务、连接查询等核心知识点。 //AN_Xml: 数据库 //AN_Xml: Fri, 17 Feb 2023 05:40:22 GMT //AN_Xml: //AN_Xml:

本文整理完善自下面这两份资料:

//AN_Xml: //AN_Xml: //AN_Xml:

基本概念

//AN_Xml:

数据库术语

//AN_Xml:
    //AN_Xml:
  • 数据库(database) - 保存有组织的数据的容器(通常是一个文件或一组文件)。
  • //AN_Xml:
  • 数据表(table) - 某种特定类型数据的结构化清单。
  • //AN_Xml:
  • 模式(schema) - 关于数据库和表的布局及特性的信息。模式定义了数据在表中如何存储,包含存储什么样的数据,数据如何分解,各部分信息如何命名等信息。数据库和表都有模式。
  • //AN_Xml:
  • 列(column) - 表中的一个字段。所有表都是由一个或多个列组成的。
  • //AN_Xml:
  • 行(row) - 表中的一个记录。
  • //AN_Xml:
  • 主键(primary key) - 一列(或一组列),其值能够唯一标识表中每一行。
  • //AN_Xml:
//AN_Xml:

SQL 语法

//AN_Xml:

SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。

//AN_Xml:

SQL 语法结构

//AN_Xml:

//AN_Xml:

SQL 语法结构包括:

//AN_Xml:
    //AN_Xml:
  • 子句 - 是语句和查询的组成成分。(在某些情况下,这些都是可选的。)
  • //AN_Xml:
  • 表达式 - 可以产生任何标量值,或由列和行的数据库表
  • //AN_Xml:
  • 谓词 - 给需要评估的 SQL 三值逻辑(3VL)(true/false/unknown)或布尔真值指定条件,并限制语句和查询的效果,或改变程序流程。
  • //AN_Xml:
  • 查询 - 基于特定条件检索数据。这是 SQL 的一个重要组成部分。
  • //AN_Xml:
  • 语句 - 可以持久地影响纲要和数据,也可以控制数据库事务、程序流程、连接、会话或诊断。
  • //AN_Xml:
//AN_Xml:

SQL 语法要点

//AN_Xml:
    //AN_Xml:
  • SQL 语句不区分大小写,但是数据库表名、列名和值是否区分,依赖于具体的 DBMS 以及配置。例如:SELECTselectSelect 是相同的。
  • //AN_Xml:
  • 多条 SQL 语句必须以分号(;)分隔
  • //AN_Xml:
  • 处理 SQL 语句时,所有空格都被忽略
  • //AN_Xml:
//AN_Xml:

SQL 语句可以写成一行,也可以分写为多行。

//AN_Xml:
-- 一行 SQL 语句
//AN_Xml:
//AN_Xml:UPDATE user SET username='robot', password='robot' WHERE username = 'root';
//AN_Xml:
//AN_Xml:-- 多行 SQL 语句
//AN_Xml:UPDATE user
//AN_Xml:SET username='robot', password='robot'
//AN_Xml:WHERE username = 'root';
//AN_Xml:

SQL 支持三种注释:

//AN_Xml:
## 注释1
//AN_Xml:-- 注释2
//AN_Xml:/* 注释3 */
//AN_Xml:

SQL 分类

//AN_Xml:

数据定义语言(DDL)

//AN_Xml:

数据定义语言(Data Definition Language,DDL)是 SQL 语言集中负责数据结构定义与数据库对象定义的语言。

//AN_Xml:

DDL 的主要功能是定义数据库对象

//AN_Xml:

DDL 的核心指令是 CREATEALTERDROP

//AN_Xml:

数据操纵语言(DML)

//AN_Xml:

数据操纵语言(Data Manipulation Language, DML)是用于数据库操作,对数据库其中的对象和数据运行访问工作的编程语句。

//AN_Xml:

DML 的主要功能是 访问数据,因此其语法都是以读写数据库为主。

//AN_Xml:

DML 的核心指令是 INSERTUPDATEDELETESELECT。这四个指令合称 CRUD(Create, Read, Update, Delete),即增删改查。

//AN_Xml:

事务控制语言(TCL)

//AN_Xml:

事务控制语言 (Transaction Control Language, TCL) 用于管理数据库中的事务。这些用于管理由 DML 语句所做的更改。它还允许将语句分组为逻辑事务。

//AN_Xml:

TCL 的核心指令是 COMMITROLLBACK

//AN_Xml:

数据控制语言(DCL)

//AN_Xml:

数据控制语言 (Data Control Language, DCL) 是一种可对数据访问权进行控制的指令,它可以控制特定用户账户对数据表、查看表、预存程序、用户自定义函数等数据库对象的控制权。

//AN_Xml:

DCL 的核心指令是 GRANTREVOKE

//AN_Xml:

DCL 以控制用户的访问权限为主,因此其指令作法并不复杂,可利用 DCL 控制的权限有:CONNECTSELECTINSERTUPDATEDELETEEXECUTEUSAGEREFERENCES

//AN_Xml:

根据不同的 DBMS 以及不同的安全性实体,其支持的权限控制也有所不同。

//AN_Xml:

我们先来介绍 DML 语句用法。 DML 的主要功能是读写数据库实现增删改查。

//AN_Xml:

增删改查

//AN_Xml:

增删改查,又称为 CRUD,数据库基本操作中的基本操作。

//AN_Xml:

插入数据

//AN_Xml:

INSERT INTO 语句用于向表中插入新记录。

//AN_Xml:

插入完整的行

//AN_Xml:
# 插入一行
//AN_Xml:INSERT INTO user
//AN_Xml:VALUES (10, 'root', 'root', 'xxxx@163.com');
//AN_Xml:# 插入多行
//AN_Xml:INSERT INTO user
//AN_Xml:VALUES (10, 'root', 'root', 'xxxx@163.com'), (12, 'user1', 'user1', 'xxxx@163.com'), (18, 'user2', 'user2', 'xxxx@163.com');
//AN_Xml:

插入行的一部分

//AN_Xml:
INSERT INTO user(username, password, email)
//AN_Xml:VALUES ('admin', 'admin', 'xxxx@163.com');
//AN_Xml:

插入查询出来的数据

//AN_Xml:
INSERT INTO user(username)
//AN_Xml:SELECT name
//AN_Xml:FROM account;
//AN_Xml:

更新数据

//AN_Xml:

UPDATE 语句用于更新表中的记录。

//AN_Xml:
UPDATE user
//AN_Xml:SET username='robot', password='robot'
//AN_Xml:WHERE username = 'root';
//AN_Xml:

删除数据

//AN_Xml:
    //AN_Xml:
  • DELETE 语句用于删除表中的记录。
  • //AN_Xml:
  • TRUNCATE TABLE 可以清空表,也就是删除所有行。说明:TRUNCATE 语句不属于 DML 语法而是 DDL 语法。
  • //AN_Xml:
//AN_Xml:

删除表中的指定数据

//AN_Xml:
DELETE FROM user
//AN_Xml:WHERE username = 'robot';
//AN_Xml:

清空表中的数据

//AN_Xml:
TRUNCATE TABLE user;
//AN_Xml:

查询数据

//AN_Xml:

SELECT 语句用于从数据库中查询数据。

//AN_Xml:

DISTINCT 用于返回唯一不同的值。它作用于所有列,也就是说所有列的值都相同才算相同。

//AN_Xml:

LIMIT 限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。

//AN_Xml:
    //AN_Xml:
  • ASC:升序(默认)
  • //AN_Xml:
  • DESC:降序
  • //AN_Xml:
//AN_Xml:

查询单列

//AN_Xml:
SELECT prod_name
//AN_Xml:FROM products;
//AN_Xml:

查询多列

//AN_Xml:
SELECT prod_id, prod_name, prod_price
//AN_Xml:FROM products;
//AN_Xml:

查询所有列

//AN_Xml:
SELECT *
//AN_Xml:FROM products;
//AN_Xml:

查询不同的值

//AN_Xml:
SELECT DISTINCT
//AN_Xml:vend_id FROM products;
//AN_Xml:

限制查询结果

//AN_Xml:
-- 返回前 5 行
//AN_Xml:SELECT * FROM mytable LIMIT 5;
//AN_Xml:SELECT * FROM mytable LIMIT 0, 5;
//AN_Xml:-- 返回第 3 ~ 5 行
//AN_Xml:SELECT * FROM mytable LIMIT 2, 3;
//AN_Xml:

排序

//AN_Xml:

order by 用于对结果集按照一个列或者多个列进行排序。默认按照升序对记录进行排序,如果需要按照降序对记录进行排序,可以使用 desc 关键字。

//AN_Xml:

order by 对多列排序的时候,先排序的列放前面,后排序的列放后面。并且,不同的列可以有不同的排序规则。

//AN_Xml:
SELECT * FROM products
//AN_Xml:ORDER BY prod_price DESC, prod_name ASC;
//AN_Xml:

分组

//AN_Xml:

group by

//AN_Xml:
    //AN_Xml:
  • group by 子句将记录分组到汇总行中。
  • //AN_Xml:
  • group by 为每个组返回一个记录。
  • //AN_Xml:
  • group by 通常还涉及聚合countmaxsumavg 等。
  • //AN_Xml:
  • group by 可以按一列或多列进行分组。
  • //AN_Xml:
  • group by 按分组字段进行排序后,order by 可以以汇总字段来进行排序。
  • //AN_Xml:
//AN_Xml:

分组

//AN_Xml:
SELECT cust_name, COUNT(cust_address) AS addr_num
//AN_Xml:FROM Customers GROUP BY cust_name;
//AN_Xml:

分组后排序

//AN_Xml:
SELECT cust_name, COUNT(cust_address) AS addr_num
//AN_Xml:FROM Customers GROUP BY cust_name
//AN_Xml:ORDER BY cust_name DESC;
//AN_Xml:

having

//AN_Xml:
    //AN_Xml:
  • having 用于对汇总的 group by 结果进行过滤。
  • //AN_Xml:
  • having 一般都是和 group by 连用。
  • //AN_Xml:
  • wherehaving 可以在相同的查询中。
  • //AN_Xml:
//AN_Xml:

使用 WHERE 和 HAVING 过滤数据

//AN_Xml:
SELECT cust_name, COUNT(*) AS NumberOfOrders
//AN_Xml:FROM Customers
//AN_Xml:WHERE cust_email IS NOT NULL
//AN_Xml:GROUP BY cust_name
//AN_Xml:HAVING COUNT(*) > 1;
//AN_Xml:

having vs where

//AN_Xml:
    //AN_Xml:
  • where:过滤过滤指定的行,后面不能加聚合函数(分组函数)。wheregroup by 前。
  • //AN_Xml:
  • having:过滤分组,一般都是和 group by 连用,不能单独使用。havinggroup by 之后。
  • //AN_Xml:
//AN_Xml:

子查询

//AN_Xml:

子查询是嵌套在较大查询中的 SQL 查询,也称内部查询或内部选择,包含子查询的语句也称为外部查询或外部选择。简单来说,子查询就是指将一个 select 查询(子查询)的结果作为另一个 SQL 语句(主查询)的数据来源或者判断条件。

//AN_Xml:

子查询可以嵌入 SELECTINSERTUPDATEDELETE 语句中,也可以和 =<>INBETWEENEXISTS 等运算符一起使用。

//AN_Xml:

子查询常用在 WHERE 子句和 FROM 子句后边:

//AN_Xml:
    //AN_Xml:
  • 当用于 WHERE 子句时,根据不同的运算符,子查询可以返回单行单列、多行单列、单行多列数据。子查询就是要返回能够作为 WHERE 子句查询条件的值。
  • //AN_Xml:
  • 当用于 FROM 子句时,一般返回多行多列数据,相当于返回一张临时表,这样才符合 FROM 后面是表的规则。这种做法能够实现多表联合查询。
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

注意:MYSQL 数据库从 4.1 版本才开始支持子查询,早期版本是不支持的。

//AN_Xml:
//AN_Xml:

用于 WHERE 子句的子查询的基本语法如下:

//AN_Xml:
select column_name [, column_name ]
//AN_Xml:from   table1 [, table2 ]
//AN_Xml:where  column_name operator
//AN_Xml:    (select column_name [, column_name ]
//AN_Xml:    from table1 [, table2 ]
//AN_Xml:    [where])
//AN_Xml:
    //AN_Xml:
  • 子查询需要放在括号( )内。
  • //AN_Xml:
  • operator 表示用于 where 子句的运算符。
  • //AN_Xml:
//AN_Xml:

用于 FROM 子句的子查询的基本语法如下:

//AN_Xml:
select column_name [, column_name ]
//AN_Xml:from (select column_name [, column_name ]
//AN_Xml:      from table1 [, table2 ]
//AN_Xml:      [where]) as temp_table_name
//AN_Xml:where  condition
//AN_Xml:

用于 FROM 的子查询返回的结果相当于一张临时表,所以需要使用 AS 关键字为该临时表起一个名字。

//AN_Xml:

子查询的子查询

//AN_Xml:
SELECT cust_name, cust_contact
//AN_Xml:FROM customers
//AN_Xml:WHERE cust_id IN (SELECT cust_id
//AN_Xml:                  FROM orders
//AN_Xml:                  WHERE order_num IN (SELECT order_num
//AN_Xml:                                      FROM orderitems
//AN_Xml:                                      WHERE prod_id = 'RGAN01'));
//AN_Xml:

内部查询首先在其父查询之前执行,以便可以将内部查询的结果传递给外部查询。执行过程可以参考下图:

//AN_Xml:

//AN_Xml:

WHERE

//AN_Xml:
    //AN_Xml:
  • WHERE 子句用于过滤记录,即缩小访问数据的范围。
  • //AN_Xml:
  • WHERE 后跟一个返回 truefalse 的条件。
  • //AN_Xml:
  • WHERE 可以与 SELECTUPDATEDELETE 一起使用。
  • //AN_Xml:
  • 可以在 WHERE 子句中使用的操作符。
  • //AN_Xml:
//AN_Xml:

| 运算符 | 描述 |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Gradle核心概念总结 //AN_Xml: https://javaguide.cn/tools/gradle/gradle-core-concepts.html //AN_Xml: https://javaguide.cn/tools/gradle/gradle-core-concepts.html //AN_Xml: Gradle核心概念总结 //AN_Xml: Gradle 就是一个运行在 JVM 上的自动化的项目构建工具,用来帮助我们自动构建项目。 //AN_Xml: 开发工具 //AN_Xml: Thu, 02 Feb 2023 10:43:22 GMT //AN_Xml: //AN_Xml:

这部分内容主要根据 Gradle 官方文档整理,做了对应的删减,主要保留比较重要的部分,不涉及实战,主要是一些重要概念的介绍。

//AN_Xml: //AN_Xml:

Gradle 这部分内容属于可选内容,可以根据自身需求决定是否学习,目前国内还是使用 Maven 普遍一些。

//AN_Xml:

Gradle 介绍

//AN_Xml:

Gradle 官方文档是这样介绍的 Gradle 的:

//AN_Xml:
//AN_Xml:

Gradle is an open-source build automation tool flexible enough to build almost any type of software. Gradle makes few assumptions about what you’re trying to build or how to build it. This makes Gradle particularly flexible.

//AN_Xml:

Gradle 是一个开源的构建自动化工具,它足够灵活,可以构建几乎任何类型的软件。Gradle 对你要构建什么或者如何构建它做了很少的假设。这使得 Gradle 特别灵活。

//AN_Xml:
//AN_Xml:

简单来说,Gradle 就是一个运行在 JVM 上的自动化的项目构建工具,用来帮助我们自动构建项目。

//AN_Xml:

对于开发者来说,Gradle 的主要作用主要有 3 个:

//AN_Xml:
    //AN_Xml:
  1. 项目构建:提供标准的、跨平台的自动化项目构建方式。
  2. //AN_Xml:
  3. 依赖管理:方便快捷的管理项目依赖的资源(jar 包),避免资源间的版本冲突问题。
  4. //AN_Xml:
  5. 统一开发结构:提供标准的、统一的项目结构。
  6. //AN_Xml:
//AN_Xml:

Gradle 构建脚本是使用 Groovy 或 Kotlin 语言编写的,表达能力非常强,也足够灵活。

//AN_Xml:

Groovy 介绍

//AN_Xml:

Gradle 是运行在 JVM 上的一个程序,它可以使用 Groovy 来编写构建脚本。

//AN_Xml:

Groovy 是运行在 JVM 上的脚本语言,是基于 Java 扩展的动态语言,它的语法和 Java 非常的相似,可以使用 Java 的类库。Groovy 可以用于面向对象编程,也可以用作纯粹的脚本语言。在语言的设计上它吸纳了 Java、Python、Ruby 和 Smalltalk 语言的优秀特性,比如动态类型转换、闭包和元编程支持。

//AN_Xml:

我们可以用学习 Java 的方式去学习 Groovy ,学习成本相对来说还是比较低的,即使开发过程中忘记 Groovy 语法,也可以用 Java 语法继续编码。

//AN_Xml:

基于 JVM 的语言有很多种比如 Groovy,Kotlin,Java,Scala,他们最终都会编译生成 Java 字节码文件并在 JVM 上运行。

//AN_Xml:

Gradle 优势

//AN_Xml:

Gradle 是新一代的构建系统,具有高效和灵活等诸多优势,广泛用于 Java 开发。不仅 Android 将其作为官方构建系统, 越来越多的 Java 项目比如 Spring Boot 也慢慢迁移到 Gradle。

//AN_Xml:
    //AN_Xml:
  • 在灵活性上,Gradle 支持基于 Groovy 语言编写脚本,侧重于构建过程的灵活性,适合于构建复杂度较高的项目,可以完成非常复杂的构建。
  • //AN_Xml:
  • 在粒度性上,Gradle 构建的粒度细化到了每一个 task 之中。并且它所有的 Task 源码都是开源的,在我们掌握了这一整套打包流程后,我们就可以通过去修改它的 Task 去动态改变其执行流程。
  • //AN_Xml:
  • 在扩展性上,Gradle 支持插件机制,所以我们可以复用这些插件,就如同复用库一样简单方便。
  • //AN_Xml:
//AN_Xml:

Gradle Wrapper 介绍

//AN_Xml:

Gradle 官方文档是这样介绍的 Gradle Wrapper 的:

//AN_Xml:
//AN_Xml:

The recommended way to execute any Gradle build is with the help of the Gradle Wrapper (in short just “Wrapper”). The Wrapper is a script that invokes a declared version of Gradle, downloading it beforehand if necessary. As a result, developers can get up and running with a Gradle project quickly without having to follow manual installation processes saving your company time and money.

//AN_Xml:

执行 Gradle 构建的推荐方法是借助 Gradle Wrapper(简而言之就是“Wrapper”)。Wrapper 它是一个脚本,调用了已经声明的 Gradle 版本,如果需要的话,可以预先下载它。因此,开发人员可以快速启动并运行 Gradle 项目,而不必遵循手动安装过程,从而为公司节省时间和金钱。

//AN_Xml:
//AN_Xml:

我们可以称 Gradle Wrapper 为 Gradle 包装器,它将 Gradle 再次包装,让所有的 Gradle 构建方法在 Gradle 包装器的帮助下运行。

//AN_Xml:

Gradle Wrapper 的工作流程图如下(图源Gradle Wrapper 官方文档介绍):

//AN_Xml:

包装器工作流程

//AN_Xml:

整个流程主要分为下面 3 步:

//AN_Xml:
    //AN_Xml:
  1. 首先当我们刚创建的时候,如果指定的版本没有被下载,就先会去 Gradle 的服务器中下载对应版本的压缩包;
  2. //AN_Xml:
  3. 下载完成后需要先进行解压缩并且执行批处理文件;
  4. //AN_Xml:
  5. 后续项目每次构建都会重用这个解压过的 Gradle 版本。
  6. //AN_Xml:
//AN_Xml:

Gradle Wrapper 会给我们带来下面这些好处:

//AN_Xml:
    //AN_Xml:
  1. 在给定的 Gradle 版本上标准化项目,从而实现更可靠和健壮的构建。
  2. //AN_Xml:
  3. 可以让我们的电脑中不安装 Gradle 环境也可以运行 Gradle 项目。
  4. //AN_Xml:
  5. 为不同的用户和执行环境(例如 IDE 或持续集成服务器)提供新的 Gradle 版本就像更改 Wrapper 定义一样简单。
  6. //AN_Xml:
//AN_Xml:

生成 Gradle Wrapper

//AN_Xml:

如果想要生成 Gradle Wrapper 的话,需要本地配置好 Gradle 环境变量。Gradle 中已经内置了 Wrapper Task,在项目根目录执行执行gradle wrapper命令即可帮助我们生成 Gradle Wrapper。

//AN_Xml:

执行命令 gradle wrapper 命令时可以指定一些参数来控制 wrapper 的生成。具体有如下两个配置参数:

//AN_Xml:
    //AN_Xml:
  • --gradle-version 用于指定使用的 Gradle 的版本
  • //AN_Xml:
  • --gradle-distribution-url 用于指定下载 Gradle 版本的 URL,该值的规则是 http://services.gradle.org/distributions/gradle-${gradleVersion}-bin.zip
  • //AN_Xml:
//AN_Xml:

执行gradle wrapper命令之后,Gradle Wrapper 就生成完成了,项目根目录中生成如下文件:

//AN_Xml:
├── gradle
//AN_Xml:│   └── wrapper
//AN_Xml:│       ├── gradle-wrapper.jar
//AN_Xml:│       └── gradle-wrapper.properties
//AN_Xml:├── gradlew
//AN_Xml:└── gradlew.bat
//AN_Xml:

每个文件的含义如下:

//AN_Xml:
    //AN_Xml:
  • gradle-wrapper.jar:包含了 Gradle 运行时的逻辑代码。
  • //AN_Xml:
  • gradle-wrapper.properties:定义了 Gradle 的版本号和 Gradle 运行时的行为属性。
  • //AN_Xml:
  • gradlew:Linux 平台下,用于执行 Gralde 命令的包装器脚本。
  • //AN_Xml:
  • gradlew.bat:Windows 平台下,用于执行 Gralde 命令的包装器脚本。
  • //AN_Xml:
//AN_Xml:

gradle-wrapper.properties 文件的内容如下:

//AN_Xml:
distributionBase=GRADLE_USER_HOME
//AN_Xml:distributionPath=wrapper/dists
//AN_Xml:distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
//AN_Xml:zipStoreBase=GRADLE_USER_HOME
//AN_Xml:zipStorePath=wrapper/dists
//AN_Xml:
    //AN_Xml:
  • distributionBase:Gradle 解包后存储的父目录。
  • //AN_Xml:
  • distributionPathdistributionBase指定目录的子目录。distributionBase+distributionPath就是 Gradle 解包后的存放的具体目录。
  • //AN_Xml:
  • distributionUrl:Gradle 指定版本的压缩包下载地址。
  • //AN_Xml:
  • zipStoreBase:Gradle 压缩包下载后存储父目录。
  • //AN_Xml:
  • zipStorePathzipStoreBase指定目录的子目录。zipStoreBase+zipStorePath就是 Gradle 压缩包的存放位置。
  • //AN_Xml:
//AN_Xml:

更新 Gradle Wrapper

//AN_Xml:

更新 Gradle Wrapper 有 2 种方式:

//AN_Xml:
    //AN_Xml:
  1. 接修改distributionUrl字段,然后执行 Gradle 命令。
  2. //AN_Xml:
  3. 执行 gradlew 命令gradlew wrapper –-gradle-version [version]
  4. //AN_Xml:
//AN_Xml:

下面的命令会将 Gradle 版本升级为 7.6。

//AN_Xml:
gradlew wrapper --gradle-version 7.6
//AN_Xml:

gradle-wrapper.properties 文件中的 distributionUrl 属性也发生了改变。

//AN_Xml:
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
//AN_Xml:

自定义 Gradle Wrapper

//AN_Xml:

Gradle 已经内置了 Wrapper Task,因此构建 Gradle Wrapper 会生成 Gradle Wrapper 的属性文件,这个属性文件可以通过自定义 Wrapper Task 来设置。比如我们想要修改要下载的 Gralde 版本为 7.6,可以这么设置:

//AN_Xml:
task wrapper(type: Wrapper) {
//AN_Xml:    gradleVersion = '7.6'
//AN_Xml:}
//AN_Xml:

也可以设置 Gradle 发行版压缩包的下载地址和 Gradle 解包后的本地存储路径等配置。

//AN_Xml:
task wrapper(type: Wrapper) {
//AN_Xml:    gradleVersion = '7.6'
//AN_Xml:    distributionUrl = '../../gradle-7.6-bin.zip'
//AN_Xml:    distributionPath=wrapper/dists
//AN_Xml:}
//AN_Xml:

distributionUrl 属性可以设置为本地的项目目录,你也可以设置为网络地址。

//AN_Xml:

Gradle 任务

//AN_Xml:

在 Gradle 中,任务(Task)是构建执行的单个工作单元。

//AN_Xml:

Gradle 的构建是基于 Task 进行的,当你运行项目的时候,实际就是在执行了一系列的 Task 比如编译 Java 源码的 Task、生成 jar 文件的 Task。

//AN_Xml:

Task 的声明方式如下(还有其他几种声明方式):

//AN_Xml:
// 声明一个名字为 helloTask 的 Task
//AN_Xml:task helloTask{
//AN_Xml:     doLast{
//AN_Xml:       println "Hello"
//AN_Xml:     }
//AN_Xml:}
//AN_Xml:

创建一个 Task 后,可以根据需要给 Task 添加不同的 Action,上面的“doLast”就是给队列尾增加一个 Action。

//AN_Xml:
 //在Action 队列头部添加Action
//AN_Xml: Task doFirst(Action<? super Task> action);
//AN_Xml: Task doFirst(Closure action);
//AN_Xml:
//AN_Xml: //在Action 队列尾部添加Action
//AN_Xml: Task doLast(Action<? super Task> action);
//AN_Xml: Task doLast(Closure action);
//AN_Xml:
//AN_Xml: //删除所有的Action
//AN_Xml: Task deleteAllActions();
//AN_Xml:

一个 Task 中可以有多个 Acton,从队列头部开始向队列尾部执行 Acton。

//AN_Xml:

Action 代表的是一个个函数、方法,每个 Task 都是一堆 Action 按序组成的执行图。

//AN_Xml:

Task 声明依赖的关键字是dependsOn,支持声明一个或多个依赖:

//AN_Xml:
task first {
//AN_Xml: doLast {
//AN_Xml:        println "+++++first+++++"
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:task second {
//AN_Xml: doLast {
//AN_Xml:        println "+++++second+++++"
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:
//AN_Xml:// 指定多个 task 依赖
//AN_Xml:task print(dependsOn :[second,first]) {
//AN_Xml: doLast {
//AN_Xml:      logger.quiet "指定多个task依赖"
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:
//AN_Xml:// 指定一个 task 依赖
//AN_Xml:task third(dependsOn : print) {
//AN_Xml: doLast {
//AN_Xml:      println '+++++third+++++'
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

执行 Task 之前,会先执行它的依赖 Task。

//AN_Xml:

我们还可以设置默认 Task,脚本中我们不调用默认 Task ,也会执行。

//AN_Xml:
defaultTasks 'clean', 'run'
//AN_Xml:
//AN_Xml:task clean {
//AN_Xml:    doLast {
//AN_Xml:        println 'Default Cleaning!'
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:
//AN_Xml:task run {
//AN_Xml:    doLast {
//AN_Xml:        println 'Default Running!'
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

Gradle 本身也内置了很多 Task 比如 copy(复制文件)、delete(删除文件)。

//AN_Xml:
task deleteFile(type: Delete) {
//AN_Xml:    delete "C:\\Users\\guide\\Desktop\\test"
//AN_Xml:}
//AN_Xml:

Gradle 插件

//AN_Xml:

Gradle 提供的是一套核心的构建机制,而 Gradle 插件则是运行在这套机制上的一些具体构建逻辑,其本质上和 .gradle 文件是相同。你可以将 Gradle 插件看作是封装了一系列 Task 并执行的工具。

//AN_Xml:

Gradle 插件主要分为两类:

//AN_Xml:
    //AN_Xml:
  • 脚本插件:脚本插件就是一个普通的脚本文件,它可以被导入都其他构建脚本中。
  • //AN_Xml:
  • 二进制插件 / 对象插件:在一个单独的插件模块中定义,其他模块通过 Plugin ID 应用插件。因为这种方式发布和复用更加友好,我们一般接触到的 Gradle 插件都是指二进制插件的形式。
  • //AN_Xml:
//AN_Xml:

虽然 Gradle 插件与 .gradle 文件本质上没有区别,.gradle 文件也能实现 Gradle 插件类似的功能。但是,Gradle 插件使用了独立模块封装构建逻辑,无论是从开发开始使用来看,Gradle 插件的整体体验都更友好。

//AN_Xml:
    //AN_Xml:
  • 逻辑复用: 将相同的逻辑提供给多个相似项目复用,减少重复维护类似逻辑开销。当然 .gradle 文件也能做到逻辑复用,但 Gradle 插件的封装性更好;
  • //AN_Xml:
  • 组件发布: 可以将插件发布到 Maven 仓库进行管理,其他项目可以使用插件 ID 依赖。当然 .gradle 文件也可以放到一个远程路径被其他项目引用;
  • //AN_Xml:
  • 构建配置: Gradle 插件可以声明插件扩展来暴露可配置的属性,提供定制化能力。当然 .gradle 文件也可以做到,但实现会麻烦些。
  • //AN_Xml:
//AN_Xml:

Gradle 构建生命周期

//AN_Xml:

Gradle 构建的生命周期有三个阶段:初始化阶段,配置阶段运行阶段

//AN_Xml:

//AN_Xml:

在初始化阶段与配置阶段之间、配置阶段结束之后、执行阶段结束之后,我们都可以加一些定制化的 Hook。

//AN_Xml:

//AN_Xml:

初始化阶段

//AN_Xml:

Gradle 支持单项目和多项目构建。在初始化阶段,Gradle 确定哪些项目将参与构建,并为每个项目创建一个 Project 实例 。本质上也就是执行 settings.gradle 脚本,从而读取整个项目中有多少个 Project 实例。

//AN_Xml:

配置阶段

//AN_Xml:

在配置阶段,Gradle 会解析每个工程的 build.gradle 文件,创建要执行的任务子集和确定各种任务之间的关系,以供执行阶段按照顺序执行,并对任务的做一些初始化配置。

//AN_Xml:

每个 build.gradle 对应一个 Project 对象,配置阶段执行的代码包括 build.gradle 中的各种语句、闭包以及 Task 中的配置语句。

//AN_Xml:

在配置阶段结束后,Gradle 会根据 Task 的依赖关系会创建一个 有向无环图

//AN_Xml:

运行阶段

//AN_Xml:

在运行阶段,Gradle 根据配置阶段创建和配置的要执行的任务子集,执行任务。

//AN_Xml:

参考

//AN_Xml: //AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: MySQL自增主键一定是连续的吗 //AN_Xml: https://javaguide.cn/database/mysql/mysql-auto-increment-primary-key-continuous.html //AN_Xml: https://javaguide.cn/database/mysql/mysql-auto-increment-primary-key-continuous.html //AN_Xml: MySQL自增主键一定是连续的吗 //AN_Xml: 详解MySQL自增主键不连续的原因,分析唯一键冲突、事务回滚、批量插入等场景下自增值的分配机制,以及InnoDB自增锁模式的配置与影响。 //AN_Xml: 数据库 //AN_Xml: Thu, 02 Feb 2023 09:54:04 GMT //AN_Xml: //AN_Xml:

作者:飞天小牛肉

//AN_Xml:

原文:https://mp.weixin.qq.com/s/qci10h9rJx_COZbHV3aygQ

//AN_Xml: //AN_Xml:

众所周知,自增主键可以让聚集索引尽量地保持递增顺序插入,避免了随机查询,从而提高了查询效率。

//AN_Xml:

但实际上,MySQL 的自增主键并不能保证一定是连续递增的。

//AN_Xml:

下面举个例子来看下,如下所示创建一张表:

//AN_Xml:

//AN_Xml:

自增值保存在哪里?

//AN_Xml:

使用 insert into test_pk values(null, 1, 1) 插入一行数据,再执行 show create table 命令来看一下表的结构定义:

//AN_Xml:

//AN_Xml:

上述表的结构定义存放在后缀名为 .frm 的本地文件中,在 MySQL 安装目录下的 data 文件夹下可以找到这个 .frm 文件:

//AN_Xml:

//AN_Xml:

从上述表结构可以看到,表定义里面出现了一个 AUTO_INCREMENT=2,表示下一次插入数据时,如果需要自动生成自增值,会生成 id = 2。

//AN_Xml:

但需要注意的是,自增值并不会保存在这个表结构也就是 .frm 文件中,不同的引擎对于自增值的保存策略不同:

//AN_Xml:

1)MyISAM 引擎的自增值保存在数据文件中

//AN_Xml:

2)InnoDB 引擎的自增值,其实是保存在了内存里,并没有持久化。第一次打开表的时候,都会去找自增值的最大值 max(id),然后将 max(id)+1 作为这个表当前的自增值。

//AN_Xml:

举个例子:我们现在表里当前数据行里最大的 id 是 1,AUTO_INCREMENT=2,对吧。这时候,我们删除 id=1 的行,AUTO_INCREMENT 还是 2。

//AN_Xml:

//AN_Xml:

但如果马上重启 MySQL 实例,重启后这个表的 AUTO_INCREMENT 就会变成 1。 也就是说,MySQL 重启可能会修改一个表的 AUTO_INCREMENT 的值。

//AN_Xml:

//AN_Xml:

//AN_Xml:

以上,是在我本地 MySQL 5.x 版本的实验,实际上,到了 MySQL 8.0 版本后,自增值的变更记录被放在了 redo log 中,提供了自增值持久化的能力 ,也就是实现了“如果发生重启,表的自增值可以根据 redo log 恢复为 MySQL 重启前的值”

//AN_Xml:

也就是说对于上面这个例子来说,重启实例后这个表的 AUTO_INCREMENT 仍然是 2。

//AN_Xml:

理解了 MySQL 自增值到底保存在哪里以后,我们再来看看自增值的修改机制,并以此引出第一种自增值不连续的场景。

//AN_Xml:

自增值不连续的场景

//AN_Xml:

自增值不连续场景 1

//AN_Xml:

在 MySQL 里面,如果字段 id 被定义为 AUTO_INCREMENT,在插入一行数据的时候,自增值的行为如下:

//AN_Xml:
    //AN_Xml:
  • 如果插入数据时 id 字段指定为 0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段;
  • //AN_Xml:
  • 如果插入数据时 id 字段指定了具体的值,就直接使用语句里指定的值。
  • //AN_Xml:
//AN_Xml:

根据要插入的值和当前自增值的大小关系,自增值的变更结果也会有所不同。假设某次要插入的值是 insert_num,当前的自增值是 autoIncrement_num

//AN_Xml:
    //AN_Xml:
  • 如果 insert_num < autoIncrement_num,那么这个表的自增值不变
  • //AN_Xml:
  • 如果 insert_num >= autoIncrement_num,就需要把当前自增值修改为新的自增值
  • //AN_Xml:
//AN_Xml:

也就是说,如果插入的 id 是 100,当前的自增值是 90,insert_num >= autoIncrement_num,那么自增值就会被修改为新的自增值即 101

//AN_Xml:

一定是这样吗?

//AN_Xml:

非也~

//AN_Xml:

了解过分布式 id 的小伙伴一定知道,为了避免两个库生成的主键发生冲突,我们可以让一个库的自增 id 都是奇数,另一个库的自增 id 都是偶数

//AN_Xml:

这个奇数偶数其实是通过 auto_increment_offsetauto_increment_increment 这两个参数来决定的,这俩分别用来表示自增的初始值和步长,默认值都是 1。

//AN_Xml:

所以,上面的例子中生成新的自增值的步骤实际是这样的:从 auto_increment_offset 开始,以 auto_increment_increment 为步长,持续叠加,直到找到第一个大于 100 的值,作为新的自增值。

//AN_Xml:

所以,这种情况下,自增值可能会是 102,103 等等之类的,就会导致不连续的主键 id。

//AN_Xml:

更遗憾的是,即使在自增初始值和步长这两个参数都设置为 1 的时候,自增主键 id 也不一定能保证主键是连续的

//AN_Xml:

自增值不连续场景 2

//AN_Xml:

举个例子,我们现在往表里插入一条 (null,1,1) 的记录,生成的主键是 1,AUTO_INCREMENT= 2,对吧

//AN_Xml:

//AN_Xml:

这时我再执行一条插入 (null,1,1) 的命令,很显然会报错 Duplicate entry,因为我们设置了一个唯一索引字段 a

//AN_Xml:

//AN_Xml:

但是,你会惊奇的发现,虽然插入失败了,但自增值仍然从 2 增加到了 3!

//AN_Xml:

这是为啥?

//AN_Xml:

我们来分析下这个 insert 语句的执行流程:

//AN_Xml:
    //AN_Xml:
  1. 执行器调用 InnoDB 引擎接口准备插入一行记录 (null,1,1);
  2. //AN_Xml:
  3. InnoDB 发现用户没有指定自增 id 的值,则获取表 test_pk 当前的自增值 2;
  4. //AN_Xml:
  5. 将传入的记录改成 (2,1,1);
  6. //AN_Xml:
  7. 将表的自增值改成 3;
  8. //AN_Xml:
  9. 继续执行插入数据操作,由于已经存在 a=1 的记录,所以报 Duplicate key error,语句返回
  10. //AN_Xml:
//AN_Xml:

可以看到,自增值修改的这个操作,是在真正执行插入数据的操作之前。

//AN_Xml:

这个语句真正执行的时候,因为碰到唯一键 a 冲突,所以 id = 2 这一行并没有插入成功,但也没有将自增值再改回去。所以,在这之后,再插入新的数据行时,拿到的自增 id 就是 3。也就是说,出现了自增主键不连续的情况。

//AN_Xml:

至此,我们已经罗列了两种自增主键不连续的情况:

//AN_Xml:
    //AN_Xml:
  1. 自增初始值和自增步长设置不为 1
  2. //AN_Xml:
  3. 唯一键冲突
  4. //AN_Xml:
//AN_Xml:

除此之外,事务回滚也会导致这种情况

//AN_Xml:

自增值不连续场景 3

//AN_Xml:

我们现在表里有一行 (1,1,1) 的记录,AUTO_INCREMENT = 3:

//AN_Xml:

//AN_Xml:

我们先插入一行数据 (null, 2, 2),也就是 (3, 2, 2) 嘛,并且 AUTO_INCREMENT 变为 4:

//AN_Xml:

//AN_Xml:

再去执行这样一段 SQL:

//AN_Xml:

//AN_Xml:

虽然我们插入了一条 (null, 3, 3) 记录,但是使用 rollback 进行回滚了,所以数据库中是没有这条记录的:

//AN_Xml:

//AN_Xml:

在这种事务回滚的情况下,自增值并没有同样发生回滚!如下图所示,自增值仍然固执地从 4 增加到了 5:

//AN_Xml:

//AN_Xml:

所以这时候我们再去插入一条数据(null, 3, 3)的时候,主键 id 就会被自动赋为 5 了:

//AN_Xml:

//AN_Xml:

那么,为什么在出现唯一键冲突或者回滚的时候,MySQL 没有把表的自增值改回去呢?回退回去的话不就不会发生自增 id 不连续了吗?

//AN_Xml:

事实上,这么做的主要原因是为了提高性能。

//AN_Xml:

我们直接用反证法来验证:假设 MySQL 在事务回滚的时候会把自增值改回去,会发生什么?

//AN_Xml:

现在有两个并行执行的事务 A 和 B,在申请自增值的时候,为了避免两个事务申请到相同的自增 id,肯定要加锁,然后顺序申请,对吧。

//AN_Xml:
    //AN_Xml:
  1. 假设事务 A 申请到了 id = 1, 事务 B 申请到 id=2,那么这时候表 t 的自增值是 3,之后继续执行。
  2. //AN_Xml:
  3. 事务 B 正确提交了,但事务 A 出现了唯一键冲突,也就是 id = 1 的那行记录插入失败了,那如果允许事务 A 把自增 id 回退,也就是把表的当前自增值改回 1,那么就会出现这样的情况:表里面已经有 id = 2 的行,而当前的自增 id 值是 1。
  4. //AN_Xml:
  5. 接下来,继续执行的其他事务就会申请到 id=2。这时,就会出现插入语句报错“主键冲突”。
  6. //AN_Xml:
//AN_Xml:

//AN_Xml:

而为了解决这个主键冲突,有两种方法:

//AN_Xml:
    //AN_Xml:
  1. 每次申请 id 之前,先判断表里面是否已经存在这个 id,如果存在,就跳过这个 id
  2. //AN_Xml:
  3. 把自增 id 的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才能再申请自增 id
  4. //AN_Xml:
//AN_Xml:

很显然,上述两个方法的成本都比较高,会导致性能问题。而究其原因呢,是我们假设的这个 “允许自增 id 回退”。

//AN_Xml:

因此,InnoDB 放弃了这个设计,语句执行失败也不回退自增 id。也正是因为这样,所以才只保证了自增 id 是递增的,但不保证是连续的。

//AN_Xml:

综上,已经分析了三种自增值不连续的场景,还有第四种场景:批量插入数据。

//AN_Xml:

自增值不连续场景 4

//AN_Xml:

对于批量插入数据的语句,MySQL 有一个批量申请自增 id 的策略:

//AN_Xml:
    //AN_Xml:
  1. 语句执行过程中,第一次申请自增 id,会分配 1 个;
  2. //AN_Xml:
  3. 1 个用完以后,这个语句第二次申请自增 id,会分配 2 个;
  4. //AN_Xml:
  5. 2 个用完以后,还是这个语句,第三次申请自增 id,会分配 4 个;
  6. //AN_Xml:
  7. 依此类推,同一个语句去申请自增 id,每次申请到的自增 id 个数都是上一次的两倍。
  8. //AN_Xml:
//AN_Xml:

注意,这里说的批量插入数据,不是在普通的 insert 语句里面包含多个 value 值!!!,因为这类语句在申请自增 id 的时候,是可以精确计算出需要多少个 id 的,然后一次性申请,申请完成后锁就可以释放了。

//AN_Xml:

而对于 insert … select、replace …… select 和 load data 这种类型的语句来说,MySQL 并不知道到底需要申请多少 id,所以就采用了这种批量申请的策略,毕竟一个一个申请的话实在太慢了。

//AN_Xml:

举个例子,假设我们现在这个表有下面这些数据:

//AN_Xml:

//AN_Xml:

我们创建一个和当前表 test_pk 有相同结构定义的表 test_pk2

//AN_Xml:

//AN_Xml:

然后使用 insert...selectteset_pk2 表中批量插入数据:

//AN_Xml:

//AN_Xml:

可以看到,成功导入了数据。

//AN_Xml:

再来看下 test_pk2 的自增值是多少:

//AN_Xml:

//AN_Xml:

如上分析,是 8 而不是 6

//AN_Xml:

具体来说,insert……select 实际上往表中插入了 5 行数据 (1 1)(2 2)(3 3)(4 4)(5 5)。但是,这五行数据是分三次申请的自增 id,结合批量申请策略,每次申请到的自增 id 个数都是上一次的两倍,所以:

//AN_Xml:
    //AN_Xml:
  • 第一次申请到了一个 id:id=1
  • //AN_Xml:
  • 第二次被分配了两个 id:id=2 和 id=3
  • //AN_Xml:
  • 第三次被分配到了 4 个 id:id=4、id = 5、id = 6、id=7
  • //AN_Xml:
//AN_Xml:

由于这条语句实际只用上了 5 个 id,所以 id=6 和 id=7 就被浪费掉了。之后,再执行 insert into test_pk2 values(null,6,6),实际上插入的数据就是(8,6,6):

//AN_Xml:

//AN_Xml:

小结

//AN_Xml:

本文总结下自增值不连续的 4 个场景:

//AN_Xml:
    //AN_Xml:
  1. 自增初始值和自增步长设置不为 1
  2. //AN_Xml:
  3. 唯一键冲突
  4. //AN_Xml:
  5. 事务回滚
  6. //AN_Xml:
  7. 批量插入(如 insert...select 语句)
  8. //AN_Xml:
//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 乐观锁和悲观锁详解 //AN_Xml: https://javaguide.cn/java/concurrent/optimistic-lock-and-pessimistic-lock.html //AN_Xml: https://javaguide.cn/java/concurrent/optimistic-lock-and-pessimistic-lock.html //AN_Xml: 乐观锁和悲观锁详解 //AN_Xml: 乐观锁与悲观锁深度对比:详解synchronized/ReentrantLock悲观锁实现、CAS/版本号乐观锁机制、适用场景分析、性能对比与选型建议。 //AN_Xml: Java //AN_Xml: Tue, 31 Jan 2023 08:27:36 GMT //AN_Xml: 如果将悲观锁(Pessimistic Lock)和乐观锁(Optimistic Lock)对应到现实生活中来。悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题。

//AN_Xml:

什么是悲观锁?

//AN_Xml:

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

//AN_Xml:

像 Java 中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

//AN_Xml:
public void performSynchronisedTask() {
//AN_Xml:    synchronized (this) {
//AN_Xml:        // 需要同步的操作
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:
//AN_Xml:private Lock lock = new ReentrantLock();
//AN_Xml:lock.lock();
//AN_Xml:try {
//AN_Xml:   // 需要同步的操作
//AN_Xml:} finally {
//AN_Xml:    lock.unlock();
//AN_Xml:}
//AN_Xml:

高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题(线程获得锁的顺序不当时),影响代码的正常运行。

//AN_Xml:

什么是乐观锁?

//AN_Xml:

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。

//AN_Xml:

在 Java 中java.util.concurrent.atomic包下面的原子变量类(比如AtomicIntegerLongAdder)就是使用了乐观锁的一种实现方式 CAS 实现的。
//AN_Xml:JUC原子类概览

//AN_Xml:
// LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好
//AN_Xml:// 代价就是会消耗更多的内存空间(空间换时间)
//AN_Xml:LongAdder sum = new LongAdder();
//AN_Xml:sum.increment();
//AN_Xml:

高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败并重试,这样同样会非常影响性能,导致 CPU 飙升。

//AN_Xml:

不过,大量失败重试的问题也是可以解决的,像我们前面提到的 LongAdder以空间换时间的方式就解决了这个问题。

//AN_Xml:

理论上来说:

//AN_Xml:
    //AN_Xml:
  • 悲观锁通常多用于写比较多的情况(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如LongAdder),也是可以考虑使用乐观锁的,要视实际情况而定。
  • //AN_Xml:
  • 乐观锁通常多用于写比较少的情况(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考java.util.concurrent.atomic包下面的原子变量类)。
  • //AN_Xml:
//AN_Xml:

如何实现乐观锁?

//AN_Xml:

乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。

//AN_Xml:

版本号机制

//AN_Xml:

一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

//AN_Xml:

举一个简单的例子:假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

//AN_Xml:
    //AN_Xml:
  1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
  2. //AN_Xml:
  3. 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
  4. //AN_Xml:
  5. 操作员 A 完成了修改工作,将数据版本号( version=1 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
  6. //AN_Xml:
  7. 操作员 B 完成了操作,也将版本号( version=1 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,而数据库记录当前版本为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
  8. //AN_Xml:
//AN_Xml:

这样就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。

//AN_Xml:

CAS 算法

//AN_Xml:

CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。

//AN_Xml:

CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。

//AN_Xml:
//AN_Xml:

原子操作 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。

//AN_Xml:
//AN_Xml:

CAS 涉及到三个操作数:

//AN_Xml:
    //AN_Xml:
  • V:要更新的变量值(Var)
  • //AN_Xml:
  • E:预期值(Expected)
  • //AN_Xml:
  • N:拟写入的新值(New)
  • //AN_Xml:
//AN_Xml:

当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。

//AN_Xml:

举一个简单的例子:线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。

//AN_Xml:
    //AN_Xml:
  1. i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。
  2. //AN_Xml:
  3. i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。
  4. //AN_Xml:
//AN_Xml:

当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

//AN_Xml:

关于 CAS 的进一步介绍,可以阅读读者写的这篇文章:CAS 详解,其中详细提到了 Java 中 CAS 的实现以及 CAS 存在的一些问题。

//AN_Xml:

总结

//AN_Xml:

本文详细介绍了乐观锁和悲观锁的概念以及乐观锁常见实现方式:

//AN_Xml:
    //AN_Xml:
  • 悲观锁基于悲观的假设,认为共享资源在每次访问时都会发生冲突,因此在每次操作时都会加锁。这种锁机制会导致其他线程阻塞,直到锁被释放。Java 中的 synchronizedReentrantLock 是悲观锁的典型实现方式。虽然悲观锁能有效避免数据竞争,但在高并发场景下会导致线程阻塞、上下文切换频繁,从而影响系统性能,并且还可能引发死锁问题。
  • //AN_Xml:
  • 乐观锁基于乐观的假设,认为共享资源在每次访问时不会发生冲突,因此无须加锁,只需在提交修改时验证数据是否被其他线程修改。Java 中的 AtomicIntegerLongAdder 等类通过 CAS(Compare-And-Swap)算法实现了乐观锁。乐观锁避免了线程阻塞和死锁问题,在读多写少的场景中性能优越。但在写操作频繁的情况下,可能会导致大量重试和失败,从而影响性能。
  • //AN_Xml:
  • 乐观锁主要通过版本号机制或 CAS 算法实现。版本号机制通过比较版本号确保数据一致性,而 CAS 通过硬件指令实现原子操作,直接比较和交换变量值。
  • //AN_Xml:
//AN_Xml:

悲观锁和乐观锁各有优缺点,适用于不同的应用场景。在实际开发中,选择合适的锁机制能够有效提升系统的并发性能和稳定性。

//AN_Xml:

参考

//AN_Xml:
    //AN_Xml:
  • 《Java 并发编程核心 78 讲》
  • //AN_Xml:
  • 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其 Java 实现!:https://zhuanlan.zhihu.com/p/71156910
  • //AN_Xml:
//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Elasticsearch常见面试题总结(付费) //AN_Xml: https://javaguide.cn/database/elasticsearch/elasticsearch-questions-01.html //AN_Xml: https://javaguide.cn/database/elasticsearch/elasticsearch-questions-01.html //AN_Xml: Elasticsearch常见面试题总结(付费) //AN_Xml: Elasticsearch常见面试题总结,涵盖ES核心概念、倒排索引原理、分片与副本机制、查询DSL、聚合分析、集群调优等高频面试知识点。 //AN_Xml: 数据库 //AN_Xml: Sun, 29 Jan 2023 03:31:13 GMT //AN_Xml: Elasticsearch 相关的面试题为我的知识星球(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了《Java 面试指北》中。

//AN_Xml:

//AN_Xml:

这本《Java 面试指北》(后端面试通用)的内容经过反复打磨,质量极高,旨在帮助每一位 Java/后端求职者从容应对面试挑战。

//AN_Xml:

用数据说话: 截至目前,专栏累计阅读量已突破 477.1W,收获点赞 5,118 个,评论互动 1,657 条。值得一提的是,评论区不仅仅是留言板,更是答疑区——几乎每一条提问,我都会用心回复,确保无疑问遗留。

//AN_Xml:

//AN_Xml:

《Java 面试指北》(点击链接即可查看详细介绍)的部分内容展示如下,你可以将其看作是 JavaGuide 的补充完善,两者可以配合使用。

//AN_Xml:

《Java 面试指北》内容概览

//AN_Xml:

下面是《Java 面试指北》收到的部分球友的真实反馈:

//AN_Xml:

《Java 面试指北》 收到的部分球友的真实反馈

//AN_Xml:

如果需要面试辅导(比如简历优化、一对一模拟问答、高频考点突击资料等),欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽然是白菜价(0.4 元/天),但质量很高、服务也很全面,主打一个良心!

//AN_Xml:

下面是星球提供的部分服务(点击下方图片即可获取知识星球的详细介绍):

//AN_Xml:

星球服务

//AN_Xml:

下面是今年收到了部分好评,每一条都是真实存在的。我看到很多培训班或者机构通过虚构一些不存在的好评来欺骗他人购买高价服务(行业内非常常见),真的很难理解。

//AN_Xml:

球友对星球的真实评价

//AN_Xml:

我有自己的原则,不割韭菜,用心做内容,真心希望帮助到你! 如果你感兴趣的话,不妨花 3 分钟左右看看星球的详细介绍:JavaGuide 知识星球详细介绍

//AN_Xml:

这里再送一张 30 元的星球专属优惠券,数量有限(价格即将上调。老用户续费半价 ,微信扫码即可续费)!

//AN_Xml:

知识星球30元优惠卷

//AN_Xml:

🚀 入圈必做(干货满满,一定要看!):

//AN_Xml:
    //AN_Xml:
  1. 星球使用指南
  2. //AN_Xml:
  3. 优质主题汇总
  4. //AN_Xml:
//AN_Xml:

无任何套路,无任何潜在收费项。用心做内容,不割韭菜!

//AN_Xml:

不过, 一定要确定需要再进 。并且, 三天之内觉得内容不满意可以全额退款

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: MySQL执行计划分析 //AN_Xml: https://javaguide.cn/database/mysql/mysql-query-execution-plan.html //AN_Xml: https://javaguide.cn/database/mysql/mysql-query-execution-plan.html //AN_Xml: MySQL执行计划分析 //AN_Xml: 详解MySQL EXPLAIN执行计划的各列含义,包括id、select_type、type、key、rows、Extra等关键字段解读,帮助你分析SQL性能瓶颈并进行针对性优化。 //AN_Xml: 数据库 //AN_Xml: Sat, 14 Jan 2023 10:33:11 GMT //AN_Xml: 优化 SQL 的第一步应该是读懂 SQL 的执行计划。本篇文章,我们一起来学习下 MySQL EXPLAIN 执行计划相关知识。

//AN_Xml:
//AN_Xml:

版本说明:本文内容基于 MySQL 5.7+ 和 8.0+ 版本。filteredpartitions 列在 MySQL 5.7+ 可用,EXPLAIN ANALYZE 和 Hash Join 特性需要 MySQL 8.0.18+ 和 8.0.20+。

//AN_Xml:
//AN_Xml:

什么是执行计划?

//AN_Xml:

执行计划 是指一条 SQL 语句在经过 MySQL 查询优化器 的优化后,具体的执行方式。

//AN_Xml:

执行计划通常用于 SQL 性能分析、优化等场景。通过 EXPLAIN 的结果,可以了解到如数据表的查询顺序、数据查询操作的操作类型、哪些索引可以被命中、哪些索引实际会命中、每个数据表有多少行记录被查询等信息。

//AN_Xml:

如何获取执行计划?

//AN_Xml:

MySQL 为我们提供了 EXPLAIN 命令,来获取执行计划的相关信息。

//AN_Xml:

需要注意的是,标准 EXPLAIN 语句并不会真的去执行相关的语句,而是通过查询优化器对语句进行分析,找出最优的查询方案,并显示对应的信息。

//AN_Xml:

MySQL 8.0.18 引入了 EXPLAIN ANALYZE,它会真正执行查询并输出每个步骤的实际耗时与行数,比标准 EXPLAIN 的估算数据更可靠,适合在测试环境深度排查慢查询:

//AN_Xml:
mysql> EXPLAIN ANALYZE SELECT * FROM users WHERE age = 25\G
//AN_Xml:*************************** 1. row ***************************
//AN_Xml:EXPLAIN: -> Covering index lookup on users using idx_age_score_name (age=25)
//AN_Xml:(cost=1.52 rows=12) (actual time=0.0272..0.0344 rows=12 loops=1)
//AN_Xml:

此外,EXPLAIN FORMAT=JSON 可以输出优化器的成本模型数据(query_cost),比表格形式更能反映各步骤的实际代价,在多表 JOIN 或子查询调优时尤为有用:

//AN_Xml:
mysql> EXPLAIN FORMAT=JSON SELECT * FROM users WHERE age = 25\G
//AN_Xml:*************************** 1. row ***************************
//AN_Xml:EXPLAIN: {
//AN_Xml:  "query_block": {
//AN_Xml:    "select_id": 1,
//AN_Xml:    "cost_info": {
//AN_Xml:      "query_cost": "1.52"
//AN_Xml:    },
//AN_Xml:    "table": {
//AN_Xml:      "table_name": "users",
//AN_Xml:      "access_type": "ref",
//AN_Xml:      "key": "idx_age_score_name",
//AN_Xml:      "rows_examined_per_scan": 12,
//AN_Xml:      "filtered": "100.00",
//AN_Xml:      "using_index": true
//AN_Xml:    }
//AN_Xml:  }
//AN_Xml:}
//AN_Xml:

EXPLAIN 执行计划支持 SELECTDELETEINSERTREPLACE 以及 UPDATE 语句。我们一般多用于分析 SELECT 查询语句,使用起来非常简单,语法如下:

//AN_Xml:
EXPLAIN SELECT 查询语句;
//AN_Xml:

我们简单来看下一条查询语句的执行计划:

//AN_Xml:

示例 1:单表查询(使用索引)

//AN_Xml:
-- 表结构:users(id, age, score, name, address),联合索引 idx_age_score_name(age, score, name)
//AN_Xml:mysql> EXPLAIN SELECT * FROM users WHERE age = 25;
//AN_Xml:+
//AN_Xml:
]]>
//AN_Xml:
//AN_Xml: //AN_Xml: NoSQL基础知识总结 //AN_Xml: https://javaguide.cn/database/nosql.html //AN_Xml: https://javaguide.cn/database/nosql.html //AN_Xml: NoSQL基础知识总结 //AN_Xml: NoSQL数据库基础知识总结,包括NoSQL与SQL的区别、NoSQL的优势、四种NoSQL数据库类型(键值、文档、图形、宽列)及其代表产品Redis、MongoDB、Neo4j等的应用场景。 //AN_Xml: 数据库 //AN_Xml: Thu, 12 Jan 2023 09:46:41 GMT //AN_Xml: NoSQL 是什么? //AN_Xml:

NoSQL(Not Only SQL 的缩写)泛指非关系型的数据库,主要针对的是键值、文档以及图形类型数据存储。并且,NoSQL 数据库天生支持分布式,数据冗余和数据分片等特性,旨在提供可扩展的高可用高性能数据存储解决方案。

//AN_Xml:

一个常见的误解是 NoSQL 数据库或非关系型数据库不能很好地存储关系型数据。NoSQL 数据库可以存储关系型数据—它们与关系型数据库的存储方式不同。

//AN_Xml:

NoSQL 数据库代表:HBase、Cassandra、MongoDB、Redis。

//AN_Xml:

//AN_Xml:

SQL 和 NoSQL 有什么区别?

//AN_Xml:

| | SQL 数据库 | NoSQL 数据库 |
//AN_Xml:| :

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: MongoDB常见面试题总结(上) //AN_Xml: https://javaguide.cn/database/mongodb/mongodb-questions-01.html //AN_Xml: https://javaguide.cn/database/mongodb/mongodb-questions-01.html //AN_Xml: MongoDB常见面试题总结(上) //AN_Xml: MongoDB常见面试题总结上篇,详解MongoDB基础概念、存储结构、数据类型、副本集高可用、分片集群水平扩展等核心知识点,助力后端面试准备。 //AN_Xml: 数据库 //AN_Xml: Thu, 12 Jan 2023 09:46:41 GMT //AN_Xml: //AN_Xml:

少部分内容参考了 MongoDB 官方文档的描述,在此说明一下。

//AN_Xml: //AN_Xml:

MongoDB 基础

//AN_Xml:

MongoDB 是什么?

//AN_Xml:

MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统,由 C++ 编写的。MongoDB 提供了 面向文档 的存储方式,操作起来比较简单和容易,支持“无模式”的数据建模,可以存储比较复杂的数据类型,是一款非常流行的 文档类型数据库

//AN_Xml:

在高负载的情况下,MongoDB 天然支持水平扩展和高可用,可以很方便地添加更多的节点/实例,以保证服务性能和可用性。在许多场景下,MongoDB 可以用于代替传统的关系型数据库或键/值存储方式,皆在为 Web 应用提供可扩展的高可用高性能数据存储解决方案。

//AN_Xml:

MongoDB 的存储结构是什么?

//AN_Xml:

MongoDB 的存储结构区别于传统的关系型数据库,主要由如下三个单元组成:

//AN_Xml:
    //AN_Xml:
  • 文档(Document):MongoDB 中最基本的单元,由 BSON 键值对(key-value)组成,类似于关系型数据库中的行(Row)。
  • //AN_Xml:
  • 集合(Collection):一个集合可以包含多个文档,类似于关系型数据库中的表(Table)。
  • //AN_Xml:
  • 数据库(Database):一个数据库中可以包含多个集合,可以在 MongoDB 中创建多个数据库,类似于关系型数据库中的数据库(Database)。
  • //AN_Xml:
//AN_Xml:

也就是说,MongoDB 将数据记录存储为文档 (更具体来说是BSON 文档),这些文档在集合中聚集在一起,数据库中存储一个或多个文档集合。

//AN_Xml:

SQL 与 MongoDB 常见术语对比

//AN_Xml:

| SQL | MongoDB |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: MongoDB常见面试题总结(下) //AN_Xml: https://javaguide.cn/database/mongodb/mongodb-questions-02.html //AN_Xml: https://javaguide.cn/database/mongodb/mongodb-questions-02.html //AN_Xml: MongoDB常见面试题总结(下) //AN_Xml: MongoDB常见面试题总结下篇,深入讲解MongoDB各类索引(单字段、复合、多键、文本、地理位置、TTL)的原理、使用场景和查询优化技巧。 //AN_Xml: 数据库 //AN_Xml: Thu, 12 Jan 2023 09:46:41 GMT //AN_Xml: MongoDB 索引 //AN_Xml:

MongoDB 索引有什么用?

//AN_Xml:

和关系型数据库类似,MongoDB 中也有索引。索引的目的主要是用来提高查询效率,如果没有索引的话,MongoDB 必须执行 集合扫描 ,即扫描集合中的每个文档,以选择与查询语句匹配的文档。如果查询存在合适的索引,MongoDB 可以使用该索引来限制它必须检查的文档数量。并且,MongoDB 可以使用索引中的排序返回排序后的结果。

//AN_Xml:

虽然索引可以显著缩短查询时间,但是使用索引、维护索引是有代价的。在执行写入操作时,除了要更新文档之外,还必须更新索引,这必然会影响写入的性能。因此,当有大量写操作而读操作少时,或者不考虑读操作的性能时,都不推荐建立索引。

//AN_Xml:

MongoDB 支持哪些类型的索引?

//AN_Xml:

MongoDB 支持多种类型的索引,包括单字段索引、复合索引、多键索引、哈希索引、文本索引、 地理位置索引等,每种类型的索引有不同的使用场合。

//AN_Xml:
    //AN_Xml:
  • 单字段索引: 建立在单个字段上的索引,索引创建的排序顺序无所谓,MongoDB 可以头/尾开始遍历。
  • //AN_Xml:
  • 复合索引: 建立在多个字段上的索引,也可以称之为组合索引、联合索引。
  • //AN_Xml:
  • 多键索引:MongoDB 的一个字段可能是数组,在对这种字段创建索引时,就是多键索引。MongoDB 会为数组的每个值创建索引。就是说你可以按照数组里面的值做条件来查询,这个时候依然会走索引。
  • //AN_Xml:
  • 哈希索引:按数据的哈希值索引,用在哈希分片集群上。
  • //AN_Xml:
  • 文本索引: 支持对字符串内容的文本搜索查询。文本索引可以包含任何值为字符串或字符串元素数组的字段。一个集合只能有一个文本搜索索引,但该索引可以覆盖多个字段。MongoDB 虽然支持全文索引,但是性能低下,暂时不建议使用。
  • //AN_Xml:
  • 地理位置索引: 基于经纬度的索引,适合 2D 和 3D 的位置查询。
  • //AN_Xml:
  • 唯一索引:确保索引字段不会存储重复值。如果集合已经存在了违反索引的唯一约束的文档,则后台创建唯一索引会失败。
  • //AN_Xml:
  • TTL 索引:TTL 索引提供了一个过期机制,允许为每一个文档设置一个过期时间,当一个文档达到预设的过期时间之后就会被删除。
  • //AN_Xml:
  • ……
  • //AN_Xml:
//AN_Xml:

复合索引中字段的顺序有影响吗?

//AN_Xml:

复合索引中字段的顺序非常重要,例如下图中的复合索引由{userid:1, score:-1}组成,则该复合索引首先按照userid升序排序;然后再每个userid的值内,再按照score降序排序。

//AN_Xml:

复合索引

//AN_Xml:

在复合索引中,按照何种方式排序,决定了该索引在查询中是否能被应用到。

//AN_Xml:

走复合索引的排序:

//AN_Xml:
db.s2.find().sort({"userid": 1, "score": -1})
//AN_Xml:db.s2.find().sort({"userid": -1, "score": 1})
//AN_Xml:

不走复合索引的排序:

//AN_Xml:
db.s2.find().sort({"userid": 1, "score": 1})
//AN_Xml:db.s2.find().sort({"userid": -1, "score": -1})
//AN_Xml:db.s2.find().sort({"score": 1, "userid": -1})
//AN_Xml:db.s2.find().sort({"score": 1, "userid": 1})
//AN_Xml:db.s2.find().sort({"score": -1, "userid": -1})
//AN_Xml:db.s2.find().sort({"score": -1, "userid": 1})
//AN_Xml:

我们可以通过 explain 进行分析:

//AN_Xml:
db.s2.find().sort({"score": -1, "userid": 1}).explain()
//AN_Xml:

复合索引遵循左前缀原则吗?

//AN_Xml:

MongoDB 的复合索引遵循左前缀原则:拥有多个键的索引,可以同时得到所有这些键的前缀组成的索引,但不包括除左前缀之外的其他子集。比如说,有一个类似 {a: 1, b: 1, c: 1, ..., z: 1} 这样的索引,那么实际上也等于有了 {a: 1}{a: 1, b: 1}{a: 1, b: 1, c: 1} 等一系列索引,但是不会有 {b: 1} 这样的非左前缀的索引。

//AN_Xml:

什么是 TTL 索引?

//AN_Xml:

TTL 索引提供了一个过期机制,允许为每一个文档设置一个过期时间 expireAfterSeconds ,当一个文档达到预设的过期时间之后就会被删除。TTL 索引除了有 expireAfterSeconds 属性外,和普通索引一样。

//AN_Xml:

数据过期对于某些类型的信息很有用,比如机器生成的事件数据、日志和会话信息,这些信息只需要在数据库中保存有限的时间。

//AN_Xml:

TTL 索引运行原理

//AN_Xml:
    //AN_Xml:
  • MongoDB 会开启一个后台线程读取该 TTL 索引的值来判断文档是否过期,但不会保证已过期的数据会立马被删除,因后台线程每 60 秒触发一次删除任务,且如果删除的数据量较大,会存在上一次的删除未完成,而下一次的任务已经开启的情况,导致过期的数据也会出现超过了数据保留时间 60 秒以上的现象。
  • //AN_Xml:
  • 对于副本集而言,TTL 索引的后台进程只会在 Primary 节点开启,在从节点会始终处于空闲状态,从节点的数据删除是由主库删除后产生的 oplog 来做同步。
  • //AN_Xml:
//AN_Xml:

TTL 索引限制

//AN_Xml:
    //AN_Xml:
  • TTL 索引是单字段索引。复合索引不支持 TTL
  • //AN_Xml:
  • _id字段不支持 TTL 索引。
  • //AN_Xml:
  • 无法在上限集合(Capped Collection)上创建 TTL 索引,因为 MongoDB 无法从上限集合中删除文档。
  • //AN_Xml:
  • 如果某个字段已经存在非 TTL 索引,那么在该字段上无法再创建 TTL 索引。
  • //AN_Xml:
//AN_Xml:

什么是覆盖索引查询?

//AN_Xml:

根据官方文档介绍,覆盖查询是以下的查询:

//AN_Xml:
    //AN_Xml:
  • 所有的查询字段是索引的一部分。
  • //AN_Xml:
  • 结果中返回的所有字段都在同一索引中。
  • //AN_Xml:
  • 查询中没有字段等于null
  • //AN_Xml:
//AN_Xml:

由于所有出现在查询中的字段是索引的一部分, MongoDB 无需在整个数据文档中检索匹配查询条件和返回使用相同索引的查询结果。因为索引存在于内存中,从索引中获取数据比通过扫描文档读取数据要快得多。

//AN_Xml:

举个例子:我们有如下 users 集合:

//AN_Xml:
{
//AN_Xml:   "_id": ObjectId("53402597d852426020000002"),
//AN_Xml:   "contact": "987654321",
//AN_Xml:   "dob": "01-01-1991",
//AN_Xml:   "gender": "M",
//AN_Xml:   "name": "Tom Benzamin",
//AN_Xml:   "user_name": "tombenzamin"
//AN_Xml:}
//AN_Xml:

我们在 users 集合中创建联合索引,字段为 genderuser_name :

//AN_Xml:
db.users.ensureIndex({gender:1,user_name:1})
//AN_Xml:

现在,该索引会覆盖以下查询:

//AN_Xml:
db.users.find({gender:"M"},{user_name:1,_id:0})
//AN_Xml:

为了让指定的索引覆盖查询,必须显式地指定 _id: 0 来从结果中排除 _id 字段,因为索引不包括 _id 字段。

//AN_Xml:

MongoDB 高可用

//AN_Xml:

复制集群

//AN_Xml:

什么是复制集群?

//AN_Xml:

MongoDB 的复制集群又称为副本集群,是一组维护相同数据集合的 mongod 进程。

//AN_Xml:

客户端连接到整个 Mongodb 复制集群,主节点机负责整个复制集群的写,从节点可以进行读操作,但默认还是主节点负责整个复制集群的读。主节点发生故障时,自动从从节点中选举出一个新的主节点,确保集群的正常使用,这对于客户端来说是无感知的。

//AN_Xml:

通常来说,一个复制集群包含 1 个主节点(Primary),多个从节点(Secondary)以及零个或 1 个仲裁节点(Arbiter)。

//AN_Xml:
    //AN_Xml:
  • 主节点:整个集群的写操作入口,接收所有的写操作,并将集合所有的变化记录到操作日志中,即 oplog。主节点挂掉之后会自动选出新的主节点。
  • //AN_Xml:
  • 从节点:从主节点同步数据,在主节点挂掉之后选举新节点。不过,从节点可以配置成 0 优先级,阻止它在选举中成为主节点。
  • //AN_Xml:
  • 仲裁节点:这个是为了节约资源或者多机房容灾用,只负责主节点选举时投票不存数据,保证能有节点获得多数赞成票。
  • //AN_Xml:
//AN_Xml:

下图是一个典型的三成员副本集群:

//AN_Xml:

//AN_Xml:

主节点与备节点之间是通过 oplog(操作日志) 来同步数据的。oplog 是 local 库下的一个特殊的 上限集合(Capped Collection) ,用来保存写操作所产生的增量日志,类似于 MySQL 中 的 Binlog。

//AN_Xml:
//AN_Xml:

上限集合类似于定长的循环队列,数据顺序追加到集合的尾部,当集合空间达到上限时,它会覆盖集合中最旧的文档。上限集合的数据将会被顺序写入到磁盘的固定空间内,所以,I/O 速度非常快,如果不建立索引,性能更好。

//AN_Xml:
//AN_Xml:

//AN_Xml:

当主节点上的一个写操作完成后,会向 oplog 集合写入一条对应的日志,而从节点则通过这个 oplog 不断拉取到新的日志,在本地进行回放以达到数据同步的目的。

//AN_Xml:

副本集最多有一个主节点。 如果当前主节点不可用,一个选举会抉择出新的主节点。MongoDB 的节点选举规则能够保证在 Primary 挂掉之后选取的新节点一定是集群中数据最全的一个。

//AN_Xml:

为什么要用复制集群?

//AN_Xml:
    //AN_Xml:
  • 实现 failover:提供自动故障恢复的功能,主节点发生故障时,自动从从节点中选举出一个新的主节点,确保集群的正常使用,这对于客户端来说是无感知的。
  • //AN_Xml:
  • 实现读写分离:我们可以设置从节点上可以读取数据,主节点负责写入数据,这样的话就实现了读写分离,减轻了主节点读写压力过大的问题。MongoDB 4.0 之前版本如果主库压力不大,不建议读写分离,因为写会阻塞读,除非业务对响应时间不是非常关注以及读取历史数据接受一定时间延迟。
  • //AN_Xml:
//AN_Xml:

分片集群

//AN_Xml:

什么是分片集群?

//AN_Xml:

分片集群是 MongoDB 的分布式版本,相较副本集,分片集群数据被均衡的分布在不同分片中, 不仅大幅提升了整个集群的数据容量上限,也将读写的压力分散到不同分片,以解决副本集性能瓶颈的难题。

//AN_Xml:

MongoDB 的分片集群由如下三个部分组成(下图来源于官方文档对分片集群的介绍):

//AN_Xml:

//AN_Xml:
    //AN_Xml:
  • Config Servers:配置服务器,本质上是一个 MongoDB 的副本集,负责存储集群的各种元数据和配置,如分片地址、Chunks 等
  • //AN_Xml:
  • Mongos:路由服务,不存具体数据,从 Config 获取集群配置讲请求转发到特定的分片,并且整合分片结果返回给客户端。
  • //AN_Xml:
  • Shard:每个分片是整体数据的一部分子集,从 MongoDB3.6 版本开始,每个 Shard 必须部署为副本集(replica set)架构
  • //AN_Xml:
//AN_Xml:

为什么要用分片集群?

//AN_Xml:

随着系统数据量以及吞吐量的增长,常见的解决办法有两种:垂直扩展和水平扩展。

//AN_Xml:

垂直扩展通过增加单个服务器的能力来实现,比如磁盘空间、内存容量、CPU 数量等;水平扩展则通过将数据存储到多个服务器上来实现,根据需要添加额外的服务器以增加容量。

//AN_Xml:

类似于 Redis Cluster,MongoDB 也可以通过分片实现 水平扩展 。水平扩展这种方式更灵活,可以满足更大数据量的存储需求,支持更高吞吐量。并且,水平扩展所需的整体成本更低,仅仅需要相对较低配置的单机服务器即可,代价是增加了部署的基础设施和维护的复杂性。

//AN_Xml:

也就是说当你遇到如下问题时,可以使用分片集群解决:

//AN_Xml:
    //AN_Xml:
  • 存储容量受单机限制,即磁盘资源遭遇瓶颈。
  • //AN_Xml:
  • 读写能力受单机限制,可能是 CPU、内存或者网卡等资源遭遇瓶颈,导致读写能力无法扩展。
  • //AN_Xml:
//AN_Xml:

什么是分片键?

//AN_Xml:

分片键(Shard Key) 是数据分区的前提, 从而实现数据分发到不同服务器上,减轻服务器的负担。也就是说,分片键决定了集合内的文档如何在集群的多个分片间的分布状况。

//AN_Xml:

分片键就是文档里面的一个字段,但是这个字段不是普通的字段,有一定的要求:

//AN_Xml:
    //AN_Xml:
  • 它必须在所有文档中都出现。
  • //AN_Xml:
  • 它必须是集合的一个索引,可以是单索引或复合索引的前缀索引,不能是多索引、文本索引或地理空间位置索引。
  • //AN_Xml:
  • MongoDB 4.2 之前的版本,文档的分片键字段值不可变。MongoDB 4.2 版本开始,除非分片键字段是不可变的 _id 字段,否则您可以更新文档的分片键值。MongoDB 5.0 版本开始,实现了实时重新分片(live resharding),可以实现分片键的完全重新选择。
  • //AN_Xml:
  • 它的大小不能超过 512 字节。
  • //AN_Xml:
//AN_Xml:

如何选择分片键?

//AN_Xml:

选择合适的片键对 sharding 效率影响很大,主要基于如下四个因素(摘自分片集群使用注意事项 - - 腾讯云文档):

//AN_Xml:
    //AN_Xml:
  • 取值基数 取值基数建议尽可能大,如果用小基数的片键,因为备选值有限,那么块的总数量就有限,随着数据增多,块的大小会越来越大,导致水平扩展时移动块会非常困难。 例如:选择年龄做一个基数,范围最多只有 100 个,随着数据量增多,同一个值分布过多时,导致 chunck 的增长超出 chuncksize 的范围,引起 jumbo chunk,从而无法迁移,导致数据分布不均匀,性能瓶颈。
  • //AN_Xml:
  • 取值分布 取值分布建议尽量均匀,分布不均匀的片键会造成某些块的数据量非常大,同样有上面数据分布不均匀,性能瓶颈的问题。
  • //AN_Xml:
  • 查询带分片 查询时建议带上分片,使用分片键进行条件查询时,mongos 可以直接定位到具体分片,否则 mongos 需要将查询分发到所有分片,再等待响应返回。
  • //AN_Xml:
  • 避免单调递增或递减 单调递增的 sharding key,数据文件挪动小,但写入会集中,导致最后一篇的数据量持续增大,不断发生迁移,递减同理。
  • //AN_Xml:
//AN_Xml:

综上,在选择片键时要考虑以上 4 个条件,尽可能满足更多的条件,才能降低 MoveChunks 对性能的影响,从而获得最优的性能体验。

//AN_Xml:

分片策略有哪些?

//AN_Xml:

MongoDB 支持两种分片算法来满足不同的查询需求(摘自MongoDB 分片集群介绍 - 阿里云文档):

//AN_Xml:

1、基于范围的分片

//AN_Xml:

//AN_Xml:

MongoDB 按照分片键(Shard Key)的值的范围将数据拆分为不同的块(Chunk),每个块包含了一段范围内的数据。当分片键的基数大、频率低且值非单调变更时,范围分片更高效。

//AN_Xml:
    //AN_Xml:
  • 优点:Mongos 可以快速定位请求需要的数据,并将请求转发到相应的 Shard 节点中。
  • //AN_Xml:
  • 缺点:可能导致数据在 Shard 节点上分布不均衡,容易造成读写热点,且不具备写分散性。
  • //AN_Xml:
  • 适用场景:分片键的值不是单调递增或单调递减、分片键的值基数大且重复的频率低、需要范围查询等业务场景。
  • //AN_Xml:
//AN_Xml:

2、基于 Hash 值的分片

//AN_Xml:

//AN_Xml:

MongoDB 计算单个字段的哈希值作为索引值,并以哈希值的范围将数据拆分为不同的块(Chunk)。

//AN_Xml:
    //AN_Xml:
  • 优点:可以将数据更加均衡地分布在各 Shard 节点中,具备写分散性。
  • //AN_Xml:
  • 缺点:不适合进行范围查询,进行范围查询时,需要将读请求分发到所有的 Shard 节点。
  • //AN_Xml:
  • 适用场景:分片键的值存在单调递增或递减、片键的值基数大且重复的频率低、需要写入的数据随机分发、数据读取随机性较大等业务场景。
  • //AN_Xml:
//AN_Xml:

除了上述两种分片策略,您还可以配置 复合片键 ,例如由一个低基数的键和一个单调递增的键组成。

//AN_Xml:

分片数据如何存储?

//AN_Xml:

Chunk(块) 是 MongoDB 分片集群的一个核心概念,其本质上就是由一组 Document 组成的逻辑数据单元。每个 Chunk 包含一定范围片键的数据,互不相交且并集为全部数据,即离散数学中划分的概念。

//AN_Xml:

分片集群不会记录每条数据在哪个分片上,而是记录 Chunk 在哪个分片上一级这个 Chunk 包含哪些数据。

//AN_Xml:

默认情况下,一个 Chunk 的最大值默认为 64MB(可调整,取值范围为 1~1024 MB。如无特殊需求,建议保持默认值),进行数据插入、更新、删除时,如果此时 Mongos 感知到了目标 Chunk 的大小或者其中的数据量超过上限,则会触发 Chunk 分裂

//AN_Xml:

Chunk 分裂

//AN_Xml:

数据的增长会让 Chunk 分裂得越来越多。这个时候,各个分片上的 Chunk 数量可能会不平衡。Mongos 中的 均衡器(Balancer) 组件就会执行自动平衡,尝试使各个 Shard 上 Chunk 的数量保持均衡,这个过程就是 再平衡(Rebalance)。默认情况下,数据库和集合的 Rebalance 是开启的。

//AN_Xml:

如下图所示,随着数据插入,导致 Chunk 分裂,让 AB 两个分片有 3 个 Chunk,C 分片只有一个,这个时候就会把 B 分配的迁移一个到 C 分片实现集群数据均衡。

//AN_Xml:

Chunk 迁移

//AN_Xml:
//AN_Xml:

Balancer 是 MongoDB 的一个运行在 Config Server 的 Primary 节点上(自 MongoDB 3.4 版本起)的后台进程,它监控每个分片上 Chunk 数量,并在某个分片上 Chunk 数量达到阈值进行迁移。

//AN_Xml:
//AN_Xml:

Chunk 只会分裂,不会合并,即使 chunkSize 的值变大。

//AN_Xml:

Rebalance 操作是比较耗费系统资源的,我们可以通过在业务低峰期执行、预分片或者设置 Rebalance 时间窗等方式来减少其对 MongoDB 正常使用所带来的影响。

//AN_Xml:

Chunk 迁移原理是什么?

//AN_Xml:

关于 Chunk 迁移原理的详细介绍,推荐阅读 MongoDB 中文社区的一文读懂 MongoDB chunk 迁移这篇文章。

//AN_Xml:

学习资料推荐

//AN_Xml: //AN_Xml:

参考

//AN_Xml: //AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 软件工程简明教程 //AN_Xml: https://javaguide.cn/system-design/basis/software-engineering.html //AN_Xml: https://javaguide.cn/system-design/basis/software-engineering.html //AN_Xml: 软件工程简明教程 //AN_Xml: 软件工程基础知识详解,涵盖软件危机、软件开发过程模型、瀑布模型、敏捷开发等软件工程核心概念。 //AN_Xml: 系统设计 //AN_Xml: Fri, 30 Dec 2022 05:45:11 GMT //AN_Xml: 大部分软件开发从业者,都会忽略软件开发中的一些最基础、最底层的一些概念。但是,这些软件开发的概念对于软件开发来说非常重要,就像是软件开发的基石一样。这也是我写这篇文章的原因。

//AN_Xml:

何为软件工程?

//AN_Xml:

1968 年 NATO(北大西洋公约组织)提出了软件危机Software crisis)一词。同年,为了解决软件危机问题,“软件工程”的概念诞生了。一门叫做软件工程的学科也就应运而生。

//AN_Xml:

随着时间的推移,软件工程这门学科也经历了一轮又一轮的完善,其中的一些核心内容比如软件开发模型越来越丰富实用!

//AN_Xml:

什么是软件危机呢?

//AN_Xml:

简单来说,软件危机描述了当时软件开发的一个痛点:我们很难高效地开发出质量高的软件。

//AN_Xml:

Dijkstra(Dijkstra 算法的作者) 在 1972 年图灵奖获奖感言中也提到过软件危机,他是这样说的:“导致软件危机的主要原因是机器变得功能强大了几个数量级!坦率地说:只要没有机器,编程就完全没有问题。当我们有一些弱小的计算机时,编程成为一个温和的问题,而现在我们有了庞大的计算机,编程也同样成为一个巨大的问题”。

//AN_Xml:

说了这么多,到底什么是软件工程呢?

//AN_Xml:

工程是为了解决实际的问题将理论应用于实践。软件工程指的就是将工程思想应用于软件开发。

//AN_Xml:

上面是我对软件工程的定义,我们再来看看比较权威的定义。IEEE 软件工程汇刊给出的定义是这样的: (1)将系统化的、规范的、可量化的方法应用到软件的开发、运行及维护中,即将工程化方法应用于软件。 (2)在(1)中所述方法的研究。

//AN_Xml:

总之,软件工程的终极目标就是:在更少资源消耗的情况下,创造出更好、更容易维护的软件。

//AN_Xml:

软件开发过程

//AN_Xml:

维基百科是这样定义软件开发过程的:

//AN_Xml:
//AN_Xml:

软件开发过程(英语:software development process),或软件过程(英语:software process),是软件开发的开发生命周期(software development life cycle),其各个阶段实现了软件的需求定义与分析、设计、实现、测试、交付和维护。软件过程是在开发与构建系统时应遵循的步骤,是软件开发的路线图。

//AN_Xml:
//AN_Xml:
    //AN_Xml:
  • 需求分析:分析用户的需求,建立逻辑模型。
  • //AN_Xml:
  • 软件设计:根据需求分析的结果对软件架构进行设计。
  • //AN_Xml:
  • 编码:编写程序运行的源代码。
  • //AN_Xml:
  • 测试 : 确定测试用例,编写测试报告。
  • //AN_Xml:
  • 交付:将做好的软件交付给客户。
  • //AN_Xml:
  • 维护:对软件进行维护比如解决 bug,完善功能。
  • //AN_Xml:
//AN_Xml:

软件开发过程只是比较笼统的层面上,定义了一个软件开发可能涉及到的一些流程。

//AN_Xml:

软件开发模型更具体地定义了软件开发过程,对开发过程提供了强有力的理论支持。

//AN_Xml:

软件开发模型

//AN_Xml:

软件开发模型有很多种,比如瀑布模型(Waterfall Model)、快速原型模型(Rapid Prototype Model)、V 模型(V-model)、W 模型(W-model)、敏捷开发模型。其中最具有代表性的还是 瀑布模型敏捷开发

//AN_Xml:

瀑布模型 定义了一套完整的软件开发周期,完整地展示了一个软件的生命周期。

//AN_Xml:

//AN_Xml:

敏捷开发模型 是目前使用的最多的一种软件开发模型。MBA 智库百科对敏捷开发的描述是这样的:

//AN_Xml:
//AN_Xml:

敏捷开发 是一种以人为核心、迭代、循序渐进的开发方法。在敏捷开发中,软件项目的构建被切分成多个子项目,各个子项目的成果都经过测试,具备集成和可运行的特征。换言之,就是把一个大项目分为多个相互联系,但也可独立运行的小项目,并分别完成,在此过程中软件一直处于可使用状态。

//AN_Xml:
//AN_Xml:

像现在比较常见的一些概念比如 持续集成重构小版本发布低文档站会结对编程测试驱动开发 都是敏捷开发的核心。

//AN_Xml:

软件开发的基本策略

//AN_Xml:

软件复用

//AN_Xml:

我们在构建一个新的软件的时候,不需要从零开始,通过复用已有的一些轮子(框架、第三方库等)、设计模式、设计原则等等现成的物料,我们可以更快地构建出一个满足要求的软件。

//AN_Xml:

像我们平时接触的开源项目就是最好的例子。我想,如果不是开源,我们构建出一个满足要求的软件,耗费的精力和时间要比现在多的多!

//AN_Xml:

分而治之

//AN_Xml:

构建软件的过程中,我们会遇到很多问题。我们可以将一些比较复杂的问题拆解为一些小问题,然后,一一攻克。

//AN_Xml:

我结合现在比较火的软件设计方法—领域驱动设计(Domain Driven Design,简称 DDD)来说说。

//AN_Xml:

在领域驱动设计中,很重要的一个概念就是领域(Domain),它就是我们要解决的问题。在领域驱动设计中,我们要做的就是把比较大的领域(问题)拆解为若干的小领域(子域)。

//AN_Xml:

除此之外,分而治之也是一个比较常用的算法思想,对应的就是分治算法。如果你想了解分治算法的话,推荐你看一下北大的《算法设计与分析 Design and Analysis of Algorithms》

//AN_Xml:

逐步演进

//AN_Xml:

软件开发是一个逐步演进的过程,我们需要不断进行迭代式增量开发,最终交付符合客户价值的产品。

//AN_Xml:

这里补充一个在软件开发领域,非常重要的概念:MVP(Minimum Viable Product,最小可行产品)。

//AN_Xml:

这个最小可行产品,可以理解为刚好能够满足客户需求的产品。下面这张图片把这个思想展示的非常精髓。

//AN_Xml:

//AN_Xml:

利用最小可行产品,我们可以也可以提早进行市场分析,这对于我们在探索产品不确定性的道路上非常有帮助。可以非常有效地指导我们下一步该往哪里走。

//AN_Xml:

优化折中

//AN_Xml:

软件开发是一个不断优化改进的过程。任何软件都有很多可以优化的点,不可能完美。我们需要不断改进和提升软件的质量。

//AN_Xml:

但是,也不要陷入这个怪圈。要学会折中,在有限的投入内,以最有效的方式提高现有软件的质量。

//AN_Xml:

参考

//AN_Xml: //AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Maven核心概念总结 //AN_Xml: https://javaguide.cn/tools/maven/maven-core-concepts.html //AN_Xml: https://javaguide.cn/tools/maven/maven-core-concepts.html //AN_Xml: Maven核心概念总结 //AN_Xml: Apache Maven 的本质是一个软件项目管理和理解工具。基于项目对象模型 (Project Object Model,POM) 的概念,Maven 可以从一条中心信息管理项目的构建、报告和文档。 //AN_Xml: 开发工具 //AN_Xml: Fri, 16 Dec 2022 14:32:28 GMT //AN_Xml: //AN_Xml:

这部分内容主要根据 Maven 官方文档整理,做了对应的删减,主要保留比较重要的部分,不涉及实战,主要是一些重要概念的介绍。

//AN_Xml: //AN_Xml:

Maven 介绍

//AN_Xml:

Maven 官方文档是这样介绍的 Maven 的:

//AN_Xml:
//AN_Xml:

Apache Maven is a software project management and comprehension tool. Based on the concept of a project object model (POM), Maven can manage a project's build, reporting and documentation from a central piece of information.

//AN_Xml:

Apache Maven 的本质是一个软件项目管理和理解工具。基于项目对象模型 (Project Object Model,POM) 的概念,Maven 可以从一条中心信息管理项目的构建、报告和文档。

//AN_Xml:
//AN_Xml:

什么是 POM? 每一个 Maven 工程都有一个 pom.xml 文件,位于根目录中,包含项目构建生命周期的详细信息。通过 pom.xml 文件,我们可以定义项目的坐标、项目依赖、项目信息、插件信息等等配置。

//AN_Xml:

对于开发者来说,Maven 的主要作用主要有 3 个:

//AN_Xml:
    //AN_Xml:
  1. 项目构建:提供标准的、跨平台的自动化项目构建方式。
  2. //AN_Xml:
  3. 依赖管理:方便快捷的管理项目依赖的资源(jar 包),避免资源间的版本冲突问题。
  4. //AN_Xml:
  5. 统一开发结构:提供标准的、统一的项目结构。
  6. //AN_Xml:
//AN_Xml:

关于 Maven 的基本使用这里就不介绍了,建议看看官网的 5 分钟上手 Maven 的教程:Maven in 5 Minutes

//AN_Xml:

Maven 坐标

//AN_Xml:

项目中依赖的第三方库以及插件可统称为构件。每一个构件都可以使用 Maven 坐标唯一标识,坐标元素包括:

//AN_Xml:
    //AN_Xml:
  • groupId(必须): 定义了当前 Maven 项目隶属的组织或公司。groupId 一般分为多段,通常情况下,第一段为域,第二段为公司名称。域又分为 org、com、cn 等,其中 org 为非营利组织,com 为商业组织,cn 表示中国。以 apache 开源社区的 tomcat 项目为例,这个项目的 groupId 是 org.apache,它的域是 org(因为 tomcat 是非营利项目),公司名称是 apache,artifactId 是 tomcat。
  • //AN_Xml:
  • artifactId(必须):定义了当前 Maven 项目的名称,项目的唯一的标识符,对应项目根目录的名称。
  • //AN_Xml:
  • version(必须):定义了 Maven 项目当前所处版本。
  • //AN_Xml:
  • packaging(可选):定义了 Maven 项目的打包方式(比如 jar,war...),默认使用 jar。
  • //AN_Xml:
  • classifier(可选):常用于区分从同一 POM 构建的具有不同内容的构件,可以是任意的字符串,附加在版本号之后。
  • //AN_Xml:
//AN_Xml:

只要你提供正确的坐标,就能从 Maven 仓库中找到相应的构件供我们使用。

//AN_Xml:

举个例子(引入阿里巴巴开源的 EasyExcel):

//AN_Xml:
<dependency>
//AN_Xml:    <groupId>com.alibaba</groupId>
//AN_Xml:    <artifactId>easyexcel</artifactId>
//AN_Xml:    <version>3.1.1</version>
//AN_Xml:</dependency>
//AN_Xml:

你可以在 https://mvnrepository.com/ 这个网站上找到几乎所有可用的构件,如果你的项目使用的是 Maven 作为构建工具,那这个网站你一定会经常接触。

//AN_Xml:

Maven 仓库

//AN_Xml:

Maven 依赖

//AN_Xml:

如果使用 Maven 构建产生的构件(例如 Jar 文件)被其他的项目引用,那么该构件就是其他项目的依赖。

//AN_Xml:

依赖配置

//AN_Xml:

配置信息示例

//AN_Xml:
<project>
//AN_Xml:    <dependencies>
//AN_Xml:        <dependency>
//AN_Xml:            <groupId></groupId>
//AN_Xml:            <artifactId></artifactId>
//AN_Xml:            <version></version>
//AN_Xml:            <type>...</type>
//AN_Xml:            <scope>...</scope>
//AN_Xml:            <optional>...</optional>
//AN_Xml:            <exclusions>
//AN_Xml:                <exclusion>
//AN_Xml:                  <groupId>...</groupId>
//AN_Xml:                  <artifactId>...</artifactId>
//AN_Xml:                </exclusion>
//AN_Xml:          </exclusions>
//AN_Xml:        </dependency>
//AN_Xml:      </dependencies>
//AN_Xml:</project>
//AN_Xml:

配置说明

//AN_Xml:
    //AN_Xml:
  • dependencies:一个 pom.xml 文件中只能存在一个这样的标签,是用来管理依赖的总标签。
  • //AN_Xml:
  • dependency:包含在 dependencies 标签中,可以有多个,每一个表示项目的一个依赖。
  • //AN_Xml:
  • groupId,artifactId,version(必要):依赖的基本坐标,对于任何一个依赖来说,基本坐标是最重要的,Maven 根据坐标才能找到需要的依赖。我们在上面解释过这些元素的具体意思,这里就不重复提了。
  • //AN_Xml:
  • type(可选):依赖的类型,对应于项目坐标定义的 packaging。大部分情况下,该元素不必声明,其默认值是 jar。
  • //AN_Xml:
  • scope(可选):依赖的范围,默认值是 compile。
  • //AN_Xml:
  • optional(可选):标记依赖是否可选
  • //AN_Xml:
  • exclusions(可选):用来排除传递性依赖,例如 jar 包冲突
  • //AN_Xml:
//AN_Xml:

依赖范围

//AN_Xml:

classpath 用于指定 .class 文件存放的位置,类加载器会从该路径中加载所需的 .class 文件到内存中。

//AN_Xml:

Maven 在编译、执行测试、实际运行有着三套不同的 classpath:

//AN_Xml:
    //AN_Xml:
  • 编译 classpath:编译主代码有效
  • //AN_Xml:
  • 测试 classpath:编译、运行测试代码有效
  • //AN_Xml:
  • 运行 classpath:项目运行时有效
  • //AN_Xml:
//AN_Xml:

Maven 的依赖范围如下:

//AN_Xml:
    //AN_Xml:
  • compile:编译依赖范围(默认),使用此依赖范围对于编译、测试、运行三种都有效,即在编译、测试和运行的时候都要使用该依赖 Jar 包。
  • //AN_Xml:
  • test:测试依赖范围,从字面意思就可以知道此依赖范围只能用于测试,而在编译和运行项目时无法使用此类依赖,典型的是 JUnit,它只用于编译测试代码和运行测试代码的时候才需要。
  • //AN_Xml:
  • provided:此依赖范围,对于编译和测试有效,而对运行时无效。比如 servlet-api.jar 在 Tomcat 中已经提供了,我们只需要的是编译期提供而已。
  • //AN_Xml:
  • runtime:运行时依赖范围,对于测试和运行有效,但是在编译主代码时无效,典型的就是 JDBC 驱动实现。
  • //AN_Xml:
  • system:系统依赖范围,使用 system 范围的依赖时必须通过 systemPath 元素显示地指定依赖文件的路径,不依赖 Maven 仓库解析,所以可能会造成建构的不可移植。
  • //AN_Xml:
//AN_Xml:

传递依赖性

//AN_Xml:

依赖冲突

//AN_Xml:

1、对于 Maven 而言,同一个 groupId 同一个 artifactId 下,只能使用一个 version。

//AN_Xml:
<dependency>
//AN_Xml:    <groupId>in.hocg.boot</groupId>
//AN_Xml:    <artifactId>mybatis-plus-spring-boot-starter</artifactId>
//AN_Xml:    <version>1.0.48</version>
//AN_Xml:</dependency>
//AN_Xml:<!-- 只会使用 1.0.49 这个版本的依赖 -->
//AN_Xml:<dependency>
//AN_Xml:    <groupId>in.hocg.boot</groupId>
//AN_Xml:    <artifactId>mybatis-plus-spring-boot-starter</artifactId>
//AN_Xml:    <version>1.0.49</version>
//AN_Xml:</dependency>
//AN_Xml:

若相同类型但版本不同的依赖存在于同一个 pom 文件,只会引入后一个声明的依赖。

//AN_Xml:

2、项目的两个依赖同时引入了某个依赖。

//AN_Xml:

举个例子,项目存在下面这样的依赖关系:

//AN_Xml:
依赖链路一:A -> B -> C -> X(1.0)
//AN_Xml:依赖链路二:A -> D -> X(2.0)
//AN_Xml:

这两条依赖路径上有两个版本的 X,为了避免依赖重复,Maven 只会选择其中的一个进行解析。

//AN_Xml:

哪个版本的 X 会被 Maven 解析使用呢?

//AN_Xml:

Maven 在遇到这种问题的时候,会遵循 路径最短优先声明顺序优先 两大原则。解决这个问题的过程也被称为 Maven 依赖调解

//AN_Xml:

路径最短优先

//AN_Xml:
依赖链路一:A -> B -> C -> X(1.0) // dist = 3
//AN_Xml:依赖链路二:A -> D -> X(2.0) // dist = 2
//AN_Xml:

依赖链路二的路径最短,因此,X(2.0)会被解析使用。

//AN_Xml:

不过,你也可以发现。路径最短优先原则并不是通用的,像下面这种路径长度相等的情况就不能单单通过其解决了:

//AN_Xml:
依赖链路一:A -> B -> X(1.0) // dist = 2
//AN_Xml:依赖链路二:A -> D -> X(2.0) // dist = 2
//AN_Xml:

因此,Maven 又定义了声明顺序优先原则。

//AN_Xml:

依赖调解第一原则不能解决所有问题,比如这样的依赖关系:A->B->Y(1.0)、A-> C->Y(2.0),Y(1.0)和 Y(2.0)的依赖路径长度是一样的,都为 2。Maven 定义了依赖调解的第二原则:

//AN_Xml:

声明顺序优先

//AN_Xml:

在依赖路径长度相等的前提下,在 pom.xml 中依赖声明的顺序决定了谁会被解析使用,顺序最前的那个依赖优胜。该例中,如果 B 的依赖声明在 D 之前,那么 X (1.0)就会被解析使用。

//AN_Xml:
<!-- A pom.xml -->
//AN_Xml:<dependencies>
//AN_Xml:    ...
//AN_Xml:    dependency B
//AN_Xml:    ...
//AN_Xml:    dependency D
//AN_Xml:</dependencies>
//AN_Xml:

排除依赖

//AN_Xml:

单纯依赖 Maven 来进行依赖调解,在很多情况下是不适用的,需要我们手动排除依赖。

//AN_Xml:

举个例子,当前项目存在下面这样的依赖关系:

//AN_Xml:
依赖链路一:A -> B -> C -> X(1.5) // dist = 3
//AN_Xml:依赖链路二:A -> D -> X(1.0) // dist = 2
//AN_Xml:

根据路径最短优先原则,X(1.0) 会被解析使用,也就是说实际用的是 1.0 版本的 X。

//AN_Xml:

但是!!!这会一些问题:如果 C 依赖用到了 1.5 版本的 X 中才有的一个类,运行项目就会报NoClassDefFoundError错误。如果 C 依赖用到了 1.5 版本的 X 中才有的一个方法,运行项目就会报NoSuchMethodError错误。

//AN_Xml:

现在知道为什么你的 Maven 项目总是会报NoClassDefFoundErrorNoSuchMethodError错误了吧?

//AN_Xml:

如何解决呢? 我们可以通过exclusion标签手动将 X(1.0) 给排除。

//AN_Xml:
<dependency>
//AN_Xml:    ......
//AN_Xml:    <exclusions>
//AN_Xml:      <exclusion>
//AN_Xml:        <artifactId>x</artifactId>
//AN_Xml:        <groupId>org.apache.x</groupId>
//AN_Xml:      </exclusion>
//AN_Xml:    </exclusions>
//AN_Xml:</dependency>
//AN_Xml:

一般我们在解决依赖冲突的时候,都会优先保留版本较高的。这是因为大部分 jar 在升级的时候都会做到向下兼容。

//AN_Xml:

如果高版本修改了低版本的一些类或者方法的话,这个时候就不能直接保留高版本了,而是应该考虑优化上层依赖,比如升级上层依赖的版本。

//AN_Xml:

还是上面的例子:

//AN_Xml:
依赖链路一:A -> B -> C -> X(1.5) // dist = 3
//AN_Xml:依赖链路二:A -> D -> X(1.0) // dist = 2
//AN_Xml:

我们保留了 1.5 版本的 X,但是这个版本的 X 删除了 1.0 版本中的某些类。这个时候,我们可以考虑升级 D 的版本到一个 X 兼容的版本。

//AN_Xml:

Maven 仓库

//AN_Xml:

在 Maven 世界中,任何一个依赖、插件或者项目构建的输出,都可以称为 构件

//AN_Xml:

坐标和依赖是构件在 Maven 世界中的逻辑表示方式,构件的物理表示方式是文件,Maven 通过仓库来统一管理这些文件。 任何一个构件都有一组坐标唯一标识。有了仓库之后,无需手动引入构件,我们直接给定构件的坐标即可在 Maven 仓库中找到该构件。

//AN_Xml:

Maven 仓库分为:

//AN_Xml:
    //AN_Xml:
  • 本地仓库:运行 Maven 的计算机上的一个目录,它缓存远程下载的构件并包含尚未发布的临时构件。settings.xml 文件中可以看到 Maven 的本地仓库路径配置,默认本地仓库路径是在 ${user.home}/.m2/repository
  • //AN_Xml:
  • 远程仓库:官方或者其他组织维护的 Maven 仓库。
  • //AN_Xml:
//AN_Xml:

Maven 远程仓库可以分为:

//AN_Xml:
    //AN_Xml:
  • 中央仓库:这个仓库是由 Maven 社区来维护的,里面存放了绝大多数开源软件的包,并且是作为 Maven 的默认配置,不需要开发者额外配置。另外为了方便查询,还提供了一个查询地址,开发者可以通过这个地址更快的搜索需要构件的坐标。
  • //AN_Xml:
  • 私服:私服是一种特殊的远程 Maven 仓库,它是架设在局域网内的仓库服务,私服一般被配置为互联网远程仓库的镜像,供局域网内的 Maven 用户使用。
  • //AN_Xml:
  • 其他的公共仓库:有一些公共仓库是为了加速访问(比如阿里云 Maven 镜像仓库)或者部分构件不存在于中央仓库中。
  • //AN_Xml:
//AN_Xml:

Maven 依赖包寻找顺序:

//AN_Xml:
    //AN_Xml:
  1. 先去本地仓库找寻,有的话,直接使用。
  2. //AN_Xml:
  3. 本地仓库没有找到的话,会去远程仓库找寻,下载包到本地仓库。
  4. //AN_Xml:
  5. 远程仓库没有找到的话,会报错。
  6. //AN_Xml:
//AN_Xml:

Maven 生命周期

//AN_Xml:

Maven 的生命周期就是为了对所有的构建过程进行抽象和统一,包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤。

//AN_Xml:

Maven 定义了 3 个生命周期META-INF/plexus/components.xml

//AN_Xml:
    //AN_Xml:
  • default 生命周期
  • //AN_Xml:
  • clean生命周期
  • //AN_Xml:
  • site生命周期
  • //AN_Xml:
//AN_Xml:

这些生命周期是相互独立的,每个生命周期包含多个阶段(phase)。并且,这些阶段是有序的,也就是说,后面的阶段依赖于前面的阶段。当执行某个阶段的时候,会先执行它前面的阶段。

//AN_Xml:

执行 Maven 生命周期的命令格式如下:

//AN_Xml:
mvn 阶段 [阶段2] ...[阶段n]
//AN_Xml:

default 生命周期

//AN_Xml:

default生命周期是在没有任何关联插件的情况下定义的,是 Maven 的主要生命周期,用于构建应用程序,共包含 23 个阶段。

//AN_Xml:
<phases>
//AN_Xml:  <!-- 验证项目是否正确,并且所有必要的信息可用于完成构建过程 -->
//AN_Xml:  <phase>validate</phase>
//AN_Xml:  <!-- 建立初始化状态,例如设置属性 -->
//AN_Xml:  <phase>initialize</phase>
//AN_Xml:  <!-- 生成要包含在编译阶段的源代码 -->
//AN_Xml:  <phase>generate-sources</phase>
//AN_Xml:  <!-- 处理源代码 -->
//AN_Xml:  <phase>process-sources</phase>
//AN_Xml:  <!-- 生成要包含在包中的资源 -->
//AN_Xml:  <phase>generate-resources</phase>
//AN_Xml:  <!-- 将资源复制并处理到目标目录中,为打包阶段做好准备。 -->
//AN_Xml:  <phase>process-resources</phase>
//AN_Xml:  <!-- 编译项目的源代码  -->
//AN_Xml:  <phase>compile</phase>
//AN_Xml:  <!-- 对编译生成的文件进行后处理,例如对 Java 类进行字节码增强/优化 -->
//AN_Xml:  <phase>process-classes</phase>
//AN_Xml:  <!-- 生成要包含在编译阶段的任何测试源代码 -->
//AN_Xml:  <phase>generate-test-sources</phase>
//AN_Xml:  <!-- 处理测试源代码 -->
//AN_Xml:  <phase>process-test-sources</phase>
//AN_Xml:  <!-- 生成要包含在编译阶段的测试源代码 -->
//AN_Xml:  <phase>generate-test-resources</phase>
//AN_Xml:  <!-- 处理从测试代码文件编译生成的文件 -->
//AN_Xml:  <phase>process-test-resources</phase>
//AN_Xml:  <!-- 编译测试源代码 -->
//AN_Xml:  <phase>test-compile</phase>
//AN_Xml:  <!-- 处理从测试代码文件编译生成的文件 -->
//AN_Xml:  <phase>process-test-classes</phase>
//AN_Xml:  <!-- 使用合适的单元测试框架(Junit 就是其中之一)运行测试 -->
//AN_Xml:  <phase>test</phase>
//AN_Xml:  <!-- 在实际打包之前,执行任何的必要的操作为打包做准备 -->
//AN_Xml:  <phase>prepare-package</phase>
//AN_Xml:  <!-- 获取已编译的代码并将其打包成可分发的格式,例如 JAR、WAR 或 EAR 文件 -->
//AN_Xml:  <phase>package</phase>
//AN_Xml:  <!-- 在执行集成测试之前执行所需的操作。 例如,设置所需的环境 -->
//AN_Xml:  <phase>pre-integration-test</phase>
//AN_Xml:  <!-- 处理并在必要时部署软件包到集成测试可以运行的环境 -->
//AN_Xml:  <phase>integration-test</phase>
//AN_Xml:  <!-- 执行集成测试后执行所需的操作。 例如,清理环境  -->
//AN_Xml:  <phase>post-integration-test</phase>
//AN_Xml:  <!-- 运行任何检查以验证打的包是否有效并符合质量标准。 -->
//AN_Xml:  <phase>verify</phase>
//AN_Xml:  <!-- 	将包安装到本地仓库中,可以作为本地其他项目的依赖 -->
//AN_Xml:  <phase>install</phase>
//AN_Xml:  <!-- 将最终的项目包复制到远程仓库中与其他开发者和项目共享 -->
//AN_Xml:  <phase>deploy</phase>
//AN_Xml:</phases>
//AN_Xml:

根据前面提到的阶段间依赖关系理论,当我们执行 mvn test命令的时候,会执行从 validate 到 test 的所有阶段,这也就解释了为什么执行测试的时候,项目的代码能够自动编译。

//AN_Xml:

clean 生命周期

//AN_Xml:

clean 生命周期的目的是清理项目,共包含 3 个阶段:

//AN_Xml:
    //AN_Xml:
  1. pre-clean
  2. //AN_Xml:
  3. clean
  4. //AN_Xml:
  5. post-clean
  6. //AN_Xml:
//AN_Xml:
<phases>
//AN_Xml:  <!--  执行一些需要在clean之前完成的工作 -->
//AN_Xml:  <phase>pre-clean</phase>
//AN_Xml:  <!--  移除所有上一次构建生成的文件 -->
//AN_Xml:  <phase>clean</phase>
//AN_Xml:  <!--  执行一些需要在clean之后立刻完成的工作 -->
//AN_Xml:  <phase>post-clean</phase>
//AN_Xml:</phases>
//AN_Xml:<default-phases>
//AN_Xml:  <clean>
//AN_Xml:    org.apache.maven.plugins:maven-clean-plugin:2.5:clean
//AN_Xml:  </clean>
//AN_Xml:</default-phases>
//AN_Xml:

根据前面提到的阶段间依赖关系理论,当我们执行 mvn clean 的时候,会执行 clean 生命周期中的 pre-clean 和 clean 阶段。

//AN_Xml:

site 生命周期

//AN_Xml:

site 生命周期的目的是建立和发布项目站点,共包含 4 个阶段:

//AN_Xml:
    //AN_Xml:
  1. pre-site
  2. //AN_Xml:
  3. site
  4. //AN_Xml:
  5. post-site
  6. //AN_Xml:
  7. site-deploy
  8. //AN_Xml:
//AN_Xml:
<phases>
//AN_Xml:  <!--  执行一些需要在生成站点文档之前完成的工作 -->
//AN_Xml:  <phase>pre-site</phase>
//AN_Xml:  <!--  生成项目的站点文档作 -->
//AN_Xml:  <phase>site</phase>
//AN_Xml:  <!--  执行一些需要在生成站点文档之后完成的工作,并且为部署做准备 -->
//AN_Xml:  <phase>post-site</phase>
//AN_Xml:  <!--  将生成的站点文档部署到特定的服务器上 -->
//AN_Xml:  <phase>site-deploy</phase>
//AN_Xml:</phases>
//AN_Xml:<default-phases>
//AN_Xml:  <site>
//AN_Xml:    org.apache.maven.plugins:maven-site-plugin:3.3:site
//AN_Xml:  </site>
//AN_Xml:  <site-deploy>
//AN_Xml:    org.apache.maven.plugins:maven-site-plugin:3.3:deploy
//AN_Xml:  </site-deploy>
//AN_Xml:</default-phases>
//AN_Xml:

Maven 能够基于 pom.xml 所包含的信息,自动生成一个友好的站点,方便团队交流和发布项目信息。

//AN_Xml:

Maven 插件

//AN_Xml:

Maven 本质上是一个插件执行框架,所有的执行过程,都是由一个一个插件独立完成的。像咱们日常使用到的 install、clean、deploy 等命令,其实底层都是一个一个的 Maven 插件。关于 Maven 的核心插件可以参考官方的这篇文档:https://maven.apache.org/plugins/index.html

//AN_Xml:

本地默认插件路径: ${user.home}/.m2/repository/org/apache/maven/plugins

//AN_Xml:

//AN_Xml:

除了 Maven 自带的插件之外,还有一些三方提供的插件比如单测覆盖率插件 jacoco-maven-plugin、帮助开发检测代码中不合规范的地方的插件 maven-checkstyle-plugin、分析代码质量的 sonar-maven-plugin。并且,我们还可以自定义插件来满足自己的需求。

//AN_Xml:

jacoco-maven-plugin 使用示例:

//AN_Xml:
<build>
//AN_Xml:  <plugins>
//AN_Xml:    <plugin>
//AN_Xml:      <groupId>org.jacoco</groupId>
//AN_Xml:      <artifactId>jacoco-maven-plugin</artifactId>
//AN_Xml:      <version>0.8.8</version>
//AN_Xml:      <executions>
//AN_Xml:        <execution>
//AN_Xml:          <goals>
//AN_Xml:            <goal>prepare-agent</goal>
//AN_Xml:          </goals>
//AN_Xml:        </execution>
//AN_Xml:        <execution>
//AN_Xml:          <id>generate-code-coverage-report</id>
//AN_Xml:          <phase>test</phase>
//AN_Xml:          <goals>
//AN_Xml:            <goal>report</goal>
//AN_Xml:          </goals>
//AN_Xml:        </execution>
//AN_Xml:      </executions>
//AN_Xml:    </plugin>
//AN_Xml:  </plugins>
//AN_Xml:</build>
//AN_Xml:

你可以将 Maven 插件理解为一组任务的集合,用户可以通过命令行直接运行指定插件的任务,也可以将插件任务挂载到构建生命周期,随着生命周期运行。

//AN_Xml:

Maven 插件被分为下面两种类型:

//AN_Xml:
    //AN_Xml:
  • Build plugins:在构建时执行。
  • //AN_Xml:
  • Reporting plugins:在网站生成过程中执行。
  • //AN_Xml:
//AN_Xml:

Maven 多模块管理

//AN_Xml:

多模块管理简单地来说就是将一个项目分为多个模块,每个模块只负责单一的功能实现。直观的表现就是一个 Maven 项目中不止有一个 pom.xml 文件,会在不同的目录中有多个 pom.xml 文件,进而实现多模块管理。

//AN_Xml:

多模块管理除了可以更加便于项目开发和管理,还有如下好处:

//AN_Xml:
    //AN_Xml:
  1. 降低代码之间的耦合性(从类级别的耦合提升到 jar 包级别的耦合);
  2. //AN_Xml:
  3. 减少重复,提升复用性;
  4. //AN_Xml:
  5. 每个模块都可以是自解释的(通过模块名或者模块文档);
  6. //AN_Xml:
  7. 模块还规范了代码边界的划分,开发者很容易通过模块确定自己所负责的内容。
  8. //AN_Xml:
//AN_Xml:

多模块管理下,会有一个父模块,其他的都是子模块。父模块通常只有一个 pom.xml,没有其他内容。父模块的 pom.xml 一般只定义了各个依赖的版本号、包含哪些子模块以及插件有哪些。不过,要注意的是,如果依赖只在某个子项目中使用,则可以在子项目的 pom.xml 中直接引入,防止父 pom 的过于臃肿。

//AN_Xml:

如下图所示,Dubbo 项目就被分成了多个子模块比如 dubbo-common(公共逻辑模块)、dubbo-remoting(远程通讯模块)、dubbo-rpc(远程调用模块)。

//AN_Xml:

//AN_Xml:

文章推荐

//AN_Xml: //AN_Xml:

参考

//AN_Xml: //AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 项目经验指南 //AN_Xml: https://javaguide.cn/interview-preparation/project-experience-guide.html //AN_Xml: https://javaguide.cn/interview-preparation/project-experience-guide.html //AN_Xml: 项目经验指南 //AN_Xml: 项目经验指南:针对没有项目/项目平淡的求职者,给出获取实战项目经验的方法与选择建议,并讲清如何做出项目亮点、如何复盘与表达,提升简历与面试竞争力。 //AN_Xml: 面试准备 //AN_Xml: Thu, 03 Nov 2022 15:33:32 GMT //AN_Xml: //AN_Xml:

友情提示

//AN_Xml:

本文节选自 《Java 面试指北》。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。

//AN_Xml: //AN_Xml:

没有项目经验怎么办?

//AN_Xml:

没有项目经验是大部分应届生会碰到的一个问题。甚至说,有很多有工作经验的程序员,对自己在公司做的项目不满意,也想找一个比较有技术含量的项目来做。

//AN_Xml:

说几种我觉得比较靠谱的获取项目经验的方式,希望能够对你有启发。

//AN_Xml:

实战项目视频/专栏

//AN_Xml:

在网上找一个符合自己能力与找工作需求的实战项目视频或者专栏,跟着老师一起做。

//AN_Xml:

你可以通过慕课网、哔哩哔哩、拉勾、极客时间、培训机构(比如黑马、尚硅谷)等渠道获取到适合自己的实战项目视频/专栏。

//AN_Xml:

慕课网实战课

//AN_Xml:

尽量选择一个适合自己的项目,没必要必须做分布式/微服务项目,对于绝大部分同学来说,能把一个单机项目做好就已经很不错了。

//AN_Xml:

我面试过很多求职者,简历上看着有微服务的项目经验,结果随便问两个问题就知道根本不是自己做的或者说做的时候压根没认真思考。这种情况会给我留下非常不好的印象。

//AN_Xml:

我在 《Java 面试指北》 的「面试准备篇」中也说过:

//AN_Xml:
//AN_Xml:

个人认为也没必要非要去做微服务或者分布式项目,不一定对你面试有利。微服务或者分布式项目涉及的知识点太多,一般人很难吃透。并且,这类项目其实对于校招生来说稍微有一点超标了。即使你做出来,很多面试官也会认为不是你独立完成的。

//AN_Xml:

其实,你能把一个单体项目做到极致也很好,对于个人能力提升不比做微服务或者分布式项目差。如何做到极致?代码质量这里就不提了,更重要的是你要尽量让自己的项目有一些亮点(比如你是如何提升项目性能的、如何解决项目中存在的一个痛点的),项目经历取得的成果尽量要量化一下比如我使用 xxx 技术解决了 xxx 问题,系统 qps 从 xxx 提高到了 xxx。

//AN_Xml:
//AN_Xml:

跟着老师做的过程中,你一定要有自己的思考,不要浅尝辄止。对于很多知识点,别人的讲解可能只是满足项目就够了,你自己想多点知识的话,对于重要的知识点就要自己学会去深入学习。

//AN_Xml:

实战类开源项目

//AN_Xml:

GitHub 或者码云上面有很多实战类别项目,你可以选择一个来研究,为了让自己对这个项目更加理解,在理解原有代码的基础上,你可以对原有项目进行改进或者增加功能。

//AN_Xml:

你可以参考 Java 优质开源实战项目 上面推荐的实战类开源项目,质量都很高,项目类型也比较全面,涵盖博客/论坛系统、考试/刷题系统、商城系统、权限管理系统、快速开发脚手架以及各种轮子。

//AN_Xml:

Java 优质开源实战项目

//AN_Xml:

一定要记住:不光要做,还要改进,改善。不论是实战项目视频或者专栏还是实战类开源项目,都一定会有很多可以完善改进的地方。

//AN_Xml:

从头开始做

//AN_Xml:

自己动手去做一个自己想完成的东西,遇到不会的东西就临时去学,现学现卖。

//AN_Xml:

这个要求比较高,我建议你已经有了一个项目经验之后,再采用这个方法。如果你没有做过项目的话,还是老老实实采用上面两个方法比较好。

//AN_Xml:

参加各种大公司组织的各种大赛

//AN_Xml:

如果参加这种赛事能获奖的话,项目含金量非常高。即使没获奖也没啥,也可以写简历上。

//AN_Xml:

阿里云天池大赛

//AN_Xml:

参与实际项目

//AN_Xml:

通常情况下,你有如下途径接触到企业实际项目的开发:

//AN_Xml:
    //AN_Xml:
  1. 老师接的项目;
  2. //AN_Xml:
  3. 自己接的私活;
  4. //AN_Xml:
  5. 实习/工作接触到的项目;
  6. //AN_Xml:
//AN_Xml:

老师接的项目和自己接的私活通常都是一些偏业务的项目,很少会涉及到性能优化。这种情况下,你可以考虑对项目进行改进,别怕花时间,某个时间用心做好一件事情就好比如你对项目的数据模型进行改进、引入缓存提高访问速度等等。

//AN_Xml:

实习/工作接触到的项目类似,如果遇到一些偏业务的项目,也是要自己私下对项目进行改进优化。

//AN_Xml:

尽量是真的对项目进行了优化,这本身也是对个人能力的提升。如果你实在是没时间去实践的话,也没关系,吃透这个项目优化手段就好,把一些面试可能会遇到的问题提前准备一下。

//AN_Xml:

有没有还不错的项目推荐?

//AN_Xml:

《Java 面试指北》 的「面试准备篇」中有一篇文章专门整理了一些比较高质量的实战项目,包含业务项目、轮子项目、国外公开课 Lab 和视频类实战项目教程推荐,非常适合用来学习或者作为项目经验。

//AN_Xml:

优质 Java 实战项目推荐

//AN_Xml:

这篇文章一共推荐了 15+ 个实战项目,有业务类的,也有轮子类的,有开源项目、也有视频教程。对于参加校招的小伙伴,我更建议做一个业务类项目加上一个轮子类的项目。

//AN_Xml:

我跟着视频做的项目会被面试官嫌弃不?

//AN_Xml:

很多应届生都是跟着视频做的项目,这个大部分面试官都心知肚明。

//AN_Xml:

不排除确实有些面试官不吃这一套,这个也看人。不过我相信大多数面试官都是能理解的,毕竟你在学校的时候实际上是没有什么获得实际项目经验的途径的。

//AN_Xml:

大部分应届生的项目经验都是自己在网上找的或者像你一样买的付费课程跟着做的,极少部分是比较真实的项目。 从你能想着做一个实战项目来说,我觉得初衷是好的,确实也能真正学到东西。 但是,究竟有多少是自己掌握了很重要。看视频最忌讳的是被动接受,自己多改进一下,多思考一下!就算是你跟着视频做的项目,也是可以优化的!

//AN_Xml:

如果你想真正学到东西的话,建议不光要把项目单纯完成跑起来,还要去自己尝试着优化!

//AN_Xml:

简单说几个比较容易的优化点:

//AN_Xml:
    //AN_Xml:
  1. 全局异常处理:很多项目这方面都做的不是很好,可以参考我的这篇文章:《使用枚举简单封装一个优雅的 Spring Boot 全局异常处理!》 来做优化。
  2. //AN_Xml:
  3. 项目的技术选型优化:比如使用 Guava 做本地缓存的地方可以换成 Caffeine 。Caffeine 的各方面的表现要更加好!再比如 Controller 层是否放了太多的业务逻辑。
  4. //AN_Xml:
  5. 数据库方面:数据库设计可否优化?索引是否使用使用正确?SQL 语句是否可以优化?是否需要进行读写分离?
  6. //AN_Xml:
  7. 缓存:项目有没有哪些数据是经常被访问的?是否引入缓存来提高响应速度?
  8. //AN_Xml:
  9. 安全:项目是否存在安全问题?
  10. //AN_Xml:
  11. ……
  12. //AN_Xml:
//AN_Xml:

另外,我在星球分享过常见的性能优化方向实践案例,涉及到多线程、异步、索引、缓存等方向,强烈推荐你看看:https://t.zsxq.com/06EqfeMZZ

//AN_Xml:

最后,再给大家推荐一个 IDEA 优化代码的小技巧,超级实用!

//AN_Xml:

分析你的代码:右键项目-> Analyze->Inspect Code

//AN_Xml:

//AN_Xml:

扫描完成之后,IDEA 会给出一些可能存在的代码坏味道比如命名问题。

//AN_Xml:

//AN_Xml:

并且,你还可以自定义检查规则。

//AN_Xml:

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Java 17 新特性概览(重要) //AN_Xml: https://javaguide.cn/java/new-features/java17.html //AN_Xml: https://javaguide.cn/java/new-features/java17.html //AN_Xml: Java 17 新特性概览(重要) //AN_Xml: 总结 JDK 17 的重要更新与 JEP,涵盖密封类、记录类与模式匹配等特性。 //AN_Xml: Java //AN_Xml: Wed, 28 Sep 2022 12:35:46 GMT //AN_Xml: Java 17 在 2021 年 9 月 14 日正式发布,是一个长期支持(LTS)版本。

//AN_Xml:

下面这张图是 Oracle 官方给出的 Oracle JDK 支持的时间线。可以看得到,Java 17 最多可以支持到 2029 年 9 月份。

//AN_Xml:

//AN_Xml:

Java 17 将是继 Java 8 以来最重要的长期支持(LTS)版本,是 Java 社区八年努力的成果。Spring 6.x 和 Spring Boot 3.x 最低支持的就是 Java 17。

//AN_Xml:

JDK 17 共有 14 个新特性,这篇文章会挑选其中较为重要的一些新特性进行详细介绍:

//AN_Xml: //AN_Xml:

下图是从 JDK 8 到 JDK 16 每个版本的更新带来的新特性数量和更新时间:

//AN_Xml:

//AN_Xml:

相关阅读:OpenJDK Java 17 文档

//AN_Xml:

JEP 356: Enhanced Pseudo-Random Number Generators(增强的伪随机数生成器)

//AN_Xml:

JDK 17 之前,我们可以借助 RandomThreadLocalRandomSplittableRandom来生成随机数。不过,这 3 个类都各有缺陷,且缺少常见的伪随机算法支持。

//AN_Xml:

Java 17 为伪随机数生成器 (pseudorandom number generator,PRNG,又称为确定性随机位生成器)增加了新的接口类型和实现,使得开发者更容易在应用程序中互换使用各种 PRNG 算法。

//AN_Xml:
//AN_Xml:

PRNG 用来生成接近于绝对随机数序列的数字序列。一般来说,PRNG 会依赖于一个初始值,也称为种子,来生成对应的伪随机数序列。只要种子确定了,PRNG 所生成的随机数就是完全确定的,因此其生成的随机数序列并不是真正随机的。

//AN_Xml:
//AN_Xml:

使用示例:

//AN_Xml:
RandomGeneratorFactory<RandomGenerator> l128X256MixRandom = RandomGeneratorFactory.of("L128X256MixRandom");
//AN_Xml:// 使用时间戳作为随机数种子
//AN_Xml:RandomGenerator randomGenerator = l128X256MixRandom.create(System.currentTimeMillis());
//AN_Xml:// 生成随机数
//AN_Xml:randomGenerator.nextInt(10);
//AN_Xml:

JEP 398: Deprecate the Applet API for Removal(标记弃用 Applet API 以便移除)

//AN_Xml:

Applet API 用于编写在 Web 浏览器端运行的 Java 小程序,很多年前就已经被淘汰了,已经没有理由使用了。

//AN_Xml:

Applet API 在 Java 9 时被标记弃用(JEP 289),但不是为了删除。

//AN_Xml:

JEP 406: Pattern Matching for switch(switch 模式匹配,预览)

//AN_Xml:

正如 instanceof 一样, switch 也紧跟着增加了类型匹配自动转换功能。

//AN_Xml:

instanceof 代码示例:

//AN_Xml:
// Old code
//AN_Xml:if (o instanceof String) {
//AN_Xml:    String s = (String)o;
//AN_Xml:    ... use s ...
//AN_Xml:}
//AN_Xml:
//AN_Xml:// New code
//AN_Xml:if (o instanceof String s) {
//AN_Xml:    ... use s ...
//AN_Xml:}
//AN_Xml:

switch 代码示例:

//AN_Xml:
// Old code
//AN_Xml:static String formatter(Object o) {
//AN_Xml:    String formatted = "unknown";
//AN_Xml:    if (o instanceof Integer i) {
//AN_Xml:        formatted = String.format("int %d", i);
//AN_Xml:    } else if (o instanceof Long l) {
//AN_Xml:        formatted = String.format("long %d", l);
//AN_Xml:    } else if (o instanceof Double d) {
//AN_Xml:        formatted = String.format("double %f", d);
//AN_Xml:    } else if (o instanceof String s) {
//AN_Xml:        formatted = String.format("String %s", s);
//AN_Xml:    }
//AN_Xml:    return formatted;
//AN_Xml:}
//AN_Xml:
//AN_Xml:// New code
//AN_Xml:static String formatterPatternSwitch(Object o) {
//AN_Xml:    return switch (o) {
//AN_Xml:        case Integer i -> String.format("int %d", i);
//AN_Xml:        case Long l    -> String.format("long %d", l);
//AN_Xml:        case Double d  -> String.format("double %f", d);
//AN_Xml:        case String s  -> String.format("String %s", s);
//AN_Xml:        default        -> o.toString();
//AN_Xml:    };
//AN_Xml:}
//AN_Xml:

对于 null 值的判断也进行了优化。

//AN_Xml:
// Old code
//AN_Xml:static void testFooBar(String s) {
//AN_Xml:    if (s == null) {
//AN_Xml:        System.out.println("oops!");
//AN_Xml:        return;
//AN_Xml:    }
//AN_Xml:    switch (s) {
//AN_Xml:        case "Foo", "Bar" -> System.out.println("Great");
//AN_Xml:        default           -> System.out.println("Ok");
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:
//AN_Xml:// New code
//AN_Xml:static void testFooBar(String s) {
//AN_Xml:    switch (s) {
//AN_Xml:        case null         -> System.out.println("Oops");
//AN_Xml:        case "Foo", "Bar" -> System.out.println("Great");
//AN_Xml:        default           -> System.out.println("Ok");
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

JEP 407: Remove RMI Activation(移除 RMI 激活机制)

//AN_Xml:

删除远程方法调用 (RMI) 激活机制,同时保留 RMI 的其余部分。RMI 激活机制已过时且不再使用。

//AN_Xml:

JEP 409: Sealed Classes(密封类)

//AN_Xml:

密封类由 JEP 360 提出预览,集成到了 Java 15 中。在 JDK 16 中, 密封类得到了改进(更加严格的引用检查和密封类的继承关系),由 JEP 397 提出了再次预览。

//AN_Xml:

Java 14 & 15 新特性概览 中,我有详细介绍到密封类,这里就不再做额外的介绍了。

//AN_Xml:

JEP 410: Remove the Experimental AOT and JIT Compiler(移除实验性的 AOT 和 JIT 编译器)

//AN_Xml:

在 Java 9 的 JEP 295 ,引入了实验性的提前 (AOT) 编译器,在启动虚拟机之前将 Java 类编译为本机代码。

//AN_Xml:

Java 17,删除实验性的提前 (AOT) 和即时 (JIT) 编译器,因为该编译器自推出以来很少使用,维护它所需的工作量很大。保留实验性的 Java 级 JVM 编译器接口 (JVMCI),以便开发人员可以继续使用外部构建的编译器版本进行 JIT 编译。

//AN_Xml:

JEP 411: Deprecate the Security Manager for Removal(标记弃用安全管理器以便移除)

//AN_Xml:

弃用安全管理器以便在将来的版本中删除。

//AN_Xml:

安全管理器可追溯到 Java 1.0,多年来,它一直不是保护客户端 Java 代码的主要方法,也很少用于保护服务器端代码。为了推动 Java 向前发展,Java 17 弃用安全管理器,以便与旧版 Applet API ( JEP 398 ) 一起移除。

//AN_Xml:

JEP 412: Foreign Function & Memory API(外部函数和内存 API,孵化)

//AN_Xml:

Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。

//AN_Xml:

外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 JEP 412 提出。第二轮孵化由JEP 419 提出并集成到了 Java 18 中,预览由 JEP 424 提出并集成到了 Java 19 中。

//AN_Xml:

Java 19 新特性概览 中,我有详细介绍到外部函数和内存 API,这里就不再做额外的介绍了。

//AN_Xml:

JEP 414: Vector API(向量 API,第二次孵化)

//AN_Xml:

向量(Vector) API 最初由 JEP 338 提出,并作为孵化 API集成到 Java 16 中。第二轮孵化由 JEP 414 提出并集成到 Java 17 中,第三轮孵化由 JEP 417 提出并集成到 Java 18 中,第四轮由 JEP 426 提出并集成到了 Java 19 中。

//AN_Xml:

该孵化器 API 提供了一个 API 的初始迭代以表达一些向量计算,这些计算在运行时可靠地编译为支持的 CPU 架构上的最佳向量硬件指令,从而获得优于同等标量计算的性能,充分利用单指令多数据(SIMD)技术(大多数现代 CPU 上都可以使用的一种指令)。尽管 HotSpot 支持自动向量化,但是可转换的标量操作集有限且易受代码更改的影响。该 API 将使开发人员能够轻松地用 Java 编写可移植的高性能向量算法。

//AN_Xml:

Java 18 新特性概览 中,我有详细介绍到向量 API,这里就不再做额外的介绍了。

//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Java 18 新特性概览 //AN_Xml: https://javaguide.cn/java/new-features/java18.html //AN_Xml: https://javaguide.cn/java/new-features/java18.html //AN_Xml: Java 18 新特性概览 //AN_Xml: 概览 JDK 18 的更新与预览特性,理解新 API 带来的改进。 //AN_Xml: Java //AN_Xml: Tue, 13 Sep 2022 12:49:20 GMT //AN_Xml: Java 18 在 2022 年 3 月 22 日正式发布,非长期支持版本。

//AN_Xml:

JDK 18 共有 8 个新特性,这篇文章会挑选其中较为重要的一些新特性进行详细介绍:

//AN_Xml: //AN_Xml:

下图是从 JDK 8 到 JDK 25 每个版本的更新带来的新特性数量和更新时间:

//AN_Xml:

 JDK 8 到 JDK 25 每个版本的更新带来的新特性数量和更新时间

//AN_Xml:

相关阅读:

//AN_Xml: //AN_Xml:

JEP 400: UTF-8 by Default(UTF-8 作为默认字符集,转正)

//AN_Xml:

JDK 终于将 UTF-8 设置为默认字符集。

//AN_Xml:

在 Java 17 及更早版本中,默认字符集是在 Java 虚拟机运行时才确定的,取决于不同的操作系统、区域设置等因素,因此存在潜在的风险。就比如说你在 Mac 上运行正常的一段打印文字到控制台的 Java 程序到了 Windows 上就会出现乱码,如果你不手动更改字符集的话。

//AN_Xml:

JEP 408: Simple Web Server(简单 Web 服务器,转正)

//AN_Xml:

Java 18 之后,你可以使用 jwebserver 命令启动一个简易的静态 Web 服务器。

//AN_Xml:
$ jwebserver
//AN_Xml:Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
//AN_Xml:Serving /cwd and subdirectories on 127.0.0.1 port 8000
//AN_Xml:URL: http://127.0.0.1:8000/
//AN_Xml:

这个服务器不支持 CGI 和 Servlet,只限于静态文件。

//AN_Xml:

JEP 413: Code Snippets in Java API Documentation(API 文档代码片段,转正)

//AN_Xml:

在 Java 18 之前,如果我们想要在 Javadoc 中引入代码片段可以使用 <pre>{@code ...}</pre>

//AN_Xml:
<pre>{@code
//AN_Xml:    lines of source code
//AN_Xml:}</pre>
//AN_Xml:

<pre>{@code ...}</pre> 这种方式生成的效果比较一般。

//AN_Xml:

在 Java 18 之后,可以通过 @snippet 标签来做这件事情。

//AN_Xml:
/**
//AN_Xml: * The following code shows how to use {@code Optional.isPresent}:
//AN_Xml: * {@snippet :
//AN_Xml: * if (v.isPresent()) {
//AN_Xml: *     System.out.println("v: " + v.get());
//AN_Xml: * }
//AN_Xml: * }
//AN_Xml: */
//AN_Xml:

@snippet 这种方式生成的效果更好且使用起来更方便一些。

//AN_Xml:

JEP 416: Reimplement Core Reflection with Method Handles(方法句柄重构核心反射,转正)

//AN_Xml:

Java 18 改进了 java.lang.reflect.MethodConstructor 的实现逻辑,使之性能更好,速度更快。这项改动不会改动相关 API ,这意味着开发中不需要改动反射相关代码,就可以体验到性能更好的反射。

//AN_Xml:

OpenJDK 官方给出了新老实现的反射性能基准测试结果。

//AN_Xml:

新老实现的反射性能基准测试结果

//AN_Xml:

JEP 417: Vector API(向量 API,第三次孵化)

//AN_Xml:

向量(Vector) API 最初由 JEP 338 提出,并作为孵化 API集成到 Java 16 中。第二轮孵化由 JEP 414 提出并集成到 Java 17 中,第三轮孵化由 JEP 417 提出并集成到 Java 18 中,第四轮由 JEP 426 提出并集成到了 Java 19 中。

//AN_Xml:

向量计算由对向量的一系列操作组成。向量 API 用来表达向量计算,该计算可以在运行时可靠地编译为支持的 CPU 架构上的最佳向量指令,从而实现优于等效标量计算的性能。

//AN_Xml:

向量 API 的目标是为用户提供简洁易用且与平台无关的表达范围广泛的向量计算。

//AN_Xml:

这是对数组元素的简单标量计算:

//AN_Xml:
void scalarComputation(float[] a, float[] b, float[] c) {
//AN_Xml:   for (int i = 0; i < a.length; i++) {
//AN_Xml:        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
//AN_Xml:   }
//AN_Xml:}
//AN_Xml:

这是使用 Vector API 进行的等效向量计算:

//AN_Xml:
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
//AN_Xml:
//AN_Xml:void vectorComputation(float[] a, float[] b, float[] c) {
//AN_Xml:    int i = 0;
//AN_Xml:    int upperBound = SPECIES.loopBound(a.length);
//AN_Xml:    for (; i < upperBound; i += SPECIES.length()) {
//AN_Xml:        // FloatVector va, vb, vc;
//AN_Xml:        var va = FloatVector.fromArray(SPECIES, a, i);
//AN_Xml:        var vb = FloatVector.fromArray(SPECIES, b, i);
//AN_Xml:        var vc = va.mul(va)
//AN_Xml:                   .add(vb.mul(vb))
//AN_Xml:                   .neg();
//AN_Xml:        vc.intoArray(c, i);
//AN_Xml:    }
//AN_Xml:    for (; i < a.length; i++) {
//AN_Xml:        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

在 JDK 18 中,向量 API 的性能得到了进一步的优化。

//AN_Xml:

JEP 418: Internet-Address Resolution SPI(互联网地址解析 SPI,转正)

//AN_Xml:

Java 18 定义了一个全新的 SPI(service-provider interface),用于主要名称和地址的解析,以便 java.net.InetAddress 可以使用平台之外的第三方解析器。

//AN_Xml:

JEP 419: Foreign Function & Memory API(外部函数和内存 API,第二次孵化)

//AN_Xml:

Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。

//AN_Xml:

外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 JEP 412 提出。第二轮孵化由JEP 419 提出并集成到了 Java 18 中,预览由 JEP 424 提出并集成到了 Java 19 中。

//AN_Xml:

Java 19 新特性概览 中,我有详细介绍到外部函数和内存 API,这里就不再做额外的介绍了。

//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Java 19 新特性概览 //AN_Xml: https://javaguide.cn/java/new-features/java19.html //AN_Xml: https://javaguide.cn/java/new-features/java19.html //AN_Xml: Java 19 新特性概览 //AN_Xml: 介绍 JDK 19 的预览特性与并发相关更新,为后续虚拟线程铺垫。 //AN_Xml: Java //AN_Xml: Tue, 13 Sep 2022 01:11:51 GMT //AN_Xml: JDK 19 于 2022 年 9 月 20 日正式发布,非长期支持版本。

//AN_Xml:

JDK 19 共有 7 个新特性,这篇文章会挑选其中较为重要的一些新特性进行详细介绍:

//AN_Xml: //AN_Xml:

下图是从 JDK 8 到 JDK 25 每个版本的更新带来的新特性数量和更新时间:

//AN_Xml:

 JDK 8 到 JDK 25 每个版本的更新带来的新特性数量和更新时间

//AN_Xml:

JEP 424: 外部函数和内存 API(预览)

//AN_Xml:

Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。

//AN_Xml:

外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 JEP 412 提出。第二轮孵化由JEP 419 提出并集成到了 Java 18 中,预览由 JEP 424 提出并集成到了 Java 19 中。

//AN_Xml:

在没有外部函数和内存 API 之前:

//AN_Xml:
    //AN_Xml:
  • Java 通过 sun.misc.Unsafe 提供一些执行低级别、不安全操作的方法(如直接访问系统内存资源、自主管理内存资源等),Unsafe 类让 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力的同时,也增加了 Java 语言的不安全性,不正确使用 Unsafe 类会使得程序出错的概率变大。
  • //AN_Xml:
  • Java 1.1 就已通过 Java 原生接口(JNI)支持了原生方法调用,但并不好用。JNI 实现起来过于复杂,步骤繁琐(具体的步骤可以参考这篇文章:Guide to JNI (Java Native Interface)),不受 JVM 的语言安全机制控制,影响 Java 语言的跨平台特性。并且,JNI 的性能也不行,因为 JNI 方法调用不能从许多常见的 JIT 优化(如内联)中受益。虽然 JNAJNRJavaCPP 等框架对 JNI 进行了改进,但效果还是不太理想。
  • //AN_Xml:
//AN_Xml:

引入外部函数和内存 API 就是为了解决 Java 访问外部函数和外部内存存在的一些痛点。

//AN_Xml:

Foreign Function & Memory API (FFM API) 定义了类和接口:

//AN_Xml:
    //AN_Xml:
  • 分配外部内存:MemorySegmentMemoryAddressSegmentAllocator
  • //AN_Xml:
  • 操作和访问结构化的外部内存:MemoryLayoutVarHandle
  • //AN_Xml:
  • 控制外部内存的分配和释放:MemorySession
  • //AN_Xml:
  • 调用外部函数:LinkerFunctionDescriptorSymbolLookup
  • //AN_Xml:
//AN_Xml:

下面是 FFM API 使用示例,这段代码获取了 C 库函数的 radixsort 方法句柄,然后使用它对 Java 数组中的四个字符串进行排序。

//AN_Xml:
// 1. 在 C 库路径上查找外部函数
//AN_Xml:Linker linker = Linker.nativeLinker();
//AN_Xml:SymbolLookup stdlib = linker.defaultLookup();
//AN_Xml:MethodHandle radixSort = linker.downcallHandle(
//AN_Xml:                             stdlib.lookup("radixsort"), ...);
//AN_Xml:// 2. 分配堆上内存以存储四个字符串
//AN_Xml:String[] javaStrings   = { "mouse", "cat", "dog", "car" };
//AN_Xml:// 3. 分配堆外内存以存储四个指针
//AN_Xml:SegmentAllocator allocator = implicitAllocator();
//AN_Xml:MemorySegment offHeap  = allocator.allocateArray(ValueLayout.ADDRESS, javaStrings.length);
//AN_Xml:// 4. 将字符串从堆上复制到堆外
//AN_Xml:for (int i = 0; i < javaStrings.length; i++) {
//AN_Xml:    // 在堆外分配一个字符串,然后存储指向它的指针
//AN_Xml:    MemorySegment cString = allocator.allocateUtf8String(javaStrings[i]);
//AN_Xml:    offHeap.setAtIndex(ValueLayout.ADDRESS, i, cString);
//AN_Xml:}
//AN_Xml:// 5. 通过调用外部函数对堆外数据进行排序
//AN_Xml:radixSort.invoke(offHeap, javaStrings.length, MemoryAddress.NULL, '\0');
//AN_Xml:// 6. 将(重新排序的)字符串从堆外复制到堆上
//AN_Xml:for (int i = 0; i < javaStrings.length; i++) {
//AN_Xml:    MemoryAddress cStringPtr = offHeap.getAtIndex(ValueLayout.ADDRESS, i);
//AN_Xml:    javaStrings[i] = cStringPtr.getUtf8String(0);
//AN_Xml:}
//AN_Xml:assert Arrays.equals(javaStrings, new String[] {"car", "cat", "dog", "mouse"});  // true
//AN_Xml:

JEP 425: 虚拟线程(预览)

//AN_Xml:

虚拟线程(Virtual Thread)是 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP),许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。

//AN_Xml:

虚拟线程在其他多线程语言中已经被证实是十分有用的,比如 Go 中的 Goroutine、Erlang 中的进程。

//AN_Xml:

虚拟线程避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂,可以有效减少编写、维护和观察高吞吐量并发应用程序的工作量。

//AN_Xml:

知乎有一个关于 Java 19 虚拟线程的讨论,感兴趣的可以去看看:https://www.zhihu.com/question/536743167

//AN_Xml:

Java 虚拟线程的详细解读和原理可以看下面这两篇文章:

//AN_Xml: //AN_Xml:

JEP 426: 向量 API(第四次孵化)

//AN_Xml:

向量(Vector) API 最初由 JEP 338 提出,并作为孵化 API集成到 Java 16 中。第二轮孵化由 JEP 414 提出并集成到 Java 17 中,第三轮孵化由 JEP 417 提出并集成到 Java 18 中,第四轮由 JEP 426 提出并集成到了 Java 19 中。

//AN_Xml:

Java 18 新特性概览 中,我有详细介绍到向量 API,这里就不再做额外的介绍了。

//AN_Xml:

JEP 428: 结构化并发(孵化)

//AN_Xml:

JDK 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代java.util.concurrent,目前处于孵化器阶段。

//AN_Xml:

结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。

//AN_Xml:

结构化并发的基本 API 是StructuredTaskScopeStructuredTaskScope 支持将任务拆分为多个并发子任务,在它们自己的线程中执行,并且子任务必须在主任务继续之前完成。

//AN_Xml:

StructuredTaskScope 的基本用法如下:

//AN_Xml:
    try (var scope = new StructuredTaskScope<Object>()) {
//AN_Xml:        // 使用fork方法派生线程来执行子任务
//AN_Xml:        Future<Integer> future1 = scope.fork(task1);
//AN_Xml:        Future<String> future2 = scope.fork(task2);
//AN_Xml:        // 等待线程完成
//AN_Xml:        scope.join();
//AN_Xml:        // 结果的处理可能包括处理或重新抛出异常
//AN_Xml:        ... process results/exceptions ...
//AN_Xml:    } // close
//AN_Xml:

结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。

//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: ARP 协议详解(网络层) //AN_Xml: https://javaguide.cn/cs-basics/network/arp.html //AN_Xml: https://javaguide.cn/cs-basics/network/arp.html //AN_Xml: ARP 协议详解(网络层) //AN_Xml: 讲解 ARP 的地址解析机制与报文流程,结合 ARP 表与广播/单播详解常见攻击与防御策略。 //AN_Xml: 计算机基础 //AN_Xml: Sun, 28 Aug 2022 07:18:49 GMT //AN_Xml: 每当我们学习一个新的网络协议的时候,都要把他结合到 OSI 七层模型中,或者是 TCP/IP 协议栈中来学习,一是要学习该协议在整个网络协议栈中的位置,二是要学习该协议解决了什么问题,地位如何?三是要学习该协议的工作原理,以及一些更深入的细节。

//AN_Xml:

ARP 协议,可以说是在协议栈中属于一个偏底层的、非常重要的、又非常简单的通信协议。

//AN_Xml:

开始阅读这篇文章之前,你可以先看看下面几个问题:

//AN_Xml:
    //AN_Xml:
  1. ARP 协议在协议栈中的位置? ARP 协议在协议栈中的位置非常重要,在理解了它的工作原理之后,也很难说它到底是网络层协议,还是链路层协议,因为它恰恰串联起了网络层和链路层。国外的大部分教程通常将 ARP 协议放在网络层。
  2. //AN_Xml:
  3. ARP 协议解决了什么问题,地位如何? ARP 协议,全称 地址解析协议(Address Resolution Protocol),它解决的是网络层地址和链路层地址之间的转换问题。因为一个 IP 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 IP 地址属于逻辑地址,而 MAC 地址才是物理地址,ARP 协议解决了 IP 地址转 MAC 地址的一些问题。
  4. //AN_Xml:
  5. ARP 工作原理? 只希望大家记住几个关键词:ARP 表、广播问询、单播响应
  6. //AN_Xml:
//AN_Xml:

MAC 地址

//AN_Xml:

在介绍 ARP 协议之前,有必要介绍一下 MAC 地址。

//AN_Xml:

MAC 地址的全称是 媒体访问控制地址(Media Access Control Address)。如果说,互联网中每一个资源都由 IP 地址唯一标识(IP 协议内容),那么一切网络设备都由 MAC 地址唯一标识。

//AN_Xml:

路由器的背面就会注明 MAC 位址

//AN_Xml:

可以理解为,MAC 地址是一个网络设备真正的身份证号,IP 地址只是一种不重复的定位方式(比如说住在某省某市某街道的张三,这种逻辑定位是 IP 地址,他的身份证号才是他的 MAC 地址),也可以理解为 MAC 地址是身份证号,IP 地址是邮政地址。MAC 地址也有一些别称,如 LAN 地址、物理地址、以太网地址等。

//AN_Xml:
//AN_Xml:

还有一点要知道的是,不仅仅是网络资源才有 IP 地址,网络设备也有 IP 地址,比如路由器。但从结构上说,路由器等网络设备的作用是组成一个网络,而且通常是内网,所以它们使用的 IP 地址通常是内网 IP,内网的设备在与内网以外的设备进行通信时,需要用到 NAT 协议。

//AN_Xml:
//AN_Xml:

MAC 地址的长度为 6 字节(48 比特),地址空间大小有 280 万亿之多($2^{48}$),MAC 地址由 IEEE 统一管理与分配,理论上,一个网络设备中的网卡上的 MAC 地址是永久的。不同的网卡生产商从 IEEE 那里购买自己的 MAC 地址空间(MAC 的前 24 比特),也就是前 24 比特由 IEEE 统一管理,保证不会重复。而后 24 比特,由各家生产商自己管理,同样保证生产的两块网卡的 MAC 地址不会重复。

//AN_Xml:

MAC 地址具有可携带性、永久性,身份证号永久地标识一个人的身份,不论他到哪里都不会改变。而 IP 地址不具有这些性质,当一台设备更换了网络,它的 IP 地址也就可能发生改变,也就是它在互联网中的定位发生了变化。

//AN_Xml:

最后,记住,MAC 地址有一个特殊地址:FF-FF-FF-FF-FF-FF(全 1 地址),该地址表示广播地址。

//AN_Xml:

ARP 协议工作原理

//AN_Xml:

ARP 协议工作时有一个大前提,那就是 ARP 表

//AN_Xml:

在一个局域网内,每个网络设备都自己维护了一个 ARP 表,ARP 表记录了某些其他网络设备的 IP 地址-MAC 地址映射关系,该映射关系以 <IP, MAC, TTL> 三元组的形式存储。其中,TTL 为该映射关系的生存周期,典型值为 20 分钟,超过该时间,该条目将被丢弃。

//AN_Xml:

ARP 的工作原理将分两种场景讨论:

//AN_Xml:
    //AN_Xml:
  1. 同一局域网内的 MAC 寻址
  2. //AN_Xml:
  3. 从一个局域网到另一个局域网中的网络设备的寻址
  4. //AN_Xml:
//AN_Xml:

同一局域网内的 MAC 寻址

//AN_Xml:

假设当前有如下场景:IP 地址为137.196.7.23的主机 A,想要给同一局域网内的 IP 地址为137.196.7.14主机 B,发送 IP 数据报文。

//AN_Xml:
//AN_Xml:

再次强调,当主机发送 IP 数据报文时(网络层),仅知道目的地的 IP 地址,并不清楚目的地的 MAC 地址,而 ARP 协议就是解决这一问题的。

//AN_Xml:
//AN_Xml:

为了达成这一目标,主机 A 将不得不通过 ARP 协议来获取主机 B 的 MAC 地址,并将 IP 报文封装成链路层帧,发送到下一跳上。在该局域网内,关于此将按照时间顺序,依次发生如下事件:

//AN_Xml:
    //AN_Xml:
  1. //AN_Xml:

    主机 A 检索自己的 ARP 表,发现 ARP 表中并无主机 B 的 IP 地址对应的映射条目,也就无从知道主机 B 的 MAC 地址。

    //AN_Xml:
  2. //AN_Xml:
  3. //AN_Xml:

    主机 A 将构造一个 ARP 查询分组,并将其广播到所在的局域网中。

    //AN_Xml:

    ARP 分组是一种特殊报文,ARP 分组有两类,一种是查询分组,另一种是响应分组,它们具有相同的格式,均包含了发送和接收的 IP 地址、发送和接收的 MAC 地址。当然了,查询分组中,发送的 IP 地址,即为主机 A 的 IP 地址,接收的 IP 地址即为主机 B 的 IP 地址,发送的 MAC 地址也是主机 A 的 MAC 地址,但接收的 MAC 地址绝不会是主机 B 的 MAC 地址(因为这正是我们要问询的!),而是一个特殊值——FF-FF-FF-FF-FF-FF,之前说过,该 MAC 地址是广播地址,也就是说,查询分组将广播给该局域网内的所有设备。

    //AN_Xml:
  4. //AN_Xml:
  5. //AN_Xml:

    主机 A 构造的查询分组将在该局域网内广播,理论上,每一个设备都会收到该分组,并检查查询分组的接收 IP 地址是否为自己的 IP 地址,如果是,说明查询分组已经到达了主机 B,否则,该查询分组对当前设备无效,丢弃之。

    //AN_Xml:
  6. //AN_Xml:
  7. //AN_Xml:

    主机 B 收到了查询分组之后,验证是对自己的问询,接着构造一个 ARP 响应分组,该分组的目的地只有一个——主机 A,发送给主机 A。同时,主机 B 提取查询分组中的 IP 地址和 MAC 地址信息,在自己的 ARP 表中构造一条主机 A 的 IP-MAC 映射记录。

    //AN_Xml:

    ARP 响应分组具有和 ARP 查询分组相同的构造,不同的是,发送和接受的 IP 地址恰恰相反,发送的 MAC 地址为发送者本身,目标 MAC 地址为查询分组的发送者,也就是说,ARP 响应分组只有一个目的地,而非广播。

    //AN_Xml:
  8. //AN_Xml:
  9. //AN_Xml:

    主机 A 终将收到主机 B 的响应分组,提取出该分组中的 IP 地址和 MAC 地址后,构造映射信息,加入到自己的 ARP 表中。

    //AN_Xml:
  10. //AN_Xml:
//AN_Xml:

//AN_Xml:

在整个过程中,有几点需要补充说明的是:

//AN_Xml:
    //AN_Xml:
  1. 主机 A 想要给主机 B 发送 IP 数据报,如果主机 B 的 IP-MAC 映射信息已经存在于主机 A 的 ARP 表中,那么主机 A 无需广播,只需提取 MAC 地址并构造链路层帧发送即可。
  2. //AN_Xml:
  3. ARP 表中的映射信息是有生存周期的,典型值为 20 分钟。
  4. //AN_Xml:
  5. 目标主机接收到了问询主机构造的问询报文后,将先把问询主机的 IP-MAC 映射存进自己的 ARP 表中,这样才能获取到响应的目标 MAC 地址,顺利的发送响应分组。
  6. //AN_Xml:
//AN_Xml:

总结来说,ARP 协议是一个广播问询,单播响应协议。

//AN_Xml:

不同局域网内的 MAC 寻址

//AN_Xml:

更复杂的情况是,发送主机 A 和接收主机 B 不在同一个子网中,假设一个一般场景,两台主机所在的子网由一台路由器联通。这里需要注意的是,一般情况下,我们说网络设备都有一个 IP 地址和一个 MAC 地址,这里说的网络设备,更严谨的说法应该是一个接口。路由器作为互联设备,具有多个接口,每个接口同样也应该具备不重复的 IP 地址和 MAC 地址。因此,在讨论 ARP 表时,路由器的多个接口都各自维护一个 ARP 表,而非一个路由器只维护一个 ARP 表。

//AN_Xml:

接下来,回顾同一子网内的 MAC 寻址,如果主机 A 发送一个广播问询分组,那么 A 所在的子网内所有设备(接口)都将会捕获该分组,因为该分组的目的 IP 与发送主机 A 的 IP 在同一个子网中。但是当目的 IP 与 A 不在同一子网时,A 所在子网内将不会有设备成功接收该分组。那么,主机 A 应该发送怎样的查询分组呢?整个过程按照时间顺序发生的事件如下:

//AN_Xml:
    //AN_Xml:
  1. //AN_Xml:

    主机 A 查询 ARP 表,期望寻找到目标路由器的本子网接口的 MAC 地址。

    //AN_Xml:

    目标路由器指的是,根据目的主机 B 的 IP 地址,分析出 B 所在的子网,能够把报文转发到 B 所在子网的那个路由器。

    //AN_Xml:
  2. //AN_Xml:
  3. //AN_Xml:

    主机 A 未能找到目标路由器的本子网接口的 MAC 地址,将采用 ARP 协议,问询到该 MAC 地址,由于目标接口与主机 A 在同一个子网内,该过程与同一局域网内的 MAC 寻址相同。

    //AN_Xml:
  4. //AN_Xml:
  5. //AN_Xml:

    主机 A 获取到目标接口的 MAC 地址,先构造 IP 数据报,其中源 IP 是 A 的 IP 地址,目的 IP 地址是 B 的 IP 地址,再构造链路层帧,其中源 MAC 地址是 A 的 MAC 地址,目的 MAC 地址是本子网内与路由器连接的接口的 MAC 地址。主机 A 将把这个链路层帧,以单播的方式,发送给目标接口。

    //AN_Xml:
  6. //AN_Xml:
  7. //AN_Xml:

    目标接口接收到了主机 A 发过来的链路层帧,解析,根据目的 IP 地址,查询转发表,将该 IP 数据报转发到与主机 B 所在子网相连的接口上。

    //AN_Xml:

    到此,该帧已经从主机 A 所在的子网,转移到了主机 B 所在的子网了。

    //AN_Xml:
  8. //AN_Xml:
  9. //AN_Xml:

    路由器接口查询 ARP 表,期望寻找到主机 B 的 MAC 地址。

    //AN_Xml:
  10. //AN_Xml:
  11. //AN_Xml:

    路由器接口如未能找到主机 B 的 MAC 地址,将采用 ARP 协议,广播问询,单播响应,获取到主机 B 的 MAC 地址。

    //AN_Xml:
  12. //AN_Xml:
  13. //AN_Xml:

    路由器接口将对 IP 数据报重新封装成链路层帧,目标 MAC 地址为主机 B 的 MAC 地址,单播发送,直到目的地。

    //AN_Xml:
  14. //AN_Xml:
//AN_Xml:

//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 分布式锁常见实现方案总结 //AN_Xml: https://javaguide.cn/distributed-system/distributed-lock-implementations.html //AN_Xml: https://javaguide.cn/distributed-system/distributed-lock-implementations.html //AN_Xml: 分布式锁常见实现方案总结 //AN_Xml: 分布式锁常见实现方案详解,包括基于Redis SETNX、Redlock、ZooKeeper临时节点实现分布式锁的原理、优缺点对比及最佳实践。 //AN_Xml: 分布式 //AN_Xml: Tue, 23 Aug 2022 10:53:21 GMT //AN_Xml: JavaGuide官方知识星球

//AN_Xml:

通常情况下,我们一般会选择基于 Redis 或者 ZooKeeper 实现分布式锁,Redis 用的要更多一点,我这里也先以 Redis 为例介绍分布式锁的实现。

//AN_Xml:

基于 Redis 实现分布式锁

//AN_Xml:

如何基于 Redis 实现一个最简易的分布式锁?

//AN_Xml:

不论是本地锁还是分布式锁,核心都在于“互斥”。

//AN_Xml:

在 Redis 中, SETNX 命令是可以帮助我们实现互斥。SETNXSET if Not eXists (对应 Java 中的 setIfAbsent 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX 啥也不做。

//AN_Xml:
> SETNX lockKey uniqueValue
//AN_Xml:(integer) 1
//AN_Xml:> SETNX lockKey uniqueValue
//AN_Xml:(integer) 0
//AN_Xml:

释放锁的话,直接通过 DEL 命令删除对应的 key 即可。

//AN_Xml:
> DEL lockKey
//AN_Xml:(integer) 1
//AN_Xml:

为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。

//AN_Xml:

选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。

//AN_Xml:
// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
//AN_Xml:if redis.call("get",KEYS[1]) == ARGV[1] then
//AN_Xml:    return redis.call("del",KEYS[1])
//AN_Xml:else
//AN_Xml:    return 0
//AN_Xml:end
//AN_Xml:

Redis 实现简易分布式锁

//AN_Xml:

这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。

//AN_Xml:

为什么要给锁设置一个过期时间?

//AN_Xml:

为了避免锁无法被释放,我们可以想到的一个解决办法就是:给这个 key(也就是锁) 设置一个过期时间

//AN_Xml:
127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX
//AN_Xml:OK
//AN_Xml:
    //AN_Xml:
  • lockKey:加锁的锁名;
  • //AN_Xml:
  • uniqueValue:能够唯一标识锁的随机字符串;
  • //AN_Xml:
  • NX:只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功;
  • //AN_Xml:
  • EX:过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。
  • //AN_Xml:
//AN_Xml:

一定要保证设置指定 key 的值和过期时间是一个原子操作!!! 不然的话,依然可能会出现锁无法被释放的问题。

//AN_Xml:

这样确实可以解决问题,不过,这种解决办法同样存在漏洞:如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。

//AN_Xml:

你或许在想:如果操作共享资源的操作还未完成,锁过期时间能够自己续期就好了!

//AN_Xml:

如何实现锁的优雅续期?

//AN_Xml:

对于 Java 开发的小伙伴来说,已经有了现成的解决方案:Redisson 。其他语言的解决方案,可以在 Redis 官方文档中找到,地址:https://redis.io/topics/distlock

//AN_Xml:

Distributed locks with Redis

//AN_Xml:

Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。并且,Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。

//AN_Xml:

Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。

//AN_Xml:

Redisson 看门狗自动续期

//AN_Xml:

看门狗名字的由来于 getLockWatchdogTimeout() 方法,这个方法返回的是看门狗给锁续期的过期时间,默认为 30 秒(redisson-3.17.6)。

//AN_Xml:
//默认 30秒,支持修改
//AN_Xml:private long lockWatchdogTimeout = 30 * 1000;
//AN_Xml:
//AN_Xml:public Config setLockWatchdogTimeout(long lockWatchdogTimeout) {
//AN_Xml:    this.lockWatchdogTimeout = lockWatchdogTimeout;
//AN_Xml:    return this;
//AN_Xml:}
//AN_Xml:public long getLockWatchdogTimeout() {
//AN_Xml:   return lockWatchdogTimeout;
//AN_Xml:}
//AN_Xml:

renewExpiration() 方法包含了看门狗的主要逻辑:

//AN_Xml:
private void renewExpiration() {
//AN_Xml:         //......
//AN_Xml:        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
//AN_Xml:            @Override
//AN_Xml:            public void run(Timeout timeout) throws Exception {
//AN_Xml:                //......
//AN_Xml:                // 异步续期,基于 Lua 脚本
//AN_Xml:                CompletionStage<Boolean> future = renewExpirationAsync(threadId);
//AN_Xml:                future.whenComplete((res, e) -> {
//AN_Xml:                    if (e != null) {
//AN_Xml:                        // 无法续期
//AN_Xml:                        log.error("Can't update lock " + getRawName() + " expiration", e);
//AN_Xml:                        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
//AN_Xml:                        return;
//AN_Xml:                    }
//AN_Xml:
//AN_Xml:                    if (res) {
//AN_Xml:                        // 递归调用实现续期
//AN_Xml:                        renewExpiration();
//AN_Xml:                    } else {
//AN_Xml:                        // 取消续期
//AN_Xml:                        cancelExpirationRenewal(null);
//AN_Xml:                    }
//AN_Xml:                });
//AN_Xml:            }
//AN_Xml:         // 延迟 internalLockLeaseTime/3(默认 10s,也就是 30/3) 再调用
//AN_Xml:        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
//AN_Xml:
//AN_Xml:        ee.setTimeout(task);
//AN_Xml:    }
//AN_Xml:

默认情况下,每过 10 秒,看门狗就会执行续期操作,将锁的超时时间设置为 30 秒。看门狗续期前也会先判断是否需要执行续期操作,需要才会执行续期,否则取消续期操作。

//AN_Xml:

Watch Dog 通过调用 renewExpirationAsync() 方法实现锁的异步续期:

//AN_Xml:
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
//AN_Xml:    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
//AN_Xml:            // 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s(默认)
//AN_Xml:            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
//AN_Xml:                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
//AN_Xml:                    "return 1; " +
//AN_Xml:                    "end; " +
//AN_Xml:                    "return 0;",
//AN_Xml:            Collections.singletonList(getRawName()),
//AN_Xml:            internalLockLeaseTime, getLockName(threadId));
//AN_Xml:}
//AN_Xml:

可以看出, renewExpirationAsync 方法其实是调用 Lua 脚本实现的续期,这样做主要是为了保证续期操作的原子性。

//AN_Xml:

我这里以 Redisson 的分布式可重入锁 RLock 为例来说明如何使用 Redisson 实现分布式锁:

//AN_Xml:
// 1.获取指定的分布式锁对象
//AN_Xml:RLock lock = redisson.getLock("lock");
//AN_Xml:// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
//AN_Xml:lock.lock();
//AN_Xml:// 3.执行业务
//AN_Xml:...
//AN_Xml:// 4.释放锁
//AN_Xml:lock.unlock();
//AN_Xml:

只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制。

//AN_Xml:
// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制
//AN_Xml:lock.lock(10, TimeUnit.SECONDS);
//AN_Xml:

如果使用 Redis 来实现分布式锁的话,还是比较推荐直接基于 Redisson 来做的。

//AN_Xml:

如何实现可重入锁?

//AN_Xml:

所谓可重入锁指的是在一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法即可重入 ,而无需重新获得锁。像 Java 中的 synchronizedReentrantLock 都属于可重入锁。

//AN_Xml:

不可重入的分布式锁基本可以满足绝大部分业务场景了,一些特殊的场景可能会需要使用可重入的分布式锁。

//AN_Xml:

可重入分布式锁的实现核心思路是线程在获取锁的时候判断是否为自己的锁,如果是的话,就不用再重新获取了。为此,我们可以为每个锁关联一个可重入计数器和一个占有它的线程。当可重入计数器大于 0 时,则锁被占有,需要判断占有该锁的线程和请求获取锁的线程是否为同一个。

//AN_Xml:

实际项目中,我们不需要自己手动实现,推荐使用我们上面提到的 Redisson ,其内置了多种类型的锁比如可重入锁(Reentrant Lock)、自旋锁(Spin Lock)、公平锁(Fair Lock)、多重锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)。

//AN_Xml:

//AN_Xml:

Redis 如何解决集群情况下分布式锁的可靠性?

//AN_Xml:

为了避免单点故障,生产环境下的 Redis 服务通常是集群化部署的。

//AN_Xml:

Redis 集群下,上面介绍到的分布式锁的实现会存在一些问题。由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。

//AN_Xml:

//AN_Xml:

针对这个问题,Redis 之父 antirez 设计了 Redlock 算法 来解决。

//AN_Xml:

//AN_Xml:

Redlock 算法的思想是让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。

//AN_Xml:

即使部分 Redis 节点出现问题,只要保证 Redis 集群中有半数以上的 Redis 节点可用,分布式锁服务就是正常的。

//AN_Xml:

Redlock 是直接操作 Redis 节点的,并不是通过 Redis 集群操作的,这样才可以避免 Redis 集群主从切换导致的锁丢失问题。

//AN_Xml:

Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患。《数据密集型应用系统设计》一书的作者 Martin Kleppmann 曾经专门发文(How to do distributed locking - Martin Kleppmann - 2016)怼过 Redlock,他认为这是一个很差的分布式锁实现。感兴趣的朋友可以看看Redis 锁从面试连环炮聊到神仙打架这篇文章,有详细介绍到 antirez 和 Martin Kleppmann 关于 Redlock 的激烈辩论。

//AN_Xml:

实际项目中不建议使用 Redlock 算法,成本和收益不成正比,可以考虑基于 Redis 主从复制+哨兵模式实现分布式锁。

//AN_Xml:

基于 ZooKeeper 实现分布式锁

//AN_Xml:

ZooKeeper 相比于 Redis 实现分布式锁,除了提供相对更高的可靠性之外,在功能层面还有一个非常有用的特性:Watch 机制。这个机制可以用来实现公平的分布式锁。不过,使用 ZooKeeper 实现的分布式锁在性能方面相对较差,因此如果对性能要求比较高的话,ZooKeeper 可能就不太适合了。

//AN_Xml:

如何基于 ZooKeeper 实现分布式锁?

//AN_Xml:

ZooKeeper 分布式锁是基于 临时顺序节点Watcher(事件监听器) 实现的。

//AN_Xml:

获取锁:

//AN_Xml:
    //AN_Xml:
  1. 首先我们要有一个持久节点/locks,客户端获取锁就是在locks下创建临时顺序节点。
  2. //AN_Xml:
  3. 假设客户端 1 创建了/locks/lock1节点,创建成功之后,会判断 lock1是否是 /locks 下最小的子节点。
  4. //AN_Xml:
  5. 如果 lock1是最小的子节点,则获取锁成功。否则,获取锁失败。
  6. //AN_Xml:
  7. 如果获取锁失败,则说明有其他的客户端已经成功获取锁。客户端 1 并不会不停地循环去尝试加锁,而是在前一个节点比如/locks/lock0上注册一个事件监听器。这个监听器的作用是当前一个节点释放锁之后通知客户端 1(避免无效自旋),这样客户端 1 就加锁成功了。
  8. //AN_Xml:
//AN_Xml:

释放锁:

//AN_Xml:
    //AN_Xml:
  1. 成功获取锁的客户端在执行完业务流程之后,会将对应的子节点删除。
  2. //AN_Xml:
  3. 成功获取锁的客户端在出现故障之后,对应的子节点由于是临时顺序节点,也会被自动删除,避免了锁无法被释放。
  4. //AN_Xml:
  5. 我们前面说的事件监听器其实监听的就是这个子节点删除事件,子节点删除就意味着锁被释放。
  6. //AN_Xml:
//AN_Xml:

//AN_Xml:

实际项目中,推荐使用 Curator 来实现 ZooKeeper 分布式锁。Curator 是 Netflix 公司开源的一套 ZooKeeper Java 客户端框架,相比于 ZooKeeper 自带的客户端 zookeeper 来说,Curator 的封装更加完善,各种 API 都可以比较方便地使用。

//AN_Xml:

Curator主要实现了下面四种锁:

//AN_Xml:
    //AN_Xml:
  • InterProcessMutex:分布式可重入排它锁
  • //AN_Xml:
  • InterProcessSemaphoreMutex:分布式不可重入排它锁
  • //AN_Xml:
  • InterProcessReadWriteLock:分布式读写锁
  • //AN_Xml:
  • InterProcessMultiLock:将多个锁作为单个实体管理的容器,获取锁的时候获取所有锁,释放锁也会释放所有锁资源(忽略释放失败的锁)。
  • //AN_Xml:
//AN_Xml:
CuratorFramework client = ZKUtils.getClient();
//AN_Xml:client.start();
//AN_Xml:// 分布式可重入排它锁
//AN_Xml:InterProcessLock lock1 = new InterProcessMutex(client, lockPath1);
//AN_Xml:// 分布式不可重入排它锁
//AN_Xml:InterProcessLock lock2 = new InterProcessSemaphoreMutex(client, lockPath2);
//AN_Xml:// 将多个锁作为一个整体
//AN_Xml:InterProcessMultiLock lock = new InterProcessMultiLock(Arrays.asList(lock1, lock2));
//AN_Xml:
//AN_Xml:if (!lock.acquire(10, TimeUnit.SECONDS)) {
//AN_Xml:   throw new IllegalStateException("不能获取多锁");
//AN_Xml:}
//AN_Xml:System.out.println("已获取多锁");
//AN_Xml:System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess());
//AN_Xml:System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess());
//AN_Xml:try {
//AN_Xml:    // 资源操作
//AN_Xml:    resource.use();
//AN_Xml:} finally {
//AN_Xml:    System.out.println("释放多个锁");
//AN_Xml:    lock.release();
//AN_Xml:}
//AN_Xml:System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess());
//AN_Xml:System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess());
//AN_Xml:client.close();
//AN_Xml:

为什么要用临时顺序节点?

//AN_Xml:

每个数据节点在 ZooKeeper 中被称为 znode,它是 ZooKeeper 中数据的最小单元。

//AN_Xml:

我们通常是将 znode 分为 4 大类:

//AN_Xml:
    //AN_Xml:
  • 持久(PERSISTENT)节点:一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。
  • //AN_Xml:
  • 临时(EPHEMERAL)节点:临时节点的生命周期是与 客户端会话(session) 绑定的,会话消失则节点消失 。并且,临时节点只能做叶子节点 ,不能创建子节点。
  • //AN_Xml:
  • 持久顺序(PERSISTENT_SEQUENTIAL)节点:除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如 /node1/app0000000001/node1/app0000000002
  • //AN_Xml:
  • 临时顺序(EPHEMERAL_SEQUENTIAL)节点:除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。
  • //AN_Xml:
//AN_Xml:

可以看出,临时节点相比持久节点,最主要的是对会话失效的情况处理不一样,临时节点会话消失则对应的节点消失。这样的话,如果客户端发生异常导致没来得及释放锁也没关系,会话失效节点自动被删除,不会发生死锁的问题。

//AN_Xml:

使用 Redis 实现分布式锁的时候,我们是通过过期时间来避免锁无法被释放导致死锁问题的,而 ZooKeeper 直接利用临时节点的特性即可。

//AN_Xml:

假设不使用顺序节点的话,所有尝试获取锁的客户端都会对持有锁的子节点加监听器。当该锁被释放之后,势必会造成所有尝试获取锁的客户端来争夺锁,这样对性能不友好。使用顺序节点之后,只需要监听前一个节点就好了,对性能更友好。

//AN_Xml:

为什么要设置对前一个节点的监听?

//AN_Xml:
//AN_Xml:

Watcher(事件监听器),是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。

//AN_Xml:
//AN_Xml:

同一时间段内,可能会有很多客户端同时获取锁,但只有一个可以获取成功。如果获取锁失败,则说明有其他的客户端已经成功获取锁。获取锁失败的客户端并不会不停地循环去尝试加锁,而是在前一个节点注册一个事件监听器。

//AN_Xml:

这个事件监听器的作用是:当前一个节点对应的客户端释放锁之后(也就是前一个节点被删除之后,监听的是删除事件),通知获取锁失败的客户端(唤醒等待的线程,Java 中的 wait/notifyAll ),让它尝试去获取锁,然后就成功获取锁了。

//AN_Xml:

如何实现可重入锁?

//AN_Xml:

这里以 Curator 的 InterProcessMutex 对可重入锁的实现来介绍(源码地址:InterProcessMutex.java)。

//AN_Xml:

当我们调用 InterProcessMutex#acquire方法获取锁的时候,会调用InterProcessMutex#internalLock方法。

//AN_Xml:
// 获取可重入互斥锁,直到获取成功为止
//AN_Xml:@Override
//AN_Xml:public void acquire() throws Exception {
//AN_Xml:  if (!internalLock(-1, null)) {
//AN_Xml:    throw new IOException("Lost connection while trying to acquire lock: " + basePath);
//AN_Xml:  }
//AN_Xml:}
//AN_Xml:

internalLock 方法会先获取当前请求锁的线程,然后从 threadData( ConcurrentMap<Thread, LockData> 类型)中获取当前线程对应的 lockDatalockData 包含锁的信息和加锁的次数,是实现可重入锁的关键。

//AN_Xml:

第一次获取锁的时候,lockDatanull。获取锁成功之后,会将当前线程和对应的 lockData 放到 threadData

//AN_Xml:
private boolean internalLock(long time, TimeUnit unit) throws Exception {
//AN_Xml:  // 获取当前请求锁的线程
//AN_Xml:  Thread currentThread = Thread.currentThread();
//AN_Xml:  // 拿对应的 lockData
//AN_Xml:  LockData lockData = threadData.get(currentThread);
//AN_Xml:  // 第一次获取锁的话,lockData 为 null
//AN_Xml:  if (lockData != null) {
//AN_Xml:    // 当前线程获取过一次锁之后
//AN_Xml:    // 因为当前线程的锁存在, lockCount 自增后返回,实现锁重入.
//AN_Xml:    lockData.lockCount.incrementAndGet();
//AN_Xml:    return true;
//AN_Xml:  }
//AN_Xml:  // 尝试获取锁
//AN_Xml:  String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
//AN_Xml:  if (lockPath != null) {
//AN_Xml:    LockData newLockData = new LockData(currentThread, lockPath);
//AN_Xml:     // 获取锁成功之后,将当前线程和对应的 lockData 放到 threadData 中
//AN_Xml:    threadData.put(currentThread, newLockData);
//AN_Xml:    return true;
//AN_Xml:  }
//AN_Xml:
//AN_Xml:  return false;
//AN_Xml:}
//AN_Xml:

LockDataInterProcessMutex中的一个静态内部类。

//AN_Xml:
private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();
//AN_Xml:
//AN_Xml:private static class LockData
//AN_Xml:{
//AN_Xml:    // 当前持有锁的线程
//AN_Xml:    final Thread owningThread;
//AN_Xml:    // 锁对应的子节点
//AN_Xml:    final String lockPath;
//AN_Xml:    // 加锁的次数
//AN_Xml:    final AtomicInteger lockCount = new AtomicInteger(1);
//AN_Xml:
//AN_Xml:    private LockData(Thread owningThread, String lockPath)
//AN_Xml:    {
//AN_Xml:      this.owningThread = owningThread;
//AN_Xml:      this.lockPath = lockPath;
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

如果已经获取过一次锁,后面再来获取锁的话,直接就会在 if (lockData != null) 这里被拦下了,然后就会执行lockData.lockCount.incrementAndGet(); 将加锁次数加 1。

//AN_Xml:

整个可重入锁的实现逻辑非常简单,直接在客户端判断当前线程有没有获取锁,有的话直接将加锁次数加 1 就可以了。

//AN_Xml:

总结

//AN_Xml:

在这篇文章中,我介绍了实现分布式锁的两种常见方式:RedisZooKeeper。至于具体选择 Redis 还是 ZooKeeper 来实现分布式锁,还是要根据业务的具体需求来决定。

//AN_Xml:
    //AN_Xml:
  • 如果对性能要求比较高的话,建议使用 Redis 实现分布式锁。推荐优先选择 Redisson 提供的现成分布式锁,而不是自己实现。实际项目中不建议使用 Redlock 算法,成本和收益不成正比,可以考虑基于 Redis 主从复制+哨兵模式实现分布式锁。
  • //AN_Xml:
  • 如果对可靠性要求比较高,建议使用 ZooKeeper 实现分布式锁,推荐基于 Curator 框架来实现。不过,现在很多项目都不会用到 ZooKeeper,如果单纯是因为分布式锁而引入 ZooKeeper 的话,那是不太可取的,不建议这样做,为了一个小小的功能增加了系统的复杂度。
  • //AN_Xml:
//AN_Xml:

需要注意的是,无论选择哪种方式实现分布式锁,包括 Redis、ZooKeeper 或 Etcd(本文没介绍,但也经常用来实现分布式锁),都无法保证 100% 的安全性,特别是在遇到进程垃圾回收(GC)、网络延迟等异常情况下。

//AN_Xml:

为了进一步提高系统的可靠性,建议引入一个兜底机制。例如,可以通过 版本号(Fencing Token)机制 来避免并发冲突。

//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 分布式锁入门介绍 //AN_Xml: https://javaguide.cn/distributed-system/distributed-lock.html //AN_Xml: https://javaguide.cn/distributed-system/distributed-lock.html //AN_Xml: 分布式锁入门介绍 //AN_Xml: 分布式锁基础概念详解,讲解为什么需要分布式锁、分布式锁的核心特性(互斥性、防死锁、可重入)、常见应用场景(秒杀、库存扣减)分析。 //AN_Xml: 分布式 //AN_Xml: Tue, 23 Aug 2022 10:53:21 GMT //AN_Xml: JavaGuide官方知识星球

//AN_Xml:

网上有很多分布式锁相关的文章,写了一个相对简洁易懂的版本,针对面试和工作应该够用了。

//AN_Xml:

这篇文章我们先介绍一下分布式锁的基本概念。

//AN_Xml:

为什么需要分布式锁?

//AN_Xml:

在多线程环境中,如果多个线程同时访问共享资源(例如商品库存、外卖订单),会发生数据竞争,可能会导致出现脏数据或者系统问题,威胁到程序的正常运行。

//AN_Xml:

举个例子,假设现在有 100 个用户参与某个限时秒杀活动,每位用户限购 1 件商品,且商品的数量只有 3 个。如果不对共享资源进行互斥访问,就可能出现以下情况:

//AN_Xml:
    //AN_Xml:
  • 线程 1、2、3 等多个线程同时进入抢购方法,每一个线程对应一个用户。
  • //AN_Xml:
  • 线程 1 查询用户已经抢购的数量,发现当前用户尚未抢购且商品库存还有 1 个,因此认为可以继续执行抢购流程。
  • //AN_Xml:
  • 线程 2 也执行查询用户已经抢购的数量,发现当前用户尚未抢购且商品库存还有 1 个,因此认为可以继续执行抢购流程。
  • //AN_Xml:
  • 线程 1 继续执行,将库存数量减少 1 个,然后返回成功。
  • //AN_Xml:
  • 线程 2 继续执行,将库存数量减少 1 个,然后返回成功。
  • //AN_Xml:
  • 此时就发生了超卖问题,导致商品被多卖了一份。
  • //AN_Xml:
//AN_Xml:

共享资源未互斥访问导致出现问题

//AN_Xml:

为了保证共享资源被安全地访问,我们需要使用互斥操作对共享资源进行保护,即同一时刻只允许一个线程访问共享资源,其他线程需要等待当前线程释放后才能访问。这样可以避免数据竞争和脏数据问题,保证程序的正确性和稳定性。

//AN_Xml:

如何才能实现共享资源的互斥访问呢? 锁是一个比较通用的解决方案,更准确点来说是悲观锁。

//AN_Xml:

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

//AN_Xml:

对于单机多线程来说,在 Java 中,我们通常使用 ReentrantLock 类、synchronized 关键字这类 JDK 自带的 本地锁 来控制一个 JVM 进程内的多个线程对本地共享资源的访问。

//AN_Xml:

下面是我对本地锁画的一张示意图。

//AN_Xml:

本地锁

//AN_Xml:

从图中可以看出,这些线程访问共享资源是互斥的,同一时刻只有一个线程可以获取到本地锁访问共享资源。

//AN_Xml:

分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,分布式锁 就诞生了。

//AN_Xml:

举个例子:系统的订单服务一共部署了 3 份,都对外提供服务。用户下订单之前需要检查库存,为了防止超卖,这里需要加锁以实现对检查库存操作的同步访问。由于订单服务位于不同的 JVM 进程中,本地锁在这种情况下就没办法正常工作了。我们需要用到分布式锁,这样的话,即使多个线程不在同一个 JVM 进程中也能获取到同一把锁,进而实现共享资源的互斥访问。

//AN_Xml:

下面是我对分布式锁画的一张示意图。

//AN_Xml:

分布式锁

//AN_Xml:

从图中可以看出,这些独立的进程中的线程访问共享资源是互斥的,同一时刻只有一个线程可以获取到分布式锁访问共享资源。

//AN_Xml:

分布式锁应该具备哪些条件?

//AN_Xml:

一个最基本的分布式锁需要满足:

//AN_Xml:
    //AN_Xml:
  • 互斥:任意一个时刻,锁只能被一个线程持有。
  • //AN_Xml:
  • 高可用:锁服务是高可用的,当一个锁服务出现问题,能够自动切换到另外一个锁服务。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。这一般是通过超时机制实现的。
  • //AN_Xml:
  • 可重入:一个节点获取了锁之后,还可以再次获取锁。
  • //AN_Xml:
//AN_Xml:

除了上面这三个基本条件之外,一个好的分布式锁还需要满足下面这些条件:

//AN_Xml:
    //AN_Xml:
  • 高性能:获取和释放锁的操作应该快速完成,并且不应该对整个系统的性能造成过大影响。
  • //AN_Xml:
  • 非阻塞:如果获取不到锁,不能无限期等待,避免对系统正常运行造成影响。
  • //AN_Xml:
//AN_Xml:

分布式锁的常见实现方式有哪些?

//AN_Xml:

常见分布式锁实现方案如下:

//AN_Xml:
    //AN_Xml:
  • 基于关系型数据库比如 MySQL 实现分布式锁。
  • //AN_Xml:
  • 基于分布式协调服务 ZooKeeper 实现分布式锁。
  • //AN_Xml:
  • 基于分布式键值存储系统比如 Redis 、Etcd 实现分布式锁。
  • //AN_Xml:
//AN_Xml:

关系型数据库的方式一般是通过唯一索引或者排他锁实现。不过,一般不会使用这种方式,问题太多比如性能太差、不具备锁失效机制。

//AN_Xml:

基于 ZooKeeper 或者 Redis 实现分布式锁这两种实现方式要用的更多一些,我专门写了一篇文章来详细介绍这两种方案:分布式锁常见实现方案总结

//AN_Xml:

总结

//AN_Xml:

这篇文章我们主要介绍了:

//AN_Xml:
    //AN_Xml:
  • 分布式锁的用途:分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。
  • //AN_Xml:
  • 分布式锁的应该具备的条件:互斥、高可用、可重入、高性能、非阻塞。
  • //AN_Xml:
  • 分布式锁的常见实现方式:关系型数据库比如 MySQL、分布式协调服务 ZooKeeper、分布式键值存储系统比如 Redis 、Etcd 。
  • //AN_Xml:
//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: CDN工作原理详解 //AN_Xml: https://javaguide.cn/high-performance/cdn.html //AN_Xml: https://javaguide.cn/high-performance/cdn.html //AN_Xml: CDN工作原理详解 //AN_Xml: 本文详解 CDN(内容分发网络)的核心原理,涵盖 GSLB 全局负载均衡调度机制、CDN 缓存策略(预热/回源/刷新)、命中率与回源率优化,以及 Referer 防盗链与时间戳防盗链等安全机制,帮助你全面掌握 CDN 加速技术。 //AN_Xml: 高性能 //AN_Xml: Sun, 21 Aug 2022 05:04:07 GMT //AN_Xml: JavaGuide官方知识星球

//AN_Xml:

什么是 CDN ?

//AN_Xml:

CDN 全称是 Content Delivery Network/Content Distribution Network,翻译过的意思是 内容分发网络

//AN_Xml:

我们可以将内容分发网络拆开来看:

//AN_Xml:
    //AN_Xml:
  • 内容:指的是静态资源,包括图片、视频、文档、JS、CSS、HTML 等。
  • //AN_Xml:
  • 分发网络:指的是将这些静态资源分发到位于多个不同地理位置机房中的服务器上,从而实现就近访问——例如北京的用户直接访问北京机房的数据。
  • //AN_Xml:
//AN_Xml:

简单来说,CDN 就是将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻源站服务器以及带宽的负担。

//AN_Xml:

类似于京东建立的庞大仓储运输体系,京东物流在全国拥有非常多的仓库,仓储网络几乎覆盖全国所有区县。这样的话,用户下单的第一时间,商品就从距离用户最近的仓库直接发往对应的配送站,再由京东小哥送到你家。

//AN_Xml:

京东仓配系统

//AN_Xml:

你可以将 CDN 看作是服务上一层的特殊缓存服务,分布在全国各地,主要用来处理静态资源的请求。

//AN_Xml:

CDN 简易示意图

//AN_Xml:

我们经常拿全站加速和内容分发网络做对比,不要把两者搞混了!全站加速(不同云服务商叫法不同,腾讯云叫 ECDN、阿里云叫 DCDN)既可以加速静态资源又可以加速动态资源,而内容分发网络(CDN) 主要针对的是 静态资源

//AN_Xml:

阿里云文档:https://help.aliyun.com/document_detail/64836.html

//AN_Xml:

绝大部分公司都会在项目开发中使用 CDN 服务,但很少会有自建 CDN 服务的公司。基于成本、稳定性和易用性考虑,建议直接选择专业的云厂商(比如阿里云、腾讯云、华为云、青云)或者 CDN 厂商(比如网宿、蓝汛)提供的开箱即用的 CDN 服务。

//AN_Xml:

为什么不直接将服务部署在多个不同的地方?

//AN_Xml:

很多朋友可能要问了:既然是就近访问,为什么不直接将服务部署在多个不同的地方呢?

//AN_Xml:

这涉及到静态资源与动态请求的架构分离问题:

//AN_Xml:
    //AN_Xml:
  1. 成本问题:多地部署完整服务需要部署多套应用、数据库、中间件,成本极高;而 CDN 只需存储静态资源,成本可控。
  2. //AN_Xml:
  3. 资源特性不同:静态资源(图片、JS、CSS)具有体积大、访问频繁、内容不变的特点,非常适合缓存分发;动态请求需要实时计算,必须回源处理。
  4. //AN_Xml:
  5. 系统资源消耗:如果用应用服务器直接处理静态资源请求,会大量占用 CPU、内存和带宽资源,可能影响核心业务的正常运行。
  6. //AN_Xml:
  7. 专业优化:CDN 针对静态资源传输进行了大量优化(如智能压缩、协议优化、边缘计算),这些能力是普通应用服务器不具备的。
  8. //AN_Xml:
//AN_Xml:
//AN_Xml:

注意:同一个服务在多个不同地方部署多份(比如同城灾备、异地灾备、同城多活、异地多活)是为了实现系统的高可用,而不是就近访问。

//AN_Xml:
//AN_Xml:

CDN 工作原理是什么?

//AN_Xml:

理解 CDN 的工作原理,需要搞懂以下三个核心问题:

//AN_Xml:
    //AN_Xml:
  1. 静态资源是如何被缓存到 CDN 节点中的?
  2. //AN_Xml:
  3. 如何找到最合适的 CDN 节点?
  4. //AN_Xml:
  5. 如何防止静态资源被盗用?
  6. //AN_Xml:
//AN_Xml:

静态资源是如何被缓存到 CDN 节点中的?

//AN_Xml:

CDN 缓存静态资源的方式主要有两种:预热回源

//AN_Xml:
    //AN_Xml:
  • //AN_Xml:

    预热(Prefetch):主动将源站的资源推送到 CDN 节点中。这样用户首次请求资源时可以直接从 CDN 节点获取,无需回源,适用于大促活动、热点内容发布等场景。

    //AN_Xml:
  • //AN_Xml:
  • //AN_Xml:

    回源(Origin Pull):当 CDN 节点上没有用户请求的资源或该资源的缓存已过期时,CDN 节点需要从源站获取最新的资源内容。

    //AN_Xml:
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

注意:当用户请求触发回源时,该请求的响应速度会比未使用 CDN 还慢,因为相比于直接访问源站,多了一层 CDN 节点的调用流程。因此,提高缓存命中率是 CDN 优化的关键目标。

//AN_Xml:
//AN_Xml:

CDN 缓存的完整生命周期如下图所示:

//AN_Xml:

CDN 缓存的完整生命周期

//AN_Xml:

如果资源有更新,可以对其进行刷新操作,删除 CDN 节点上缓存的旧资源,并强制 CDN 节点在下次请求时回源获取最新资源。

//AN_Xml:

几乎所有云厂商提供的 CDN 服务都具备缓存的刷新和预热功能(下图是阿里云 CDN 服务提供的相应功能):

//AN_Xml:

CDN 缓存的刷新和预热

//AN_Xml:

命中率回源率是衡量 CDN 服务质量的两个核心指标:

//AN_Xml:
    //AN_Xml:
  • 命中率:用户请求直接由 CDN 节点响应的比例,越高越好
  • //AN_Xml:
  • 回源率:用户请求需要回源站获取的比例,越低越好
  • //AN_Xml:
//AN_Xml:

如何找到最合适的 CDN 节点?

//AN_Xml:

GSLB(Global Server Load Balance,全局负载均衡) 是 CDN 的大脑,负责多个 CDN 节点之间的协调调度,最常用的实现方式是基于 DNS 的 GSLB

//AN_Xml:

CDN 请求的完整调度流程如下图所示:

//AN_Xml:

详细流程说明

//AN_Xml:
    //AN_Xml:
  1. 用户浏览器向本地 DNS 服务器发送域名解析请求。
  2. //AN_Xml:
  3. 本地 DNS 向权威 DNS 查询,发现该域名配置了 CNAME(Canonical Name)别名记录,指向 CDN 服务商的域名。
  4. //AN_Xml:
  5. 本地 DNS 继续向 CDN 的 GSLB 发起解析请求。
  6. //AN_Xml:
  7. GSLB 根据用户 IP 地址、CDN 节点状态(负载、性能、响应时间、带宽) 等指标,综合判断并返回最优 CDN 节点的 IP 地址。
  8. //AN_Xml:
  9. 用户浏览器直接向该 CDN 节点(边缘服务器)发起资源请求。
  10. //AN_Xml:
  11. CDN 节点检查本地缓存,若命中则直接返回;若未命中或已过期,则回源获取后再返回给用户。
  12. //AN_Xml:
//AN_Xml:
//AN_Xml:

补充说明:上图做了一定简化。实际上,GSLB 内部可以看作是 CDN 专用 DNS 服务器负载均衡系统的组合。CDN 专用 DNS 服务器会返回负载均衡系统的 IP 地址,浏览器通过该 IP 请求负载均衡系统,进而找到对应的 CDN 节点。

//AN_Xml:
//AN_Xml:

如何防止资源被盗刷?

//AN_Xml:

如果静态资源被其他用户或网站非法盗刷,将会产生大量额外的带宽费用。常见的防盗链机制有以下几种:

//AN_Xml:

| 防盗链机制 | 原理 | 安全强度 | 实现成本 | 绕过难度 |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Web 实时消息推送详解 //AN_Xml: https://javaguide.cn/system-design/web-real-time-message-push.html //AN_Xml: https://javaguide.cn/system-design/web-real-time-message-push.html //AN_Xml: Web 实时消息推送详解 //AN_Xml: 消息推送通常是指网站的运营工作等人员,通过某种工具对用户当前网页或移动设备 APP 进行的主动消息推送。 //AN_Xml: 系统设计 //AN_Xml: Fri, 19 Aug 2022 03:13:03 GMT //AN_Xml: //AN_Xml:

原文地址:https://juejin.cn/post/7122014462181113887,JavaGuide 对本文进行了完善总结。

//AN_Xml: //AN_Xml:

我有一个朋友做了一个小破站,现在要实现一个站内信 Web 消息推送的功能,对,就是下图这个小红点,一个很常用的功能。

//AN_Xml:

站内信 Web 消息推送

//AN_Xml:

不过他还没想好用什么方式做,这里我帮他整理了一下几种方案,并简单做了实现。

//AN_Xml:

什么是消息推送?

//AN_Xml:

推送的场景比较多,比如有人关注我的公众号,这时我就会收到一条推送消息,以此来吸引我点击打开应用。

//AN_Xml:

消息推送通常是指网站的运营工作等人员,通过某种工具对用户当前网页或移动设备 APP 进行的主动消息推送。

//AN_Xml:

消息推送一般又分为 Web 端消息推送和移动端消息推送。

//AN_Xml:

移动端消息推送示例:

//AN_Xml:

移动端消息推送示例

//AN_Xml:

Web 端消息推送示例:

//AN_Xml:

Web 端消息推送示例

//AN_Xml:

在具体实现之前,咱们再来分析一下前边的需求,其实功能很简单,只要触发某个事件(主动分享了资源或者后台主动推送消息),Web 页面的通知小红点就会实时的 +1 就可以了。

//AN_Xml:

通常在服务端会有若干张消息推送表,用来记录用户触发不同事件所推送不同类型的消息,前端主动查询(拉)或者被动接收(推)用户所有未读的消息数。

//AN_Xml:

消息推送表

//AN_Xml:

消息推送无非是推(push)和拉(pull)两种形式,下边我们逐个了解下。

//AN_Xml:

消息推送常见方案

//AN_Xml:

短轮询

//AN_Xml:

轮询(polling) 应该是实现消息推送方案中最简单的一种,这里我们暂且将轮询分为短轮询和长轮询。

//AN_Xml:

短轮询很好理解,指定的时间间隔,由浏览器向服务器发出 HTTP 请求,服务器实时返回未读消息数据给客户端,浏览器再做渲染显示。

//AN_Xml:

一个简单的 JS 定时器就可以搞定,每秒钟请求一次未读消息数接口,返回的数据展示即可。

//AN_Xml:
setInterval(() => {
//AN_Xml:  // 方法请求
//AN_Xml:  messageCount().then((res) => {
//AN_Xml:    if (res.code === 200) {
//AN_Xml:      this.messageCount = res.data;
//AN_Xml:    }
//AN_Xml:  });
//AN_Xml:}, 1000);
//AN_Xml:

效果还是可以的,短轮询实现固然简单,缺点也是显而易见,由于推送数据并不会频繁变更,无论后端此时是否有新的消息产生,客户端都会进行请求,势必会对服务端造成很大压力,浪费带宽和服务器资源。

//AN_Xml:

长轮询

//AN_Xml:

长轮询是对上边短轮询的一种改进版本,在尽可能减少对服务器资源浪费的同时,保证消息的相对实时性。长轮询在中间件中应用的很广泛,比如 Nacos 和 Apollo 配置中心,消息队列 Kafka、RocketMQ 中都有用到长轮询。

//AN_Xml:

Nacos 配置中心交互模型是 push 还是 pull?一文中我详细介绍过 Nacos 长轮询的实现原理,感兴趣的小伙伴可以瞅瞅。

//AN_Xml:

长轮询其实原理跟轮询差不多,都是采用轮询的方式。不过,如果服务端的数据没有发生变更,会 一直 hold 住请求,直到服务端的数据发生变化,或者等待一定时间超时才会返回。返回后,客户端又会立即再次发起下一次长轮询。

//AN_Xml:

这次我使用 Apollo 配置中心实现长轮询的方式,应用了一个类DeferredResult,它是在 Servlet3.0 后经过 Spring 封装提供的一种异步请求机制,直意就是延迟结果。

//AN_Xml:

长轮询示意图

//AN_Xml:

DeferredResult可以允许容器线程快速释放占用的资源,不阻塞请求线程,以此接受更多的请求提升系统的吞吐量,然后启动异步工作线程处理真正的业务逻辑,处理完成调用DeferredResult.setResult(200)提交响应结果。

//AN_Xml:

下边我们用长轮询来实现消息推送。

//AN_Xml:

因为一个 ID 可能会被多个长轮询请求监听,所以我采用了 Guava 包提供的Multimap结构存放长轮询,一个 key 可以对应多个 value。一旦监听到 key 发生变化,对应的所有长轮询都会响应。前端得到非请求超时的状态码,知晓数据变更,主动查询未读消息数接口,更新页面数据。

//AN_Xml:
@Controller
//AN_Xml:@RequestMapping("/polling")
//AN_Xml:public class PollingController {
//AN_Xml:
//AN_Xml:    // 存放监听某个Id的长轮询集合
//AN_Xml:    // 线程同步结构
//AN_Xml:    public static Multimap<String, DeferredResult<String>> watchRequests = Multimaps.synchronizedMultimap(HashMultimap.create());
//AN_Xml:
//AN_Xml:    /**
//AN_Xml:     * 设置监听
//AN_Xml:     */
//AN_Xml:    @GetMapping(path = "watch/{id}")
//AN_Xml:    @ResponseBody
//AN_Xml:    public DeferredResult<String> watch(@PathVariable String id) {
//AN_Xml:        // 延迟对象设置超时时间
//AN_Xml:        DeferredResult<String> deferredResult = new DeferredResult<>(TIME_OUT);
//AN_Xml:        // 异步请求完成时移除 key,防止内存溢出
//AN_Xml:        deferredResult.onCompletion(() -> {
//AN_Xml:            watchRequests.remove(id, deferredResult);
//AN_Xml:        });
//AN_Xml:        // 注册长轮询请求
//AN_Xml:        watchRequests.put(id, deferredResult);
//AN_Xml:        return deferredResult;
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    /**
//AN_Xml:     * 变更数据
//AN_Xml:     */
//AN_Xml:    @GetMapping(path = "publish/{id}")
//AN_Xml:    @ResponseBody
//AN_Xml:    public String publish(@PathVariable String id) {
//AN_Xml:        // 数据变更 取出监听ID的所有长轮询请求,并一一响应处理
//AN_Xml:        if (watchRequests.containsKey(id)) {
//AN_Xml:            Collection<DeferredResult<String>> deferredResults = watchRequests.get(id);
//AN_Xml:            for (DeferredResult<String> deferredResult : deferredResults) {
//AN_Xml:                deferredResult.setResult("我更新了" + new Date());
//AN_Xml:            }
//AN_Xml:        }
//AN_Xml:        return "success";
//AN_Xml:    }
//AN_Xml:

当请求超过设置的超时时间,会抛出AsyncRequestTimeoutException异常,这里直接用@ControllerAdvice全局捕获统一返回即可,前端获取约定好的状态码后再次发起长轮询请求,如此往复调用。

//AN_Xml:
@ControllerAdvice
//AN_Xml:public class AsyncRequestTimeoutHandler {
//AN_Xml:
//AN_Xml:    @ResponseStatus(HttpStatus.NOT_MODIFIED)
//AN_Xml:    @ResponseBody
//AN_Xml:    @ExceptionHandler(AsyncRequestTimeoutException.class)
//AN_Xml:    public String asyncRequestTimeoutHandler(AsyncRequestTimeoutException e) {
//AN_Xml:        System.out.println("异步请求超时");
//AN_Xml:        return "304";
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

我们来测试一下,首先页面发起长轮询请求/polling/watch/10086监听消息更变,请求被挂起,不变更数据直至超时,再次发起了长轮询请求;紧接着手动变更数据/polling/publish/10086,长轮询得到响应,前端处理业务逻辑完成后再次发起请求,如此循环往复。

//AN_Xml:

长轮询相比于短轮询在性能上提升了很多,但依然会产生较多的请求,这是它的一点不完美的地方。

//AN_Xml:

iframe 流

//AN_Xml:

iframe 流就是在页面中插入一个隐藏的<iframe>标签,通过在src中请求消息数量 API 接口,由此在服务端和客户端之间创建一条长连接,服务端持续向iframe传输数据。

//AN_Xml:

传输的数据通常是 HTML、或是内嵌的 JavaScript 脚本,来达到实时更新页面的效果。

//AN_Xml:

iframe 流示意图

//AN_Xml:

这种方式实现简单,前端只要一个<iframe>标签搞定了

//AN_Xml:
<iframe src="/iframe/message" style="display:none"></iframe>
//AN_Xml:

服务端直接组装 HTML、JS 脚本数据向 response 写入就行了

//AN_Xml:
@Controller
//AN_Xml:@RequestMapping("/iframe")
//AN_Xml:public class IframeController {
//AN_Xml:    @GetMapping(path = "message")
//AN_Xml:    public void message(HttpServletResponse response) throws IOException, InterruptedException {
//AN_Xml:        while (true) {
//AN_Xml:            response.setHeader("Pragma", "no-cache");
//AN_Xml:            response.setDateHeader("Expires", 0);
//AN_Xml:            response.setHeader("Cache-Control", "no-cache,no-store");
//AN_Xml:            response.setStatus(HttpServletResponse.SC_OK);
//AN_Xml:            response.getWriter().print(" <script type=\"text/javascript\">\n" +
//AN_Xml:                    "parent.document.getElementById('clock').innerHTML = \"" + count.get() + "\";" +
//AN_Xml:                    "parent.document.getElementById('count').innerHTML = \"" + count.get() + "\";" +
//AN_Xml:                    "<AN_Scri>");
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

iframe 流的服务器开销很大,而且 IE、Chrome 等浏览器一直会处于 loading 状态,图标会不停旋转,简直是强迫症杀手。

//AN_Xml:

iframe 流效果

//AN_Xml:

iframe 流非常不友好,强烈不推荐。

//AN_Xml:

SSE (推荐)

//AN_Xml:

很多人可能不知道,服务端向客户端推送消息,其实除了可以用WebSocket这种耳熟能详的机制外,还有一种服务器发送事件(Server-Sent Events),简称 SSE。这是一种服务器端到客户端(浏览器)的单向消息推送。

//AN_Xml:

大名鼎鼎的 ChatGPT 就是采用的 SSE。对于需要长时间等待响应的对话场景,ChatGPT 采用了一种巧妙的策略:它会将已经计算出的数据“推送”给用户,并利用 SSE 技术在计算过程中持续返回数据。这样做的好处是可以避免用户因等待时间过长而选择关闭页面。

//AN_Xml:

ChatGPT 使用 SSE 实现对话

//AN_Xml:

SSE 基于 HTTP 协议的,我们知道一般意义上的 HTTP 协议是无法做到服务端主动向客户端推送消息的,但 SSE 是个例外,它变换了一种思路。

//AN_Xml:

SSE 图解

//AN_Xml:

SSE 在服务器和客户端之间打开一个单向通道,服务端响应的不再是一次性的数据包而是text/event-stream类型的数据流信息,在有数据变更时从服务器流式传输到客户端。

//AN_Xml:

整体的实现思路有点类似于在线视频播放,视频流会连续不断的推送到浏览器,你也可以理解成,客户端在完成一次用时很长(网络不畅)的下载。

//AN_Xml:

SSE 示意图

//AN_Xml:

SSE 与 WebSocket 作用相似,都可以建立服务端与浏览器之间的通信,实现服务端向客户端推送消息,但还是有些许不同:

//AN_Xml:
    //AN_Xml:
  • SSE 是基于 HTTP 协议的,它们不需要特殊的协议或服务器实现即可工作;WebSocket 需单独服务器来处理协议。
  • //AN_Xml:
  • SSE 单向通信,只能由服务端向客户端单向通信;WebSocket 全双工通信,即通信的双方可以同时发送和接受信息。
  • //AN_Xml:
  • SSE 实现简单开发成本低,无需引入其他组件;WebSocket 传输数据需做二次解析,开发门槛高一些。
  • //AN_Xml:
  • SSE 默认支持断线重连;WebSocket 则需要自己实现。
  • //AN_Xml:
  • SSE 只能传送文本消息,二进制数据需要经过编码后传送;WebSocket 默认支持传送二进制数据。
  • //AN_Xml:
//AN_Xml:

SSE 与 WebSocket 该如何选择?

//AN_Xml:
//AN_Xml:

技术并没有好坏之分,只有哪个更合适。

//AN_Xml:
//AN_Xml:

SSE 好像一直不被大家所熟知,一部分原因是出现了 WebSocket,这个提供了更丰富的协议来执行双向、全双工通信。对于游戏、即时通信以及需要双向近乎实时更新的场景,拥有双向通道更具吸引力。

//AN_Xml:

但是,在某些情况下,不需要从客户端发送数据。而你只需要一些服务器操作的更新。比如:站内信、未读消息数、状态更新、股票行情、监控数量等场景,SSE 不管是从实现的难易和成本上都更加有优势。此外,SSE 具有 WebSocket 在设计上缺乏的多种功能,例如:自动重新连接、事件 ID 和发送任意事件的能力。

//AN_Xml:

前端只需进行一次 HTTP 请求,带上唯一 ID,打开事件流,监听服务端推送的事件就可以了

//AN_Xml:
<script>
//AN_Xml:    let source = null;
//AN_Xml:    let userId = 7777
//AN_Xml:    if (window.EventSource) {
//AN_Xml:        // 建立连接
//AN_Xml:        source = new EventSource('http://localhost:7777/sse/sub/'+userId);
//AN_Xml:        setMessageInnerHTML("连接用户=" + userId);
//AN_Xml:        /**
//AN_Xml:         * 连接一旦建立,就会触发open事件
//AN_Xml:         * 另一种写法:source.onopen = function (event) {}
//AN_Xml:         */
//AN_Xml:        source.addEventListener('open', function (e) {
//AN_Xml:            setMessageInnerHTML("建立连接。。。");
//AN_Xml:        }, false);
//AN_Xml:        /**
//AN_Xml:         * 客户端收到服务器发来的数据
//AN_Xml:         * 另一种写法:source.onmessage = function (event) {}
//AN_Xml:         */
//AN_Xml:        source.addEventListener('message', function (e) {
//AN_Xml:            setMessageInnerHTML(e.data);
//AN_Xml:        });
//AN_Xml:    } else {
//AN_Xml:        setMessageInnerHTML("你的浏览器不支持SSE");
//AN_Xml:    }
//AN_Xml:</script>
//AN_Xml:

服务端的实现更简单,创建一个SseEmitter对象放入sseEmitterMap进行管理

//AN_Xml:
private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
//AN_Xml:
//AN_Xml:/**
//AN_Xml: * 创建连接
//AN_Xml: */
//AN_Xml:public static SseEmitter connect(String userId) {
//AN_Xml:    try {
//AN_Xml:        // 设置超时时间,0表示不过期。默认30秒
//AN_Xml:        SseEmitter sseEmitter = new SseEmitter(0L);
//AN_Xml:        // 注册回调
//AN_Xml:        sseEmitter.onCompletion(completionCallBack(userId));
//AN_Xml:        sseEmitter.onError(errorCallBack(userId));
//AN_Xml:        sseEmitter.onTimeout(timeoutCallBack(userId));
//AN_Xml:        sseEmitterMap.put(userId, sseEmitter);
//AN_Xml:        count.getAndIncrement();
//AN_Xml:        return sseEmitter;
//AN_Xml:    } catch (Exception e) {
//AN_Xml:        log.info("创建新的sse连接异常,当前用户:{}", userId);
//AN_Xml:    }
//AN_Xml:    return null;
//AN_Xml:}
//AN_Xml:
//AN_Xml:/**
//AN_Xml: * 给指定用户发送消息
//AN_Xml: */
//AN_Xml:public static void sendMessage(String userId, String message) {
//AN_Xml:
//AN_Xml:    if (sseEmitterMap.containsKey(userId)) {
//AN_Xml:        try {
//AN_Xml:            sseEmitterMap.get(userId).send(message);
//AN_Xml:        } catch (IOException e) {
//AN_Xml:            log.error("用户[{}]推送异常:{}", userId, e.getMessage());
//AN_Xml:            removeUser(userId);
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

注意: SSE 不支持 IE 浏览器,对其他主流浏览器兼容性做的还不错。

//AN_Xml:

SSE 兼容性

//AN_Xml:

Websocket

//AN_Xml:

Websocket 应该是大家都比较熟悉的一种实现消息推送的方式,上边我们在讲 SSE 的时候也和 Websocket 进行过比较。

//AN_Xml:

这是一种在 TCP 连接上进行全双工通信的协议,建立客户端和服务器之间的通信渠道。浏览器和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

//AN_Xml:

Websocket 示意图

//AN_Xml:

WebSocket 的工作过程可以分为以下几个步骤:

//AN_Xml:
    //AN_Xml:
  1. 客户端向服务器发送一个 HTTP 请求,请求头中包含 Upgrade: websocketSec-WebSocket-Key 等字段,表示要求升级协议为 WebSocket;
  2. //AN_Xml:
  3. 服务器收到这个请求后,会进行升级协议的操作,如果支持 WebSocket,它将回复一个 HTTP 101 状态码,响应头中包含 ,Connection: UpgradeSec-WebSocket-Accept: xxx 等字段、表示成功升级到 WebSocket 协议。
  4. //AN_Xml:
  5. 客户端和服务器之间建立了一个 WebSocket 连接,可以进行双向的数据传输。数据以帧(frames)的形式进行传送,而不是传统的 HTTP 请求和响应。WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息。
  6. //AN_Xml:
  7. 客户端或服务器可以主动发送一个关闭帧,表示要断开连接。另一方收到后,也会回复一个关闭帧,然后双方关闭 TCP 连接。
  8. //AN_Xml:
//AN_Xml:

另外,建立 WebSocket 连接之后,通过心跳机制来保持 WebSocket 连接的稳定性和活跃性。

//AN_Xml:

SpringBoot 整合 WebSocket,先引入 WebSocket 相关的工具包,和 SSE 相比有额外的开发成本。

//AN_Xml:
<!-- 引入websocket -->
//AN_Xml:<dependency>
//AN_Xml:    <groupId>org.springframework.boot</groupId>
//AN_Xml:    <artifactId>spring-boot-starter-websocket</artifactId>
//AN_Xml:</dependency>
//AN_Xml:

服务端使用@ServerEndpoint注解标注当前类为一个 WebSocket 服务器,客户端可以通过ws://localhost:7777/webSocket/10086来连接到 WebSocket 服务器端。

//AN_Xml:
@Component
//AN_Xml:@Slf4j
//AN_Xml:@ServerEndpoint("/websocket/{userId}")
//AN_Xml:public class WebSocketServer {
//AN_Xml:    //与某个客户端的连接会话,需要通过它来给客户端发送数据
//AN_Xml:    private Session session;
//AN_Xml:    private static final CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>();
//AN_Xml:    // 用来存在线连接数
//AN_Xml:    private static final Map<String, Session> sessionPool = new HashMap<String, Session>();
//AN_Xml:    /**
//AN_Xml:     * 链接成功调用的方法
//AN_Xml:     */
//AN_Xml:    @OnOpen
//AN_Xml:    public void onOpen(Session session, @PathParam(value = "userId") String userId) {
//AN_Xml:        try {
//AN_Xml:            this.session = session;
//AN_Xml:            webSockets.add(this);
//AN_Xml:            sessionPool.put(userId, session);
//AN_Xml:            log.info("websocket消息: 有新的连接,总数为:" + webSockets.size());
//AN_Xml:        } catch (Exception e) {
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:    /**
//AN_Xml:     * 收到客户端消息后调用的方法
//AN_Xml:     */
//AN_Xml:    @OnMessage
//AN_Xml:    public void onMessage(String message) {
//AN_Xml:        log.info("websocket消息: 收到客户端消息:" + message);
//AN_Xml:    }
//AN_Xml:    /**
//AN_Xml:     * 此为单点消息
//AN_Xml:     */
//AN_Xml:    public void sendOneMessage(String userId, String message) {
//AN_Xml:        Session session = sessionPool.get(userId);
//AN_Xml:        if (session != null && session.isOpen()) {
//AN_Xml:            try {
//AN_Xml:                log.info("websocket消: 单点消息:" + message);
//AN_Xml:                session.getAsyncRemote().sendText(message);
//AN_Xml:            } catch (Exception e) {
//AN_Xml:                e.printStackTrace();
//AN_Xml:            }
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

服务端还需要注入ServerEndpointerExporter,这个 Bean 就会自动注册使用了@ServerEndpoint注解的 WebSocket 服务器。

//AN_Xml:
@Configuration
//AN_Xml:public class WebSocketConfiguration {
//AN_Xml:
//AN_Xml:    /**
//AN_Xml:     * 用于注册使用了 @ServerEndpoint 注解的 WebSocket 服务器
//AN_Xml:     */
//AN_Xml:    @Bean
//AN_Xml:    public ServerEndpointExporter serverEndpointExporter() {
//AN_Xml:        return new ServerEndpointExporter();
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

前端初始化打开 WebSocket 连接,并监听连接状态,接收服务端数据或向服务端发送数据。

//AN_Xml:
<script>
//AN_Xml:    var ws = new WebSocket('ws://localhost:7777/webSocket/10086');
//AN_Xml:    // 获取连接状态
//AN_Xml:    console.log('ws连接状态:' + ws.readyState);
//AN_Xml:    //监听是否连接成功
//AN_Xml:    ws.onopen = function () {
//AN_Xml:        console.log('ws连接状态:' + ws.readyState);
//AN_Xml:        //连接成功则发送一个数据
//AN_Xml:        ws.send('test1');
//AN_Xml:    }
//AN_Xml:    // 接听服务器发回的信息并处理展示
//AN_Xml:    ws.onmessage = function (data) {
//AN_Xml:        console.log('接收到来自服务器的消息:');
//AN_Xml:        console.log(data);
//AN_Xml:        //完成通信后关闭WebSocket连接
//AN_Xml:        ws.close();
//AN_Xml:    }
//AN_Xml:    // 监听连接关闭事件
//AN_Xml:    ws.onclose = function () {
//AN_Xml:        // 监听整个过程中websocket的状态
//AN_Xml:        console.log('ws连接状态:' + ws.readyState);
//AN_Xml:    }
//AN_Xml:    // 监听并处理error事件
//AN_Xml:    ws.onerror = function (error) {
//AN_Xml:        console.log(error);
//AN_Xml:    }
//AN_Xml:    function sendMessage() {
//AN_Xml:        var content = $("#message").val();
//AN_Xml:        $.ajax({
//AN_Xml:            url: '/socket/publish?userId=10086&message=' + content,
//AN_Xml:            type: 'GET',
//AN_Xml:            data: { "id": "7777", "content": content },
//AN_Xml:            success: function (data) {
//AN_Xml:                console.log(data)
//AN_Xml:            }
//AN_Xml:        })
//AN_Xml:    }
//AN_Xml:</script>
//AN_Xml:

页面初始化建立 WebSocket 连接,之后就可以进行双向通信了,效果还不错。

//AN_Xml:

//AN_Xml:

MQTT

//AN_Xml:

什么是 MQTT 协议?

//AN_Xml:

MQTT (Message Queue Telemetry Transport)是一种基于发布/订阅(publish/subscribe)模式的轻量级通讯协议,通过订阅相应的主题来获取消息,是物联网(Internet of Thing)中的一个标准传输协议。

//AN_Xml:

该协议将消息的发布者(publisher)与订阅者(subscriber)进行分离,因此可以在不可靠的网络环境中,为远程连接的设备提供可靠的消息服务,使用方式与传统的 MQ 有点类似。

//AN_Xml:

MQTT 协议示例

//AN_Xml:

TCP 协议位于传输层,MQTT 协议位于应用层,MQTT 协议构建于 TCP/IP 协议上,也就是说只要支持 TCP/IP 协议栈的地方,都可以使用 MQTT 协议。

//AN_Xml:

为什么要用 MQTT 协议?

//AN_Xml:

MQTT 协议为什么在物联网(IOT)中如此受偏爱?而不是其它协议,比如我们更为熟悉的 HTTP 协议呢?

//AN_Xml:
    //AN_Xml:
  • 首先 HTTP 协议它是一种同步协议,客户端请求后需要等待服务器的响应。而在物联网(IOT)环境中,设备会很受制于环境的影响,比如带宽低、网络延迟高、网络通信不稳定等,显然异步消息协议更为适合 IOT 应用程序。
  • //AN_Xml:
  • HTTP 是单向的,如果要获取消息客户端必须发起连接,而在物联网(IOT)应用程序中,设备或传感器往往都是客户端,这意味着它们无法被动地接收来自网络的命令。
  • //AN_Xml:
  • 通常需要将一条命令或者消息,发送到网络上的所有设备上。HTTP 要实现这样的功能不但很困难,而且成本极高。
  • //AN_Xml:
//AN_Xml:

具体的 MQTT 协议介绍和实践,这里我就不再赘述了,大家可以参考我之前的两篇文章,里边写的也都很详细了。

//AN_Xml: //AN_Xml:

总结

//AN_Xml:
//AN_Xml:

以下内容为 JavaGuide 补充

//AN_Xml:
//AN_Xml:

| | 介绍 | 优点 | 缺点 |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Java 语法糖详解 //AN_Xml: https://javaguide.cn/java/basis/syntactic-sugar.html //AN_Xml: https://javaguide.cn/java/basis/syntactic-sugar.html //AN_Xml: Java 语法糖详解 //AN_Xml: 深入剖析Java语法糖原理:详解自动装箱拆箱、泛型擦除、增强for、可变参数、枚举、Lambda等语法糖的编译期实现机制,避免使用误区。 //AN_Xml: Java //AN_Xml: Thu, 18 Aug 2022 12:45:01 GMT //AN_Xml: //AN_Xml:

作者:Hollis

//AN_Xml:

原文:https://mp.weixin.qq.com/s/o4XdEMq1DL-nBS-f8Za5Aw

//AN_Xml: //AN_Xml:

语法糖是大厂 Java 面试常问的一个知识点。

//AN_Xml:

本文从 Java 编译原理角度,深入字节码及 class 文件,抽丝剥茧,了解 Java 中的语法糖原理及用法,帮助大家在学会如何使用 Java 语法糖的同时,了解这些语法糖背后的原理。

//AN_Xml:

什么是语法糖?

//AN_Xml:

语法糖(Syntactic Sugar) 也称糖衣语法,是英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。

//AN_Xml:

//AN_Xml:
//AN_Xml:

有意思的是,在编程领域,除了语法糖,还有语法盐和语法糖精的说法,篇幅有限这里不做扩展了。

//AN_Xml:
//AN_Xml:

我们所熟知的编程语言中几乎都有语法糖。作者认为,语法糖的多少是评判一个语言够不够牛逼的标准之一。很多人说 Java 是一个“低糖语言”,其实从 Java 7 开始 Java 语言层面上一直在添加各种糖,主要是在“Project Coin”项目下研发。尽管现在 Java 有人还是认为现在的 Java 是低糖,未来还会持续向着“高糖”的方向发展。

//AN_Xml:

Java 中有哪些常见的语法糖?

//AN_Xml:

前面提到过,语法糖的存在主要是方便开发人员使用。但其实, Java 虚拟机并不支持这些语法糖。这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖。

//AN_Xml:

说到编译,大家肯定都知道,Java 语言中,javac命令可以将后缀名为.java的源文件编译为后缀名为.class的可以运行于 Java 虚拟机的字节码。如果你去看com.sun.tools.javac.main.JavaCompiler的源码,你会发现在compile()中有一个步骤就是调用desugar(),这个方法就是负责解语法糖的实现的。

//AN_Xml:

Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。本文主要来分析下这些语法糖背后的原理。一步一步剥去糖衣,看看其本质。

//AN_Xml:

我们这里会用到反编译,你可以通过 Decompilers online 对 Class 文件进行在线反编译。

//AN_Xml:

switch 支持 String 与枚举

//AN_Xml:

前面提到过,从 Java 7 开始,Java 语言中的语法糖在逐渐丰富,其中一个比较重要的就是 Java 7 中switch开始支持String

//AN_Xml:

在开始之前先科普下,Java 中的switch自身原本就支持基本类型。比如intchar等。对于int类型,直接进行数值的比较。对于char类型则是比较其 ascii 码。所以,对于编译器来说,switch中其实只能使用整型,任何类型的比较都要转换成整型。比如byteshortchar(ascii 码是整型)以及int

//AN_Xml:

那么接下来看下switchString的支持,有以下代码:

//AN_Xml:
public class switchDemoString {
//AN_Xml:    public static void main(String[] args) {
//AN_Xml:        String str = "world";
//AN_Xml:        switch (str) {
//AN_Xml:        case "hello":
//AN_Xml:            System.out.println("hello");
//AN_Xml:            break;
//AN_Xml:        case "world":
//AN_Xml:            System.out.println("world");
//AN_Xml:            break;
//AN_Xml:        default:
//AN_Xml:            break;
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

反编译后内容如下:

//AN_Xml:
public class switchDemoString
//AN_Xml:{
//AN_Xml:    public switchDemoString()
//AN_Xml:    {
//AN_Xml:    }
//AN_Xml:    public static void main(String args[])
//AN_Xml:    {
//AN_Xml:        String str = "world";
//AN_Xml:        String s;
//AN_Xml:        switch((s = str).hashCode())
//AN_Xml:        {
//AN_Xml:        default:
//AN_Xml:            break;
//AN_Xml:        case 99162322:
//AN_Xml:            if(s.equals("hello"))
//AN_Xml:                System.out.println("hello");
//AN_Xml:            break;
//AN_Xml:        case 113318802:
//AN_Xml:            if(s.equals("world"))
//AN_Xml:                System.out.println("world");
//AN_Xml:            break;
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

看到这个代码,你知道原来 字符串的 switch 是通过equals()hashCode()方法来实现的。 还好hashCode()方法返回的是int,而不是long

//AN_Xml:

仔细看下可以发现,进行switch的实际是哈希值,然后通过使用equals方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行 switch 或者使用纯整数常量,但这也不是很差。

//AN_Xml:

泛型

//AN_Xml:

我们都知道,很多语言都是支持泛型的,但是很多人不知道的是,不同的编译器对于泛型的处理方式是不同的,通常情况下,一个编译器处理泛型有两种方式:Code specializationCode sharing。C++和 C#是使用Code specialization的处理机制,而 Java 使用的是Code sharing的机制。

//AN_Xml:
//AN_Xml:

Code sharing 方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。

//AN_Xml:
//AN_Xml:

也就是说,对于 Java 虚拟机来说,他根本不认识Map<String, String> map这样的语法。需要在编译阶段通过类型擦除的方式进行解语法糖。

//AN_Xml:

类型擦除的主要过程如下:1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。 2.移除所有的类型参数。

//AN_Xml:

以下代码:

//AN_Xml:
Map<String, String> map = new HashMap<String, String>();
//AN_Xml:map.put("name", "hollis");
//AN_Xml:map.put("wechat", "Hollis");
//AN_Xml:map.put("blog", "www.hollischuang.com");
//AN_Xml:

解语法糖之后会变成:

//AN_Xml:
Map map = new HashMap();
//AN_Xml:map.put("name", "hollis");
//AN_Xml:map.put("wechat", "Hollis");
//AN_Xml:map.put("blog", "www.hollischuang.com");
//AN_Xml:

以下代码:

//AN_Xml:
public static <A extends Comparable<A>> A max(Collection<A> xs) {
//AN_Xml:    Iterator<A> xi = xs.iterator();
//AN_Xml:    A w = xi.next();
//AN_Xml:    while (xi.hasNext()) {
//AN_Xml:        A x = xi.next();
//AN_Xml:        if (w.compareTo(x) < 0)
//AN_Xml:            w = x;
//AN_Xml:    }
//AN_Xml:    return w;
//AN_Xml:}
//AN_Xml:

类型擦除后会变成:

//AN_Xml:
 public static Comparable max(Collection xs){
//AN_Xml:    Iterator xi = xs.iterator();
//AN_Xml:    Comparable w = (Comparable)xi.next();
//AN_Xml:    while(xi.hasNext())
//AN_Xml:    {
//AN_Xml:        Comparable x = (Comparable)xi.next();
//AN_Xml:        if(w.compareTo(x) < 0)
//AN_Xml:            w = x;
//AN_Xml:    }
//AN_Xml:    return w;
//AN_Xml:}
//AN_Xml:

虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的Class类对象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class

//AN_Xml:

自动装箱与拆箱

//AN_Xml:

自动装箱就是 Java 自动将原始类型值转换成对应的对象,比如将 int 的变量转换成 Integer 对象,这个过程叫做装箱,反之将 Integer 对象转换成 int 类型值,这个过程叫做拆箱。因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱。原始类型 byte, short, char, int, long, float, double 和 boolean 对应的封装类为 Byte, Short, Character, Integer, Long, Float, Double, Boolean。

//AN_Xml:

先来看个自动装箱的代码:

//AN_Xml:
 public static void main(String[] args) {
//AN_Xml:    int i = 10;
//AN_Xml:    Integer n = i;
//AN_Xml:}
//AN_Xml:

反编译后代码如下:

//AN_Xml:
public static void main(String args[])
//AN_Xml:{
//AN_Xml:    int i = 10;
//AN_Xml:    Integer n = Integer.valueOf(i);
//AN_Xml:}
//AN_Xml:

再来看个自动拆箱的代码:

//AN_Xml:
public static void main(String[] args) {
//AN_Xml:
//AN_Xml:    Integer i = 10;
//AN_Xml:    int n = i;
//AN_Xml:}
//AN_Xml:

反编译后代码如下:

//AN_Xml:
public static void main(String args[])
//AN_Xml:{
//AN_Xml:    Integer i = Integer.valueOf(10);
//AN_Xml:    int n = i.intValue();
//AN_Xml:}
//AN_Xml:

从反编译得到内容可以看出,在装箱的时候自动调用的是IntegervalueOf(int)方法。而在拆箱的时候自动调用的是IntegerintValue方法。

//AN_Xml:

所以,装箱过程是通过调用包装器的 valueOf 方法实现的,而拆箱过程是通过调用包装器的 xxxValue 方法实现的。

//AN_Xml:

可变长参数

//AN_Xml:

可变参数(variable arguments)是在 Java 1.5 中引入的一个特性。它允许一个方法把任意数量的值作为参数。

//AN_Xml:

看下以下可变参数代码,其中 print 方法接收可变参数:

//AN_Xml:
public static void main(String[] args)
//AN_Xml:    {
//AN_Xml:        print("Holis", "公众号:Hollis", "博客:www.hollischuang.com", "QQ:907607222");
//AN_Xml:    }
//AN_Xml:
//AN_Xml:public static void print(String... strs)
//AN_Xml:{
//AN_Xml:    for (int i = 0; i < strs.length; i++)
//AN_Xml:    {
//AN_Xml:        System.out.println(strs[i]);
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

反编译后代码:

//AN_Xml:
 public static void main(String args[])
//AN_Xml:{
//AN_Xml:    print(new String[] {
//AN_Xml:        "Holis", "\u516C\u4F17\u53F7:Hollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com", "QQ\uFF1A907607222"
//AN_Xml:    });
//AN_Xml:}
//AN_Xml:
//AN_Xml:public static transient void print(String strs[])
//AN_Xml:{
//AN_Xml:    for(int i = 0; i < strs.length; i++)
//AN_Xml:        System.out.println(strs[i]);
//AN_Xml:
//AN_Xml:}
//AN_Xml:

从反编译后代码可以看出,可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。(注:transient 仅在修饰成员变量时有意义,此处 “修饰方法” 是由于在 javassist 中使用相同数值分别表示 transient 以及 vararg,见 此处。)

//AN_Xml:

枚举

//AN_Xml:

Java SE5 提供了一种新的类型-Java 的枚举类型,关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。

//AN_Xml:

要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是enum吗?答案很明显不是,enum就和class一样,只是一个关键字,他并不是一个类,那么枚举是由什么类维护的呢,我们简单的写一个枚举:

//AN_Xml:
public enum t {
//AN_Xml:    SPRING,SUMMER;
//AN_Xml:}
//AN_Xml:

然后我们使用反编译,看看这段代码到底是怎么实现的,反编译后代码内容如下:

//AN_Xml:
//Java编译器会自动将枚举名处理为合法类名(首字母大写): t -> T
//AN_Xml:public final class T extends Enum
//AN_Xml:{
//AN_Xml:    private T(String s, int i)
//AN_Xml:    {
//AN_Xml:        super(s, i);
//AN_Xml:    }
//AN_Xml:    public static T[] values()
//AN_Xml:    {
//AN_Xml:        T at[];
//AN_Xml:        int i;
//AN_Xml:        T at1[];
//AN_Xml:        System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
//AN_Xml:        return at1;
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    public static T valueOf(String s)
//AN_Xml:    {
//AN_Xml:        return (T)Enum.valueOf(demo/T, s);
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    public static final T SPRING;
//AN_Xml:    public static final T SUMMER;
//AN_Xml:    private static final T ENUM$VALUES[];
//AN_Xml:    static
//AN_Xml:    {
//AN_Xml:        SPRING = new T("SPRING", 0);
//AN_Xml:        SUMMER = new T("SUMMER", 1);
//AN_Xml:        ENUM$VALUES = (new T[] {
//AN_Xml:            SPRING, SUMMER
//AN_Xml:        });
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

通过反编译后代码我们可以看到,public final class T extends Enum,说明,该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。

//AN_Xml:

当我们使用enum来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。

//AN_Xml:

内部类

//AN_Xml:

内部类又称为嵌套类,可以把内部类理解为外部类的一个普通成员。

//AN_Xml:

内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念,outer.java里面定义了一个内部类inner,一旦编译成功,就会生成两个完全不同的.class文件了,分别是outer.classouter$inner.class。所以内部类的名字完全可以和它的外部类名字相同。

//AN_Xml:
public class OuterClass {
//AN_Xml:    private String userName;
//AN_Xml:
//AN_Xml:    public String getUserName() {
//AN_Xml:        return userName;
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    public void setUserName(String userName) {
//AN_Xml:        this.userName = userName;
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    public static void main(String[] args) {
//AN_Xml:
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    class InnerClass{
//AN_Xml:        private String name;
//AN_Xml:
//AN_Xml:        public String getName() {
//AN_Xml:            return name;
//AN_Xml:        }
//AN_Xml:
//AN_Xml:        public void setName(String name) {
//AN_Xml:            this.name = name;
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

以上代码编译后会生成两个 class 文件:OuterClass$InnerClass.classOuterClass.class 。当我们尝试对OuterClass.class文件进行反编译的时候,命令行会打印以下内容:Parsing OuterClass.class...Parsing inner class OuterClass$InnerClass.class... Generating OuterClass.jad 。他会把两个文件全部进行反编译,然后一起生成一个OuterClass.jad文件。文件内容如下:

//AN_Xml:
public class OuterClass
//AN_Xml:{
//AN_Xml:    class InnerClass
//AN_Xml:    {
//AN_Xml:        public String getName()
//AN_Xml:        {
//AN_Xml:            return name;
//AN_Xml:        }
//AN_Xml:        public void setName(String name)
//AN_Xml:        {
//AN_Xml:            this.name = name;
//AN_Xml:        }
//AN_Xml:        private String name;
//AN_Xml:        final OuterClass this$0;
//AN_Xml:
//AN_Xml:        InnerClass()
//AN_Xml:        {
//AN_Xml:            this.this$0 = OuterClass.this;
//AN_Xml:            super();
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    public OuterClass()
//AN_Xml:    {
//AN_Xml:    }
//AN_Xml:    public String getUserName()
//AN_Xml:    {
//AN_Xml:        return userName;
//AN_Xml:    }
//AN_Xml:    public void setUserName(String userName){
//AN_Xml:        this.userName = userName;
//AN_Xml:    }
//AN_Xml:    public static void main(String args1[])
//AN_Xml:    {
//AN_Xml:    }
//AN_Xml:    private String userName;
//AN_Xml:}
//AN_Xml:

为什么内部类可以使用外部类的 private 属性

//AN_Xml:

我们在 InnerClass 中增加一个方法,打印外部类的 userName 属性

//AN_Xml:
//省略其他属性
//AN_Xml:public class OuterClass {
//AN_Xml:    private String userName;
//AN_Xml:    ......
//AN_Xml:    class InnerClass{
//AN_Xml:    ......
//AN_Xml:        public void printOut(){
//AN_Xml:            System.out.println("Username from OuterClass:"+userName);
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:
//AN_Xml:// 此时,使用javap -p命令对OuterClass反编译结果:
//AN_Xml:public classOuterClass {
//AN_Xml:    private String userName;
//AN_Xml:    ......
//AN_Xml:    static String access$000(OuterClass);
//AN_Xml:}
//AN_Xml:// 此时,InnerClass的反编译结果:
//AN_Xml:class OuterClass$InnerClass {
//AN_Xml:    final OuterClass this$0;
//AN_Xml:    ......
//AN_Xml:    public void printOut();
//AN_Xml:}
//AN_Xml:

实际上,在编译完成之后,inner 实例内部会有指向 outer 实例的引用this$0,但是简单的outer.name是无法访问 private 属性的。从反编译的结果可以看到,outer 中会有一个桥方法static String access$000(OuterClass),恰好返回 String 类型,即 userName 属性。正是通过这个方法实现内部类访问外部类私有属性。所以反编译后的printOut()方法大致如下:

//AN_Xml:
public void printOut() {
//AN_Xml:    System.out.println("Username from OuterClass:" + OuterClass.access$000(this.this$0));
//AN_Xml:}
//AN_Xml:

补充:

//AN_Xml:
    //AN_Xml:
  1. 匿名内部类、局部内部类、静态内部类也是通过桥方法来获取 private 属性。
  2. //AN_Xml:
  3. 静态内部类没有this$0的引用
  4. //AN_Xml:
  5. 匿名内部类、局部内部类通过复制使用局部变量,该变量初始化之后就不能被修改。以下是一个案例:
  6. //AN_Xml:
//AN_Xml:
public class OuterClass {
//AN_Xml:    private String userName;
//AN_Xml:
//AN_Xml:    public void test(){
//AN_Xml:        //这里i初始化为1后就不能再被修改
//AN_Xml:        int i=1;
//AN_Xml:        class Inner{
//AN_Xml:            public void printName(){
//AN_Xml:                System.out.println(userName);
//AN_Xml:                System.out.println(i);
//AN_Xml:            }
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

反编译后:

//AN_Xml:
//javap命令反编译Inner的结果
//AN_Xml://i被复制进内部类,且为final
//AN_Xml:class OuterClass$1Inner {
//AN_Xml:  final int val$i;
//AN_Xml:  final OuterClass this$0;
//AN_Xml:  OuterClass$1Inner();
//AN_Xml:  public void printName();
//AN_Xml:}
//AN_Xml:

条件编译

//AN_Xml:

—般情况下,程序中的每一行代码都要参加编译。但有时候出于对程序代码优化的考虑,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。

//AN_Xml:

如在 C 或 CPP 中,可以通过预处理语句来实现条件编译。其实在 Java 中也可实现条件编译。我们先来看一段代码:

//AN_Xml:
public class ConditionalCompilation {
//AN_Xml:    public static void main(String[] args) {
//AN_Xml:        final boolean DEBUG = true;
//AN_Xml:        if(DEBUG) {
//AN_Xml:            System.out.println("Hello, DEBUG!");
//AN_Xml:        }
//AN_Xml:
//AN_Xml:        final boolean ONLINE = false;
//AN_Xml:
//AN_Xml:        if(ONLINE){
//AN_Xml:            System.out.println("Hello, ONLINE!");
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

反编译后代码如下:

//AN_Xml:
public class ConditionalCompilation
//AN_Xml:{
//AN_Xml:
//AN_Xml:    public ConditionalCompilation()
//AN_Xml:    {
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    public static void main(String args[])
//AN_Xml:    {
//AN_Xml:        boolean DEBUG = true;
//AN_Xml:        System.out.println("Hello, DEBUG!");
//AN_Xml:        boolean ONLINE = false;
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

首先,我们发现,在反编译后的代码中没有System.out.println("Hello, ONLINE!");,这其实就是条件编译。当if(ONLINE)为 false 的时候,编译器就没有对其内的代码进行编译。

//AN_Xml:

所以,Java 语法的条件编译,是通过判断条件为常量的 if 语句实现的。其原理也是 Java 语言的语法糖。根据 if 判断条件的真假,编译器直接把分支为 false 的代码块消除。通过该方式实现的条件编译,必须在方法体内实现,而无法在整个 Java 类的结构或者类的属性上进行条件编译,这与 C/C++的条件编译相比,确实更有局限性。在 Java 语言设计之初并没有引入条件编译的功能,虽有局限,但是总比没有更强。

//AN_Xml:

断言

//AN_Xml:

在 Java 中,assert关键字是从 JAVA SE 1.4 引入的,为了避免和老版本的 Java 代码中使用了assert关键字导致错误,Java 在执行的时候默认是不启动断言检查的(这个时候,所有的断言语句都将忽略!),如果要开启断言检查,则需要用开关-enableassertions-ea来开启。

//AN_Xml:

看一段包含断言的代码:

//AN_Xml:
public class AssertTest {
//AN_Xml:    public static void main(String args[]) {
//AN_Xml:        int a = 1;
//AN_Xml:        int b = 1;
//AN_Xml:        assert a == b;
//AN_Xml:        System.out.println("公众号:Hollis");
//AN_Xml:        assert a != b : "Hollis";
//AN_Xml:        System.out.println("博客:www.hollischuang.com");
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

反编译后代码如下:

//AN_Xml:
public class AssertTest {
//AN_Xml:   public AssertTest()
//AN_Xml:    {
//AN_Xml:    }
//AN_Xml:    public static void main(String args[])
//AN_Xml:{
//AN_Xml:    int a = 1;
//AN_Xml:    int b = 1;
//AN_Xml:    if(!$assertionsDisabled && a != b)
//AN_Xml:        throw new AssertionError();
//AN_Xml:    System.out.println("\u516C\u4F17\u53F7\uFF1AHollis");
//AN_Xml:    if(!$assertionsDisabled && a == b)
//AN_Xml:    {
//AN_Xml:        throw new AssertionError("Hollis");
//AN_Xml:    } else
//AN_Xml:    {
//AN_Xml:        System.out.println("\u535A\u5BA2\uFF1Awww.hollischuang.com");
//AN_Xml:        return;
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:
//AN_Xml:static final boolean $assertionsDisabled = !com/hollis/suguar/AssertTest.desiredAssertionStatus();
//AN_Xml:
//AN_Xml:}
//AN_Xml:

很明显,反编译之后的代码要比我们自己的代码复杂的多。所以,使用了 assert 这个语法糖我们节省了很多代码。其实断言的底层实现就是 if 语言,如果断言结果为 true,则什么都不做,程序继续执行,如果断言结果为 false,则程序抛出 AssertError 来打断程序的执行。-enableassertions会设置$assertionsDisabled 字段的值。

//AN_Xml:

数值字面量

//AN_Xml:

在 java 7 中,数值字面量,不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,目的就是方便阅读。

//AN_Xml:

比如:

//AN_Xml:
public class Test {
//AN_Xml:    public static void main(String... args) {
//AN_Xml:        int i = 10_000;
//AN_Xml:        System.out.println(i);
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

反编译后:

//AN_Xml:
public class Test
//AN_Xml:{
//AN_Xml:  public static void main(String[] args)
//AN_Xml:  {
//AN_Xml:    int i = 10000;
//AN_Xml:    System.out.println(i);
//AN_Xml:  }
//AN_Xml:}
//AN_Xml:

反编译后就是把_删除了。也就是说 编译器并不认识在数字字面量中的_,需要在编译阶段把他去掉。

//AN_Xml:

for-each

//AN_Xml:

增强 for 循环(for-each)相信大家都不陌生,日常开发经常会用到的,他会比 for 循环要少写很多代码,那么这个语法糖背后是如何实现的呢?

//AN_Xml:
public static void main(String... args) {
//AN_Xml:    String[] strs = {"Hollis", "公众号:Hollis", "博客:www.hollischuang.com"};
//AN_Xml:    for (String s : strs) {
//AN_Xml:        System.out.println(s);
//AN_Xml:    }
//AN_Xml:    List<String> strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com");
//AN_Xml:    for (String s : strList) {
//AN_Xml:        System.out.println(s);
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

反编译后代码如下:

//AN_Xml:
public static transient void main(String args[])
//AN_Xml:{
//AN_Xml:    String strs[] = {
//AN_Xml:        "Hollis", "\u516C\u4F17\u53F7\uFF1AHollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com"
//AN_Xml:    };
//AN_Xml:    String args1[] = strs;
//AN_Xml:    int i = args1.length;
//AN_Xml:    for(int j = 0; j < i; j++)
//AN_Xml:    {
//AN_Xml:        String s = args1[j];
//AN_Xml:        System.out.println(s);
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    List strList = ImmutableList.of("Hollis", "\u516C\u4F17\u53F7\uFF1AHollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com");
//AN_Xml:    String s;
//AN_Xml:    for(Iterator iterator = strList.iterator(); iterator.hasNext(); System.out.println(s))
//AN_Xml:        s = (String)iterator.next();
//AN_Xml:
//AN_Xml:}
//AN_Xml:

代码很简单,for-each 的实现原理其实就是使用了普通的 for 循环和迭代器。

//AN_Xml:

try-with-resource

//AN_Xml:

Java 里,对于文件操作 IO 流、数据库连接等开销非常昂贵的资源,用完之后必须及时通过 close 方法将其关闭,否则资源会一直处于打开状态,可能会导致内存泄露等问题。

//AN_Xml:

关闭资源的常用方式就是在finally块里是释放,即调用close方法。比如,我们经常会写这样的代码:

//AN_Xml:
public static void main(String[] args) {
//AN_Xml:    BufferedReader br = null;
//AN_Xml:    try {
//AN_Xml:        String line;
//AN_Xml:        br = new BufferedReader(new FileReader("d:\\hollischuang.xml"));
//AN_Xml:        while ((line = br.readLine()) != null) {
//AN_Xml:            System.out.println(line);
//AN_Xml:        }
//AN_Xml:    } catch (IOException e) {
//AN_Xml:        // handle exception
//AN_Xml:    } finally {
//AN_Xml:        try {
//AN_Xml:            if (br != null) {
//AN_Xml:                br.close();
//AN_Xml:            }
//AN_Xml:        } catch (IOException ex) {
//AN_Xml:            // handle exception
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

从 Java 7 开始,jdk 提供了一种更好的方式关闭资源,使用try-with-resources语句,改写一下上面的代码,效果如下:

//AN_Xml:
public static void main(String... args) {
//AN_Xml:    try (BufferedReader br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"))) {
//AN_Xml:        String line;
//AN_Xml:        while ((line = br.readLine()) != null) {
//AN_Xml:            System.out.println(line);
//AN_Xml:        }
//AN_Xml:    } catch (IOException e) {
//AN_Xml:        // handle exception
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

看,这简直是一大福音啊,虽然我之前一般使用IOUtils去关闭流,并不会使用在finally中写很多代码的方式,但是这种新的语法糖看上去好像优雅很多呢。看下他的背后:

//AN_Xml:
public static transient void main(String args[])
//AN_Xml:    {
//AN_Xml:        BufferedReader br;
//AN_Xml:        Throwable throwable;
//AN_Xml:        br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"));
//AN_Xml:        throwable = null;
//AN_Xml:        String line;
//AN_Xml:        try
//AN_Xml:        {
//AN_Xml:            while((line = br.readLine()) != null)
//AN_Xml:                System.out.println(line);
//AN_Xml:        }
//AN_Xml:        catch(Throwable throwable2)
//AN_Xml:        {
//AN_Xml:            throwable = throwable2;
//AN_Xml:            throw throwable2;
//AN_Xml:        }
//AN_Xml:        finally
//AN_Xml:        {
//AN_Xml:            if(br != null)
//AN_Xml:                if(throwable != null)
//AN_Xml:                    try
//AN_Xml:                    {
//AN_Xml:                        br.close();
//AN_Xml:                    }
//AN_Xml:                    catch(Throwable throwable1)
//AN_Xml:                    {
//AN_Xml:                        throwable.addSuppressed(throwable1);
//AN_Xml:                    }
//AN_Xml:                else
//AN_Xml:                    br.close();
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

其实背后的原理也很简单,那些我们没有做的关闭资源的操作,编译器都帮我们做了。所以,再次印证了,语法糖的作用就是方便程序员的使用,但最终还是要转成编译器认识的语言。

//AN_Xml:

Lambda 表达式

//AN_Xml:

关于 lambda 表达式,有人可能会有质疑,因为网上有人说他并不是语法糖。其实我想纠正下这个说法。Lambda 表达式不是匿名内部类的语法糖,但是他也是一个语法糖。实现方式其实是依赖了几个 JVM 底层提供的 lambda 相关 api。

//AN_Xml:

先来看一个简单的 lambda 表达式。遍历一个 list:

//AN_Xml:
public static void main(String... args) {
//AN_Xml:    List<String> strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com");
//AN_Xml:
//AN_Xml:    strList.forEach( s -> { System.out.println(s); } );
//AN_Xml:}
//AN_Xml:

为啥说他并不是内部类的语法糖呢,前面讲内部类我们说过,内部类在编译之后会有两个 class 文件,但是,包含 lambda 表达式的类编译后只有一个文件。

//AN_Xml:

反编译后代码如下:

//AN_Xml:
public static /* varargs */ void main(String ... args) {
//AN_Xml:    ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"\u516c\u4f17\u53f7\uff1aHollis", (Object)"\u535a\u5ba2\uff1awww.hollischuang.com");
//AN_Xml:    strList.forEach((Consumer<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)());
//AN_Xml:}
//AN_Xml:
//AN_Xml:private static /* synthetic */ void lambda$main$0(String s) {
//AN_Xml:    System.out.println(s);
//AN_Xml:}
//AN_Xml:

可以看到,在forEach方法中,其实是调用了java.lang.invoke.LambdaMetafactory#metafactory方法,该方法的第四个参数 implMethod 指定了方法实现。可以看到这里其实是调用了一个lambda$main$0方法进行了输出。

//AN_Xml:

再来看一个稍微复杂一点的,先对 List 进行过滤,然后再输出:

//AN_Xml:
public static void main(String... args) {
//AN_Xml:    List<String> strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com");
//AN_Xml:
//AN_Xml:    List HollisList = strList.stream().filter(string -> string.contains("Hollis")).collect(Collectors.toList());
//AN_Xml:
//AN_Xml:    HollisList.forEach( s -> { System.out.println(s); } );
//AN_Xml:}
//AN_Xml:

反编译后代码如下:

//AN_Xml:
public static /* varargs */ void main(String ... args) {
//AN_Xml:    ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"\u516c\u4f17\u53f7\uff1aHollis", (Object)"\u535a\u5ba2\uff1awww.hollischuang.com");
//AN_Xml:    List<Object> HollisList = strList.stream().filter((Predicate<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)Z, lambda$main$0(java.lang.String ), (Ljava/lang/String;)Z)()).collect(Collectors.toList());
//AN_Xml:    HollisList.forEach((Consumer<Object>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$1(java.lang.Object ), (Ljava/lang/Object;)V)());
//AN_Xml:}
//AN_Xml:
//AN_Xml:private static /* synthetic */ void lambda$main$1(Object s) {
//AN_Xml:    System.out.println(s);
//AN_Xml:}
//AN_Xml:
//AN_Xml:private static /* synthetic */ boolean lambda$main$0(String string) {
//AN_Xml:    return string.contains("Hollis");
//AN_Xml:}
//AN_Xml:

两个 lambda 表达式分别调用了lambda$main$1lambda$main$0两个方法。

//AN_Xml:

所以,lambda 表达式的实现其实是依赖了一些底层的 api,在编译阶段,编译器会把 lambda 表达式进行解糖,转换成调用内部 api 的方式。

//AN_Xml:

可能遇到的坑

//AN_Xml:

泛型

//AN_Xml:

一、当泛型遇到重载

//AN_Xml:
public class GenericTypes {
//AN_Xml:
//AN_Xml:    public static void method(List<String> list) {
//AN_Xml:        System.out.println("invoke method(List<String> list)");
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    public static void method(List<Integer> list) {
//AN_Xml:        System.out.println("invoke method(List<Integer> list)");
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是List<String>另一个是List<Integer> ,但是,这段代码是编译通不过的。因为我们前面讲过,参数List<Integer>List<String>编译之后都被擦除了,变成了一样的原生类型 List,擦除动作导致这两个方法的特征签名变得一模一样。

//AN_Xml:

二、当泛型遇到 catch

//AN_Xml:

泛型的类型参数不能用在 Java 异常处理的 catch 语句中。因为异常处理是由 JVM 在运行时刻来进行的。由于类型信息被擦除,JVM 是无法区分两个异常类型MyException<String>MyException<Integer>

//AN_Xml:

三、当泛型内包含静态变量

//AN_Xml:
public class StaticTest{
//AN_Xml:    public static void main(String[] args){
//AN_Xml:        GT<Integer> gti = new GT<Integer>();
//AN_Xml:        gti.var=1;
//AN_Xml:        GT<String> gts = new GT<String>();
//AN_Xml:        gts.var=2;
//AN_Xml:        System.out.println(gti.var);
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:class GT<T>{
//AN_Xml:    public static int var=0;
//AN_Xml:    public void nothing(T x){}
//AN_Xml:}
//AN_Xml:

以上代码输出结果为:2!

//AN_Xml:

有些同学可能会误认为泛型类是不同的类,对应不同的字节码,其实
//AN_Xml:由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的静态变量是共享的。上面例子里的GT<Integer>.varGT<String>.var其实是一个变量。

//AN_Xml:

自动装箱与拆箱

//AN_Xml:

对象相等比较

//AN_Xml:
public static void main(String[] args) {
//AN_Xml:    Integer a = 1000;
//AN_Xml:    Integer b = 1000;
//AN_Xml:    Integer c = 100;
//AN_Xml:    Integer d = 100;
//AN_Xml:    System.out.println("a == b is " + (a == b));
//AN_Xml:    System.out.println(("c == d is " + (c == d)));
//AN_Xml:}
//AN_Xml:

输出结果:

//AN_Xml:
a == b is false
//AN_Xml:c == d is true
//AN_Xml:

在 Java 5 中,在 Integer 的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。

//AN_Xml:
//AN_Xml:

适用于整数值区间-128 至 +127。

//AN_Xml:

只适用于自动装箱。使用构造函数创建对象不适用。

//AN_Xml:
//AN_Xml:

增强 for 循环

//AN_Xml:
for (Student stu : students) {
//AN_Xml:    if (stu.getId() == 2)
//AN_Xml:        students.remove(stu);
//AN_Xml:}
//AN_Xml:

会抛出ConcurrentModificationException异常。

//AN_Xml:

Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出java.util.ConcurrentModificationException异常。

//AN_Xml:

所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 Iterator 本身的方法remove()来删除对象,Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性。

//AN_Xml:

总结

//AN_Xml:

前面介绍了 12 种 Java 中常用的语法糖。所谓语法糖就是提供给开发人员便于开发的一种语法而已。但是这种语法只有开发人员认识。要想被执行,需要进行解糖,即转成 JVM 认识的语法。当我们把语法糖解糖之后,你就会发现其实我们日常使用的这些方便的语法,其实都是一些其他更简单的语法构成的。

//AN_Xml:

有了这些语法糖,我们在日常开发的时候可以大大提升效率,但是同时也要避过度使用。使用之前最好了解下原理,避免掉坑。

//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 权限系统设计详解 //AN_Xml: https://javaguide.cn/system-design/security/design-of-authority-system.html //AN_Xml: https://javaguide.cn/system-design/security/design-of-authority-system.html //AN_Xml: 权限系统设计详解 //AN_Xml: 基于角色的访问控制(Role-Based Access Control,简称 RBAC)指的是通过用户的角色(Role)授权其相关权限,实现了灵活的访问控制,相比直接授予用户权限,要更加简单、高效、可扩展。 //AN_Xml: 系统设计 //AN_Xml: Mon, 15 Aug 2022 07:12:39 GMT //AN_Xml: 《SpringAI 智能面试平台+RAG 知识库》

//AN_Xml:
//AN_Xml:

作者:转转技术团队

//AN_Xml:

原文:https://mp.weixin.qq.com/s/ONMuELjdHYa0yQceTj01Iw

//AN_Xml:
//AN_Xml:

老权限系统的问题与现状

//AN_Xml:

转转公司在过去并没有一个统一的权限管理系统,权限管理由各业务自行研发或是使用其他业务的权限系统,权限管理的不统一带来了不少问题:

//AN_Xml:
    //AN_Xml:
  1. 各业务重复造轮子,维护成本高
  2. //AN_Xml:
  3. 各系统只解决部分场景问题,方案不够通用,新项目选型时没有可靠的权限管理方案
  4. //AN_Xml:
  5. 缺乏统一的日志管理与审批流程,在授权信息追溯上十分困难
  6. //AN_Xml:
//AN_Xml:

基于上述问题,去年底公司启动建设转转统一权限系统,目标是开发一套灵活、易用、安全的权限管理系统,供各业务使用。

//AN_Xml:

业界权限系统的设计方式

//AN_Xml:

目前业界主流的权限模型有两种,下面分别介绍下:

//AN_Xml:
    //AN_Xml:
  • 基于角色的访问控制(RBAC)
  • //AN_Xml:
  • 基于属性的访问控制(ABAC)
  • //AN_Xml:
//AN_Xml:

RBAC 模型

//AN_Xml:

基于角色的访问控制(Role-Based Access Control,简称 RBAC) 指的是通过用户的角色(Role)授权其相关权限,实现了灵活的访问控制,相比直接授予用户权限,要更加简单、高效、可扩展。

//AN_Xml:

一个用户可以拥有若干角色,每一个角色又可以被分配若干权限这样,就构造成“用户-角色-权限” 的授权模型。在这种模型中,用户与角色、角色与权限之间构成了多对多的关系。

//AN_Xml:

用一个图来描述如下:

//AN_Xml:

RBAC 权限模型示意图

//AN_Xml:

当使用 RBAC模型 时,通过分析用户的实际情况,基于共同的职责和需求,授予他们不同角色。这种 用户 -> 角色 -> 权限 间的关系,让我们可以不用再单独管理单个用户权限,用户从授予的角色里面获取所需的权限。

//AN_Xml:

以一个简单的场景(Gitlab 的权限系统)为例,用户系统中有 AdminMaintainerOperator 三种角色,这三种角色分别具备不同的权限,比如只有 Admin 具备创建代码仓库、删除代码仓库的权限,其他的角色都不具备。我们授予某个用户 Admin 这个角色,他就具备了 创建代码仓库删除代码仓库 这两个权限。

//AN_Xml:

通过 RBAC模型 ,当存在多个用户拥有相同权限时,我们只需要创建好拥有该权限的角色,然后给不同的用户分配不同的角色,后续只需要修改角色的权限,就能自动修改角色内所有用户的权限。

//AN_Xml:

ABAC 模型

//AN_Xml:

基于属性的访问控制(Attribute-Based Access Control,简称 ABAC) 是一种比 RBAC模型 更加灵活的授权模型,它的原理是通过各种属性来动态判断一个操作是否可以被允许。这个模型在云系统中使用的比较多,比如 AWS,阿里云等。

//AN_Xml:

考虑下面这些场景的权限控制:

//AN_Xml:
    //AN_Xml:
  1. 授权某个人具体某本书的编辑权限
  2. //AN_Xml:
  3. 当一个文档的所属部门跟用户的部门相同时,用户可以访问这个文档
  4. //AN_Xml:
  5. 当用户是一个文档的拥有者并且文档的状态是草稿,用户可以编辑这个文档
  6. //AN_Xml:
  7. 早上九点前禁止 A 部门的人访问 B 系统
  8. //AN_Xml:
  9. 在除了上海以外的地方禁止以管理员身份访问 A 系统
  10. //AN_Xml:
  11. 用户对 2022-06-07 之前创建的订单有操作权限
  12. //AN_Xml:
//AN_Xml:

可以发现上述的场景通过 RBAC模型 很难去实现,因为 RBAC模型 仅仅描述了用户可以做什么操作,但是操作的条件,以及操作的数据,RBAC模型 本身是没有这些限制的。但这恰恰是 ABAC模型 的长处,ABAC模型 的思想是基于用户、访问的数据的属性、以及各种环境因素去动态计算用户是否有权限进行操作。

//AN_Xml:

ABAC 模型的原理

//AN_Xml:

ABAC模型 中,一个操作是否被允许是基于对象、资源、操作和环境信息共同动态计算决定的。

//AN_Xml:
    //AN_Xml:
  • 对象:对象是当前请求访问资源的用户。用户的属性包括 ID,个人资源,角色,部门和组织成员身份等
  • //AN_Xml:
  • 资源:资源是当前用户要访问的资产或对象,例如文件,数据,服务器,甚至 API
  • //AN_Xml:
  • 操作:操作是用户试图对资源进行的操作。常见的操作包括“读取”,“写入”,“编辑”,“复制”和“删除”
  • //AN_Xml:
  • 环境:环境是每个访问请求的上下文。环境属性包含访问的时间和位置,对象的设备,通信协议和加密强度等
  • //AN_Xml:
//AN_Xml:

ABAC模型 的决策语句的执行过程中,决策引擎会根据定义好的决策语句,结合对象、资源、操作、环境等因素动态计算出决策结果。每当发生访问请求时,ABAC模型 决策系统都会分析属性值是否与已建立的策略匹配。如果有匹配的策略,访问请求就会被通过。

//AN_Xml:

新权限系统的设计思想

//AN_Xml:

结合转转的业务现状,RBAC模型 满足了转转绝大部分业务场景,并且开发成本远低于 ABAC模型 的权限系统,所以新权限系统选择了基于 RBAC模型 来实现。对于实在无法满足的业务系统,我们选择了暂时性不支持,这样可以保障新权限系统的快速落地,更快的让业务使用起来。

//AN_Xml:

标准的 RBAC模型 是完全遵守 用户 -> 角色 -> 权限 这个链路的,也就是用户的权限完全由他所拥有的角色来控制,但是这样会有一个缺点,就是给用户加权限必须新增一个角色,导致实际操作起来效率比较低。所以我们在 RBAC模型 的基础上,新增了给用户直接增加权限的能力,也就是说既可以给用户添加角色,也可以给用户直接添加权限。最终用户的权限是由拥有的角色和权限点组合而成。

//AN_Xml:

新权限系统的权限模型:用户最终权限 = 用户拥有的角色带来的权限 + 用户独立配置的权限,两者取并集。

//AN_Xml:

新权限系统方案如下图:

//AN_Xml:

新权限系统方案

//AN_Xml:
    //AN_Xml:
  • 首先,将集团所有的用户(包括外部用户),通过 统一登录与注册 功能实现了统一管理,同时与公司的组织架构信息模块打通,实现了同一个人员在所有系统中信息的一致,这也为后续基于组织架构进行权限管理提供了可行性。
  • //AN_Xml:
  • 其次,因为新权限系统需要服务集团所有业务,所以需要支持多系统权限管理。用户进行权限管理前,需要先选择相应的系统,然后配置该系统的 菜单权限数据权限 信息,建立好系统的各个权限点。PS:菜单权限和数据权限的具体说明,下文会详细介绍。
  • //AN_Xml:
  • 最后,创建该系统下的不同角色,给不同角色配置好权限点。比如店长角色,拥有店员操作权限、本店数据查看权限等,配置好这个角色后,后续只需要给店长增加这个角色,就可以让他拥有对应的权限。
  • //AN_Xml:
//AN_Xml:

完成上述配置后,就可以进行用户的权限管理了。有两种方式可以给用户加权限:

//AN_Xml:
    //AN_Xml:
  1. 先选用户,然后添加权限。该方式可以给用户添加任意角色或是菜单/数据权限点。
  2. //AN_Xml:
  3. 先选择角色,然后关联用户。该方式只可给用户添加角色,不能单独添加菜单/数据权限点。
  4. //AN_Xml:
//AN_Xml:

这两种方式的具体设计方案,后文会详细说明。

//AN_Xml:

权限系统自身的权限管理

//AN_Xml:

对于权限系统来说,首先需要设计好系统自身的权限管理,也就是需要管理好 ”谁可以进入权限系统,谁可以管理其他系统的权限“,对于权限系统自身的用户,会分为三类:

//AN_Xml:
    //AN_Xml:
  1. 超级管理员:拥有权限系统的全部操作权限,可以进行系统自身的任何操作,也可以管理接入权限的应用系统的管理操作。
  2. //AN_Xml:
  3. 权限操作用户:拥有至少一个已接入的应用系统的超级管理员角色的用户。该用户能进行的操作限定在所拥有的应用系统权限范围内。权限操作用户是一种身份,无需分配,而是根据规则自动获得的。
  4. //AN_Xml:
  5. 普通用户:普通用户也可以认为是一种身份,除去上述 2 类人,其余的都为普通用户。他们只能申请接入系统以及访问权限申请页面。
  6. //AN_Xml:
//AN_Xml:

权限类型的定义

//AN_Xml:

新权限系统中,我们把权限分为两大类,分别是:

//AN_Xml:
    //AN_Xml:
  • 菜单功能权限:包括系统的目录导航、菜单的访问权限,以及按钮和 API 操作的权限
  • //AN_Xml:
  • 数据权限:包括定义数据的查询范围权限,在不同系统中,通常叫做 “组织”、”站点“等,在新权限系统中,统一称作 ”组织“ 来管理数据权限
  • //AN_Xml:
//AN_Xml:

默认角色的分类

//AN_Xml:

每个系统中设计了三个默认角色,用来满足基本的权限管理需求,分别如下:

//AN_Xml:
    //AN_Xml:
  • 超级管理员:该角色拥有该系统的全部权限,可以修改系统的角色权限等配置,可以给其他用户授权。
  • //AN_Xml:
  • 系统管理员:该角色拥有给其他用户授权以及修改系统的角色权限等配置能力,但角色本身不具有任何权限。
  • //AN_Xml:
  • 授权管理员:该角色拥有给其他用户授权的能力。但是授权的范围不超出自己所拥有的权限。
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

举个栗子:授权管理员 A 可以给 B 用户添加权限,但添加的范围 小于等于 A 用户已拥有的权限。

//AN_Xml:
//AN_Xml:

经过这么区分,把 拥有权限拥有授权能力 ,这两部分给分隔开来,可以满足所有的权限控制的场景。

//AN_Xml:

新权限系统的核心模块设计

//AN_Xml:

上面介绍了新权限系统的整体设计思想,接下来分别介绍下核心模块的设计

//AN_Xml:

系统/菜单/数据权限管理

//AN_Xml:

把一个新系统接入权限系统有下列步骤:

//AN_Xml:
    //AN_Xml:
  1. 创建系统
  2. //AN_Xml:
  3. 配置菜单功能权限
  4. //AN_Xml:
  5. 配置数据权限(可选)
  6. //AN_Xml:
  7. 创建系统的角色
  8. //AN_Xml:
//AN_Xml:

其中,1、2、3 的步骤,都是在系统管理模块完成,具体流程如下图:

//AN_Xml:

系统接入流程图

//AN_Xml:

用户可以对系统的基本信息进行增删改查的操作,不同系统之间通过 系统编码 作为唯一区分。同时 系统编码 也会用作于菜单和数据权限编码的前缀,通过这样的设计保证权限编码全局唯一性。

//AN_Xml:

例如系统的编码为 test_online,那么该系统的菜单编码格式便为 test_online:m_xxx

//AN_Xml:

系统管理界面设计如下:

//AN_Xml:

系统管理界面设计

//AN_Xml:

菜单管理

//AN_Xml:

新权限系统首先对菜单进行了分类,分别是 目录菜单操作,示意如下图

//AN_Xml:

菜单管理界面

//AN_Xml:

它们分别代表的含义是:

//AN_Xml:
    //AN_Xml:
  • 目录:指的是应用系统中最顶部的一级目录,通常在系统 Logo 的右边
  • //AN_Xml:
  • 菜单:指的是应用系统左侧的多层级菜单,通常在系统 Logo 的下面,也是最常用的菜单结构
  • //AN_Xml:
  • 操作:指页面中的按钮、接口等一系列可以定义为操作或页面元素的部分。
  • //AN_Xml:
//AN_Xml:

菜单管理界面设计如下:

//AN_Xml:

菜单管理界面设计

//AN_Xml:

菜单权限数据的使用,也提供两种方式:

//AN_Xml:
    //AN_Xml:
  • 动态菜单模式:这种模式下,菜单的增删完全由权限系统接管。也就是说在权限系统增加菜单,应用系统会同步增加。这种模式好处是修改菜单无需项目上线。
  • //AN_Xml:
  • 静态菜单模式:菜单的增删由应用系统的前端控制,权限系统只控制访问权限。这种模式下,权限系统只能标识出用户是否拥有当前菜单的权限,而具体的显示控制是由前端根据权限数据来决定。
  • //AN_Xml:
//AN_Xml:

角色与用户管理

//AN_Xml:

角色与用户管理都是可以直接改变用户权限的核心模块,整个设计思路如下图:

//AN_Xml:

角色与用户管理模块设计

//AN_Xml:

这个模块设计重点是需要考虑到批量操作。无论是通过角色关联用户,还是给用户批量增加/删除/重置权限,批量操作的场景都是系统需要设计好的。

//AN_Xml:

权限申请

//AN_Xml:

除了给其他用户添加权限外,新权限系统同时支持了用户自主申请权限。这个模块除了常规的审批流(申请、审批、查看)等,有一个比较特别的功能,就是如何让用户能选对自己要的权限。所以在该模块的设计上,除了直接选择角色外,还支持通过菜单/数据权限点,反向选择角色,如下图:

//AN_Xml:

权限申请界面

//AN_Xml:

操作日志

//AN_Xml:

系统操作日志会分为两大类:

//AN_Xml:
    //AN_Xml:
  1. 操作流水日志:用户可看、可查的关键操作日志
  2. //AN_Xml:
  3. 服务 Log 日志:系统服务运行过程中产生的 Log 日志,其中,服务 Log 日志信息量大于操作流水日志,但是不方便搜索查看。所以权限系统需要提供操作流水日志功能。
  4. //AN_Xml:
//AN_Xml:

在新权限系统中,用户所有的操作可以分为三类,分别为新增、更新、删除。所有的模块也可枚举,例如用户管理、角色管理、菜单管理等。明确这些信息后,那么一条日志就可以抽象为:什么人(Who)在什么时间(When)对哪些人(Target)的哪些模块做了哪些操作。
//AN_Xml:这样把所有的记录都入库,就可以方便的进行日志的查看和筛选了。

//AN_Xml:

总结与展望

//AN_Xml:

至此,新权限系统的核心设计思路与模块都已介绍完成,新系统在转转内部有大量的业务接入使用,权限管理相比以前方便了许多。权限系统作为每家公司的一个基础系统,灵活且完备的设计可以助力日后业务的发展更加高效。

//AN_Xml:

后续两篇:

//AN_Xml: //AN_Xml:

参考

//AN_Xml: //AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: 应用层常见协议总结(应用层) //AN_Xml: https://javaguide.cn/cs-basics/network/application-layer-protocol.html //AN_Xml: https://javaguide.cn/cs-basics/network/application-layer-protocol.html //AN_Xml: 应用层常见协议总结(应用层) //AN_Xml: 汇总应用层常见协议的核心概念与典型场景,重点对比 HTTP 与 WebSocket 的通信模型与能力边界。 //AN_Xml: 计算机基础 //AN_Xml: Sun, 14 Aug 2022 09:10:17 GMT //AN_Xml: HTTP:超文本传输协议 //AN_Xml:

超文本传输协议(HTTP,HyperText Transfer Protocol) 是一种用于传输超文本和多媒体内容的协议,主要是为 Web 浏览器与 Web 服务器之间的通信而设计的。当我们使用浏览器浏览网页的时候,我们网页就是通过 HTTP 请求进行加载的。

//AN_Xml:

HTTP 使用客户端-服务器模型,客户端向服务器发送 HTTP Request(请求),服务器响应请求并返回 HTTP Response(响应),整个过程如下图所示。

//AN_Xml:

//AN_Xml:

HTTP 协议基于 TCP 协议,发送 HTTP 请求之前首先要建立 TCP 连接也就是要经历 3 次握手。目前使用的 HTTP 协议大部分都是 1.1。在 1.1 的协议里面,默认是开启了 Keep-Alive 的,这样的话建立的连接就可以在多次请求中被复用了。

//AN_Xml:

另外, HTTP 协议是“无状态”的协议,它无法记录客户端用户的状态,一般我们都是通过 Session 来记录客户端用户的状态。

//AN_Xml:

Websocket:全双工通信协议

//AN_Xml:

WebSocket 是一种基于 TCP 连接的全双工通信协议,即客户端和服务器可以同时发送和接收数据。

//AN_Xml:

WebSocket 协议在 2008 年诞生,2011 年成为国际标准,几乎所有主流较新版本的浏览器都支持该协议。不过,WebSocket 不只能在基于浏览器的应用程序中使用,很多编程语言、框架和服务器都提供了 WebSocket 支持。

//AN_Xml:

WebSocket 协议本质上是应用层的协议,用于弥补 HTTP 协议在持久通信能力上的不足。客户端和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

//AN_Xml:

Websocket 示意图

//AN_Xml:

下面是 WebSocket 的常见应用场景:

//AN_Xml:
    //AN_Xml:
  • 视频弹幕
  • //AN_Xml:
  • 实时消息推送,详见Web 实时消息推送详解这篇文章
  • //AN_Xml:
  • 实时游戏对战
  • //AN_Xml:
  • 多用户协同编辑
  • //AN_Xml:
  • 社交聊天
  • //AN_Xml:
  • ……
  • //AN_Xml:
//AN_Xml:

WebSocket 的工作过程可以分为以下几个步骤:

//AN_Xml:
    //AN_Xml:
  1. 客户端向服务器发送一个 HTTP 请求,请求头中包含 Upgrade: websocketSec-WebSocket-Key 等字段,表示要求升级协议为 WebSocket;
  2. //AN_Xml:
  3. 服务器收到这个请求后,会进行升级协议的操作,如果支持 WebSocket,它将回复一个 HTTP 101 状态码,响应头中包含 ,Connection: UpgradeSec-WebSocket-Accept: xxx 等字段、表示成功升级到 WebSocket 协议。
  4. //AN_Xml:
  5. 客户端和服务器之间建立了一个 WebSocket 连接,可以进行双向的数据传输。数据以帧(frames)的形式进行传送,WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息。
  6. //AN_Xml:
  7. 客户端或服务器可以主动发送一个关闭帧,表示要断开连接。另一方收到后,也会回复一个关闭帧,然后双方关闭 TCP 连接。
  8. //AN_Xml:
//AN_Xml:

另外,建立 WebSocket 连接之后,通过心跳机制来保持 WebSocket 连接的稳定性和活跃性。

//AN_Xml:

SMTP:简单邮件传输(发送)协议

//AN_Xml:

简单邮件传输(发送)协议(SMTP,Simple Mail Transfer Protocol) 基于 TCP 协议,是一种用于发送电子邮件的协议

//AN_Xml:

SMTP 协议

//AN_Xml:

注意 ⚠️:接受邮件的协议不是 SMTP 而是 POP3 协议。

//AN_Xml:

SMTP 协议这块涉及的内容比较多,下面这两个问题比较重要:

//AN_Xml:
    //AN_Xml:
  1. 电子邮件的发送过程
  2. //AN_Xml:
  3. 如何判断邮箱是真正存在的?
  4. //AN_Xml:
//AN_Xml:

电子邮件的发送过程?

//AN_Xml:

比如我的邮箱是“dabai@cszhinan.com”,我要向“xiaoma@qq.com”发送邮件,整个过程可以简单分为下面几步:

//AN_Xml:
    //AN_Xml:
  1. 通过 SMTP 协议,我将我写好的邮件交给 163 邮箱服务器(邮局)。
  2. //AN_Xml:
  3. 163 邮箱服务器发现我发送的邮箱是 qq 邮箱,然后它使用 SMTP 协议将我的邮件转发到 qq 邮箱服务器。
  4. //AN_Xml:
  5. qq 邮箱服务器接收邮件之后就通知邮箱为“xiaoma@qq.com”的用户来收邮件,然后用户就通过 POP3/IMAP 协议将邮件取出。
  6. //AN_Xml:
//AN_Xml:

如何判断邮箱是真正存在的?

//AN_Xml:

很多场景(比如邮件营销)下面我们需要判断我们要发送的邮箱地址是否真的存在,这个时候我们可以利用 SMTP 协议来检测:

//AN_Xml:
    //AN_Xml:
  1. 查找邮箱域名对应的 SMTP 服务器地址
  2. //AN_Xml:
  3. 尝试与服务器建立连接
  4. //AN_Xml:
  5. 连接成功后尝试向需要验证的邮箱发送邮件
  6. //AN_Xml:
  7. 根据返回结果判定邮箱地址的真实性
  8. //AN_Xml:
//AN_Xml:

推荐几个在线邮箱是否有效检测工具:

//AN_Xml:
    //AN_Xml:
  1. https://verify-email.org/
  2. //AN_Xml:
  3. http://tool.chacuo.net/mailverify
  4. //AN_Xml:
  5. https://www.emailcamel.com/
  6. //AN_Xml:
//AN_Xml:

POP3/IMAP:邮件接收的协议

//AN_Xml:

这两个协议没必要多做阐述,只需要了解 POP3 和 IMAP 两者都是负责邮件接收的协议 即可(二者也是基于 TCP 协议)。另外,需要注意不要将这两者和 SMTP 协议搞混淆了。SMTP 协议只负责邮件的发送,真正负责接收的协议是 POP3/IMAP。

//AN_Xml:

IMAP 协议是比 POP3 更新的协议,它在功能和性能上都更加强大。IMAP 支持邮件搜索、标记、分类、归档等高级功能,而且可以在多个设备之间同步邮件状态。几乎所有现代电子邮件客户端和服务器都支持 IMAP。

//AN_Xml:

FTP:文件传输协议

//AN_Xml:

FTP 协议 基于 TCP 协议,是一种用于在计算机之间传输文件的协议,可以屏蔽操作系统和文件存储方式。

//AN_Xml:

FTP 是基于客户—服务器(C/S)模型而设计的,在客户端与 FTP 服务器之间建立两个连接。如果我们要基于 FTP 协议开发一个文件传输的软件的话,首先需要搞清楚 FTP 的原理。关于 FTP 的原理,很多书籍上已经描述的非常详细了:

//AN_Xml:
//AN_Xml:

FTP 的独特的优势同时也是与其它客户服务器程序最大的不同点就在于它在两台通信的主机之间使用了两条 TCP 连接(其它客户服务器应用程序一般只有一条 TCP 连接):

//AN_Xml:
    //AN_Xml:
  1. 控制连接:用于传送控制信息(命令和响应)
  2. //AN_Xml:
  3. 数据连接:用于数据传送;
  4. //AN_Xml:
//AN_Xml:

这种将命令和数据分开传送的思想大大提高了 FTP 的效率。

//AN_Xml:
//AN_Xml:

FTP工作过程

//AN_Xml:

注意 ⚠️:FTP 是一种不安全的协议,因为它在传输过程中不会对数据进行加密。因此,FTP 传输的文件可能会被窃听或篡改。建议在传输敏感数据时使用更安全的协议,如 SFTP(SSH File Transfer Protocol,一种基于 SSH 协议的安全文件传输协议,用于在网络上安全地传输文件)。

//AN_Xml:

Telnet:远程登陆协议

//AN_Xml:

Telnet 协议 基于 TCP 协议,用于通过一个终端登陆到其他服务器。Telnet 协议的最大缺点之一是所有数据(包括用户名和密码)均以明文形式发送,这有潜在的安全风险。这就是为什么如今很少使用 Telnet,而是使用一种称为 SSH 的非常安全的网络传输协议的主要原因。

//AN_Xml:

Telnet:远程登陆协议

//AN_Xml:

SSH:安全的网络传输协议

//AN_Xml:

SSH(Secure Shell) 基于 TCP 协议,通过加密和认证机制实现安全的访问和文件传输等业务。

//AN_Xml:

SSH 的经典用途是登录到远程电脑中执行命令。除此之外,SSH 也支持隧道协议、端口映射和 X11 连接(允许用户在本地运行远程服务器上的图形应用程序)。借助 SFTP(SSH File Transfer Protocol) 或 SCP(Secure Copy Protocol) 协议,SSH 还可以安全传输文件。

//AN_Xml:

SSH 使用客户端-服务器模型,默认端口是 22。SSH 是一个守护进程,负责实时监听客户端请求,并进行处理。大多数现代操作系统都提供了 SSH。

//AN_Xml:

如下图所示,SSH Client(SSH 客户端)和 SSH Server(SSH 服务器)通过公钥交换生成共享的对称加密密钥,用于后续的加密通信。

//AN_Xml:

SSH:安全的网络传输协议

//AN_Xml:

RTP:实时传输协议

//AN_Xml:

RTP(Real-time Transport Protocol,实时传输协议)通常基于 UDP 协议,但也支持 TCP 协议。它提供了端到端的实时传输数据的功能,但不包含资源预留存、不保证实时传输质量,这些功能由 WebRTC 实现。

//AN_Xml:

RTP 协议分为两种子协议:

//AN_Xml:
    //AN_Xml:
  • RTP(Real-time Transport Protocol,实时传输协议):传输具有实时特性的数据。
  • //AN_Xml:
  • RTCP(RTP Control Protocol,RTP 控制协议):提供实时传输过程中的统计信息(如网络延迟、丢包率等),WebRTC 正是根据这些信息处理丢包
  • //AN_Xml:
//AN_Xml:

DNS:域名系统

//AN_Xml:

DNS(Domain Name System,域名管理系统)通常基于 UDP 协议(端口 53),用于解决域名和 IP 地址的映射问题。当响应数据超过 UDP 长度限制或进行区域传送时会改用 TCP。

//AN_Xml:

DNS:域名系统

//AN_Xml:

参考

//AN_Xml: //AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: TCP 传输可靠性保障(传输层) //AN_Xml: https://javaguide.cn/cs-basics/network/tcp-reliability-guarantee.html //AN_Xml: https://javaguide.cn/cs-basics/network/tcp-reliability-guarantee.html //AN_Xml: TCP 传输可靠性保障(传输层) //AN_Xml: 系统梳理 TCP 的可靠性保障机制,覆盖重传/选择确认、流量与拥塞控制,明确端到端可靠传输的实现要点。 //AN_Xml: 计算机基础 //AN_Xml: Sun, 14 Aug 2022 09:10:17 GMT //AN_Xml: TCP 如何保证传输的可靠性? //AN_Xml:
    //AN_Xml:
  1. 基于数据块传输:应用数据被分割成 TCP 认为最适合发送的数据块,再传输给网络层,数据块被称为报文段或段。
  2. //AN_Xml:
  3. 对失序数据包重新排序以及去重:TCP 为了保证不发生丢包,就给每个包一个序列号,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据就可以实现数据包去重。
  4. //AN_Xml:
  5. 校验和 : TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。
  6. //AN_Xml:
  7. 重传机制 : 在数据包丢失或延迟的情况下,重新发送数据包,直到收到对方的确认应答(ACK)。TCP 重传机制主要有:基于计时器的重传(也就是超时重传)、快速重传(基于接收端的反馈信息来引发重传)、SACK(在快速重传的基础上,返回最近收到的报文段的序列号范围,这样客户端就知道,哪些数据包已经到达服务器了)、D-SACK(重复 SACK,在 SACK 的基础上,额外携带信息,告知发送方有哪些数据包自己重复接收了)。关于重传机制的详细介绍,可以查看详解 TCP 超时与重传机制这篇文章。
  8. //AN_Xml:
  9. 流量控制 : TCP 连接的每一方都有固定大小的缓冲空间,TCP 的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议(TCP 利用滑动窗口实现流量控制)。
  10. //AN_Xml:
  11. 拥塞控制 : 当网络拥塞时,减少数据的发送。TCP 在发送数据的时候,需要考虑两个因素:一是接收方的接收能力,二是网络的拥塞程度。接收方的接收能力由滑动窗口表示,表示接收方还有多少缓冲区可以用来接收数据。网络的拥塞程度由拥塞窗口表示,它是发送方根据网络状况自己维护的一个值,表示发送方认为可以在网络中传输的数据量。发送方发送数据的大小是滑动窗口和拥塞窗口的最小值,这样可以保证发送方既不会超过接收方的接收能力,也不会造成网络的过度拥塞。
  12. //AN_Xml:
//AN_Xml:

TCP 如何实现流量控制?

//AN_Xml:

TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。

//AN_Xml:

为什么需要流量控制? 这是因为双方在通信的时候,发送方的速率与接收方的速率是不一定相等,如果发送方的发送速率太快,会导致接收方处理不过来。如果接收方处理不过来的话,就只能把处理不过来的数据存在 接收缓冲区(Receiving Buffers) 里(失序的数据包也会被存放在缓存区里)。如果缓存区满了发送方还在狂发数据的话,接收方只能把收到的数据包丢掉。出现丢包问题的同时又疯狂浪费着珍贵的网络资源。因此,我们需要控制发送方的发送速率,让接收方与发送方处于一种动态平衡才好。

//AN_Xml:

这里需要注意的是(常见误区):

//AN_Xml:
    //AN_Xml:
  • 发送端不等同于客户端
  • //AN_Xml:
  • 接收端不等同于服务端
  • //AN_Xml:
//AN_Xml:

TCP 为全双工(Full-Duplex, FDX)通信,双方可以进行双向通信,客户端和服务端既可能是发送端又可能是服务端。因此,两端各有一个发送缓冲区与接收缓冲区,两端都各自维护一个发送窗口和一个接收窗口。接收窗口大小取决于应用、系统、硬件的限制(TCP 传输速率不能大于应用的数据处理速率)。通信双方的发送窗口和接收窗口的要求相同

//AN_Xml:

TCP 发送窗口可以划分成四个部分

//AN_Xml:
    //AN_Xml:
  1. 已经发送并且确认的 TCP 段(已经发送并确认);
  2. //AN_Xml:
  3. 已经发送但是没有确认的 TCP 段(已经发送未确认);
  4. //AN_Xml:
  5. 未发送但是接收方准备接收的 TCP 段(可以发送);
  6. //AN_Xml:
  7. 未发送并且接收方也并未准备接受的 TCP 段(不可发送)。
  8. //AN_Xml:
//AN_Xml:

TCP 发送窗口结构图示

//AN_Xml:

TCP发送窗口结构

//AN_Xml:
    //AN_Xml:
  • SND.WND:发送窗口。
  • //AN_Xml:
  • SND.UNA:Send Unacknowledged 指针,指向发送窗口的第一个字节。
  • //AN_Xml:
  • SND.NXT:Send Next 指针,指向可用窗口的第一个字节。
  • //AN_Xml:
//AN_Xml:

可用窗口大小 = SND.UNA + SND.WND - SND.NXT

//AN_Xml:

TCP 接收窗口可以划分成三个部分

//AN_Xml:
    //AN_Xml:
  1. 已经接收并且已经确认的 TCP 段(已经接收并确认);
  2. //AN_Xml:
  3. 等待接收且允许发送方发送 TCP 段(可以接收未确认);
  4. //AN_Xml:
  5. 不可接收且不允许发送方发送 TCP 段(不可接收)。
  6. //AN_Xml:
//AN_Xml:

TCP 接收窗口结构图示

//AN_Xml:

TCP接收窗口结构

//AN_Xml:

接收窗口的大小是根据接收端处理数据的速度动态调整的。 如果接收端读取数据快,接收窗口可能会扩大。 否则,它可能会缩小。

//AN_Xml:

另外,这里的滑动窗口大小只是为了演示使用,实际窗口大小通常会远远大于这个值。

//AN_Xml:

TCP 的拥塞控制是怎么实现的?

//AN_Xml:

在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。

//AN_Xml:

TCP的拥塞控制

//AN_Xml:

为了进行拥塞控制,TCP 发送方要维持一个 拥塞窗口(cwnd) 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。

//AN_Xml:

TCP 的拥塞控制采用了四种算法,即 慢开始拥塞避免快重传快恢复。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。

//AN_Xml:
    //AN_Xml:
  • 慢开始: 慢开始算法的思路是当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,因为现在还不知道网络的负荷情况。经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是由小到大逐渐增大拥塞窗口数值。cwnd 初始值为 1,每经过一个传播轮次,cwnd 加倍。
  • //AN_Xml:
  • 拥塞避免: 拥塞避免算法的思路是让拥塞窗口 cwnd 缓慢增大,即每经过一个往返时间 RTT 就把发送方的 cwnd 加 1.
  • //AN_Xml:
  • 快重传与快恢复: 在 TCP/IP 中,快速重传和恢复(fast retransmit and recovery,FRR)是一种拥塞控制算法,它能快速恢复丢失的数据包。没有 FRR,如果数据包丢失了,TCP 将会使用定时器来要求传输暂停。在暂停的这段时间内,没有新的或复制的数据包被发送。有了 FRR,如果接收机接收到一个不按顺序的数据段,它会立即给发送机发送一个重复确认。如果发送机接收到三个重复确认,它会假定确认件指出的数据段丢失了,并立即重传这些丢失的数据段。有了 FRR,就不会因为重传时要求的暂停被耽误。  当有单独的数据包丢失时,快速重传和恢复(FRR)能最有效地工作。当有多个数据信息包在某一段很短的时间内丢失时,它则不能很有效地工作。
  • //AN_Xml:
//AN_Xml:

ARQ 协议了解吗?

//AN_Xml:

自动重传请求(Automatic Repeat-reQuest,ARQ)是 OSI 模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认信息(Acknowledgements,就是我们常说的 ACK),它通常会重新发送,直到收到确认或者重试超过一定的次数。

//AN_Xml:

ARQ 包括停止等待 ARQ 协议和连续 ARQ 协议。

//AN_Xml:

停止等待 ARQ 协议

//AN_Xml:

停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认(回复 ACK)。如果过了一段时间(超时时间后),还是没有收到 ACK 确认,说明没有发送成功,需要重新发送,直到收到确认后再发下一个分组;

//AN_Xml:

在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认。

//AN_Xml:

1) 无差错情况:

//AN_Xml:

发送方发送分组,接收方在规定时间内收到,并且回复确认.发送方再次发送。

//AN_Xml:

2) 出现差错情况(超时重传):

//AN_Xml:

停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为 自动重传请求 ARQ 。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。

//AN_Xml:

3) 确认丢失和确认迟到

//AN_Xml:
    //AN_Xml:
  • 确认丢失:确认消息在传输过程丢失。当 A 发送 M1 消息,B 收到后,B 向 A 发送了一个 M1 确认消息,但却在传输过程中丢失。而 A 并不知道,在超时计时过后,A 重传 M1 消息,B 再次收到该消息后采取以下两点措施:1. 丢弃这个重复的 M1 消息,不向上层交付。 2. 向 A 发送确认消息。(不会认为已经发送过了,就不再发送。A 能重传,就证明 B 的确认消息丢失)。
  • //AN_Xml:
  • 确认迟到:确认消息在传输过程中迟到。A 发送 M1 消息,B 收到并发送确认。在超时时间内没有收到确认消息,A 重传 M1 消息,B 仍然收到并继续发送确认消息(B 收到了 2 份 M1)。此时 A 收到了 B 第二次发送的确认消息。接着发送其他数据。过了一会,A 收到了 B 第一次发送的对 M1 的确认消息(A 也收到了 2 份确认消息)。处理如下:1. A 收到重复的确认后,直接丢弃。2. B 收到重复的 M1 后,也直接丢弃重复的 M1。
  • //AN_Xml:
//AN_Xml:

连续 ARQ 协议

//AN_Xml:

连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。

//AN_Xml:
    //AN_Xml:
  • 优点: 信道利用率高,容易实现,即使确认丢失,也不必重传。
  • //AN_Xml:
  • 缺点: 不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如:发送方发送了 5 条 消息,中间第三条丢失(3 号),这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落,而只好把后三个全部重传一次。这也叫 Go-Back-N(回退 N),表示需要退回来重传已经发送过的 N 个消息。
  • //AN_Xml:
//AN_Xml:

超时重传如何实现?超时重传时间怎么确定?

//AN_Xml:

当发送方发送数据之后,它启动一个定时器,等待目的端确认收到这个报文段。接收端实体对已成功收到的包发回一个相应的确认信息(ACK)。如果发送端实体在合理的往返时延(RTT)内未收到确认消息,那么对应的数据包就被假设为已丢失并进行重传。

//AN_Xml:
    //AN_Xml:
  • RTT(Round Trip Time):往返时间,也就是数据包从发出去到收到对应 ACK 的时间。
  • //AN_Xml:
  • RTO(Retransmission Time Out):重传超时时间,即从数据发送时刻算起,超过这个时间便执行重传。
  • //AN_Xml:
//AN_Xml:

RTO 的确定是一个关键问题,因为它直接影响到 TCP 的性能和效率。如果 RTO 设置得太小,会导致不必要的重传,增加网络负担;如果 RTO 设置得太大,会导致数据传输的延迟,降低吞吐量。因此,RTO 应该根据网络的实际状况,动态地进行调整。

//AN_Xml:

RTT 的值会随着网络的波动而变化,所以 TCP 不能直接使用 RTT 作为 RTO。为了动态地调整 RTO,TCP 协议采用了一些算法,如加权移动平均(EWMA)算法,Karn 算法,Jacobson 算法等,这些算法都是根据往返时延(RTT)的测量和变化来估计 RTO 的值。

//AN_Xml:

参考

//AN_Xml:
    //AN_Xml:
  1. 《计算机网络(第 7 版)》
  2. //AN_Xml:
  3. 《图解 HTTP》
  4. //AN_Xml:
  5. https://www.9tut.com/tcp-and-udp-tutorial
  6. //AN_Xml:
  7. https://github.com/wolverinn/Waking-Up/blob/master/Computer%20Network.md
  8. //AN_Xml:
  9. TCP Flow Control—https://www.brianstorti.com/tcp-flow-control/
  10. //AN_Xml:
  11. TCP 流量控制(Flow Control):https://notfalse.net/24/tcp-flow-control
  12. //AN_Xml:
  13. TCP 之滑动窗口原理 : https://cloud.tencent.com/developer/article/1857363
  14. //AN_Xml:
//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: TCP 三次握手和四次挥手(传输层) //AN_Xml: https://javaguide.cn/cs-basics/network/tcp-connection-and-disconnection.html //AN_Xml: https://javaguide.cn/cs-basics/network/tcp-connection-and-disconnection.html //AN_Xml: TCP 三次握手和四次挥手(传输层) //AN_Xml: 一文讲清 TCP 三次握手与四次挥手:SEQ/ACK/SYN/FIN 如何同步,TIME_WAIT 与 2MSL 的原因,半连接队列(SYN Queue)与全连接队列(Accept Queue)的工作机制,以及 backlog/somaxconn/syncookies 在高并发与 SYN Flood 下的影响。 //AN_Xml: 计算机基础 //AN_Xml: Sat, 13 Aug 2022 14:50:48 GMT //AN_Xml: TCP(Transmission Control Protocol)是一种面向连接可靠的传输层协议。所谓“可靠”,通常体现在:按序交付、差错检测、丢包重传、流量控制与拥塞控制等。为了在不可靠的网络之上建立一条逻辑可靠的端到端连接,TCP 在传输数据前必须先完成连接建立过程,即 三次握手(Three-way Handshake)

//AN_Xml:

建立连接-TCP 三次握手

//AN_Xml:

TCP 三次握手图解

//AN_Xml:

建立一个 TCP 连接需要“三次握手”,缺一不可:

//AN_Xml:
    //AN_Xml:
  1. 第一次握手 (SYN): 客户端向服务端发送一个 SYN(Synchronize Sequence Numbers)报文段,其中包含一个由客户端随机生成的初始序列号(Initial Sequence Number, ISN),例如 seq=x。发送后,客户端进入 SYN_SENT 状态,等待服务端的确认。
  2. //AN_Xml:
  3. 第二次握手 (SYN+ACK): 服务端收到 SYN 报文段后,如果同意建立连接,会向客户端回复一个确认报文段。该报文段包含两个关键信息: //AN_Xml:
      //AN_Xml:
    • SYN:服务端也需要同步自己的初始序列号,因此报文段中也包含一个由服务端随机生成的初始序列号,例如 seq=y。
    • //AN_Xml:
    • ACK (Acknowledgement):用于确认收到了客户端的请求。其确认号被设置为客户端初始序列号加一,即 ack=x+1。
    • //AN_Xml:
    • 发送该报文段后,服务端进入 SYN_RCVD (也称 SYN_RECV)状态。
    • //AN_Xml:
    //AN_Xml:
  4. //AN_Xml:
  5. 第三次握手 (ACK): 客户端收到服务端的 SYN+ACK 报文段后,会向服务端发送一个最终的确认报文段。该报文段包含确认号 ack=y+1。发送后,客户端进入 ESTABLISHED 状态。服务端收到这个 ACK 报文段后,也进入 ESTABLISHED 状态。
  6. //AN_Xml:
//AN_Xml:

至此,双方都确认了连接的建立,TCP 连接成功创建,可以开始进行双向数据传输。

//AN_Xml:

什么是半连接队列和全连接队列?

//AN_Xml:

在 TCP 三次握手过程中,服务端内核通常会用两个队列来管理连接请求(不同操作系统/内核版本实现细节可能略有差异,下面以常见 Linux 行为为例):

//AN_Xml:
    //AN_Xml:
  1. 半连接队列(也称 SYN Queue): //AN_Xml:
      //AN_Xml:
    • 保存“握手未完成”的请求:服务端收到 SYN 并回 SYN+ACK 后,连接进入 SYN_RCVD,等待客户端最终 ACK。
    • //AN_Xml:
    • 如果一直收不到 ACK,内核会按重传策略重发 SYN+ACK,最终超时清理。
    • //AN_Xml:
    • 常见相关参数:net.ipv4.tcp_max_syn_backlog;在 SYN Flood 场景下可配合 net.ipv4.tcp_syncookies
    • //AN_Xml:
    //AN_Xml:
  2. //AN_Xml:
  3. 全连接队列(也称 Accept Queue): //AN_Xml:
      //AN_Xml:
    • 保存“握手已完成但应用还没 accept”的连接:服务端收到最终 ACK 后连接变为 ESTABLISHED,并进入 全连接队列,等待应用层 accept() 取走。
    • //AN_Xml:
    • 队列容量受 listen(fd, backlog) 与系统上限 net.core.somaxconn 共同影响;实践中常见有效上限近似为 min(backlog, somaxconn)(具体行为与内核版本相关)。
    • //AN_Xml:
    //AN_Xml:
  4. //AN_Xml:
//AN_Xml:

总结:

//AN_Xml:

| 队列 | 作用 | 状态 | 移出条件 |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: JMM(Java 内存模型)详解 //AN_Xml: https://javaguide.cn/java/concurrent/jmm.html //AN_Xml: https://javaguide.cn/java/concurrent/jmm.html //AN_Xml: JMM(Java 内存模型)详解 //AN_Xml: 深入解析Java内存模型JMM:详解CPU缓存模型、指令重排序机制、happens-before原则、内存可见性保证,理解多线程并发编程的底层规范。 //AN_Xml: Java //AN_Xml: Thu, 04 Aug 2022 13:00:03 GMT //AN_Xml: 对于 Java 来说,你可以把 JMM(Java 内存模型) 看作是 Java 定义的并发编程相关的一组规范。除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的转化过程要遵守哪些并发相关的原则和规范。其主要目的是为了简化多线程编程增强程序的可移植性

//AN_Xml:

JMM 主要定义了对于一个共享变量,当一个线程执行写操作后,该变量对其他线程的可见性

//AN_Xml:

要想透彻理解 JMM,我们需要从 CPU 缓存模型指令重排序说起。

//AN_Xml:

从 CPU 缓存模型说起

//AN_Xml:

为什么要弄一个 CPU 高速缓存呢? 类比我们开发网站后台系统使用的缓存(比如 Redis)是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。 CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。

//AN_Xml:

我们甚至可以把 内存看作外存的高速缓存,程序运行的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样提高了处理速度。

//AN_Xml:

总结:CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。

//AN_Xml:

为了更好地理解,我画了一个简单的 CPU Cache 示意图如下所示。

//AN_Xml:
//AN_Xml:

🐛 修正(参见:issue#1848:对 CPU 缓存模型绘图不严谨的地方进行完善。

//AN_Xml:
//AN_Xml:

CPU 缓存模型示意图

//AN_Xml:

现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache。有些 CPU 可能还有 L4 Cache,这里不做讨论,并不常见

//AN_Xml:

CPU Cache 的工作方式: 先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 内存缓存不一致性的问题 !比如我执行一个 i++ 操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 i++ 运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。

//AN_Xml:

CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议(比如 MESI 协议)或者其他手段来解决。 这个缓存一致性协议指的是在 CPU 高速缓存与主内存交互的时候需要遵守的原则和规范。不同的 CPU 中,使用的缓存一致性协议通常也会有所不同。

//AN_Xml:

缓存一致性协议

//AN_Xml:

我们的程序运行在操作系统之上,操作系统屏蔽了底层硬件的操作细节,将各种硬件资源虚拟化。于是,操作系统也就同样需要解决内存缓存不一致性问题。

//AN_Xml:

操作系统通过 内存模型(Memory Model) 定义一系列规范来解决这个问题。无论是 Windows 系统,还是 Linux 系统,它们都有特定的内存模型。

//AN_Xml:

指令重排序

//AN_Xml:

说完了 CPU 缓存模型,我们再来看看另外一个比较重要的概念 指令重排序

//AN_Xml:

为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序。

//AN_Xml:

什么是指令重排序? 简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。

//AN_Xml:

常见的指令重排序有下面 2 种情况:

//AN_Xml:
    //AN_Xml:
  • 编译器优化重排:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
  • //AN_Xml:
  • 指令并行重排:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • //AN_Xml:
//AN_Xml:

另外,内存系统也会有“重排序”,但又不是真正意义上的重排序。在 JMM 里表现为主存和本地内存的内容可能不一致,进而导致程序在多线程下执行可能出现问题。

//AN_Xml:

Java 源代码会经历 编译器优化重排 —> 指令并行重排 —> 内存系统重排 的过程,最终才变成操作系统可执行的指令序列。

//AN_Xml:

指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。

//AN_Xml:

对于编译器优化重排和处理器的指令重排序(指令并行重排和内存系统重排都属于是处理器级别的指令重排序),处理该问题的方式不一样。

//AN_Xml:
    //AN_Xml:
  • //AN_Xml:

    对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序。

    //AN_Xml:
  • //AN_Xml:
  • //AN_Xml:

    对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。

    //AN_Xml:
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。另外,为了达到屏障的效果,它会在处理器写入值时,强制将写缓冲区中的数据刷新到主内存;在读取值之前,使处理器本地缓存中的相关数据失效,强制从主内存中加载最新值,从而保障变量的可见性。

//AN_Xml:
//AN_Xml:

JMM(Java Memory Model)

//AN_Xml:

什么是 JMM?为什么需要 JMM?

//AN_Xml:

Java 是最早尝试提供内存模型的编程语言。由于早期内存模型存在一些缺陷(比如非常容易削弱编译器的优化能力),从 Java5 开始,Java 开始使用新的内存模型 《JSR-133:Java Memory Model and Thread Specification》

//AN_Xml:

一般来说,编程语言也可以直接复用操作系统层面的内存模型。不过,不同的操作系统内存模型不同。如果直接复用操作系统层面的内存模型,就可能会导致同样一套代码换了一个操作系统就无法执行了。Java 语言是跨平台的,它需要自己提供一套内存模型以屏蔽系统差异。

//AN_Xml:

这只是 JMM 存在的其中一个原因。实际上,对于 Java 来说,你可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。

//AN_Xml:

为什么要遵守这些并发相关的原则和规范呢? 这是因为并发编程下,像 CPU 多级缓存和指令重排这类设计可能会导致程序运行出现一些问题。就比如说我们上面提到的指令重排序就可能会让多线程程序的执行出现问题,为此,JMM 抽象了 happens-before 原则(后文会详细介绍到)来解决这个指令重排序问题。

//AN_Xml:

JMM 说白了就是定义了一些规范来解决这些问题,开发者可以利用这些规范更方便地开发多线程程序。对于 Java 开发者说,你不需要了解底层原理,直接使用并发相关的一些关键字和类(比如 volatilesynchronized、各种 Lock)即可开发出并发安全的程序。

//AN_Xml:

JMM 是如何抽象线程和主内存之间的关系?

//AN_Xml:

Java 内存模型(JMM) 抽象了线程和主内存之间的关系,就比如说线程之间的共享变量必须存储在主内存中。

//AN_Xml:

在 JDK1.2 之前,Java 的内存模型实现总是从 主存 (即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存 本地内存 (比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。

//AN_Xml:

这和我们上面讲到的 CPU 缓存模型非常相似。

//AN_Xml:

什么是主内存?什么是本地内存?

//AN_Xml:
    //AN_Xml:
  • 主内存:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量,还是局部变量,类信息、常量、静态变量都是放在主内存中。为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
  • //AN_Xml:
  • 本地内存:每个线程都有一个私有的本地内存,本地内存存储了该线程已读 / 写共享变量的副本。每个线程只能操作自己本地内存中的变量,无法直接访问其他线程的本地内存。如果线程间需要通信,必须通过主内存来进行。本地内存是 JMM 抽象出来的一个概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
  • //AN_Xml:
//AN_Xml:

Java 内存模型的抽象示意图如下:

//AN_Xml:

JMM(Java 内存模型)

//AN_Xml:

从上图来看,线程 1 与线程 2 之间如果要进行通信的话,必须要经历下面 2 个步骤:

//AN_Xml:
    //AN_Xml:
  1. 线程 1 把本地内存中修改过的共享变量副本的值同步到主内存中去。
  2. //AN_Xml:
  3. 线程 2 到主存中读取对应的共享变量的值。
  4. //AN_Xml:
//AN_Xml:

也就是说,JMM 为共享变量提供了可见性的保障。

//AN_Xml:

不过,多线程下,对主内存中的一个共享变量进行操作有可能诱发线程安全问题。举个例子:

//AN_Xml:
    //AN_Xml:
  1. 线程 1 和线程 2 分别对同一个共享变量进行操作,一个执行修改,一个执行读取。
  2. //AN_Xml:
  3. 线程 2 读取到的是线程 1 修改之前的值还是修改后的值并不确定,都有可能,因为线程 1 和线程 2 都是先将共享变量从主内存拷贝到对应线程的工作内存中。
  4. //AN_Xml:
//AN_Xml:

关于主内存与工作内存直接的具体交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之间的实现细节,Java 内存模型定义来以下八种同步操作(了解即可,无需死记硬背):

//AN_Xml:
    //AN_Xml:
  • 锁定(lock): 作用于主内存中的变量,将他标记为一个线程独享变量。
  • //AN_Xml:
  • 解锁(unlock): 作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定。
  • //AN_Xml:
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
  • //AN_Xml:
  • load(载入):把 read 操作从主内存中得到的变量值放入工作内存的变量的副本中。
  • //AN_Xml:
  • use(使用):把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令。
  • //AN_Xml:
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • //AN_Xml:
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。
  • //AN_Xml:
  • write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
  • //AN_Xml:
//AN_Xml:

除了这 8 种同步操作之外,还规定了下面这些同步规则来保证这些同步操作的正确执行(了解即可,无需死记硬背):

//AN_Xml:
    //AN_Xml:
  • 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中。
  • //AN_Xml:
  • 一个新的变量只能在主内存中 “诞生”,不允许在工作内存中直接使用一个未被初始化(load 或 assign)的变量,换句话说就是对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作。
  • //AN_Xml:
  • 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。
  • //AN_Xml:
  • 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值。
  • //AN_Xml:
  • 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定住的变量。
  • //AN_Xml:
  • ……
  • //AN_Xml:
//AN_Xml:

Java 内存区域和 JMM 有何区别?

//AN_Xml:

这是一个比较常见的问题,很多初学者非常容易搞混。 Java 内存区域和内存模型是完全不一样的两个东西

//AN_Xml:
    //AN_Xml:
  • JVM 内存结构和 Java 虚拟机的运行时区域相关,定义了 JVM 在运行时如何分区存储程序数据,就比如说堆主要用于存放对象实例。
  • //AN_Xml:
  • Java 内存模型和 Java 的并发编程相关,抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中,规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。
  • //AN_Xml:
//AN_Xml:

happens-before 原则是什么?

//AN_Xml:

happens-before 这个概念最早诞生于 Leslie Lamport 于 1978 年发表的论文《Time,Clocks and the Ordering of Events in a Distributed System》。在这篇论文中,Leslie Lamport 提出了逻辑时钟的概念,这也成了第一个逻辑时钟算法 。在分布式环境中,通过一系列规则来定义逻辑时钟的变化,从而能通过逻辑时钟来对分布式系统中的事件的先后顺序进行判断。逻辑时钟并不度量时间本身,仅区分事件发生的前后顺序,其本质就是定义了一种 happens-before 关系。

//AN_Xml:

上面提到的 happens-before 这个概念诞生的背景并不是重点,简单了解即可。

//AN_Xml:

JSR 133 引入了 happens-before 这个概念来描述两个操作之间的内存可见性。

//AN_Xml:

为什么需要 happens-before 原则? happens-before 原则的诞生是为了程序员和编译器、处理器之间的平衡。程序员追求的是易于理解和编程的强内存模型,遵守既定规则编码即可。编译器和处理器追求的是较少约束的弱内存模型,让它们尽己所能地去优化性能,让性能最大化。happens-before 原则的设计思想其实非常简单:

//AN_Xml:
    //AN_Xml:
  • 为了对编译器和处理器的约束尽可能少,只要不改变程序的执行结果(单线程程序和正确执行的多线程程序),编译器和处理器怎么进行重排序优化都行。
  • //AN_Xml:
  • 对于会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序。
  • //AN_Xml:
//AN_Xml:

下面这张是我根据 《Java 并发编程的艺术》这本书中的一张 JMM 设计思想示意图重新绘制的。

//AN_Xml:

 JMM 设计思想

//AN_Xml:

了解了 happens-before 原则的设计思想,我们再来看看 JSR-133 对 happens-before 原则的定义:

//AN_Xml:
    //AN_Xml:
  • 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,并且第一个操作的执行顺序排在第二个操作之前。
  • //AN_Xml:
  • 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系来执行的结果一致,那么 JMM 也允许这样的重排序。
  • //AN_Xml:
//AN_Xml:

我们看下面这段代码:

//AN_Xml:
int userNum = getUserNum();   // 1
//AN_Xml:int teacherNum = getTeacherNum();   // 2
//AN_Xml:int totalNum = userNum + teacherNum;  // 3
//AN_Xml:
    //AN_Xml:
  • 1 happens-before 2
  • //AN_Xml:
  • 2 happens-before 3
  • //AN_Xml:
  • 1 happens-before 3
  • //AN_Xml:
//AN_Xml:

虽然 1 happens-before 2,但对 1 和 2 进行重排序不会影响代码的执行结果,所以 JMM 是允许编译器和处理器执行这种重排序的。但 1 和 2 必须是在 3 执行之前,也就是说 1,2 happens-before 3 。

//AN_Xml:

happens-before 原则表达的意义其实并不是一个操作发生在另外一个操作的前面,虽然这从程序员的角度上来说也并无大碍。更准确地来说,它更想表达的意义是前一个操作的结果对于后一个操作是可见的,无论这两个操作是否在同一个线程里。

//AN_Xml:

举个例子:操作 1 happens-before 操作 2,即使操作 1 和操作 2 不在同一个线程内,JMM 也会保证操作 1 的结果对操作 2 是可见的。

//AN_Xml:

happens-before 常见规则有哪些?谈谈你的理解?

//AN_Xml:

happens-before 的规则就 8 条,说多不多,重点了解下面列举的 5 条即可。全记是不可能的,很快就忘记了,意义不大,随时查阅即可。

//AN_Xml:
    //AN_Xml:
  1. 程序顺序规则:一个线程内,按照代码顺序,书写在前面的操作 happens-before 于书写在后面的操作;
  2. //AN_Xml:
  3. 解锁规则:解锁 happens-before 于加锁;
  4. //AN_Xml:
  5. volatile 变量规则:对一个 volatile 变量的写操作 happens-before 于后面对这个 volatile 变量的读操作。说白了就是对 volatile 变量的写操作的结果对于发生于其后的任何操作都是可见的。
  6. //AN_Xml:
  7. 传递规则:如果 A happens-before B,且 B happens-before C,那么 A happens-before C;
  8. //AN_Xml:
  9. 线程启动规则:Thread 对象的 start()方法 happens-before 于此线程的每一个动作。
  10. //AN_Xml:
//AN_Xml:

如果两个操作不满足上述任意一个 happens-before 规则,那么这两个操作就没有顺序的保障,JVM 可以对这两个操作进行重排序。

//AN_Xml:

happens-before 和 JMM 什么关系?

//AN_Xml:

happens-before 与 JMM 的关系如下图所示:

//AN_Xml:

jmm-vs-happens-before

//AN_Xml:
    //AN_Xml:
  • JMM 向程序员提供了 “ happens-before 规则 ”(如程序顺序规则、volatile 变量规则等)。这是一种 “ 强内存模型 ” 的假象:程序员不需要关心底层复杂的重排序细节,只需要按照这些规则编写代码,就能保证多线程下的内存可见性。
  • //AN_Xml:
  • JVM 在执行时,会将 happens-before 规则映射到具体的实现上。为了在保证正确性的前提下不丧失性能,JMM 只会 “ 禁止影响执行结果的重排序 ”。对于不影响单线程执行结果的重排序,JMM 是允许的。
  • //AN_Xml:
  • 最底层是编译器和处理器真实的 “ 重排序规则 ”
  • //AN_Xml:
//AN_Xml:

总结来说,JMM 就像是一个中间层:它向上通过 happens-before 为程序员提供简单的编程模型;向下通过禁止特定重排序,利用底层硬件性能。这种设计既保证了多线程的安全性,又最大限度释放了硬件的性能。

//AN_Xml:

再看并发编程三个重要特性

//AN_Xml:

原子性

//AN_Xml:

一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。

//AN_Xml:

在 Java 中,可以借助synchronized、各种 Lock 以及各种原子类实现原子性。

//AN_Xml:

synchronized 和各种 Lock 可以保证任一时刻只有一个线程访问该代码块,因此可以保障原子性。各种原子类是利用 CAS (compare and swap) 操作(可能也会用到 volatile或者final关键字)来保证原子操作。

//AN_Xml:

可见性

//AN_Xml:

当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。

//AN_Xml:

在 Java 中,可以借助synchronizedvolatile 以及各种 Lock 实现可见性。

//AN_Xml:

如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

//AN_Xml:

有序性

//AN_Xml:

由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。

//AN_Xml:

我们上面讲重排序的时候也提到过:

//AN_Xml:
//AN_Xml:

指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。

//AN_Xml:
//AN_Xml:

在 Java 中,volatile 关键字可以禁止指令进行重排序优化。

//AN_Xml:

总结

//AN_Xml:
    //AN_Xml:
  • Java 是最早尝试提供内存模型的语言,其主要目的是为了简化多线程编程,增强程序可移植性的。
  • //AN_Xml:
  • CPU 可以通过制定缓存一致协议(比如 MESI 协议)来解决内存缓存不一致性问题。
  • //AN_Xml:
  • 为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序。 简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。
  • //AN_Xml:
  • 你可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。
  • //AN_Xml:
  • JSR 133 引入了 happens-before 这个概念来描述两个操作之间的内存可见性。
  • //AN_Xml:
//AN_Xml:

参考

//AN_Xml: //AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: RabbitMQ常见问题总结 //AN_Xml: https://javaguide.cn/high-performance/message-queue/rabbitmq-questions.html //AN_Xml: https://javaguide.cn/high-performance/message-queue/rabbitmq-questions.html //AN_Xml: RabbitMQ常见问题总结 //AN_Xml: 本文总结 RabbitMQ 常见面试题与核心知识点,涵盖 AMQP 协议、Exchange 交换机类型(Direct/Topic/Fanout)、消息确认机制、死信队列、延迟队列、优先级队列、高可用集群(镜像队列)等,助力 RabbitMQ 学习与面试准备。 //AN_Xml: 高性能 //AN_Xml: Tue, 02 Aug 2022 10:06:55 GMT //AN_Xml: RabbitMQ 作为老牌消息中间件,凭借其成熟的路由机制、丰富的协议支持和完善的可靠性保障,在企业级应用中占据重要地位。但自 RabbitMQ 3.8 引入 Quorum Queue、3.9 引入 Streams、4.0 移除镜像队列以来,其技术架构发生了重大变化,许多传统的最佳实践已不再适用。

//AN_Xml:

本文已针对 RabbitMQ 4.0 进行全面更新,明确标注各特性的版本依赖,特别强调了镜像队列(已移除)、Quorum Queue(推荐)和 Streams(3.9+)的选型差异。

//AN_Xml:

RabbitMQ 是什么?

//AN_Xml:

RabbitMQ 是一个在 AMQP(Advanced Message Queuing Protocol )基础上实现的,可复用的企业消息系统。它可以用于大型软件系统各个模块之间的高效通信,支持高并发,支持可扩展。它支持多种客户端如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP 等,支持 AJAX,持久化,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

//AN_Xml:

RabbitMQ 是使用 Erlang 编写的一个开源的消息队列,本身支持很多的协议:AMQP、XMPP、SMTP、STOMP,也正是如此,使得它变得非常重量级,更适合于企业级的开发。它同时实现了一个 Broker 构架,这意味着消息在发送给客户端时先在中心队列排队,对路由(Routing)、负载均衡(Load Balance)或者数据持久化都有很好的支持。

//AN_Xml:

RabbitMQ 特点

//AN_Xml:
    //AN_Xml:
  • 可靠性:RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认及发布确认等。
  • //AN_Xml:
  • 灵活的路由:在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能,RabbitMQ 已经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。
  • //AN_Xml:
  • 扩展性:多个 RabbitMQ 节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中的节点。
  • //AN_Xml:
  • 高可用性:Quorum Queue 基于 Raft 协议实现数据复制,Streams 支持多节点副本,在部分节点出现问题的情况下队列仍然可用。
  • //AN_Xml:
  • 多种协议:RabbitMQ 除了原生支持 AMQP 协议,还支持 STOMP、MQTT 等多种消息中间件协议。
  • //AN_Xml:
  • 多语言客户端:RabbitMQ 几乎支持所有常用语言,比如 Java、Python、Ruby、PHP、C#、JavaScript 等。
  • //AN_Xml:
  • 管理界面:RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集群中的节点等。
  • //AN_Xml:
  • 插件机制:RabbitMQ 提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。
  • //AN_Xml:
//AN_Xml:

RabbitMQ 核心概念?

//AN_Xml:

RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。可以把消息传递的过程想象成:当你将一个包裹送到邮局,邮局会暂存并最终将邮件通过邮递员送到收件人的手上,RabbitMQ 就好比由邮局、邮箱和邮递员组成的一个系统。从计算机术语层面来说,RabbitMQ 模型更像是一种交换机模型。

//AN_Xml:

RabbitMQ 的整体模型架构如下:

//AN_Xml:

RabbitMQ 4.0 核心架构与消息生命周期流转图

//AN_Xml:

下面我会一一介绍上图中的一些概念。

//AN_Xml:

Producer(生产者) 和 Consumer(消费者)

//AN_Xml:
    //AN_Xml:
  • Producer(生产者) :生产消息的一方(邮件投递者)
  • //AN_Xml:
  • Consumer(消费者) :消费消息的一方(邮件收件人)
  • //AN_Xml:
//AN_Xml:

消息一般由 2 部分组成:消息头(或者说是标签 Label)和 消息体。消息体也可以称为 payload,消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括 routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。生产者把消息交由 RabbitMQ 后,RabbitMQ 会根据消息头把消息发送给感兴趣的 Consumer(消费者)。

//AN_Xml:

Exchange(交换器)

//AN_Xml:

在 RabbitMQ 中,消息并不是直接被投递到 Queue(消息队列) 中的,中间还必须经过 Exchange(交换器) 这一层,Exchange(交换器) 会把我们的消息分配到对应的 Queue(消息队列) 中。

//AN_Xml:

Exchange(交换器) 用来接收生产者发送的消息并将这些消息路由给服务器中的队列中,如果路由不到,或许会返回给 Producer(生产者) ,或许会被直接丢弃掉 。这里可以将 RabbitMQ 中的交换器看作一个简单的实体。

//AN_Xml:

RabbitMQ 的 Exchange(交换器) 有 4 种类型,不同的类型对应着不同的路由策略directfanout, topic, 和 headers,不同类型的 Exchange 转发消息的策略有所区别。这个会在介绍 Exchange Types(交换器类型) 的时候介绍到。

//AN_Xml:
//AN_Xml:

注意:AMQP 规范定义了一个默认交换器(Default Exchange),它是一个 pre-declared 的 direct 类型交换器,但创建新交换器时必须显式指定类型,不能省略。

//AN_Xml:
//AN_Xml:

生产者将消息发给交换器的时候,一般会指定一个 RoutingKey(路由键),用来指定这个消息的路由规则,而这个 RoutingKey 需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效

//AN_Xml:

RabbitMQ 中通过 Binding(绑定)Exchange(交换器)Queue(消息队列) 关联起来,在绑定的时候一般会指定一个 BindingKey(绑定键) ,这样 RabbitMQ 就知道如何正确将消息路由到队列了,如下图所示。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和 Queue 的绑定可以是多对多的关系。

//AN_Xml:

生产者将消息发送给交换器时,需要一个 RoutingKey,当 BindingKey 和 RoutingKey 相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的 BindingKey。BindingKey 并不是在所有的情况下都生效,它依赖于交换器类型,比如 fanout 类型的交换器就会无视,而是将消息路由到所有绑定到该交换器的队列中。

//AN_Xml:

Queue(消息队列)

//AN_Xml:

Queue(消息队列) 用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。

//AN_Xml:

RabbitMQ 在经典架构中,消息只能存储在 队列 中,这一点和 Kafka 这种消息中间件相反。Kafka 将消息存储在 topic(主题) 这个逻辑层面,而相对应的队列逻辑只是 topic 实际存储文件中的位移标识。RabbitMQ 的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。

//AN_Xml:
//AN_Xml:

版本说明(3.9+ 重要更新):从 RabbitMQ 3.9 版本开始,官方引入了 Streams 数据结构。Streams 提供了一种类似 Kafka 的 append-only 日志存储模型,支持非破坏性消费、大规模消息堆积以及基于 Offset 的历史数据重放(Replay)。

//AN_Xml:

架构选型建议

//AN_Xml:
    //AN_Xml:
  • 普通队列:适用于传统消息队列场景,消息被消费后即删除
  • //AN_Xml:
  • Streams:适用于需要高频重放、海量堆积或事件溯源的场景
  • //AN_Xml:
  • 核心瓶颈差异:使用 Stream 时,磁盘 I/O 吞吐量(MB/s)取代了传统的每秒入队率(msg/s)成为核心瓶颈指标
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

多个消费者可以订阅同一个队列,默认情况下队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理,这样避免消息被重复消费。

//AN_Xml:
//AN_Xml:

注意:实际分发策略受 prefetch_count 参数影响。默认行为(prefetch_count=0)会尽可能多地分发消息给各 Consumer,可能导致负载不均。推荐设置 prefetch_count=1 或更高值,让 Consumer 确认后再发送下一条,实现公平分发。

//AN_Xml:
//AN_Xml:

RabbitMQ 不支持队列层面的广播消费,如果有广播消费的需求,需要在其上进行二次开发,这样会很麻烦,不建议这样做。

//AN_Xml:

Broker(消息中间件的服务节点)

//AN_Xml:

对于 RabbitMQ 来说,一个 RabbitMQ Broker 可以简单地看作一个 RabbitMQ 服务节点,或者 RabbitMQ 服务实例。大多数情况下也可以将一个 RabbitMQ Broker 看作一台 RabbitMQ 服务器。

//AN_Xml:

Exchange Types(交换器类型)

//AN_Xml:

RabbitMQ 常用的 Exchange Type 有 fanoutdirecttopicheaders 这四种(AMQP 规范里还提到两种 Exchange Type,分别为 system 与自定义,这里不予以描述)。

//AN_Xml:

RabbitMQ Exchange 四种类型对比

//AN_Xml:

1、fanout(广播模式)

//AN_Xml:
    //AN_Xml:
  • 路由规则:把所有发送到该 Exchange 的消息路由到所有与它绑定的 Queue 中,忽略 BindingKey
  • //AN_Xml:
  • 特点:不需要做任何判断操作,是所有交换机类型里面速度最快的
  • //AN_Xml:
  • 典型使用场景: //AN_Xml:
      //AN_Xml:
    • 系统配置更新广播(如配置中心推送)
    • //AN_Xml:
    • 实时排行榜同步(多实例数据同步)
    • //AN_Xml:
    • 缓存失效广播(如 Redis 缓存清理通知)
    • //AN_Xml:
    • 日志分发(将日志同时发送到多个存储系统)
    • //AN_Xml:
    //AN_Xml:
  • //AN_Xml:
//AN_Xml:

2、direct(直连模式)

//AN_Xml:
    //AN_Xml:
  • 路由规则:把消息路由到那些 BindingKey 与 RoutingKey 完全匹配的 Queue 中
  • //AN_Xml:
  • 特点:精确匹配,路由效率高
  • //AN_Xml:
  • 典型使用场景: //AN_Xml:
      //AN_Xml:
    • 基础点对点任务分发:根据任务级别路由(如 errorwarninginfo
    • //AN_Xml:
    • 优先级队列:高优先级任务分配更多资源
    • //AN_Xml:
    • 按服务类型分发(如 order-servicepayment-service
    • //AN_Xml:
    //AN_Xml:
  • //AN_Xml:
//AN_Xml:

示例:以上图为例,如果发送消息时设置路由键为 "warning",消息会路由到 Queue1 和 Queue2;如果设置路由键为 "info""debug",消息只会路由到 Queue2。

//AN_Xml:

3、topic(主题模式)

//AN_Xml:
    //AN_Xml:
  • 路由规则:基于 BindingKey 和 RoutingKey 的模糊匹配
  • //AN_Xml:
  • 匹配规则: //AN_Xml:
      //AN_Xml:
    • RoutingKey 为点号 "." 分隔的字符串(如 com.rabbitmq.clientorder.china.beijing
    • //AN_Xml:
    • BindingKey 中可以使用两种通配符: //AN_Xml:
        //AN_Xml:
      • "*":匹配一个单词
      • //AN_Xml:
      • "#":匹配零个或多个单词
      • //AN_Xml:
      //AN_Xml:
    • //AN_Xml:
    //AN_Xml:
  • //AN_Xml:
  • 典型使用场景: //AN_Xml:
      //AN_Xml:
    • 按地域或业务模块过滤(如 order.china.* 匹配中国所有地区订单)
    • //AN_Xml:
    • 多级路由(如 com.rabbitmq.clientjava.util.concurrent
    • //AN_Xml:
    • 发布订阅系统(分类通知、按标签订阅)
    • //AN_Xml:
    //AN_Xml:
  • //AN_Xml:
//AN_Xml:

示例

//AN_Xml:
    //AN_Xml:
  • 路由键为 "com.rabbitmq.client" 的消息会同时路由到绑定 "*.rabbitmq.*""*.client.#" 的队列
  • //AN_Xml:
  • 路由键为 "order.china.beijing" 的消息会路由到绑定 "order.china.*" 的队列
  • //AN_Xml:
//AN_Xml:

4、headers(不推荐)

//AN_Xml:
    //AN_Xml:
  • 路由规则:根据消息内容中的 headers 键值对进行匹配
  • //AN_Xml:
  • 特点: //AN_Xml:
      //AN_Xml:
    • 不依赖 RoutingKey,支持 x-match=all(全部匹配)或 x-match=any(任一匹配)
    • //AN_Xml:
    • 性能较差,匹配效率远低于其他三种类型
    • //AN_Xml:
    //AN_Xml:
  • //AN_Xml:
  • 典型使用场景: //AN_Xml:
      //AN_Xml:
    • 几乎不使用,面试时可提到"因为匹配性能较差,生产环境建议用 Topic 替代"
    • //AN_Xml:
    • 仅适用于极其复杂的路由规则且消息量极小的场景
    • //AN_Xml:
    //AN_Xml:
  • //AN_Xml:
//AN_Xml:

AMQP 是什么?

//AN_Xml:

RabbitMQ 就是 AMQP 协议的 Erlang 的实现(当然 RabbitMQ 还支持 STOMPMQTT 等协议)。AMQP 的模型架构 和 RabbitMQ 的模型架构是一样的,生产者将消息发送给交换器,交换器和队列绑定。

//AN_Xml:

RabbitMQ 中的交换器、交换器类型、队列、绑定、路由键等都是遵循的 AMQP 协议中相应的概念。

//AN_Xml:
//AN_Xml:

版本说明

//AN_Xml:
    //AN_Xml:
  • AMQP 0-9-1:RabbitMQ 的传统协议,广泛使用,功能完整
  • //AN_Xml:
  • AMQP 1.0:RabbitMQ 4.x 已将其提升为一等公民协议,显著优化了原生 AMQP 1.0 的解析效率,不再需要像旧版本那样通过复杂的插件转换。这提升了与其他消息中间件(如 ActiveMQ、Service Bus)的互操作性,适合需要跨平台集成的场景
  • //AN_Xml:
  • 新项目可考虑使用 AMQP 1.0 以获得更好的跨平台兼容性
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

AMQP 协议的三层

//AN_Xml:
    //AN_Xml:
  • Module Layer:协议最高层,主要定义了一些客户端调用的命令,客户端可以用这些命令实现自己的业务逻辑。
  • //AN_Xml:
  • Session Layer:中间层,主要负责客户端命令发送给服务器,再将服务端应答返回客户端,提供可靠性同步机制和错误处理。
  • //AN_Xml:
  • TransportLayer:最底层,主要传输二进制数据流,提供帧的处理、信道复用、错误检测和数据表示等。
  • //AN_Xml:
//AN_Xml:

AMQP 模型的三大组件

//AN_Xml:
    //AN_Xml:
  • 交换器 (Exchange):消息代理服务器中用于把消息路由到队列的组件。
  • //AN_Xml:
  • 队列 (Queue):用来存储消息的数据结构,位于硬盘或内存中。
  • //AN_Xml:
  • 绑定 (Binding):一套规则,告知交换器消息应该将消息投递给哪个队列。
  • //AN_Xml:
//AN_Xml:

说说生产者 Producer 和消费者 Consumer

//AN_Xml:

生产者

//AN_Xml:
    //AN_Xml:
  • 消息生产者,就是投递消息的一方。
  • //AN_Xml:
  • 消息一般包含两个部分:消息体(payload)和消息头(Label/Headers)。
  • //AN_Xml:
//AN_Xml:

消费者

//AN_Xml:
    //AN_Xml:
  • 消费消息,也就是接收消息的一方。
  • //AN_Xml:
  • 消费者连接到 RabbitMQ 服务器,并订阅到队列上。消费消息时只消费消息体,丢弃标签。
  • //AN_Xml:
//AN_Xml:

说说 Broker 服务节点、Queue 队列、Exchange 交换器?

//AN_Xml:
    //AN_Xml:
  • Broker:可以看做 RabbitMQ 的服务节点。一般情况下一个 Broker 可以看做一个 RabbitMQ 服务器。
  • //AN_Xml:
  • Queue:RabbitMQ 的内部对象,用于存储消息。多个消费者可以订阅同一队列,这时队列中的消息会被平摊(轮询)给多个消费者进行处理。
  • //AN_Xml:
  • Exchange:生产者将消息发送到交换器,由交换器将消息路由到一个或者多个队列中。当路由不到时,或返回给生产者或直接丢弃。
  • //AN_Xml:
//AN_Xml:

什么是死信队列?如何导致的?

//AN_Xml:

DLX,全称为 Dead-Letter-Exchange(死信交换器),当消息在一个队列中变成死信(dead message)之后,它能被重新发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。

//AN_Xml:

导致的死信的几种原因

//AN_Xml:
    //AN_Xml:
  • 消息被拒(Basic.RejectBasic.Nack)且 requeue = false
  • //AN_Xml:
  • 消息 TTL 过期。
  • //AN_Xml:
  • 队列满了,无法再添加。
  • //AN_Xml:
//AN_Xml:

什么是延迟队列?RabbitMQ 怎么实现延迟队列?

//AN_Xml:

延迟队列指的是存储对应的延迟消息,消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。

//AN_Xml:

RabbitMQ 本身是没有延迟队列的,要实现延迟消息,一般有两种方式:

//AN_Xml:
    //AN_Xml:
  1. //AN_Xml:

    通过 RabbitMQ 本身队列的特性来实现,需要使用 RabbitMQ 的死信交换机(Exchange)和消息的存活时间 TTL(Time To Live)。

    //AN_Xml:
      //AN_Xml:
    • 缺点:消息按队列过期而非单消息级别(除非给每个消息单独队列)
    • //AN_Xml:
    //AN_Xml:
  2. //AN_Xml:
  3. //AN_Xml:

    在 RabbitMQ 3.5.7 及以上的版本提供了一个插件(rabbitmq-delayed-message-exchange)来实现延迟队列功能。同时,插件依赖 Erlang/OTP 18.0 及以上。

    //AN_Xml:
      //AN_Xml:
    • 原理:将消息暂存在 Mnesia 表中,定时轮询并投递到目标交换器
    • //AN_Xml:
    • 容量边界警告(严重):该插件将延迟消息全部暂存在 Erlang 的 Mnesia 内部数据库中,不具备良好的磁盘换页(Paging)能力。如果单节点堆积数十万到上百万级别的延迟消息,会导致 Broker 内存剧增甚至触发内存高水位(Memory Watermark)告警,进而产生 全局背压(Global Backpressure) 阻塞所有生产者的 TCP 连接。
    • //AN_Xml:
    • 生产建议:针对海量延迟(千万级以上),必须退化使用外部定时任务(如时间轮、SchedulerX、XXL-JOB)调度或死信链表方案
    • //AN_Xml:
    //AN_Xml:
  4. //AN_Xml:
//AN_Xml:

也就是说,AMQP 协议以及 RabbitMQ 本身没有直接支持延迟队列的功能,但是可以通过 TTL 和 DLX 模拟出延迟队列的功能。

//AN_Xml:

什么是优先级队列?

//AN_Xml:

RabbitMQ 自 V3.5.0 有优先级队列实现,优先级高的队列会先被消费。

//AN_Xml:

可以通过x-max-priority参数来实现优先级队列。不过,当消费速度大于生产速度且 Broker 没有堆积的情况下,优先级显得没有意义。

//AN_Xml:

RabbitMQ 有哪些工作模式?

//AN_Xml:
    //AN_Xml:
  • 简单模式
  • //AN_Xml:
  • work 工作模式
  • //AN_Xml:
  • pub/sub 发布订阅模式
  • //AN_Xml:
  • Routing 路由模式
  • //AN_Xml:
  • Topic 主题模式
  • //AN_Xml:
//AN_Xml:

RabbitMQ 消息怎么传输?

//AN_Xml:

由于 TCP 链接的创建和销毁开销较大(三次握手、慢启动等),且并发数受系统资源限制,会造成性能瓶颈,所以 RabbitMQ 使用信道的方式来传输数据。信道(Channel)是生产者、消费者与 RabbitMQ 通信的渠道,信道是建立在 TCP 链接上的虚拟链接。

//AN_Xml:
//AN_Xml:

注意:

//AN_Xml:
    //AN_Xml:
  • 单个 TCP 连接可承载多个 Channel,但官方建议不超过 100-200 个/连接
  • //AN_Xml:
  • 每个 Channel 有独立的编号,但共享同一 TCP 连接的流量控制
  • //AN_Xml:
  • Channel 不是线程安全的,多线程应使用不同 Channel 实例
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

如何保证消息的可靠性?

//AN_Xml:

RabbitMQ 4.0 消息可靠性与队列架构全景图

//AN_Xml:

消息可能在三个环节丢失:生产者 → Broker、Broker 存储期间、Broker → 消费者

//AN_Xml:

1. 生产者 → Broker

//AN_Xml:

保证生产者端零丢失需要双重机制兜底

//AN_Xml:
    //AN_Xml:
  • //AN_Xml:

    Publisher Confirms(异步确认):确认消息是否到达 Broker

    //AN_Xml:
    channel.confirmSelect();
    //AN_Xml:channel.addConfirmListener((sequenceNumber, multiple) -> {
    //AN_Xml:    // 消息已到达 Broker 并落盘/同步到镜像
    //AN_Xml:}, (sequenceNumber, multiple) -> {
    //AN_Xml:    // 消息未到达 Broker,记录日志并重试
    //AN_Xml:});
    //AN_Xml:
  • //AN_Xml:
  • //AN_Xml:

    Mandatory + Return Listener(路由失败处理):捕获消息到达 Exchange 但无法路由到 Queue 的情况

    //AN_Xml:
    // 开启 mandatory 模式
    //AN_Xml:channel.basicPublish("exchange", "routingKey",
    //AN_Xml:    true,  // mandatory=true
    //AN_Xml:    null,
    //AN_Xml:    messageBody);
    //AN_Xml:
    //AN_Xml:// 配置 Return Listener
    //AN_Xml:channel.addReturnListener((replyCode, replyText, exchange, routingKey, properties, body) -> {
    //AN_Xml:    // 消息到达 Exchange 但路由失败,记录日志或发送到备用交换器
    //AN_Xml:    log.error("Message returned: {}", replyText);
    //AN_Xml:});
    //AN_Xml:
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

关键警告:若仅开启 Confirm 未处理 Return,配置漂移(如误删队列或绑定)会导致生产者认为发送成功,但消息在 Broker 内部被静默丢弃,形成消息黑洞

//AN_Xml:
//AN_Xml:
    //AN_Xml:
  • 事务机制(不推荐):同步阻塞,性能显著下降(官方文档未给出具体倍数,实际影响取决于消息大小和网络延迟) //AN_Xml:
      //AN_Xml:
    • 注意:事务机制和 Confirm 机制是互斥的,两者不能共存
    • //AN_Xml:
    //AN_Xml:
  • //AN_Xml:
//AN_Xml:

2. Broker 存储期间

//AN_Xml:
    //AN_Xml:
  • 消息持久化delivery_mode=2,消息写入磁盘
  • //AN_Xml:
  • 队列持久化durable=true,重启后队列重建
  • //AN_Xml:
  • 集群模式: //AN_Xml:
      //AN_Xml:
    • 镜像队列(Classic Queue Mirroring,已于 4.0 移除):主从同步,仅用于老版本维护
    • //AN_Xml:
    • Quorum Queue(3.8+ 推荐,4.0 后为默认):基于 Raft 协议,支持更严格的仲裁写入(N/2 + 1)
    • //AN_Xml:
    • Streams(3.9+):适用于事件溯源和高频重放场景
    • //AN_Xml:
    //AN_Xml:
  • //AN_Xml:
//AN_Xml:

3. Broker → 消费者

//AN_Xml:
    //AN_Xml:
  • 手动 AckbasicAck(deliveryTag, multiple),确保消费成功后再确认
  • //AN_Xml:
  • 重试机制:消费失败时 basicNackbasicRejectrequeue=true
  • //AN_Xml:
  • 死信队列:达到最大重试次数后路由到 DLQ 人工介入
  • //AN_Xml:
  • 幂等性保障:业务层实现,避免重复消费导致的数据不一致。幂等性具体实现方案参考这篇文章:接口幂等方案总结
  • //AN_Xml:
//AN_Xml:

以下时序图展示了从生产者到消费者的完整消息流转及各环节的异常处理策略:

//AN_Xml:

关键路径说明

//AN_Xml:
    //AN_Xml:
  • Confirm + Returns(互为补充): //AN_Xml:
      //AN_Xml:
    • Confirm 确认消息是否到达 Broker 并落盘/同步
    • //AN_Xml:
    • Mandatory + Return Listener 捕获路由失败事件(消息到达 Exchange 但无法进入 Queue)
    • //AN_Xml:
    //AN_Xml:
  • //AN_Xml:
  • Quorum Queue:Raft 多数派确认后才返回 Ack,保证数据不丢
  • //AN_Xml:
  • 手动 Ack:确保消费成功后才删除消息
  • //AN_Xml:
  • DLQ 兜底:重试超限后路由到死信队列,避免消息无限重试
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

注意:Alternate Exchange(备用交换器)是另一种独立的路由失败处理机制,与 Mandatory + Return Listener 互斥。配置 Alternate Exchange 后,路由失败的消息会被转发到备用交换器,生产者收到的是正常的 Confirm Ack 而非 Return。

//AN_Xml:
//AN_Xml:

如何保证 RabbitMQ 消息的顺序性?

//AN_Xml:

RabbitMQ 仅保证单个 Queue 内的 FIFO 顺序,但多消费者场景下可能出现乱序。解决方案:

//AN_Xml:

1. 单 Consumer 模式

//AN_Xml:
    //AN_Xml:
  • 一个 Queue 只绑定一个 Consumer
  • //AN_Xml:
  • 优点:保证顺序
  • //AN_Xml:
  • 缺点:成为瓶颈,吞吐量受限
  • //AN_Xml:
//AN_Xml:

2. 分区有序(推荐,但需注意失效模式)

//AN_Xml:
    //AN_Xml:
  • 按业务 key(如订单ID)哈希到不同 Queue
  • //AN_Xml:
  • 每个 Queue 独立 Consumer
  • //AN_Xml:
  • 优点:既保证顺序又提高吞吐量
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

失效模式警告

//AN_Xml:
    //AN_Xml:
  • 拓扑变更乱序:当后端队列扩缩容导致哈希环发生变化时,同一个业务 Key 的新老消息可能进入不同队列
  • //AN_Xml:
  • 重试乱序:若消费者内部处理失败执行 Nack 并 Requeue,该消息会被重新推入队列尾部,导致后续消息先被消费
  • //AN_Xml:
  • 应用层防护:极端严格顺序场景下,消费者业务表必须设计基于状态机版本号的幂等与防并发覆盖机制
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

3. 内部内存队列(慎重)

//AN_Xml:
    //AN_Xml:
  • 单一 Consumer 内部维护内存队列分发到 Worker 线程池
  • //AN_Xml:
  • 需处理: //AN_Xml:
      //AN_Xml:
    • Consumer 挂掉时内存队列丢失风险
    • //AN_Xml:
    • 需实现背压机制防止 OOM
    • //AN_Xml:
    • 增加 ack 复杂度(需追踪具体 Worker 处理状态)
    • //AN_Xml:
    //AN_Xml:
  • //AN_Xml:
  • 生产环境慎用此方案
  • //AN_Xml:
//AN_Xml:

如何保证 RabbitMQ 高可用的?

//AN_Xml:

RabbitMQ 是比较有代表性的,因为是基于主从(非分布式)做高可用性的,我们就以 RabbitMQ 为例子讲解第一种 MQ 的高可用性怎么实现。RabbitMQ 有四种模式:单机模式、普通集群模式、镜像集群模式(已废弃)、Quorum Queue(推荐)。

//AN_Xml:
//AN_Xml:

版本演进说明

//AN_Xml:
    //AN_Xml:
  • 3.8 前:镜像队列(Classic Queue Mirroring)是主要高可用方案
  • //AN_Xml:
  • 3.8+:Quorum Queue 作为推荐替代方案,镜像队列被标记为 deprecated
  • //AN_Xml:
  • 3.13:镜像队列仍可用但已废弃
  • //AN_Xml:
  • 4.0+:镜像队列完全移除,Quorum Queue 成为默认高可用方案
  • //AN_Xml:
//AN_Xml:

网络分区警告(严重):无论是普通集群还是早期的镜像集群,均依赖 Erlang 内部的分布式同步机制,对网络抖动极度敏感。在多机房或跨可用区部署时,极易发生网络分区(Split-brain)。必须在 rabbitmq.conf 中明确配置分区恢复策略:

//AN_Xml:
    //AN_Xml:
  • pause_minority:少数派节点自动暂停服务以防数据分化(推荐)
  • //AN_Xml:
  • autoheal:自动选择一方继续运行(有数据丢失风险)
  • //AN_Xml:
  • 对于 3.8 以上版本,强烈建议直接使用基于 Raft 一致性算法的 Quorum Queue,从根本上解决网络分区导致的消息丢失与状态不一致问题
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

单机模式

//AN_Xml:

Demo 级别的,一般就是你本地启动了玩玩儿的,没人生产用单机模式。

//AN_Xml:

普通集群模式

//AN_Xml:

意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。

//AN_Xml:

你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个 queue 的读写操作。

//AN_Xml:

镜像集群模式(Classic Queue Mirroring,已废弃)

//AN_Xml:
//AN_Xml:

⚠️ 重要警告:镜像队列已在 RabbitMQ 4.0 中被完全移除。RabbitMQ 3.8 引入 Quorum Queue 作为推荐替代方案,3.13 版本镜像队列仍可用但已废弃,4.0 版本正式移除。新项目请使用 Quorum Queue 或 Streams。

//AN_Xml:
//AN_Xml:

这种模式是 RabbitMQ 早期版本的高可用方案。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据。每次写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。

//AN_Xml:

工作原理

//AN_Xml:
    //AN_Xml:
  • Queue 主节点接收消息,同步到 N 个镜像节点
  • //AN_Xml:
  • 主节点宕机时,最老的镜像节点升级为主节点
  • //AN_Xml:
  • 通过管理控制台新增策略,指定数据同步到所有节点或指定数量的节点
  • //AN_Xml:
//AN_Xml:

优点

//AN_Xml:
    //AN_Xml:
  • 任何机器宕机,其他节点包含该 queue 的完整数据
  • //AN_Xml:
  • Consumer 可以切换到其他节点继续消费
  • //AN_Xml:
//AN_Xml:

缺点

//AN_Xml:
    //AN_Xml:
  • 性能开销大,消息需要同步到所有机器上
  • //AN_Xml:
  • 网络带宽压力和消耗重
  • //AN_Xml:
  • 不是真正的分布式架构,是主从复制
  • //AN_Xml:
//AN_Xml:

Quorum Queue(3.8+ 推荐,4.0 后为默认高可用方案)

//AN_Xml:

基于 Raft 协议的复制队列,是 RabbitMQ 3.8+ 推荐的高可用方案,4.0 后成为默认选项:

//AN_Xml:
    //AN_Xml:
  • 基于 Raft 协议:通过日志复制和选举实现一致性
  • //AN_Xml:
  • 仲裁写入:需要多数节点确认(N/2 + 1)才认为写入成功
  • //AN_Xml:
  • 更严格的一致性:避免镜像队列的脑裂风险
  • //AN_Xml:
  • 适用场景:对可靠性要求高的场景
  • //AN_Xml:
//AN_Xml:

声明方式(客户端)

//AN_Xml:

Java:

//AN_Xml:
// Java 客户端声明 Quorum Queue
//AN_Xml:Map<String, Object> args = new HashMap<>();
//AN_Xml:args.put("x-queue-type", "quorum");  // 关键参数,必须在声明时指定
//AN_Xml:channel.queueDeclare("my-queue", true, false, false, args);
//AN_Xml:

Python:

//AN_Xml:
# Python (pika) 客户端声明 Quorum Queue
//AN_Xml:channel.queue_declare(
//AN_Xml:    queue='my-queue',
//AN_Xml:    durable=True,
//AN_Xml:    arguments={'x-queue-type': 'quorum'}  # 关键参数
//AN_Xml:)
//AN_Xml:
//AN_Xml:

重要提示x-queue-type 参数必须在队列声明时由客户端提供,不能通过 Policy 设置或修改。Policy 只能配置 max-length、delivery-limit 等运行时参数。

//AN_Xml:
//AN_Xml:

如何解决消息队列的延时以及过期失效问题?

//AN_Xml:

RabbitMQ 可以设置消息过期时间(TTL)。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 清理掉,导致数据丢失。

//AN_Xml:

批量重导方案(适用于数据可恢复的场景):

//AN_Xml:

当大量消息积压或过期时,可采取以下步骤:

//AN_Xml:
    //AN_Xml:
  1. 临时丢弃:高峰期直接丢弃无法及时处理的数据,保证系统可用性
  2. //AN_Xml:
  3. 低峰期恢复:在业务低峰期(如凌晨),编写临时程序从数据库中查询丢失的数据
  4. //AN_Xml:
  5. 重新投递:将查询到的数据重新发送到 MQ 中进行补偿
  6. //AN_Xml:
//AN_Xml:

示例场景

//AN_Xml:
    //AN_Xml:
  • 假设 1 万个订单积压在 MQ 中未处理
  • //AN_Xml:
  • 其中 1000 个订单因 TTL 过期被丢弃
  • //AN_Xml:
  • 处理方案:编写临时程序从数据库查询这 1000 个订单,手动重新发送到 MQ 补偿
  • //AN_Xml:
//AN_Xml:

注意事项

//AN_Xml:
    //AN_Xml:
  • 确保数据源(如数据库)中有完整的历史数据
  • //AN_Xml:
  • 补偿过程需要做好幂等性处理,避免重复消费
  • //AN_Xml:
  • 建议设置监控告警,及时发现消息积压情况
  • //AN_Xml:
//AN_Xml:

生产环境最佳实践与监控告警

//AN_Xml:

核心监控指标

//AN_Xml:

1. 内存水位线告警(严重)

//AN_Xml:
    //AN_Xml:
  • 监控 rabbitmq_memory_limit 占比
  • //AN_Xml:
  • 告警阈值:默认高水位为 0.4(40%)
  • //AN_Xml:
  • 影响:一旦达到高水位,RabbitMQ 会直接 block 所有生产者的 TCP Socket(全局背压)
  • //AN_Xml:
  • 建议配置:
    {rabbit, [
    //AN_Xml:  {vm_memory_high_watermark, 0.4},  % 内存高水位 40%
    //AN_Xml:  {vm_memory_high_watermark_paging_ratio, 0.5}  % 开始分页的比例
    //AN_Xml:]}
    //AN_Xml:
  • //AN_Xml:
//AN_Xml:

2. 文件句柄消耗

//AN_Xml:
    //AN_Xml:
  • 监控 File Descriptors 使用率
  • //AN_Xml:
  • 风险:连接数风暴或海量未确认消息会耗尽句柄导致节点 Crash
  • //AN_Xml:
  • 建议值:系统限制至少 100,000+(ulimit -n 100000
  • //AN_Xml:
//AN_Xml:

3. Channel Churn Rate

//AN_Xml:
    //AN_Xml:
  • 监控信道的创建与销毁速率
  • //AN_Xml:
  • 风险:高频创建销毁(而非复用)会导致 Erlang 进程抖动,引发 CPU 飙升
  • //AN_Xml:
  • 生产建议:单连接 Channel 数建议 50-100,避免频繁创建/销毁
  • //AN_Xml:
//AN_Xml:

4. 消息积压深度

//AN_Xml:
    //AN_Xml:
  • 监控 Queue 消息数量和 Consumer Lag
  • //AN_Xml:
  • 告警阈值:根据业务定义(如 > 10,000 条)
  • //AN_Xml:
  • 工具:RabbitMQ Management UI、Prometheus + Grafana
  • //AN_Xml:
//AN_Xml:

5. 磁盘空间与 I/O

//AN_Xml:
    //AN_Xml:
  • 监控磁盘剩余空间和 IOPS
  • //AN_Xml:
  • 告警阈值:磁盘剩余 < 20% 触发告警
  • //AN_Xml:
  • Quorum Queue 对磁盘 I/O 要求较高,建议使用 NVMe SSD
  • //AN_Xml:
//AN_Xml:

常见生产误区与避坑指南

//AN_Xml:

误区 1:Quorum Queue 是银弹,能解决所有问题

//AN_Xml:
    //AN_Xml:
  • 真相:Quorum Queue 的 Raft 日志在 flush 时会 fsync,且 Confirm 需等待多数节点 fsync 后才返回。如果底层不是高性能 NVMe SSD,其吞吐量会受到影响
  • //AN_Xml:
  • 限制:Quorum Queue 会将所有消息(包括 delivery_mode=1 的非持久化消息)强制持久化存储到磁盘
  • //AN_Xml:
  • 选型建议: //AN_Xml:
      //AN_Xml:
    • 高吞吐量场景:考虑 Classic Queue(非镜像,单节点)或 Streams(3.9+)
    • //AN_Xml:
    • 高可靠性场景:使用 Quorum Queue(3.8+)
    • //AN_Xml:
    //AN_Xml:
  • //AN_Xml:
//AN_Xml:

误区 2:Prefetch Count 设置越大越好

//AN_Xml:
    //AN_Xml:
  • 真相:客户端批量拉取大量消息但在本地卡死,导致服务端队列看似空闲,实则消息全部处于 Unacked 状态,拖垮客户端本地内存并阻碍其他消费者接盘
  • //AN_Xml:
  • 生产建议:核心业务初始值设为 10 到 50 之间,根据处理耗时调整
    channel.basicQos(20);  // 推荐起始值
    //AN_Xml:
  • //AN_Xml:
//AN_Xml:

误区 3:延迟队列插件可以无限制使用

//AN_Xml:
    //AN_Xml:
  • 真相:延迟插件将所有延迟消息存储在 Mnesia 内存表中,不支持磁盘换页
  • //AN_Xml:
  • 风险:单节点堆积百万级延迟消息会触发 OOM 或全局背压
  • //AN_Xml:
  • 替代方案:海量延迟场景使用外部定时任务系统(如 XXL-JOB、SchedulerX)
  • //AN_Xml:
//AN_Xml:

误区 4:网络分区不会发生在我们环境

//AN_Xml:
    //AN_Xml:
  • 真相:跨机房部署或网络抖动都会触发 Erlang 的网络分区检测
  • //AN_Xml:
  • 后果:Split-brain 导致消息丢失、状态不一致
  • //AN_Xml:
  • 防护: //AN_Xml:
      //AN_Xml:
    • 3.8+ 使用 Quorum Queue(基于 Raft,天然抗分区)
    • //AN_Xml:
    • 配置分区恢复策略:cluster_partition_handling = pause_minority
    • //AN_Xml:
    //AN_Xml:
  • //AN_Xml:
//AN_Xml:

误区 5:开启了事务机制就万无一失

//AN_Xml:
    //AN_Xml:
  • 真相:事务机制是同步阻塞模式,性能显著低于 Publisher Confirms(官方文档未给出具体倍数,实际影响取决于消息大小和网络延迟)
  • //AN_Xml:
  • 替代方案:使用 Publisher Confirms + Mandatory Returns(异步且高性能)
  • //AN_Xml:
//AN_Xml:

生产配置参考

//AN_Xml:
//AN_Xml:

重要说明:RabbitMQ 3.7+ 使用新的 rabbitmq.conf 格式(sysctl 风格),而非旧的 advanced.config(Erlang 术语格式)。以下配置适用于 rabbitmq.conf

//AN_Xml:
//AN_Xml:
# rabbitmq.conf 生产环境推荐配置
//AN_Xml:
//AN_Xml:# 内存管理
//AN_Xml:vm_memory_high_watermark.relative = 0.4
//AN_Xml:vm_memory_high_watermark_paging_ratio = 0.5
//AN_Xml:
//AN_Xml:# 磁盘管理
//AN_Xml:disk_free_limit.absolute = 5GB
//AN_Xml:
//AN_Xml:# 连接与通道
//AN_Xml:channel_max = 200
//AN_Xml:connection_max = infinity
//AN_Xml:
//AN_Xml:# 心跳检测(秒)
//AN_Xml:heartbeat = 60
//AN_Xml:
//AN_Xml:# 网络分区处理(重要)
//AN_Xml:cluster_partition_handling = pause_minority
//AN_Xml:
//AN_Xml:# 默认用户(生产环境请修改或删除)
//AN_Xml:default_user = guest
//AN_Xml:default_pass = guest
//AN_Xml:loopback_users = none
//AN_Xml:
//AN_Xml:# 管理插件监听端口
//AN_Xml:management.tcp.port = 15672
//AN_Xml:

如需使用 Erlang 术语格式(高级配置),请使用 advanced.config 文件,但不要与 rabbitmq.conf 混用

//AN_Xml:

总结

//AN_Xml:

本文系统梳理了 RabbitMQ 的核心知识点,从基础概念到生产实践,涵盖了面试和实际应用中最重要的内容。让我们回顾一下关键要点:

//AN_Xml:

核心技术架构演进

//AN_Xml:

| 版本里程碑 | 重要变化 | 生产影响 |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: HTTP与RPC对比 //AN_Xml: https://javaguide.cn/distributed-system/rpc/http_rpc.html //AN_Xml: https://javaguide.cn/distributed-system/rpc/http_rpc.html //AN_Xml: HTTP与RPC对比 //AN_Xml: HTTP与RPC对比详解,从TCP层出发讲解两种通信方式的本质区别、性能差异(序列化/连接复用)、传输协议对比及在微服务架构中的选型建议。 //AN_Xml: 分布式 //AN_Xml: Tue, 02 Aug 2022 06:41:11 GMT //AN_Xml: //AN_Xml:

本文来自小白 debug投稿,原文:https://juejin.cn/post/7121882245605883934

//AN_Xml: //AN_Xml:

我想起了我刚工作的时候,第一次接触 RPC 协议,当时就很懵,我 HTTP 协议用的好好的,为什么还要用 RPC 协议?

//AN_Xml:

于是就到网上去搜。

//AN_Xml:

不少解释显得非常官方,我相信大家在各种平台上也都看到过,解释了又好像没解释,都在用一个我们不认识的概念去解释另外一个我们不认识的概念,懂的人不需要看,不懂的人看了还是不懂。

//AN_Xml:

这种看了,又好像没看的感觉,云里雾里的很难受,我懂

//AN_Xml:

为了避免大家有强烈的审丑疲劳,今天我们来尝试重新换个方式讲一讲。

//AN_Xml:

从 TCP 聊起

//AN_Xml:

作为一个程序员,假设我们需要在 A 电脑的进程发一段数据到 B 电脑的进程,我们一般会在代码里使用 socket 进行编程。

//AN_Xml:

这时候,我们可选项一般也就TCP 和 UDP 二选一。TCP 可靠,UDP 不可靠。 除非是马总这种神级程序员(早期 QQ 大量使用 UDP),否则,只要稍微对可靠性有些要求,普通人一般无脑选 TCP 就对了。

//AN_Xml:

类似下面这样。

//AN_Xml:
fd = socket(AF_INET,SOCK_STREAM,0);
//AN_Xml:

其中SOCK_STREAM,是指使用字节流传输数据,说白了就是TCP 协议

//AN_Xml:

在定义了 socket 之后,我们就可以愉快的对这个 socket 进行操作,比如用bind()绑定 IP 端口,用connect()发起建连。

//AN_Xml:

握手建立连接流程

//AN_Xml:

在连接建立之后,我们就可以使用send()发送数据,recv()接收数据。

//AN_Xml:

光这样一个纯裸的 TCP 连接,就可以做到收发数据了,那是不是就够了?

//AN_Xml:

不行,这么用会有问题。

//AN_Xml:

使用纯裸 TCP 会有什么问题

//AN_Xml:

八股文常背,TCP 是有三个特点,面向连接可靠、基于字节流

//AN_Xml:

TCP是什么

//AN_Xml:

这三个特点真的概括的 非常精辟 ,这个八股文我们没白背。

//AN_Xml:

每个特点展开都能聊一篇文章,而今天我们需要关注的是 基于字节流 这一点。

//AN_Xml:

字节流可以理解为一个双向的通道里流淌的二进制数据,也就是 01 串 。纯裸 TCP 收发的这些 01 串之间是 没有任何边界 的,你根本不知道到哪个地方才算一条完整消息。

//AN_Xml:

01二进制字节流

//AN_Xml:

正因为这个没有任何边界的特点,所以当我们选择使用 TCP 发送 "夏洛"和"特烦恼" 的时候,接收端收到的就是 "夏洛特烦恼" ,这时候接收端没发区分你是想要表达 "夏洛"+"特烦恼" 还是 "夏洛特"+"烦恼"

//AN_Xml:

消息对比

//AN_Xml:

这就是所谓的 粘包问题,之前也写过一篇专门的文章聊过这个问题。

//AN_Xml:

说这个的目的是为了告诉大家,纯裸 TCP 是不能直接拿来用的,你需要在这个基础上加入一些 自定义的规则 ,用于区分 消息边界

//AN_Xml:

于是我们会把每条要发送的数据都包装一下,比如加入 消息头 ,消息头里写清楚一个完整的包长度是多少,根据这个长度可以继续接收数据,截取出来后它们就是我们真正要传输的 消息体

//AN_Xml:

消息边界长度标志

//AN_Xml:

而这里头提到的 消息头 ,还可以放各种东西,比如消息体是否被压缩过和消息体格式之类的,只要上下游都约定好了,互相都认就可以了,这就是所谓的 协议。

//AN_Xml:

每个使用 TCP 的项目都可能会定义一套类似这样的协议解析标准,他们可能 有区别,但原理都类似

//AN_Xml:

于是基于 TCP,就衍生了非常多的协议,比如 HTTP 和 RPC。

//AN_Xml:

HTTP 和 RPC

//AN_Xml:

RPC 其实是一种调用方式

//AN_Xml:

我们回过头来看网络的分层图。

//AN_Xml:

四层网络协议

//AN_Xml:

TCP 是传输层的协议 ,而基于 TCP 造出来的 HTTP 和各类 RPC 协议,它们都只是定义了不同消息格式的 应用层协议 而已。

//AN_Xml:

HTTPHyper Text Transfer Protocol)协议又叫做 超文本传输协议 。我们用的比较多,平时上网在浏览器上敲个网址就能访问网页,这里用到的就是 HTTP 协议。

//AN_Xml:

HTTP调用

//AN_Xml:

RPCRemote Procedure Call)又叫做 远程过程调用,它本身并不是一个具体的协议,而是一种 调用方式

//AN_Xml:

举个例子,我们平时调用一个 本地方法 就像下面这样。

//AN_Xml:
 res = localFunc(req)
//AN_Xml:

如果现在这不是个本地方法,而是个远端服务器暴露出来的一个方法remoteFunc,如果我们还能像调用本地方法那样去调用它,这样就可以屏蔽掉一些网络细节,用起来更方便,岂不美哉?

//AN_Xml:
res = remoteFunc(req)
//AN_Xml:

RPC可以像调用本地方法那样调用远端方法

//AN_Xml:

基于这个思路,大佬们造出了非常多款式的 RPC 协议,比如比较有名的gRPCthrift

//AN_Xml:

值得注意的是,虽然大部分 RPC 协议底层使用 TCP,但实际上 它们不一定非得使用 TCP,改用 UDP 或者 HTTP,其实也可以做到类似的功能。

//AN_Xml:

到这里,我们回到文章标题的问题。

//AN_Xml:

那既然有 RPC 了,为什么还要有 HTTP 呢?

//AN_Xml:

其实,TCP 是 70 年 代出来的协议,而 HTTP 是 90 年代 才开始流行的。而直接使用裸 TCP 会有问题,可想而知,这中间这么多年有多少自定义的协议,而这里面就有 80 年代 出来的RPC

//AN_Xml:

所以我们该问的不是 既然有 HTTP 协议为什么要有 RPC ,而是 为什么有 RPC 还要有 HTTP 协议?

//AN_Xml:

现在电脑上装的各种联网软件,比如 xx 管家,xx 卫士,它们都作为客户端(Client) 需要跟服务端(Server) 建立连接收发消息,此时都会用到应用层协议,在这种 Client/Server (C/S) 架构下,它们可以使用自家造的 RPC 协议,因为它只管连自己公司的服务器就 ok 了。

//AN_Xml:

但有个软件不同,浏览器(Browser) ,不管是 Chrome 还是 IE,它们不仅要能访问自家公司的服务器(Server) ,还需要访问其他公司的网站服务器,因此它们需要有个统一的标准,不然大家没法交流。于是,HTTP 就是那个时代用于统一 Browser/Server (B/S) 的协议。

//AN_Xml:

也就是说在多年以前,HTTP 主要用于 B/S 架构,而 RPC 更多用于 C/S 架构。但现在其实已经没分那么清了,B/S 和 C/S 在慢慢融合。 很多软件同时支持多端,比如某度云盘,既要支持网页版,还要支持手机端和 PC 端,如果通信协议都用 HTTP 的话,那服务器只用同一套就够了。而 RPC 就开始退居幕后,一般用于公司内部集群里,各个微服务之间的通讯。

//AN_Xml:

那这么说的话,都用 HTTP 得了,还用什么 RPC?

//AN_Xml:

仿佛又回到了文章开头的样子,那这就要从它们之间的区别开始说起。

//AN_Xml:

HTTP 和 RPC 有什么区别

//AN_Xml:

我们来看看 RPC 和 HTTP 区别比较明显的几个点。

//AN_Xml:

服务发现

//AN_Xml:

首先要向某个服务器发起请求,你得先建立连接,而建立连接的前提是,你得知道 IP 地址和端口 。这个找到服务对应的 IP 端口的过程,其实就是 服务发现

//AN_Xml:

HTTP 中,你知道服务的域名,就可以通过 DNS 服务 去解析得到它背后的 IP 地址,默认 80 端口

//AN_Xml:

RPC 的话,就有些区别,一般会有专门的中间服务去保存服务名和 IP 信息,比如 Consul、Etcd、Nacos、ZooKeeper,甚至是 Redis。想要访问某个服务,就去这些中间服务去获得 IP 和端口信息。由于 DNS 也是服务发现的一种,所以也有基于 DNS 去做服务发现的组件,比如 CoreDNS

//AN_Xml:

可以看出服务发现这一块,两者是有些区别,但不太能分高低。

//AN_Xml:

底层连接形式

//AN_Xml:

以主流的 HTTP1.1 协议为例,其默认在建立底层 TCP 连接之后会一直保持这个连接(keep alive),之后的请求和响应都会复用这条连接。

//AN_Xml:

RPC 协议,也跟 HTTP 类似,也是通过建立 TCP 长链接进行数据交互,但不同的地方在于,RPC 协议一般还会再建个 连接池,在请求量大的时候,建立多条连接放在池内,要发数据的时候就从池里取一条连接出来,用完放回去,下次再复用,可以说非常环保。

//AN_Xml:

connection_pool

//AN_Xml:

由于连接池有利于提升网络请求性能,所以不少编程语言的网络库里都会给 HTTP 加个连接池,比如 Go 就是这么干的。

//AN_Xml:

可以看出这一块两者也没太大区别,所以也不是关键。

//AN_Xml:

传输的内容

//AN_Xml:

基于 TCP 传输的消息,说到底,无非都是 消息头 Header 和消息体 Body。

//AN_Xml:

Header 是用于标记一些特殊信息,其中最重要的是 消息体长度

//AN_Xml:

Body 则是放我们真正需要传输的内容,而这些内容只能是二进制 01 串,毕竟计算机只认识这玩意。所以 TCP 传字符串和数字都问题不大,因为字符串可以转成编码再变成 01 串,而数字本身也能直接转为二进制。但结构体呢,我们得想个办法将它也转为二进制 01 串,这样的方案现在也有很多现成的,比如 JSON,Protocol Buffers (Protobuf)

//AN_Xml:

这个将结构体转为二进制数组的过程就叫 序列化 ,反过来将二进制数组复原成结构体的过程叫 反序列化

//AN_Xml:

序列化和反序列化

//AN_Xml:

对于主流的 HTTP1.1,虽然它现在叫超文本协议,支持音频视频,但 HTTP 设计 初是用于做网页文本展示的,所以它传的内容以字符串为主。Header 和 Body 都是如此。在 Body 这块,它使用 JSON序列化 结构体数据。

//AN_Xml:

我们可以随便截个图直观看下。

//AN_Xml:

HTTP报文

//AN_Xml:

可以看到这里面的内容非常多的冗余,显得非常啰嗦。最明显的,像 Header 里的那些信息,其实如果我们约定好头部的第几位是 Content-Type,就不需要每次都真的把 Content-Type 这个字段都传过来,类似的情况其实在 Body 的 JSON 结构里也特别明显。

//AN_Xml:

而 RPC,因为它定制化程度更高,可以采用体积更小的 Protobuf 或其他序列化协议去保存结构体数据,同时也不需要像 HTTP 那样考虑各种浏览器行为,比如 302 重定向跳转啥的。因此性能也会更好一些,这也是在公司内部微服务中抛弃 HTTP,选择使用 RPC 的最主要原因。

//AN_Xml:

HTTP原理

//AN_Xml:

RPC原理

//AN_Xml:

当然上面说的 HTTP,其实 特指的是现在主流使用的 HTTP1.1HTTP2在前者的基础上做了很多改进,所以 性能可能比很多 RPC 协议还要好。而 gRPC 正是基于 HTTP/2 实现的(虽然它基于 HTTP/2 的帧格式定义了自己的协议,但传输层仍是 HTTP/2)。

//AN_Xml:

那么问题又来了。

//AN_Xml:

为什么既然有了 HTTP2,还要有 RPC 协议?

//AN_Xml:

这个是由于 HTTP2 是 2015 年出来的。那时候很多公司内部的 RPC 协议都已经跑了好些年了,基于历史原因,一般也没必要去换了。

//AN_Xml:

总结

//AN_Xml:
    //AN_Xml:
  • 纯裸 TCP 是能收发数据,但它是个无边界的数据流,上层需要定义消息格式用于定义 消息边界 。于是就有了各种协议,HTTP 和各类 RPC 协议就是在 TCP 之上定义的应用层协议。
  • //AN_Xml:
  • RPC 本质上不算是协议,而是一种调用方式,而像 gRPC 和 Thrift 这样的具体实现,才是协议,它们是实现了 RPC 调用的协议。目的是希望程序员能像调用本地方法那样去调用远端的服务方法。同时 RPC 有很多种实现方式,不一定非得基于 TCP 协议
  • //AN_Xml:
  • 从发展历史来说,HTTP 主要用于 B/S 架构,而 RPC 更多用于 C/S 架构。但现在其实已经没分那么清了,B/S 和 C/S 在慢慢融合。 很多软件同时支持多端,所以对外一般用 HTTP 协议,而内部集群的微服务之间则采用 RPC 协议进行通讯。
  • //AN_Xml:
  • RPC 其实比 HTTP 出现的要早,且比目前主流的 HTTP1.1 性能要更好,所以大部分公司内部都还在使用 RPC。
  • //AN_Xml:
  • HTTP2.0HTTP1.1 的基础上做了优化,性能可能比很多 RPC 协议都要好,但由于是这几年才出来的,所以也不太可能取代掉 RPC。
  • //AN_Xml:
//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Java SPI 机制详解 //AN_Xml: https://javaguide.cn/java/basis/spi.html //AN_Xml: https://javaguide.cn/java/basis/spi.html //AN_Xml: Java SPI 机制详解 //AN_Xml: 全面讲解Java SPI机制原理与应用:理解ServiceLoader服务发现机制、SPI在JDBC/Dubbo/Spring中的应用、与API对比及最佳实践。 //AN_Xml: Java //AN_Xml: Sun, 24 Jul 2022 09:10:58 GMT //AN_Xml: //AN_Xml:

本文来自 Kingshion 投稿。欢迎更多朋友参与到 JavaGuide 的维护工作,这是一件非常有意义的事情。详细信息请看:JavaGuide 贡献指南

//AN_Xml: //AN_Xml:

面向对象设计鼓励模块间基于接口而非具体实现编程,以降低模块间的耦合,遵循依赖倒置原则,并支持开闭原则(对扩展开放,对修改封闭)。然而,直接依赖具体实现会导致在替换实现时需要修改代码,违背了开闭原则。为了解决这个问题,SPI 应运而生,它提供了一种服务发现机制,允许在程序外部动态指定具体实现。这与控制反转(IoC)的思想相似,将组件装配的控制权移交给了程序之外。

//AN_Xml:

SPI 机制也解决了 Java 类加载体系中双亲委派模型带来的限制。双亲委派模型虽然保证了核心库的安全性和一致性,但也限制了核心库或扩展库加载应用程序类路径上的类(通常由第三方实现)。SPI 允许核心或扩展库定义服务接口,第三方开发者提供并部署实现,SPI 服务加载机制则在运行时动态发现并加载这些实现。例如,JDBC 4.0 及之后版本利用 SPI 自动发现和加载数据库驱动,开发者只需将驱动 JAR 包放置在类路径下即可,无需使用Class.forName()显式加载驱动类。

//AN_Xml:

SPI 介绍

//AN_Xml:

何谓 SPI?

//AN_Xml:

SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。

//AN_Xml:

SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。

//AN_Xml:

很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。

//AN_Xml: //AN_Xml:

SPI 和 API 有什么区别?

//AN_Xml:

那 SPI 和 API 有啥区别?

//AN_Xml:

说到 SPI 就不得不说一下 API(Application Programming Interface) 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下:

//AN_Xml:

SPI VS API

//AN_Xml:

一般模块之间都是通过接口进行通讯,因此我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。

//AN_Xml:
    //AN_Xml:
  • 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。
  • //AN_Xml:
  • 当接口存在于调用方这边时,这就是 SPI 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。
  • //AN_Xml:
//AN_Xml:

举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。

//AN_Xml:

实战演示

//AN_Xml:

SLF4J (Simple Logging Facade for Java)是 Java 的一个日志门面(接口),其具体实现有几种,比如:Logback、Log4j、Log4j2 等等,而且还可以切换,在切换日志具体实现的时候我们是不需要更改项目代码的,只需要在 Maven 依赖里面修改一些 pom 依赖就好了。

//AN_Xml:

//AN_Xml:

这就是依赖 SPI 机制实现的,那我们接下来就实现一个简易版本的日志框架。

//AN_Xml:

Service Provider Interface

//AN_Xml:

新建一个 Java 项目 service-provider-interface 目录结构如下:(注意直接新建 Java 项目就好了,不用新建 Maven 项目,Maven 项目会涉及到一些编译配置,如果有私服的话,直接 deploy 会比较方便,但是没有的话,在过程中可能会遇到一些奇怪的问题。)

//AN_Xml:
│  service-provider-interface.iml
//AN_Xml:
//AN_Xml:├─.idea
//AN_Xml:│  │  .gitignore
//AN_Xml:│  │  misc.xml
//AN_Xml:│  │  modules.xml
//AN_Xml:│  └─ workspace.xml
//AN_Xml:
//AN_Xml:└─src
//AN_Xml:    └─edu
//AN_Xml:        └─jiangxuan
//AN_Xml:            └─up
//AN_Xml:                └─spi
//AN_Xml:                        Logger.java
//AN_Xml:                        LoggerService.java
//AN_Xml:                        Main.class
//AN_Xml:

新建 Logger 接口,这个就是 SPI , 服务提供者接口,后面的服务提供者就要针对这个接口进行实现。

//AN_Xml:
package edu.jiangxuan.up.spi;
//AN_Xml:
//AN_Xml:public interface Logger {
//AN_Xml:    void info(String msg);
//AN_Xml:    void debug(String msg);
//AN_Xml:}
//AN_Xml:

接下来就是 LoggerService 类,这个主要是为服务使用者(调用方)提供特定功能的。这个类也是实现 Java SPI 机制的关键所在,如果存在疑惑的话可以先往后面继续看。

//AN_Xml:
package edu.jiangxuan.up.spi;
//AN_Xml:
//AN_Xml:import java.util.ArrayList;
//AN_Xml:import java.util.List;
//AN_Xml:import java.util.ServiceLoader;
//AN_Xml:
//AN_Xml:public class LoggerService {
//AN_Xml:    private static final LoggerService SERVICE = new LoggerService();
//AN_Xml:
//AN_Xml:    private final Logger logger;
//AN_Xml:
//AN_Xml:    private final List<Logger> loggerList;
//AN_Xml:
//AN_Xml:    private LoggerService() {
//AN_Xml:        ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class);
//AN_Xml:        List<Logger> list = new ArrayList<>();
//AN_Xml:        for (Logger log : loader) {
//AN_Xml:            list.add(log);
//AN_Xml:        }
//AN_Xml:        // LoggerList 是所有 ServiceProvider
//AN_Xml:        loggerList = list;
//AN_Xml:        if (!list.isEmpty()) {
//AN_Xml:            // Logger 只取一个
//AN_Xml:            logger = list.get(0);
//AN_Xml:        } else {
//AN_Xml:            logger = null;
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    public static LoggerService getService() {
//AN_Xml:        return SERVICE;
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    public void info(String msg) {
//AN_Xml:        if (logger == null) {
//AN_Xml:            System.out.println("info 中没有发现 Logger 服务提供者");
//AN_Xml:        } else {
//AN_Xml:            logger.info(msg);
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    public void debug(String msg) {
//AN_Xml:        if (loggerList.isEmpty()) {
//AN_Xml:            System.out.println("debug 中没有发现 Logger 服务提供者");
//AN_Xml:        }
//AN_Xml:        loggerList.forEach(log -> log.debug(msg));
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

新建 Main 类(服务使用者,调用方),启动程序查看结果。

//AN_Xml:
package org.spi.service;
//AN_Xml:
//AN_Xml:public class Main {
//AN_Xml:    public static void main(String[] args) {
//AN_Xml:        LoggerService service = LoggerService.getService();
//AN_Xml:
//AN_Xml:        service.info("Hello SPI");
//AN_Xml:        service.debug("Hello SPI");
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

程序结果:

//AN_Xml:
//AN_Xml:

info 中没有发现 Logger 服务提供者
//AN_Xml:debug 中没有发现 Logger 服务提供者

//AN_Xml:
//AN_Xml:

此时我们只是空有接口,并没有为 Logger 接口提供任何的实现,所以输出结果中没有按照预期打印相应的结果。

//AN_Xml:

你可以使用命令或者直接使用 IDEA 将整个程序直接打包成 jar 包。

//AN_Xml:

Service Provider

//AN_Xml:

接下来新建一个项目用来实现 Logger 接口

//AN_Xml:

新建项目 service-provider 目录结构如下:

//AN_Xml:
│  service-provider.iml
//AN_Xml:
//AN_Xml:├─.idea
//AN_Xml:│  │  .gitignore
//AN_Xml:│  │  misc.xml
//AN_Xml:│  │  modules.xml
//AN_Xml:│  └─ workspace.xml
//AN_Xml:
//AN_Xml:├─lib
//AN_Xml:│      service-provider-interface.jar
//AN_Xml:|
//AN_Xml:└─src
//AN_Xml:    ├─edu
//AN_Xml:    │  └─jiangxuan
//AN_Xml:    │      └─up
//AN_Xml:    │          └─spi
//AN_Xml:    │              └─service
//AN_Xml:    │                      Logback.java
//AN_Xml:
//AN_Xml:    └─META-INF
//AN_Xml:        └─services
//AN_Xml:                edu.jiangxuan.up.spi.Logger
//AN_Xml:

新建 Logback

//AN_Xml:
package edu.jiangxuan.up.spi.service;
//AN_Xml:
//AN_Xml:import edu.jiangxuan.up.spi.Logger;
//AN_Xml:
//AN_Xml:public class Logback implements Logger {
//AN_Xml:    @Override
//AN_Xml:    public void info(String s) {
//AN_Xml:        System.out.println("Logback info 打印日志:" + s);
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    @Override
//AN_Xml:    public void debug(String s) {
//AN_Xml:        System.out.println("Logback debug 打印日志:" + s);
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

service-provider-interface 的 jar 导入项目中。

//AN_Xml:

新建 lib 目录,然后将 jar 包拷贝过来,再添加到项目中。

//AN_Xml:

//AN_Xml:

再点击 OK 。

//AN_Xml:

//AN_Xml:

接下来就可以在项目中导入 jar 包里面的一些类和方法了,就像 JDK 工具类导包一样的。

//AN_Xml:

实现 Logger 接口,在 src 目录下新建 META-INF/services 文件夹,然后新建文件 edu.jiangxuan.up.spi.Logger (SPI 的全类名),文件里面的内容是:edu.jiangxuan.up.spi.service.Logback (Logback 的全类名,即 SPI 的实现类的包名 + 类名)。

//AN_Xml:

这是 JDK SPI 机制 ServiceLoader 约定好的标准。

//AN_Xml:

这里先大概解释一下:Java 中的 SPI 机制就是在每次类加载的时候会先去找到 class 相对目录下的 META-INF 文件夹下的 services 文件夹下的文件,将这个文件夹下面的所有文件先加载到内存中,然后根据这些文件的文件名和里面的文件内容找到相应接口的具体实现类,找到实现类后就可以通过反射去生成对应的对象,保存在一个 list 列表里面,所以可以通过迭代或者遍历的方式拿到对应的实例对象,生成不同的实现。

//AN_Xml:

所以会提出一些规范要求:文件名一定要是接口的全类名,然后里面的内容一定要是实现类的全类名,实现类可以有多个,直接换行就好了,多个实现类的时候,会一个一个的迭代加载。

//AN_Xml:

接下来同样将 service-provider 项目打包成 jar 包,这个 jar 包就是服务提供方的实现。通常我们导入 maven 的 pom 依赖就有点类似这种,只不过我们现在没有将这个 jar 包发布到 maven 公共仓库中,所以在需要使用的地方只能手动的添加到项目中。

//AN_Xml:

效果展示

//AN_Xml:

为了更直观的展示效果,我这里再新建一个专门用来测试的工程项目:java-spi-test

//AN_Xml:

然后先导入 Logger 的接口 jar 包,再导入具体的实现类的 jar 包。

//AN_Xml:

//AN_Xml:

新建 Main 方法测试:

//AN_Xml:
package edu.jiangxuan.up.service;
//AN_Xml:
//AN_Xml:import edu.jiangxuan.up.spi.LoggerService;
//AN_Xml:
//AN_Xml:public class TestJavaSPI {
//AN_Xml:    public static void main(String[] args) {
//AN_Xml:        LoggerService loggerService = LoggerService.getService();
//AN_Xml:        loggerService.info("你好");
//AN_Xml:        loggerService.debug("测试Java SPI 机制");
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

运行结果如下:

//AN_Xml:
//AN_Xml:

Logback info 打印日志:你好
//AN_Xml:Logback debug 打印日志:测试 Java SPI 机制

//AN_Xml:
//AN_Xml:

说明导入 jar 包中的实现类生效了。

//AN_Xml:

如果我们不导入具体的实现类的 jar 包,那么此时程序运行的结果就会是:

//AN_Xml:
//AN_Xml:

info 中没有发现 Logger 服务提供者
//AN_Xml:debug 中没有发现 Logger 服务提供者

//AN_Xml:
//AN_Xml:

通过使用 SPI 机制,可以看出服务(LoggerService)和 服务提供者两者之间的耦合度非常低,如果说我们想要换一种实现,那么其实只需要修改 service-provider 项目中针对 Logger 接口的具体实现就可以了,只需要换一个 jar 包即可,也可以有在一个项目里面有多个实现,这不就是 SLF4J 原理吗?

//AN_Xml:

如果某一天需求变更了,此时需要将日志输出到消息队列,或者做一些别的操作,这个时候完全不需要更改 Logback 的实现,只需要新增一个服务实现(service-provider)可以通过在本项目里面新增实现也可以从外部引入新的服务实现 jar 包。我们可以在服务(LoggerService)中选择一个具体的 服务实现(service-provider) 来完成我们需要的操作。

//AN_Xml:

那么接下来我们具体来说说 Java SPI 工作的重点原理—— ServiceLoader

//AN_Xml:

ServiceLoader

//AN_Xml:

ServiceLoader 具体实现

//AN_Xml:

想要使用 Java 的 SPI 机制是需要依赖 ServiceLoader 来实现的,那么我们接下来看看 ServiceLoader 具体是怎么做的:

//AN_Xml:

ServiceLoader 是 JDK 提供的一个工具类, 位于package java.util;包下。

//AN_Xml:
A facility to load implementations of a service.
//AN_Xml:

这是 JDK 官方给的注释:一种加载服务实现的工具。

//AN_Xml:

再往下看,我们发现这个类是一个 final 类型的,所以是不可被继承修改,同时它实现了 Iterable 接口。之所以实现了迭代器,是为了方便后续我们能够通过迭代的方式得到对应的服务实现。

//AN_Xml:
public final class ServiceLoader<S> implements Iterable<S>{ xxx...}
//AN_Xml:

可以看到一个熟悉的常量定义:

//AN_Xml:

private static final String PREFIX = "META-INF/services/";

//AN_Xml:

下面是 load 方法:可以发现 load 方法支持两种重载后的入参;

//AN_Xml:
public static <S> ServiceLoader<S> load(Class<S> service) {
//AN_Xml:    ClassLoader cl = Thread.currentThread().getContextClassLoader();
//AN_Xml:    return ServiceLoader.load(service, cl);
//AN_Xml:}
//AN_Xml:
//AN_Xml:public static <S> ServiceLoader<S> load(Class<S> service,
//AN_Xml:                                        ClassLoader loader) {
//AN_Xml:    return new ServiceLoader<>(service, loader);
//AN_Xml:}
//AN_Xml:
//AN_Xml:private ServiceLoader(Class<S> svc, ClassLoader cl) {
//AN_Xml:    service = Objects.requireNonNull(svc, "Service interface cannot be null");
//AN_Xml:    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
//AN_Xml:    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
//AN_Xml:    reload();
//AN_Xml:}
//AN_Xml:
//AN_Xml:public void reload() {
//AN_Xml:    providers.clear();
//AN_Xml:    lookupIterator = new LazyIterator(service, loader);
//AN_Xml:}
//AN_Xml:

其解决第三方类加载的机制其实就蕴含在 ClassLoader cl = Thread.currentThread().getContextClassLoader(); 中,cl 就是线程上下文类加载器(Thread Context ClassLoader)。这是每个线程持有的类加载器,JDK 的设计允许应用程序或容器(如 Web 应用服务器)设置这个类加载器,以便核心类库能够通过它来加载应用程序类。

//AN_Xml:

线程上下文类加载器默认情况下是应用程序类加载器(Application ClassLoader),它负责加载 classpath 上的类。当核心库需要加载应用程序提供的类时,它可以使用线程上下文类加载器来完成。这样,即使是由引导类加载器加载的核心库代码,也能够加载并使用由应用程序类加载器加载的类。

//AN_Xml:

根据代码的调用顺序,在 reload() 方法中是通过一个内部类 LazyIterator 实现的。先继续往下面看。

//AN_Xml:

ServiceLoader 实现了 Iterable 接口的方法后,具有了迭代的能力,在这个 iterator 方法被调用时,首先会在 ServiceLoaderProvider 缓存中进行查找,如果缓存中没有命中那么则在 LazyIterator 中进行查找。

//AN_Xml:

//AN_Xml:public Iterator<S> iterator() {
//AN_Xml:    return new Iterator<S>() {
//AN_Xml:
//AN_Xml:        Iterator<Map.Entry<String, S>> knownProviders
//AN_Xml:                = providers.entrySet().iterator();
//AN_Xml:
//AN_Xml:        public boolean hasNext() {
//AN_Xml:            if (knownProviders.hasNext())
//AN_Xml:                return true;
//AN_Xml:            return lookupIterator.hasNext(); // 调用 LazyIterator
//AN_Xml:        }
//AN_Xml:
//AN_Xml:        public S next() {
//AN_Xml:            if (knownProviders.hasNext())
//AN_Xml:                return knownProviders.next().getValue();
//AN_Xml:            return lookupIterator.next(); // 调用 LazyIterator
//AN_Xml:        }
//AN_Xml:
//AN_Xml:        public void remove() {
//AN_Xml:            throw new UnsupportedOperationException();
//AN_Xml:        }
//AN_Xml:
//AN_Xml:    };
//AN_Xml:}
//AN_Xml:

在调用 LazyIterator 时,具体实现如下:

//AN_Xml:

//AN_Xml:public boolean hasNext() {
//AN_Xml:    if (acc == null) {
//AN_Xml:        return hasNextService();
//AN_Xml:    } else {
//AN_Xml:        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
//AN_Xml:            public Boolean run() {
//AN_Xml:                return hasNextService();
//AN_Xml:            }
//AN_Xml:        };
//AN_Xml:        return AccessController.doPrivileged(action, acc);
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:
//AN_Xml:private boolean hasNextService() {
//AN_Xml:    if (nextName != null) {
//AN_Xml:        return true;
//AN_Xml:    }
//AN_Xml:    if (configs == null) {
//AN_Xml:        try {
//AN_Xml:            //通过PREFIX(META-INF/services/)和类名 获取对应的配置文件,得到具体的实现类
//AN_Xml:            String fullName = PREFIX + service.getName();
//AN_Xml:            if (loader == null)
//AN_Xml:                configs = ClassLoader.getSystemResources(fullName);
//AN_Xml:            else
//AN_Xml:                configs = loader.getResources(fullName);
//AN_Xml:        } catch (IOException x) {
//AN_Xml:            fail(service, "Error locating configuration files", x);
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:    while ((pending == null) || !pending.hasNext()) {
//AN_Xml:        if (!configs.hasMoreElements()) {
//AN_Xml:            return false;
//AN_Xml:        }
//AN_Xml:        pending = parse(service, configs.nextElement());
//AN_Xml:    }
//AN_Xml:    nextName = pending.next();
//AN_Xml:    return true;
//AN_Xml:}
//AN_Xml:
//AN_Xml:
//AN_Xml:public S next() {
//AN_Xml:    if (acc == null) {
//AN_Xml:        return nextService();
//AN_Xml:    } else {
//AN_Xml:        PrivilegedAction<S> action = new PrivilegedAction<S>() {
//AN_Xml:            public S run() {
//AN_Xml:                return nextService();
//AN_Xml:            }
//AN_Xml:        };
//AN_Xml:        return AccessController.doPrivileged(action, acc);
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:
//AN_Xml:private S nextService() {
//AN_Xml:    if (!hasNextService())
//AN_Xml:        throw new NoSuchElementException();
//AN_Xml:    String cn = nextName;
//AN_Xml:    nextName = null;
//AN_Xml:    Class<?> c = null;
//AN_Xml:    try {
//AN_Xml:        c = Class.forName(cn, false, loader);
//AN_Xml:    } catch (ClassNotFoundException x) {
//AN_Xml:        fail(service,
//AN_Xml:                "Provider " + cn + " not found");
//AN_Xml:    }
//AN_Xml:    if (!service.isAssignableFrom(c)) {
//AN_Xml:        fail(service,
//AN_Xml:                "Provider " + cn + " not a subtype");
//AN_Xml:    }
//AN_Xml:    try {
//AN_Xml:        S p = service.cast(c.newInstance());
//AN_Xml:        providers.put(cn, p);
//AN_Xml:        return p;
//AN_Xml:    } catch (Throwable x) {
//AN_Xml:        fail(service,
//AN_Xml:                "Provider " + cn + " could not be instantiated",
//AN_Xml:                x);
//AN_Xml:    }
//AN_Xml:    throw new Error();          // This cannot happen
//AN_Xml:}
//AN_Xml:

可能很多人看这个会觉得有点复杂,没关系,我这边实现了一个简单的 ServiceLoader 的小模型,流程和原理都是保持一致的,可以先从自己实现一个简易版本的开始学:

//AN_Xml:

自己实现一个 ServiceLoader

//AN_Xml:

我先把代码贴出来:

//AN_Xml:
package edu.jiangxuan.up.service;
//AN_Xml:
//AN_Xml:import java.io.BufferedReader;
//AN_Xml:import java.io.InputStream;
//AN_Xml:import java.io.InputStreamReader;
//AN_Xml:import java.lang.reflect.Constructor;
//AN_Xml:import java.net.URL;
//AN_Xml:import java.net.URLConnection;
//AN_Xml:import java.util.ArrayList;
//AN_Xml:import java.util.Enumeration;
//AN_Xml:import java.util.List;
//AN_Xml:
//AN_Xml:public class MyServiceLoader<S> {
//AN_Xml:
//AN_Xml:    // 对应的接口 Class 模板
//AN_Xml:    private final Class<S> service;
//AN_Xml:
//AN_Xml:    // 对应实现类的 可以有多个,用 List 进行封装
//AN_Xml:    private final List<S> providers = new ArrayList<>();
//AN_Xml:
//AN_Xml:    // 类加载器
//AN_Xml:    private final ClassLoader classLoader;
//AN_Xml:
//AN_Xml:    // 暴露给外部使用的方法,通过调用这个方法可以开始加载自己定制的实现流程。
//AN_Xml:    public static <S> MyServiceLoader<S> load(Class<S> service) {
//AN_Xml:        return new MyServiceLoader<>(service);
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    // 构造方法私有化
//AN_Xml:    private MyServiceLoader(Class<S> service) {
//AN_Xml:        this.service = service;
//AN_Xml:        this.classLoader = Thread.currentThread().getContextClassLoader();
//AN_Xml:        doLoad();
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    // 关键方法,加载具体实现类的逻辑
//AN_Xml:    private void doLoad() {
//AN_Xml:        try {
//AN_Xml:            // 读取所有 jar 包里面 META-INF/services 包下面的文件,这个文件名就是接口名,然后文件里面的内容就是具体的实现类的路径加全类名
//AN_Xml:            Enumeration<URL> urls = classLoader.getResources("META-INF/services/" + service.getName());
//AN_Xml:            // 挨个遍历取到的文件
//AN_Xml:            while (urls.hasMoreElements()) {
//AN_Xml:                // 取出当前的文件
//AN_Xml:                URL url = urls.nextElement();
//AN_Xml:                System.out.println("File = " + url.getPath());
//AN_Xml:                // 建立链接
//AN_Xml:                URLConnection urlConnection = url.openConnection();
//AN_Xml:                urlConnection.setUseCaches(false);
//AN_Xml:                // 获取文件输入流
//AN_Xml:                InputStream inputStream = urlConnection.getInputStream();
//AN_Xml:                // 从文件输入流获取缓存
//AN_Xml:                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
//AN_Xml:                // 从文件内容里面得到实现类的全类名
//AN_Xml:                String className = bufferedReader.readLine();
//AN_Xml:
//AN_Xml:                while (className != null) {
//AN_Xml:                    // 通过反射拿到实现类的实例
//AN_Xml:                    Class<?> clazz = Class.forName(className, false, classLoader);
//AN_Xml:                    // 如果声明的接口跟这个具体的实现类是属于同一类型,(可以理解为Java的一种多态,接口跟实现类、父类和子类等等这种关系。)则构造实例
//AN_Xml:                    if (service.isAssignableFrom(clazz)) {
//AN_Xml:                        Constructor<? extends S> constructor = (Constructor<? extends S>) clazz.getConstructor();
//AN_Xml:                        S instance = constructor.newInstance();
//AN_Xml:                        // 把当前构造的实例对象添加到 Provider的列表里面
//AN_Xml:                        providers.add(instance);
//AN_Xml:                    }
//AN_Xml:                    // 继续读取下一行的实现类,可以有多个实现类,只需要换行就可以了。
//AN_Xml:                    className = bufferedReader.readLine();
//AN_Xml:                }
//AN_Xml:            }
//AN_Xml:        } catch (Exception e) {
//AN_Xml:            System.out.println("读取文件异常。。。");
//AN_Xml:        }
//AN_Xml:    }
//AN_Xml:
//AN_Xml:    // 返回spi接口对应的具体实现类列表
//AN_Xml:    public List<S> getProviders() {
//AN_Xml:        return providers;
//AN_Xml:    }
//AN_Xml:}
//AN_Xml:

关键信息基本已经通过代码注释描述出来了,

//AN_Xml:

主要的流程就是:

//AN_Xml:
    //AN_Xml:
  1. 通过 URL 工具类从 jar 包的 /META-INF/services 目录下面找到对应的文件,
  2. //AN_Xml:
  3. 读取这个文件的名称找到对应的 spi 接口,
  4. //AN_Xml:
  5. 通过 InputStream 流将文件里面的具体实现类的全类名读取出来,
  6. //AN_Xml:
  7. 根据获取到的全类名,先判断跟 spi 接口是否为同一类型,如果是的,那么就通过反射的机制构造对应的实例对象,
  8. //AN_Xml:
  9. 将构造出来的实例对象添加到 Providers 的列表中。
  10. //AN_Xml:
//AN_Xml:

总结

//AN_Xml:

其实不难发现,SPI 机制的具体实现本质上还是通过反射完成的。即:我们按照规定将要暴露对外使用的具体实现类在 META-INF/services/ 文件下声明。

//AN_Xml:

另外,SPI 机制在很多框架中都有应用:Spring 框架的基本原理也是类似的方式。还有 Dubbo 框架提供同样的 SPI 扩展机制,只不过 Dubbo 和 spring 框架中的 SPI 机制具体实现方式跟咱们今天学得这个有些细微的区别,不过整体的原理都是一致的,相信大家通过对 JDK 中 SPI 机制的学习,能够一通百通,加深对其他高深框架的理解。

//AN_Xml:

通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:

//AN_Xml:
    //AN_Xml:
  1. 遍历加载所有的实现类,这样效率还是相对较低的;
  2. //AN_Xml:
  3. 当多个 ServiceLoader 同时 load 时,会有并发问题。
  4. //AN_Xml:
//AN_Xml:

写在最后

//AN_Xml:

感谢你能看到这里,也希望这篇文章对你有点用。

//AN_Xml:

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee

//AN_Xml:

如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

//AN_Xml:JavaGuide 公众号 //AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Redis常见面试题总结(下) //AN_Xml: https://javaguide.cn/database/redis/redis-questions-02.html //AN_Xml: https://javaguide.cn/database/redis/redis-questions-02.html //AN_Xml: Redis常见面试题总结(下) //AN_Xml: 最新Redis面试题总结(下):深度剖析Redis事务原理、性能优化(pipeline/Lua/bigkey/hotkey)、缓存穿透/击穿/雪崩解决方案、慢查询与内存碎片、Redis Sentinel与Cluster集群详解。助你轻松应对后端技术面试! //AN_Xml: 数据库 //AN_Xml: Wed, 20 Jul 2022 14:27:00 GMT //AN_Xml: 《SpringAI 智能面试平台+RAG 知识库》

//AN_Xml:

Redis 事务

//AN_Xml:

什么是 Redis 事务?

//AN_Xml:

你可以将 Redis 中的事务理解为:Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。

//AN_Xml:

Redis 事务实际开发中使用的非常少,功能比较鸡肋,不要将其和我们平时理解的关系型数据库的事务混淆了。

//AN_Xml:

除了不满足原子性和持久性之外,事务中的每条命令都会与 Redis 服务器进行网络交互,这是比较浪费资源的行为。明明一次批量执行多个命令就可以了,这种操作实在是看不懂。

//AN_Xml:

因此,Redis 事务是不建议在日常开发中使用的。

//AN_Xml:

如何使用 Redis 事务?

//AN_Xml:

Redis 可以通过 MULTIEXECDISCARDWATCH 等命令来实现事务(Transaction)功能。

//AN_Xml:
> MULTI
//AN_Xml:OK
//AN_Xml:> SET PROJECT "JavaGuide"
//AN_Xml:QUEUED
//AN_Xml:> GET PROJECT
//AN_Xml:QUEUED
//AN_Xml:> EXEC
//AN_Xml:1) OK
//AN_Xml:2) "JavaGuide"
//AN_Xml:

MULTI 命令后可以输入多个命令,Redis 不会立即执行这些命令,而是将它们放到队列,当调用了 EXEC 命令后,再执行所有的命令。

//AN_Xml:

这个过程是这样的:

//AN_Xml:
    //AN_Xml:
  1. 开始事务(MULTI);
  2. //AN_Xml:
  3. 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行);
  4. //AN_Xml:
  5. 执行事务(EXEC)。
  6. //AN_Xml:
//AN_Xml:

你也可以通过 DISCARD 命令取消一个事务,它会清空事务队列中保存的所有命令。

//AN_Xml:
> MULTI
//AN_Xml:OK
//AN_Xml:> SET PROJECT "JavaGuide"
//AN_Xml:QUEUED
//AN_Xml:> GET PROJECT
//AN_Xml:QUEUED
//AN_Xml:> DISCARD
//AN_Xml:OK
//AN_Xml:

你可以通过WATCH 命令监听指定的 Key,当调用 EXEC 命令执行事务时,如果一个被 WATCH 命令监视的 Key 被 其他客户端/Session 修改的话,整个事务都不会被执行。

//AN_Xml:
# 客户端 1
//AN_Xml:> SET PROJECT "RustGuide"
//AN_Xml:OK
//AN_Xml:> WATCH PROJECT
//AN_Xml:OK
//AN_Xml:> MULTI
//AN_Xml:OK
//AN_Xml:> SET PROJECT "JavaGuide"
//AN_Xml:QUEUED
//AN_Xml:
//AN_Xml:# 客户端 2
//AN_Xml:# 在客户端 1 执行 EXEC 命令提交事务之前修改 PROJECT 的值
//AN_Xml:> SET PROJECT "GoGuide"
//AN_Xml:
//AN_Xml:# 客户端 1
//AN_Xml:# 修改失败,因为 PROJECT 的值被客户端2修改了
//AN_Xml:> EXEC
//AN_Xml:(nil)
//AN_Xml:> GET PROJECT
//AN_Xml:"GoGuide"
//AN_Xml:

不过,如果 WATCH事务 在同一个 Session 里,并且被 WATCH 监视的 Key 被修改的操作发生在事务内部,这个事务是可以被执行成功的(相关 issue:WATCH 命令碰到 MULTI 命令时的不同效果)。

//AN_Xml:

事务内部修改 WATCH 监视的 Key:

//AN_Xml:
> SET PROJECT "JavaGuide"
//AN_Xml:OK
//AN_Xml:> WATCH PROJECT
//AN_Xml:OK
//AN_Xml:> MULTI
//AN_Xml:OK
//AN_Xml:> SET PROJECT "JavaGuide1"
//AN_Xml:QUEUED
//AN_Xml:> SET PROJECT "JavaGuide2"
//AN_Xml:QUEUED
//AN_Xml:> SET PROJECT "JavaGuide3"
//AN_Xml:QUEUED
//AN_Xml:> EXEC
//AN_Xml:1) OK
//AN_Xml:2) OK
//AN_Xml:3) OK
//AN_Xml:127.0.0.1:6379> GET PROJECT
//AN_Xml:"JavaGuide3"
//AN_Xml:

事务外部修改 WATCH 监视的 Key:

//AN_Xml:
> SET PROJECT "JavaGuide"
//AN_Xml:OK
//AN_Xml:> WATCH PROJECT
//AN_Xml:OK
//AN_Xml:> SET PROJECT "JavaGuide2"
//AN_Xml:OK
//AN_Xml:> MULTI
//AN_Xml:OK
//AN_Xml:> GET USER
//AN_Xml:QUEUED
//AN_Xml:> EXEC
//AN_Xml:(nil)
//AN_Xml:

Redis 官网相关介绍 https://redis.io/topics/transactions 如下:

//AN_Xml:

Redis 事务

//AN_Xml:

Redis 事务支持原子性吗?

//AN_Xml:

Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性:1. 原子性2. 隔离性3. 持久性4. 一致性

//AN_Xml:
    //AN_Xml:
  1. 原子性(Atomicity):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. //AN_Xml:
  3. 隔离性(Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  4. //AN_Xml:
  5. 持久性(Durability):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响;
  6. //AN_Xml:
  7. 一致性(Consistency):执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的。
  8. //AN_Xml:
//AN_Xml:

Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 事务是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的。

//AN_Xml:

Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis 开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis 开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。

//AN_Xml:

Redis 为什么不支持回滚

//AN_Xml:

相关 issue

//AN_Xml: //AN_Xml:

Redis 事务支持持久性吗?

//AN_Xml:

Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式:

//AN_Xml:
    //AN_Xml:
  • 快照(snapshotting,RDB);
  • //AN_Xml:
  • 只追加文件(append-only file,AOF);
  • //AN_Xml:
  • RDB 和 AOF 的混合持久化(Redis 4.0 新增)。
  • //AN_Xml:
//AN_Xml:

与 RDB 持久化相比,AOF 持久化的实时性更好。在 Redis 的配置文件中存在三种不同的 AOF 持久化方式(fsync 策略),它们分别是:

//AN_Xml:
appendfsync always    #每次有数据修改发生时,都会调用fsync函数同步AOF文件,fsync完成后线程返回,这样会严重降低Redis的速度
//AN_Xml:appendfsync everysec  #每秒钟调用fsync函数同步一次AOF文件
//AN_Xml:appendfsync no        #让操作系统决定何时进行同步,一般为30秒一次
//AN_Xml:

AOF 持久化的 fsync 策略为 no、everysec 时都会存在数据丢失的情况。always 下可以基本是可以满足持久性要求的,但性能太差,实际开发过程中不会使用。

//AN_Xml:

因此,Redis 事务的持久性也是没办法保证的。

//AN_Xml:

如何解决 Redis 事务的缺陷?

//AN_Xml:

Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常类似。我们可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。

//AN_Xml:

一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。

//AN_Xml:

不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此,严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。

//AN_Xml:

如果想要让 Lua 脚本中的命令全部执行,必须保证语句语法和命令都是对的。

//AN_Xml:

另外,Redis 7.0 新增了 Redis functions 特性,你可以将 Redis functions 看作是比 Lua 更强大的脚本。

//AN_Xml:

⭐️Redis 性能优化(重要)

//AN_Xml:

除了下面介绍的内容之外,再推荐两篇不错的文章:

//AN_Xml: //AN_Xml:

使用批量操作减少网络传输

//AN_Xml:

一个 Redis 命令的执行可以简化为以下 4 步:

//AN_Xml:
    //AN_Xml:
  1. 发送命令;
  2. //AN_Xml:
  3. 命令排队;
  4. //AN_Xml:
  5. 命令执行;
  6. //AN_Xml:
  7. 返回结果。
  8. //AN_Xml:
//AN_Xml:

其中,第 1 步和第 4 步耗费时间之和称为 Round Trip Time(RTT,往返时间),也就是数据在网络上传输的时间。

//AN_Xml:

使用批量操作可以减少网络传输次数,进而有效减小网络开销,大幅减少 RTT。

//AN_Xml:

另外,除了能减少 RTT 之外,发送一次命令的 socket I/O 成本也比较高(涉及上下文切换,存在 read()write() 系统调用),批量操作还可以减少 socket I/O 成本。这个在官方对 pipeline 的介绍中有提到:https://redis.io/docs/manual/pipelining/

//AN_Xml:

原生批量操作命令

//AN_Xml:

Redis 中有一些原生支持批量操作的命令,比如:

//AN_Xml:
    //AN_Xml:
  • MGET(获取一个或多个指定 key 的值)、MSET(设置一个或多个指定 key 的值)、
  • //AN_Xml:
  • HMGET(获取指定哈希表中一个或者多个指定字段的值)、HMSET(同时将一个或多个 field-value 对设置到指定哈希表中)、
  • //AN_Xml:
  • SADD(向指定集合添加一个或多个元素)
  • //AN_Xml:
  • ……
  • //AN_Xml:
//AN_Xml:

不过,在 Redis 官方提供的分片集群解决方案 Redis Cluster 下,使用这些原生批量操作命令可能会存在一些小问题需要解决。就比如说 MGET 无法保证所有的 key 都在同一个 hash slot(哈希槽) 上,MGET可能还是需要多次网络传输,原子操作也无法保证了。不过,相较于非批量操作,还是可以节省不少网络传输次数。

//AN_Xml:

整个步骤的简化版如下(通常由 Redis 客户端实现,无需我们自己再手动实现):

//AN_Xml:
    //AN_Xml:
  1. 找到 key 对应的所有 hash slot;
  2. //AN_Xml:
  3. 分别向对应的 Redis 节点发起 MGET 请求获取数据;
  4. //AN_Xml:
  5. 等待所有请求执行结束,重新组装结果数据,保持跟入参 key 的顺序一致,然后返回结果。
  6. //AN_Xml:
//AN_Xml:

如果想要解决这个多次网络传输的问题,比较常用的办法是自己维护 key 与 slot 的关系。不过这样不太灵活,虽然带来了性能提升,但同样让系统复杂性提升。

//AN_Xml:
//AN_Xml:

Redis Cluster 并没有使用一致性哈希,采用的是 哈希槽分区,每一个键值对都属于一个 hash slot(哈希槽)。当客户端发送命令请求的时候,需要先根据 key 通过上面的计算公式找到的对应的哈希槽,然后再查询哈希槽和节点的映射关系,即可找到目标 Redis 节点。

//AN_Xml:

我在 Redis 集群详解(付费) 这篇文章中详细介绍了 Redis Cluster 这部分的内容,感兴趣地可以看看。

//AN_Xml:
//AN_Xml:

pipeline

//AN_Xml:

对于不支持批量操作的命令,我们可以利用 pipeline(流水线) 将一批 Redis 命令封装成一组,这些 Redis 命令会被一次性提交到 Redis 服务器,只需要一次网络传输。不过,需要注意控制一次批量操作的 元素个数(例如 500 以内,实际也和元素字节数有关),避免网络传输的数据量过大。

//AN_Xml:

MGETMSET 等原生批量操作命令一样,pipeline 同样在 Redis Cluster 上使用会存在一些小问题。原因类似,无法保证所有的 key 都在同一个 hash slot(哈希槽) 上。如果想要使用的话,客户端需要自己维护 key 与 slot 的关系。

//AN_Xml:

原生批量操作命令和 pipeline 的是有区别的,使用的时候需要注意:

//AN_Xml:
    //AN_Xml:
  • 原生批量操作命令是原子操作,pipeline 是非原子操作。
  • //AN_Xml:
  • pipeline 可以打包不同的命令,原生批量操作命令不可以。
  • //AN_Xml:
  • 原生批量操作命令是 Redis 服务端支持实现的,而 pipeline 需要服务端和客户端的共同实现。
  • //AN_Xml:
//AN_Xml:

顺带补充一下 pipeline 和 Redis 事务的对比:

//AN_Xml:
    //AN_Xml:
  • 事务是原子操作,pipeline 是非原子操作。两个不同的事务不会同时运行,而 pipeline 可以同时以交错方式执行。
  • //AN_Xml:
  • Redis 事务中每个命令都需要发送到服务端,而 Pipeline 只需要发送一次,请求次数更少。
  • //AN_Xml:
//AN_Xml:
//AN_Xml:

事务可以看作是一个原子操作,但其实并不满足原子性。当我们提到 Redis 中的原子操作时,主要指的是这个操作(比如事务、Lua 脚本)不会被其他操作(比如其他事务、Lua 脚本)打扰,并不能完全保证这个操作中的所有写命令要么都执行要么都不执行。这主要也是因为 Redis 是不支持回滚操作。

//AN_Xml:
//AN_Xml:

//AN_Xml:

另外,pipeline 不适用于执行顺序有依赖关系的一批命令。就比如说,你需要将前一个命令的结果给后续的命令使用,pipeline 就没办法满足你的需求了。对于这种需求,我们可以使用 Lua 脚本

//AN_Xml:

Lua 脚本

//AN_Xml:

Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作一条命令执行,可以看作是 原子操作。也就是说,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰,这是 pipeline 所不具备的。

//AN_Xml:

并且,Lua 脚本中支持一些简单的逻辑处理比如使用命令读取值并在 Lua 脚本中进行处理,这同样是 pipeline 所不具备的。

//AN_Xml:

不过, Lua 脚本依然存在下面这些缺陷:

//AN_Xml:
    //AN_Xml:
  • 如果 Lua 脚本运行时出错并中途结束,之后的操作不会进行,但是之前已经发生的写操作不会撤销,所以即使使用了 Lua 脚本,也不能实现类似数据库回滚的原子性。
  • //AN_Xml:
  • Redis Cluster 下 Lua 脚本的原子操作也无法保证了,原因同样是无法保证所有的 key 都在同一个 hash slot(哈希槽) 上。
  • //AN_Xml:
//AN_Xml:

大量 key 集中过期问题

//AN_Xml:

我在前面提到过:对于过期 key,Redis 采用的是 定期删除+惰性/懒汉式删除 策略。

//AN_Xml:

定期删除执行过程中,如果突然遇到大量过期 key 的话,客户端请求必须等待定期清理过期 key 任务线程执行完成,因为这个这个定期任务线程是在 Redis 主线程中执行的。这就导致客户端请求没办法被及时处理,响应速度会比较慢。

//AN_Xml:

如何解决呢? 下面是两种常见的方法:

//AN_Xml:
    //AN_Xml:
  1. 给 key 设置随机过期时间。
  2. //AN_Xml:
  3. 开启 lazy-free(惰性删除/延迟释放)。lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
  4. //AN_Xml:
//AN_Xml:

个人建议不管是否开启 lazy-free,我们都尽量给 key 设置随机过期时间。

//AN_Xml:

Redis bigkey(大 Key)

//AN_Xml:

什么是 bigkey?

//AN_Xml:

简单来说,如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准:

//AN_Xml:
    //AN_Xml:
  • String 类型的 value 超过 1MB
  • //AN_Xml:
  • 复合类型(List、Hash、Set、Sorted Set 等)的 value 包含的元素超过 5000 个(不过,对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。
  • //AN_Xml:
//AN_Xml:

bigkey 判定标准

//AN_Xml:

bigkey 是怎么产生的?有什么危害?

//AN_Xml:

bigkey 通常是由于下面这些原因产生的:

//AN_Xml:
    //AN_Xml:
  • 程序设计不当,比如直接使用 String 类型存储较大的文件对应的二进制数据。
  • //AN_Xml:
  • 对于业务的数据规模考虑不周到,比如使用集合类型的时候没有考虑到数据量的快速增长。
  • //AN_Xml:
  • 未及时清理垃圾数据,比如哈希中冗余了大量的无用键值对。
  • //AN_Xml:
//AN_Xml:

bigkey 除了会消耗更多的内存空间和带宽,还会对性能造成比较大的影响。

//AN_Xml:

Redis 常见阻塞原因总结 这篇文章中我们提到:大 key 还会造成阻塞问题。具体来说,主要体现在下面三个方面:

//AN_Xml:
    //AN_Xml:
  1. 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
  2. //AN_Xml:
  3. 网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
  4. //AN_Xml:
  5. 工作线程阻塞:如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
  6. //AN_Xml:
//AN_Xml:

大 key 造成的阻塞问题还会进一步影响到主从同步和集群扩容。

//AN_Xml:

综上,大 key 带来的潜在问题是非常多的,我们应该尽量避免 Redis 中存在 bigkey。

//AN_Xml:

如何发现 bigkey?

//AN_Xml:

1、使用 Redis 自带的 --bigkeys 参数来查找。

//AN_Xml:
# redis-cli -p 6379 --bigkeys
//AN_Xml:
//AN_Xml:# Scanning the entire keyspace to find biggest keys as well as
//AN_Xml:# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
//AN_Xml:# per 100 SCAN commands (not usually needed).
//AN_Xml:
//AN_Xml:[00.00%] Biggest string found so far '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28057c20"' with 4437 bytes
//AN_Xml:[00.00%] Biggest list   found so far '"my-list"' with 17 items
//AN_Xml:
]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Redis 3 种特殊数据类型详解 //AN_Xml: https://javaguide.cn/database/redis/redis-data-structures-02.html //AN_Xml: https://javaguide.cn/database/redis/redis-data-structures-02.html //AN_Xml: Redis 3 种特殊数据类型详解 //AN_Xml: 详解Redis三种特殊数据类型Bitmap、HyperLogLog、GEO的使用方法和应用场景,包括签到统计、UV统计、附近的人等典型业务场景实现。 //AN_Xml: 数据库 //AN_Xml: Wed, 20 Jul 2022 07:08:15 GMT //AN_Xml: 除了 5 种基本的数据类型之外,Redis 还支持 3 种特殊的数据类型:Bitmap、HyperLogLog、GEO。

//AN_Xml:

Bitmap (位图)

//AN_Xml:

介绍

//AN_Xml:

根据官网介绍:

//AN_Xml:
//AN_Xml:

Bitmaps are not an actual data type, but a set of bit-oriented operations defined on the String type which is treated like a bit vector. Since strings are binary safe blobs and their maximum length is 512 MB, they are suitable to set up to 2^32 different bits.

//AN_Xml:

Bitmap 不是 Redis 中的实际数据类型,而是在 String 类型上定义的一组面向位的操作,将其视为位向量。由于字符串是二进制安全的块,且最大长度为 512 MB,它们适合用于设置最多 2^32 个不同的位。

//AN_Xml:
//AN_Xml:

Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。

//AN_Xml:

你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。

//AN_Xml:

//AN_Xml:

常用命令

//AN_Xml:

| 命令 | 介绍 |
//AN_Xml:|

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: Redis 5 种基本数据类型详解 //AN_Xml: https://javaguide.cn/database/redis/redis-data-structures-01.html //AN_Xml: https://javaguide.cn/database/redis/redis-data-structures-01.html //AN_Xml: Redis 5 种基本数据类型详解 //AN_Xml: 详解Redis五种基本数据类型String、List、Set、Hash、Zset的使用方法和应用场景,深入分析SDS、跳表、压缩列表等底层数据结构实现原理。 //AN_Xml: 数据库 //AN_Xml: Mon, 18 Jul 2022 10:43:07 GMT //AN_Xml: Redis 共有 5 种基本数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。

//AN_Xml:

这 5 种数据类型是直接提供给用户使用的,是数据的保存形式,其底层实现主要依赖这 8 种数据结构:简单动态字符串(SDS)、LinkedList(双向链表)、Dict(哈希表/字典)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)。

//AN_Xml:

Redis 5 种基本数据类型对应的底层数据结构实现如下表所示:

//AN_Xml:

| String | List | Hash | Set | Zset |
//AN_Xml:| :

//AN_Xml:]]>
//AN_Xml: //AN_Xml:
//AN_Xml: //AN_Xml: