diff --git a/.gitignore b/.gitignore index 7ee001fcc8..67fb8cb593 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,51 @@ -*.DS_Store -*.iml -*.xml -*.txt -*.dio +# Created by .ignore support plugin (hsz.mobi) +### Java template +# Compiled class file +*.class + +# Log file *.log +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +.DS_Store +.idea/JavaEdge.iml +.idea/compiler.xml .idea/inspectionProfiles/ +.idea/misc.xml +.idea/modules.xml +.idea/vcs.xml +.idea/workspace.xml +Python/.DS_Store +大数据/.DS_Store +操作系统/.DS_Store +数据存储/.DS_Store +面试题系列/.DS_Store + .metals/metals.h2.db +.metals/metals.log .vscode/settings.json +UML/demo.dio Spring/feign.md -TODO/MySQL会丢数据吗?.md +*.dio +TODO/uml/MySQL会丢数据吗?.md TODO/【阿里最新数据库面试题】MySQL主从一致性.md TODO/【阿里数据库面试题解】MySQL高可用原理.md TODO/MySQL执行更新语句时做了什么?.md @@ -18,8 +53,3 @@ TODO/为何阿里不推荐MySQL使用join?.md TODO/【阿里MySQL面试题】内部临时表.md TODO/MySQL数据查询太多会OOM吗?.md TODO/有了InnoDB,Memory存储引擎还有意义吗?.md -TODO/MySQL执行insert会如何加锁?.md -TODO/MySQL的自增id竟然用到头了怎么办?.md -TODO/MySQL全局锁和表锁.md -TODO/MySQL事务是怎么实现隔离的?.md - diff --git "a/Git/Git\347\211\210\346\234\254\345\233\236\351\200\200\346\226\271\346\263\225\350\256\272(\345\217\257\350\203\275\350\247\243\345\206\263\344\275\240101%\351\201\207\345\210\260\347\232\204Git\347\211\210\346\234\254\351\227\256\351\242\230).md" "b/Git/Git\347\211\210\346\234\254\345\233\236\351\200\200\346\226\271\346\263\225\350\256\272(\345\217\257\350\203\275\350\247\243\345\206\263\344\275\240101%\351\201\207\345\210\260\347\232\204Git\347\211\210\346\234\254\351\227\256\351\242\230).md" deleted file mode 100644 index 8d5f27d5c5..0000000000 --- "a/Git/Git\347\211\210\346\234\254\345\233\236\351\200\200\346\226\271\346\263\225\350\256\272(\345\217\257\350\203\275\350\247\243\345\206\263\344\275\240101%\351\201\207\345\210\260\347\232\204Git\347\211\210\346\234\254\351\227\256\351\242\230).md" +++ /dev/null @@ -1,120 +0,0 @@ -# 1 本地回退 -你在本地做了错误的 commit,先找到要回退的版本的`commit id`: - -```bash -git reflog -``` -![](https://img-blog.csdnimg.cn/20200414142250436.png) -接着回退版本: - -```bash -git reset --hard cac0 -``` -> cac0就是你要回退的版本的`commit id`的前面几位 - -回退到某次提交。回退到的指定提交以后的提交都会从提交日志上消失。 - -> 工作区和暂存区的内容都会被重置到指定提交的时候,如果不加`--hard`则只移动`HEAD`指针,不影响工作区和暂存区的内容。 - -结合`git reflog`找回提交日志上看不到的版本历史,撤回某次操作前的状态 -这个方法可以对你的回退操作进行回退,因为这时候`git log`已经找不到历史提交的hash值了。 - -# 2 远程回退 -## 2.1 回退自己的远程分支 -你的错误commit已经推送到远程分支,就需要回滚远程分支。 -- 首先要回退本地分支: - -```bash -git reflog -git reset --hard cac0 -``` -![](https://img-blog.csdnimg.cn/20200414142459436.png) -- 由于本地分支回滚后,版本将落后远程分支,必须使用强制推送覆盖远程分支,否则后面将无法推送到远程分支。 - -```bash -git push -f -``` -![](https://img-blog.csdnimg.cn/20200414142539953.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -- 注意修正为`git push -f origin branch_name` -![](https://img-blog.csdnimg.cn/20200414142624784.png) - -## 2.2 回退公共远程分支 -如果你回退公共远程分支,把别人的提交给丢掉了怎么办? -> 本人毕业时在前东家 hw 经常干的蠢事。 - -### 分析 -假如你的远程master分支情况是这样的: - -```bash -A1–A2–B1 -``` -> A、B分别代表两个人 -> A1、A2、B1代表各自的提交 -> 所有人的本地分支都已经更新到最新版本,和远程分支一致 - -这时发现A2这次commit有误,你用reset回滚远程分支master到A1,那么理想状态是你的同事一拉代码git pull,他们的master分支也回滚了 -然而现实却是,你的同事会看到下面的提示: - -```bash -$ git status -On branch master -Your branch is ahead of 'origin/master' by 2 commits. - (use "git push" to publish your local commits) -nothing to commit, working directory clean -``` -也就是说,你的同事的分支并没有主动回退,而是比远程分支超前了两次提交,因为远程分支回退了。 -不幸的是,现实中,我们经常遇到的都是猪一样的队友,他们一看到下面提示: - -```bash -$ git status -On branch master -Your branch is ahead of 'origin/master' by 2 commits. - (use "git push" to publish your local commits) -nothing to commit, working directory clean -``` -就习惯性的git push一下,或者他们直接用的SourceTree这样的图形界面工具,一看到界面上显示的是推送的提示就直接点了推送按钮,卧槽,辛辛苦苦回滚的版本就这样轻松的被你猪一样的队友给还原了,所以,只要有一个队友push之后,远程master又变成了: - -```bash -A1 – A2 – B1 -``` - -这就是分布式,每个人都有副本。 - -用另外一种方法来回退版本。 - -# 3 公共远程回退 -使用git reset回退公共远程分支的版本后,需要其他所有人手动用远程master分支覆盖本地master分支,显然,这不是优雅的回退方法。 - -```bash -git revert HEAD //撤销最近一次提交 -git revert HEAD~1 //撤销上上次的提交,注意:数字从0开始 -git revert 0ffaacc //撤销0ffaacc这次提交 -``` -git revert 命令意思是撤销某次提交。它会产生一个新的提交,虽然代码回退了,但是版本依然是向前的,所以,当你用revert回退之后,所有人pull之后,他们的代码也自动的回退了。但是,要注意以下几点: - -- revert 是撤销一次提交,所以后面的commit id是你需要回滚到的版本的前一次提交 -- 使用revert HEAD是撤销最近的一次提交,如果你最近一次提交是用revert命令产生的,那么你再执行一次,就相当于撤销了上次的撤销操作,换句话说,你连续执行两次revert HEAD命令,就跟没执行是一样的 -- 使用revert HEAD~1 表示撤销最近2次提交,这个数字是从0开始的,如果你之前撤销过产生了commi id,那么也会计算在内的 -- 如果使用 revert 撤销的不是最近一次提交,那么一定会有代码冲突,需要你合并代码,合并代码只需要把当前的代码全部去掉,保留之前版本的代码就可以了 -- git revert 命令的好处就是不会丢掉别人的提交,即使你撤销后覆盖了别人的提交,他更新代码后,可以在本地用 reset 向前回滚,找到自己的代码,然后拉一下分支,再回来合并上去就可以找回被你覆盖的提交了。 - -# 4 revert 合并代码,解决冲突 -使用revert命令,如果不是撤销的最近一次提交,那么一定会有冲突,如下所示: - -```bash -<<<<<<< HEAD -全部清空 -第一次提交 -======= -全部清空 ->>>>>>> parent of c24cde7... 全部清空 -``` -解决冲突很简单,因为我们只想回到某次提交,因此需要把当前最新的代码去掉即可,也就是HEAD标记的代码: - -```bash -<<<<<<< HEAD -全部清空 -第一次提交 -======= -``` -把上面部分代码去掉就可以了,然后再提交一次代码就可以解决冲突了。 \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/AbstractQueuedSynchronizer\345\216\237\347\220\206\350\247\243\346\236\220.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/AbstractQueuedSynchronizer\345\216\237\347\220\206\350\247\243\346\236\220.md" deleted file mode 100644 index 75c4862c41..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/AbstractQueuedSynchronizer\345\216\237\347\220\206\350\247\243\346\236\220.md" +++ /dev/null @@ -1,448 +0,0 @@ -AbstractQueuedSynchronizer 抽象同步队列简称 AQS ,它是实现同步器的基础组件, -并发包中锁的底层就是使用 AQS 实现的. -大多数开发者可能永远不会直接使用AQS ,但是知道其原理对于架构设计还是很有帮助的,而且要理解ReentrantLock、CountDownLatch等高级锁我们必须搞懂 AQS. - -# 1 整体感知 -## 1.1 架构图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMjA3XzIwMjAwMjA5MTk1MDI3NjQ4LnBuZw?x-oss-process=image/format,png) -AQS框架大致分为五层,自上而下由浅入深,从AQS对外暴露的API到底层基础数据. - - -当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。当自定义同步器进行加锁或者解锁操作时,先经过第一层的API进入AQS内部方法,然后经过第二层进行锁的获取,接着对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层。 - -AQS 本身就是一套锁的框架,它定义了获得锁和释放锁的代码结构,所以如果要新建锁,只要继承 AQS,并实现相应方法即可。 - -## 1.2 类设计 -该类提供了一种框架,用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关的同步器(信号量,事件等)。此类的设计旨在为大多数依赖单个原子int值表示 state 的同步器提供切实有用的基础。子类必须定义更改此 state 的 protected 方法,并定义该 state 对于 acquired 或 released 此对象而言意味着什么。鉴于这些,此类中的其他方法将执行全局的排队和阻塞机制。子类可以维护其他状态字段,但是就同步而言,仅跟踪使用方法 *getState*,*setState* 和 *compareAndSetState* 操作的原子更新的int值。 -子类应定义为用于实现其所在类的同步属性的非公共内部帮助器类。 - -子类应定义为用于实现其所在类的同步属性的非 public 内部辅助类。类AbstractQueuedSynchronizer不实现任何同步接口。 相反,它定义了诸如*acquireInterruptible*之类的方法,可以通过具体的锁和相关的同步器适当地调用这些方法来实现其 public 方法。 - -此类支持默认的排他模式和共享模式: -- 当以独占方式进行获取时,其他线程尝试进行的获取将无法成功 -- 由多个线程获取的共享模式可能(但不一定)成功 - -该类不理解这些差异,只是从机制的意义上说,当共享模式获取成功时,下一个等待线程(如果存在)也必须确定它是否也可以获取。在不同模式下等待的线程们共享相同的FIFO队列。 通常,实现的子类仅支持这些模式之一,但也可以同时出现,比如在ReadWriteLock.仅支持排他模式或共享模式的子类无需定义支持未使用模式的方法. - -此类定义了一个内嵌的 **ConditionObject** 类,可由支持独占模式的子类用作Condition 的实现,该子类的 *isHeldExclusively* 方法报告相对于当前线程是否独占同步,使用当前 *getState* 值调用的方法 *release* 会完全释放此对象 ,并获得给定的此保存状态值,最终将该对象恢复为其先前的获取状态。否则,没有AbstractQueuedSynchronizer方***创建这样的条件,因此,如果无法满足此约束,请不要使用它。ConditionObject的行为当然取决于其同步器实现的语义。 - -此类提供了内部队列的检查,检测和监视方法,以及条件对象的类似方法。 可以根据需要使用 AQS 将它们导出到类中以实现其同步机制。 - -此类的序列化仅存储基础原子整数维护状态,因此反序列化的对象具有空线程队列。 需要序列化性的典型子类将定义一个readObject方法,该方法在反序列化时将其恢复为已知的初始状态。 - -# 2 用法 -要将此类用作同步器的基础,使用*getState* *setState*和/或*compareAndSetState*检查和/或修改同步状态,以重新定义以下方法(如适用) -- tryAcquire -- tryRelease -- tryAcquireShared -- tryReleaseShared -- isHeldExclusively - -默认情况下,这些方法中的每一个都会抛 *UnsupportedOperationException*。 -这些方法的实现必须在内部是线程安全的,并且通常应简短且不阻塞。 定义这些方法是使用此类的**唯一**受支持的方法。 所有其他方法都被声明为final,因为它们不能独立变化。 - -从 AQS 继承的方法对跟踪拥有排他同步器的线程很有用。 鼓励使用它们-这将启用监视和诊断工具,以帮助用户确定哪些线程持有锁。 - -虽然此类基于内部的FIFO队列,它也不会自动执行FIFO获取策略。 独占同步的核心采用以下形式: -- Acquire -```java -while (!tryAcquire(arg)) { - 如果线程尚未入队,则将其加入队列; - 可能阻塞当前线程; -} -``` -- Release - -```java -if (tryRelease(arg)) - 取消阻塞第一个入队的线程; -``` -共享模式与此相似,但可能涉及级联的signal。 - -acquire 中的检查是在入队前被调用,所以新获取的线程可能会在被阻塞和排队的其他线程之前插入。但若需要,可以定义tryAcquire、tryAcquireShared以通过内部调用一或多种检查方法来禁用插入,从而提供公平的FIFO获取顺序。 - -特别是,若 hasQueuedPredecessors()(公平同步器专门设计的一种方法)返回true,则大多数公平同步器都可以定义tryAcquire返回false. - -- 公平与否取决于如下一行代码: -```java -if (c == 0) { - if (!hasQueuedPredecessors() && - compareAndSetState(0, acquires)) { - setExclusiveOwnerThread(current); - return true; - } -} -``` -### hasQueuedPredecessors -```java -public final boolean hasQueuedPredecessors() { - // The correctness of this depends on head being initialized - // before tail and on head.next being accurate if the current - // thread is first in queue. - Node t = tail; // Read fields in reverse initialization order - Node h = head; - // s代表等待队列的第一个节点 - Node s; - // (s = h.next) == null 说明此时有另一个线程正在尝试成为头节点,详见AQS的acquireQueued方法 - // s.thread != Thread.currentThread():此线程不是等待的头节点 - return h != t && - ((s = h.next) == null || s.thread != Thread.currentThread()); -} -``` - - - -对于默认的插入(也称为贪婪,放弃和convoey-avoidance)策略,吞吐量和可伸缩性通常最高。 尽管不能保证这是公平的或避免饥饿,但允许较早排队的线程在较晚排队的线程之前进行重新竞争,并且每个重新争用都有一次机会可以毫无偏向地成功竞争过进入的线程。 -同样,尽管获取通常无需自旋,但在阻塞前,它们可能会执行tryAcquire的多次调用,并插入其他任务。 如果仅短暂地保持排他同步,则这将带来自旋的大部分好处,而如果不进行排他同步,则不会带来很多负担。 如果需要的话,可以通过在调用之前使用“fast-path”检查来获取方法来增强此功能,并可能预先检查*hasContended*()和/或*hasQueuedThreads()*,以便仅在同步器可能不存在争用的情况下这样做。 - -此类为同步提供了有效且可扩展的基础,部分是通过将其使用范围规范化到可以依赖于int状态,acquire 和 release 参数以及内部的FIFO等待队列的同步器。 当这还不够时,可以使用原子类、自定义队列类和锁支持阻塞支持从较低级别构建同步器。 - -# 3 使用案例 -这里是一个不可重入的排他锁,它使用值0表示解锁状态,使用值1表示锁定状态。虽然不可重入锁并不严格要求记录当前所有者线程,但是这个类这样做是为了更容易监视使用情况。它还支持条件,并暴露其中一个检测方法: - -```java -class Mutex implements Lock, java.io.Serializable { - - // 我们内部的辅助类 - private static class Sync extends AbstractQueuedSynchronizer { - // 报告是否处于锁定状态 - protected boolean isHeldExclusively() { - return getState() == 1; - } - - // 如果 state 是 0,获取锁 - public boolean tryAcquire(int acquires) { - assert acquires == 1; // Otherwise unused - if (compareAndSetState(0, 1)) { - setExclusiveOwnerThread(Thread.currentThread()); - return true; - } - return false; - } - - // 通过将 state 置 0 来释放锁 - protected boolean tryRelease(int releases) { - assert releases == 1; // Otherwise unused - if (getState() == 0) throw new IllegalMonitorStateException(); - setExclusiveOwnerThread(null); - setState(0); - return true; - } - - // 提供一个 Condition - Condition newCondition() { return new ConditionObject(); } - - // 反序列化属性 - private void readObject(ObjectInputStream s) - throws IOException, ClassNotFoundException { - s.defaultReadObject(); - setState(0); // 重置到解锁状态 - } - } - - // 同步对象完成所有的工作。我们只是期待它. - private final Sync sync = new Sync(); - - public void lock() { sync.acquire(1); } - public boolean tryLock() { return sync.tryAcquire(1); } - public void unlock() { sync.release(1); } - public Condition newCondition() { return sync.newCondition(); } - public boolean isLocked() { return sync.isHeldExclusively(); } - public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); } - public void lockInterruptibly() throws InterruptedException { - sync.acquireInterruptibly(1); - } - public boolean tryLock(long timeout, TimeUnit unit) - throws InterruptedException { - return sync.tryAcquireNanos(1, unit.toNanos(timeout)); - } -} -``` - -这是一个闩锁类,它类似于*CountDownLatch*,只是它只需要一个单信号就可以触发。因为锁存器是非独占的,所以它使用共享的获取和释放方法。 - -```java - class BooleanLatch { - - private static class Sync extends AbstractQueuedSynchronizer { - boolean isSignalled() { return getState() != 0; } - - protected int tryAcquireShared(int ignore) { - return isSignalled() ? 1 : -1; - } - - protected boolean tryReleaseShared(int ignore) { - setState(1); - return true; - } - } - - private final Sync sync = new Sync(); - public boolean isSignalled() { return sync.isSignalled(); } - public void signal() { sync.releaseShared(1); } - public void await() throws InterruptedException { - sync.acquireSharedInterruptibly(1); - } - } -``` - -# 4 基本属性与框架 -## 4.1 继承体系图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyOTU4XzIwMjAwMjEwMjMyNTQwMzUwLnBuZw?x-oss-process=image/format,png) -## 4.2 定义 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDk2XzIwMjAwMjEwMjMxMTIwMTMzLnBuZw?x-oss-process=image/format,png) - -可知 AQS 是一个抽象类,生来就是被各种子类锁继承的。继承自AbstractOwnableSynchronizer,其作用就是为了知道当前是哪个线程获得了锁,便于后续的监控 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDYyXzIwMjAwMjEwMjMyMzE4MzcyLnBuZw?x-oss-process=image/format,png) - - -## 4.3 属性 -### 4.3.1 状态信息 -- volatile 修饰,对于可重入锁,每次获得锁 +1,释放锁 -1 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMTEwXzIwMjAwMjEwMjMyOTI0MTczLnBuZw?x-oss-process=image/format,png) -- 可以通过 *getState* 得到同步状态的当前值。该操作具有 volatile 读的内存语义。 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyOTM5XzIwMjAwMjEwMjMzMjM0OTE0LnBuZw?x-oss-process=image/format,png) -- setState 设置同步状态的值。该操作具有 volatile 写的内存语义 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDc4XzIwMjAwMjEwMjMzOTI2NjQ3LnBuZw?x-oss-process=image/format,png) -- compareAndSetState 如果当前状态值等于期望值,则以原子方式将同步状态设置为给定的更新值。此操作具有 volatile 读和写的内存语义 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDMyXzIwMjAwMjEwMjM1MjM1NDAzLnBuZw?x-oss-process=image/format,png) -- 自旋比使用定时挂起更快。粗略估计足以在非常短的超时时间内提高响应能力,当设置等待时间时才会用到这个属性 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDY0XzIwMjAwMjExMDAzOTIzNjQxLnBuZw?x-oss-process=image/format,png) - -这写方法都是Final的,子类无法重写。 -- 独占模式 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyODIzXzIwMjAwMjExMDIyNTI0NjM5LnBuZw?x-oss-process=image/format,png) -- 共享模式 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDQzXzIwMjAwMjExMDIyNTUxODMwLnBuZw?x-oss-process=image/format,png) -### 4.3.2 同步队列 -- CLH 队列( FIFO) -![](https://img-blog.csdnimg.cn/2020100800492945.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center)![在这里插入图片描述](https://img-blog.csdnimg.cn/img_convert/22686302fc050911b9dc9cdaf672934b.png) - -- 作用 -阻塞获取不到锁(独占锁)的线程,并在适当时机从队首释放这些线程。 - -同步队列底层数据结构是个双向链表。 - -- 等待队列的头,延迟初始化。 除初始化外,只能通过 *setHead* 方法修改 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyODc4XzIwMjAwMjExMDIzMjU4OTU4LnBuZw?x-oss-process=image/format,png) -注意:如果head存在,则其waitStatus保证不会是 *CANCELLED* - -- 等待队列的尾部,延迟初始化。 仅通过方法 *enq* 修改以添加新的等待节点 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDk0XzIwMjAwMjExMDIzNTU3MTkyLnBuZw?x-oss-process=image/format,png) -### 4.3.4 条件队列 -#### 为什么需要条件队列? -同步队列并非所有场景都能cover,遇到锁 + 队列结合的场景时,就需要 Lock + Condition,先使用 Lock 决定: -- 哪些线程可以获得锁 -- 哪些线程需要到同步队列里排队阻塞 - -获得锁的多个线程在碰到队列满或空时,可使用 Condition 来管理这些线程,让这些线程阻塞等待,然后在合适的时机后,被正常唤醒。 - -**同步队列 + 条件队列的协作多被用在锁 + 队列场景。** -#### 作用 -AQS 的内部类,结合锁实现线程同步。存放调用条件变量的 await 方法后被阻塞的线程 - -- 实现了 Condition 接口,而 Condition 接口就相当于 Object 的各种监控方法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMTEwXzIwMjAwMjExMDI0MDUzNi5wbmc?x-oss-process=image/format,png) -需要使用时,直接 new ConditionObject()。 - -### 4.3.5 Node -同步队列和条件队列的共用节点。 -入队时,用 Node 把线程包装一下,然后把 Node 放入两个队列中,我们看下 Node 的数据结构,如下: -#### 4.3.5.1 模式 -- 共享模式 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDUyXzIwMjAwMjExMDI1NTQxMTYyLnBuZw?x-oss-process=image/format,png) -- 独占模式 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyOTAzXzIwMjAwMjExMDI1NjE2NzkyLnBuZw?x-oss-process=image/format,png) - -#### 4.3.5.2 waitstatus - 等待状态 -```java -volatile int waitStatus; -``` -仅能为如下值: -##### SIGNAL -- 同步队列中的节点在自旋获取锁时,如果前一个节点的状态是 `SIGNAL`,那么自己就直接被阻塞,否则一直自旋 -- 该节点的后继节点会被(或很快)阻塞(通过park),因此当前节点释放或取消时必须unpark其后继节点。为避免竞争,acquire方法必须首先指示它们需要一个 signal,然后重试原子获取,然后在失败时阻塞。 -```java -static final int SIGNAL = -1; -``` - -##### CANCELLED -表示线程获取锁的请求已被取消了: -```java -static final int CANCELLED = 1; -``` -可能由于超时或中断,该节点被取消。 - -节点永远不会离开此状态,此为一种终极状态。具有 cancelled 节点的线程永远不会再次阻塞。 -##### CONDITION -该节点当前在条件队列,当节点从同步队列被转移到条件队列,状态就会被更改该态: -```java -static final int CONDITION = -2; -``` -在被转移之前,它不会用作同步队列的节点,此时状态将置0(该值的使用与该字段的其他用途无关,仅是简化了机制)。 - -##### PROPAGATE -线程处在 `SHARED` 情景下,该字段才会启用。 - -指示下一个**acquireShared**应该无条件传播,共享模式下,该状态的线程处Runnable态 -```java -static final int PROPAGATE = -3; -``` -*releaseShared* 应该传播到其他节点。 在*doReleaseShared*中对此进行了设置(仅适用于头节点),以确保传播继续进行,即使此后进行了其他操作也是如此。 -##### 0 -初始化时的默认值。 -##### 小结 -这些值是以数字方式排列,极大方便了开发者的使用。我们在平时开发也可以定义一些有特殊意义的常量值。 - -非负值表示节点不需要 signal。 因此,大多数代码并不需要检查特定值,检查符号即可。 - -- 对于普通的同步节点,该字段初始化为0 -- 对于条件节点,该字段初始化为`CONDITION` - -使用CAS(或在可能的情况下进行无条件的 volatile 写)对其进行修改。 - -注意两个状态的区别 -- state 是锁的状态,int 型,子类继承 AQS 时,都是要根据 state 字段来判断有无得到锁 -- waitStatus 是节点(Node)的状态 - -#### 4.3.5.3 数据结构 -##### 前驱节点 -- 链接到当前节点/线程所依赖的用来检查 *waitStatus* 的前驱节点 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyODY0XzIwMjAwMjEyMDMyNjI1NjYxLnBuZw?x-oss-process=image/format,png) - -在入队期间赋值,并且仅在出队时将其清空(为了GC)。 - -此外,在取消一个前驱结点后,在找到一个未取消的节点后会短路,这将始终存在,因为头节点永远不会被取消:只有成功 acquire 后,一个节点才会变为头。 - -取消的线程永远不会成功获取,并且线程只会取消自身,不会取消任何其他节点。 - -##### 后继节点 -链接到后继节点,当前节点/线程在释放时将其unpark。 在入队时赋值,在绕过已取消的前驱节点时进行调整,在出队时置null(为了GC)。 -入队操作直到附加后才赋值前驱节点的`next`字段,因此看到`next`字段为 null,并不一定意味该节点位于队尾(有时间间隙)。 - -但若`next == null`,则可从队尾开始扫描`prev`以进行再次检查。 -```java -// 若节点通过从tail向前搜索发现在在同步队列上,则返回 true -// 仅在调用了 isOnSyncQueue 且有需要时才调用 -private boolean findNodeFromTail(Node node) { - Node t = tail; - for (;;) { - if (t == node) - return true; - if (t == null) - return false; - t = t.prev; - } -} -``` -```java -final boolean isOnSyncQueue(Node node) { - if (node.waitStatus == Node.CONDITION || node.prev == null) - return false; - if (node.next != null) // If has successor, it must be on queue - return true; - /** - * node.prev 可以非null,但还没有在队列中,因为将它放在队列中的 CAS 可能会失败。 - * 所以必须从队尾向前遍历以确保它确实成功了。 - * 在调用此方法时,它将始终靠近tail,并且除非 CAS 失败(这不太可能) - * 否则它会在那里,因此几乎不会遍历太多 - */ - return findNodeFromTail(node); -} -``` -已取消节点的`next`字段设置为指向节点本身而不是null,以使isOnSyncQueue更轻松。 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDg3XzIwMjAwMjEyMDM1NTAzNjAyLnBuZw?x-oss-process=image/format,png) -- 使该节点入队的线程。 在构造时初始化,使用后消亡。![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyODcwXzIwMjAwMjEyMjAwMTI3OTkwLnBuZw?x-oss-process=image/format,png) - -在同步队列中,nextWaiter 表示当前节点是独占模式还是共享模式 -在条件队列中,nextWaiter 表示下一个节点元素 - -链接到在条件队列等待的下一个节点,或者链接到特殊值`SHARED`。 由于条件队列仅在以独占模式保存时才被访问,因此我们只需要一个简单的链接队列即可在节点等待条件时保存节点。 然后将它们转移到队列中以重新获取。 并且由于条件只能是独占的,因此我们使用特殊值来表示共享模式来保存字段。![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDUxXzIwMjAwMjEyMjAxMjMyODMucG5n?x-oss-process=image/format,png) -# 5 Condition 接口 -JDK5 时提供。 -- 条件队列 ConditionObject 实现了 Condition 接口 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDc1XzIwMjAwMjEyMjA0NjMxMTMzLnBuZw?x-oss-process=image/format,png) -- 本节就让我们一起来研究之 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDgwXzIwMjAwMjEyMjA1MzQ2NzIyLnBuZw?x-oss-process=image/format,png) - -Condition 将对象监视方法(wait,notify和notifyAll)分解为不同的对象,从而通过与任意Lock实现结合使用,从而使每个对象具有多个wait-sets。 当 Lock 替换了 synchronized 方法和语句的使用,Condition 就可以替换了Object监视器方法的使用。 - -Condition 的实现可以提供与 Object 监视方法不同的行为和语义,例如保证通知的顺序,或者在执行通知时不需要保持锁定。 如果实现提供了这种专门的语义,则实现必须记录这些语义。 - -Condition实例只是普通对象,它们本身可以用作 synchronized 语句中的目标,并且可以调用自己的监视器 wait 和 notification 方法。 获取 Condition 实例的监视器锁或使用其监视器方法与获取与该条件相关联的锁或使用其 await 和 signal 方法没有特定的关系。 建议避免混淆,除非可能在自己的实现中,否则不要以这种方式使用 Condition 实例。 - -```java - class BoundedBuffer { - final Lock lock = new ReentrantLock(); - final Condition notFull = lock.newCondition(); - final Condition notEmpty = lock.newCondition(); - - final Object[] items = new Object[100]; - int putptr, takeptr, count; - - public void put(Object x) throws InterruptedException { - lock.lock(); - try { - while (count == items.length) - notFull.await(); - items[putptr] = x; - if (++putptr == items.length) putptr = 0; - ++count; - notEmpty.signal(); - } finally { - lock.unlock(); - } - } - - public Object take() throws InterruptedException { - lock.lock(); - try { - while (count == 0) - notEmpty.await(); - Object x = items[takeptr]; - if (++takeptr == items.length) takeptr = 0; - --count; - notFull.signal(); - return x; - } finally { - lock.unlock(); - } - } - } -``` -(ArrayBlockingQueue类提供了此功能,因此没有理由实现此示例用法类。) -定义出一些方法,这些方法奠定了条件队列的基础 -## API -### await -- 使当前线程等待,直到被 signalled 或被中断 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyOTkwXzIwMjAwMjEyMjExNzU3ODYyLnBuZw?x-oss-process=image/format,png) - -与此 Condition 相关联的锁被原子释放,并且出于线程调度目的,当前线程被禁用,并且处于休眠状态,直到发生以下四种情况之一: -- 其它线程为此 Condition 调用了 signal 方法,并且当前线程恰好被选择为要唤醒的线程 -- 其它线程为此 Condition 调用了 signalAll 方法 -- 其它线程中断了当前线程,并且当前线程支持被中断 -- 发生“虚假唤醒”。 - -在所有情况下,在此方法可以返回之前,必须重新获取与此 Condition 关联的锁,才能真正被唤醒。当线程返回时,可以保证保持此锁。 -### await 超时时间 -- 使当前线程等待,直到被 signal 或中断,或经过指定的等待时间 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDczXzIwMjAwMjEyMjIxMjU3NTcyLnBuZw?x-oss-process=image/format,png) - -此方法在行为上等效于: - - -```java -awaitNanos(unit.toNanos(time)) > 0 -``` -所以,虽然入参可以是任意单位的时间,但其实仍会转化成纳秒 -### awaitNanos -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyOTg0XzIwMjAwMjEyMjIxOTMxNTU5LnBuZw?x-oss-process=image/format,png) -注意这里选择纳秒是为了避免计算剩余等待时间时的截断误差 - - -### signal() -- 唤醒条件队列中的一个线程,在被唤醒前必须先获得锁 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDYzXzIwMjAwMjEyMjMwNTQ3NjMzLnBuZw?x-oss-process=image/format,png) -### signalAll() -- 唤醒条件队列中的所有线程 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyODkyXzIwMjAwMjEyMjMwNjUzNTM5LnBuZw?x-oss-process=image/format,png) \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/FurureTask.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/FurureTask.md" index 149a2a9585..3c1bbd6ce7 100644 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/FurureTask.md" +++ "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/FurureTask.md" @@ -1,26 +1,32 @@ -# 1 简介 -- 使用继承方式的好处是方便传参,可以在子类里面添加成员变量,通过 set 方法设置参数或者通过构造函数进行传递 +# 1 引导语 +研究源码,一般我们都从整体以及实例先入手,再研究细节,不至于一开始就“深陷其中而"当局者迷". +本文,我们来看最后一种有返回值的线程创建方式。 + +- 使用继承方式的好处是方便传参,可以在子类里面添加成员变量,通过 set 方法设置参数或者通过构造函数进行传递 - 使用 Runnable 方式,则只能使用主线程里面被声明为 final 变量 -不好的地方是 Java 不支持多继承,如果继承了 Thread 类,那么子类不能再继承其他 ,而 Runable接口则没有这个限制 。而且 Thread 类和 Runnable 接口都不允许声明检查型异常,也不能定义返回值。没有返回值这点稍微有点麻烦。前两种方式都没办法拿到任务的返回结果,但今天的主角 FutureTask 却可以。 +不好的地方是 Java 不支持多继承,如果继承了 Thread 类,那么子类不能再继承其他 ,而 Runable接口则没有这个限制 。而且 Thread 类和 Runnable 接口都不允许声明检查型异常,也不能定义返回值。没有返回值这点稍微有点麻烦。前两种方式都没办法拿到任务的返回结果,但今天的主角 FutureTask 却可以. 不能声明抛出检查型异常则更麻烦一些。run()方法意味着必须捕获并处理检查型异常。即使小心地保存了异常信息(在捕获异常时)以便稍后检查,但也不能保证这个 Runnable 对象的所有使用者都读取异常信息。你也可以修改Runnable实现的getter,让它们都能抛出任务执行中的异常。但这种方法除了繁琐也不是十分安全可靠,你不能强迫使用者调用这些方法,程序员很可能会调用join()方法等待线程结束然后就不管了。 但是现在不用担心了,以上的问题终于在1.5中解决了。Callable接口和Future接口的引入以及他们对线程池的支持优雅地解决了这两个问题。 # 2 案例 先看一个demo,了解 FutureTask 相关组件是如何使用的 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY3MjUwXzIwMjAwMjA3MjIxNjM4OTk2LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177167250_20200207221638996.png) +CallerTask 类实现了 Callable 接口的 call() 方法 。在 main 函数内首先创建FutrueTask对 象(构造函数为 CallerTask 实例), 然后使用创建的 FutureTask 作为任务创建了一个线程并且启动它, 最后通过 futureTask.get()等待任务执行完毕并返回结果. + + # 3 Callable Callable函数式接口定义了唯一方法 - call(). 我们可以在Callable的实现中声明强类型的返回值,甚至是抛出异常。同时,利用call()方法直接返回结果的能力,省去读取值时的类型转换。 - 源码定义 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NjQ1XzIwMjAwMjAyMjA0MjA0MjIyLnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166645_20200202204204222.png) 注意到返回值是一个泛型,使用的时候,不会直接使用 Callable,而是和 FutureTask 协同. # 4 Future - Callable 可以返回线程的执行结果,在获取结果时,就需要用到 Future 接口. -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2OTIwXzIwMjAwMjA0MDQwMDA3MzMucG5n?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166920_2020020404000733.png) Future是 Java5 中引入的接口,当提交一个Callable对象给线程池时,将得到一个Future对象,并且它和传入的Callable有相同的结果类型声明。 @@ -34,7 +40,7 @@ Future表示异步计算的结果。提供了一些方法来检查计算是否 ## 4.1 Future API ### 4.1.1 cancel - 尝试取消执行任务 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2Njc4XzIwMjAwMjA0MDIxOTEwMTI1LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166678_20200204021910125.png) 一个比较复杂的方法,当任务处于不同状态时,该方法有不同响应: - 任务 已经完成 / 已经取消 / 由于某些其他原因无法被取消,该尝试会直接失败 - 尝试成功,且此时任务尚未开始,调用后是可以取消成功的 @@ -45,42 +51,34 @@ Future表示异步计算的结果。提供了一些方法来检查计算是否 如果此方法返回 true,则随后对 *isCancelled* 的调用将始终返回 true. ### 4.1.2 isCancelled - 是否被取消 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2ODM0XzIwMjAwMjA0MDMwMzU2OTM1LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166834_20200204030356935.png) 如果此任务在正常完成之前被取消,则返回true. ### 4.1.3 isDone - 是否完成 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NTc0XzIwMjAwMjA0MDMxMDA1NDg4LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166574_20200204031005488.png) 如果此任务完成,则返回true. 完成可能是由于正常终止,异常或取消引起的,在所有这些情况下,此方法都将返回true. ### 4.1.4 get - 获取结果 -等待任务完成,然后获取其结果。 -![](https://img-blog.csdnimg.cn/20210615233538777.png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166906_20200204031206355.png) +等待任务完成,然后获取其结果. -若: -- 任务被取消,抛 *CancellationException* -- 当前线程在等待时被中断,抛 *InterruptedException* -- 任务抛出了异常,抛 *ExecutionException* +- 如果任务被取消,抛 *CancellationException* +- 如果当前线程在等待时被中断,抛 *InterruptedException* +- 如果任务抛出了异常,抛 *ExecutionException* ### 4.1.5 timed get - 超时获取 -- 必要时最多等待给定时间以完成任务,然后获取其结果(如果有的话)。 -![](https://img-blog.csdnimg.cn/20210615233634165.png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166633_20200204033827757.png) +必要时最多等待给定时间以完成任务,然后获取其结果(如果有的话)。 - 抛CancellationException 如果任务被取消 - 抛 ExecutionException 如果任务抛了异常 - 抛InterruptedException 如果当前线程在等待时被中断 - 抛TimeoutException 如果等待超时了 -两个get()方法都是阻塞的,若被调用时,任务还没有执行完,则调用get()方法的线程会阻塞,直到任务执行完才会被唤醒。所以future.get()是会阻塞当前调用线程。 -- 阻塞异步线程 -![](https://img-blog.csdnimg.cn/20210420165206559.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -![](https://img-blog.csdnimg.cn/2021042016492740.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 阻塞主线程 -![](https://img-blog.csdnimg.cn/20210420165240137.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +需要注意:这两个get()方法都是阻塞式的,如果被调用的时候,任务还没有执行完,那么调用get()方法的线程会阻塞,直到任务执行完才会被唤醒。 -![](https://img-blog.csdnimg.cn/20210420165122388.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -Future 接口定义了许多对任务进行管理的 API,极大地方便了我们的开发调控。 +Future 接口定义了许多对任务进行管理的 API,极大地方便了我们的开发调控. # 5 RunnableFuture Java6 时提供的持有 Runnable 性质的 Future. @@ -88,7 +86,7 @@ Java6 时提供的持有 Runnable 性质的 Future. 成功执行run方法导致Future的完成,并允许访问其结果. RunnableFuture接口比较简单,就是继承了 Runnable 和 Future 接口。只提供一个*run*方法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2OTE2XzIwMjAwMjA0MDQwMjM0NzM1LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166916_20200204040234735.png) 现在,我们应该都知道,创建任务有两种方式 - 无返回值的 Runnable @@ -99,8 +97,8 @@ RunnableFuture接口比较简单,就是继承了 Runnable 和 Future 接口。 所以铺垫了这么多,本文的主角 FutureTask 来了! # 6 FutureTask -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NTk5XzIwMjAwMjAyMjA1MzM1MzA3LnBuZw?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2OTQ3XzIwMjAwMjA4MjI0MjEzNDYucG5n?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166599_20200202205335307.png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166947_2020020822421346.png) 前面的Future是一个接口,而 FutureTask 才是一个实实在在的工具类,是线程运行的具体任务. - 实现了 RunnableFuture 接口 - 也就是实现了 Runnnable 接口,即FutureTask 本身就是个 Runnnable @@ -114,7 +112,7 @@ RunnableFuture接口比较简单,就是继承了 Runnable 和 Future 接口。 - 注意这些常量字段的定义方式,遵循避免魔鬼数字的编程规约. -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2ODg4XzIwMjAwMjA4MTM0OTU2MTQ1LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166888_20200208134956145.png) - NEW 线程任务创建,开始状态 @@ -139,28 +137,28 @@ RunnableFuture接口比较简单,就是继承了 Runnable 和 Future 接口。 ### 6.1.2 其他属性 - 组合的 callable,这样就具备了转化 Callable 和 Runnable 的功能 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2ODQ3XzIwMjAwMjA4MjI0MDM0NzU0LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166847_20200208224034754.png) - 从ge()返回或抛出异常的结果,非volatile,受状态读/写保护 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NTYzXzIwMjAwMjA4MjI0MzQzNjQ2LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166563_20200208224343646.png) - 运行 callable 的线程; 在run()期间进行CAS -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NzkyXzIwMjAwMjA4MjI1NDU3Mzc2LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166792_20200208225457376.png) - 记录调用 get 方法时被等待的线程 - 栈形式 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NjI5XzIwMjAwMjA4MjI1NzM1NzE5LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166629_20200208225735719.png) 从属性上我们明显看到 Callable 是作为 FutureTask 的属性之一,这也就让 FutureTask 接着我们看下 FutureTask 的构造器,看看两者是如何转化的。 ## 6.2 构造方法 ### 6.2.1 Callable 参数 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY3MTI4XzIwMjAwMjA4MjMwMjIyNDg2LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177167128_20200208230222486.png) ### 6.2.2 Runnable 参数 为协调 callable 属性,辅助result 参数 Runnable 是没有返回值的,所以 result 一般没有用,置为 null 即可,正如 JDK 所推荐写法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2ODcwXzIwMjAwMjA4MjMxNTEwMzEwLnBuZw?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2OTcwXzIwMjAwMjA4MjMwMzM2MjIxLnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166870_20200208231510310.png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166970_20200208230336221.png) - Executors.callable 方法负责将 runnable 适配成 callable. - ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NzE0XzIwMjAwMjA4MjMyMDUxMjQ2LnBuZw?x-oss-process=image/format,png) + ![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166714_20200208232051246.png) - 通过转化类 RunnableAdapter进行适配 - ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY3MzE0XzIwMjAwMjA4MjMyMjExNjE2LnBuZw?x-oss-process=image/format,png) + ![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177167314_20200208232211616.png) ### 6.2.3 小结 我们可以学习这里的适配器模式,目标是要把 Runnable 适配成 Callable,那么我们首先要实现 Callable 接口,并且在 Callable 的 call 方法里面调用被适配对象即 Runnable的方法即可. @@ -236,12 +234,12 @@ private int awaitDone(boolean timed, long nanos) } ``` -get 是一种阻塞式方法,当发现任务还在进行中,没有完成时,就会阻塞当前进程,等待任务完成后再返回结果值。 - -阻塞底层使用的是 LockSupport.park 方法,使当前线程进入 `WAITING` 或 `TIMED_WAITING` 态。 +get 是一种阻塞式方法,当发现任务还在进行中,没有完成时,就会阻塞当前进程,等待任务完成后再返回结果值. +阻塞底层使用的是 LockSupport.park 方法,使当前线程进入 `WAITING` 或 `TIMED_WAITING` 态. ## 6.4 run -该方法可被直接调用,也可由线程池调用 +该方法可被直接调用,也可由线程池调用 + ```java public void run() { // 状态非 NEW 或当前任务已有线程在执行,直接返回 @@ -277,9 +275,12 @@ public void run() { } ``` -run 方法没有返回值,通过给 outcome 属性赋值(set(result)),get 时就能从 outcome 属性中拿到返回值。 +run 方法我们再说明几点: + +run 方法是没有返回值的,通过给 outcome 属性赋值(set(result)),get 时就能从 outcome 属性中拿到返回值; FutureTask 两种构造器,最终都转化成了 Callable,所以在 run 方法执行的时候,只需要执行 Callable 的 call 方法即可,在执行 c.call() 代码时,如果入参是 Runnable 的话, 调用路径为 c.call() -> RunnableAdapter.call() -> Runnable.run(),如果入参是 Callable 的话,直接调用。 ## 6.5 cancel + ```java // 取消任务,如果正在运行,尝试去打断 public boolean cancel(boolean mayInterruptIfRunning) { @@ -308,4 +309,4 @@ public boolean cancel(boolean mayInterruptIfRunning) { ``` # 7 总结 -FutureTask 统一了 Runnnable 和 Callable,方便了我们后续对线程池的使用。 \ No newline at end of file +FutureTask 统一了 Runnnable 和 Callable,方便了我们后续对线程池的使用. \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/CountDownLatch.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/JDK\346\272\220\347\240\201\350\247\243\346\236\220\345\256\236\346\210\230 - CountDownLatch.md" similarity index 100% rename from "JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/CountDownLatch.md" rename to "JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/JDK\346\272\220\347\240\201\350\247\243\346\236\220\345\256\236\346\210\230 - CountDownLatch.md" diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/Java\351\233\206\345\220\210\344\271\213HashMap\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/Java\351\233\206\345\220\210\344\271\213HashMap\346\272\220\347\240\201\350\247\243\346\236\220.md" deleted file mode 100644 index 59050eb978..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/Java\351\233\206\345\220\210\344\271\213HashMap\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ /dev/null @@ -1,715 +0,0 @@ -# 1 概述 -HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长. - -HashMap是非线程安全的,只适用于单线程环境,多线程环境可以采用并发包下的`concurrentHashMap` - -HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆 - -HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射操作,并允许使用null值和null键.此类不保证映射的顺序,特别是它不保证该顺序恒久不变. - -Java8中又对此类底层实现进行了优化,比如引入了红黑树的结构以解决哈希碰撞 -  -# 2 HashMap的数据结构 -在Java中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造,HashMap也不例外. -HashMap实际上是一个"链表散列"的数据结构,即数组和链表的结合体. - -![HashMap的结构](https://img-blog.csdnimg.cn/img_convert/c5ac3aacefa3b745d9d2fa48c577b11d.png) -HashMap的主结构类似于一个数组,添加值时通过`key`确定储存位置. -每个位置是一个Entry的数据结构,该结构可组成链表. -当发生冲突时,相同hash值的键值对会组成链表. -这种`数组+链表`的组合形式大部分情况下都能有不错的性能效果,Java6、7就是这样设计的. -然而,在极端情况下,一组(比如经过精心设计的)键值对都发生了冲突,这时的哈希结构就会退化成一个链表,使HashMap性能急剧下降. - -所以在Java8中,HashMap的结构实现变为数组+链表+红黑树 -![Java8 HashMap的结构](https://img-blog.csdnimg.cn/img_convert/7668e49d6bb167520dcf09ab09537378.png) -可以看出,HashMap底层就是一个数组结构 -数组中的每一项又是一个链表 -当新建一个HashMap时,就会初始化一个数组. - -# 3 三大集合与迭代子 -HashMap使用三大集合和三种迭代子来轮询其Key、Value和Entry对象 -```java -public class HashMapExam { - public static void main(String[] args) { - Map map = new HashMap<>(16); - for (int i = 0; i < 15; i++) { - map.put(i, new String(new char[]{(char) ('A'+ i)})); - } - - System.out.println("======keySet======="); - Set set = map.keySet(); - Iterator iterator = set.iterator(); - while (iterator.hasNext()) { - System.out.println(iterator.next()); - } - - System.out.println("======values======="); - Collection values = map.values(); - Iterator stringIterator=values.iterator(); - while (stringIterator.hasNext()) { - System.out.println(stringIterator.next()); - } - - System.out.println("======entrySet======="); - for (Map.Entry entry : map.entrySet()) { - System.out.println(entry); - } - } -} -``` - -# 4 源码分析 -```java - //默认的初始容量16,且实际容量是2的整数幂 - static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; - - //最大容量(传入容量过大将被这个值替换) - static final int MAXIMUM_CAPACITY = 1 << 30; - - // 默认加载因子为0.75(当表达到3/4满时,才会再散列),这个因子在时间和空间代价之间达到了平衡.更高的因子可以降低表所需的空间,但是会增加查找代价,而查找是最频繁操作 - static final float DEFAULT_LOAD_FACTOR = 0.75f; - - //桶的树化阈值:即 链表转成红黑树的阈值,在存储数据时,当链表长度 >= 8时,则将链表转换成红黑树 - static final int TREEIFY_THRESHOLD = 8; - // 桶的链表还原阈值:即 红黑树转为链表的阈值,当在扩容(resize())时(HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 <= 6时,则将 红黑树转换成链表 - static final int UNTREEIFY_THRESHOLD = 6; - //最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树) -``` -因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要 -链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短 - -还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换 -假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。 -``` - // 为了避免扩容/树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD - // 小于该值时使用的是扩容哦!!! - static final int MIN_TREEIFY_CAPACITY = 64; - - // 存储数据的Node数组,长度是2的幂. - // HashMap采用链表法解决冲突,每一个Node本质上是一个单向链表 - //HashMap底层存储的数据结构,是一个Node数组.上面得知Node类为元素维护了一个单向链表.至此,HashMap存储的数据结构也就很清晰了:维护了一个数组,每个数组又维护了一个单向链表.之所以这么设计,考虑到遇到哈希冲突的时候,同index的value值就用单向链表来维护 - //与 JDK 1.7 的对比(Entry类),仅仅只是换了名字 - transient Node[] table; - - // HashMap的底层数组中已用槽的数量 - transient int size; - // HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子) - int threshold; - - // 负载因子实际大小 - final float loadFactor; - - // HashMap被改变的次数 - transient int modCount; - - // 指定“容量大小”和“加载因子”的构造函数,是最基础的构造函数 - public HashMap(int initialCapacity, float loadFactor) { - if (initialCapacity < 0) - throw new IllegalArgumentException("Illegal initial capacity: " + - initialCapacity); - // HashMap的最大容量只能是MAXIMUM_CAPACITY - if (initialCapacity > MAXIMUM_CAPACITY) - initialCapacity = MAXIMUM_CAPACITY; - //负载因子须大于0 - if (loadFactor <= 0 || Float.isNaN(loadFactor)) - throw new IllegalArgumentException("Illegal load factor: " + - loadFactor); - // 设置"负载因子" - this.loadFactor = loadFactor; - // 设置"HashMap阈值",当HashMap中存储数据的数量达到threshold时,就需将HashMap的容量加倍 - this.threshold = tableSizeFor(initialCapacity); - } -``` - -- 上面的tableSizeFor有何用? -tableSizeFor方法保证函数返回值是大于等于给定参数initialCapacity最小的2的幂次方的数值 -``` - static final int tableSizeFor(int cap) { - int n = cap - 1; - n |= n >>> 1; - n |= n >>> 2; - n |= n >>> 4; - n |= n >>> 8; - n |= n >>> 16; - return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; - } -``` - 可以看出该方法是一系列的二进制位操作 - ->a |= b 等同于 a = a|b - -逐行分析 -- `int n = cap - 1` -给定的cap 减 1,为了避免参数cap本来就是2的幂次方,这样一来,经过后续操作,cap将会变成2 * cap,是不符合我们预期的 - -- `n |= n >>> 1` -n >>> 1 : n无符号右移1位,即n二进制最高位的1右移一位 -n | (n >>> 1) 导致 n二进制的高2位值为1 -目前n的高1~2位均为1 -- `n |= n >>> 2` -n继续无符号右移2位 -n | (n >>> 2) 导致n二进制表示的高3~4位经过运算值均为1 -目前n的高1~4位均为1 -- `n |= n >>> 4` -n继续无符号右移4位 -n | (n >>> 4) 导致n二进制表示的高5~8位经过运算值均为1 -目前n的高1~8位均为1 -- `n |= n >>> 8` -n继续无符号右移8位 -n | (n >>> 8) 导致n二进制表示的高9~16位经过运算值均为1 -目前n的高1~16位均为1 - -可以看出,无论给定cap(cap < MAXIMUM_CAPACITY )的值是多少,经过以上运算,其值的二进制所有位都会是1.再将其加1,这时候这个值一定是2的幂次方. -当然如果经过运算值大于MAXIMUM_CAPACITY,直接选用MAXIMUM_CAPACITY. -![例子](https://img-blog.csdnimg.cn/img_convert/e58fef4c158d1c978777f5aa40ebee5e.png) -至此tableSizeFor如何保证cap为2的幂次方已经显而易见了,那么问题来了 - -## 4.1 **为什么cap要保持为2的幂次方?** -主要与HashMap中的数据存储有关. - -在Java8中,HashMap中key的Hash值由Hash(key)方法计得 -![](https://img-blog.csdnimg.cn/img_convert/0882f5d36b225a33c5e17666f5fb6695.png) - -HashMap中存储数据table的index是由key的Hash值决定的. -在HashMap存储数据时,我们期望数据能均匀分布,以防止哈希冲突. -自然而然我们就会想到去用`%`取余操作来实现我们这一构想 - ->取余(%)操作 : 如果除数是2的幂次则等价于与其除数减一的与(&)操作. - -这也就解释了为什么一定要求cap要为2的幂次方.再来看看table的index的计算规则: -![](https://img-blog.csdnimg.cn/img_convert/d8df9e11f3a143218a08f515ba6e805e.png) - 等价于: -``` - index = e.hash % newCap -``` -采用二进制位操作&,相对于%,能够提高运算效率,这就是cap的值被要求为2幂次的原因 -![](https://img-blog.csdnimg.cn/img_convert/6fcd40c3f37371a2a9ebb2a209f3be58.png) -![数据结构 & 参数与 JDK 7 / 8](https://img-blog.csdnimg.cn/img_convert/3d946e0e1bd86c8ae41c1d6857377445.png) -## 4.2 **Node类** - -``` -static class Node implements Map.Entry { - final int hash; - final K key; - V value; - Node next; - - Node(int hash, K key, V value, Node next) { - this.hash = hash; - this.key = key; - this.value = value; - this.next = next; - } - - public final K getKey() { return key; } - public final V getValue() { return value; } - public final String toString() { return key + "=" + value; } - - public final int hashCode() { - return Objects.hashCode(key) ^ Objects.hashCode(value); - } - - public final V setValue(V newValue) { - V oldValue = value; - value = newValue; - return oldValue; - } - - public final boolean equals(Object o) { - if (o == this) - return true; - if (o instanceof Map.Entry) { - Map.Entry e = (Map.Entry)o; - if (Objects.equals(key, e.getKey()) && - Objects.equals(value, e.getValue())) - return true; - } - return false; - } - } -``` -Node 类是HashMap中的静态内部类,实现Map.Entry接口.定义了key键、value值、next节点,也就是说元素之间构成了单向链表. - -## 4.3 TreeNode -``` -static final class TreeNode extends LinkedHashMap.Entry { - TreeNode parent; // red-black tree links - TreeNode left; - TreeNode right; - TreeNode prev; // needed to unlink next upon deletion - boolean red; - TreeNode(int hash, K key, V val, Node next) {} - - // 返回当前节点的根节点 - final TreeNode root() { - for (TreeNode r = this, p;;) { - if ((p = r.parent) == null) - return r; - r = p; - } - } - } -``` -红黑树结构包含前、后、左、右节点,以及标志是否为红黑树的字段 -此结构是Java8新加的 - -## 4.4 hash方法 -Java 8中的散列值优化函数 -![](https://img-blog.csdnimg.cn/img_convert/5a02fb83605c5435e8585b2c37175e69.png) -只做一次16位右位移异或 -key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值 - -理论上散列值是一个int型,如果直接拿散列值作为下标访问HashMap主数组的话,考虑到2进制32位带符号的int范围大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。 -但问题是一个40亿长度的数组,内存是放不下的.HashMap扩容之前的数组初始大小才16,所以这个散列值是不能直接拿来用的. -用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组下标 -源码中模运算就是把散列值和数组长度做一个"与"操作, -![](https://img-blog.csdnimg.cn/img_convert/d92c8b227a759d2c17736bc2b6403d57.png) -这也正好解释了为什么HashMap的数组长度要取2的整次幂 -因为这样(数组长度-1)正好相当于一个“低位掩码” -“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问 - -以初始长度16为例,16-1=15 -2进制表示是00000000 00000000 00001111 -和某散列值做“与”操作如下,结果就是截取了最低的四位值 -![](https://img-blog.csdnimg.cn/img_convert/553e83380e5587fd4e40724b373d79e4.png) -但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重 - -这时候“扰动函数”的价值就体现出来了 -![](https://img-blog.csdnimg.cn/img_convert/b2051fb82b033621ca8d154861ff5c15.png) -右位移16位,正好是32位一半,自己的高半区和低半区做异或,就是为了混合原始hashCode的高位和低位,以此来加大低位的随机性 -而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。 - -index的运算规则是 -``` -e.hash & (newCap - 1) -``` -newCap是2的幂,所以newCap - 1的高位全0 - -若e.hash值只用自身的hashcode,index只会和e.hash的低位做&操作.这样一来,index的值就只有低位参与运算,高位毫无存在感,从而会带来哈希冲突的风险 -所以在计算key的hashCode时,用其自身hashCode与其低16位做异或操作 -这也就让高位参与到index的计算中来了,即降低了哈希冲突的风险又不会带来太大的性能问题 - -## 4.5 Put方法 -![](https://img-blog.csdnimg.cn/img_convert/da29710b339ad70260610e2ed358305f.png) - -![](https://img-blog.csdnimg.cn/img_convert/977877dab11a1fcd23dffadecbe6b2cd.png) - -![HashMap-put(k,v)](https://img-blog.csdnimg.cn/img_convert/0df25cf27e264797c7f1451e11c927f1.png) -①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容 - -②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③ - -③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals - -④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤ - -⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可 - -⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,执行resize()扩容 -``` - public V put(K key, V value) { - // 对key的hashCode()做hash - return putVal(hash(key), key, value, false, true); - } - -final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { - Node[] tab; Node p; int n, i; - // 步骤① tab为空则调用resize()初始化创建 - if ((tab = table) == null || (n = tab.length) == 0) - n = (tab = resize()).length; - // 步骤② 计算index,并对null做处理 - //tab[i = (n - 1) & hash对应下标的第一个节点 - if ((p = tab[i = (n - 1) & hash]) == null) - // 无哈希冲突的情况下,将value直接封装为Node并赋值 - tab[i] = newNode(hash, key, value, null); - else { - Node e; K k; - // 步骤③ 节点的key相同,直接覆盖节点 - if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) - e = p; - // 步骤④ 判断该链为红黑树 - else if (p instanceof TreeNode) - // p是红黑树类型,则调用putTreeVal方式赋值 - e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); - // 步骤⑤ p非红黑树类型,该链为链表 - else { - // index 相同的情况下 - for (int binCount = 0; ; ++binCount) { - if ((e = p.next) == null) { - // 如果p的next为空,将新的value值添加至链表后面 - p.next = newNode(hash, key, value, null); - if (binCount >= TREEIFY_THRESHOLD - 1) - // 如果链表长度大于8,链表转化为红黑树,执行插入 - treeifyBin(tab, hash); - break; - } - // key相同则跳出循环 - if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) - break; - //就是移动指针方便继续取 p.next - - p = e; - } - } - if (e != null) { // existing mapping for key - V oldValue = e.value; - //根据规则选择是否覆盖value - if (!onlyIfAbsent || oldValue == null) - e.value = value; - afterNodeAccess(e); - return oldValue; - } - } - ++modCount; - // 步骤⑥:超过最大容量,就扩容 - if (++size > threshold) - // size大于加载因子,扩容 - resize(); - afterNodeInsertion(evict); - return null; - } -``` -在构造函数中最多也只是设置了initialCapacity、loadFactor的值,并没有初始化table,table的初始化工作是在put方法中进行的. -## 4.6 resize -![](https://img-blog.csdnimg.cn/img_convert/24a22ebcda082c0c13df1c2193ee18d6.png) -扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,内部的数组无法装载更多的元素时,就需要扩大数组的长度. -当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组 -``` - /** - * 该函数有2种使用情况:1.初始化哈希表 2.当前数组容量过小,需扩容 - */ -final Node[] resize() { - Node[] oldTab = table; - int oldCap = (oldTab == null) ? 0 : oldTab.length; - int oldThr = threshold; - int newCap, newThr = 0; - - // 针对情况2:若扩容前的数组容量超过最大值,则不再扩充 - if (oldCap > 0) { - if (oldCap >= MAXIMUM_CAPACITY) { - threshold = Integer.MAX_VALUE; - return oldTab; - } - // 针对情况2:若无超过最大值,就扩充为原来的2倍 - else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && - oldCap >= DEFAULT_INITIAL_CAPACITY) - //newCap设置为oldCap的2倍并小于MAXIMUM_CAPACITY,且大于默认值, 新的threshold增加为原来的2倍 - newThr = oldThr << 1; // double threshold - } - - // 针对情况1:初始化哈希表(采用指定 or 默认值) - else if (oldThr > 0) // initial capacity was placed in threshold - // threshold>0, 将threshold设置为newCap,所以要用tableSizeFor方法保证threshold是2的幂次方 - newCap = oldThr; - else { // zero initial threshold signifies using defaults - // 默认初始化 - newCap = DEFAULT_INITIAL_CAPACITY; - newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); - } - - // 计算新的resize上限 - if (newThr == 0) { - // newThr为0,newThr = newCap * 0.75 - float ft = (float)newCap * loadFactor; - newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? - (int)ft : Integer.MAX_VALUE); - } - threshold = newThr; - @SuppressWarnings({"rawtypes","unchecked"}) - // 新生成一个table数组 - Node[] newTab = (Node[])new Node[newCap]; - table = newTab; - if (oldTab != null) { - // oldTab 复制到 newTab - for (int j = 0; j < oldCap; ++j) { - Node e; - if ((e = oldTab[j]) != null) { - oldTab[j] = null; - if (e.next == null) - // 链表只有一个节点,直接赋值 - //为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。 - newTab[e.hash & (newCap - 1)] = e; - else if (e instanceof TreeNode) - // e为红黑树的情况 - ((TreeNode)e).split(this, newTab, j, oldCap); - else { // preserve order链表优化重hash的代码块 - Node loHead = null, loTail = null; - Node hiHead = null, hiTail = null; - Node next; - do { - next = e.next; - // 原索引 - if ((e.hash & oldCap) == 0) { - if (loTail == null) - loHead = e; - else - loTail.next = e; - loTail = e; - } - // 原索引 + oldCap - else { - if (hiTail == null) - hiHead = e; - else - hiTail.next = e; - hiTail = e; - } - } while ((e = next) != null); - // 原索引放到bucket里 - if (loTail != null) { - loTail.next = null; - newTab[j] = loHead; - } - // 原索引+oldCap放到bucket里 - if (hiTail != null) { - hiTail.next = null; - newTab[j + oldCap] = hiHead; - } - } - } - } - } - return newTab; - } -``` -![图片发自简书App](https://img-blog.csdnimg.cn/img_convert/dd74408150f2d30938f08296a6501d71.png) - - - -## 4.7 remove方法 -remove(key) 方法 和 remove(key, value) 方法都是通过调用removeNode的方法来实现删除元素的 -```java - final Node removeNode(int hash, Object key, Object value, - boolean matchValue, boolean movable) { - Node[] tab; Node p; int n, index; - if ((tab = table) != null && (n = tab.length) > 0 && - (p = tab[index = (n - 1) & hash]) != null) { - Node node = null, e; K k; V v; - if (p.hash == hash && - ((k = p.key) == key || (key != null && key.equals(k)))) - // index 元素只有一个元素 - node = p; - else if ((e = p.next) != null) { - if (p instanceof TreeNode) - // index处是一个红黑树 - node = ((TreeNode)p).getTreeNode(hash, key); - else { - // index处是一个链表,遍历链表返回node - do { - if (e.hash == hash && - ((k = e.key) == key || - (key != null && key.equals(k)))) { - node = e; - break; - } - p = e; - } while ((e = e.next) != null); - } - } - // 分不同情形删除节点 - if (node != null && (!matchValue || (v = node.value) == value || - (value != null && value.equals(v)))) { - if (node instanceof TreeNode) - ((TreeNode)node).removeTreeNode(this, tab, movable); - else if (node == p) - tab[index] = node.next; - else - p.next = node.next; - ++modCount; - --size; - afterNodeRemoval(node); - return node; - } - } - return null; - } -``` -## 4.8 get -```java -/** - * 函数原型 - * 作用:根据键key,向HashMap获取对应的值 - */ - map.get(key); - - - /** - * 源码分析 - */ - public V get(Object key) { - Node e; - // 1. 计算需获取数据的hash值 - // 2. 通过getNode()获取所查询的数据 ->>分析1 - // 3. 获取后,判断数据是否为空 - return (e = getNode(hash(key), key)) == null ? null : e.value; -} - -/** - * 分析1:getNode(hash(key), key)) - */ -final Node getNode(int hash, Object key) { - Node[] tab; Node first, e; int n; K k; - - // 1. 计算存放在数组table中的位置 - if ((tab = table) != null && (n = tab.length) > 0 && - (first = tab[(n - 1) & hash]) != null) { - - // 4. 通过该函数,依次在数组、红黑树、链表中查找(通过equals()判断) - // a. 先在数组中找,若存在,则直接返回 - if (first.hash == hash && // always check first node - ((k = first.key) == key || (key != null && key.equals(k)))) - return first; - - // b. 若数组中没有,则到红黑树中寻找 - if ((e = first.next) != null) { - // 在树中get - if (first instanceof TreeNode) - return ((TreeNode)first).getTreeNode(hash, key); - - // c. 若红黑树中也没有,则通过遍历,到链表中寻找 - do { - if (e.hash == hash && - ((k = e.key) == key || (key != null && key.equals(k)))) - return e; - } while ((e = e.next) != null); - } - } - return null; -} -``` -> 在JDK1.7及以前的版本中,HashMap里是没有红黑树的实现的,在JDK1.8中加入了红黑树是为了防止哈希表碰撞攻击,当链表链长度为8时,及时转成红黑树,提高map的效率 - -如果某个桶中的记录过大的话(当前是TREEIFY_THRESHOLD = 8),HashMap会动态的使用一个专门的treemap实现来替换掉它。这样做的结果会更好,是O(logn),而不是糟糕的O(n)。它是如何工作的? -前面产生冲突的那些KEY对应的记录只是简单的追加到一个链表后面,这些记录只能通过遍历来进行查找。但是超过这个阈值后HashMap开始将列表升级成一个二叉树,使用哈希值作为树的分支变量,如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。如果哈希值相等,HashMap希望key值最好是实现了Comparable接口的,这样它可以按照顺序来进行插入。这对HashMap的key来说并不是必须的,不过如果实现了当然最好。如果没有实现这个接口,在出现严重的哈希碰撞的时候,你就并别指望能获得性能提升了。 - -这个性能提升有什么用处?比方说恶意的程序,如果它知道我们用的是哈希算法,它可能会发送大量的请求,导致产生严重的哈希碰撞。然后不停的访问这些key就能显著的影响服务器的性能,这样就形成了一次拒绝服务攻击(DoS)。JDK 8中从O(n)到O(logn)的飞跃,可以有效地防止类似的攻击,同时也让HashMap性能的可预测性稍微增强了一些 -```java -/** - * 源码分析:resize(2 * table.length) - * 作用:当容量不足时(容量 > 阈值),则扩容(扩到2倍) - */ - void resize(int newCapacity) { - - // 1. 保存旧数组(old table) - Entry[] oldTable = table; - - // 2. 保存旧容量(old capacity ),即数组长度 - int oldCapacity = oldTable.length; - - // 3. 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出 - if (oldCapacity == MAXIMUM_CAPACITY) { - threshold = Integer.MAX_VALUE; - return; - } - - // 4. 根据新容量(2倍容量)新建1个数组,即新table - Entry[] newTable = new Entry[newCapacity]; - - // 5. (重点分析)将旧数组上的数据(键值对)转移到新table中,从而完成扩容 ->>分析1.1 - transfer(newTable); - - // 6. 新数组table引用到HashMap的table属性上 - table = newTable; - - // 7. 重新设置阈值 - threshold = (int)(newCapacity * loadFactor); -} - - /** - * 分析1.1:transfer(newTable); - * 作用:将旧数组上的数据(键值对)转移到新table中,从而完成扩容 - * 过程:按旧链表的正序遍历链表、在新链表的头部依次插入 - */ -void transfer(Entry[] newTable) { - // 1. src引用了旧数组 - Entry[] src = table; - - // 2. 获取新数组的大小 = 获取新容量大小 - int newCapacity = newTable.length; - - // 3. 通过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中 - for (int j = 0; j < src.length; j++) { - // 3.1 取得旧数组的每个元素 - Entry e = src[j]; - if (e != null) { - // 3.2 释放旧数组的对象引用(for循环后,旧数组不再引用任何对象) - src[j] = null; - - do { - // 3.3 遍历 以该数组元素为首 的链表 - // 注:转移链表时,因是单链表,故要保存下1个结点,否则转移后链表会断开 - Entry next = e.next; - // 3.3 重新计算每个元素的存储位置 - int i = indexFor(e.hash, newCapacity); - // 3.4 将元素放在数组上:采用单链表的头插入方式 = 在链表头上存放数据 = 将数组位置的原有数据放在后1个指针、将需放入的数据放到数组位置中 - // 即 扩容后,可能出现逆序:按旧链表的正序遍历链表、在新链表的头部依次插入 - e.next = newTable[i]; - newTable[i] = e; - // 访问下1个Entry链上的元素,如此不断循环,直到遍历完该链表上的所有节点 - e = next; - } while (e != null); - // 如此不断循环,直到遍历完数组上的所有数据元素 - } - } - } -``` -从上面可看出:在扩容resize()过程中,在将旧数组上的数据 转移到 新数组上时,转移数据操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况 - ->`设重新计算存储位置后不变,即扩容前 = 1->2->3,扩容后 = 3->2->1` - -此时若并发执行 put 操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环(Infinite Loop),即死锁 -![](https://img-blog.csdnimg.cn/img_convert/cd3bbd816bcefb360280b591d5cf41cf.png) -![image.png](https://img-blog.csdnimg.cn/img_convert/cb6354c50e5af1ef24c1ab36b802217e.png) -![](https://img-blog.csdnimg.cn/img_convert/5e35431f5f3c179e389f5528e1b03d42.png) -![为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键](https://img-blog.csdnimg.cn/img_convert/38321f907a385a91616ff972204237d4.png) -## 4.9 getOrDefault -getOrDefault() 方法获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值。 -![](https://img-blog.csdnimg.cn/55e1bdebe6cd4fc9aaa22d6662c501e4.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -# 5 单线程rehash -单线程情况下,rehash无问题 -[![HashMap rehash single thread](https://img-blog.csdnimg.cn/img_convert/50437495a5eda75989d04de58316b1f7.png)](http://www.jasongj.com/img/java/concurrenthashmap/single_thread_rehash.png) -# 6 多线程并发下的rehash - -这里假设有两个线程同时执行了put操作并引发了rehash,执行了transfer方法,并假设线程一进入transfer方法并执行完next = e.next后,因为线程调度所分配时间片用完而“暂停”,此时线程二完成了transfer方法的执行。此时状态如下。 - -[![HashMap rehash multi thread step 1](https://img-blog.csdnimg.cn/img_convert/a5aaefdb773217a54d29cbe9809e2aa9.png)](http://www.jasongj.com/img/java/concurrenthashmap/multi_thread_rehash_1.png) -接着线程1被唤醒,继续执行第一轮循环的剩余部分 -``` -e.next = newTable[1] = null -newTable[1] = e = key(5) -e = next = key(9) -``` -结果如下图所示 -[![HashMap rehash multi thread step 2](https://img-blog.csdnimg.cn/img_convert/a261863e3e27077ba7b3223a3914f0de.png)](http://www.jasongj.com/img/java/concurrenthashmap/multi_thread_rehash_2.png) - -接着执行下一轮循环,结果状态图如下所示 -[![HashMap rehash multi thread step 3](https://img-blog.csdnimg.cn/img_convert/03cea3cdb5a9477ca25e98ee6f37cf43.png)](http://www.jasongj.com/img/java/concurrenthashmap/multi_thread_rehash_3.png) - -继续下一轮循环,结果状态图如下所示 -[![HashMap rehash multi thread step 4](https://img-blog.csdnimg.cn/img_convert/22e59112a7635a44dff4fa42a7e6a840.png)](http://www.jasongj.com/img/java/concurrenthashmap/multi_thread_rehash_4.png) - -此时循环链表形成,并且key(11)无法加入到线程1的新数组。在下一次访问该链表时会出现死循环。 -# 7 Fast-fail -## 产生原因 - -在使用迭代器的过程中如果HashMap被修改,那么`ConcurrentModificationException`将被抛出,也即Fast-fail策略。 - -当HashMap的iterator()方法被调用时,会构造并返回一个新的EntryIterator对象,并将EntryIterator的expectedModCount设置为HashMap的modCount(该变量记录了HashMap被修改的次数)。 -``` -HashIterator() { - expectedModCount = modCount; - if (size > 0) { // advance to first entry - Entry[] t = table; - while (index < t.length && (next = t[index++]) == null) - ; - } -} -``` - - -在通过该Iterator的next方法访问下一个Entry时,它会先检查自己的expectedModCount与HashMap的modCount是否相等,如果不相等,说明HashMap被修改,直接抛出`ConcurrentModificationException`。该Iterator的remove方法也会做类似的检查。该异常的抛出意在提醒用户及早意识到线程安全问题。 - -## 线程安全解决方案 -单线程条件下,为避免出现`ConcurrentModificationException`,需要保证只通过HashMap本身或者只通过Iterator去修改数据,不能在Iterator使用结束之前使用HashMap本身的方法修改数据。因为通过Iterator删除数据时,HashMap的modCount和Iterator的expectedModCount都会自增,不影响二者的相等性。如果是增加数据,只能通过HashMap本身的方法完成,此时如果要继续遍历数据,需要重新调用iterator()方法从而重新构造出一个新的Iterator,使得新Iterator的expectedModCount与更新后的HashMap的modCount相等。 - -多线程条件下,可使用`Collections.synchronizedMap`方法构造出一个同步Map,或者直接使用线程安全的ConcurrentHashMap。 \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ReentrantLock\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ReentrantLock\346\272\220\347\240\201\350\247\243\346\236\220.md" index ad32141823..a1b5860985 100644 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ReentrantLock\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ReentrantLock\346\272\220\347\240\201\350\247\243\346\236\220.md" @@ -74,79 +74,90 @@ ReentrantLock 就负责实现这些接口,使用时,直接调用的也是这 ## 4.2 FairSync - 公平锁 只实现 *lock* 和 *tryAcquire* 两个方法 ### 4.2.1 lock -lock 方法加锁成功,直接返回,所以可以继续执行业务逻辑。 - 公平模式的 lock ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzJhNGEyZmViNzY?x-oss-process=image/format,png) + 直接调用 acquire,而没有像非公平模式先试图获取,因为这样可能导致违反“公平”的语义:在已等待在队列中的线程之前获取了锁。 -*acquire* 是 AQS 的方法,表示先尝试获得锁,失败之后进入同步队列阻塞等待。 +*acquire* 是 AQS 的方法,表示先尝试获得锁,失败之后进入同步队列阻塞等待,详情见本专栏的上一文 ### 4.2.2 tryAcquire +公平模式的 *tryAcquire*。不要授予访问权限,除非递归调用或没有等待线程或是第一个调用的。 - 该方法是 AQS 在 acquire 方法中留给子类去具体实现的 -![](https://img-blog.csdnimg.cn/20210705225313380.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -#### 公平模式 -不要授予访问权限,除非递归调用或没有等待线程或是第一个调用的。 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzJhMjJmYmVkZWM?x-oss-process=image/format,png) + +话不多说,看源码: ```java protected final boolean tryAcquire(int acquires) { - // 获取当前的线程 + // 获取当前的线程 final Thread current = Thread.currentThread(); - // 获取 state 锁的状态(volatile 读语义) + // 获取 state 锁的状态 int c = getState(); // state == 0 => 尚无线程获取锁 if (c == 0) { - // 判断 AQS 的同步对列里是否有线程等待 + // 判断 AQS 的同步对列里是否有线程等待,若没有则直接 CAS 获取锁 if (!hasQueuedPredecessors() && - // 若没有则直接 CAS(保证原子性,线程安全) 获取锁 compareAndSetState(0, acquires)) { // 获取锁成功,设置独占线程 setExclusiveOwnerThread(current); return true; } } - // 已经获取锁的是否为当前的线程? + // 判断已经获取锁是否为当前的线程 else if (current == getExclusiveOwnerThread()) { // 锁的重入, 即 state 加 1 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); - // 已经获取 lock,所以这里不考虑并发 setState(nextc); return true; } return false; } ``` -和Sync#nonfairTryAcquire类似,唯一不同的是当发现锁未被占用时,使用 **hasQueuedPredecessors** 确保了公平性。 +和 Sync 的 *nonfairTryAcquire* 方法实现类似,唯一不同的是当发现锁未被占用时,使用 *hasQueuedPredecessors* 确保了公平性。 #### hasQueuedPredecessors -判断当前线程是不是属于同步队列的头节点的下一个节点(头节点是释放锁的节点): -- 是(返回false),符合FIFO,可以获得锁 -- 不是(返回true),则继续等待 +会判断当前线程是不是属于同步队列的头节点的下一个节点(头节点是释放锁的节点) +- 如果是(返回false),符合FIFO,可以获得锁 +- 如果不是(返回true),则继续等待 ```java -public final boolean hasQueuedPredecessors() { - // 这种方法的正确性取决于头在尾之前初始化和头初始化。如果当前线程是队列中的第一个线程,则next是精确的 - Node t = tail; // 按反初始化顺序读取字段 - Node h = head; - Node s; - return h != t && - ((s = h.next) == null || s.thread != Thread.currentThread()); -} + public final boolean hasQueuedPredecessors() { + // 这种方法的正确性取决于头在尾之前初始化和头初始化。如果当前线程是队列中的第一个线程,则next是精确的 + Node t = tail; // 按反初始化顺序读取字段 + Node h = head; + Node s; + return h != t && + ((s = h.next) == null || s.thread != Thread.currentThread()); + } ``` + + + # 5 nonfairTryAcquire -执行非公平的 *tryLock*。 *tryAcquire* 是在子类中实现的,但是都需要对*trylock* 方法进行非公平的尝试。 +执行非公平的 *tryLock*。 +*tryAcquire* 是在子类中实现的,但是都需要对*trylock* 方法进行非公平的尝试。 + ```java final boolean nonfairTryAcquire(int acquires) { + // 获取当前的线程 final Thread current = Thread.currentThread(); + // 获取 AQS 中的 state 字段 int c = getState(); + // state 为 0,表示同步器的锁尚未被持有 if (c == 0) { - // 这里可能有竞争,所以可能失败 + // CAS state 获取锁(这里可能有竞争,所以可能失败) if (compareAndSetState(0, acquires)) { // 获取锁成功, 设置获取独占锁的线程 setExclusiveOwnerThread(current); + // 直接返回 true return true; } } + // 判断现在获取独占锁的线程是否为当前线程(可重入锁的体现) else if (current == getExclusiveOwnerThread()) { + // state 计数加1(重入获取锁) int nextc = c + acquires; - if (nextc < 0) - throw new Error("Maximum lock count exceeded"); + if (nextc < 0) // 整型溢出 + throw new Error("Maximum lock count exceeded"); + // 已经获取 lock,所以这里不考虑并发 setState(nextc); return true; } @@ -154,7 +165,6 @@ final boolean nonfairTryAcquire(int acquires) { } ``` 无参的 *tryLock* 调用的就是此方法 - # 6 tryLock ## 6.1 无参 Lock 接口中定义的方法。 @@ -166,6 +176,19 @@ Lock 接口中定义的方法。 如果当前线程已经持有该锁,那么持有计数将增加1,方法返回true。 如果锁被另一个线程持有,那么这个方法将立即返回值false。 +- 典型的使用方法 +```java + Lock lock = ...; + if (lock.tryLock()) { + try { + // manipulate protected state + } finally { + lock.unlock(); + } + } else { + // 执行可选的操作 + } +``` ## 6.2 有参 - 提供了超时时间的入参,在时间内,仍没有得到锁,会返回 false ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzJhMzZhNTAwMzQ?x-oss-process=image/format,png) @@ -191,4 +214,6 @@ protected final boolean tryRelease(int releases) { setState(c); return free; } -``` \ No newline at end of file +``` +# 8 总结 +AQS 搭建了整个锁架构,子类锁的实现只需要根据场景,实现 AQS 对应的方法即可。 \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ThreadLocal.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ThreadLocal.md" deleted file mode 100644 index 5f5a4c37b5..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ThreadLocal.md" +++ /dev/null @@ -1,246 +0,0 @@ - -# 1 前言 - -此类提供线程本地变量,与普通变量不同,因为每个访问一个变量(通过其get或set方法)的线程都有其自己的,独立初始化的变量副本。 -ThreadLocal 实例通常是期望将状态与线程(例如,用户ID或事务ID)关联的类中的 private static 字段。 - -例如,下面的类生成每个线程本地的唯一标识符。线程的ID是在第一次调用ThreadId.get() 时赋值的,并且在以后的调用中保持不变。 - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzcvMTcxZWZhYzdiMDdiMDMzNw?x-oss-process=image/format,png) - -只要线程是活跃的并且 ThreadLocal 实例是可访问的,则每个线程都对其线程本地变量的副本持有隐式的引用。线程消失后,线程本地实例的所有副本都会被 GC(除非存在对这些副本的其他引用)。 - -# 2 继续体系 -- 继承?不存在的,这其实也是 java.lang 包下的工具类,但是 ThreadLocal 定义带有泛型,说明可以储存任意格式的数据。 -![](https://img-blog.csdnimg.cn/20210615235535658.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - -# 3 属性 -ThreadLocal 依赖于附加到每个线程(Thread.threadLocals和InheritableThreadLocals)的线程线性探测哈希表。 - -## threadLocalHashCode -ThreadLocal 对象充当key,通过 threadLocalHashCode 进行搜索。这是一个自定义哈希码(仅在ThreadLocalMaps 中有用),它消除了在相同线程使用连续构造的threadlocal的常见情况下的冲突,而在不太常见的情况下仍然表现良好。 - -ThreadLocal 通过这样的 hashCode,计算当前 ThreadLocal 在 ThreadLocalMap 中的索引 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzcvMTcxZWZhYzdiNDcxM2Q2Mw?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzcvMTcxZWZhYzdhZjZjMDQwMg?x-oss-process=image/format,png) - -- 连续生成的哈希码之间的差值,该值的设定参考文章[ThreadLocal的hash算法(关于 0x61c88647)](https://juejin.im/post/5cced289f265da03804380f2) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzcvMTcxZWZhYzdiNmUwNjA0NQ?x-oss-process=image/format,png) - -- 注意 static 修饰。ThreadLocalMap 会被 set 多个 ThreadLocal ,而多个 ThreadLocal 就根据 threadLocalHashCode 区分 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzcvMTcxZWZhYzdkMTkzOWUwMw?x-oss-process=image/format,png) -# 4 ThreadLocalMap -自定义的哈希表,仅适用于维护线程本地的值。没有操作导出到ThreadLocal类之外。 -该类包私有,允许在 Thread 类中的字段声明。为帮助处理非常长的使用寿命,哈希表节点使用 WeakReferences 作为key。 -但由于不使用引用队列,因此仅在表空间不足时,才保证删除过时的节点。 -```java -static class ThreadLocalMap { - - /** - * 此哈希表中的节点使用其主引用字段作为key(始终是一个 ThreadLocal 对象),继承了 WeakReference。 - * 空键(即entry.get()== null)意味着不再引用该键,因此可以从表中删除该节点。 - * 在下面的代码中,此类节点称为 "stale entries" - */ - static class Entry extends WeakReference> { - /** 与此 ThreadLocal 关联的值 */ - Object value; - - Entry(ThreadLocal k, Object v) { - super(k); - value = v; - } - } - - private static final int INITIAL_CAPACITY = 16; - - private Entry[] table; - - private int size = 0; - - private int threshold; // 默认为 0 -``` -## 特点 -- key 是 ThreadLocal 的引用 -- value 是 ThreadLocal 保存的值 -- 数组的数据结构 -# 5 set -## 5.1 ThreadLocal#set -将此线程本地变量的当前线程副本设置为指定值。子类无需重写此方法,而仅依靠initialValue方法设置线程本地变量的值。 -![](https://img-blog.csdnimg.cn/20210616000550930.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 执行流程 -1. 获取当前线程 -2. 获取线程所对应的ThreadLocalMap。每个线程都是独立的,所以该方法天然线程安全 -3. 判断 map 是否为 null - - 否,K.V 对赋值,k 为this(即当前的 ThreaLocal 对象) - - 是,初始化一个 ThreadLocalMap 来维护 K.V 对 - -来具体看看ThreadLocalMap中的 set - -## 5.2 ThreadLocalMap#set -```java -private void set(ThreadLocal key, Object value) { - // 新引用指向 table - Entry[] tab = table; - int len = tab.length; - // 获取对应 ThreadLocal 在table 中的索引 - int i = key.threadLocalHashCode & (len-1); - - /** - * 从该下标开始循环遍历 - * 1、如遇相同key,则直接替换value - * 2、如果该key已经被回收失效,则替换该失效的key - */ - for (Entry e = tab[i]; - e != null; - e = tab[i = nextIndex(i, len)]) { - ThreadLocal k = e.get(); - // 找到内存地址一样的 ThreadLocal,直接替换 - if (k == key) { - e.value = value; - return; - } - // 若 k 为 null,说明 ThreadLocal 被清理了,则替换当前失效的 k - if (k == null) { - replaceStaleEntry(key, value, i); - return; - } - } - // 找到空位,创建节点并插入 - tab[i] = new Entry(key, value); - // table内元素size自增 - int sz = ++size; - // 达到阈值(数组大小的三分之二)时,执行扩容 - if (!cleanSomeSlots(i, sz) && sz >= threshold) - rehash(); -} -``` -注意通过 hashCode 计算的索引位置 i 处如果已经有值了,会从 i 开始,通过 +1 不断的往后寻找,直到找到索引位置为空的地方,把当前 ThreadLocal 作为 key 放进去。 - -# 6 get -```java -public T get() { - // 获取当前线程 - Thread t = Thread.currentThread(); - // 获取当前线程对应的ThreadLocalMap - ThreadLocalMap map = getMap(t); - - // 如果map不为空 - if (map != null) { - // 取得当前ThreadLocal对象对应的Entry - ThreadLocalMap.Entry e = map.getEntry(this); - // 如果不为空,读取当前 ThreadLocal 中保存的值 - if (e != null) { - @SuppressWarnings("unchecked") - T result = (T)e.value; - return result; - } - } - // 否则都执行 setInitialValue - return setInitialValue(); -} -``` -### setInitialValue -```java -private T setInitialValue() { - // 获取初始值,一般是子类重写 - T value = initialValue(); - - // 获取当前线程 - Thread t = Thread.currentThread(); - - // 获取当前线程对应的ThreadLocalMap - ThreadLocalMap map = getMap(t); - - // 如果map不为null - if (map != null) - - // 调用ThreadLocalMap的set方法进行赋值 - map.set(this, value); - - // 否则创建个ThreadLocalMap进行赋值 - else - createMap(t, value); - return value; -} -``` - -接着我们来看下 -## ThreadLocalMap#getEntry -```java -// 得到当前 thradLocal 对应的值,值的类型是由 thradLocal 的泛型决定的 -// 由于 thradLocalMap set 时解决数组索引位置冲突的逻辑,导致 thradLocalMap get 时的逻辑也是对应的 -// 首先尝试根据 hashcode 取模数组大小-1 = 索引位置 i 寻找,找不到的话,自旋把 i+1,直到找到索引位置不为空为止 -private Entry getEntry(ThreadLocal key) { - // 计算索引位置:ThreadLocal 的 hashCode 取模数组大小-1 - int i = key.threadLocalHashCode & (table.length - 1); - Entry e = table[i]; - // e 不为空,并且 e 的 ThreadLocal 的内存地址和 key 相同,直接返回,否则就是没有找到,继续通过 getEntryAfterMiss 方法找 - if (e != null && e.get() == key) - return e; - else - // 这个取数据的逻辑,是因为 set 时数组索引位置冲突造成的 - return getEntryAfterMiss(key, i, e); -} -// 自旋 i+1,直到找到为止 -private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { - Entry[] tab = table; - int len = tab.length; - // 在大量使用不同 key 的 ThreadLocal 时,其实还蛮耗性能的 - while (e != null) { - ThreadLocal k = e.get(); - // 内存地址一样,表示找到了 - if (k == key) - return e; - // 删除没用的 key - if (k == null) - expungeStaleEntry(i); - // 继续使索引位置 + 1 - else - i = nextIndex(i, len); - e = tab[i]; - } - return null; -} -``` -# 6 扩容 -ThreadLocalMap 中的 ThreadLocal 的个数超过阈值时,ThreadLocalMap 就要开始扩容了,我们一起来看下扩容的逻辑: -```java -private void resize() { - // 拿出旧的数组 - Entry[] oldTab = table; - int oldLen = oldTab.length; - // 新数组的大小为老数组的两倍 - int newLen = oldLen * 2; - // 初始化新数组 - Entry[] newTab = new Entry[newLen]; - int count = 0; - // 老数组的值拷贝到新数组上 - for (int j = 0; j < oldLen; ++j) { - Entry e = oldTab[j]; - if (e != null) { - ThreadLocal k = e.get(); - if (k == null) { - e.value = null; // Help the GC - } else { - // 计算 ThreadLocal 在新数组中的位置 - int h = k.threadLocalHashCode & (newLen - 1); - // 如果索引 h 的位置值不为空,往后+1,直到找到值为空的索引位置 - while (newTab[h] != null) - h = nextIndex(h, newLen); - // 给新数组赋值 - newTab[h] = e; - count++; - } - } - } - // 给新数组初始化下次扩容阈值,为数组长度的三分之二 - setThreshold(newLen); - size = count; - table = newTab; -} -``` -扩容时是绝对没有线程安全问题的,因为 ThreadLocalMap 是线程的一个属性,一个线程同一时刻只能对 ThreadLocalMap 进行操作,因为同一个线程执行业务逻辑必然是串行的,那么操作 ThreadLocalMap 必然也是串行的。 -# 7 总结 -我们在写中间件的时候经常会用到,比如说流程引擎中上下文的传递,调用链ID的传递等。 \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/\343\200\220\346\255\273\347\243\225JDK\346\272\220\347\240\201\343\200\221ThreadPoolExecutor\346\272\220\347\240\201\344\277\235\345\247\206\347\272\247\350\257\246\350\247\243.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/\343\200\220\346\255\273\347\243\225JDK\346\272\220\347\240\201\343\200\221ThreadPoolExecutor\346\272\220\347\240\201\344\277\235\345\247\206\347\272\247\350\257\246\350\247\243.md" deleted file mode 100644 index 6968385400..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/\343\200\220\346\255\273\347\243\225JDK\346\272\220\347\240\201\343\200\221ThreadPoolExecutor\346\272\220\347\240\201\344\277\235\345\247\206\347\272\247\350\257\246\350\247\243.md" +++ /dev/null @@ -1,686 +0,0 @@ -位运算表示线程池状态,因为位运算是改变当前值的一种高效手段。 - -# 属性 -## 线程池状态 -Integer 有32位: -- 最左边3位表示线程池状态,可表示从0至7的8个不同数值 -- 最右边29位表工作线程数 -```java -private static final int COUNT_BITS = Integer.SIZE - 3; -``` - - -线程池的状态用高3位表示,其中包括了符号位。五种状态的十进制值按从小到大依次排序为: - -```bash -RUNNING < SHUTDOWN < STOP < TIDYING =核心线程数 或线程创建失败,则将当前任务放到工作队列中 -// 只有线程池处于 RUNNING 态,才执行后半句 : 置入队列 -if (isRunning(c) && workQueue.offer(command)) { - int recheck = ctl.get(); - - // 只有线程池处于 RUNNING 态,才执行后半句 : 置入队列 - if (! isRunning(recheck) && remove(command)) - reject(command); - // 若之前的线程已被消费完,新建一个线程 - else if (workerCountOf(recheck) == 0) - addWorker(null, false); -// 核心线程和队列都已满,尝试创建一个新线程 -} -else if (!addWorker(command, false)) - // 抛出RejectedExecutionException异常 - // 若 addWorker 返回是 false,即创建失败,则唤醒拒绝策略. - reject(command); -} -``` -发生拒绝的理由有两个 -( 1 )线程池状态为非RUNNING状态 -(2)等待队列已满。 - -下面继续分析`addWorker` - -## addWorker 源码解析 -原子性地检查 runState 和 workerCount,通过返回 false 来防止在不应该添加线程时出现误报。 - -根据当前线程池状态,检查是否可以添加新的线程: -- 若可 -则创建并启动任务;若一切正常则返回true; -- 返回false的可能原因: -1. 线程池没有处`RUNNING`态 -2. 线程工厂创建新的任务线程失败 -### 参数 -- firstTask -外部启动线程池时需要构造的第一个线程,它是线程的母体 -- core -新增工作线程时的判断指标 - - true -需要判断当前`RUNNING`态的线程是否少于`corePoolsize` - - false -需要判断当前`RUNNING`态的线程是否少于`maximumPoolsize` -### JDK8源码 -```java -private boolean addWorker(Runnable firstTask, boolean core) { - // 1. 不需要任务预定义的语法标签,响应下文的continue retry - // 快速退出多层嵌套循环 - retry: - // 外自旋,判断线程池的运行状态 - for (;;) { - int c = ctl.get(); - int rs = runStateOf(c); - // 2. 若RUNNING态,则条件为false,不执行后面判断 - // 若STOP及以上状态,或firstTask初始线程非空,或队列为空 - // 都会直接返回创建失败 - // Check if queue empty only if necessary. - if (rs >= SHUTDOWN && - ! (rs == SHUTDOWN && - firstTask == null && - ! workQueue.isEmpty())) - return false; - - for (;;) { - int wc = workerCountOf(c); - // 若超过最大允许线程数,则不能再添加新线程 - if (wc >= CAPACITY || - wc >= (core ? corePoolSize : maximumPoolSize)) - return false; - // 3. 将当前活动线程数+1 - if (compareAndIncrementWorkerCount(c)) - break retry; - // 线程池状态和工作线程数是可变化的,需经常读取最新值 - c = ctl.get(); // Re-read ctl - // 若已关闭,则再次从retry 标签处进入,在第2处再做判断(第4处) - if (runStateOf(c) != rs) - continue retry; - //如果线程池还是RUNNING态,说明仅仅是第3处失败 -//继续循环执行(第5外) - // else CAS failed due to workerCount change; retry inner loop - } - } - - // 开始创建工作线程 - boolean workerStarted = false; - boolean workerAdded = false; - Worker w = null; - try { - // 利用Worker 构造方法中的线程池工厂创建线程,并封装成工作线程Worker对象 - // 和 AQS 有关!!! - w = new Worker(firstTask); - // 6. 注意这是Worker中的属性对象thread - final Thread t = w.thread; - if (t != null) { - // 在进行ThreadpoolExecutor的敏感操作时 - // 都需要持有主锁,避免在添加和启动线程时被干扰 - final ReentrantLock mainLock = this.mainLock; - mainLock.lock(); - try { - // Recheck while holding lock. - // Back out on ThreadFactory failure or if - // shut down before lock acquired. - int rs = runStateOf(ctl.get()); - // 当线程池状态为RUNNING 或SHUTDOWN - // 且firstTask 初始线程为空时 - if (rs < SHUTDOWN || - (rs == SHUTDOWN && firstTask == null)) { - if (t.isAlive()) // precheck that t is startable - throw new IllegalThreadStateException(); - workers.add(w); - int s = workers.size(); - // 整个线程池在运行期间的最大并发任务个数 - if (s > largestPoolSize) - // 更新为工作线程的个数 - largestPoolSize = s; - // 新增工作线程成功 - workerAdded = true; - } - } finally { - mainLock.unlock(); - } - if (workerAdded) { - // 看到亲切迷人的start方法了! - // 这并非线程池的execute 的command 参数指向的线程 - t.start(); - workerStarted = true; - } - } - } finally { - // 线程启动失败,把刚才第3处加,上的工作线程计数再减-回去 - if (! workerStarted) - addWorkerFailed(w); - } - return workerStarted; -} -``` -#### 第1处 -配合循环语句出现的标签,类似于goto语法作用。label 定义时,必须把标签和冒号的组合语句紧紧相邻定义在循环体之前,否则编译报错。目的是在实现多重循环时能够快速退出到任何一层。出发点似乎非常贴心,但在大型软件项目中,滥用标签行跳转的后果将是无法维护的! - - -在 **workerCount** 加1成功后,直接退出两层循环。 - -#### 第2处,这样的表达式不利于阅读,应如是 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzg1XzQ2ODU5NjgtMDg2ZTlkNWY5ZGEyYWZkNC5wbmc?x-oss-process=image/format,png) - -#### 第3处 -与第1处的标签呼应,`AtomicInteger`对象的加1操作是原子性的。`break retry`表 直接跳出与`retry` 相邻的这个循环体 - -#### 第4处 -此`continue`跳转至标签处,继续执行循环. -如果条件为false,则说明线程池还处于运行状态,即继续在`for(;)`循环内执行. - -#### 第5处 -`compareAndIncrementWorkerCount `方法执行失败的概率非常低. -即使失败,再次执行时成功的概率也是极高的,类似于自旋原理. -这里是先加1,创建失败再减1,这是轻量处理并发创建线程的方式; -如果先创建线程,成功再加1,当发现超出限制后再销毁线程,那么这样的处理方式明显比前者代价要大. - -#### 第6处 -`Worker `对象是工作线程的核心类实现。它实现了`Runnable`接口,并把本对象作为参数输入给`run()`中的`runWorker (this)`。所以内部属性线程`thread`在`start`的时候,即会调用`runWorker`。 - -```java -private final class Worker - extends AbstractQueuedSynchronizer - implements Runnable -{ - /** - * This class will never be serialized, but we provide a - * serialVersionUID to suppress a javac warning. - */ - private static final long serialVersionUID = 6138294804551838833L; - - /** Thread this worker is running in. Null if factory fails. */ - final Thread thread; - /** Initial task to run. Possibly null. */ - Runnable firstTask; - /** Per-thread task counter */ - volatile long completedTasks; - - /** - * Creates with given first task and thread from ThreadFactory. - * @param firstTask the first task (null if none) - */ - Worker(Runnable firstTask) { - setState(-1); // 直到调用runWorker前,禁止被中断 - this.firstTask = firstTask; - this.thread = getThreadFactory().newThread(this); - } - - /** 将主线程的 run 循环委托给外部的 runWorker 执行 */ - public void run() { - runWorker(this); - } - - // Lock methods - // - // The value 0 represents the unlocked state. - // The value 1 represents the locked state. - - protected boolean isHeldExclusively() { - return getState() != 0; - } - - protected boolean tryAcquire(int unused) { - if (compareAndSetState(0, 1)) { - setExclusiveOwnerThread(Thread.currentThread()); - return true; - } - return false; - } - - protected boolean tryRelease(int unused) { - setExclusiveOwnerThread(null); - setState(0); - return true; - } - - public void lock() { acquire(1); } - public boolean tryLock() { return tryAcquire(1); } - public void unlock() { release(1); } - public boolean isLocked() { return isHeldExclusively(); } - - void interruptIfStarted() { - Thread t; - if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) { - try { - t.interrupt(); - } catch (SecurityException ignore) { - } - } - } -} -``` -#### setState(-1)是为何 -设置个简单的状态,检查状态以防止中断。在调用停止线程池时会判断state 字段,决定是否中断之。 -![](https://img-blog.csdnimg.cn/20210713174701301.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70)![](https://img-blog.csdnimg.cn/20210713175625198.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70)![](https://img-blog.csdnimg.cn/20210713175645371.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70)![](https://img-blog.csdnimg.cn/20210713175718745.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -#### t 到底是谁? -![](https://img-blog.csdnimg.cn/20210713180707150.png) -# 源码分析 -```java - /** - * 检查是否可以根据当前池状态和给定的边界(核心或最大) - * 添加新工作线程。如果是这样,工作线程数量会相应调整,如果可能的话,一个新的工作线程创建并启动 - * 将firstTask作为其运行的第一项任务。 - * 如果池已停止此方法返回false - * 如果线程工厂在被访问时未能创建线程,也返回false - * 如果线程创建失败,或者是由于线程工厂返回null,或者由于异常(通常是在调用Thread.start()后的OOM)),我们干净地回滚。 - */ - private boolean addWorker(Runnable firstTask, boolean core) { - /** - * Check if queue empty only if necessary. - * - * 如果线程池已关闭,并满足以下条件之一,那么不创建新的 worker: - * 1. 线程池状态大于 SHUTDOWN,也就是 STOP, TIDYING, 或 TERMINATED - * 2. firstTask != null - * 3. workQueue.isEmpty() - * 简单分析下: - * 状态控制的问题,当线程池处于 SHUTDOWN ,不允许提交任务,但是已有任务继续执行 - * 当状态大于 SHUTDOWN ,不允许提交任务,且中断正在执行任务 - * 多说一句:若线程池处于 SHUTDOWN,但 firstTask 为 null,且 workQueue 非空,是允许创建 worker 的 - * - */ - if (rs >= SHUTDOWN && - ! (rs == SHUTDOWN && - firstTask == null && - ! workQueue.isEmpty())) - return false; - - for (;;) { - int wc = workerCountOf(c); - if (wc >= CAPACITY || - wc >= (core ? corePoolSize : maximumPoolSize)) - return false; - // 如果成功,那么就是所有创建线程前的条件校验都满足了,准备创建线程执行任务 - // 这里失败的话,说明有其他线程也在尝试往线程池中创建线程 - if (compareAndIncrementWorkerCount(c)) - break retry; - // 由于有并发,重新再读取一下 ctl - c = ctl.get(); // Re-read ctl - // 正常如果是 CAS 失败的话,进到下一个里层的for循环就可以了 - // 可如果是因为其他线程的操作,导致线程池的状态发生了变更,如有其他线程关闭了这个线程池 - // 那么需要回到外层的for循环 - if (runStateOf(c) != rs) - continue retry; - // else CAS failed due to workerCount change; retry inner loop - } - } - - /* * - * 到这里,我们认为在当前这个时刻,可以开始创建线程来执行任务 - */ - - // worker 是否已经启动 - boolean workerStarted = false; - // 是否已将这个 worker 添加到 workers 这个 HashSet 中 - boolean workerAdded = false; - Worker w = null; - try { - // 把 firstTask 传给 worker 的构造方法 - w = new Worker(firstTask); - // 取 worker 中的线程对象,Worker的构造方法会调用 ThreadFactory 来创建一个新的线程 - final Thread t = w.thread; - if (t != null) { - //先加锁 - final ReentrantLock mainLock = this.mainLock; - // 这个是整个类的全局锁,持有这个锁才能让下面的操作“顺理成章”, - // 因为关闭一个线程池需要这个锁,至少我持有锁的期间,线程池不会被关闭 - mainLock.lock(); - try { - // Recheck while holding lock. - // Back out on ThreadFactory failure or if - // shut down before lock acquired. - int rs = runStateOf(ctl.get()); - - // 小于 SHUTTDOWN 即 RUNNING - // 如果等于 SHUTDOWN,不接受新的任务,但是会继续执行等待队列中的任务 - if (rs < SHUTDOWN || - (rs == SHUTDOWN && firstTask == null)) { - // worker 里面的 thread 不能是已启动的 - if (t.isAlive()) // precheck that t is startable - throw new IllegalThreadStateException(); - // 加到 workers 这个 HashSet 中 - workers.add(w); - int s = workers.size(); - if (s > largestPoolSize) - largestPoolSize = s; - workerAdded = true; - } - } finally { - mainLock.unlock(); - } - // 若添加成功 - if (workerAdded) { - // 启动线程 - t.start(); - workerStarted = true; - } - } - } finally { - // 若线程没有启动,做一些清理工作,若前面 workCount 加了 1,将其减掉 - if (! workerStarted) - addWorkerFailed(w); - } - // 返回线程是否启动成功 - return workerStarted; - } -``` -看下 `addWorkFailed` -![](https://img-blog.csdnimg.cn/20210714141244398.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - -![记录 workers 中的个数的最大值,因为 workers 是不断增加减少的,通过这个值可以知道线程池的大小曾经达到的最大值](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzA4XzQ2ODU5NjgtMDc4NDcyYjY4MmZjYzljZC5wbmc?x-oss-process=image/format,png) -继续看 -### runWorker -```java -// worker 线程启动后调用,while 循环(即自旋!)不断从等待队列获取任务并执行 -// worker 初始化时,可指定 firstTask,那么第一个任务也就可以不需要从队列中获取 -final void runWorker(Worker w) { - Thread wt = Thread.currentThread(); - // 该线程的第一个任务(若有) - Runnable task = w.firstTask; - w.firstTask = null; - // 允许中断 - w.unlock(); - - boolean completedAbruptly = true; - try { - // 循环调用 getTask 获取任务 - while (task != null || (task = getTask()) != null) { - w.lock(); - // 若线程池状态大于等于 STOP,那么意味着该线程也要中断 - /** - * 若线程池STOP,请确保线程 已被中断 - * 如果没有,请确保线程未被中断 - * 这需要在第二种情况下进行重新检查,以便在关中断时处理shutdownNow竞争 - */ - if ((runStateAtLeast(ctl.get(), STOP) || - (Thread.interrupted() && - runStateAtLeast(ctl.get(), STOP))) && - !wt.isInterrupted()) - wt.interrupt(); - try { - // 这是一个钩子方法,留给需要的子类实现 - beforeExecute(wt, task); - Throwable thrown = null; - try { - // 到这里终于可以执行任务了 - task.run(); - } catch (RuntimeException x) { - thrown = x; throw x; - } catch (Error x) { - thrown = x; throw x; - } catch (Throwable x) { - // 这里不允许抛出 Throwable,所以转换为 Error - thrown = x; throw new Error(x); - } finally { - // 也是一个钩子方法,将 task 和异常作为参数,留给需要的子类实现 - afterExecute(task, thrown); - } - } finally { - // 置空 task,准备 getTask 下一个任务 - task = null; - // 累加完成的任务数 - w.completedTasks++; - // 释放掉 worker 的独占锁 - w.unlock(); - } - } - completedAbruptly = false; - } finally { - // 到这里,需要执行线程关闭 - // 1. 说明 getTask 返回 null,也就是说,这个 worker 的使命结束了,执行关闭 - // 2. 任务执行过程中发生了异常 - // 第一种情况,已经在代码处理了将 workCount 减 1,这个在 getTask 方法分析中说 - // 第二种情况,workCount 没有进行处理,所以需要在 processWorkerExit 中处理 - processWorkerExit(w, completedAbruptly); - } -} -``` -看看 -### getTask() -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzgwXzQ2ODU5NjgtNWU5NDc3MzE5M2Q5Y2Y0OS5wbmc?x-oss-process=image/format,png) -```java -// 此方法有三种可能 -// 1. 阻塞直到获取到任务返回。默认 corePoolSize 之内的线程是不会被回收的,它们会一直等待任务 -// 2. 超时退出。keepAliveTime 起作用的时候,也就是如果这么多时间内都没有任务,那么应该执行关闭 -// 3. 如果发生了以下条件,须返回 null -// 池中有大于 maximumPoolSize 个 workers 存在(通过调用 setMaximumPoolSize 进行设置) -// 线程池处于 SHUTDOWN,而且 workQueue 是空的,前面说了,这种不再接受新的任务 -// 线程池处于 STOP,不仅不接受新的线程,连 workQueue 中的线程也不再执行 -private Runnable getTask() { - boolean timedOut = false; // Did the last poll() time out? - - for (;;) { - // 允许核心线程数内的线程回收,或当前线程数超过了核心线程数,那么有可能发生超时关闭 - - // 这里 break,是为了不往下执行后一个 if (compareAndDecrementWorkerCount(c)) - // 两个 if 一起看:如果当前线程数 wc > maximumPoolSize,或者超时,都返回 null - // 那这里的问题来了,wc > maximumPoolSize 的情况,为什么要返回 null? - // 换句话说,返回 null 意味着关闭线程。 - // 那是因为有可能开发者调用了 setMaximumPoolSize 将线程池的 maximumPoolSize 调小了 - - // 如果此 worker 发生了中断,采取的方案是重试 - // 解释下为什么会发生中断,这个读者要去看 setMaximumPoolSize 方法, - // 如果开发者将 maximumPoolSize 调小了,导致其小于当前的 workers 数量, - // 那么意味着超出的部分线程要被关闭。重新进入 for 循环,自然会有部分线程会返回 null - int c = ctl.get(); - int rs = runStateOf(c); - - // Check if queue empty only if necessary. - if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { - // CAS 操作,减少工作线程数 - decrementWorkerCount(); - return null; - } - - int wc = workerCountOf(c); - - // Are workers subject to culling? - boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; - - if ((wc > maximumPoolSize || (timed && timedOut)) - && (wc > 1 || workQueue.isEmpty())) { - if (compareAndDecrementWorkerCount(c)) - return null; - continue; - } - - try { - Runnable r = timed ? - workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : - workQueue.take(); - if (r != null) - return r; - timedOut = true; - } catch (InterruptedException retry) { - // 如果此 worker 发生了中断,采取的方案是重试 - // 解释下为什么会发生中断,这个读者要去看 setMaximumPoolSize 方法, - // 如果开发者将 maximumPoolSize 调小了,导致其小于当前的 workers 数量, - // 那么意味着超出的部分线程要被关闭。重新进入 for 循环,自然会有部分线程会返回 null - timedOut = false; - } - } -} -``` -到这里,基本上也说完了整个流程,回到 execute(Runnable command) 方法,看看各个分支,我把代码贴过来一下: -```java - public void execute(Runnable command) { - if (command == null) - throw new NullPointerException(); - //表示 “线程池状态” 和 “线程数” 的整数 - int c = ctl.get(); - // 如果当前线程数少于核心线程数,直接添加一个 worker 执行任务, - // 创建一个新的线程,并把当前任务 command 作为这个线程的第一个任务(firstTask) - if (workerCountOf(c) < corePoolSize) { - // 添加任务成功,即结束 - // 执行的结果,会包装到 FutureTask - // 返回 false 代表线程池不允许提交任务 - if (addWorker(command, true)) - return; - - c = ctl.get(); - } - // 到这说明,要么当前线程数大于等于核心线程数,要么刚刚 addWorker 失败 - - // 如果线程池处于 RUNNING ,把这个任务添加到任务队列 workQueue 中 - if (isRunning(c) && workQueue.offer(command)) { - /* 若任务进入 workQueue,我们是否需要开启新的线程 - * 线程数在 [0, corePoolSize) 是无条件开启新线程的 - * 若线程数已经大于等于 corePoolSize,则将任务添加到队列中,然后进到这里 - */ - int recheck = ctl.get(); - // 若线程池不处于 RUNNING ,则移除已经入队的这个任务,并且执行拒绝策略 - if (! isRunning(recheck) && remove(command)) - reject(command); - // 若线程池还是 RUNNING ,且线程数为 0,则开启新的线程 - // 这块代码的真正意图:担心任务提交到队列中了,但是线程都关闭了 - else if (workerCountOf(recheck) == 0) - addWorker(null, false); - } - // 若 workQueue 满,到该分支 - // 以 maximumPoolSize 为界创建新 worker, - // 若失败,说明当前线程数已经达到 maximumPoolSize,执行拒绝策略 - else if (!addWorker(command, false)) - reject(command); - } -``` -**工作线程**:线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会循环获取工作队列里的任务来执行.我们可以从Worker类的run()方法里看到这点 - -```java - public void run() { - try { - Runnable task = firstTask; - firstTask = null; - while (task != null || (task = getTask()) != null) { - runTask(task); - task = null; - } - } finally { - workerDone(this); - } - } - boolean workerStarted = false; - boolean workerAdded = false; - Worker w = null; - try { - w = new Worker(firstTask); - - final Thread t = w.thread; - if (t != null) { - //先加锁 - final ReentrantLock mainLock = this.mainLock; - mainLock.lock(); - try { - // Recheck while holding lock. - // Back out on ThreadFactory failure or if - // shut down before lock acquired. - int rs = runStateOf(ctl.get()); - - if (rs < SHUTDOWN || - (rs == SHUTDOWN && firstTask == null)) { - if (t.isAlive()) // precheck that t is startable - throw new IllegalThreadStateException(); - workers.add(w); - int s = workers.size(); - if (s > largestPoolSize) - largestPoolSize = s; - workerAdded = true; - } - } finally { - mainLock.unlock(); - } - if (workerAdded) { - t.start(); - workerStarted = true; - } - } - } finally { - if (! workerStarted) - addWorkerFailed(w); - } - return workerStarted; - } -``` -线程池中的线程执行任务分两种情况 - - 在execute()方法中创建一个线程时,会让这个线程执行当前任务 - - 这个线程执行完上图中 1 的任务后,会反复从BlockingQueue获取任务来执行 \ No newline at end of file diff --git "a/JDK/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213\346\267\261\345\205\245\350\257\246\350\247\243(JMM).md" "b/JDK/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213\346\267\261\345\205\245\350\257\246\350\247\243(JMM).md" deleted file mode 100644 index 1dbb544b41..0000000000 --- "a/JDK/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213\346\267\261\345\205\245\350\257\246\350\247\243(JMM).md" +++ /dev/null @@ -1,128 +0,0 @@ -# 前言 -定义俩共享变量及俩方法: -- 第一个方法, -- 第二个方法 -- (r1,r2)的可能值有哪些? -![](https://img-blog.csdnimg.cn/05139ccfbb40447a869632ff35959841.png) - -在单线程环境下,可先调用第一个方法,最终(r1,r2)为(1,0) -也可以先调用第二个方法,最终为(0,2)。 - -![](https://img-blog.csdnimg.cn/20200404214401993.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -# 1 Java内存模型的意义 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTkyYTFmZGY0OGJlMTllMDYucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTIzZTVlOWE0OWFkZWI1YTEucG5n?x-oss-process=image/format,png) -JMM 与硬件内存架构对应关系![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTVlMTM3NGEwYWJmOWM5MjkucG5n?x-oss-process=image/format,png) -JMM抽象结构图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQ0ZWE4ODQzYTg4YTk0MGQucG5n?x-oss-process=image/format,png) -内存模型描述程序的可能行为。 - -Java虚拟机规范中试图定义一种Java内存模型,`来屏蔽掉各种硬件和os的内存访问差异`,规定: -- 线程如何、何时能看到其他线程修改过的共享变量的值 -- 必要时,如何同步地访问共享变量 - -以实现让Java程序在各种平台下都能达到一致性的内存访问效果。 - -JMM通过检查执行跟踪中的每个读操作,并根据某些规则检查该读操作观察到的写操作是否有效来工作。 - -只要程序的所有执行产生的结果都可由JMM预测。具体实现者任意实现,包括操作的重新排序和删除不必要的同步。 - -JMM决定了在程序的每个点上可以读取什么值。 -## 1.1 共享变量(Shared Variables) -可在线程之间共享的内存称为`共享内存或堆内存`。所有实例字段、静态字段和数组元素都存储在堆内存。 -不包括局部变量与方法参数,因为这些是线程私有的,不存在共享。 - -对同一变量的两次访问(读或写),若有一个是写请求,则是冲突的! -# 2 主内存与工作内存 -工作内存缓存 -![](https://img-blog.csdnimg.cn/20191014024209488.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -JMM的主要是定义了`各个变量的访问规则`,在JVM中的如下底层细节: -- 将变量存储到内存 -- 从内存中取出变量值 - -为获得较好执行效率,JMM并未限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器调整代码执行顺序这类权限。 - -JMM规定: -- 所有变量都存储在主内存(Main Memory) -- 每条线程有自己的工作内存(Working Memory) -保存了该线程使用到的`变量的主内存副本拷贝`(线程所访问对象的引用或者对象中某个在线程访问到的字段,不会是整个对象的拷贝) -线程对变量的所有操作(读,赋值等)都必须在工作内存进行,不能直接读写主内存中的变量 -volatile变量依然有工作内存的拷贝,,是他特殊的操作顺序性规定,看起来如同直接在主内存读写 -不同线程间,无法直接访问对方工作内存中的变量,线程间变量值的传递均要通过主内存 - -线程、主内存、工作内存三者的交互关系: -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTEyMjA5YjEyZDU3OGEyZWQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWJiM2QzN2MxNTVjZDgyZDgucG5n?x-oss-process=image/format,png) - -JVM模型与JMM不是同一层次的内存划分,基本毫无关系的,硬要对应起来,从变量,内存,工作内存的定义来看 -- 主内存 《=》Java堆中的对象实例数据部分 -- 工作内存 《=》虚拟机栈中的部分区域 - -从更底层的层次来看: -- 主内存直接对应物理硬件的内存 -- 为更好的运行速度,虚拟机(甚至硬件系统的本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存器,因为程序运行时主要访问读写的是工作内存 -# 3 内存间同步操作 -## 3.1 线程操作的定义 -### 操作定义 -write要写的变量以及要写的值。 -read要读的变量以及可见的写入值(由此,我们可以确定可见的值)。 -lock要锁定的管程(监视器monitor)。 -unlock要解锁的管程。 -外部操作(socket等等..) -启动和终止 -### 程序顺序 -如果一个程序没有数据竞争,那么程序的所有执行看起来都是顺序一致的 - -本规范只涉及线程间的操作; -一个变量如何从主内存拷贝到工作内存,从工作内存同步回主内存的实现细节 - -JMM 本身已经定义实现了以下8种操作来完成,且都具备`原子性` -- lock(锁定) -作用于主内存变量,把一个变量标识为一条线程独占的状态 -- unlock(解锁) -作用于主内存变量,把一个处于锁定状态的变量释放,释放后的变量才可以被其它线程锁定 -unlock之前必须将变量值同步回主内存 -- read(读取) -作用于主内存变量,把一个变量的值从主内存传输到工作内存,以便随后的load -- load(载入) -作用于工作内存变量,把read从主内存中得到的变量值放入工作内存的变量副本 -- use(使用) -作用于工作内存变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到的变量的值得字节码指令时将会执行这个操作 -- assign(赋值) -作用于工作内存变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 -- store(存储) -作用于工作内存变量,把工作内存中一个变量的值传送到主内存,以便随后的write操作使用 -- write(写入) -作用于主内存变量,把store操作从工作内存中得到的值放入主内存的变量中 - -- 把一个变量从主内存`复制`到工作内存 -就要顺序执行read和load - -- 把变量从工作内存`同步`回主内存 -就要顺序地执行store和write操作 - -JMM只要求上述两个操作必须`按序执行`,而没有保证连续执行 -也就是说read/load之间、store/write之间可以插入其它指令 -如对主内存中的变量a,b访问时,一种可能出现的顺序是read a->readb->loadb->load a - -JMM规定执行上述八种基础操作时必须满足如下 -## 3.1 同步规则 -◆ 对于监视器 m 的解锁与所有后续操作对于 m 的加锁 `同步`(之前的操作保持可见) -◆对 volatile变量v的写入,与所有其他线程后续对v的读同步 - -◆ `启动` 线程的操作与线程中的第一个操作同步 -◆ 对于每个属性写入默认值(0, false, null)与每个线程对其进行的操作同步 -◆ 线程 T1的最后操作与线程T2发现线程T1已经结束同步。( isAlive ,join可以判断线程是否终结) -◆ 如果线程 T1中断了T2,那么线程T1的中断操作与其他所有线程发现T2被中断了同步通过抛出*InterruptedException*异常,或者调用*Thread.interrupted*或*Thread.isInterrupted* - -- 不允许read/load、store/write操作之一单独出现 -不允许一个变量从主内存读取了但工作内存不接收,或从工作内存发起回写但主内存不接收 -- 不允许一个线程丢弃它的最近的assign -即变量在工作内存中改变(为工作内存变量赋值)后必须把该变化同步回主内存 -- 新变量只能在主内存“诞生”,不允许在工作内存直接使用一个未被初始化(load或assign)的变量 -换话说就是一个变量在实施use,store之前,必须先执行过assign和load -- 如果一个变量事先没有被load锁定,则不允许对它执行unlock,也不允许去unlock一个被其它线程锁定的变量 -- 对一个变量执行unloack前,必须把此变量同步回主内存中(执行store,write) - -> 参考 -> - https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.1 \ No newline at end of file diff --git "a/JDK/JVM/Jprofile\350\247\243\346\236\220dump\346\226\207\344\273\266\344\275\277\347\224\250\350\257\246\350\247\243.md" "b/JDK/JVM/Jprofile\350\247\243\346\236\220dump\346\226\207\344\273\266\344\275\277\347\224\250\350\257\246\350\247\243.md" deleted file mode 100644 index a2c6a21772..0000000000 --- "a/JDK/JVM/Jprofile\350\247\243\346\236\220dump\346\226\207\344\273\266\344\275\277\347\224\250\350\257\246\350\247\243.md" +++ /dev/null @@ -1,213 +0,0 @@ -# 1 Jprofile简介 -- [官网](https://www.ej-technologies.com/products/jprofiler/overview.html) -![在这里插入图片描述](https://img-blog.csdnimg.cn/20200229024741487.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 下载对应的系统版本即可 -![](https://img-blog.csdnimg.cn/20200229024854171.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -性能查看工具JProfiler,可用于查看java执行效率,查看线程状态,查看内存占用与内存对象,还可以分析dump日志. - - -# 2 功能简介 -- 选择attach to a locally running jvm -![](https://img-blog.csdnimg.cn/20200229025200914.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 选择需要查看运行的jvm,双击或者点击start -![](https://img-blog.csdnimg.cn/20200229025304638.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 等待进度完成,弹出模式选择 -![](https://img-blog.csdnimg.cn/20200229025406155.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - - Instrumentation模式记录所有的信息。包括方法执行次数等Sampling模式则只支持部分功能,不纪录方法调用次数等,并且更为安全 -由于纪录信息很多,java运行会变的比正常执行慢很多,sampling模式则不会 - - 常规使用选择sampling模式即可,当需要调查方法执行次数才需要选择Instrumentation模式,模式切换需要重启jprofiler - -- 点击OK -![](https://img-blog.csdnimg.cn/20200229025604706.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 选择Live Momory可以查看内存中的对象和大小 -![](https://img-blog.csdnimg.cn/202002290257375.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 选择cpu views点击下图框中的按钮来纪录cpu的执行时间 -![](https://img-blog.csdnimg.cn/20200229025934939.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 这时候可以在外部对需要录的jvm操作进行记录了,得出的结果可以轻松看出方法执行调用过程与消耗时间比例: - -- 根据cpu截图的信息,可以找到效率低的地方进行处理,如果是Instrumentation模式则在时间位置会显示调用次数 - - -在Thread界面则可以实时查看线程运行状态,黄色的是wait 红色是block 绿色的是runnable蓝色是网络和I/O请求状态 -![](https://img-blog.csdnimg.cn/20200229030202768.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -选择ThreadDumps,可以录制瞬时线程的调用堆栈信息,如下图所示: -![](https://img-blog.csdnimg.cn/20200229030320692.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -# 3 dump 文件分析 -## 3.1 dump 生成 -### JProfiler 在线 - -当JProfiler连接到JVM之后选择Heap Walker,选择Take snapshot图标,然后等待即可 -![](https://img-blog.csdnimg.cn/20200229030546342.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20200229030602936.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -如果内存很大,jprofiler万一参数设置的不正确打不开就需要要重新生成,内存小的时候无所谓 - -### 使用JProfiler生成文件 -当JProfiler连接到JVM之后选择菜单上的Profiling->save HPROF snapshot 弹出下拉框保存即可,这时候生成的文件就可以一直保存在文件上 - -### jmap - -```bash -jmap -dump:format=b,file=文件名 pid - -windows下不用[],路径要加引号 - -jmap -dump:format=b,file="D:\a.dump" 8632 -``` -命令中文件名就是要保存的dump文件路径, pid就是当前jvm进程的id - - -### JVM启动参数 -在发生outofmemory的时候自动生成dump文件: - -```bash --XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\heapdump -``` -Pah后面是一个存在的可访问的路径,将改参数放入jvm启动参数可以在发生内存outofmemory的时候自动生成dump文件,但是正式环境使用的时候不要加这个参数,不然在内存快满的时候总是会生成dump而导致jvm卡半天,需要调试的时候才需要加这个参数 - -注意:通过WAS生成的PHD文件dump不能分析出出问题的模板,因为PHD文件不包含对象的值内容,无法根据PHD文件找到出问题的模板,所以PHD文件没有太大的参考价值 - -## 3.2 dump文件分析 -dump文件生成后,将dump压缩传输到本地,不管当前dump的后缀名是什么,直接改成*.hprof,就可以直接用jprofiler打开了 - -打开的过程时间可能会很长,主要是要对dump进行预处理,计算什么的,注意 这个过程不能点skip,否则就不太好定位大文件 -- 直接打开`.hprof`文件 -![](https://img-blog.csdnimg.cn/20200229003455158.png) -- 注意如下过程,中途可以喝一杯☕️,不要作死手滑点击了 skip! -![](https://img-blog.csdnimg.cn/20200227174740284.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70)![](https://img-blog.csdnimg.cn/20200229002551182.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20200229002604848.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20200229002723570.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20200229002943350.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/2020022900330228.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20200229032220529.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -这样界面的时候下面可以开始进行操作了! - -# 4 模块功能点详解 -也可以使用工具栏中的“转到开始”按钮访问第一个数据集 - -## 4.1 内存视图 Memory Views -JProfiler的内存视图部分可以提供动态的内存使用状况更新视图和显示关于内存分配状况信息的视图。所有的视图都有几个聚集层并且能够显示现有存在的对象和作为垃圾回收的对象。 -- 所有对象 All Objects -显示类或在状况统计和尺码信息堆上所有对象的包。你可以标记当前值并显示差异值。 -- 记录对象 Record Objects -显示类或所有已记录对象的包。你可以标记出当前值并且显示差异值。 -- 分配访问树 Allocation Call Tree -显示一棵请求树或者方法、类、包或对已选择类有带注释的分配信息的J2EE组件。 -- 分配热点 Allocation Hot Spots -显示一个列表,包括方法、类、包或分配已选类的J2EE组件。你可以标注当前值并且显示差异值。对于每个热点都可以显示它的跟踪记录树。 -- 类追踪器 Class Tracker -类跟踪视图可以包含任意数量的图表,显示选定的类和包的实例与时间。 - - - - -## 4.2 堆遍历 Heap Walker -### 使用背景 -在视图中找到增长快速的对象类型,在memory视图中找到Concurrenthashmap---点右键----选择“Show Selectiion In Heap Walker”,切换到HeapWarker 视图;切换前会弹出选项页面,注意一定要选择“Select recorded objects”,这样Heap Walker会在刚刚的那段记录中进行分析;否则,会分析tomcat的所有内存对象,这样既耗时又不准确; - - - -在JProfiler的堆遍历器(Heap Walker)中,你可以对堆的状况进行快照并且可以通过选择步骤下寻找感兴趣的对象。堆遍历器有五个视图: -- 类 Classes -显示所有类和它们的实例,可以右击具体的类"Used Selected Instance"实现进一步跟踪。 -- 分配 Allocations -为所有记录对象显示分配树和分配热点。 -- 索引 References -为单个对象和“显示到垃圾回收根目录的路径”提供索引图的显示功能。还能提供合并输入视图和输出视图的功能。 -- 时间 Time -显示一个对已记录对象的解决时间的柱状图。 -- 检查 Inspections -显示了一个数量的操作,将分析当前对象集在某种条件下的子集,实质是一个筛选的过程。 - -### 在HeapWalker中,找到泄漏的对象 -HeapWarker 会分析内存中的所有对象,包括对象的引用、创建、大小和数量. -通过切换到References页签,可以看到这个类的具体对象实例。 为了在这些内存对象中,找到泄漏的对象(应该被回收),可以在该对象上点击右键,选择“Use Selected Instances”缩小对象范围 -![](https://img-blog.csdnimg.cn/2020022904291115.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -### 通过引用分析该对象 -References 可以看到该对象的的引用关系,选项显示引用的类型 -![](https://img-blog.csdnimg.cn/20200229043146569.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- incoming -显示这个对象被谁引用 -- outcoming -显示这个对象引用的其他对象 - -选择“Show In Graph”将引用关系使用图形方式展现; - -- 选中该对象,点击`Show Paths To GC Root`,会找到引用的根节点 -![](https://img-blog.csdnimg.cn/20200229043837414.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -### 通过创建分析该对象 -如果还不能定位内存泄露的地方,我们可以尝试使用Allocations页签,该页签显示对象是如何创建出来的; -我们可以从创建方法开始检查,检查所有用到该对象的地方,直到找到泄漏位置; -## 图表 Graph -你需要在references视图和biggest视图手动添加对象到图表,它可以显示对象的传入和传出引用,能方便的找到垃圾收集器根源。 - -tips:在工具栏点击"Go To Start"可以使堆内存重新计数,也就是回到初始状态。 - -   - -## CPU 视图 CPU Views -  JProfiler 提供不同的方法来记录访问树以优化性能和细节。线程或者线程组以及线程状况可以被所有的视图选择。所有的视图都可以聚集到方法、类、包或J2EE组件等不同层上。CPU视图部分包括: - -   - -访问树 Call Tree -显示一个积累的自顶向下的树,树中包含所有在JVM中已记录的访问队列。JDBC,JMS和JNDI服务请求都被注释在请求树中。请求树可以根据Servlet和JSP对URL的不同需要进行拆分。 -热点 Hot Spots -显示消耗时间最多的方法的列表。对每个热点都能够显示回溯树。该热点可以按照方法请求,JDBC,JMS和JNDI服务请求以及按照URL请求来进行计算。 -访问图 Call Graph -显示一个从已选方法、类、包或J2EE组件开始的访问队列的图。 -方法统计 Method Statistis -显示一段时间内记录的方法的调用时间细节。 -## 线程视图 Thread Views -  JProfiler通过对线程历史的监控判断其运行状态,并监控是否有线程阻塞产生,还能将一个线程所管理的方法以树状形式呈现。对线程剖析,JProfiler提供以下视图: - -   - -线程历史 Thread History -显示一个与线程活动和线程状态在一起的活动时间表。 -线程监控 Thread Monitor -显示一个列表,包括所有的活动线程以及它们目前的活动状况。 -线程转储 Thread Dumps -显示所有线程的堆栈跟踪。 -## 监控器视图 Monitor Views -  JProfiler提供了不同的监控器视图,如下所示: - -   - -当前锁定图表 Current Locking Graph -显示JVM中的当前锁定情况。 -当前监视器 Current Monitors -显示当前正在等待或阻塞中的线程操作。 -锁定历史图表 Locking History Graph -显示记录在JVM中的锁定历史。 -监控器历史 Monitor History -显示等待或者阻塞的历史。 -监控器使用统计 Monitor Usage Statistics -计算统计监控器监控的数据。 -## VM遥感勘测技术视图 VM Telemetry Views -  观察JVM的内部状态,JProfiler提供了不同的遥感勘测视图,如下所示: - -   - -内存 Memory -显示堆栈的使用状况和堆栈尺寸大小活动时间表。 -记录的对象 Recorded Objects -显示一张关于活动对象与数组的图表的活动时间表。 -记录的生产量 Recorded Throughput -显示一段时间累计的JVM生产和释放的活动时间表。 -垃圾回收活动 GC Activity -显示一张关于垃圾回收活动的活动时间表。 -类 Classes -显示一个与已装载类的图表的活动时间表。 -线程 Threads -显示一个与动态线程图表的活动时间表。 -CPU负载 CPU Load -显示一段时间中CPU的负载图表。 - - -> 参考 -> - [使用JProfiler进行内存分析](https://www.cnblogs.com/onmyway20xx/p/3963735.html) \ No newline at end of file diff --git "a/JDK/JVM/\344\270\200\344\270\252\347\272\277\347\250\213 OOM \345\220\216\357\274\214\345\205\266\344\273\226\347\272\277\347\250\213\350\277\230\350\203\275\350\277\220\350\241\214\345\220\227\357\274\237.md" "b/JDK/JVM/\344\270\200\344\270\252\347\272\277\347\250\213 OOM \345\220\216\357\274\214\345\205\266\344\273\226\347\272\277\347\250\213\350\277\230\350\203\275\350\277\220\350\241\214\345\220\227\357\274\237.md" deleted file mode 100644 index ac577f29cb..0000000000 --- "a/JDK/JVM/\344\270\200\344\270\252\347\272\277\347\250\213 OOM \345\220\216\357\274\214\345\205\266\344\273\226\347\272\277\347\250\213\350\277\230\350\203\275\350\277\220\350\241\214\345\220\227\357\274\237.md" +++ /dev/null @@ -1,208 +0,0 @@ -![](https://img-blog.csdnimg.cn/20210628172622676.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -由于面试官仅提到OOM,但 Java 的OOM又分很多类型的呀: -- 堆溢出(“java.lang.OutOfMemoryError: Java heap space”) -- 永久代溢出(“java.lang.OutOfMemoryError:Permgen space”) -- 不能创建线程(“java.lang.OutOfMemoryError:Unable to create new native thread”) - - OOM在《Java虚拟机规范》里,除程序计数器,虚拟机内存的其他几个运行时区域都可能发生OOM,那本文的目的是啥呢? -- 通过代码验证《Java虚拟机规范》中描述的各个运行时区域储存的内容 -- 在工作中遇到实际的内存溢出异常时,能根据异常的提示信息迅速得知是哪个区域的内存溢出,知道怎样的代码可能会导致这些区域内存溢出,以及出现这些异常后该如何处理。 - -本文代码均由笔者在基于OpenJDK 8中的HotSpot虚拟机上进行过实际测试。 - -# 1 Java堆溢出 -Java堆用于储存对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免GC机制清除这些对象,则随对象数量增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。 - -限制Java堆的大小20MB,不可扩展 -```bash --XX:+HeapDumpOnOutOf-MemoryError -``` -可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照。 -## 案例1 -![](https://img-blog.csdnimg.cn/20210628172310407.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 报错 -![](https://img-blog.csdnimg.cn/20210628172350886.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -Java堆内存的OOM是实际应用中最常见的内存溢出异常场景。出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟随进一步提示“Java heap space”。 - -那既然发生了,**如何解决这个内存区域的异常呢**? -一般先通过内存映像分析工具(如jprofile)对Dump出来的堆转储快照进行分析。 -第一步首先确认内存中导致OOM的对象是否是必要的,即先分清楚到底是 -- 内存泄漏(Memory Leak) -- 还是内存溢出(Memory Overflow) - -- 下图是使用 jprofile打开的堆转储快照文件(java_pid44526.hprof) -![](https://img-blog.csdnimg.cn/20210628174931619.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -若是内存泄漏,可查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。 - -若不是内存泄漏,即就是内存中的对象确实都必须存活,则应: -1. 检查JVM堆参数(-Xmx与-Xms)的设置,与机器内存对比,看是否还有向上调整的空间 -2. 再检查代码是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运 行期的内存消耗 - -以上是处理Java堆内存问题的简略思路。 - -## 案例 2 -JVM启动参数设置: -```bash --Xms5m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError -``` -![](https://img-blog.csdnimg.cn/20210628191435587.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20210628191613580.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - - -- JVM堆空间的变化 -![](https://img-blog.csdnimg.cn/20210628191655896.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -堆的使用大小,突然抖动!说明当一个线程抛OOM后,它所占据的内存资源会全部被释放掉,而不会影响其他线程的正常运行! -所以一个线程溢出后,进程里的其他线程还能照常运行。 -发生OOM的线程一般情况下会死亡,也就是会被终结掉,该线程持有的对象占用的heap都会被gc了,释放内存。因为发生OOM之前要进行gc,就算其他线程能够正常工作,也会因为频繁gc产生较大的影响。 - -堆溢出和栈溢出,结论是一样的。 -# 2 虚拟机栈/本地方法栈溢出 -由于**HotSpot JVM并不区分虚拟机栈和本地方法栈**,因此HotSpot的`-Xoss`参数(设置本地方法栈的大小)虽然存在,但无任何效果,栈容量只能由`-Xss`参数设定。 - -关于虚拟机栈和本地方法栈,《Java虚拟机规范》描述如下异常: -1. 若线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常 -2. 若虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 OutOfMemoryError异常 - -《Java虚拟机规范》明确允许JVM实现自行选择是否支持栈的动态扩展,而HotSpot虚拟机的选择是**不支持扩展**,所以除非在创建线程申请内存时就因无法获得足够内存而出现OOM,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError。 - -## 如何验证呢? -做俩实验,先在单线程操作,尝试下面两种行为是否能让HotSpot OOM: -### 使用`-Xss`减少栈内存容量 -- 示例 -![](https://img-blog.csdnimg.cn/20210628195430820.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 结果 -![](https://img-blog.csdnimg.cn/20210628210011222.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -抛StackOverflowError异常,异常出现时输出的堆栈深度相应缩小。 - -不同版本的Java虚拟机和不同的操作系统,栈容量最小值可能会有所限制,这主要取决于操作系统内存分页大小。譬如上述方法中的参数-Xss160k可以正常用于62位macOS系统下的JDK 8,但若用于64位Windows系统下的JDK 11,则会提示栈容量最小不能低于180K,而在Linux下这个值则可能是228K,如果低于这个最小限制,HotSpot虚拟器启动时会给出如下提示: - -```bash -The stack size specified is too small, Specify at -``` - -### 定义大量局部变量,增大此方法帧中本地变量表的长度 -- 示例 -![](https://img-blog.csdnimg.cn/20210628210928936.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 结果 -![](https://img-blog.csdnimg.cn/20210628210958901.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70)所以无论是由于栈帧太或虚拟机栈容量太小,当新的栈帧内存无法分配时, HotSpot 都抛SOF。可若在允许动态扩展栈容量大小的虚拟机上,相同代码则会导致不同情况。 - -若测试时不限于单线程,而是不断新建线程,在HotSpot上也会产生OOM。但这样产生OOM和栈空间是否足够不存在直接的关系,主要取决于os本身内存使用状态。甚至说这种情况下,给每个线程的栈分配的内存越大,反而越容易产生OOM。 -不难理解,os分配给每个进程的内存有限制,比如32位Windows的单个进程最大内存限制为2G。HotSpot提供参数可以控制Java堆和方法区这两部分的内存的最大值,那剩余的内存即为2G(os限制)减去最大堆容量,再减去最大方法区容量,由于程序计数器消耗内存很小,可忽略,若把直接内存和虚拟机进程本身耗费的内存也去掉,剩下的内存就由虚拟机栈和本地方法栈来分配了。因此为每个线程分配到的栈内存越大,可以建立的线程数量越少,建立线程时就越容易把剩下的内存耗尽: -- 示例 -![](https://img-blog.csdnimg.cn/20210628215114730.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 结果 -```bash -Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread -``` -出现SOF时,会有明确错误堆栈可供分析,相对容易定位问题。如果使用HotSpot虚拟机默认参数,栈深度在大多数情况下(因为每个方法压入栈的帧大小并不是一样的)到达1000~2000没有问题,对于正常的方法调用(包括不能做尾递归优化的递归调用),这个深度应该完全够用。但如果是建立过多线程导致的内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量换取更多的线程。这种通过“减少内存”手段解决内存溢出的方式,如果没有这方面处理经验,一般比较难以想到。也是由于这种问题较为隐蔽,从 JDK 7起,以上提示信息中“unable to create native thread”后面,虚拟机会特别注明原因可能是“possibly - -```bash -#define OS_NATIVE_THREAD_CREATION_FAILED_MSG - "unable to create native thread: possibly out of memory or process/resource limits reached" -``` -# 3 方法区和运行时常量池溢出 -运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放到一起。 - -HotSpot从JDK 7开始逐步“去永久代”,在JDK 8中完全使用元空间代替永久代,那么方法区使用“永久代”还是“元空间”来实现,对程序有何影响呢。 - -String::intern()是一个本地方法:若字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池,并且返回此String对象的引用。 - -在JDK6或之前HotSpot虚拟机,常量池都是分配在永久代,可以通过如下两个参数: -![](https://img-blog.csdnimg.cn/20210628222603375.png) -限制永久代的大小,即可间接限制其中常量池的容量, -- 实例 -![](https://img-blog.csdnimg.cn/20210628223514617.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -- 结果 - -```bash -Exception in thread "main" java.lang.OutOfMemoryError: PermGen space - at java.lang.String.intern(Native Method) - at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18) -``` -可见,运行时常量池溢出时,在OutOfMemoryError异常后面跟随的提示信息是“PermGen space”,说明运行时常量池的确是属于方法区(即JDK 6的HotSpot虚拟机中的永久代)的 一部分。 - -而使用JDK 7或更高版本的JDK来运行这段程序并不会得到相同的结果,无论是在JDK 7中继续使 用-XX:MaxPermSize参数或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize参数把方法区容量同样限制在6MB,也都不会重现JDK 6中的溢出异常,循环将一直进行下去,永不停歇。 -这种变化是因为自JDK 7起,原本存放在永久代的字符串常量池被移至Java堆,所以在JDK 7及以上版 本,限制方法区的容量对该测试用例来说是毫无意义。 - -这时候使用-Xmx参数限制最大堆到6MB就能看到以下两种运行结果之一,具体取决于哪里的对象分配时产生了溢出: - -```bash -// OOM异常一: Exception in thread "main" java.lang.OutOfMemoryError: Java heap space -at java.base/java.lang.Integer.toString(Integer.java:440) -at java.base/java.lang.String.valueOf(String.java:3058) -at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:12) - -// OOM异常二: Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.base/java.util.HashMap.resize(HashMap.java:699) -at java.base/java.util.HashMap.putVal(HashMap.java:658) -at java.base/java.util.HashMap.put(HashMap.java:607) -at java.base/java.util.HashSet.add(HashSet.java:220) -at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java from InputFile-Object:14) -``` - -字符串常量池的实现位置还有很多趣事: -![](https://img-blog.csdnimg.cn/20210629230502171.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -JDK 6中运行,结果是两个false -JDK 7中运行,一个true和一个false -![](https://img-blog.csdnimg.cn/20210629231042658.png) -因为JDK6的intern()会把首次遇到的字符串实例复制到永久代的字符串常量池中,返回的也是永久代里这个字符串实例的引用,而由StringBuilder创建的字符串对象实例在 Java 堆,所以不可能是同一个引用,结果将返回false。 - -JDK 7及以后的intern()无需再拷贝字符串的实例到永久代,字符串常量池已移到Java堆,只需在常量池里记录一下首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。 - -str2比较返回false,这是因为“java”这个字符串在执行String-Builder.toString()之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern()方法要求“首次遇到”的原则,而“计算机软件”这个字符串则是首次 出现的,因此结果返回true! - -对于方法区的测试,基本的思路是运行时产生大量类去填满方法区,直到溢出。虽然直接使用Java SE API也可动态产生类(如反射时的 GeneratedConstructorAccessor和动态代理),但操作麻烦。 -借助了CGLib直接操作字节码运行时生成大量动态类。 当前的很多主流框架,如Spring、Hibernate对类进行增强时,都会使用到 CGLib字节码增强,当增强的类越多,就需要越大的方法区以保证动态生成的新类型可以载入内存。 -很多运行于JVM的动态语言(例如Groovy)通常都会持续创建新类型来支撑语言的动态性,随着这类动态语言的流行,与如下代码相似的溢出场景也越来越容易遇到 - -在JDK 7中的运行结果: -```bash -Caused by: java.lang.OutOfMemoryError: PermGen space - at java.lang.ClassLoader.defineClass1(Native Method) - at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632) - at java.lang.ClassLoader.defineClass(ClassLoader.java:616) -``` - -JDK8及以后:可以使用 - -```bash --XX:MetaspaceSize=10M --XX:MaxMetaspaceSize=10M -``` -设置元空间初始大小以及最大可分配大小。 -1.如果不指定元空间的大小,默认情况下,元空间最大的大小是系统内存的大小,元空间一直扩大,虚拟机可能会消耗完所有的可用系统内存。 -2.如果元空间内存不够用,就会报OOM。 -3.默认情况下,对应一个64位的服务端JVM来说,其默认的-XX:MetaspaceSize值为21MB,这就是初始的高水位线,一旦元空间的大小触及这个高水位线,就会触发Full GC并会卸载没有用的类,然后高水位线的值将会被重置。 -4.从第3点可以知道,如果初始化的高水位线设置过低,会频繁的触发Full GC,高水位线会被多次调整。所以为了避免频繁GC以及调整高水位线,建议将-XX:MetaspaceSize设置为较高的值,而-XX:MaxMetaspaceSize不进行设置。 - - -JDK8 运行结果: -![](https://img-blog.csdnimg.cn/20210630161717101.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -一个类如果要被gc,要达成的条件比较苛刻。在经常运行时生成大量动态类的场景,就应该特别关注这些类的回收状况。 -这类场景除了之前提到的程序使用了CGLib字节码增强和动态语言外,常见的还有: -- 大量JSP或动态产生JSP 文件的应用(JSP第一次运行时需要编译为Java类) -- 基于OSGi的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类) - -JDK8后,永久代完全废弃,而使用元空间作为其替代者。在默认设置下,前面列举的那些正常的动态创建新类型的测试用例已经很难再迫使虚拟机产生方法区OOM。 -为了让使用者有预防实际应用里出现类似于如上代码那样的破坏性操作,HotSpot还是提供了一些参数作为元空间的防御措施: -- -XX:MetaspaceSize -指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整。如果释放了大量的空间,就适当降低该值,如果释放了很少空间,则在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值 -- -XX:MaxMetaspaceSize -设置元空间最大值,默认-1,即不限制,或者说只受限于本地内存的大小 -- -XX:MinMetaspaceFreeRatio -在GC后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的GC频率 -- -XX:Max-MetaspaceFreeRatio -控制最大的元空间剩余容量的百分比 - -# 本机直接内存溢出 -直接内存(Direct Memory)的容量大小可通过`-XX:MaxDirectMemorySize`指定,若不指定,则默认与Java堆最大值(`-Xmx`)一致。 - -这里越过DirectByteBuffer类,直接通过反射获取Unsafe实例进行内存分配。 -Unsafe类的getUnsafe()指定只有引导类加载器才会返回实例,体现了设计者希望只有虚拟机标准类库里面的类才能使用Unsafe,JDK10时才将Unsafe的部分功能通过VarHandle开放给外部。 -因为虽然使用DirectByteBuffer分配内存也会抛OOM,但它抛异常时并未真正向os申请分配内存,而是通过计算得知内存无法分配,就在代码里手动抛了OOM,真正申请分配内存的方法是**Unsafe::allocateMemory()** -- 使用unsafe分配本机内存 -![](https://img-blog.csdnimg.cn/20210630163836454.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 结果 -![](https://img-blog.csdnimg.cn/2021063016375144.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显异常,若发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了 DirectMemory(比如使用NIO),则该考虑直接内存了。 \ No newline at end of file diff --git "a/JDK/JVM/\345\244\215\344\271\240\357\274\232GC\345\210\206\347\261\273.md" "b/JDK/JVM/\345\244\215\344\271\240\357\274\232GC\345\210\206\347\261\273.md" deleted file mode 100644 index fe5b5ef69a..0000000000 --- "a/JDK/JVM/\345\244\215\344\271\240\357\274\232GC\345\210\206\347\261\273.md" +++ /dev/null @@ -1,340 +0,0 @@ - -## 复习:GC分类 - -针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC) - -- 部分收集(Partial GC):不是完整收集整个 Java 堆的垃圾收集。其中又分为: - - - 新生代收集(Minor GC / Young GC):只是新生代(Eden / S0, S1)的垃圾收集 - - 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。目前,只有 CMS GC 会有单独收集老年代的行为。注意,很多时候 Major GC 会和 Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收。 -- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前,只有 G1 GC 会有这种行为 -- 整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集。 - -1. 新生代收集:只有当Eden区满的时候就会进行新生代收集,所以新生代收集和S0区域和S1区域情况无关 - -2. 老年代收集和新生代收集的关系:进行老年代收集之前会先进行一次年轻代的垃圾收集,原因如下:一个比较大的对象无法放入新生代,那它自然会往老年代去放,如果老年代也放不下,那会先进行一次新生代的垃圾收集,之后尝试往新生代放,如果还是放不下,才会进行老年代的垃圾收集,之后在往老年代去放,这是一个过程,我来说明一下为什么需要往老年代放,但是放不下,而进行新生代垃圾收集的原因,这是因为新生代垃圾收集比老年代垃圾收集更加简单,这样做可以节省性能 - -3. 进行垃圾收集的时候,堆包含新生代、老年代、元空间/永久代:可以看出Heap后面包含着新生代、老年代、元空间,但是我们设置堆空间大小的时候设置的只是新生代、老年代而已,元空间是分开设置的 - -4. 哪些情况会触发Full GC: -- 老年代空间不足 -- 方法区空间不足 -- 显示调用System.gc() -- Minior GC进入老年代的数据的平均大小 大于 老年代的可用内存 -- 大对象直接进入老年代,而老年代的可用空间不足 - - - -## 不同GC分类的GC细节 - -用例代码: - -```Java -/** - * -XX:+PrintCommandLineFlags - * - * -XX:+UseSerialGC:表明新生代使用Serial GC ,同时老年代使用Serial Old GC - * - * -XX:+UseParNewGC:标明新生代使用ParNew GC - * - * -XX:+UseParallelGC:表明新生代使用Parallel GC - * -XX:+UseParallelOldGC : 表明老年代使用 Parallel Old GC - * 说明:二者可以相互激活 - * - * -XX:+UseConcMarkSweepGC:表明老年代使用CMS GC。同时,年轻代会触发对ParNew 的使用 - * @author shkstart - * @create 17:19 - */ -public class GCUseTest { - public static void main(String[] args) { - ArrayList list = new ArrayList<>(); - - while(true){ - byte[] arr = new byte[1024 * 10];//10kb - list.add(arr); -// try { -// Thread.sleep(5); -// } catch (InterruptedException e) { -// e.printStackTrace(); -// } - } - } -} -``` - -### 老年代使用CMS GC - -**GC设置方法**:参数中使用-XX:+UseConcMarkSweepGC,说明老年代使用CMS GC,同时年轻代也会触发对ParNew的使用,因此添加该参数之后,新生代使用ParNew GC,而老年代使用CMS GC,整体是并发垃圾收集,主打低延迟 - -![image-20220419202643](http://pan.icebule.top/%E6%9C%89%E9%81%93%E4%BA%91%E7%AC%94%E8%AE%B0%E5%9B%BE%E5%BA%8A/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/JAVA/JVM/20220419202551.png) - -打印出来的GC细节: - -![image-20220419211943](http://pan.icebule.top/%E6%9C%89%E9%81%93%E4%BA%91%E7%AC%94%E8%AE%B0%E5%9B%BE%E5%BA%8A/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/JAVA/JVM/20220419211943.png) - - - -### 新生代使用Serial GC - - **GC设置方法**:参数中使用-XX:+UseSerialGC,说明新生代使用Serial GC,同时老年代也会触发对Serial Old GC的使用,因此添加该参数之后,新生代使用Serial GC,而老年代使用Serial Old GC,整体是串行垃圾收集 - -![image-20220419212907](http://pan.icebule.top/%E6%9C%89%E9%81%93%E4%BA%91%E7%AC%94%E8%AE%B0%E5%9B%BE%E5%BA%8A/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/JAVA/JVM/20220419212907.png) - - 打印出来的GC细节: - -![image-20220419212940](http://pan.icebule.top/%E6%9C%89%E9%81%93%E4%BA%91%E7%AC%94%E8%AE%B0%E5%9B%BE%E5%BA%8A/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/JAVA/JVM/20220419212940.png) - -DefNew代表新生代使用Serial GC,然后Tenured代表老年代使用Serial Old GC - -## GC 日志分类 - -### MinorGC - -MinorGC(或 young GC 或 YGC)日志: - -```java -[GC (Allocation Failure) [PSYoungGen: 31744K->2192K (36864K) ] 31744K->2200K (121856K), 0.0139308 secs] [Times: user=0.05 sys=0.01, real=0.01 secs] -``` - -![image-20220419202643](http://pan.icebule.top/%E6%9C%89%E9%81%93%E4%BA%91%E7%AC%94%E8%AE%B0%E5%9B%BE%E5%BA%8A/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/JAVA/JVM/20220419202643.png) - -![image-20220419202718](http://pan.icebule.top/%E6%9C%89%E9%81%93%E4%BA%91%E7%AC%94%E8%AE%B0%E5%9B%BE%E5%BA%8A/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/JAVA/JVM/20220419202718.png) - -### FullGC - -```java -[Full GC (Metadata GC Threshold) [PSYoungGen: 5104K->0K (132096K) ] [Par01dGen: 416K->5453K (50176K) ]5520K->5453K (182272K), [Metaspace: 20637K->20637K (1067008K) ], 0.0245883 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] -``` - -![image-20220419202740](http://pan.icebule.top/%E6%9C%89%E9%81%93%E4%BA%91%E7%AC%94%E8%AE%B0%E5%9B%BE%E5%BA%8A/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/JAVA/JVM/20220419202740.png) - -![image-20220419202804](http://pan.icebule.top/%E6%9C%89%E9%81%93%E4%BA%91%E7%AC%94%E8%AE%B0%E5%9B%BE%E5%BA%8A/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/JAVA/JVM/20220419202804.png) - -## GC 日志结构剖析 - -### 透过日志看垃圾收集器 - -- Serial 收集器:新生代显示 "[DefNew",即 Default New Generation - -- ParNew 收集器:新生代显示 "[ParNew",即 Parallel New Generation - -- Parallel Scavenge 收集器:新生代显示"[PSYoungGen",JDK1.7 使用的即 PSYoungGen - -- Parallel Old 收集器:老年代显示"[ParoldGen" - -- G1 收集器:显示”garbage-first heap“ - -### 透过日志看 GC 原因 - -- Allocation Failure:表明本次引起 GC 的原因是因为新生代中没有足够的区域存放需要分配的数据 -- Metadata GCThreshold:Metaspace 区不够用了 -- FErgonomics:JVM 自适应调整导致的 GC -- System:调用了 System.gc()方法 - -### 透过日志看 GC 前后情况 - -通过图示,我们可以发现 GC 日志格式的规律一般都是:GC 前内存占用-> GC 后内存占用(该区域内存总大小) - -```java -[PSYoungGen: 5986K->696K (8704K) ] 5986K->704K (9216K) -``` - -- 中括号内:GC 回收前年轻代堆大小,回收后大小,(年轻代堆总大小) - -- 括号外:GC 回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总大小) - -注意:Minor GC 堆内存总容量 = 9/10 年轻代 + 老年代。原因是 Survivor 区只计算 from 部分,而 JVM 默认年轻代中 Eden 区和 Survivor 区的比例关系,Eden:S0:S1=8:1:1。 - -### 透过日志看 GC 时间 - -GC 日志中有三个时间:user,sys 和 real - -- user:进程执行用户态代码(核心之外)所使用的时间。这是执行此进程所使用的实际 CPU 时间,其他进程和此进程阻塞的时间并不包括在内。在垃圾收集的情况下,表示 GC 线程执行所使用的 CPU 总时间。 -- sys:进程在内核态消耗的 CPU 时间,即在内核执行系统调用或等待系统事件所使用的 CPU 时间 -- real:程序从开始到结束所用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等待 I/O 完成)。对于并行 gc,这个数字应该接近(用户时间+系统时间)除以垃圾收集器使用的线程数。 - -由于多核的原因,一般的 GC 事件中,real time 是小于 sys time + user time 的,因为一般是多个线程并发的去做 GC,所以 real time 是要小于 sys + user time 的。如果 real > sys + user 的话,则你的应用可能存在下列问题:IO 负载非常重或 CPU 不够用。 - -## Minor GC 日志解析 - -### 日志格式 - -```Java -2021-09-06T08:44:49.453+0800: 4.396: [GC (Allocation Failure) [PSYoungGen: 76800K->8433K(89600K)] 76800K->8449K(294400K), 0.0060231 secs] [Times: user=0.02 sys=0.01, real=0.01 secs] -``` - -### 日志解析 - -#### 2021-09-06T08:44:49.453+0800 - -日志打印时间 日期格式 如 2013-05-04T21:53:59.234+0800 - -添加-XX:+PrintGCDateStamps参数 - -#### 4.396 - -gc 发生时,Java 虚拟机启动以来经过的秒数 - -添加-XX:+PrintGCTimeStamps该参数 - -#### [GC (Allocation Failure) - -发生了一次垃圾回收,这是一次 Minor GC。它不区分新生代 GC 还是老年代 GC,括号里的内容是 gc 发生的原因,这里 Allocation Failure 的原因是新生代中没有足够区域能够存放需要分配的数据而失败。 - -#### [PSYoungGen: 76800K->8433K(89600K)] - -**PSYoungGen**:表示GC发生的区域,区域名称与使用的GC收集器是密切相关的 - -- **Serial收集器**:Default New Generation 显示Defnew -- **ParNew收集器**:ParNew -- **Parallel Scanvenge收集器**:PSYoung -- 老年代和新生代同理,也是和收集器名称相关 - -**76800K->8433K(89600K)**:GC前该内存区域已使用容量->GC后盖区域容量(该区域总容量) - -- 如果是新生代,总容量则会显示整个新生代内存的9/10,即eden+from/to区 -- 如果是老年代,总容量则是全身内存大小,无变化 - -#### 76800K->8449K(294400K) - -虽然本次是Minor GC,只会进行新生代的垃圾收集,但是也肯定会打印堆中总容量相关信息 - -在显示完区域容量GC的情况之后,会接着显示整个堆内存区域的GC情况:GC前堆内存已使用容量->GC后堆内存容量(堆内存总容量),并且堆内存总容量 = 9/10 新生代 + 老年代,然后堆内存总容量肯定小于初始化的内存大小 - -#### ,0.0088371 - -整个GC所花费的时间,单位是秒 - -#### [Times:user=0.02 sys=0.01,real=0.01 secs] - -- **user**:指CPU工作在用户态所花费的时间 -- **sys**:指CPU工作在内核态所花费的时间 -- **real**:指在此次事件中所花费的总时间 - -## Full GC 日志解析 - -### 日志格式 - -```Java -2021-09-06T08:44:49.453+0800: 4.396: [Full GC (Metadata GC Threshold) [PSYoungGen: 10082K->0K(89600K)] [ParOldGen: 32K->9638K(204800K)] 10114K->9638K(294400K), [Metaspace: 20158K->20156K(1067008K)], 0.0149928 secs] [Times: user=0.06 sys=0.02, real=0.02 secs] -``` - -### 日志解析 - -#### 2020-11-20T17:19:43.794-0800 - -日志打印时间 日期格式 如 2013-05-04T21:53:59.234+0800 - -添加-XX:+PrintGCDateStamps参数 - -#### 1.351 - -gc 发生时,Java 虚拟机启动以来经过的秒数 - -添加-XX:+PrintGCTimeStamps该参数 - -#### Full GC(Metadata GCThreshold) - -括号中是gc发生的原因,原因:Metaspace区不够用了。 -除此之外,还有另外两种情况会引起Full GC,如下: - -1. Full GC(FErgonomics) - 原因:JVM自适应调整导致的GC -2. Full GC(System) - 原因:调用了System.gc()方法 - -#### [PSYoungGen: 100082K->0K(89600K)] - -**PSYoungGen**:表示GC发生的区域,区域名称与使用的GC收集器是密切相关的 - -- **Serial收集器**:Default New Generation 显示DefNew -- **ParNew收集器**:ParNew -- **Parallel Scanvenge收集器**:PSYoungGen -- 老年代和新生代同理,也是和收集器名称相关 - -**10082K->0K(89600K)**:GC前该内存区域已使用容量->GC该区域容量(该区域总容量) - -- 如果是新生代,总容量会显示整个新生代内存的9/10,即eden+from/to区 - -- 如果是老年代,总容量则是全部内存大小,无变化 - -#### ParOldGen:32K->9638K(204800K) - -老年代区域没有发生GC,因此本次GC是metaspace引起的 - -#### 10114K->9638K(294400K), - -在显示完区域容量GC的情况之后,会接着显示整个堆内存区域的GC情况:GC前堆内存已使用容量->GC后堆内存容量(堆内存总容量),并且堆内存总容量 = 9/10 新生代 + 老年代,然后堆内存总容量肯定小于初始化的内存大小 - -#### [Meatspace:20158K->20156K(1067008K)], - -metaspace GC 回收2K空间 - - - -## 论证FullGC是否会回收元空间/永久代垃圾 - -```Java -/** - * jdk6/7中: - * -XX:PermSize=10m -XX:MaxPermSize=10m - *

- * jdk8中: - * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m - * - * @author IceBlue - * @create 2020 22:24 - */ -public class OOMTest extends ClassLoader { - public static void main(String[] args) { - int j = 0; - try { - for (int i = 0; i < 100000; i++) { - OOMTest test = new OOMTest(); - //创建ClassWriter对象,用于生成类的二进制字节码 - ClassWriter classWriter = new ClassWriter(0); - //指明版本号,修饰符,类名,包名,父类,接口 - classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); - //返回byte[] - byte[] code = classWriter.toByteArray(); - //类的加载 - test.defineClass("Class" + i, code, 0, code.length);//Class对象 - test = null; - j++; - } - } finally { - System.out.println(j); - } - } -} -``` - -输出结果: - -``` -[GC (Metadata GC Threshold) [PSYoungGen: 10485K->1544K(152576K)] 10485K->1552K(500736K), 0.0011517 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] -[Full GC (Metadata GC Threshold) [PSYoungGen: 1544K->0K(152576K)] [ParOldGen: 8K->658K(236544K)] 1552K->658K(389120K), [Metaspace: 3923K->3320K(1056768K)], 0.0051012 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] -[GC (Metadata GC Threshold) [PSYoungGen: 5243K->832K(152576K)] 5902K->1490K(389120K), 0.0009536 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] - --------省略N行------- - -[Full GC (Last ditch collection) [PSYoungGen: 0K->0K(2427904K)] [ParOldGen: 824K->824K(5568000K)] 824K->824K(7995904K), [Metaspace: 3655K->3655K(1056768K)], 0.0041177 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] -Heap - PSYoungGen total 2427904K, used 0K [0x0000000755f80000, 0x00000007ef080000, 0x00000007ffe00000) - eden space 2426880K, 0% used [0x0000000755f80000,0x0000000755f80000,0x00000007ea180000) - from space 1024K, 0% used [0x00000007ea180000,0x00000007ea180000,0x00000007ea280000) - to space 1536K, 0% used [0x00000007eef00000,0x00000007eef00000,0x00000007ef080000) - ParOldGen total 5568000K, used 824K [0x0000000602200000, 0x0000000755f80000, 0x0000000755f80000) - object space 5568000K, 0% used [0x0000000602200000,0x00000006022ce328,0x0000000755f80000) - Metaspace used 3655K, capacity 4508K, committed 9728K, reserved 1056768K - class space used 394K, capacity 396K, committed 2048K, reserved 1048576K - -进程已结束,退出代码0 - -``` - -通过不断地动态生成类对象,输出GC日志 - -根据GC日志我们可以看出当元空间容量耗尽时,会触发FullGC,而每次FullGC之前,至会进行一次MinorGC,而MinorGC只会回收新生代空间; - -只有在FullGC时,才会对新生代,老年代,永久代/元空间全部进行垃圾收集 \ No newline at end of file diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Iterator\350\277\255\344\273\243\345\231\250\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210?.md" "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Iterator\350\277\255\344\273\243\345\231\250\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210?.md" deleted file mode 100644 index 09ae906da6..0000000000 --- "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Iterator\350\277\255\344\273\243\345\231\250\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210?.md" +++ /dev/null @@ -1,109 +0,0 @@ -我们常使用 JDK 提供的迭代接口进行 Java 集合的迭代。 -```java -Iterator iterator = list.iterator(); -while (iterator.hasNext()) { - String string = iterator.next(); - //do something -} -``` -迭代可以简单地理解为遍历,是一个标准化遍历各类容器里面的所有对象的方法类。Iterator 模式是用于遍历集合类的标准访问方法。它可以把访问逻辑从不同类型集合类中抽象出来,从而避免向客户端暴露集合内部结构。 - -在没有迭代器时,我们都这么处理: -数组处理: -```java -int[] arrays = new int[10]; -for(int i = 0 ; i < arrays.length ; i++){ - int a = arrays[i]; - // do sth -} -``` -ArrayList处理: -```java -List list = new ArrayList(); -for(int i = 0 ; i < list.size() ; i++){ - String string = list.get(i); - // do sth -} -``` -这些方式,都需要事先知道集合内部结构,访问代码和集合结构本身紧耦合,无法将访问逻辑从集合类和客户端代码中分离。同时每一种集合对应一种遍历方法,客户端代码无法复用。 - -实际应用中,若需要将上面将两个集合进行整合,则很麻烦。所以为解决如上问题, Iterator 模式诞生了。 -它总是用同一种逻辑遍历集合,从而客户端无需再维护集合内部结构,所有内部状态都由 Iterator 维护。客户端不直接和集合类交互,它只控制 Iterator,向它发送”向前”,”向后”,”取当前元素”的命令,即可实现对客户端透明地遍历整个集合。 - -# java.util.Iterator -在 Java 中 Iterator 为一个接口,它只提供迭代的基本规则,在 JDK 中他是这样定义的:对 collection 进行迭代的迭代器。 -![](https://img-blog.csdnimg.cn/20210627215010751.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -迭代器取代了Java集合框架中的 Enumeration。迭代器与枚举有两点不同: -1. 迭代器允许调用者利用定义良好的语义在迭代期间,从迭代器所指向的 collection 移除元素 -2. 优化方法名 - -其接口定义如下: -![](https://img-blog.csdnimg.cn/20210627215245597.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -```java -Object next():返回迭代器刚越过的元素的引用,返回值是 Object,需要强制转换成自己需要的类型 - -boolean hasNext():判断容器内是否还有可供访问的元素 - -void remove():删除迭代器刚越过的元素 -``` -一般只需使用 next()、hasNext() 即可完成迭代: -```java -for(Iterator it = c.iterator(); it.hasNext(); ) { -   Object o = it.next(); -    // do sth -} -``` -所以Iterator一大优点是无需知道集合内部结构。集合的内部结构、状态都由 Iterator 维护,通过统一的方法 hasNext()、next() 来判断、获取下一个元素,至于具体的内部实现我们就不用关心了。 -# 各个集合的 Iterator 实现 -ArrayList 的内部实现采用数组,所以我们只需要记录相应位置的索引即可。 - -## ArrayList 的 Iterator 实现 -在 ArrayList 内部首先是定义一个内部类 Itr,该内部类实现 Iterator 接口,如下: -![](https://img-blog.csdnimg.cn/20210703230958705.png) - -- ArrayList#iterator() :返回的是 Itr() 内部类 -![](https://img-blog.csdnimg.cn/20210703231158870.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -### 成员变量 -在 Itr 内部定义了三个 int 型的变量: -- cursor -下一个元素的索引位置 -- lastRet -上一个元素的索引位置 -```java -int cursor; -int lastRet = -1; -int expectedModCount = modCount; -``` -所以lastRet 一直比 cursor 小 1。所以 hasNext() 实现很简单: -![](https://img-blog.csdnimg.cn/20210703231439532.png) -### next() -实现其实也是比较简单,只要返回 cursor 索引位置处的元素即可,然后更新cursor、lastRet : -```java -public E next() { - checkForComodification(); - // 记录索引位置 - int i = cursor; - // 如果获取元素大于集合元素个数,则抛出异常 - if (i >= size) - throw new NoSuchElementException(); - Object[] elementData = ArrayList.this.elementData; - if (i >= elementData.length) - throw new ConcurrentModificationException(); - // cursor + 1 - cursor = i + 1; - // lastRet + 1 且返回cursor处元素 - return (E) elementData[lastRet = i]; -} -``` -checkForComodification() 主要判断集合的修改次数是否合法,即判断遍历过程中集合是否被修改过。 -modCount 用于记录 ArrayList 集合的修改次数,初始化为 0。每当集合被修改一次(结构上面的修改,内部update不算),如 add、remove 等方法,modCount + 1。 -所以若 modCount 不变,则表示集合内容未被修改。该机制主要用于实现 ArrayList 集合的快速失败机制。所以要保证在遍历过程中不出错误,我们就应该保证在遍历过程中不会对集合产生结构上的修改(当然 remove 方法除外),出现了异常错误,我们就应该认真检查程序是否出错而不是 catch 后不做处理。 -![](https://img-blog.csdnimg.cn/20210703233049909.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -### remove() -调用 ArrayList 本身的 remove() 方法删除 lastRet 位置元素,然后修改 modCount 即可。 -![](https://img-blog.csdnimg.cn/20210703233234428.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- SubList.this#remove(lastRet) -![](https://img-blog.csdnimg.cn/20210703233837589.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- ArrayList#remove -![](https://img-blog.csdnimg.cn/20210703233933396.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) \ No newline at end of file diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java8\346\226\260\347\211\271\346\200\247\344\271\213Lambda\350\241\250\350\276\276\345\274\217&Stream\346\265\201&\346\226\271\346\263\225\345\274\225\347\224\250.md" "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java8\346\226\260\347\211\271\346\200\247\344\271\213Lambda\350\241\250\350\276\276\345\274\217&Stream\346\265\201&\346\226\271\346\263\225\345\274\225\347\224\250.md" deleted file mode 100644 index 1801de8086..0000000000 --- "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java8\346\226\260\347\211\271\346\200\247\344\271\213Lambda\350\241\250\350\276\276\345\274\217&Stream\346\265\201&\346\226\271\346\263\225\345\274\225\347\224\250.md" +++ /dev/null @@ -1,1842 +0,0 @@ -> 集合优化了对象的存储,而流和对象的处理有关。 - -流是一系列与特定存储机制无关的元素——实际上,流并没有“存储”之说。 - -利用流,无需迭代集合中的元素,就可以提取和操作它们。这些管道通常被组合在一起,在流上形成一条操作管道。 - -大多数情况下,将对象存储在集合是为了处理他们,因此你将会发现编程焦点从集合转移到了流。流的一个核心好处是,它使得**程序更加短小并且更易理解**。当 Lambda 表达式和方法引用和流一起使用的时候会让人感觉自成一体。流使得 Java 8 更添魅力。 - -假如你要随机展示 5 至 20 之间不重复的整数并进行排序。实际上,你的关注点首先是创建一个有序集合。围绕这个集合进行后续操作。但使用流式编程,就可以简单陈述你想做什么: -```java -// streams/Randoms.java -import java.util.*; -public class Randoms { - public static void main(String[] args) { - new Random(47) - .ints(5, 20) - .distinct() - .limit(7) - .sorted() - .forEach(System.out::println); - } -} -``` - -输出结果: - -``` -6 -10 -13 -16 -17 -18 -19 -``` - -首先,我们给 **Random** 对象一个种子(以便程序再次运行时产生相同的输出)。`ints()` 方法产生一个流并且 `ints()` 方法有多种方式的重载 — 两个参数限定了数值产生的边界。这将生成一个整数流。我们可以使用中间流操作(intermediate stream operation) `distinct()` 来获取它们的非重复值,然后使用 `limit()` 方法获取前 7 个元素。接下来,我们使用 `sorted()` 方法排序。最终使用 `forEach()` 方法遍历输出,它根据传递给它的函数对每个流对象执行操作。在这里,我们传递了一个可以在控制台显示每个元素的方法引用。`System.out::println` 。 - -注意 `Randoms.java` 中没有声明任何变量。流可以在不使用赋值或可变数据的情况下对有状态的系统建模,这非常有用。 - -声明式编程(Declarative programming)是一种:声明要做什么,而非怎么做的编程风格。正如我们在函数式编程中所看到的。**注意**,命令式编程的形式更难以理解。代码示例: - -```java -// streams/ImperativeRandoms.java -import java.util.*; -public class ImperativeRandoms { - public static void main(String[] args) { - Random rand = new Random(47); - SortedSet rints = new TreeSet<>(); - while(rints.size() < 7) { - int r = rand.nextInt(20); - if(r < 5) continue; - rints.add(r); - } - System.out.println(rints); - } -} -``` - -输出结果: - -``` -[7, 8, 9, 11, 13, 15, 18] -``` - -在 `Randoms.java` 中,我们无需定义任何变量,但在这里我们定义了 3 个变量: `rand`,`rints` 和 `r`。由于 `nextInt()` 方法没有下限的原因(其内置的下限永远为 0),这段代码实现起来更复杂。所以我们要生成额外的值来过滤小于 5 的结果。 - -**注意**,你必须要研究程序的真正意图,而在 `Randoms.java` 中,代码只是告诉了你它正在做什么。这种语义清晰性也是 Java 8 的流式编程更受推崇的重要原因。 - -在 `ImperativeRandoms.java` 中显式地编写迭代机制称为外部迭代。而在 `Randoms.java` 中,流式编程采用内部迭代,这是流式编程的核心特性之一。这种机制使得编写的代码可读性更强,也更能利用多核处理器的优势。通过放弃对迭代过程的控制,我们把控制权交给并行化机制。我们将在[并发编程](24-Concurrent-Programming.md)一章中学习这部分内容。 - -另一个重要方面,流是懒加载的。这代表着它只在绝对必要时才计算。你可以将流看作“延迟列表”。由于计算延迟,流使我们能够表示非常大(甚至无限)的序列,而不需要考虑内存问题。 - - - -## 流支持 - -Java 设计者面临着这样一个难题:现存的大量类库不仅为 Java 所用,同时也被应用在整个 Java 生态圈数百万行的代码中。如何将一个全新的流的概念融入到现有类库中呢? - -比如在 **Random** 中添加更多的方法。只要不改变原有的方法,现有代码就不会受到干扰。 - -问题是,接口部分怎么改造呢?特别是涉及集合类接口的部分。如果你想把一个集合转换为流,直接向接口添加新方法会破坏所有老的接口实现类。 - -Java 8 采用的解决方案是:在[接口](10-Interfaces.md)中添加被 `default`(`默认`)修饰的方法。通过这种方案,设计者们可以将流式(*stream*)方法平滑地嵌入到现有类中。流方法预置的操作几乎已满足了我们平常所有的需求。流操作的类型有三种:创建流,修改流元素(中间操作, Intermediate Operations),消费流元素(终端操作, Terminal Operations)。最后一种类型通常意味着收集流元素(通常是到集合中)。 - -下面我们来看下每种类型的流操作。 - - -## 流创建 - -你可以通过 `Stream.of()` 很容易地将一组元素转化成为流(`Bubble` 类在本章的后面定义): - -```java -// streams/StreamOf.java -import java.util.stream.*; -public class StreamOf { - public static void main(String[] args) { - Stream.of(new Bubble(1), new Bubble(2), new Bubble(3)) - .forEach(System.out::println); - Stream.of("It's ", "a ", "wonderful ", "day ", "for ", "pie!") - .forEach(System.out::print); - System.out.println(); - Stream.of(3.14159, 2.718, 1.618) - .forEach(System.out::println); - } -} -``` - -输出结果: - -``` -Bubble(1) -Bubble(2) -Bubble(3) -It's a wonderful day for pie! -3.14159 -2.718 -1.618 -``` - -每个集合都可通过 `stream()` 产生一个流。示例: - -```java -import java.util.*; -import java.util.stream.*; -public class CollectionToStream { - public static void main(String[] args) { - List bubbles = Arrays.asList(new Bubble(1), new Bubble(2), new Bubble(3)); - System.out.println(bubbles.stream() - .mapToInt(b -> b.i) - .sum()); - - Set w = new HashSet<>(Arrays.asList("It's a wonderful day for pie!".split(" "))); - w.stream() - .map(x -> x + " ") - .forEach(System.out::print); - System.out.println(); - - Map m = new HashMap<>(); - m.put("pi", 3.14159); - m.put("e", 2.718); - m.put("phi", 1.618); - m.entrySet().stream() - .map(e -> e.getKey() + ": " + e.getValue()) - .forEach(System.out::println); - } -} -``` - -输出结果: - -``` -6 -a pie! It's for wonderful day -phi: 1.618 -e: 2.718 -pi: 3.14159 -``` - -- 创建 `List` 对象后,只需简单调用**所有集合中都有**的 `stream()`。 -- 中间操作 `map()` 会获取流中的所有元素,并且对流中元素应用操作从而产生新的元素,并将其传递到后续的流中。通常 `map()` 会获取对象并产生新的对象,但在这里产生了特殊的用于数值类型的流。例如,`mapToInt()` 方法将一个对象流(object stream)转换成为包含整型数字的 `IntStream`。 -- 通过调用字符串的 `split()`来获取元素用于定义变量 `w`。 -- 为了从 **Map** 集合中产生流数据,我们首先调用 `entrySet()` 产生一个对象流,每个对象都包含一个 `key` 键以及与其相关联的 `value` 值。然后分别调用 `getKey()` 和 `getValue()` 获取值。 - -### 随机数流 - -`Random` 类被一组生成流的方法增强了。代码示例: - -```java -// streams/RandomGenerators.java -import java.util.*; -import java.util.stream.*; -public class RandomGenerators { - public static void show(Stream stream) { - stream - .limit(4) - .forEach(System.out::println); - System.out.println("++++++++"); - } - - public static void main(String[] args) { - Random rand = new Random(47); - show(rand.ints().boxed()); - show(rand.longs().boxed()); - show(rand.doubles().boxed()); - // 控制上限和下限: - show(rand.ints(10, 20).boxed()); - show(rand.longs(50, 100).boxed()); - show(rand.doubles(20, 30).boxed()); - // 控制流大小: - show(rand.ints(2).boxed()); - show(rand.longs(2).boxed()); - show(rand.doubles(2).boxed()); - // 控制流的大小和界限 - show(rand.ints(3, 3, 9).boxed()); - show(rand.longs(3, 12, 22).boxed()); - show(rand.doubles(3, 11.5, 12.3).boxed()); - } -} -``` - -输出结果: - -``` --1172028779 -1717241110 --2014573909 -229403722 -++++++++ -2955289354441303771 -3476817843704654257 --8917117694134521474 -4941259272818818752 -++++++++ -0.2613610344283964 -0.0508673570556899 -0.8037155449603999 -0.7620665811558285 -++++++++ -16 -10 -11 -12 -++++++++ -65 -99 -54 -58 -++++++++ -29.86777681078574 -24.83968447804611 -20.09247112332014 -24.046793846338723 -++++++++ -1169976606 -1947946283 -++++++++ -2970202997824602425 --2325326920272830366 -++++++++ -0.7024254510631527 -0.6648552384607359 -++++++++ -6 -7 -7 -++++++++ -17 -12 -20 -++++++++ -12.27872414236691 -11.732085449736195 -12.196509449817267 -++++++++ -``` - -为了消除冗余代码,我创建了一个泛型方法 `show(Stream stream)` (在讲解泛型之前就使用这个特性,确实有点作弊,但是回报是值得的)。类型参数 `T` 可以是任何类型,所以这个方法对 **Integer**、**Long** 和 **Double** 类型都生效。但是 **Random** 类只能生成基本类型 **int**, **long**, **double** 的流。幸运的是, `boxed()` 流操作将会自动地把基本类型包装成为对应的装箱类型,从而使得 `show()` 能够接受流。 - -我们可以使用 **Random** 为任意对象集合创建 **Supplier**。如下是一个文本文件提供字符串对象的例子。 - -Cheese.dat 文件内容: - -``` -// streams/Cheese.dat -Not much of a cheese shop really, is it? -Finest in the district, sir. -And what leads you to that conclusion? -Well, it's so clean. -It's certainly uncontaminated by cheese. -``` - -我们通过 **File** 类将 Cheese.dat 文件的所有行读取到 `List` 中。代码示例: - -```java -// streams/RandomWords.java -import java.util.*; -import java.util.stream.*; -import java.util.function.*; -import java.io.*; -import java.nio.file.*; -public class RandomWords implements Supplier { - List words = new ArrayList<>(); - Random rand = new Random(47); - RandomWords(String fname) throws IOException { - List lines = Files.readAllLines(Paths.get(fname)); - // 略过第一行 - for (String line : lines.subList(1, lines.size())) { - for (String word : line.split("[ .?,]+")) - words.add(word.toLowerCase()); - } - } - public String get() { - return words.get(rand.nextInt(words.size())); - } - @Override - public String toString() { - return words.stream() - .collect(Collectors.joining(" ")); - } - public static void main(String[] args) throws Exception { - System.out.println( - Stream.generate(new RandomWords("Cheese.dat")) - .limit(10) - .collect(Collectors.joining(" "))); - } -} -``` - -输出结果: - -``` -it shop sir the much cheese by conclusion district is -``` - -在这里你可以看到更为复杂的 `split()` 运用。在构造器中,每一行都被 `split()` 通过空格或者被方括号包裹的任意标点符号进行分割。在结束方括号后面的 `+` 代表 `+` 前面的东西可以出现一次或者多次。 - -我们注意到在构造函数中循环体使用命令式编程(外部迭代)。在以后的例子中,你甚至会看到我们如何消除这一点。这种旧的形式虽不是特别糟糕,但使用流会让人感觉更好。 - -在 `toString()` 和主方法中你看到了 `collect()` 收集操作,它根据参数来组合所有流中的元素。 - -当你使用 **Collectors.**`joining()`,你将会得到一个 `String` 类型的结果,每个元素都根据 `joining()` 的参数来进行分割。还有许多不同的 `Collectors` 用于产生不同的结果。 - -在主方法中,我们提前看到了 **Stream.**`generate()` 的用法,它可以把任意 `Supplier` 用于生成 `T` 类型的流。 - - -### int 类型的范围 - -`IntStream` 类提供了 `range()` 方法用于生成整型序列的流。编写循环时,这个方法会更加便利: - -```java -// streams/Ranges.java -import static java.util.stream.IntStream.*; -public class Ranges { - public static void main(String[] args) { - // 传统方法: - int result = 0; - for (int i = 10; i < 20; i++) - result += i; - System.out.println(result); - // for-in 循环: - result = 0; - for (int i : range(10, 20).toArray()) - result += i; - System.out.println(result); - // 使用流: - System.out.println(range(10, 20).sum()); - } -} -``` - -输出结果: - -``` -145 -145 -145 -``` - -在主方法中的第一种方式是我们传统编写 `for` 循环的方式;第二种方式,我们使用 `range()` 创建了流并将其转化为数组,然后在 `for-in` 代码块中使用。但是,如果你能像第三种方法那样全程使用流是更好的。我们对范围中的数字进行求和。在流中可以很方便的使用 `sum()` 操作求和。 - -注意 **IntStream.**`range()` 相比 `onjava.Range.range()` 拥有更多的限制。这是由于其可选的第三个参数,后者允许步长大于 1,并且可以从大到小来生成。 - -实用小功能 `repeat()` 可以用来替换简单的 `for` 循环。代码示例: - -```java -// onjava/Repeat.java -package onjava; -import static java.util.stream.IntStream.*; -public class Repeat { - public static void repeat(int n, Runnable action) { - range(0, n).forEach(i -> action.run()); - } -} -``` - -其产生的循环更加清晰: - -```java -// streams/Looping.java -import static onjava.Repeat.*; -public class Looping { - static void hi() { - System.out.println("Hi!"); - } - public static void main(String[] args) { - repeat(3, () -> System.out.println("Looping!")); - repeat(2, Looping::hi); - } -} -``` - -输出结果: - -``` -Looping! -Looping! -Looping! -Hi! -Hi! -``` - -原则上,在代码中包含并解释 `repeat()` 并不值得。诚然它是一个相当透明的工具,但结果取决于你的团队和公司的运作方式。 - -### generate() - -参照 `RandomWords.java` 中 **Stream.**`generate()` 搭配 `Supplier` 使用的例子。代码示例: - -```java -// streams/Generator.java -import java.util.*; -import java.util.function.*; -import java.util.stream.*; - -public class Generator implements Supplier { - Random rand = new Random(47); - char[] letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); - - public String get() { - return "" + letters[rand.nextInt(letters.length)]; - } - - public static void main(String[] args) { - String word = Stream.generate(new Generator()) - .limit(30) - .collect(Collectors.joining()); - System.out.println(word); - } -} -``` - -输出结果: - -``` -YNZBRNYGCFOWZNTCQRGSEGZMMJMROE -``` - -使用 `Random.nextInt()` 方法来挑选字母表中的大写字母。`Random.nextInt()` 的参数代表可以接受的最大的随机数范围,所以使用数组边界是经过深思熟虑的。 - -如果要创建包含相同对象的流,只需要传递一个生成那些对象的 `lambda` 到 `generate()` 中: - -```java -// streams/Duplicator.java -import java.util.stream.*; -public class Duplicator { - public static void main(String[] args) { - Stream.generate(() -> "duplicate") - .limit(3) - .forEach(System.out::println); - } -} -``` - -输出结果: - -``` -duplicate -duplicate -duplicate -``` - -如下是在本章之前例子中使用过的 `Bubble` 类。**注意**它包含了自己的静态生成器(Static generator)方法。 - -```java -// streams/Bubble.java -import java.util.function.*; -public class Bubble { - public final int i; - - public Bubble(int n) { - i = n; - } - - @Override - public String toString() { - return "Bubble(" + i + ")"; - } - - private static int count = 0; - public static Bubble bubbler() { - return new Bubble(count++); - } -} -``` - -由于 `bubbler()` 与 `Supplier` 是接口兼容的,我们可以将其方法引用直接传递给 **Stream.**`generate()`: - -```java -// streams/Bubbles.java -import java.util.stream.*; -public class Bubbles { - public static void main(String[] args) { - Stream.generate(Bubble::bubbler) - .limit(5) - .forEach(System.out::println); - } -} -``` - -输出结果: - -``` -Bubble(0) -Bubble(1) -Bubble(2) -Bubble(3) -Bubble(4) -``` - -这是创建单独工厂类(Separate Factory class)的另一种方式。在很多方面它更加整洁,但是这是一个对于代码组织和品味的问题——你总是可以创建一个完全不同的工厂类。 - -### iterate() - -**Stream.**`iterate()` 以种子(第一个参数)开头,并将其传给方法(第二个参数)。方法的结果将添加到流,并存储作为第一个参数用于下次调用 `iterate()`,依次类推。我们可以利用 `iterate()` 生成一个斐波那契数列。代码示例: - -```java -// streams/Fibonacci.java -import java.util.stream.*; -public class Fibonacci { - int x = 1; - - Stream numbers() { - return Stream.iterate(0, i -> { - int result = x + i; - x = i; - return result; - }); - } - - public static void main(String[] args) { - new Fibonacci().numbers() - .skip(20) // 过滤前 20 个 - .limit(10) // 然后取 10 个 - .forEach(System.out::println); - } -} -``` - -输出结果: - -``` -6765 -10946 -17711 -28657 -46368 -75025 -121393 -196418 -317811 -514229 -``` - -斐波那契数列将数列中最后两个元素进行求和以产生下一个元素。`iterate()` 只能记忆结果,因此我们需要利用一个变量 `x` 追踪另外一个元素。 - -在主方法中,我们使用了一个之前没有见过的 `skip()` 操作。它根据参数丢弃指定数量的流元素。在这里,我们丢弃了前 20 个元素。 - -### 流的建造者模式 - -在建造者设计模式(也称构造器模式)中,首先创建一个 `builder` 对象,传递给它多个构造器信息,最后执行“构造”。**Stream** 库提供了这样的 `Builder`。在这里,我们重新审视文件读取并将其转换成为单词流的过程。代码示例: - -```java -// streams/FileToWordsBuilder.java -import java.io.*; -import java.nio.file.*; -import java.util.stream.*; - -public class FileToWordsBuilder { - Stream.Builder builder = Stream.builder(); - - public FileToWordsBuilder(String filePath) throws Exception { - Files.lines(Paths.get(filePath)) - .skip(1) // 略过开头的注释行 - .forEach(line -> { - for (String w : line.split("[ .?,]+")) - builder.add(w); - }); - } - - Stream stream() { - return builder.build(); - } - - public static void main(String[] args) throws Exception { - new FileToWordsBuilder("Cheese.dat") - .stream() - .limit(7) - .map(w -> w + " ") - .forEach(System.out::print); - } -} -``` - -输出结果: - -``` -Not much of a cheese shop really -``` - -**注意**,构造器会添加文件中的所有单词(除了第一行,它是包含文件路径信息的注释),但是其并没有调用 `build()`。只要你不调用 `stream()` 方法,就可以继续向 `builder` 对象中添加单词。 - -在该类的更完整形式中,你可以添加一个标志位用于查看 `build()` 是否被调用,并且可能的话增加一个可以添加更多单词的方法。在 `Stream.Builder` 调用 `build()` 方法后继续尝试添加单词会产生一个异常。 - -### Arrays - -`Arrays` 类中含有一个名为 `stream()` 的静态方法用于把数组转换成为流。我们可以重写 `interfaces/Machine.java` 中的主方法用于创建一个流,并将 `execute()` 应用于每一个元素。代码示例: - -```java -// streams/Machine2.java -import java.util.*; -import onjava.Operations; -public class Machine2 { - public static void main(String[] args) { - Arrays.stream(new Operations[] { - () -> Operations.show("Bing"), - () -> Operations.show("Crack"), - () -> Operations.show("Twist"), - () -> Operations.show("Pop") - }).forEach(Operations::execute); - } -} -``` - -输出结果: - -``` -Bing -Crack -Twist -Pop -``` - -`new Operations[]` 表达式动态创建了 `Operations` 对象的数组。 - -`stream()` 同样可以产生 **IntStream**,**LongStream** 和 **DoubleStream**。 - -```java -// streams/ArrayStreams.java -import java.util.*; -import java.util.stream.*; - -public class ArrayStreams { - public static void main(String[] args) { - Arrays.stream(new double[] { 3.14159, 2.718, 1.618 }) - .forEach(n -> System.out.format("%f ", n)); - System.out.println(); - - Arrays.stream(new int[] { 1, 3, 5 }) - .forEach(n -> System.out.format("%d ", n)); - System.out.println(); - - Arrays.stream(new long[] { 11, 22, 44, 66 }) - .forEach(n -> System.out.format("%d ", n)); - System.out.println(); - - // 选择一个子域: - Arrays.stream(new int[] { 1, 3, 5, 7, 15, 28, 37 }, 3, 6) - .forEach(n -> System.out.format("%d ", n)); - } -} -``` - -输出结果: - -``` -3.141590 2.718000 1.618000 -1 3 5 -11 22 44 66 -7 15 28 -``` - -最后一次 `stream()` 的调用有两个额外的参数。第一个参数告诉 `stream()` 从数组的哪个位置开始选择元素,第二个参数用于告知在哪里停止。每种不同类型的 `stream()` 都有类似的操作。 - -### 正则表达式 - -Java 的正则表达式将在[字符串](18-Strings.md)这一章节详细介绍。Java 8 在 `java.util.regex.Pattern` 中增加了一个新的方法 `splitAsStream()`。这个方法可以根据传入的公式将字符序列转化为流。但是有一个限制,输入只能是 **CharSequence**,因此不能将流作为 `splitAsStream()` 的参数。 - -我们再一次查看将文件处理为单词流的过程。这一次,我们使用流将文件分割为单独的字符串,接着使用正则表达式将字符串转化为单词流。 - -```java -// streams/FileToWordsRegexp.java -import java.io.*; -import java.nio.file.*; -import java.util.stream.*; -import java.util.regex.Pattern; -public class FileToWordsRegexp { - private String all; - public FileToWordsRegexp(String filePath) throws Exception { - all = Files.lines(Paths.get(filePath)) - .skip(1) // First (comment) line - .collect(Collectors.joining(" ")); - } - public Stream stream() { - return Pattern - .compile("[ .,?]+").splitAsStream(all); - } - public static void - main(String[] args) throws Exception { - FileToWordsRegexp fw = new FileToWordsRegexp("Cheese.dat"); - fw.stream() - .limit(7) - .map(w -> w + " ") - .forEach(System.out::print); - fw.stream() - .skip(7) - .limit(2) - .map(w -> w + " ") - .forEach(System.out::print); - } -} -``` - -输出结果: - -``` -Not much of a cheese shop really is it -``` - -在构造器中我们读取了文件中的所有内容(跳过第一行注释,并将其转化成为单行字符串)。现在,当你调用 `stream()` 的时候,可以像往常一样获取一个流,但这次你可以多次调用 `stream()` 在已存储的字符串中创建一个新的流。这里有个限制,整个文件必须存储在内存中;在大多数情况下这并不是什么问题,但是这损失了流操作非常重要的优势: - -1. 流“不需要存储”。当然它们需要一些内部存储,但是这只是序列的一小部分,和持有整个序列并不相同。 -2. 它们是懒加载计算的。 - -幸运的是,我们稍后就会知道如何解决这个问题。 - - - -## 中间操作 - -中间操作用于从一个流中获取对象,并将对象作为另一个流从后端输出,以连接到其他操作。 - -### 跟踪和调试 - -`peek()` 操作的目的是帮助调试。它允许你无修改地查看流中的元素。代码示例: - -```java -// streams/Peeking.java -class Peeking { - public static void main(String[] args) throws Exception { - FileToWords.stream("Cheese.dat") - .skip(21) - .limit(4) - .map(w -> w + " ") - .peek(System.out::print) - .map(String::toUpperCase) - .peek(System.out::print) - .map(String::toLowerCase) - .forEach(System.out::print); - } -} -``` - -输出结果: - -``` -Well WELL well it IT it s S s so SO so -``` - -`FileToWords` 稍后定义,但它的功能实现貌似和之前我们看到的差不多:产生字符串对象的流。之后在其通过管道时调用 `peek()` 进行处理。 - -因为 `peek()` 符合无返回值的 **Consumer** 函数式接口,所以我们只能观察,无法使用不同的元素来替换流中的对象。 - -### 流元素排序 - -在 `Randoms.java` 中,我们熟识了 `sorted()` 的默认比较器实现。其实它还有另一种形式的实现:传入一个 **Comparator** 参数。代码示例: - -```java -// streams/SortedComparator.java -import java.util.*; -public class SortedComparator { - public static void main(String[] args) throws Exception { - FileToWords.stream("Cheese.dat") - .skip(10) - .limit(10) - .sorted(Comparator.reverseOrder()) - .map(w -> w + " ") - .forEach(System.out::print); - } -} -``` - -输出结果: - -``` -you what to the that sir leads in district And -``` - -`sorted()` 预设了一些默认的比较器。这里我们使用的是反转“自然排序”。当然你也可以把 Lambda 函数作为参数传递给 `sorted()`。 - -### 移除元素 - -* `distinct()`:在 `Randoms.java` 类中的 `distinct()` 可用于消除流中的重复元素。相比创建一个 **Set** 集合,该方法的工作量要少得多。 - -* `filter(Predicate)`:过滤操作会保留与传递进去的过滤器函数计算结果为 `true` 的元素。 - -在下例中,`isPrime()` 作为过滤器函数,用于检测质数。 - -```java -import java.util.stream.*; -import static java.util.stream.LongStream.*; -public class Prime { - public static Boolean isPrime(long n) { - return rangeClosed(2, (long)Math.sqrt(n)) - .noneMatch(i -> n % i == 0); - } - public LongStream numbers() { - return iterate(2, i -> i + 1) - .filter(Prime::isPrime); - } - public static void main(String[] args) { - new Prime().numbers() - .limit(10) - .forEach(n -> System.out.format("%d ", n)); - System.out.println(); - new Prime().numbers() - .skip(90) - .limit(10) - .forEach(n -> System.out.format("%d ", n)); - } -} -``` - -输出结果: - -``` -2 3 5 7 11 13 17 19 23 29 -467 479 487 491 499 503 509 521 523 541 -``` - -`rangeClosed()` 包含了上限值。如果不能整除,即余数不等于 0,则 `noneMatch()` 操作返回 `true`,如果出现任何等于 0 的结果则返回 `false`。 `noneMatch()` 操作一旦有失败就会退出。 - -### 应用函数到元素 - -- `map(Function)`:将函数操作应用在输入流的元素中,并将返回值传递到输出流中。 - -- `mapToInt(ToIntFunction)`:操作同上,但结果是 **IntStream**。 - -- `mapToLong(ToLongFunction)`:操作同上,但结果是 **LongStream**。 - -- `mapToDouble(ToDoubleFunction)`:操作同上,但结果是 **DoubleStream**。 - -在这里,我们使用 `map()` 映射多种函数到一个字符串流中。代码示例: - -```java -// streams/FunctionMap.java -import java.util.*; -import java.util.stream.*; -import java.util.function.*; -class FunctionMap { - static String[] elements = { "12", "", "23", "45" }; - static Stream - testStream() { - return Arrays.stream(elements); - } - static void test(String descr, Function func) { - System.out.println(" ---( " + descr + " )---"); - testStream() - .map(func) - .forEach(System.out::println); - } - public static void main(String[] args) { - test("add brackets", s -> "[" + s + "]"); - test("Increment", s -> { - try { - return Integer.parseInt(s) + 1 + ""; - } - catch(NumberFormatException e) { - return s; - } - } - ); - test("Replace", s -> s.replace("2", "9")); - test("Take last digit", s -> s.length() > 0 ? - s.charAt(s.length() - 1) + "" : s); - } -} -``` - -输出结果: - -``` ----( add brackets )--- -[12] -[] -[23] -[45] ----( Increment )--- -13 -24 -46 ----( Replace )--- -19 -93 -45 ----( Take last digit )--- -2 -3 -5 -``` - -在上面的自增示例中,我们使用 `Integer.parseInt()` 尝试将一个字符串转化为整数。如果字符串不能转化成为整数就会抛出 **NumberFormatException** 异常,我们只须回过头来将原始字符串放回到输出流中。 - -在以上例子中,`map()` 将一个字符串映射为另一个字符串,但是我们完全可以产生和接收类型完全不同的类型,从而改变流的数据类型。下面代码示例: - -```java -// streams/FunctionMap2.java -// Different input and output types (不同的输入输出类型) -import java.util.*; -import java.util.stream.*; -class Numbered { - final int n; - Numbered(int n) { - this.n = n; - } - @Override - public String toString() { - return "Numbered(" + n + ")"; - } -} -class FunctionMap2 { - public static void main(String[] args) { - Stream.of(1, 5, 7, 9, 11, 13) - .map(Numbered::new) - .forEach(System.out::println); - } -} -``` - -输出结果: - -``` -Numbered(1) -Numbered(5) -Numbered(7) -Numbered(9) -Numbered(11) -Numbered(13) -``` - -我们将获取到的整数通过构造器 `Numbered::new` 转化成为 `Numbered` 类型。 - -如果使用 **Function** 返回的结果是数值类型的一种,我们必须使用合适的 `mapTo数值类型` 进行替代。代码示例: - -```java -// streams/FunctionMap3.java -// Producing numeric output streams( 产生数值输出流) -import java.util.*; -import java.util.stream.*; -class FunctionMap3 { - public static void main(String[] args) { - Stream.of("5", "7", "9") - .mapToInt(Integer::parseInt) - .forEach(n -> System.out.format("%d ", n)); - System.out.println(); - Stream.of("17", "19", "23") - .mapToLong(Long::parseLong) - .forEach(n -> System.out.format("%d ", n)); - System.out.println(); - Stream.of("17", "1.9", ".23") - .mapToDouble(Double::parseDouble) - .forEach(n -> System.out.format("%f ", n)); - } -} -``` - -输出结果: - -``` -5 7 9 -17 19 23 -17.000000 1.900000 0.230000 -``` - -遗憾的是,Java 设计者并没有尽最大努力去消除基本类型。 - -### 在 `map()` 中组合流 -假设现有一个传入的元素流,并且打算对流元素使用 `map()` 函数。现在你已经找到了一些可爱并独一无二的函数功能,但问题来了:这个函数功能是产生一个流。我们想要产生一个元素流,而实际却产生了一个元素流的流。 - -`flatMap()` 做了两件事: -- 将产生流的函数应用在每个元素上(与 `map()` 所做的相同) -- 然后将每个流都扁平化为元素 - -因而最终产生的仅是元素。 - -`flatMap(Function)`:当 `Function` 产生流时使用。 - -`flatMapToInt(Function)`:当 `Function` 产生 `IntStream` 时使用。 - -`flatMapToLong(Function)`:当 `Function` 产生 `LongStream` 时使用。 - -`flatMapToDouble(Function)`:当 `Function` 产生 `DoubleStream` 时使用。 - -为了弄清其工作原理,我们从传入一个刻意设计的函数给 `map()` 开始。该函数接受一个整数并产生一个字符串流: -![](https://img-blog.csdnimg.cn/29e0507209fe478f9cc9e1a4e8607c86.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -我们天真地希望能够得到字符串流,但实际得到的却是“Head”流的流。 -可使用 `flatMap()` 解决: -![](https://img-blog.csdnimg.cn/ef3d02412fe9421c94572bea489b0482.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -从map返回的每个流都会自动扁平为组成它的字符串。 - -现在从一个整数流开始,然后使用每个整数去创建更多的随机数。 -![](https://img-blog.csdnimg.cn/fd12336ec98f45de924bf36d7da930ad.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - `concat()`以参数顺序组合两个流。 如此,我们在每个随机 `Integer` 流的末尾添加一个 -1 作为标记。你可以看到最终流确实是从一组扁平流中创建的。 - -因为 `rand.ints()` 产生的是一个 `IntStream`,所以必须使用 `flatMap()`、`concat()` 和 `of()` 的特定整数形式。 - -将文件划分为单词流。 -最后使用到的是 **FileToWordsRegexp.java**,它的问题是需要将整个文件读入行列表中 —— 显然需要存储该列表。而我们真正想要的是创建一个不需要中间存储层的单词流。 - -下面,我们再使用 ` flatMap()` 来解决这个问题: -![](https://img-blog.csdnimg.cn/3ac1b73f2dd34700972a38578eae8070.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -`stream()` 现在是个静态方法,因为它可自己完成整个流创建过程。 - -**注意**:`\\W+` 是一个正则表达式,表示非单词字符,`+` 表示可出现一或多次。小写形式的 `\\w` 表示“单词字符”。 - -之前遇到的问题是 `Pattern.compile().splitAsStream()` 产生的结果为流,这意味着当只想要一个简单的单词流时,在传入的行流(stream of lines)上调用 `map()` 会产生一个单词流的流。 -好在 `flatMap()` 可将**元素流的流**扁平化为一个**简单的元素流**。或者,可使用 `String.split()` 生成一个数组,其可以被 `Arrays.stream()` 转化成为流: -```java -.flatMap(line -> Arrays.stream(line.split("\\W+")))) -``` -有了真正的、而非 `FileToWordsRegexp.java` 中基于集合存储的流,我们每次使用都必须从头创建,因为流不能被复用: -![](https://img-blog.csdnimg.cn/2e91abc8794944bdb9aba4a352ba8992.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -在 `System.out.format()` 中的 `%s` 表明参数为 **String** 类型。 - -## Optional类 -若在一个空流取元素会发生什么?我们喜欢为了“happy path”而将流连接起来,并假设流不会被中断。在流中放置 `null` 就是个很好的中断方法。那么是否有某种对象,可作为流元素的持有者,即使查看的元素不存在也能友好地提示我们(即不会粗暴地抛异常)? - -**Optional** 就可以。一些标准流操作返回 **Optional** 对象,因为它们**并不能保证预期结果一定存在**: -- `findFirst()` -返回一个包含第一个元素的 **Optional** 对象,若流为空则返回 **Optional.empty** -- `findAny()` -返回包含任意元素的 **Optional** 对象,若流为空则返回 **Optional.empty** -- `max()` 和 `min()` -返回一个包含最大值或者最小值的 **Optional** 对象,若流为空则返回 **Optional.empty** - - `reduce()` 不再以 `identity` 形式开头,而是将其返回值包装在 **Optional** 中。(`identity` 对象成为其他形式的 `reduce()` 的默认结果,因此不存在空结果的风险) - -对于数字流 **IntStream**、**LongStream** 和 **DoubleStream**,`average()` 会将结果包装在 **Optional** 以防止流为空。 - -以下是对空流进行所有这些操作的简单测试: -```java -class OptionalsFromEmptyStreams { - public static void main(String[] args) { - System.out.println(Stream.empty() - .findFirst()); - System.out.println(Stream.empty() - .findAny()); - System.out.println(Stream.empty() - .max(String.CASE_INSENSITIVE_ORDER)); - System.out.println(Stream.empty() - .min(String.CASE_INSENSITIVE_ORDER)); - System.out.println(Stream.empty() - .reduce((s1, s2) -> s1 + s2)); - System.out.println(IntStream.empty() - .average()); - } -} - -Optional.empty -Optional.empty -Optional.empty -Optional.empty -Optional.empty -OptionalDouble.empty -``` - -当流为空的时候你会获得一个 **Optional.empty** 对象,而不是抛异常。**Optional** 的 `toString()` 方法可以用于展示有用信息。 - -空流是通过 `Stream.empty()` 创建的。如果你在没有任何上下文环境的情况下调用 `Stream.empty()`,Java 并不知道它的数据类型;这个语法解决了这个问题。如果编译器拥有了足够的上下文信息,比如: -```java -Stream s = Stream.empty(); -``` -就可以在调用 `empty()` 时推断类型。 - -**Optional** 的两个基本用法: -```java -class OptionalBasics { - static void test(Optional optString) { - if(optString.isPresent()) - System.out.println(optString.get()); - else - System.out.println("Nothing inside!"); - } - public static void main(String[] args) { - test(Stream.of("Epithets").findFirst()); - test(Stream.empty().findFirst()); - } -} - -Epithets -Nothing inside! -``` - -当你接收到 **Optional** 对象时,应首先调用 `isPresent()` 检查其中是否包含元素。如果存在,可使用 `get()` 获取。 - -### 便利函数 -有许多便利函数可以解包 **Optional** ,这简化了上述“对所包含的对象的检查和执行操作”的过程: - -- `ifPresent(Consumer)`:当值存在时调用 **Consumer**,否则什么也不做。 -- `orElse(otherObject)`:如果值存在则直接返回,否则生成 **otherObject**。 -- `orElseGet(Supplier)`:如果值存在则直接返回,否则使用 **Supplier** 函数生成一个可替代对象。 -- `orElseThrow(Supplier)`:如果值存在直接返回,否则使用 **Supplier** 函数生成一个异常。 - -如下是针对不同便利函数的简单演示: -```java -public class Optionals { - static void basics(Optional optString) { - if(optString.isPresent()) - System.out.println(optString.get()); - else - System.out.println("Nothing inside!"); - } - static void ifPresent(Optional optString) { - optString.ifPresent(System.out::println); - } - static void orElse(Optional optString) { - System.out.println(optString.orElse("Nada")); - } - static void orElseGet(Optional optString) { - System.out.println( - optString.orElseGet(() -> "Generated")); - } - static void orElseThrow(Optional optString) { - try { - System.out.println(optString.orElseThrow( - () -> new Exception("Supplied"))); - } catch(Exception e) { - System.out.println("Caught " + e); - } - } - static void test(String testName, Consumer> cos) { - System.out.println(" === " + testName + " === "); - cos.accept(Stream.of("Epithets").findFirst()); - cos.accept(Stream.empty().findFirst()); - } - public static void main(String[] args) { - test("basics", Optionals::basics); - test("ifPresent", Optionals::ifPresent); - test("orElse", Optionals::orElse); - test("orElseGet", Optionals::orElseGet); - test("orElseThrow", Optionals::orElseThrow); - } -} - -=== basics === -Epithets -Nothing inside! -=== ifPresent === -Epithets -=== orElse === -Epithets -Nada -=== orElseGet === -Epithets -Generated -=== orElseThrow === -Epithets -Caught java.lang.Exception: Supplied -``` - -`test()` 通过传入所有方法都适用的 **Consumer** 来避免重复代码。 - -`orElseThrow()` 通过 **catch** 关键字来捕获抛出的异常。 -### 创建 Optional -当我们在自己的代码中加入 **Optional** 时,可以使用下面 3 个静态方法: -- `empty()`:生成一个空 **Optional**。 -- `of(value)`:将一个非空值包装到 **Optional** 里。 -- `ofNullable(value)`:针对一个可能为空的值,为空时自动生成 **Optional.empty**,否则将值包装在 **Optional** 中。 - -代码示例: -```java -class CreatingOptionals { - static void test(String testName, Optional opt) { - System.out.println(" === " + testName + " === "); - System.out.println(opt.orElse("Null")); - } - public static void main(String[] args) { - test("empty", Optional.empty()); - test("of", Optional.of("Howdy")); - try { - test("of", Optional.of(null)); - } catch(Exception e) { - System.out.println(e); - } - test("ofNullable", Optional.ofNullable("Hi")); - test("ofNullable", Optional.ofNullable(null)); - } -} - -=== empty === -Null -=== of === -Howdy -java.lang.NullPointerException -=== ofNullable === -Hi -=== ofNullable === -Null -``` - -我们不能通过传递 `null` 到 `of()` 来创建 `Optional` 对象。最安全的方法是, 使用 `ofNullable()` 来优雅地处理 `null`。 - -### Optional 对象操作 -当我们的流管道生成了 **Optional** 对象,如下方法可使得 **Optional** 的后续能做更多操作: - -- `filter(Predicate)`:将 **Predicate** 应用于 **Optional** 中的内容并返回结果。当 **Optional** 不满足 **Predicate** 时返回空。如果 **Optional** 为空,则直接返回。 - -- `map(Function)`:如果 **Optional** 不为空,应用 **Function** 于 **Optional** 中的内容,并返回结果。否则直接返回 **Optional.empty**。 - -- `flatMap(Function)`:同 `map()`,但是提供的映射函数将结果包装在 **Optional** 对象中,因此 `flatMap()` 不会在最后进行任何包装。 - -以上方法都不适用于数值型 **Optional**。 -一般来说,流的 `filter()` 会在 **Predicate** 返回 `false` 时移除流元素。 -而 `Optional.filter()` 在失败时不会删除 **Optional**,而是将其保留下来,并转化为空。 - -```java -class OptionalFilter { - static String[] elements = { - "Foo", "", "Bar", "Baz", "Bingo" - }; - static Stream testStream() { - return Arrays.stream(elements); - } - static void test(String descr, Predicate pred) { - System.out.println(" ---( " + descr + " )---"); - for(int i = 0; i <= elements.length; i++) { - System.out.println( - testStream() - .skip(i) - .findFirst() - .filter(pred)); - } - } - public static void main(String[] args) { - test("true", str -> true); - test("false", str -> false); - test("str != \"\"", str -> str != ""); - test("str.length() == 3", str -> str.length() == 3); - test("startsWith(\"B\")", - str -> str.startsWith("B")); - } -} -``` -即使输出看起来像流,特别是 `test()` 中的 for 循环。每一次的 for 循环时重新启动流,然后根据 for 循环的索引跳过指定个数的元素,这就是它最终在流中的每个连续元素上的结果。接下来调用 `findFirst()` 获取剩余元素中的第一个元素,结果会包装在 **Optional** 中。 - -**注意**,不同于普通 for 循环,这里的索引值范围并不是 `i < elements.length`, 而是 `i <= elements.length`。所以最后一个元素实际上超出了流。方便的是,这将自动成为 **Optional.empty**。 - -同 `map()` 一样 , `Optional.map()` 应用于函数。它仅在 **Optional** 不为空时才应用映射函数,并将 **Optional** 的内容提取到映射函数。代码示例: - -```java -class OptionalMap { - static String[] elements = {"12", "", "23", "45"}; - - static Stream testStream() { - return Arrays.stream(elements); - } - - static void test(String descr, Function func) { - System.out.println(" ---( " + descr + " )---"); - for (int i = 0; i <= elements.length; i++) { - System.out.println( - testStream() - .skip(i) - .findFirst() // Produces an Optional - .map(func)); - } - } - - public static void main(String[] args) { - // If Optional is not empty, map() first extracts - // the contents which it then passes - // to the function: - test("Add brackets", s -> "[" + s + "]"); - test("Increment", s -> { - try { - return Integer.parseInt(s) + 1 + ""; - } catch (NumberFormatException e) { - return s; - } - }); - test("Replace", s -> s.replace("2", "9")); - test("Take last digit", s -> s.length() > 0 ? - s.charAt(s.length() - 1) + "" : s); - } - // After the function is finished, map() wraps the - // result in an Optional before returning it: -} -``` -映射函数的返回结果会自动包装成为 **Optional**。**Optional.empty** 会被直接跳过。 - -**Optional** 的 `flatMap()` 应用于已生成 **Optional** 的映射函数,所以 `flatMap()` 不会像 `map()` 那样将结果封装在 **Optional** 中。代码示例: - -```java -// streams/OptionalFlatMap.java -import java.util.Arrays; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Stream; - -class OptionalFlatMap { - static String[] elements = {"12", "", "23", "45"}; - - static Stream testStream() { - return Arrays.stream(elements); - } - - static void test(String descr, - Function> func) { - System.out.println(" ---( " + descr + " )---"); - for (int i = 0; i <= elements.length; i++) { - System.out.println( - testStream() - .skip(i) - .findFirst() - .flatMap(func)); - } - } - - public static void main(String[] args) { - test("Add brackets", - s -> Optional.of("[" + s + "]")); - test("Increment", s -> { - try { - return Optional.of( - Integer.parseInt(s) + 1 + ""); - } catch (NumberFormatException e) { - return Optional.of(s); - } - }); - test("Replace", - s -> Optional.of(s.replace("2", "9"))); - test("Take last digit", - s -> Optional.of(s.length() > 0 ? - s.charAt(s.length() - 1) + "" - : s)); - } -} -``` -同 `map()`,`flatMap()` 将提取非空 **Optional** 的内容并将其应用在映射函数。唯一的区别就是 `flatMap()` 不会把结果包装在 **Optional** 中,因为映射函数已经被包装过了。在如上示例中,我们已经在每一个映射函数中显式地完成了包装,但是很显然 `Optional.flatMap()` 是为那些自己已经生成 **Optional** 的函数而设计的。 - -### Optional 流 -假设你的生成器可能产生 `null` 值,那么当用它来创建流时,你会想到用 **Optional** 包装元素: -![](https://img-blog.csdnimg.cn/20210701144821407.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -使用这个流时,必须清楚如何解包 **Optional**: -![](https://img-blog.csdnimg.cn/20210701145701858.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -输出结果: -![](https://img-blog.csdnimg.cn/20210701145528863.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -由于每种情况都需要定义“空值”的含义,所以通常我们要为每个应用程序采用不同的方法。 - -## 终端操作 -以下操作将会获取流的最终结果。至此我们无法再继续往后传递流。可以说,终端操作总是我们在流管道中所做的最后一件事。 - -### 数组 -- `toArray()`:将流转换成适当类型的数组 -- `toArray(generator)`:在特殊情况下,生成自定义类型的数组 - -假设需复用流产生的随机数: -![](https://img-blog.csdnimg.cn/20210701150017569.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -这样每次调用 `rands()` 的时候可以重复获取相同的整数流。 - -### 循环 -- `forEach(Consumer)`常见如 `System.out::println` 作为 **Consumer** 函数。 -- `forEachOrdered(Consumer)`: 保证 `forEach` 按照原始流顺序操作。 - -第一种形式:无序操作,仅在引入并行流时才有意义。 `parallel()`:可实现多处理器并行操作。实现原理为将流分割为多个(通常数目为 CPU 核心数)并在不同处理器上分别执行操作。因为我们采用的是内部迭代,而不是外部迭代,所以这是可能实现的。 - -下例引入 `parallel()` 来帮助理解 `forEachOrdered(Consumer)` 的作用和使用场景: -```java -// streams/ForEach.java -import java.util.*; -import java.util.stream.*; -import static streams.RandInts.*; -public class ForEach { - static final int SZ = 14; - public static void main(String[] args) { - rands().limit(SZ) - .forEach(n -> System.out.format("%d ", n)); - System.out.println(); - rands().limit(SZ) - .parallel() - .forEach(n -> System.out.format("%d ", n)); - System.out.println(); - rands().limit(SZ) - .parallel() - .forEachOrdered(n -> System.out.format("%d ", n)); - } -} -``` -为了方便测试不同大小的数组,我们抽离出了 `SZ` 变量。结果很有趣:在第一个流中,未使用 `parallel()` ,所以 `rands()` 按照元素迭代出现的顺序显示结果;在第二个流中,引入`parallel()` ,即便流很小,输出的结果顺序也和前面不一样。这是由于多处理器并行操作的原因。多次运行测试,结果均不同。多处理器并行操作带来的非确定性因素造成了这样的结果。 - -在最后一个流中,同时使用了 `parallel()` 和 `forEachOrdered()` 来强制保持原始流顺序。因此,对非并行流使用 `forEachOrdered()` 是没有任何影响的。 - -### 集合 -- `collect(Collector)`:使用 **Collector** 收集流元素到结果集合中。 -- `collect(Supplier, BiConsumer, BiConsumer)`:同上,第一个参数 **Supplier** 创建了一个新结果集合,第二个参数 **BiConsumer** 将下一个元素包含到结果中,第三个参数 **BiConsumer** 用于将两个值组合起来。 - -假设我们现在为保证元素有序,将元素存储在 **TreeSet**。**Collectors** 没有特定的 `toTreeSet()`,但可以通过将集合的构造器引用传递给 `Collectors.toCollection()`,从而构建任意类型的集合。 - -比如,将一个文件中的单词收集到 **TreeSet**: -![](https://img-blog.csdnimg.cn/2021060515055616.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -我们也可以在流中生成 **Map**。代码示例: - -```java -// streams/MapCollector.java -import java.util.*; -import java.util.stream.*; -class Pair { - public final Character c; - public final Integer i; - Pair(Character c, Integer i) { - this.c = c; - this.i = i; - } - public Character getC() { return c; } - public Integer getI() { return i; } - @Override - public String toString() { - return "Pair(" + c + ", " + i + ")"; - } -} -class RandomPair { - Random rand = new Random(47); - // An infinite iterator of random capital letters: - Iterator capChars = rand.ints(65,91) - .mapToObj(i -> (char)i) - .iterator(); - public Stream stream() { - return rand.ints(100, 1000).distinct() - .mapToObj(i -> new Pair(capChars.next(), i)); - } -} -public class MapCollector { - public static void main(String[] args) { - Map map = - new RandomPair().stream() - .limit(8) - .collect( - Collectors.toMap(Pair::getI, Pair::getC)); - System.out.println(map); - } -} -``` - -输出结果: - -``` -{688=W, 309=C, 293=B, 761=N, 858=N, 668=G, 622=F, 751=N} -``` - -**Pair** 只是一个基础的数据对象。**RandomPair** 创建了随机生成的 **Pair** 对象流。在 Java 中,我们不能直接以某种方式组合两个流。所以这里创建了一个整数流,并且使用 `mapToObj()` 将其转化成为 **Pair** 流。 **capChars** 随机生成的大写字母迭代器从流开始,然后 `iterator()` 允许我们在 `stream()` 中使用它。就我所知,这是组合多个流以生成新的对象流的唯一方法。 - -在这里,我们只使用最简单形式的 `Collectors.toMap()`,这个方法值需要一个可以从流中获取键值对的函数。还有其他重载形式,其中一种形式是在遇到键值冲突时,需要一个函数来处理这种情况。 - -大多数情况下,`java.util.stream.Collectors` 中预设的 **Collector** 就能满足我们的要求。 -还可以使用第二种形式的 `collect()`。 -```java -// streams/SpecialCollector.java -import java.util.*; -import java.util.stream.*; -public class SpecialCollector { - public static void main(String[] args) throws Exception { - ArrayList words = - FileToWords.stream("Cheese.dat") - .collect(ArrayList::new, - ArrayList::add, - ArrayList::addAll); - words.stream() - .filter(s -> s.equals("cheese")) - .forEach(System.out::println); - } -} -``` - -输出结果: - -``` -cheese -cheese -``` - -在这里, **ArrayList** 的方法已经执行了你所需要的操作,但是似乎更有可能的是,如果你必须使用这种形式的 `collect()`,则必须自己创建特殊的定义。 - -#### 对List根据一个或多个字段分组 -项目中遇到了需要对list进行分组的场景,根据List中entity的某字段或者多个字段进行分组,形成Map,然后根据map进行相关的业务操作。 -##### 根据一个字段进行分组 -```java -public class ListGroupBy { - public static void main(String[] args) { - List scoreList = new ArrayList<>(); - scoreList.add(new Score().setStudentId("001").setScoreYear("2018").setScore(100.0)); - scoreList.add(new Score().setStudentId("001").setScoreYear("2019").setScore(59.5)); - scoreList.add(new Score().setStudentId("001").setScoreYear("2019").setScore(99.0)); - scoreList.add(new Score().setStudentId("002").setScoreYear("2018").setScore(99.6)); - //根据scoreYear字段进行分组 - Map> map = scoreList.stream().collect( - Collectors.groupingBy( - score -> score.getScoreYear() - )); - System.out.println(JSONUtil.toJsonPrettyStr(map)); - } -} -``` -结果: - -```java -{ - "2019": [ - { - "studentId": "001", - "score": 59.5, - "scoreYear": "2019" - }, - { - "studentId": "001", - "score": 99, - "scoreYear": "2019" - } - ], - "2018": [ - { - "studentId": "001", - "score": 100, - "scoreYear": "2018" - }, - { - "studentId": "002", - "score": 99.6, - "scoreYear": "2018" - } - ] -} -``` -##### 根据多个字段进行分组 -将 - -```java -//根据scoreYear字段进行分组 -Map> map = scoreList.stream().collect( - Collectors.groupingBy( - score -> score.getScoreYear() - )); -``` - 改为 - - -```java -//根据scoreYear和studentId字段进行分组 - Map> map = scoreList.stream().collect( - Collectors.groupingBy( - score -> score.getScoreYear()+'-'+score.getStudentId() - )); -``` -结果: - -```java -{ - "2019-001": [ - { - "studentId": "001", - "score": 59.5, - "scoreYear": "2019" - }, - { - "studentId": "001", - "score": 99, - "scoreYear": "2019" - } - ], - "2018-001": [ - { - "studentId": "001", - "score": 100, - "scoreYear": "2018" - } - ], - "2018-002": [ - { - "studentId": "002", - "score": 99.6, - "scoreYear": "2018" - } - ] -} -``` - - - - - -### 组合 - -- `reduce(BinaryOperator)`:使用 **BinaryOperator** 来组合所有流中的元素。因为流可能为空,其返回值为 **Optional**。 -- `reduce(identity, BinaryOperator)`:功能同上,但是使用 **identity** 作为其组合的初始值。因此如果流为空,**identity** 就是结果。 -- `reduce(identity, BiFunction, BinaryOperator)`:更复杂的使用形式(暂不介绍),这里把它包含在内,因为它可以提高效率。通常,我们可以显式地组合 `map()` 和 `reduce()` 来更简单的表达它。 - -下面来看下 `reduce` 的代码示例: - -```java -// streams/Reduce.java -import java.util.*; -import java.util.stream.*; -class Frobnitz { - int size; - Frobnitz(int sz) { size = sz; } - @Override - public String toString() { - return "Frobnitz(" + size + ")"; - } - // Generator: - static Random rand = new Random(47); - static final int BOUND = 100; - static Frobnitz supply() { - return new Frobnitz(rand.nextInt(BOUND)); - } -} -public class Reduce { - public static void main(String[] args) { - Stream.generate(Frobnitz::supply) - .limit(10) - .peek(System.out::println) - .reduce((fr0, fr1) -> fr0.size < 50 ? fr0 : fr1) - .ifPresent(System.out::println); - } -} -``` - -输出结果: - -``` -Frobnitz(58) -Frobnitz(55) -Frobnitz(93) -Frobnitz(61) -Frobnitz(61) -Frobnitz(29) -Frobnitz(68) -Frobnitz(0) -Frobnitz(22) -Frobnitz(7) -Frobnitz(29) -``` - -**Frobnitz** 包含了一个名为 `supply()` 的生成器;因为这个方法对于 `Supplier` 是签名兼容的,我们可以将其方法引用传递给 `Stream.generate()`(这种签名兼容性被称作结构一致性)。无“初始值”的 `reduce()`方法返回值是 **Optional** 类型。`Optional.ifPresent()` 只有在结果非空的时候才会调用 `Consumer` (`println` 方法可以被调用是因为 **Frobnitz** 可以通过 `toString()` 方法转换成 **String**)。 - -Lambda 表达式中的第一个参数 `fr0` 是上一次调用 `reduce()` 的结果。而第二个参数 `fr1` 是从流传递过来的值。 - -`reduce()` 中的 Lambda 表达式使用了三元表达式来获取结果,当其长度小于 50 的时候获取 `fr0` 否则获取序列中的下一个值 `fr1`。当取得第一个长度小于 50 的 `Frobnitz`,只要得到结果就会忽略其他。这是个非常奇怪的约束, 也确实让我们对 `reduce()` 有了更多的了解。 - - -### 匹配 - -- `allMatch(Predicate)` :如果流的每个元素根据提供的 **Predicate** 都返回 true 时,结果返回为 true。在第一个 false 时,则停止执行计算。 -- `anyMatch(Predicate)`:如果流中的任意一个元素根据提供的 **Predicate** 返回 true 时,结果返回为 true。在第一个 false 是停止执行计算。 -- `noneMatch(Predicate)`:如果流的每个元素根据提供的 **Predicate** 都返回 false 时,结果返回为 true。在第一个 true 时停止执行计算。 - -我们已经在 `Prime.java` 中看到了 `noneMatch()` 的示例;`allMatch()` 和 `anyMatch()` 的用法基本上是等同的。下面我们来探究一下短路行为。为了消除冗余代码,我们创建了 `show()`。首先我们必须知道如何统一地描述这三个匹配器的操作,然后再将其转换为 **Matcher** 接口。代码示例: - -```java -// streams/Matching.java -// Demonstrates short-circuiting of *Match() operations -import java.util.stream.*; -import java.util.function.*; -import static streams.RandInts.*; - -interface Matcher extends BiPredicate, Predicate> {} - -public class Matching { - static void show(Matcher match, int val) { - System.out.println( - match.test( - IntStream.rangeClosed(1, 9) - .boxed() - .peek(n -> System.out.format("%d ", n)), - n -> n < val)); - } - public static void main(String[] args) { - show(Stream::allMatch, 10); - show(Stream::allMatch, 4); - show(Stream::anyMatch, 2); - show(Stream::anyMatch, 0); - show(Stream::noneMatch, 5); - show(Stream::noneMatch, 0); - } -} -``` - -输出结果: - -``` -1 2 3 4 5 6 7 8 9 true -1 2 3 4 false -1 true -1 2 3 4 5 6 7 8 9 false -1 false -1 2 3 4 5 6 7 8 9 true -``` - -**BiPredicate** 是一个二元谓词,它只能接受两个参数且只返回 true 或者 false。它的第一个参数是我们要测试的流,第二个参数是一个谓词 **Predicate**。**Matcher** 适用于所有的 **Stream::\*Match** 方法,所以我们可以传递每一个到 `show()` 中。`match.test()` 的调用会被转换成 **Stream::\*Match** 函数的调用。 - -`show()` 获取两个参数,**Matcher** 匹配器和用于表示谓词测试 **n < val** 中最大值的 **val**。这个方法生成一个1-9之间的整数流。`peek()` 是用于向我们展示测试在短路之前的情况。从输出中可以看到每次都发生了短路。 - -### 查找 - -- `findFirst()`:返回第一个流元素的 **Optional**,如果流为空返回 **Optional.empty**。 -- `findAny(`:返回含有任意流元素的 **Optional**,如果流为空返回 **Optional.empty**。 - -代码示例: - -```java -// streams/SelectElement.java -import java.util.*; -import java.util.stream.*; -import static streams.RandInts.*; -public class SelectElement { - public static void main(String[] args) { - System.out.println(rands().findFirst().getAsInt()); - System.out.println( - rands().parallel().findFirst().getAsInt()); - System.out.println(rands().findAny().getAsInt()); - System.out.println( - rands().parallel().findAny().getAsInt()); - } -} -``` - -输出结果: - -``` -258 -258 -258 -242 -``` - -`findFirst()` 无论流是否为并行化的,总是会选择流中的第一个元素。对于非并行流,`findAny()`会选择流中的第一个元素(即使从定义上来看是选择任意元素)。在这个例子中,我们使用 `parallel()` 来并行流从而引入 `findAny()` 选择非第一个流元素的可能性。 - -如果必须选择流中最后一个元素,那就使用 `reduce()`。代码示例: - -```java -// streams/LastElement.java -import java.util.*; -import java.util.stream.*; -public class LastElement { - public static void main(String[] args) { - OptionalInt last = IntStream.range(10, 20) - .reduce((n1, n2) -> n2); - System.out.println(last.orElse(-1)); - // Non-numeric object: - Optional lastobj = - Stream.of("one", "two", "three") - .reduce((n1, n2) -> n2); - System.out.println( - lastobj.orElse("Nothing there!")); - } -} -``` - -输出结果: - -``` -19 -three -``` - -`reduce()` 的参数只是用最后一个元素替换了最后两个元素,最终只生成最后一个元素。如果为数字流,你必须使用相近的数字 **Optional** 类型( numeric optional type),否则使用 **Optional** 类型,就像上例中的 `Optional`。 - - - -### 信息 - -- `count()`:流中的元素个数。 -- `max(Comparator)`:根据所传入的 **Comparator** 所决定的“最大”元素。 -- `min(Comparator)`:根据所传入的 **Comparator** 所决定的“最小”元素。 - -**String** 类型有预设的 **Comparator** 实现。代码示例: - -```java -// streams/Informational.java -import java.util.stream.*; -import java.util.function.*; -public class Informational { - public static void - main(String[] args) throws Exception { - System.out.println( - FileToWords.stream("Cheese.dat").count()); - System.out.println( - FileToWords.stream("Cheese.dat") - .min(String.CASE_INSENSITIVE_ORDER) - .orElse("NONE")); - System.out.println( - FileToWords.stream("Cheese.dat") - .max(String.CASE_INSENSITIVE_ORDER) - .orElse("NONE")); - } -} -``` - -输出结果: - -``` -32 -a -you -``` - -`min()` 和 `max()` 的返回类型为 **Optional**,这需要我们使用 `orElse()`来解包。 - - -### 数字流信息 - -- `average()` :求取流元素平均值。 -- `max()` 和 `min()`:数值流操作无需 **Comparator**。 -- `sum()`:对所有流元素进行求和。 -- `summaryStatistics()`:生成可能有用的数据。目前并不太清楚这个方法存在的必要性,因为我们其实可以用更直接的方法获得需要的数据。 - -```java -// streams/NumericStreamInfo.java -import java.util.stream.*; -import static streams.RandInts.*; -public class NumericStreamInfo { - public static void main(String[] args) { - System.out.println(rands().average().getAsDouble()); - System.out.println(rands().max().getAsInt()); - System.out.println(rands().min().getAsInt()); - System.out.println(rands().sum()); - System.out.println(rands().summaryStatistics()); - } -} -``` - -输出结果: - -``` -507.94 -998 -8 -50794 -IntSummaryStatistics{count=100, sum=50794, min=8, average=507.940000, max=998} -``` - -上例操作对于 **LongStream** 和 **DoubleStream** 同样适用。 \ No newline at end of file diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java8\351\233\206\345\220\210\346\272\220\347\240\201\350\247\243\346\236\220-Hashtable\346\272\220\347\240\201\345\211\226\346\236\220.md" "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java8\351\233\206\345\220\210\346\272\220\347\240\201\350\247\243\346\236\220-Hashtable\346\272\220\347\240\201\345\211\226\346\236\220.md" index a4a829979c..47eb49416a 100644 --- "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java8\351\233\206\345\220\210\346\272\220\347\240\201\350\247\243\346\236\220-Hashtable\346\272\220\347\240\201\345\211\226\346\236\220.md" +++ "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java8\351\233\206\345\220\210\346\272\220\347\240\201\350\247\243\346\236\220-Hashtable\346\272\220\347\240\201\345\211\226\346\236\220.md" @@ -16,7 +16,7 @@ NOTE: This class is obsolete. New implementations should ``` 这个类的方法如下(全是抽象方法): -```java +``` public abstract class Dictionary { @@ -33,7 +33,7 @@ class Dictionary { ``` ##成员变量 -```java +``` //存储键值对的桶数组 private transient Entry[] table; @@ -52,7 +52,7 @@ class Dictionary { 成员变量跟HashMap基本类似,但是HashMap更加规范,HashMap内部还定义了一些常量,比如默认的负载因子,默认的容量,最大容量等等。 ##构造方法 -```java +``` //可指定初始容量和加载因子 public Hashtable(int initialCapacity, float loadFactor) { if (initialCapacity < 0) diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\344\270\216\347\272\277\347\250\213.md" "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\344\270\216\347\272\277\347\250\213.md" index 44310f6ea2..eb873095d4 100644 --- "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\344\270\216\347\272\277\347\250\213.md" +++ "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\344\270\216\347\272\277\347\250\213.md" @@ -1,16 +1,16 @@ -并发不一定要依赖多线程(如PHP的多进程并发),但在Java中谈论并发,大多数都与线程脱不开关系。 +并发不一定要依赖多线程(如PHP的多进程并发),但在Java中谈论并发,大多数都与线程脱不开关系 # 线程的实现 线程是CPU调度的基本单位。 Thread类与大部分的Java API有显著的差别,它的所有关键方法都是声明为Native的。 意味着这个方法没有使用或无法使用平台无关的手段来实现。 -# 内核线程(Kernel-Level Thread,KLT) +# 内核线程(Kernel-Lever Thread,KLT) 直接由操作系统内核(Kermel,下称内核)支持的线程 由内核来完成线程切换,内核通过操纵调度器(Sheduler) 对线程进行调度,并负责将线程的任务映射到各个处理器上。 每个内核线程可以视为内核的一个分身,这样OS就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核(Multi-Threads Kernel )。 -程序一般不会直接去使用KLT,而使用KLT的一种高级接口即轻量级进程(Light Weight Process,LWP),即我们通常意义上所讲的线程,由于每个LWP都由一个KLT支持,因此只有先支持KLT,才能有LWP。这1:1的关系称为`一对一的线程模型`。 -![KLT与LWP之间1:1的关系](https://img-blog.csdnimg.cn/img_convert/0e73bbc9abbaf006ede71ad793a18f7e.png) +程序一般不会直接去使用KLT,而使用KLT的一种高级接口即轻量级进程(Light Weight Process,LWP),即我们通常意义上所讲的线程,由于每个LWP都由一个KLT支持,因此只有先支持KLT,才能有LWP。这1:1的关系称为`一对一的线程模型`。 +![KLT与LWP之间1:1的关系](https://upload-images.jianshu.io/upload_images/4685968-497d1cfc86f6b084.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - 局限性 由于是基于KLT实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态和内核态中来回切换 其次,每个LWP都需要有一个KLT的支持,因此LWP要消耗一定的内核资源(如KLT的栈空间),因此一个系统支持LWP的数量是有限的 @@ -19,26 +19,23 @@ Thread类与大部分的Java API有显著的差别,它的所有关键方法都 # 用户线程混合轻量级进程 # Java线程的实现 -用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。 - -os提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。 +用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并 +且可以支持大规模的用户线程并发 +操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。 在这种混合模式中,用户线程与轻量级进程的数量比是不定的,即为N :M 的关系 -![用户线程与轮量级进程之间N :M 的关系](https://img-blog.csdnimg.cn/img_convert/357820c4bfd3f180b02e36d2fed85e13.png) -许多Unix 系列的os,如Solaris、HP-UX 等都提供了N: M 的线程模型实现。 -# Java 线程 +![用户线程与轮量级进程之间N :M 的关系](https://upload-images.jianshu.io/upload_images/4685968-b64e81020899c37e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +许多UN1X 系列的操作系统,如Solaris、HP-UX 等都提供了N: M 的线程模型实现。 +#.Java 线程 JDK 1.2 之前是基于称为“绿色线程”(Green-Threads )的用户线程实现 在JDK 1.2 中替换为基于操作系统原生线程模型来实现 因此,在目前的JDK 版本中,操作系统支持怎样的线程模型,在很大程度上决定了Java 虚拟机的线程是怎样映射的,这点在不同的平台上没有办法达成一致,虚拟机规范中也并未限定Java 线程需要使用哪种线程模型来实现。 线程模型只对线程的并发规模和操作成本产生影响,对Java 程序的编码和运行过程来说,这些差异都是透明的。 -对于Sun JDK 来说,它的Windows 版与Linux版都是使用一对一的线程模型实现的,一条Java线程就映射到一条轻量级进程之中,因为Windows 和Linux系统提供的线程模型就是一对一的。 +对于Siun JDK 来说,它的Windows 版与Linux版都是使用一对一的线程模型实现的,一条Java线程就映射到一条轻量级进程之中,因为Windows 和Linux系统提供的线程模型就是一对一的 而在Solaris 平台中,由于操作系统的线程特性可以同时支持一对一(通过Bound -Threads或Alternate Libthread实现)及多对多( 通过LWP/Thread Based Synchronization实现) 的线程模型,因此在Solaris 版的JDK 中也对应提供了两个平台专有的虚拟机参数: -```java --XX:+UseLWPSynchronization (默认值) --XX:+UseBoyndThreads -``` - -明确指定虚拟机使用哪种线程模型。 +Threaids或Alternate Libthread实现)及多对多( 通过LWP/Thread Based Synchronization +实现) 的线程模型,因此在Solaris 版的JDK 中也对应提供了两个平台专有的虚拟桃参数: +-XX:+UseLWPSynchronization (默认值) 和-XX:+UseBoyndThreads 来明确指定虚拟 +机使用哪种线程模型。 # Java线程调度 - 线程调度 系统为线程分配处理器使用权的过程,主要调度方式有两种 @@ -48,15 +45,17 @@ Threads或Alternate Libthread实现)及多对多( 通过LWP/Thread Based Synchro 使用协同式调度的多线程系统,线程执行时间由线程本身控制,线程把自己工作执行完后,要主动通知系统切换到另外一个线程上。 协同式多线程 - 最大好处 -实现简单,而且由于线程要把自己的事情干完后才进行线程切换,切换操作对线程自己是可知的,所以没有什么线程同步的问题 +实现简单,而且由于线程要把自己的事情干完后才进行线程切换,切换操作对线程白己是可知的,所以没有什么线程同步的问题 - 坏处也很明显 线程执行时间不可控制 -使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身决定,在这种实现线程调度的方式下,线程执行时间系统可控的。Java使用的线程调度方式就是抢占式调度。 +使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身决定,在这种实现线程调度的方式下,线程执行时间系统可控的 +Java使用的线程调度方式就是抢占式调度 -虽然Java线程调度是系统自动完成的,但是我们还是可以“建议”系统给某些线程多分配一点执行时间,可以通过设置线程优先级来完成。Java 语言一共设置了10个级别的线程优先级(Thread.MIN_PRIORITY 至Thread.MAX_PRIORITY ),在两个线程同时处于Ready 状态时,优先级越高的线程越容易被系统选择执行。 +虽然Java线程调度是系统自动完成的,但是我们还是可“建议”系统给某些线程多分配一点执行时间,可以通过设置线程优先级来完成。Java 语言一共设置了10个级别的线程优先级(Thread.MIN_PRIORITY 至Thread.MAX_PRIORITY ),在两个线程同时处于Ready 状态时,优先级越高的线程越容易被系统选择执行。 -Java 的线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于OS,虽然现在很多OS都提供线程优先级的概念,但是并不见得能与Java线程的优先级对应,如Solaris中有2147483648 (2^32 )种优先级,但Windows中就只有7种,比Java 线程优先级多的系统还好说,中间留下一点空位就可以了,但比Java线程优先级少的系统,就不得不出现几个优先级相同的情况了 +Java 的线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于OS,虽然现在很多OS都提供线程优先级的概念,但是并不见得能与Java线程的优先级对应,如Solaris中有2147483648 (232 )种优先级,但Windows中就只有7种,比Java 线程优先级多的系统还好说,中间留下一点空位就可以了,但比Java线程优先级少的系统,就不得不出现几个优先级相同的情况了 不仅仅是说在一些平台上不同的优先级实际会变得相同这一点,还有其他情况让我们不能太依赖优先级:优先级可能会被系统自行改变。 -例如,在Windows 系统中存在一个称为“优先级推进器”(Priority Boosting,当然它可以被关闭掉) 的功能,它的大致作用就是当系统发现一个线程执行得特别“勤奋努力”的话,可能会越过线程优先级去为它分配执行时间。因此,我们不能在程序中通过优先级完全准确地判断一组状态都为Ready 的线程将会先执行哪一个。 \ No newline at end of file +例如,在Windows 系统中存在一个称为“优先级推进器”(Priority Boosting,当然它可以被 +关闭掉) 的功能,它的大致作用就是当系统发现一个线程执行得特别“勤奋努力”的话,可能会越过线程优先级去为它分配执行时间。因此,我们不能在程序中通过优先级完全准确地判断一组状态都为Ready 的线程将会先执行哪一个 diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\345\212\250\346\200\201\344\273\243\347\220\206\346\250\241\345\274\217\344\271\213JDK\345\222\214Cglib.md" "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\345\212\250\346\200\201\344\273\243\347\220\206\346\250\241\345\274\217jdk\345\222\214cglib.md" similarity index 100% rename from "JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\345\212\250\346\200\201\344\273\243\347\220\206\346\250\241\345\274\217\344\271\213JDK\345\222\214Cglib.md" rename to "JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\345\212\250\346\200\201\344\273\243\347\220\206\346\250\241\345\274\217jdk\345\222\214cglib.md" diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\345\217\215\345\260\204\351\201\207\345\210\260\346\263\233\345\236\213\346\227\266\347\232\204bug.md" "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\345\217\215\345\260\204\351\201\207\345\210\260\346\263\233\345\236\213\346\227\266\347\232\204bug.md" deleted file mode 100644 index c16c89e15f..0000000000 --- "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\345\217\215\345\260\204\351\201\207\345\210\260\346\263\233\345\236\213\346\227\266\347\232\204bug.md" +++ /dev/null @@ -1,125 +0,0 @@ -# 1 当反射遇见重载 -重载**level**方法,入参分别是**int**和**Integer**。 -![](https://img-blog.csdnimg.cn/2021063020523870.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70)若不使用反射,选用哪个重载方法很清晰,比如: -- 传入**666**就走int参数重载 -- 传入`Integer.valueOf(“666”)`走Integer重载 - -那反射调用方法也是根据入参类型确定使用哪个重载方法吗? -使用`getDeclaredMethod`获取 `grade`方法,然后传入`Integer.valueOf(“36”)` -![](https://img-blog.csdnimg.cn/20210630205520594.png) -结果是: -![](https://img-blog.csdnimg.cn/20210630205642767.png) -因为反射进行方法调用是通过 -## 方法签名 -来确定方法。本例的`getDeclaredMethod`传入的参数类型`Integer.TYPE`其实代表`int`。 -![](https://img-blog.csdnimg.cn/2021063020590337.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -所以不管传包装类型还是基本类型,最终都是调用int入参重载方法。 - -将`Integer.TYPE`改为`Integer.class`,则实际执行的参数类型就是Integer了。且无论传包装类型还是基本类型,最终都调用Integer入参重载方法。 - -综上,反射调用方法,是以反射获取方法时传入的方法名和参数类型来确定调用的方法。 - -# 2 泛型的类型擦除 -泛型允许SE使用类型参数替代精确类型,实例化时再指明具体类型。利于代码复用,将一套代码应用到多种数据类型。 - -泛型的类型检测,可以在编译时检查很多泛型编码错误。但由于历史兼容性而妥协的泛型类型擦除方案,在运行时还有很多坑。 - -## 案例 -现在期望在类的字段内容变动时记录日志,于是SE想到定义一个泛型父类,并在父类中定义一个统一的日志记录方法,子类可继承该方法。上线后总有日志重复记录。 - -- 父类 -![](https://img-blog.csdnimg.cn/20210701102521658.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 子类1 -![](https://img-blog.csdnimg.cn/20210701102727240.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 通过反射调用子类方法: -![](https://img-blog.csdnimg.cn/20210701103212354.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -虽Base.value正确设置为了JavaEdge,但父类setValue调用了两次,计数器显示2 -![](https://img-blog.csdnimg.cn/20210701103441331.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -两次调用Base.setValue,是因为getMethods找到了两个`setValue`: -![](https://img-blog.csdnimg.cn/20210701104024581.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -![](https://img-blog.csdnimg.cn/20210701103932953.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -### 子类重写父类方法失败原因 -- 子类未指定String泛型参数,父类的泛型方法`setValue(T value)`泛型擦除后是`setValue(Object value)`,于是子类入参String的`setValue`被当作新方法 -- 子类的`setValue`未加`@Override`注解,编译器未能检测到重写失败 -![](https://img-blog.csdnimg.cn/20210701104847761.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -有的同学会认为是因为反射API使用错误导致而非重写失败: -- `getMethods` -得到**当前类和父类**的所有`public`方法 -- `getDeclaredMethods` -获得**当前类**所有的public、protected、package和private方法 - -于是用`getDeclaredMethods`替换`getMethods`: -![](https://img-blog.csdnimg.cn/20210701110017589.png)![](https://img-blog.csdnimg.cn/20210701110341844.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -虽然这样做可以规避重复记录日志,但未解决子类重写父类方法失败的问题 -- 使用Sub1时还是会发现有俩个`setValue` -![](https://img-blog.csdnimg.cn/20210701111541648.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -于是,终于明白还得重新实现**Sub2**,继承Base时将**String**作为泛型T类型,并使用 **@Override** 注解 **setValue** -![](https://img-blog.csdnimg.cn/20210701131719235.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -- 但还是出现重复日志 -![](https://img-blog.csdnimg.cn/20210701131952964.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -Sub2的`setValue`竟然调用了两次,难道是JDK反射有Bug!`getDeclaredMethods`查找到的方法肯定来自`Sub2`;而且Sub2看起来也就一个setValue,怎么会重复? - -调试发现,Child2类其实有俩`setValue`:入参分别是String、Object。 -![](https://img-blog.csdnimg.cn/20210701133219777.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -这就是因为**泛型类型擦除**。 - -## 反射下的泛型擦除“天坑” -Java泛型类型在编译后被擦除为**Object**。子类虽指定父类泛型T类型是**String**,但编译后T会被擦除成为Object,所以父类`setValue`入参是**Object**,value也是**Object**。 -若**Sub2.setValue**想重写父类,那入参也须为**Object**。所以,编译器会为我们生成一个桥接方法。 -Sub2类的class字节码: -```bash -➜ genericandinheritance git:(master) ✗ javap -c Sub2.class -Compiled from "GenericAndInheritanceApplication.java" -class com.javaedge.oop.genericandinheritance.Sub2 extends com.javaedge.oop.genericandinheritance.Base { - com.javaedge.oop.genericandinheritance.Sub2(); - Code: - 0: aload_0 - 1: invokespecial #1 // Method com/javaedge/oop/genericandinheritance/Base."":()V - 4: return - - public void setValue(java.lang.String); - Code: - 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; - 3: ldc #3 // String call Sub2.setValue - 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V - 8: aload_0 - 9: aload_1 - 10: invokespecial #5 // Method com/javaedge/oop/genericandinheritance/Base.setValue:(Ljava/lang/Object;)V - 13: return - - public void setValue(java.lang.Object); - Code: - 0: aload_0 - 1: aload_1 - 2: checkcast #6 // class java/lang/String - // 入参为Object的setValue在内部调用了入参为String的setValue方法 - 5: invokevirtual #7 // Method setValue:(Ljava/lang/String;)V - 8: return -} -``` -若编译器未帮我们实现该桥接方法,则Sub2重写的是父类泛型类型擦除后、入参是Object的setValue。这两个方法的参数,一个String一个Object,显然不符Java重写。 - -入参为Object的桥接方法上标记了`public synthetic bridge`: -- synthetic代表由编译器生成的不可见代码 -- bridge代表这是泛型类型擦除后生成的桥接代码 -![](https://img-blog.csdnimg.cn/20210701135257552.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -## 修正 -知道了桥接方法的存在,现在就该知道如何修正代码了。 -- 通过**getDeclaredMethods**获取所有方法后,还得加上非isBridge这个过滤条件: -![](https://img-blog.csdnimg.cn/20210701143227136.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 结果 -![](https://img-blog.csdnimg.cn/20210701143616524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -> 参考 -> - https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/index.html -> - https://docs.oracle.com/javase/tutorial/reflect/index.html -> - https://docs.oracle.com/javase/8/docs/technotes/guides/language/annotations.html -> - https://docs.oracle.com/javase/tutorial/java/annotations/index.html -> - https://docs.oracle.com/javase/8/docs/technotes/guides/language/generics.html -> - https://docs.oracle.com/javase/tutorial/java/generics/index.html \ No newline at end of file diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\346\227\245\345\277\227\347\272\247\345\210\253\357\274\214\351\207\215\345\244\215\350\256\260\345\275\225\343\200\201\344\270\242\346\227\245\345\277\227\351\227\256\351\242\230.md" "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\346\227\245\345\277\227\347\272\247\345\210\253\357\274\214\351\207\215\345\244\215\350\256\260\345\275\225\343\200\201\344\270\242\346\227\245\345\277\227\351\227\256\351\242\230.md" deleted file mode 100644 index 5da763f8e9..0000000000 --- "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\346\227\245\345\277\227\347\272\247\345\210\253\357\274\214\351\207\215\345\244\215\350\256\260\345\275\225\343\200\201\344\270\242\346\227\245\345\277\227\351\227\256\351\242\230.md" +++ /dev/null @@ -1,243 +0,0 @@ -# 1 SLF4J -## 日志行业的现状 -- 框架繁 -不同类库可能使用不同日志框架,兼容难,无法接入统一日志,让运维很头疼! -- 配置复杂 -由于配置文件一般是 xml 文件,内容繁杂!很多人喜欢从其他项目或网上闭眼copy! -- 随意度高 -因为不会直接导致代码 bug,测试人员也难发现问题,开发就没仔细考虑日志内容获取的性能开销,随意选用日志级别! - -Logback、Log4j、Log4j2、commons-logging及java.util.logging等,都是Java体系的日志框架。 -不同的类库,还可能选择使用不同的日志框架,导致日志统一管理困难。 - -- SLF4J(Simple Logging Facade For Java)就为解决该问题而生 -![](https://img-blog.csdnimg.cn/20201205201659631.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -- 提供统一的日志门面API -图中紫色部分,实现中立的日志记录API -- 桥接功能 -蓝色部分,把各种日志框架API桥接到SLF4J API。这样即使你的程序使用了各种日志API记录日志,最终都可桥接到SLF4J门面API -- 适配功能 -红色部分,绑定SLF4J API和实际的日志框架(灰色部分) - -SLF4J只是日志标准,还是需要实际日志框架。日志框架本身未实现SLF4J API,所以需要有个前置转换。 -Logback本身就按SLF4J API标准实现,所以无需绑定模块做转换。 - -虽然可用`log4j-over-slf4j`实现Log4j桥接到SLF4J,也可使用`slf4j-log4j12`实现SLF4J适配到Log4j,也把它们画到了一列,但是它不能同时使用它们,否则就会产生死循环。jcl和jul同理。 - -虽然图中有4个灰色的日志实现框架,但业务开发使用最多的还是Logback和Log4j,都是同一人开发的。Logback可认为是Log4j改进版,更推荐使用,已是社会主流。 - -Spring Boot的日志框架也是Logback。那为什么我们没有手动引入Logback包,就可直接使用Logback? - -spring-boot-starter模块依赖**spring-boot-starter-logging**模块,而 -**spring-boot-starter-logging**自动引入**logback-classic**(包含SLF4J和Logback日志框架)和SLF4J的一些适配器。 -# 2 异步日志就肯定能提高性能? -如何避免日志记录成为系统性能瓶颈呢? -这关系到磁盘(比如机械磁盘)IO性能较差、日志量又很大的情况下,如何记录日志。 - -## 2.1 案例 -定义如下的日志配置,一共有两个Appender: -- **FILE**是一个FileAppender,用于记录所有的日志 -- **CONSOLE**是一个ConsoleAppender,用于记录带有time标记的日志 -![](https://img-blog.csdnimg.cn/20201205223014732.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -把大量日志输出到文件中,日志文件会非常大,若性能测试结果也混在其中,就很难找到那条日志了。 -所以,这里使用EvaluatorFilter对日志按照标记进行过滤,并将过滤出的日志单独输出到控制台。该案例中给输出测试结果的那条日志上做了time标记。 - -> **配合使用标记和EvaluatorFilter,可实现日志的按标签过滤**。 - -- 测试代码:实现记录指定次数的大日志,每条日志包含1MB字节的模拟数据,最后记录一条以time为标记的方法执行耗时日志:![](https://img-blog.csdnimg.cn/20201205223508352.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -执行程序后发现,记录1000次日志和10000次日志的调用耗时,分别是5.1s和39s -![](https://img-blog.csdnimg.cn/2020120522373754.png)![](https://img-blog.csdnimg.cn/20201205224030888.png) -对只记录文件日志的代码,这耗时过长了。 -## 2.2 源码解析 -FileAppender继承自OutputStreamAppender -![](https://img-blog.csdnimg.cn/20201205224309140.png) -在追加日志时,是直接把日志写入OutputStream中,属**同步记录日志** -![](https://img-blog.csdnimg.cn/20201206132744194.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -所以日志大量写入才会旷日持久。如何才能实现大量日志写入时,不会过多影响业务逻辑执行耗时而影响吞吐量呢? -## 2.3 AsyncAppender -使用Logback的**AsyncAppender**,即可实现异步日志记录。 - -**AsyncAppender**类似装饰模式,在不改变类原有基本功能情况下,为其增添新功能。这便可把**AsyncAppender**附加在其他**Appender**,将其变为异步。 - -定义一个异步Appender ASYNCFILE,包装之前的同步文件日志记录的FileAppender, 即可实现异步记录日志到文件 -![](https://img-blog.csdnimg.cn/20201206133807409.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -- 记录1000次日志和10000次日志的调用耗时,分别是537ms和1019ms -![](https://img-blog.csdnimg.cn/20201206133959275.png)![](https://img-blog.csdnimg.cn/2020120613391870.png) -异步日志真的如此高性能?并不,因为它并没有记录下所有日志。 -# 3 AsyncAppender异步日志的天坑 -- 记录异步日志撑爆内存 -- 记录异步日志出现日志丢失 -- 记录异步日志出现阻塞。 - -## 3.1 案例 -模拟个慢日志记录场景: -首先,自定义一个继承自**ConsoleAppender**的**MySlowAppender**,作为记录到控制台的输出器,写入日志时睡1s。 -![](https://img-blog.csdnimg.cn/20201206134635409.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -- 配置文件中使用**AsyncAppender**,将**MySlowAppender**包装为异步日志记录 -![](https://img-blog.csdnimg.cn/20201206141303471.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -- 测试代码 -![](https://img-blog.csdnimg.cn/20201206135344681.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -- 耗时很短但出现日志丢失:要记录1000条日志,最终控制台只能搜索到215条日志,而且日志行号变问号。 -![](https://img-blog.csdnimg.cn/20201206141514155.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -- 原因分析 -**AsyncAppender**提供了一些配置参数,而当前没用对。 - -### 源码解析 -- includeCallerData -默认false:方法行号、方法名等信息不显示 -- queueSize -控制阻塞队列大小,使用的ArrayBlockingQueue阻塞队列,默认容量256:内存中最多保存256条日志 -- discardingThreshold -丢弃日志的阈值,为防止队列满后发生阻塞。默认`队列剩余容量 < 队列长度的20%`,就会丢弃TRACE、DEBUG和INFO级日志 -- neverBlock -控制队列满时,加入的数据是否直接丢弃,不会阻塞等待,默认是false - - 队列满时:offer不阻塞,而put会阻塞 - - neverBlock为true时,使用offer -```java -public class AsyncAppender extends AsyncAppenderBase { - // 是否收集调用方数据 - boolean includeCallerData = false; - protected boolean isDiscardable(ILoggingEvent event) { - Level level = event.getLevel(); - // 丢弃 ≤ INFO级日志 - return level.toInt() <= Level.INFO_INT; - } - protected void preprocess(ILoggingEvent eventObject) { - eventObject.prepareForDeferredProcessing(); - if (includeCallerData) - eventObject.getCallerData(); - } -} -public class AsyncAppenderBase extends UnsynchronizedAppenderBase implements AppenderAttachable { - - // 阻塞队列:实现异步日志的核心 - BlockingQueue blockingQueue; - // 默认队列大小 - public static final int DEFAULT_QUEUE_SIZE = 256; - int queueSize = DEFAULT_QUEUE_SIZE; - static final int UNDEFINED = -1; - int discardingThreshold = UNDEFINED; - // 当队列满时:加入数据时是否直接丢弃,不会阻塞等待 - boolean neverBlock = false; - - @Override - public void start() { - ... - blockingQueue = new ArrayBlockingQueue(queueSize); - if (discardingThreshold == UNDEFINED) - //默认丢弃阈值是队列剩余量低于队列长度的20%,参见isQueueBelowDiscardingThreshold方法 - discardingThreshold = queueSize / 5; - ... - } - - @Override - protected void append(E eventObject) { - if (isQueueBelowDiscardingThreshold() && isDiscardable(eventObject)) { //判断是否可以丢数据 - return; - } - preprocess(eventObject); - put(eventObject); - } - - private boolean isQueueBelowDiscardingThreshold() { - return (blockingQueue.remainingCapacity() < discardingThreshold); - } - - private void put(E eventObject) { - if (neverBlock) { //根据neverBlock决定使用不阻塞的offer还是阻塞的put方法 - blockingQueue.offer(eventObject); - } else { - putUninterruptibly(eventObject); - } - } - //以阻塞方式添加数据到队列 - private void putUninterruptibly(E eventObject) { - boolean interrupted = false; - try { - while (true) { - try { - blockingQueue.put(eventObject); - break; - } catch (InterruptedException e) { - interrupted = true; - } - } - } finally { - if (interrupted) { - Thread.currentThread().interrupt(); - } - } - } -} -``` - -默认队列大小256,达到80%后开始丢弃<=INFO级日志后,即可理解日志中为什么只有两百多条INFO日志了。 -### queueSize 过大 -可能导致**OOM** -### queueSize 较小 -默认值256就已经算很小了,且**discardingThreshold**设置为大于0(或为默认值),队列剩余容量少于**discardingThreshold**的配置就会丢弃<=INFO日志。这里的坑点有两个: -1. 因为**discardingThreshold**,所以设置**queueSize**时容易踩坑。 -比如本案例最大日志并发1000,即便置**queueSize**为1000,同样会导致日志丢失 -2. **discardingThreshold**参数容易有歧义,它`不是百分比,而是日志条数`。对于总容量10000队列,若希望队列剩余容量少于1000时丢弃,需配置为1000 -### neverBlock 默认false -意味总可能会出现阻塞。 -- 若**discardingThreshold = 0**,那么队列满时再有日志写入就会阻塞 -- 若**discardingThreshold != 0**,也只丢弃≤INFO级日志,出现大量错误日志时,还是会阻塞 - -queueSize、discardingThreshold和neverBlock三参密不可分,务必按业务需求设置: -- 若优先绝对性能,设置`neverBlock = true`,永不阻塞 -- 若优先绝不丢数据,设置`discardingThreshold = 0`,即使≤INFO级日志也不会丢。但最好把queueSize设置大一点,毕竟默认的queueSize显然太小,太容易阻塞。 -- 若兼顾,可丢弃不重要日志,把**queueSize**设置大点,再设置合理的**discardingThreshold** - -以上日志配置最常见两个误区 - -再看日志记录本身的误区。 - -# 4 如何选择日志级别? - -> 使用{}占位符,就不用判断log level了吗? - -据不知名网友说道:SLF4J的{}占位符语法,到真正记录日志时才会获取实际参数,因此解决了日志数据获取的性能问题。 -**是真的吗?** -![](https://img-blog.csdnimg.cn/596e199b79c1443a89723becb9809f8b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_12,color_FFFFFF,t_70,g_se,x_16) - -- 验证代码:返回结果耗时1s -![](https://img-blog.csdnimg.cn/2020120619541620.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -若记录DEBUG日志,并设置只记录>=INFO级日志,程序是否也会耗时1s? -三种方法测试: -- 拼接字符串方式记录slowString -- 使用占位符方式记录slowString -- 先判断日志级别是否启用DEBUG。 - -![](https://img-blog.csdnimg.cn/20201206200002878.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20201206200446663.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -前俩方式都调用slowString,所以都耗时1s。且方式二就是使用占位符记录slowString,这种方式虽允许传Object,不显式拼接String,但也只是延迟(若日志不记录那就是省去)**日志参数对象.toString()**和**字符串拼接**的耗时。 - -本案例除非事先判断日志级别,否则必调用slowString。所以使用`{}占位符`不能通过延迟参数值获取,来解决日志数据获取的性能问题。 - -除事先判断日志级别,还可通过lambda表达式延迟参数内容获取。但SLF4J的API还不支持lambda,因此需使用Log4j2日志API,把**Lombok的@Slf4j注解**替换为**@Log4j2**注解,即可提供lambda表达式参数的方法: -![](https://img-blog.csdnimg.cn/20201206201751860.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -这样调用debug,签名**Supplier**,参数就会延迟到真正需要记录日志时再获取: -![](https://img-blog.csdnimg.cn/20201206202233179.png) -![](https://img-blog.csdnimg.cn/20201206202311160.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20201206202346847.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20201206202419598.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -所以debug4并不会调用slowString方法 -![](https://img-blog.csdnimg.cn/20201206203249411.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -只是换成**Log4j2 API**,真正的日志记录还是走的**Logback**,这就是**SLF4J**适配的好处。 - -# 总结 -- SLF4J统一了Java日志框架。在使用SLF4J时,要理清楚其桥接API和绑定。若程序启动时出现SLF4J错误提示,可能是配置问题,可使用Maven的`dependency:tree`命令梳理依赖关系。 -- 异步日志解决性能问题,是用空间换时间。但空间毕竟有限,当空间满,要考虑阻塞等待or丢弃日志。若更希望不丢弃重要日志,那么选择阻塞等待;如果更希望程序不要因为日志记录而阻塞,那么就需要丢弃日志。 -- 日志框架提供的参数化记录方式不能完全取代日志级别的判断。若日志量很大,获取日志参数代价也很大,就要判断日志级别,避免不记录日志也要耗时获取日志参数! \ No newline at end of file diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\347\272\277\347\250\213\347\212\266\346\200\201.md" "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\347\272\277\347\250\213\347\212\266\346\200\201.md" index c3304c0ee1..603e47cbf7 100644 --- "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\347\272\277\347\250\213\347\212\266\346\200\201.md" +++ "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\347\272\277\347\250\213\347\212\266\346\200\201.md" @@ -207,7 +207,14 @@ while(true) { 2. 当一个线程1被另外一个线程2唤醒时,1线程进入锁池状态,去争夺对象锁。 3. 锁池是在同步的环境下才有的概念,`一个对象对应一个锁池` - +# **几个方法的比较** +- Thread.sleep(long millis) +一定是当前线程调用此方法,当前线程进入阻塞,不释放对象锁,millis后线程自动苏醒进入可运行态。 +作用:给其它线程执行机会的最佳方式。 +- Thread.yield() +一定是当前线程调用此方法,当前线程放弃获取的cpu时间片,由运行状态变会可运行状态,让OS再次选择线程。 +作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。 +3. t.join()/t.join(long millis),当前线程里调用其它线程1的join方法,当前线程阻塞,但不释放对象锁,直到线程1执行完毕或者millis时间到,当前线程进入可运行状态。 4. obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到自动唤醒。 5. obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。 diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\350\257\255\346\263\225\347\263\226.md" "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\350\257\255\346\263\225\347\263\226.md" new file mode 100644 index 0000000000..dacb3b9a0b --- /dev/null +++ "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\350\257\255\346\263\225\347\263\226.md" @@ -0,0 +1,27 @@ +#1 泛型与类型擦除 +泛型是JDK 1.5的一项新增特性,它的本质是参数化类型(Parametersized Type) 的应用,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类,接口和方法的创建中, 分別称为泛型类、泛型接口和泛型方法。 + +在Java语言处于还没有出现泛型的版本时。只能通过Object 是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。例如,在哈希表的存取中,JDK 1.5之前使用HashMap的get() 方法,返回值就是个0bject。由于Java语言里面所有的类型都维承于java.lang.Object 所以Object转型成任何对象都是有可能的。但是也因为有无限的可能性。就只有程序员和运行期的虚拟机才知道这个Objet到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功。如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的风险就会转嫁到程予运行期之中。 + +Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type) ,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java来说AraylistAralist就是同一个类。所以泛型是Java语言的一颗语法糖Java称为类型擦除,基于这种方法实现的泛型称为伪泛型。 + +![泛型擦除前的例子](https://upload-images.jianshu.io/upload_images/4685968-2c93db4ccfaf1fc4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +把这段Java代码编译成Class文件,然后再用字节码反编译后,將会发现泛型都不见了,又变回了Java泛型出现之前的写法,泛型类型都变回了原类型.如 +![](https://upload-images.jianshu.io/upload_images/4685968-73b7f4720749ecbb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +通过擦除來实现泛型丧失了一些泛型思想应有的优雅 +![当泛型遇见重载1](https://upload-images.jianshu.io/upload_images/4685968-38a718d9584c5ed6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +不能被编译的,因为参数List和List编译之后都被擦除了。变成了一样的原生类型List,擦除动作导致这两种方法的特征签名变得一模一样。初步看来,无法重载的原因已经找到了,但真的就如此吗? 只能说,泛型擦除成相同的原生类型只是无法重载的部分原因 + +![当泛型遇见置载2](https://upload-images.jianshu.io/upload_images/4685968-f6c1eeaed390cd9a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +由于Java泛型的引入,各种场景(虚拟机解析,反射等> 下的方法调用都可能对原有基础产生影响,如在泛型类中如何获取传入的参数化类型等。因此,JCP组织对虚拟机规范做出了相应的修改,引人了诺如Signature,LocalVariableTypeTable 等新的属性用于解决伴随泛型而来的参数类型的识别问题,Signature 是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。修改后的虚拟机规范裂水所有能识别49.0以上版本的Class +文件的虚拟机都要能正确地识别Signature参数. + +从Signature属性的出现我们还可以得出结论,所谓的擦除,仅仅是对方法的Code属性中的宇节码进行擦除,实际上元数据还是保留了泛型信息,这也是我们能通过反射取得参数化类型的根本依据。 +![自动装箱: 拆箱与遍历循环](https://upload-images.jianshu.io/upload_images/4685968-2866b12ca5564476.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +![自动装箱: 拆箱与遍历循环编译后](https://upload-images.jianshu.io/upload_images/4685968-0433b37de73e7230.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +遍历循环则把代码还原成了迭代器的实现,这也是为何遍历循环需要被遍历的类实现Iterable接口的原因。最后再看看变长参数,它在调用的时候变成了一个数组类型的参数,在变长参数出现之前,程序员就是使用数组来完成类似功能的。 +![](https://upload-images.jianshu.io/upload_images/4685968-7fe6b94f0458dfda.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + + diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\350\257\255\346\263\225\347\263\226\344\271\213\346\263\233\345\236\213\344\270\216\347\261\273\345\236\213\346\223\246\351\231\244.md" "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\350\257\255\346\263\225\347\263\226\344\271\213\346\263\233\345\236\213\344\270\216\347\261\273\345\236\213\346\223\246\351\231\244.md" deleted file mode 100644 index d1adafb052..0000000000 --- "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\350\257\255\346\263\225\347\263\226\344\271\213\346\263\233\345\236\213\344\270\216\347\261\273\345\236\213\346\223\246\351\231\244.md" +++ /dev/null @@ -1,38 +0,0 @@ -# 1 泛型与类型擦除 -泛型,JDK 1.5新特性,本质是参数化类型(Parametersized Type) 的应用,即所操作的数据类型被指定为一个参数。这种参数类型可用在: -- 类 -- 接口 -- 方法 - -的创建中, 分别称为: -- 泛型类 -- 泛型接口 -- 泛型方法 - -在Java还没有泛型的版本时。只能通过: -1. Object 是所有类型的父类 -2. 类型强制转换 - -两个特性协作实现类型泛化。例如,在哈希表的存取中,JDK 1.5之前使用HashMap的get() 方法,返回值就是个Object。由于Java语言里面所有的类型都维承于**java.lang.Object**,所以Object转型成任何对象都有可能。但也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Objet到底是个什么类型的对象。 -编译期间,编译器无法检查该Object的强制转型是否成功。若仅仅依赖程序员去保障正确性,许多ClassCastException的风险就会延迟到程序运行期。 - -Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type) ,并在相应地方插入强制转换代码。 -因此,对运行期的Java来说`Araylist`、`Aralist`是同一个类。所以泛型是Java语言的一颗语法糖Java称为类型擦除,基于这种方法实现的泛型称为伪泛型。 -- 泛型擦除前的例子 -![](https://img-blog.csdnimg.cn/img_convert/d347cb20042fbdffec7af32a5cef72b4.png) -把这段Java代码编译成Class文件,然后再用字节码反编译后,將会发现泛型都不见了,又变回了Java泛型出现之前的写法,泛型类型都变回了原类型。如: -![](https://img-blog.csdnimg.cn/img_convert/9f9d591480dae3af68c7017b6a39419d.png) -通过擦除实现泛型,丧失了一些泛型思想应有的优雅 -- 当泛型遇见重载1 -![](https://img-blog.csdnimg.cn/img_convert/c1980fb6370cdc0e40467bc8ed51fbf3.png) -不能被编译的,因为参数`List`和`List`编译之后都被擦除了。变成了一样的原生类型`List`,擦除动作导致这两种方法的特征签名变得一模一样。初步看来,无法重载的原因已经找到了,但真的就如此吗? 只能说,泛型擦除成相同的原生类型只是无法重载的部分原因 -- 当泛型遇见置载2 -![](https://img-blog.csdnimg.cn/img_convert/7122295ac6c3431e2b732e6668008da6.png) -由于Java泛型的引入,各种场景(虚拟机解析、反射等)下的方法调用都可能对原有基础产生影响,如在泛型类中如何获取传入的参数化类型等。因此,JCP组织对虚拟机规范做出了相应的修改,引入了诸如**Signature、LocalVariableTypeTable** 等新的属性用于解决伴随泛型而来的参数类型的识别问题,Signature 是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。修改后的虚拟机规范要求所有能识别49.0以上版本的Class文件的虚拟机都要能正确地识别Signature参数。 - -从Signature属性的出现我们还可以得出结论,所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据还是保留了泛型信息,这也是我们能通过反射取得参数化类型的根本依据。 -- 自动装箱: 拆箱与遍历循环 -![](https://img-blog.csdnimg.cn/img_convert/ac763ea9a774a914879ccd27cbdd0498.png) -- 自动装箱: 拆箱与遍历循环编译后![](https://img-blog.csdnimg.cn/img_convert/51a73df3f1ed22fef870234c2ae178a6.png) -遍历循环则把代码还原成了迭代器的实现,这也是为何遍历循环需要被遍历的类实现Iterable接口的原因。最后再看看变长参数,它在调用的时候变成了一个数组类型的参数,在变长参数出现之前,程序员就是使用数组来完成类似功能的。 -![](https://img-blog.csdnimg.cn/img_convert/8909a05f4b0a56f95cca330f36147b52.png) \ No newline at end of file diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java8\347\232\204NIO\346\226\260\346\226\207\344\273\266IO\345\210\260\345\272\225\346\234\211\345\244\232\345\245\275\347\224\250\357\274\237.md" "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/nio.file\346\226\260IO API.md" similarity index 100% rename from "JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java8\347\232\204NIO\346\226\260\346\226\207\344\273\266IO\345\210\260\345\272\225\346\234\211\345\244\232\345\245\275\347\224\250\357\274\237.md" rename to "JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/nio.file\346\226\260IO API.md" diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/\344\270\200\346\226\207\346\220\236\346\207\202Java\347\232\204SPI\346\234\272\345\210\266.md" "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/\344\270\200\346\226\207\346\220\236\346\207\202Java\347\232\204SPI\346\234\272\345\210\266.md" deleted file mode 100644 index 1c69b61c1e..0000000000 --- "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/\344\270\200\346\226\207\346\220\236\346\207\202Java\347\232\204SPI\346\234\272\345\210\266.md" +++ /dev/null @@ -1,63 +0,0 @@ -# 1 简介 -SPI,Service Provider Interface,一种服务发现机制。 -![](https://img-blog.csdnimg.cn/7921efaa5683447cbb1bc6cf351c4332.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -有了SPI,即可实现服务接口与服务实现的解耦: -- 服务提供者(如 springboot starter)提供出 SPI 接口。身为服务提供者,在你无法形成绝对规范强制时,适度"放权" 比较明智,适当让客户端去自定义实现 -- 客户端(普通的 springboot 项目)即可通过本地注册的形式,将实现类注册到服务端,轻松实现可插拔 - -## 缺点 -- 不能按需加载。虽然 ServiceLoader 做了延迟加载,但是只能通过遍历的方式全部获取。如果其中某些实现类很耗时,而且你也不需要加载它,那么就形成了资源浪费 -- 获取某个实现类的方式不够灵活,只能通过迭代器的形式获取 - -> Dubbo SPI 实现方式对以上两点进行了业务优化。 - -# 源码 -![](https://img-blog.csdnimg.cn/339efe7e74764bbc91f8ea037c3f69a6.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -应用程序通过迭代器接口获取对象实例,这里首先会判断 providers 对象中是否有实例对象: -- 有实例,那么就返回 -- 没有,执行类的装载步骤,具体类装载实现如下: - -LazyIterator#hasNextService 读取 META-INF/services 下的配置文件,获得所有能被实例化的类的名称,并完成 SPI 配置文件的解析 - - -LazyIterator#nextService 负责实例化 hasNextService() 读到的实现类,并将实例化后的对象存放到 providers 集合中缓存 - -# 使用 -如某接口有3个实现类,那系统运行时,该接口到底选择哪个实现类呢? -这时就需要SPI,**根据指定或默认配置,找到对应实现类,加载进来,然后使用该实现类实例**。 - -如下系统运行时,加载配置,用实现A2实例化一个对象来提供服务: -![](https://img-blog.csdnimg.cn/20201220141747102.png) -再如,你要通过jar包给某个接口提供实现,就在自己jar包的`META-INF/services/`目录下放一个接口同名文件,指定接口的实现是自己这个jar包里的某类即可: -![](https://img-blog.csdnimg.cn/20201220142131599.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -别人用这个接口,然后用你的jar包,就会在运行时通过你的jar包指定文件找到这个接口该用哪个实现类。这是JDK内置提供的功能。 - -> 我就不定义在 META-INF/services 下面行不行?就想定义在别的地方可以吗? - -No!JDK 已经规定好配置路径,你若随便定义,类加载器可就不知道去哪里加载了 -![](https://img-blog.csdnimg.cn/bba23763598a4d19a80616e85623c7c9.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -假设你有个工程P,有个接口A,A在P无实现类,系统运行时怎么给A选实现类呢? -可以自己搞个jar包,`META-INF/services/`,放上一个文件,文件名即接口名,接口A的实现类=`com.javaedge.service.实现类A2`。 -让P来依赖你的jar包,等系统运行时,P跑起来了,对于接口A,就会扫描依赖的jar包,看看有没有`META-INF/services`文件夹: -- 有,再看看有无名为接口A的文件: - - 有,在里面查找指定的接口A的实现是你的jar包里的哪个类即可 -# 适用场景 -## 插件扩展 -比如你开发了一个开源框架,若你想让别人自己写个插件,安排到你的开源框架里中,扩展功能时。 - -如JDBC。Java定义了一套JDBC的接口,但并未提供具体实现类,而是在不同云厂商提供的数据库实现包。 -> 但项目运行时,要使用JDBC接口的哪些实现类呢? - -一般要根据自己使用的数据库驱动jar包,比如我们最常用的MySQL,其`mysql-jdbc-connector.jar` 里面就有: -![](https://img-blog.csdnimg.cn/20201220151405844.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -系统运行时碰到你使用JDBC的接口,就会在底层使用你引入的那个jar中提供的实现类。 -## 案例 -如sharding-jdbc 数据加密模块,本身支持 AES 和 MD5 两种加密方式。但若客户端不想用内置的两种加密,偏偏想用 RSA 算法呢?难道每加一种算法,sharding-jdbc 就要发个版本? - -sharding-jdbc 可不会这么蠢,首先提供出 EncryptAlgorithm 加密算法接口,并引入 SPI 机制,做到服务接口与服务实现分离的效果。 -客户端想要使用自定义加密算法,只需在客户端项目 `META-INF/services` 的路径下定义接口的全限定名称文件,并在文件内写上加密实现类的全限定名 -![](https://img-blog.csdnimg.cn/fea9f40870554ee8b579af6e34e22171.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/2025dd20872942c8a0560338787e9a63.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -这就显示了SPI的优点: -- 客户端(自己的项目)提供了服务端(sharding-jdbc)的接口自定义实现,但是与服务端状态分离,只有在客户端提供了自定义接口实现时才会加载,其它并没有关联;客户端的新增或删除实现类不会影响服务端 -- 如果客户端不想要 RSA 算法,又想要使用内置的 AES 算法,那么可以随时删掉实现类,可扩展性强,插件化架构 \ No newline at end of file diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/\345\255\220\347\261\273\345\217\257\344\273\245\347\273\247\346\211\277\345\210\260\347\210\266\347\261\273\344\270\212\347\232\204\346\263\250\350\247\243\345\220\227\357\274\237.md" "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/\345\255\220\347\261\273\345\217\257\344\273\245\347\273\247\346\211\277\345\210\260\347\210\266\347\261\273\344\270\212\347\232\204\346\263\250\350\247\243\345\220\227\357\274\237.md" deleted file mode 100644 index e48364b7f6..0000000000 --- "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/\345\255\220\347\261\273\345\217\257\344\273\245\347\273\247\346\211\277\345\210\260\347\210\266\347\261\273\344\270\212\347\232\204\346\263\250\350\247\243\345\220\227\357\274\237.md" +++ /dev/null @@ -1,68 +0,0 @@ -![](https://img-blog.csdnimg.cn/20210703161507167.png) - -> 子类重写父类方法后,可以继承方法上的注解吗? - -![](https://img-blog.csdnimg.cn/20210703161525416.png) -这个不急,让我来分析一下,假设有如下注解: -![](https://img-blog.csdnimg.cn/20210703172456417.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - -- 定义被注解的类 -![](https://img-blog.csdnimg.cn/2021070316221838.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - -- 子类直接继承父类 -![](https://img-blog.csdnimg.cn/20210703162259213.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - -- 获取父子类和方法的注解信息,并输出注解的value属性的值 -![](https://img-blog.csdnimg.cn/20210703164459591.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -- 日志输出 -![](https://img-blog.csdnimg.cn/20210703172622900.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -可见**子类及子类的方法,无法自动继承父类和父类方法上的注解**。 - -不对呀,你得使用`@Inherited`元注解才能实现注解的继承!行,那咱就加上![](https://img-blog.csdnimg.cn/20210703173231671.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 再看一遍控制台信息 -![](https://img-blog.csdnimg.cn/20210703173317381.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -可见使用`@Inherited`只能实现类上的注解继承。 - -> 那么如何实现方法上注解的继承呢? - -最简单暴力地,可通过反射技术,在继承链找到对应方法上的注解。但这样很麻烦,还需要考虑桥接方法。幸好Spring足够强大,提供了**AnnotatedElementUtils**类。 -### 对`@Inherited`的支持 -遵循get语义的方法将遵循Java的`@Inherited`注解的约定,除了在本地声明的批注(包括自定义组成的注解)优于继承的注解之外。相反,遵循find语义的方法将完全忽略`@Inherited`的存在,因为find搜索算法手动遍历类型和方法层次结构,从而隐式支持注解继承,而无需`@Inherited`。 - -### Find V.S Get Semantics -此类中的方法使用的搜索算法遵循find或get语义。 -#### Get 语义 -仅限于搜索存在于`AnnotatedElement`上的注解(即在本地声明或继承)或在AnnotatedElement上方的注解层次结构中声明的注释。 -#### Find 语义 -更加详尽,提供了获取语义以及对以下内容的支持: -- 搜索接口(如果带注释的元素是类) -- 搜索超类(如果带注释的元素是一个类) -- 解析桥接方法(如果带注释的元素是方法) -- 如果带注解的元素是方法,则在接口中搜索方法 -- 如果带注解的元素是方法,则在超类中搜索方法 - -如下俩方法其实也很相像,有何区别呢? -##### findAllMergedAnnotations -Find 对应 `SearchStrategy.TYPE_HIERARCHY` -![](https://img-blog.csdnimg.cn/20201120223339913.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -**findMergedAnnotation**方法可一次性找出父类和接口、父类方法和接口方法上的注解 -![](https://img-blog.csdnimg.cn/2021070317413821.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -##### getAllMergedAnnotations -Get对应 `SearchStrategy.INHERITED_ANNOTATIONS`: -![](https://img-blog.csdnimg.cn/20201120223435146.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) - -> 想想 Spring 的@Service、@Controller 等注解支持继承吗? - -我们通常的controller类,都会使用controller注解,如果可以被继承的话,Spring就不会只让我们使用Controller注解了,会提供另一种方式注入Controller组件,就是继承BaseController类。 -Spring 官方对此也有回应:继承的问题在于那些注解真的应该应用于特定的具体类。 - -> 参考 -> - https://github.com/spring-projects/spring-framework/issues/8859 -> - https://docs.oracle.com/javase/8/docs/api/java/lang/annotation/Inherited.html -> https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/annotation/AnnotatedElementUtils.html -> https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html \ No newline at end of file diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/\346\267\261\345\205\245\345\210\206\346\236\220-Java-\347\232\204\346\236\232\344\270\276-enum.md" "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/\346\267\261\345\205\245\345\210\206\346\236\220-Java-\347\232\204\346\236\232\344\270\276-enum.md" index b64692d19d..f8a4e2a73c 100644 --- "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/\346\267\261\345\205\245\345\210\206\346\236\220-Java-\347\232\204\346\236\232\344\270\276-enum.md" +++ "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/\346\267\261\345\205\245\345\210\206\346\236\220-Java-\347\232\204\346\236\232\344\270\276-enum.md" @@ -5,7 +5,7 @@ 创建需要enum关键字,如: ```java -public enum Color { +public enum Color{ RED, GREEN, BLUE, BLACK, PINK, WHITE; } ``` @@ -13,10 +13,11 @@ public enum Color { enum的语法看似与类不同,但它实际上就是一个类。 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTIyOGI1MmY1ZmE5M2E5ZWYucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-228b52f5fa93a9ef.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 把上面的编译成 Gender.class, 然后用 ` javap -c Gender `反编译 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTk4NTJkMDc3YTJkZTJkMzUucG5n?x-oss-process=image/format,png) + +![](https://upload-images.jianshu.io/upload_images/4685968-9852d077a2de2d35.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 可得到 - Gender 是 final 的 @@ -26,6 +27,7 @@ enum的语法看似与类不同,但它实际上就是一个类。 - static{} 对所有成员进行初始化 结合字节码,还原 Gender 的普通类形式 + ```java public final class Gender extends java.lang.Enum { @@ -54,29 +56,29 @@ public final class Gender extends java.lang.Enum { ``` 创建的枚举类型默认是java.lang.enum<枚举类型名>(抽象类)的子类 -每个枚举项的类型都为`public static final`。 +每个枚举项的类型都为public static final 。 上面的那个类是无法编译的,因为编译器限制了我们显式的继承自 java.Lang.Enum 类, 报错 "The type Gender may not subclass Enum explicitly", 虽然 java.Lang.Enum 声明的是 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTI0NTA4NjYxMDNjYWU3N2MucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-2450866103cae77c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 这样看来枚举类其实用了多例模式,枚举类的实例是有范围限制的 它同样像我们的传统常量类,只是它的元素是有限的枚举类本身的实例 -它继承自 java.lang.Enum, 所以可以直接调用 java.lang.Enum 的方法,如 name(), original() 等。 +它继承自 java.lang.Enum, 所以可以直接调用 java.lang.Enum 的方法,如 name(), original() 等 name 就是常量名称 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTM4MzcxMzc4M2JmMzUyYjkucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-383713783bf352b9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) original 与 C 的枚举一样的编号 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTAwYmY1YzYwMjk3NjA0MmUucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-00bf5c602976042e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 因为Java的单继承机制,emum不能再用extends继承其他的类。 -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtOTBiZWEyMGY0ODQ5ZmJiZQ?x-oss-process=image/format,png) +![](http://upload-images.jianshu.io/upload_images/4685968-90bea20f4849fbbe?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 可以在枚举类中自定义构造方法,但必须是 private 或 package protected, 因为枚举本质上是不允许在外面用 new Gender() 方式来构造实例的(Cannot instantiate the type Gender) 结合枚举实现接口以及自定义方法,可以写出下面那样的代码 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWE4ZGFiNDMyYTZjMzVlZDEucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-a8dab432a6c35ed1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 方法可以定义成所有实例公有,也可以让个别元素独有 需要特别注明一下,上面在 Male {} 声明一个 print() 方法后实际产生一个 Gender 的匿名子类,编译后的 Gender$1,反编译它 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTI3ZGZkNmFjMjJhZDdmNmUucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-27dfd6ac22ad7f6e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 所以在 emum Gender 那个枚举中的成员 Male 相当于是 ``` public static final Male = new Gender$1("Male", 0); //而不是 new Gender("Male", 0) @@ -84,7 +86,7 @@ public static final Male = new Gender$1("Male", 0); //而不是 new Gender("Male 上面` 4: Invokespecial #1` 要调用到下面的` Gender(java.lang.String, int, Gender$1) `方法 若要研究完整的 Male 元素的初始化过程就得 javap -c Gender 看 Gender.java 产生的所有字节码,在此列出片断 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWIxOWNjNTZhZGE4NTFjMzcucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-b19cc56ada851c37.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 在 static{} 中大致看下 Male 的初始过程:加载 Gender$1, 并调用它的 Gender$1(java.lang.String, int) 构造函数生成一个 Gender$1 实例赋给 Male 属性 @@ -155,34 +157,35 @@ Enum("WHITE", 5); **枚举类型的常用方法:** -int compareTo(E o) 比较此枚举与指定对象的顺序。 +int compareTo(E o)  比较此枚举与指定对象的顺序。 -Class getDeclaringClass() 返回与此枚举常量的枚举类型相对应的 Class 对象。 +Class getDeclaringClass()  返回与此枚举常量的枚举类型相对应的 Class 对象。 -String name() 返回此枚举常量的名称,在其枚举声明中对其进行声明。 +String name()   返回此枚举常量的名称,在其枚举声明中对其进行声明。 -int ordinal() 返回枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为零 +int ordinal()  返回枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为零 -String toString() 返回枚举常量的名称,它包含在声明中。 +String toString()    返回枚举常量的名称,它包含在声明中。 -static > T valueOf(Class enumType, String name) 返回带指定名称的指定枚举类型的枚举常量。 +static > T valueOf(Class enumType, String name)         返回带指定名称的指定枚举类型的枚举常量。 -# 2 常用用法 -## 2.1 常量 +二、常用用法 +用法一:常量 在JDK1.5 之前,我们定义常量都是: public static fianl.... 。现在好了,有了枚举,可以把相关的常量分组到一个枚举类型里,而且枚举提供了比常量更多的方法。 -## 2.2 switch -JDK1.6之前的switch语句只支持int、char、enum类型,使用枚举,能让我们的代码可读性更强。 +用法二:switch + +JDK1.6之前的switch语句只支持int,char,enum类型,使用枚举,能让我们的代码可读性更强。 + ```java +enum Color{ + RED, GREEN, BLUE, BLACK, PINK, WHITE; +} public class TestEnum { - - public void changeColor() { - - Color color = Color.RED; - - System.out.println("原色:" + color); - + public void changeColor(){ + Color color = Color.RED; + System.out.println("原色:" + color); switch(color){ case RED: color = Color.GREEN; @@ -210,14 +213,13 @@ public class TestEnum { break; } } - public static void main(String[] args){ TestEnum testEnum = new TestEnum(); testEnum.changeColor(); } } ``` -## 2.3 实现接口 +用法三:实现接口 ```java public interface Behaviour { void print(); @@ -251,11 +253,30 @@ public interface Behaviour { } } ``` -## 枚举集合 -- EnumSet保证集合中的元素不重复 -- EnumMap中的 key是enum类型,而value则可以是任意类型 +用法四:枚举集合的应用 -![](https://img-blog.csdnimg.cn/20200627221540659.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +java.util.EnumSet和java.util.EnumMap是两个枚举集合。EnumSet保证集合中的元素不重复;EnumMap中的 key是enum类型,而value则可以是任意类型。关于这个两个集合的使用就不在这里赘述,可以参考JDK文档 +```java +public class Test { + public static void main(String[] args) { + // EnumSet的使用 + EnumSet weekSet = EnumSet.allOf(EnumTest.class); + for (EnumTest day : weekSet) { + System.out.println(day); + } + + // EnumMap的使用 + EnumMap weekMap = new EnumMap(EnumTest.class); + weekMap.put(EnumTest.MON, "星期一"); + weekMap.put(EnumTest.TUE, "星期二"); + // ... ... + for (Iterator> iter = weekMap.entrySet().iterator(); iter.hasNext();) { + Entry entry = iter.next(); + System.out.println(entry.getKey().name() + ":" + entry.getValue()); + } + } +} +``` **三、综合实例** ### **最简单的使用** 最简单的枚举类 @@ -266,11 +287,11 @@ public enum Weekday { ``` 如何使用它呢? 先来看看它有哪些方法: -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtMzVhOWRkMTQ4ZWRjNmUzMy5wbmc?x-oss-process=image/format,png) +![](http://upload-images.jianshu.io/upload_images/4685968-35a9dd148edc6e33.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 这是Weekday可以调用的方法和参数。发现它有两个方法:values()和valueOf()。还有我们刚刚定义的七个变量 -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtZmEwMGM5OTIwNDI5NzU4ZC5wbmc?x-oss-process=image/format,png) +![](http://upload-images.jianshu.io/upload_images/4685968-fa00c9920429758d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 这些事枚举变量的方法。我们接下来会演示几个比较重要的 -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtNWQ1MGE2NmViYjhlOTFmYi5wbmc?x-oss-process=image/format,png) +![](http://upload-images.jianshu.io/upload_images/4685968-5d50a66ebb8e91fb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 这段代码,我们演示了几个常用的方法和功能: @@ -294,24 +315,24 @@ public enum Weekday { 5. 枚举变量的compareTo()方法。 > 该方法用来比较两个枚举变量的”大小”,实际上比较的是两个枚举变量的次序,返回两个次序相减后的结果,如果为负数,就证明变量1”小于”变量2 (变量1.compareTo(变量2),返回【变量1.ordinal() - 变量2.ordinal()】) -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtOTIzNjkyYjQyYmFhMjM0OS5wbmc?x-oss-process=image/format,png) +![](http://upload-images.jianshu.io/upload_images/4685968-923692b42baa2349.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) > 这是compareTo的源码,会先判断是不是同一个枚举类的变量,然后再返回差值。 6. 枚举类的name()方法。 - > 它和toString()方法的返回值一样,事实上,这两个方法本来就是一样的: -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtZDE4MWQ2YzQyNjI3NjcyMS5wbmc?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtZmU1MTVhYzc5ODNlNmFiMi5wbmc?x-oss-process=image/format,png) + > 它和toString()方法的返回值一样,事实上,这两个方法本来就是一样的:  +![](http://upload-images.jianshu.io/upload_images/4685968-d181d6c426276721.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](http://upload-images.jianshu.io/upload_images/4685968-fe515ac7983e6ab2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) > 这两个方法的默认实现是一样的,唯一的区别是,你可以重写toString方法。name变量就是枚举变量的字符串形式。 还有一些其他的方法我就暂时不介绍了,感兴趣的话可以自己去看看文档或者源码,都挺简单的。 > **要点:** > -> * 使用的是enum关键字而不是class。 -> * 多个枚举变量直接用逗号隔开。 -> * 枚举变量最好大写,多个单词之间使用”_”隔开(比如:INT_SUM)。 -> * 定义完所有的变量后,以分号结束,如果只有枚举变量,而没有自定义变量,分号可以省略(例如上面的代码就忽略了分号)。 +> * 使用的是enum关键字而不是class。  +> * 多个枚举变量直接用逗号隔开。  +> * 枚举变量最好大写,多个单词之间使用”_”隔开(比如:INT_SUM)。  +> * 定义完所有的变量后,以分号结束,如果只有枚举变量,而没有自定义变量,分号可以省略(例如上面的代码就忽略了分号)。  > * 在其他类中使用enum变量的时候,只需要【类名.变量名】就可以了,和使用静态变量一样。 但是这种简单的使用显然不能体现出枚举的强大,我们来学习一下复杂的使用: @@ -341,7 +362,7 @@ public enum Weekday { 看代码: -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtZmRmOThhMWVhMTQxYTAxMS5wbmc?x-oss-process=image/format,png) +![](http://upload-images.jianshu.io/upload_images/4685968-fdf98a1ea141a011.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 我们对上面的代码做了一些改变: @@ -353,10 +374,10 @@ public enum Weekday { > 请注意:这里有三点需要注意: > -> 1. 一定要把枚举变量的定义放在第一行,并且以分号结尾。 +> 1. 一定要把枚举变量的定义放在第一行,并且以分号结尾。  > > -> 2. 构造函数必须私有化。事实上,private是多余的,你完全没有必要写,因为它默认并强制是private,如果你要写,也只能写private,写public是不能通过编译的。 +> 2. 构造函数必须私有化。事实上,private是多余的,你完全没有必要写,因为它默认并强制是private,如果你要写,也只能写private,写public是不能通过编译的。  > > > 3. 自定义变量与默认的ordinal属性并不冲突,ordinal还是按照它的规则给每个枚举变量按顺序赋值。 @@ -365,7 +386,7 @@ public enum Weekday { 当然可以: -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtY2VlNzUwYzFjMGQ3OTAxYi5wbmc?x-oss-process=image/format,png) +![](http://upload-images.jianshu.io/upload_images/4685968-cee750c1c0d7901b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 你可以定义任何你想要的变量。学完了这些,大概枚举类你也应该掌握了,但是,还有没有其他用法呢? @@ -378,7 +399,7 @@ public enum Weekday { 你应该知道,有抽象方法的类必然是抽象类,抽象类就需要子类继承它然后实现它的抽象方法,但是呢,枚举类不能被继承。。你是不是有点乱? 我们先来看代码: -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtMzJkMGVlZDYxN2NkZWFjYS5wbmc?x-oss-process=image/format,png) +![](http://upload-images.jianshu.io/upload_images/4685968-32d0eed617cdeaca.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 你好像懂了点什么。但是你好像又不太懂。为什么一个变量的后边可以带一个代码块并且实现抽象方法呢? @@ -387,10 +408,10 @@ public enum Weekday { ## **枚举类的实现原理** 从最简单的看起: -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtMDBjN2QwZjQ5NzhjMmFkOC5wbmc?x-oss-process=image/format,png) +![](http://upload-images.jianshu.io/upload_images/4685968-00c7d0f4978c2ad8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 还是这段熟悉的代码,我们编译一下它,再反编译一下看看它到底是什么样子的: -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtZjhlNzNmNGVkNjk5OGU4Yi5wbmc?x-oss-process=image/format,png) +![](http://upload-images.jianshu.io/upload_images/4685968-f8e73f4ed6998e8b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 你是不是觉得很熟悉?反编译出来的代码和我们用静态变量自己写的类出奇的相似! @@ -402,21 +423,22 @@ public enum Weekday { 并且,这个类是final的!所以它不能被继承! 回到我们刚才的那个疑问: -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtODA3Y2ZmZTMzYWRlOWU0Ni5wbmc?x-oss-process=image/format,png) -为什么会有这么神奇的代码?现在你差不多懂了。因为RED本身就是一个TrafficLamp对象的引用。实际上,在初始化这个枚举类的时候,你可以理解为执行的是`TrafficLamp RED = new TrafficLamp(30)` ,但是因为TrafficLamp里面有抽象方法,还记得匿名内部类么? +![](http://upload-images.jianshu.io/upload_images/4685968-807cffe33ade9e46.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +为什么会有这么神奇的代码?现在你差不多懂了。因为RED本身就是一个TrafficLamp对象的引用。实际上,在初始化这个枚举类的时候,你可以理解为执行的是`TrafficLamp RED = new TrafficLamp(30)` ,但是因为TrafficLamp里面有抽象方法,还记得匿名内部类么? 我们可以这样来创建一个TrafficLamp引用: -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtMjY0MTYyYzg5OGNlMDJiMy5wbmc?x-oss-process=image/format,png) +![](http://upload-images.jianshu.io/upload_images/4685968-264162c898ce02b3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 而在枚举类中,我们只需要像上面那样写【`RED(30){}`】就可以了,因为java会自动的去帮我们完成这一系列操作 -## 枚举类用法 +## **枚举类的其他用法** +![ switch语句](http://upload-images.jianshu.io/upload_images/4685968-8cbb5f4a39c40739.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 虽然枚举类不能继承其他类,但是还是可以实现接口的 -![接口定义](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtMjY1YzYzNDNjYWE2NWYyZC5wbmc?x-oss-process=image/format,png) -![实现接口](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtMzE5MjhjYTMwZTU5NTc3OC5wbmc?x-oss-process=image/format,png) -![使用接口组织枚举](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtNzkyNWZjNzkxYjQwNDg5OS5wbmc?x-oss-process=image/format,png) +![接口定义](http://upload-images.jianshu.io/upload_images/4685968-265c6343caa65f2d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![实现接口](http://upload-images.jianshu.io/upload_images/4685968-31928ca30e595778.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![使用接口组织枚举](http://upload-images.jianshu.io/upload_images/4685968-7925fc791b404899.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ### **使用枚举创建单例模式** @@ -480,8 +502,16 @@ public class Singleton{ 上面的两种方式就是懒汉式和恶汉式单利的创建,但是无论哪一种,都不如枚举来的方便。而且传统的单例模式的另外一个问题是一旦你实现了serializable接口,他们就不再是单例的了。但是枚举类的父类【Enum类】实现了Serializable接口,也就是说,所有的枚举类都是可以实现序列化的,这也是一个优点。 ## **总结** -- 可以创建一个enum类,把它看做一个普通的类。除了它不能继承其他类了。(java是单继承,它已经继承了Enum),可以添加其他方法,覆盖它本身的方法 -- switch()参数可以使用enum -- values()方法是编译器插入到enum定义中的static方法,所以,当你将enum实例向上转型为父类Enum是,values()就不可访问了。解决办法:在Class中有一个getEnumConstants()方法,所以即便Enum接口中没有values()方法,我们仍然可以通过Class对象取得所有的enum实例 -- 无法从enum继承子类,如果需要扩展enum中的元素,在一个接口的内部,创建实现该接口的枚举,以此将元素进行分组。达到将枚举元素进行分组。 -- enum允许程序员为eunm实例编写方法。所以可以为每个enum实例赋予各自不同的行为。 \ No newline at end of file +> * 可以创建一个enum类,把它看做一个普通的类。除了它不能继承其他类了。(java是单继承,它已经继承了Enum),可以添加其他方法,覆盖它本身的方法  +> +> +> * switch()参数可以使用enum  +> +> +> * values()方法是编译器插入到enum定义中的static方法,所以,当你将enum实例向上转型为父类Enum是,values()就不可访问了。解决办法:在Class中有一个getEnumConstants()方法,所以即便Enum接口中没有values()方法,我们仍然可以通过Class对象取得所有的enum实例  +> +> +> * 无法从enum继承子类,如果需要扩展enum中的元素,在一个接口的内部,创建实现该接口的枚举,以此将元素进行分组。达到将枚举元素进行分组。  +> +> +> * enum允许程序员为eunm实例编写方法。所以可以为每个enum实例赋予各自不同的行为。 diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/\346\267\261\345\205\245\350\247\243\346\236\220Java\347\232\204\346\263\250\350\247\243\346\234\272\345\210\266.md" "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/\346\267\261\345\205\245\350\247\243\346\236\220Java\347\232\204\346\263\250\350\247\243\346\234\272\345\210\266.md" deleted file mode 100644 index 0533174275..0000000000 --- "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/\346\267\261\345\205\245\350\247\243\346\236\220Java\347\232\204\346\263\250\350\247\243\346\234\272\345\210\266.md" +++ /dev/null @@ -1,1810 +0,0 @@ -@[toc](目录) - -注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方式,使我们可以在稍后的某个时刻更容易的使用这些数据。 - -注解在一定程度上是把元数据和源代码文件结合在一起的趋势所激发的,而不是保存在外部文档。这同样是对像 C# 语言对于 Java 语言特性压力的一种回应。 - -注解是 Java 5 所引入的众多语言变化之一。它们提供了 Java 无法表达的但是你需要完整表述程序所需的信息。因此,注解使得我们可以以编译器验证的格式存储程序的额外信息。注解可以生成描述符文件,甚至是新的类定义,并且有助于减轻编写“样板”代码的负担。通过使用注解,你可以将元数据保存在 Java 源代码。并拥有如下优势: -- 简单易读的代码 -- 编译器类型检查 -- 使用 annotation API 为自己的注解构造处理工具 -- 为Java代码提供元数据 -- 暴露功能 -比如Spring的`@Service`、`@Controller` - -通过注解配置框架,属于声明式交互: -- 简化框架配置 -- 和框架解耦 - -即使 Java 定义了一些类型的元数据,但是一般来说注解类型的添加和如何使用完全取决于你。 - -注解的语法十分简单,主要是在现有语法中添加 @ 符号。 -Java 5 引入了前三种定义在 **java.lang** 包中的注解: -- **@Override**:表示当前的方法定义将覆盖基类的方法。如果你不小心拼写错误,或者方法签名被错误拼写的时候,编译器就会发出错误提示。 -- **@Deprecated**:如果使用该注解的元素被调用,编译器就会发出警告信息。 -- **@SuppressWarnings**:关闭不当的编译器警告信息。 -- **@SafeVarargs**:在 Java 7 中加入用于禁止对具有泛型varargs参数的方法或构造函数的调用方发出警告。 -- **@FunctionalInterface**:Java 8 中加入用于表示类型声明为函数式接口 - -还有 5 种额外的注解类型用于创造新的注解。 -每当创建涉及重复工作的类或接口时,通常可以使用注解来自动化和简化流程。 - -注解是真正语言层级的概念,以前构造出来就享有编译器的类型检查保护。注解在源代码级别保存所有信息而不是通过注释文字,这使得代码更加整洁和便于维护。通过使用拓展的 annotation API 或外部的字节码工具类库,你会拥有对源代码及字节码强大的检查与操作能力。 - -# 1 基本语法 - - - -在下面的例子中,使用 `@Test` 对 `testExecute()` 进行注解。该注解本身不做任何事情,但是编译器要保证其类路径上有 `@Test` 注解的定义。你将在本章看到,我们通过注解创建了一个工具用于运行这个方法: - -```java -// annotations/Testable.java -package annotations; -import onjava.atunit.*; -public class Testable { - public void execute() { - System.out.println("Executing.."); - } - @Test - void testExecute() { execute(); } -} -``` - -被注解标注的方法和其他的方法没有任何区别。在这个例子中,注解 `@Test` 可以和任何修饰符共同用于方法,诸如 **public**、**static** 或 **void**。从语法的角度上看,注解的使用方式和修饰符的使用方式一致。 - -## 1.1 定义注解 - -如下是一个注解的定义。注解的定义看起来很像接口的定义。事实上,它们和其他 Java 接口一样,也会被编译成 class 文件。 - -```java -// onjava/atunit/Test.java -// The @Test tag -package onjava.atunit; -import java.lang.annotation.*; -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface Test {} -``` - -除了 @ 符号之外, `@Test` 的定义看起来更像一个空接口。注解的定义也需要一些元注解(meta-annoation),比如 `@Target` 和 `@Retention`。 -- `@Target` 定义你的注解可以应用在哪里(例如是方法还是字段) -- `@Retention` 定义了注解在哪里可用 - - 在源代码中(SOURCE) - - class文件(CLASS) - - 运行时(RUNTIME) - -注解通常会包含一些表示特定值的元素。当分析处理注解的时候,程序或工具可以利用这些值。注解的元素看起来就像接口的方法,但是可以为其指定默认值。 - -不包含任何元素的注解称为标记注解(marker annotation),例如上例中的 `@Test` 就是标记注解。 - -下面是一个简单的注解,我们可以用它来追踪项目中的用例。程序员可以使用该注解来标注满足特定用例的一个方法或者一组方法。于是,项目经理可以通过统计已经实现的用例来掌控项目的进展,而开发者在维护项目时可以轻松的找到用例用于更新,或者他们可以调试系统中业务逻辑。 - -```java -// annotations/UseCase.java -import java.lang.annotation.*; -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface UseCase { - int id(); - String description() default "no description"; -} -``` - -注意 **id** 和 **description** 与方法定义类似。由于编译器会对 **id** 进行类型检查,因此将跟踪数据库与用例文档和源代码相关联是可靠的方式。**description** 元素拥有一个 **default** 值,如果在注解某个方法时没有给出 **description** 的值。则该注解的处理器会使用此元素的默认值。 - -在下面的类中,有三个方法被注解为用例: - -```java -// annotations/PasswordUtils.java -import java.util.*; -public class PasswordUtils { - @UseCase(id = 47, description = - "Passwords must contain at least one numeric") - public boolean validatePassword(String passwd) { - return (passwd.matches("\\w*\\d\\w*")); - } - @UseCase(id = 48) - public String encryptPassword(String passwd) { - return new StringBuilder(passwd) - .reverse().toString(); - } - @UseCase(id = 49, description = - "New passwords can't equal previously used ones") - public boolean checkForNewPassword( - List prevPasswords, String passwd) { - return !prevPasswords.contains(passwd); - } -} -``` - -注解的元素在使用时表现为 名-值 对的形式,并且需要放置在 `@UseCase` 声明之后的括号内。在 `encryptPassword()` 方法的注解中,并没有给出 **description** 的值,所以在 **@interface UseCase** 的注解处理器分析处理这个类的时候会使用该元素的默认值。 - -你应该能够想象到如何使用这套工具来“勾勒”出将要建造的系统,然后在建造的过程中逐渐实现系统的各项功能。 - -## 1.2 元注解 -Java 有 5 种标准注解及 5 种元注解。 -元注解用于注解其他的注解 - -| 注解 | 解释 | -| ----------- | ------------------------------------------------------------ | -| @Target | 表注解可用于哪些地方。可能的 **ElementType** 参数包括:
**CONSTRUCTOR**:构造器的声明
**FIELD**:字段声明(包括 enum 实例)
**LOCAL_VARIABLE**:局部变量声明
**METHOD**:方法声明
**PACKAGE**:包声明
**PARAMETER**:参数声明
**TYPE**:类、接口(包括注解)或者 enum 声明 | -| @Retention | 表示注解信息保存的时长。可选的 **RetentionPolicy** 参数包括:
**SOURCE**:注解将被编译器丢弃
**CLASS**:注解在 class 文件中可用,但是会被 VM 丢弃。
**RUNTIME**:VM 将在运行期也保留注解,因此可以通过反射机制读取注解的信息。 | -| @Documented | 将此注解保存在 Javadoc 中 | -| @Inherited | 允许子类继承父类的注解 | -| @Repeatable | 允许一个注解可以被使用一次或者多次(Java 8)。 | - -大多数时候,程序员定义自己的注解,并编写自己的处理器来处理他们。 - -## 编写注解处理器 - -如果没有用于读取注解的工具,那么注解不会比注释更有用。使用注解中一个很重要的部分就是,创建与使用注解处理器。Java 拓展了反射机制的 API 用于帮助你创造这类工具。同时他还提供了 javac 编译器钩子在编译时使用注解。 - -下面是一个非常简单的注解处理器,我们用它来读取被注解的 **PasswordUtils** 类,并且使用反射机制来寻找 **@UseCase** 标记。给定一组 **id** 值,然后列出在 **PasswordUtils** 中找到的用例,以及缺失的用例。 - -```java -// annotations/UseCaseTracker.java -import java.util.*; -import java.util.stream.*; -import java.lang.reflect.*; -public class UseCaseTracker { - public static void - trackUseCases(List useCases, Class cl) { - for(Method m : cl.getDeclaredMethods()) { - UseCase uc = m.getAnnotation(UseCase.class); - if(uc != null) { - System.out.println("Found Use Case " + - uc.id() + "\n " + uc.description()); - useCases.remove(Integer.valueOf(uc.id())); - } - } - useCases.forEach(i -> - System.out.println("Missing use case " + i)); - } - public static void main(String[] args) { - List useCases = IntStream.range(47, 51) - .boxed().collect(Collectors.toList()); - trackUseCases(useCases, PasswordUtils.class); - } -} -``` - -输出为: - -```java -Found Use Case 48 -no description -Found Use Case 47 -Passwords must contain at least one numeric -Found Use Case 49 -New passwords can't equal previously used ones -Missing use case 50 -``` - -这个程序用了两个反射的方法:`getDeclaredMethods()` 和 `getAnnotation()`,它们都属于 **AnnotatedElement** 接口(**Class**,**Method** 与 **Field** 类都实现了该接口)。`getAnnotation()` 方法返回指定类型的注解对象,在本例中就是 “**UseCase**”。如果被注解的方法上没有该类型的注解,返回值就为 **null**。我们通过调用 `id()` 和 `description()` 方法来提取元素值。注意 `encryptPassword()` 方法在注解的时候没有指定 **description** 的值,因此处理器在处理它对应的注解时,通过 `description()` 取得的是默认值 “no description”。 - -### 注解元素 - -在 **UseCase.java** 中定义的 **@UseCase** 的标签包含 int 元素 **id** 和 String 元素 **description**。注解元素可用的类型如下所示: - -- 所有基本类型(int、float、boolean等) -- String -- Class -- enum -- Annotation -- 以上类型的数组 - -如果你使用了其他类型,编译器就会报错。注意,也不允许使用任何包装类型,但是由于自动装箱的存在,这不算是什么限制。注解也可以作为元素的类型。稍后你会看到,注解嵌套是一个非常有用的技巧。 - -### 默认值限制 - -编译器对于元素的默认值有些过于挑剔。首先,元素不能有不确定的值。也就是说,元素要么有默认值,要么就在使用注解时提供元素的值。 - -这里有另外一个限制:任何非基本类型的元素, 无论是在源代码声明时还是在注解接口中定义默认值时,都不能使用 null 作为其值。这个限制使得处理器很难表现一个元素的存在或者缺失的状态,因为在每个注解的声明中,所有的元素都存在,并且具有相应的值。为了绕开这个约束,可以自定义一些特殊的值,比如空字符串或者负数用于表达某个元素不存在。 - -```java -// annotations/SimulatingNull.java -import java.lang.annotation.*; -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface SimulatingNull { - int id() default -1; - String description() default ""; -} -``` - -这是一个在定义注解的习惯用法。 - -### 生成外部文件 - -当有些框架需要一些额外的信息才能与你的源代码协同工作,这种情况下注解就会变得十分有用。像 Enterprise JavaBeans (EJB3 之前)这样的技术,每一个 Bean 都需要需要大量的接口和部署描述文件,而这些就是“样板”文件。Web Service,自定义标签库以及对象/关系映射工具(例如 Toplink 和 Hibernate)通常都需要 XML 描述文件,而这些文件脱离于代码之外。除了定义 Java 类,程序员还必须忍受沉闷,重复的提供某些信息,例如类名和包名等已经在原始类中已经提供的信息。每当你使用外部描述文件时,他就拥有了一个类的两个独立信息源,这经常导致代码的同步问题。同时这也要求了为项目工作的程序员在知道如何编写 Java 程序的同时,也必须知道如何编辑描述文件。 - -假设你想提供一些基本的对象/关系映射功能,能够自动生成数据库表。你可以使用 XML 描述文件来指明类的名字、每个成员以及数据库映射的相关信息。但是,通过使用注解,你可以把所有信息都保存在 **JavaBean** 源文件中。为此你需要一些用于定义数据库表名称、数据库列以及将 SQL 类型映射到属性的注解。 - -以下是一个注解的定义,它告诉注解处理器应该创建一个数据库表: - -```java -// annotations/database/DBTable.java -package annotations.database; -import java.lang.annotation.*; -@Target(ElementType.TYPE) // Applies to classes only -@Retention(RetentionPolicy.RUNTIME) -public @interface DBTable { - String name() default ""; -} -``` - -在 `@Target` 注解中指定的每一个 **ElementType** 就是一个约束,它告诉编译器,这个自定义的注解只能用于指定的类型。你可以指定 **enum ElementType** 中的一个值,或者以逗号分割的形式指定多个值。如果想要将注解应用于所有的 **ElementType**,那么可以省去 `@Target` 注解,但是这并不常见。 - -注意 **@DBTable** 中有一个 `name()` 元素,该注解通过这个元素为处理器创建数据库时提供表的名字。 - -如下是修饰字段的注解: - -```java -// annotations/database/Constraints.java -package annotations.database; -import java.lang.annotation.*; -@Target(ElementType.FIELD) -@Retention(RetentionPolicy.RUNTIME) -public @interface Constraints { - boolean primaryKey() default false; - boolean allowNull() default true; - boolean unique() default false; -} -``` - -```java -// annotations/database/SQLString.java -package annotations.database; -import java.lang.annotation.*; -@Target(ElementType.FIELD) -@Retention(RetentionPolicy.RUNTIME) -public @interface SQLString { - int value() default 0; - String name() default ""; - Constraints constraints() default @Constraints; -} -``` - -```java -// annotations/database/SQLInteger.java -package annotations.database; -import java.lang.annotation.*; -@Target(ElementType.FIELD) -@Retention(RetentionPolicy.RUNTIME) -public @interface SQLInteger { - String name() default ""; - Constraints constraints() default @Constraints; -} -``` - -**@Constraints** 注解允许处理器提供数据库表的元数据。**@Constraints** 代表了数据库通常提供的约束的一小部分,但是它所要表达的思想已经很清楚了。`primaryKey()`,`allowNull()` 和 `unique()` 元素明显的提供了默认值,从而使得在大多数情况下,该注解的使用者不需要输入太多东西。 - -另外两个 **@interface** 定义的是 SQL 类型。如果希望这个框架更有价值的话,我们应该为每个 SQL 类型都定义相应的注解。不过为为示例,两个元素足够了。 - -这些 SQL 类型具有 `name()` 元素和 `constraints()` 元素。后者利用了嵌套注解的功能,将数据库列的类型约束信息嵌入其中。注意 `constraints()` 元素的默认值是 **@Constraints**。由于在 **@Constraints** 注解类型之后,没有在括号中指明 **@Constraints** 元素的值,因此,**constraints()** 的默认值为所有元素都为默认值的 **@Constraints** 注解。如果要使得嵌入的 **@Constraints** 注解中的 `unique()` 元素为 true,并作为 `constraints()` 元素的默认值,你可以像如下定义: - -```java -// annotations/database/Uniqueness.java -// Sample of nested annotations -package annotations.database; -public @interface Uniqueness { - Constraints constraints() - default @Constraints(unique = true); -} -``` - -下面是一个简单的,使用了如上注解的类: - -```java -// annotations/database/Member.java -package annotations.database; -@DBTable(name = "MEMBER") -public class Member { - @SQLString(30) String firstName; - @SQLString(50) String lastName; - @SQLInteger Integer age; - @SQLString(value = 30, - constraints = @Constraints(primaryKey = true)) - String reference; - static int memberCount; - public String getReference() { return reference; } - public String getFirstName() { return firstName; } - public String getLastName() { return lastName; } - @Override - public String toString() { return reference; } - public Integer getAge() { return age; } -} -``` - -类注解 **@DBTable** 注解给定了元素值 MEMBER,它将会作为标的名字。类的属性 **firstName** 和 **lastName** 都被注解为 **@SQLString** 类型并且给了默认元素值分别为 30 和 50。这些注解都有两个有趣的地方:首先,他们都使用了嵌入的 **@Constraints** 注解的默认值;其次,它们都是用了快捷方式特性。如果你在注解中定义了名为 **value** 的元素,并且在使用该注解时,**value** 为唯一一个需要赋值的元素,你就不需要使用名—值对的语法,你只需要在括号中给出 **value** 元素的值即可。这可以应用于任何合法类型的元素。这也限制了你必须将元素命名为 **value**,不过在上面的例子中,这样的注解语句也更易于理解: - -```java -@SQLString(30) -``` - -处理器将在创建表的时候使用该值设置 SQL 列的大小。 - -默认值的语法虽然很灵巧,但是它很快就变的复杂起来。以 **reference** 字段的注解为例,上面拥有 **@SQLString** 注解,但是这个字段也将成为表的主键,因此在嵌入的 **@Constraint** 注解中设定 **primaryKey** 元素的值。这时事情就变的复杂了。你不得不为这个嵌入的注解使用很长的键—值对的形式,来指定元素名称和 **@interface** 的名称。同时,由于有特殊命名的 **value** 也不是唯一需要赋值的元素,因此不能再使用快捷方式特性。如你所见,最终结果不算清晰易懂。 - -### 替代方案 - -可以使用多种不同的方式来定义自己的注解用于上述任务。例如,你可以使用一个单一的注解类 **@TableColumn**,它拥有一个 **enum** 元素,元素值定义了 **STRING**,**INTEGER**,**FLOAT** 等类型。这消除了每个 SQL 类型都需要定义一个 **@interface** 的负担,不过也使得用额外信息修饰 SQL 类型变的不可能,这些额外的信息例如长度或精度等,都可能是非常有用的。 - -你也可以使用一个 **String** 类型的元素来描述实际的 SQL 类型,比如 “VARCHAR(30)” 或者 “INTEGER”。这使得你可以修饰 SQL 类型,但是这也将 Java 类型到 SQL 类型的映射绑在了一起,这不是一个好的设计。你并不想在数据库更改之后重新编译你的代码;如果我们只需要告诉注解处理器,我们正在使用的是什么“口味(favor)”的 SQL,然后注解助力器来为我们处理 SQL 类型的细节,那将是一个优雅的设计。 - -第三种可行的方案是一起使用两个注解,**@Constraints** 和相应的 SQL 类型(例如,**@SQLInteger**)去注解同一个字段。这可能会让代码有些混乱,但是编译器允许你对同一个目标使用多个注解。在 Java 8,在使用多个注解的时候,你可以重复使用同一个注解。 - -### 注解不支持继承 -你不能使用 **extends** 关键字来继承 **@interfaces**。这真是一个遗憾,如果可以定义 **@TableColumn** 注解,同时嵌套一个 **@SQLType** 类型的注解,将成为一个优雅的设计。按照这种方式,你可以通过继承 **@SQLType** 来创造各种 SQL 类型。例如 **@SQLInteger** 和 **@SQLString**。如果支持继承,就会大大减少打字的工作量并且使得语法更整洁。在 Java 的未来版本中,似乎没有任何关于让注解支持继承的提案,所以在当前情况下,上例中的解决方案可能已经是最佳方案了。 - -### 实现处理器 -下面是一个注解处理器的例子,他将读取一个类文件,检查上面的数据库注解,并生成用于创建数据库的 SQL 命令: -```java -// annotations/database/TableCreator.java -// Reflection-based annotation processor -// {java annotations.database.TableCreator -// annotations.database.Member} -package annotations.database; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; - -public class TableCreator { - public static void - main(String[] args) throws Exception { - if (args.length < 1) { - System.out.println( - "arguments: annotated classes"); - System.exit(0); - } - for (String className : args) { - Class cl = Class.forName(className); - DBTable dbTable = cl.getAnnotation(DBTable.class); - if (dbTable == null) { - System.out.println( - "No DBTable annotations in class " + - className); - continue; - } - String tableName = dbTable.name(); - // If the name is empty, use the Class name: - if (tableName.length() < 1) - tableName = cl.getName().toUpperCase(); - List columnDefs = new ArrayList<>(); - for (Field field : cl.getDeclaredFields()) { - String columnName = null; - Annotation[] anns = - field.getDeclaredAnnotations(); - if (anns.length < 1) - continue; // Not a db table column - if (anns[0] instanceof SQLInteger) { - SQLInteger sInt = (SQLInteger) anns[0]; - // Use field name if name not specified - if (sInt.name().length() < 1) - columnName = field.getName().toUpperCase(); - else - columnName = sInt.name(); - columnDefs.add(columnName + " INT" + - getConstraints(sInt.constraints())); - } - if (anns[0] instanceof SQLString) { - SQLString sString = (SQLString) anns[0]; - // Use field name if name not specified. - if (sString.name().length() < 1) - columnName = field.getName().toUpperCase(); - else - columnName = sString.name(); - columnDefs.add(columnName + " VARCHAR(" + - sString.value() + ")" + - getConstraints(sString.constraints())); - } - StringBuilder createCommand = new StringBuilder( - "CREATE TABLE " + tableName + "("); - for (String columnDef : columnDefs) - createCommand.append( - "\n " + columnDef + ","); - // Remove trailing comma - String tableCreate = createCommand.substring( - 0, createCommand.length() - 1) + ");"; - System.out.println("Table Creation SQL for " + - className + " is:\n" + tableCreate); - } - } - } - - private static String getConstraints(Constraints con) { - String constraints = ""; - if (!con.allowNull()) - constraints += " NOT NULL"; - if (con.primaryKey()) - constraints += " PRIMARY KEY"; - if (con.unique()) - constraints += " UNIQUE"; - return constraints; - } -} -``` - -输出为: - -```sql -Table Creation SQL for annotations.database.Member is: -CREATE TABLE MEMBER( - FIRSTNAME VARCHAR(30)); -Table Creation SQL for annotations.database.Member is: -CREATE TABLE MEMBER( - FIRSTNAME VARCHAR(30), - LASTNAME VARCHAR(50)); -Table Creation SQL for annotations.database.Member is: -CREATE TABLE MEMBER( - FIRSTNAME VARCHAR(30), - LASTNAME VARCHAR(50), - AGE INT); -Table Creation SQL for annotations.database.Member is: -CREATE TABLE MEMBER( - FIRSTNAME VARCHAR(30), - LASTNAME VARCHAR(50), - AGE INT, - REFERENCE VARCHAR(30) PRIMARY KEY); -``` - -主方法会循环处理命令行传入的每一个类名。每一个类都是用 ` forName()` 方法进行加载,并使用 `getAnnotation(DBTable.class)` 来检查该类是否带有 **@DBTable** 注解。如果存在,将表名存储起来。然后读取这个类的所有字段,并使用 `getDeclaredAnnotations()` 进行检查。这个方法返回一个包含特定字段上所有注解的数组。然后使用 **instanceof** 操作符判断这些注解是否是 **@SQLInteger** 或者 **@SQLString** 类型。如果是的话,在对应的处理块中将构造出相应的数据库列的字符串片段。注意,由于注解没有继承机制,如果要获取近似多态的行为,使用 `getDeclaredAnnotations()` 似乎是唯一的方式。 - -嵌套的 **@Constraint** 注解被传递给 `getConstraints()`方法,并用它来构造一个包含 SQL 约束的 String 对象。 - -需要提醒的是,上面演示的技巧对于真实的对象/映射关系而言,是十分幼稚的。使用 **@DBTable** 的注解来获取表的名称,这使得如果要修改表的名字,则迫使你重新编译 Java 代码。这种效果并不理想。现在已经有了很多可用的框架,用于将对象映射到数据库中,并且越来越多的框架开始使用注解了。 - -## 使用javac处理注解 - -通过 **javac**,你可以通过创建编译时(compile-time)注解处理器在 Java 源文件上使用注解,而不是编译之后的 class 文件。但是这里有一个重大限制:你不能通过处理器来改变源代码。唯一影响输出的方式就是创建新的文件。 - -如果你的注解处理器创建了新的源文件,在新一轮处理中注解会检查源文件本身。工具在检测一轮之后持续循环,直到不再有新的源文件产生。然后它编译所有的源文件。 - -每一个你编写的注解都需要处理器,但是 **javac** 可以非常容易的将多个注解处理器合并在一起。你可以指定多个需要处理的类,并且你可以添加监听器用于监听注解处理完成后接到通知。 - -本节中的示例将帮助你开始学习,但如果你必须深入学习,请做好反复学习,大量访问 Google 和StackOverflow 的准备。 - -### 最简单的处理器 - -让我们开始定义我们能想到的最简单的处理器,只是为了编译和测试。如下是注解的定义: - -```java -// annotations/simplest/Simple.java -// A bare-bones annotation -package annotations.simplest; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.lang.annotation.ElementType; -@Retention(RetentionPolicy.SOURCE) -@Target({ElementType.TYPE, ElementType.METHOD, - ElementType.CONSTRUCTOR, - ElementType.ANNOTATION_TYPE, - ElementType.PACKAGE, ElementType.FIELD, - ElementType.LOCAL_VARIABLE}) -public @interface Simple { - String value() default "-default-"; -} -``` - -**@Retention** 的参数现在为 **SOURCE**,这意味着注解不会再存留在编译后的代码。这在编译时处理注解是没有必要的,它只是指出,在这里,**javac** 是唯一有机会处理注解的代理。 - -**@Target** 声明了几乎所有的目标类型(除了 **PACKAGE**) ,同样是为了演示。下面是一个测试示例。 - -```java -// annotations/simplest/SimpleTest.java -// Test the "Simple" annotation -// {java annotations.simplest.SimpleTest} -package annotations.simplest; -@Simple -public class SimpleTest { - @Simple - int i; - @Simple - public SimpleTest() {} - @Simple - public void foo() { - System.out.println("SimpleTest.foo()"); - } - @Simple - public void bar(String s, int i, float f) { - System.out.println("SimpleTest.bar()"); - } - @Simple - public static void main(String[] args) { - @Simple - SimpleTest st = new SimpleTest(); - st.foo(); - } -} -``` - -输出为: - -```java -SimpleTest.foo() -``` - -在这里我们使用 **@Simple** 注解了所有 **@Target** 声明允许的地方。 - -**SimpleTest.java** 只需要 **Simple.java** 就可以编译成功。当我们编译的时候什么都没有发生。 - -**javac** 允许 **@Simple** 注解(只要它存在)在我们创建处理器并将其 hook 到编译器之前,不做任何事情。 - -如下是一个十分简单的处理器,其所作的事情就是把注解相关的信息打印出来: - -```java -// annotations/simplest/SimpleProcessor.java -// A bare-bones annotation processor -package annotations.simplest; -import javax.annotation.processing.*; -import javax.lang.model.SourceVersion; -import javax.lang.model.element.*; -import java.util.*; -@SupportedAnnotationTypes( - "annotations.simplest.Simple") -@SupportedSourceVersion(SourceVersion.RELEASE_8) -public class SimpleProcessor - extends AbstractProcessor { - @Override - public boolean process( - Set annotations, - RoundEnvironment env) { - for(TypeElement t : annotations) - System.out.println(t); - for(Element el : - env.getElementsAnnotatedWith(Simple.class)) - display(el); - return false; - } - private void display(Element el) { - System.out.println("==== " + el + " ===="); - System.out.println(el.getKind() + - " : " + el.getModifiers() + - " : " + el.getSimpleName() + - " : " + el.asType()); - if(el.getKind().equals(ElementKind.CLASS)) { - TypeElement te = (TypeElement)el; - System.out.println(te.getQualifiedName()); - System.out.println(te.getSuperclass()); - System.out.println(te.getEnclosedElements()); - } - if(el.getKind().equals(ElementKind.METHOD)) { - ExecutableElement ex = (ExecutableElement)el; - System.out.print(ex.getReturnType() + " "); - System.out.print(ex.getSimpleName() + "("); - System.out.println(ex.getParameters() + ")"); - } - } -} -``` - -(旧的,失效的)**apt** 版本的处理器需要额外的方法来确定支持哪些注解以及支持的 Java 版本。不过,你现在可以简单的使用 **@SupportedAnnotationTypes** 和 **@SupportedSourceVersion** 注解(这是一个很好的示例关于注解如何简化你的代码)。 - -你唯一需要实现的方法就是 `process()`,这里是所有行为发生的地方。第一个参数告诉你哪个注解是存在的,第二个参数保留了剩余信息。我们所做的事情只是打印了注解(这里只存在一个),可以看 **TypeElement** 文档中的其他行为。通过使用 `process()` 的第二个操作,我们循环所有被 **@Simple** 注解的元素,并且针对每一个元素调用我们的 `display()` 方法。所有 **Element** 展示了本身的基本信息;例如,`getModifiers()` 告诉你它是否为 **public** 和 **static** 的。 - -**Element** 只能执行那些编译器解析的所有基本对象共有的操作,而类和方法之类的东西有额外的信息需要提取。所以(如果你阅读了正确的文档,但是我没有在任何文档中找到——我不得不通过 StackOverflow 寻找线索)你检查它是哪种 **ElementKind**,然后将其向下转换为更具体的元素类型,注入针对 CLASS 的 TypeElement 和 针对 METHOD 的ExecutableElement。此时,可以为这些元素调用其他方法。 - -动态向下转型(在编译期不进行检查)并不像是 Java 的做事方式,这非常不直观这也是为什么我从未想过要这样做事。相反,我花了好几天的时间,试图发现你应该如何访问这些信息,而这些信息至少在某种程度上是用不起作用的恰当方法简单明了的。我还没有遇到任何东西说上面是规范的形式,但在我看来是。 - -如果只是通过平常的方式来编译 **SimpleTest.java**,你不会得到任何结果。为了得到注解输出,你必须增加一个 **processor** 标志并且连接注解处理器类 - -```shell -javac -processor annotations.simplest.SimpleProcessor SimpleTest.java -``` - -现在编译器有了输出 - -```shell -annotations.simplest.Simple -==== annotations.simplest.SimpleTest ==== -CLASS : [public] : SimpleTest : annotations.simplest.SimpleTest -annotations.simplest.SimpleTest -java.lang.Object -i,SimpleTest(),foo(),bar(java.lang.String,int,float),main(java.lang.String[]) -==== i ==== -FIELD : [] : i : int -==== SimpleTest() ==== -CONSTRUCTOR : [public] : : ()void -==== foo() ==== -METHOD : [public] : foo : ()void -void foo() -==== bar(java.lang.String,int,float) ==== -METHOD : [public] : bar : (java.lang.String,int,float)void -void bar(s,i,f) -==== main(java.lang.String[]) ==== -METHOD : [public, static] : main : (java.lang.String[])void -void main(args) -``` - -这给了你一些可以发现的东西,包括参数名和类型、返回值等。 - -### 更复杂的处理器 - -当你创建用于 javac 注解处理器时,你不能使用 Java 的反射特性,因为你处理的是源代码,而并非是编译后的 class 文件。各种 mirror[^3 ] 解决这个问题的方法是,通过允许你在未编译的源代码中查看方法、字段和类型。 - -如下是一个用于提取类中方法的注解,所以它可以被抽取成为一个接口: - -```java -// annotations/ifx/ExtractInterface.java -// javac-based annotation processing -package annotations.ifx; -import java.lang.annotation.*; -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.SOURCE) -public @interface ExtractInterface { - String interfaceName() default "-!!-"; -} -``` - -**RetentionPolicy** 的值为 **SOURCE**,这是为了在提取类中的接口之后不再将注解信息保留在 class 文件中。接下来的测试类提供了一些公用方法,这些方法可以成为接口的一部分: - -```java -// annotations/ifx/Multiplier.java -// javac-based annotation processing -// {java annotations.ifx.Multiplier} -package annotations.ifx; -@ExtractInterface(interfaceName="IMultiplier") -public class Multiplier { - public boolean flag = false; - private int n = 0; - public int multiply(int x, int y) { - int total = 0; - for(int i = 0; i < x; i++) - total = add(total, y); - return total; - } - public int fortySeven() { return 47; } - private int add(int x, int y) { - return x + y; - } - public double timesTen(double arg) { - return arg * 10; - } - public static void main(String[] args) { - Multiplier m = new Multiplier(); - System.out.println( - "11 * 16 = " + m.multiply(11, 16)); - } -} -``` - -输出为: - -```java -11 * 16 = 176 -``` - -**Multiplier** 类(只能处理正整数)拥有一个 `multiply()` 方法,这个方法会多次调用私有方法 `add()` 来模拟乘法操作。` add()` 是私有方法,因此不能成为接口的一部分。其他的方法提供了语法多样性。注解被赋予 **IMultiplier** 的 **InterfaceName** 作为要创建的接口的名称。 - -这里有一个编译时处理器用于提取有趣的方法,并创建一个新的 interface 源代码文件(这个源文件将会在下一轮中被自动编译): - -```java -// annotations/ifx/IfaceExtractorProcessor.java -// javac-based annotation processing -package annotations.ifx; -import javax.annotation.processing.*; -import javax.lang.model.SourceVersion; -import javax.lang.model.element.*; -import javax.lang.model.util.*; -import java.util.*; -import java.util.stream.*; -import java.io.*; -@SupportedAnnotationTypes( - "annotations.ifx.ExtractInterface") -@SupportedSourceVersion(SourceVersion.RELEASE_8) -public class IfaceExtractorProcessor - extends AbstractProcessor { - private ArrayList - interfaceMethods = new ArrayList<>(); - Elements elementUtils; - private ProcessingEnvironment processingEnv; - @Override - public void init( - ProcessingEnvironment processingEnv) { - this.processingEnv = processingEnv; - elementUtils = processingEnv.getElementUtils(); - } - @Override - public boolean process( - Set annotations, - RoundEnvironment env) { - for(Element elem:env.getElementsAnnotatedWith( - ExtractInterface.class)) { - String interfaceName = elem.getAnnotation( - ExtractInterface.class).interfaceName(); - for(Element enclosed : - elem.getEnclosedElements()) { - if(enclosed.getKind() - .equals(ElementKind.METHOD) && - enclosed.getModifiers() - .contains(Modifier.PUBLIC) && - !enclosed.getModifiers() - .contains(Modifier.STATIC)) { - interfaceMethods.add(enclosed); - } - } - if(interfaceMethods.size() > 0) - writeInterfaceFile(interfaceName); - } - return false; - } - private void - writeInterfaceFile(String interfaceName) { - try( - Writer writer = processingEnv.getFiler() - .createSourceFile(interfaceName) - .openWriter() - ) { - String packageName = elementUtils - .getPackageOf(interfaceMethods - .get(0)).toString(); - writer.write( - "package " + packageName + ";\n"); - writer.write("public interface " + - interfaceName + " {\n"); - for(Element elem : interfaceMethods) { - ExecutableElement method = - (ExecutableElement)elem; - String signature = " public "; - signature += method.getReturnType() + " "; - signature += method.getSimpleName(); - signature += createArgList( - method.getParameters()); - System.out.println(signature); - writer.write(signature + ";\n"); - } - writer.write("}"); - } catch(Exception e) { - throw new RuntimeException(e); - } - } - private String createArgList( - List parameters) { - String args = parameters.stream() - .map(p -> p.asType() + " " + p.getSimpleName()) - .collect(Collectors.joining(", ")); - return "(" + args + ")"; - } -} -``` - -**Elements** 对象实例 **elementUtils** 是一组静态方法的工具;我们用它来寻找 **writeInterfaceFile()** 中含有的包名。 - -`getEnclosedElements()`方法会通过指定的元素生成所有的“闭包”元素。在这里,这个类闭包了它的所有元素。通过使用 `getKind()` 我们会找到所有的 **public** 和 **static** 方法,并将其添加到 **interfaceMethods** 列表中。接下来 `writeInterfaceFile()` 使用 **interfaceMethods** 列表里面的值生成新的接口定义。注意,在 `writeInterfaceFile()` 使用了向下转型到 **ExecutableElement**,这使得我们可以获取所有的方法信息。**createArgList()** 是一个帮助方法,用于生成参数列表。 - -**Filer**是 `getFiler()` 生成的,并且是 **PrintWriter** 的一种实例,可以用于创建新文件。我们使用 **Filer** 对象,而不是原生的 **PrintWriter** 原因是,这个对象可以运行 **javac** 追踪你创建的新文件,这使得它可以在新一轮中检查新文件中的注解并编译文件。 - -如下是一个命令行,可以在编译的时候使用处理器: - -```shell -javac -processor annotations.ifx.IfaceExtractorProcessor Multiplier.java -``` - -新生成的 **IMultiplier.java** 的文件,正如你通过查看上面处理器的 `println()` 语句所猜测的那样,如下所示: - -```java -package annotations.ifx; -public interface IMultiplier { - public int multiply(int x, int y); - public int fortySeven(); - public double timesTen(double arg); -} -``` - -这个类同样会被 **javac** 编译(在某一轮中),所以你会在同一个目录中看到 **IMultiplier.class** 文件。 - - - -## 基于注解的单元测试 - -单元测试是对类中每个方法提供一个或者多个测试的一种事件,其目的是为了有规律的测试一个类中每个部分是否具备正确的行为。在 Java 中,最著名的单元测试工具就是 **JUnit**。**JUnit** 4 版本已经包含了注解。在注解版本之前的 JUnit 一个最主要的问题是,为了启动和运行 **JUnit** 测试,有大量的“仪式”需要标注。这种负担已经减轻了一些,**但是**注解使得测试更接近“可以工作的最简单的测试系统”。 - -在注解版本之前的 JUnit,你必须创建一个单独的文件来保存单元测试。通过注解,我们可以将单元测试集成在需要被测试的类中,从而将单元测试的时间和麻烦降到了最低。这种方式有额外的好处,就是使得测试私有方法和公有方法变的一样容易。 - -这个基于注解的测试框架叫做 **@Unit**。其最基本的测试形式,可能也是你使用的最多的一个注解是 **@Test**,我们使用 **@Test** 来标记测试方法。测试方法不带参数,并返回 **boolean** 结果来说明测试方法成功或者失败。你可以任意命名它的测试方法。同时 **@Unit** 测试方法可以是任意你喜欢的访问修饰方法,包括 **private**。 - -要使用 **@Unit**,你必须导入 **onjava.atunit** 包,并且使用 **@Unit** 的测试标记为合适的方法和字段打上标签(在接下来的例子中你会学到),然后让你的构建系统对编译后的类运行 **@Unit**,下面是一个简单的例子: - -```java -// annotations/AtUnitExample1.java -// {java onjava.atunit.AtUnit -// build/classes/main/annotations/AtUnitExample1.class} -package annotations; -import onjava.atunit.*; -import onjava.*; -public class AtUnitExample1 { - public String methodOne() { - return "This is methodOne"; - } - public int methodTwo() { - System.out.println("This is methodTwo"); - return 2; - } - @Test - boolean methodOneTest() { - return methodOne().equals("This is methodOne"); - } - @Test - boolean m2() { return methodTwo() == 2; } - @Test - private boolean m3() { return true; } - // Shows output for failure: - @Test - boolean failureTest() { return false; } - @Test - boolean anotherDisappointment() { - return false; - } -} -``` - -输出为: - -```java -annotations.AtUnitExample1 -. m3 -. methodOneTest -. m2 This is methodTwo -. failureTest (failed) -. anotherDisappointment (failed) -(5 tests) ->>> 2 FAILURES <<< -annotations.AtUnitExample1: failureTest -annotations.AtUnitExample1: anotherDisappointment -``` - -使用 **@Unit** 进行测试的类必须定义在某个包中(即必须包括 **package** 声明)。 - -**@Test** 注解被置于 `methodOneTest()`、 `m2()`、`m3()`、`failureTest()` 以及 a`notherDisappointment()` 方法之前,它们告诉 **@Unit** 方法作为单元测试来运行。同时 **@Test** 确保这些方法没有任何参数并且返回值为 **boolean** 或者 **void**。当你填写单元测试时,唯一需要做的就是决定测试是成功还是失败,(对于返回值为 **boolean** 的方法)应该返回 **ture** 还是 **false**。 - -如果你熟悉 **JUnit**,你还将注意到 **@Unit** 输出的信息更多。你会看到现在正在运行的测试的输出更有用,最后它会告诉你导致失败的类和测试。 - -你并非必须将测试方法嵌入到原来的类中,有时候这种事情根本做不到。要生产一个非嵌入式的测试,最简单的方式就是继承: - -```java -// annotations/AUExternalTest.java -// Creating non-embedded tests -// {java onjava.atunit.AtUnit -// build/classes/main/annotations/AUExternalTest.class} -package annotations; -import onjava.atunit.*; -import onjava.*; -public class AUExternalTest extends AtUnitExample1 { - @Test - boolean _MethodOne() { - return methodOne().equals("This is methodOne"); - } - @Test - boolean _MethodTwo() { - return methodTwo() == 2; - } -} -``` - -输出为: - -```java -annotations.AUExternalTest -. tMethodOne -. tMethodTwo This is methodTwo -OK (2 tests) -``` - -这个示例还表现出灵活命名的价值。在这里,**@Test** 方法被命名为下划线前缀加上要测试的方法名称(我并不认为这是一种理想的命名形式,这只是表现一种可能性罢了)。 - -你也可以使用组合来创建非嵌入式的测试: - -```java -// annotations/AUComposition.java -// Creating non-embedded tests -// {java onjava.atunit.AtUnit -// build/classes/main/annotations/AUComposition.class} -package annotations; -import onjava.atunit.*; -import onjava.*; -public class AUComposition { - AtUnitExample1 testObject = new AtUnitExample1(); - @Test - boolean tMethodOne() { - return testObject.methodOne() - .equals("This is methodOne"); - } - @Test - boolean tMethodTwo() { - return testObject.methodTwo() == 2; - } -} -``` - -输出为: - -```java -annotations.AUComposition -. tMethodTwo This is methodTwo -. tMethodOne -OK (2 tests) -``` - -因为在每一个测试里面都会创建 **AUComposition** 对象,所以创建新的成员变量 **testObject** 用于以后的每一个测试方法。 - -因为 **@Unit** 中没有 **JUnit** 中特殊的 **assert** 方法,不过另一种形式的 **@Test** 方法仍然允许返回值为 **void**(如果你还想使用 **true** 或者 **false** 的话,也可以使用 **boolean** 作为方法返回值类型)。为了表示测试成功,可以使用 Java 的 **assert** 语句。Java 断言机制需要你在 java 命令行行加上 **-ea** 标志来开启,但是 **@Unit** 已经自动开启了该功能。要表示测试失败的话,你甚至可以使用异常。**@Unit** 的设计目标之一就是尽可能减少添加额外的语法,而 Java 的 **assert** 和异常对于报告错误而言,即已经足够了。一个失败的 **assert** 或者从方法从抛出的异常都被视为测试失败,但是 **@Unit** 不会在这个失败的测试上卡住,它会继续运行,直到所有测试完毕,下面是一个示例程序: - -```java -// annotations/AtUnitExample2.java -// Assertions and exceptions can be used in @Tests -// {java onjava.atunit.AtUnit -// build/classes/main/annotations/AtUnitExample2.class} -package annotations; -import java.io.*; -import onjava.atunit.*; -import onjava.*; -public class AtUnitExample2 { - public String methodOne() { - return "This is methodOne"; - } - public int methodTwo() { - System.out.println("This is methodTwo"); - return 2; - } - @Test - void assertExample() { - assert methodOne().equals("This is methodOne"); - } - @Test - void assertFailureExample() { - assert 1 == 2: "What a surprise!"; - } - @Test - void exceptionExample() throws IOException { - try(FileInputStream fis = - new FileInputStream("nofile.txt")) {} // Throws - } - @Test - boolean assertAndReturn() { - // Assertion with message: - assert methodTwo() == 2: "methodTwo must equal 2"; - return methodOne().equals("This is methodOne"); - } -} -``` - -输出为: - -```java -annotations.AtUnitExample2 -. exceptionExample java.io.FileNotFoundException: -nofile.txt (The system cannot find the file specified) -(failed) -. assertExample -. assertAndReturn This is methodTwo -. assertFailureExample java.lang.AssertionError: What -a surprise! -(failed) -(4 tests) ->>> 2 FAILURES <<< -annotations.AtUnitExample2: exceptionExample -annotations.AtUnitExample2: assertFailureExample -``` - -如下是一个使用非嵌入式测试的例子,并且使用了断言,它将会对 **java.util.HashSet** 进行一些简单的测试: - -```java -// annotations/HashSetTest.java -// {java onjava.atunit.AtUnit -// build/classes/main/annotations/HashSetTest.class} -package annotations; -import java.util.*; -import onjava.atunit.*; -import onjava.*; -public class HashSetTest { - HashSet testObject = new HashSet<>(); - @Test - void initialization() { - assert testObject.isEmpty(); - } - @Test - void _Contains() { - testObject.add("one"); - assert testObject.contains("one"); - } - @Test - void _Remove() { - testObject.add("one"); - testObject.remove("one"); - assert testObject.isEmpty(); - } -} -``` - -采用继承的方式可能会更简单,也没有一些其他的约束。 - -对每一个单元测试而言,**@Unit** 都会使用默认的无参构造器,为该测试类所属的类创建出一个新的实例。并在此新创建的对象上运行测试,然后丢弃该对象,以免对其他测试产生副作用。如此创建对象导致我们依赖于类的默认构造器。如果你的类没有默认构造器,或者对象需要复杂的构造过程,那么你可以创建一个 **static** 方法专门负责构造对象,然后使用 **@TestObjectCreate** 注解标记该方法,例子如下: - -```java -// annotations/AtUnitExample3.java -// {java onjava.atunit.AtUnit -// build/classes/main/annotations/AtUnitExample3.class} -package annotations; -import onjava.atunit.*; -import onjava.*; -public class AtUnitExample3 { - private int n; - public AtUnitExample3(int n) { this.n = n; } - public int getN() { return n; } - public String methodOne() { - return "This is methodOne"; - } - public int methodTwo() { - System.out.println("This is methodTwo"); - return 2; - } - @TestObjectCreate - static AtUnitExample3 create() { - return new AtUnitExample3(47); - } - @Test - boolean initialization() { return n == 47; } - @Test - boolean methodOneTest() { - return methodOne().equals("This is methodOne"); - } - @Test - boolean m2() { return methodTwo() == 2; } -} -``` - -输出为: - -```java -annotations.AtUnitExample3 -. initialization -. m2 This is methodTwo -. methodOneTest -OK (3 tests) -``` - -**@TestObjectCreate** 修饰的方法必须声明为 **static** ,且必须返回一个你正在测试的类型对象,这一切都由 **@Unit** 负责确保成立。 - -有的时候,你需要向单元测试中增加一些字段。这时候可以使用 **@TestProperty** 注解,由它注解的字段表示只在单元测试中使用(因此,在你将产品发布给客户之前,他们应该被删除)。在下面的例子中,一个 **String** 通过 `String.split()` 方法进行分割,从其中读取一个值,这个值将会被生成测试对象: - -```java -// annotations/AtUnitExample4.java -// {java onjava.atunit.AtUnit -// build/classes/main/annotations/AtUnitExample4.class} -// {VisuallyInspectOutput} -package annotations; -import java.util.*; -import onjava.atunit.*; -import onjava.*; -public class AtUnitExample4 { - static String theory = "All brontosauruses " + - "are thin at one end, much MUCH thicker in the " + - "middle, and then thin again at the far end."; - private String word; - private Random rand = new Random(); // Time-based seed - public AtUnitExample4(String word) { - this.word = word; - } - public String getWord() { return word; } - public String scrambleWord() { - List chars = Arrays.asList( - ConvertTo.boxed(word.toCharArray())); - Collections.shuffle(chars, rand); - StringBuilder result = new StringBuilder(); - for(char ch : chars) - result.append(ch); - return result.toString(); - } - @TestProperty - static List input = - Arrays.asList(theory.split(" ")); - @TestProperty - static Iterator words = input.iterator(); - @TestObjectCreate - static AtUnitExample4 create() { - if(words.hasNext()) - return new AtUnitExample4(words.next()); - else - return null; - } - @Test - boolean words() { - System.out.println("'" + getWord() + "'"); - return getWord().equals("are"); - } - @Test - boolean scramble1() { -// Use specific seed to get verifiable results: - rand = new Random(47); - System.out.println("'" + getWord() + "'"); - String scrambled = scrambleWord(); - System.out.println(scrambled); - return scrambled.equals("lAl"); - } - @Test - boolean scramble2() { - rand = new Random(74); - System.out.println("'" + getWord() + "'"); - String scrambled = scrambleWord(); - System.out.println(scrambled); - return scrambled.equals("tsaeborornussu"); - } -} -``` - -输出为: - -```java -annotations.AtUnitExample4 -. words 'All' -(failed) -. scramble1 'brontosauruses' -ntsaueorosurbs -(failed) -. scramble2 'are' -are -(failed) -(3 tests) ->>> 3 FAILURES <<< -annotations.AtUnitExample4: words -annotations.AtUnitExample4: scramble1 -annotations.AtUnitExample4: scramble2 -``` - -**@TestProperty** 也可以用来标记那些只在测试中使用的方法,但是它们本身不是测试方法。 - -如果你的测试对象需要执行某些初始化工作,并且使用完成之后还需要执行清理工作,那么可以选择使用 **static** 的 **@TestObjectCleanup** 方法,当测试对象使用结束之后,该方法会为你执行清理工作。在下面的示例中,**@TestObjectCleanup** 为每一个测试对象都打开了一个文件,因此必须在丢弃测试的时候关闭该文件: - -```java -// annotations/AtUnitExample5.java -// {java onjava.atunit.AtUnit -// build/classes/main/annotations/AtUnitExample5.class} -package annotations; -import java.io.*; -import onjava.atunit.*; -import onjava.*; -public class AtUnitExample5 { - private String text; - public AtUnitExample5(String text) { - this.text = text; - } - @Override - public String toString() { return text; } - @TestProperty - static PrintWriter output; - @TestProperty - static int counter; - @TestObjectCreate - static AtUnitExample5 create() { - String id = Integer.toString(counter++); - try { - output = new PrintWriter("Test" + id + ".txt"); - } catch(IOException e) { - throw new RuntimeException(e); - } - return new AtUnitExample5(id); - } - @TestObjectCleanup - static void cleanup(AtUnitExample5 tobj) { - System.out.println("Running cleanup"); - output.close(); - } - @Test - boolean test1() { - output.print("test1"); - return true; - } - @Test - boolean test2() { - output.print("test2"); - return true; - } - @Test - boolean test3() { - output.print("test3"); - return true; - } -} -``` - -输出为: - -```java -annotations.AtUnitExample5 -. test1 -Running cleanup -. test3 -Running cleanup -. test2 -Running cleanup -OK (3 tests) -``` - -在输出中我们可以看到,清理方法会在每个测试方法结束之后自动运行。 - -### 在 @Unit 中使用泛型 -泛型为 **@Unit** 出了一个难题,因为我们不可能“通用测试”。我们必须针对某个特定类型的参数或者参数集才能进行测试。解决方法十分简单,让测试类继承自泛型类的一个特定版本即可: - -下面是一个 **stack** 的简单实现: -```java -package annotations; -import java.util.*; -public class StackL { - private LinkedList list = new LinkedList<>(); - public void push(T v) { list.addFirst(v); } - public T top() { return list.getFirst(); } - public T pop() { return list.removeFirst(); } -} -``` - -为了测试 String 版本,我们直接让测试类继承一个 Stack\ : - -```java -// annotations/StackLStringTst.java -// Applying @Unit to generics -// {java onjava.atunit.AtUnit -// build/classes/main/annotations/StackLStringTst.class} -package annotations; -import onjava.atunit.*; -import onjava.*; -public class -StackLStringTst extends StackL { - @Test - void tPush() { - push("one"); - assert top().equals("one"); - push("two"); - assert top().equals("two"); - } - @Test - void tPop() { - push("one"); - push("two"); - assert pop().equals("two"); - assert pop().equals("one"); - } - @Test - void tTop() { - push("A"); - push("B"); - assert top().equals("B"); - assert top().equals("B"); - } -} -``` - -输出为: - -```java -annotations.StackLStringTst -. tTop -. tPush -. tPop -OK (3 tests) -``` - -这种方法存在的唯一缺点是,继承使我们失去了访问被测试的类中 **private** 方法的能力。这对你非常重要,那你要么把 private 方法变为 **protected**,要么添加一个非 **private** 的 **@TestProperty** 方法,由它来调用 **private** 方法(稍后我们会看到,**AtUnitRemover** 会删除产品中的 **@TestProperty** 方法)。 - -**@Unit** 搜索那些包含合适注解的类文件,然后运行 **@Test** 方法。我的主要目标就是让 **@Unit** 测试系统尽可能的透明,使得人们使用它的时候只需要添加 **@Test** 注解,而不需要特殊的编码和知识(现在版本的 **JUnit** 符合这个实践)。不过,如果说编写测试不会遇到任何困难,也不太可能,因此 **@Unit** 会尽量让这些困难变的微不足道,希望通过这种方式,你们会更乐意编写测试。 - -### 实现 @Unit - -首先我们需要定义所有的注解类型。这些都是简单的标签,并且没有任何字段。@Test 标签在本章开头已经定义过了,这里是其他所需要的注解: - -```java -// onjava/atunit/TestObjectCreate.java -// The @Unit @TestObjectCreate tag -package onjava.atunit; -import java.lang.annotation.*; -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface TestObjectCreate {} -``` - -```java -// onjava/atunit/TestObjectCleanup.java -// The @Unit @TestObjectCleanup tag -package onjava.atunit; -import java.lang.annotation.*; -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface TestObjectCleanup {} -``` - -```java -// onjava/atunit/TestProperty.java -// The @Unit @TestProperty tag -package onjava.atunit; -import java.lang.annotation.*; -// Both fields and methods can be tagged as properties: -@Target({ElementType.FIELD, ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface TestProperty {} -``` - -所有测试的保留属性都为 **RUNTIME**,这是因为 **@Unit** 必须在编译后的代码中发现这些注解。 - -要实现系统并运行测试,我们还需要反射机制来提取注解。下面这个程序通过注解中的信息,决定如何构造测试对象,并在测试对象上运行测试。正是由于注解帮助,这个程序才会如此短小而直接: - -```java -// onjava/atunit/AtUnit.java -// An annotation-based unit-test framework -// {java onjava.atunit.AtUnit} -package onjava.atunit; -import java.lang.reflect.*; -import java.io.*; -import java.util.*; -import java.nio.file.*; -import java.util.stream.*; -import onjava.*; -public class AtUnit implements ProcessFiles.Strategy { - static Class testClass; - static List failedTests= new ArrayList<>(); - static long testsRun = 0; - static long failures = 0; - public static void - main(String[] args) throws Exception { - ClassLoader.getSystemClassLoader() - .setDefaultAssertionStatus(true); // Enable assert - new ProcessFiles(new AtUnit(), "class").start(args); - if(failures == 0) - System.out.println("OK (" + testsRun + " tests)"); - else { - System.out.println("(" + testsRun + " tests)"); - System.out.println( - "\n>>> " + failures + " FAILURE" + - (failures > 1 ? "S" : "") + " <<<"); - for(String failed : failedTests) - System.out.println(" " + failed); - } - } - @Override - public void process(File cFile) { - try { - String cName = ClassNameFinder.thisClass( - Files.readAllBytes(cFile.toPath())); - if(!cName.startsWith("public:")) - return; - cName = cName.split(":")[1]; - if(!cName.contains(".")) - return; // Ignore unpackaged classes - testClass = Class.forName(cName); - } catch(IOException | ClassNotFoundException e) { - throw new RuntimeException(e); - } - TestMethods testMethods = new TestMethods(); - Method creator = null; - Method cleanup = null; - for(Method m : testClass.getDeclaredMethods()) { - testMethods.addIfTestMethod(m); - if(creator == null) - creator = checkForCreatorMethod(m); - if(cleanup == null) - cleanup = checkForCleanupMethod(m); - } - if(testMethods.size() > 0) { - if(creator == null) - try { - if(!Modifier.isPublic(testClass - .getDeclaredConstructor() - .getModifiers())) { - System.out.println("Error: " + testClass + - " no-arg constructor must be public"); - System.exit(1); - } - } catch(NoSuchMethodException e) { -// Synthesized no-arg constructor; OK - } - System.out.println(testClass.getName()); - } - for(Method m : testMethods) { - System.out.print(" . " + m.getName() + " "); - try { - Object testObject = createTestObject(creator); - boolean success = false; - try { - if(m.getReturnType().equals(boolean.class)) - success = (Boolean)m.invoke(testObject); - else { - m.invoke(testObject); - success = true; // If no assert fails - } - } catch(InvocationTargetException e) { -// Actual exception is inside e: - System.out.println(e.getCause()); - } - System.out.println(success ? "" : "(failed)"); - testsRun++; - if(!success) { - failures++; - failedTests.add(testClass.getName() + - ": " + m.getName()); - } - if(cleanup != null) - cleanup.invoke(testObject, testObject); - } catch(IllegalAccessException | - IllegalArgumentException | - InvocationTargetException e) { - throw new RuntimeException(e); - } - } - } - public static - class TestMethods extends ArrayList { - void addIfTestMethod(Method m) { - if(m.getAnnotation(Test.class) == null) - return; - if(!(m.getReturnType().equals(boolean.class) || - m.getReturnType().equals(void.class))) - throw new RuntimeException("@Test method" + - " must return boolean or void"); - m.setAccessible(true); // If it's private, etc. - add(m); - } - } - private static - Method checkForCreatorMethod(Method m) { - if(m.getAnnotation(TestObjectCreate.class) == null) - return null; - if(!m.getReturnType().equals(testClass)) - throw new RuntimeException("@TestObjectCreate " + - "must return instance of Class to be tested"); - if((m.getModifiers() & - java.lang.reflect.Modifier.STATIC) < 1) - throw new RuntimeException("@TestObjectCreate " + - "must be static."); - m.setAccessible(true); - return m; - } - private static - Method checkForCleanupMethod(Method m) { - if(m.getAnnotation(TestObjectCleanup.class) == null) - return null; - if(!m.getReturnType().equals(void.class)) - throw new RuntimeException("@TestObjectCleanup " + - "must return void"); - if((m.getModifiers() & - java.lang.reflect.Modifier.STATIC) < 1) - throw new RuntimeException("@TestObjectCleanup " + - "must be static."); - if(m.getParameterTypes().length == 0 || - m.getParameterTypes()[0] != testClass) - throw new RuntimeException("@TestObjectCleanup " + - "must take an argument of the tested type."); - m.setAccessible(true); - return m; - } - private static Object - createTestObject(Method creator) { - if(creator != null) { - try { - return creator.invoke(testClass); - } catch(IllegalAccessException | - IllegalArgumentException | - InvocationTargetException e) { - throw new RuntimeException("Couldn't run " + - "@TestObject (creator) method."); - } - } else { // Use the no-arg constructor: - try { - return testClass.newInstance(); - } catch(InstantiationException | - IllegalAccessException e) { - throw new RuntimeException( - "Couldn't create a test object. " + - "Try using a @TestObject method."); - } - } - } -} -``` - -虽然它可能是“过早的重构”(因为它只在书中使用过一次),**AtUnit.java** 使用了 **ProcessFiles** 工具逐步判断命令行中的参数,决定它是一个目录还是文件,并采取相应的行为。这可以应用于不同的解决方法,是因为它包含了一个 可用于自定义的 **Strategy** 接口: - -```java -// onjava/ProcessFiles.java -package onjava; -import java.io.*; -import java.nio.file.*; -public class ProcessFiles { - public interface Strategy { - void process(File file); - } - private Strategy strategy; - private String ext; - public ProcessFiles(Strategy strategy, String ext) { - this.strategy = strategy; - this.ext = ext; - } - public void start(String[] args) { - try { - if(args.length == 0) - processDirectoryTree(new File(".")); - else - for(String arg : args) { - File fileArg = new File(arg); - if(fileArg.isDirectory()) - processDirectoryTree(fileArg); - else { -// Allow user to leave off extension: - if(!arg.endsWith("." + ext)) - arg += "." + ext; - strategy.process( - new File(arg).getCanonicalFile()); - } - } - } catch(IOException e) { - throw new RuntimeException(e); - } - } - public void processDirectoryTree(File root) throws IOException { - PathMatcher matcher = FileSystems.getDefault() - .getPathMatcher("glob:**/*.{" + ext + "}"); - Files.walk(root.toPath()) - .filter(matcher::matches) - .forEach(p -> strategy.process(p.toFile())); - } -} -``` - -**AtUnit** 类实现了 **ProcessFiles.Strategy**,其包含了一个 `process()` 方法。在这种方式下,**AtUnit** 实例可以作为参数传递给 **ProcessFiles** 构造器。第二个构造器的参数告诉 **ProcessFiles** 如寻找所有包含 “class” 拓展名的文件。 - -如下是一个简单的使用示例: - -```java -// annotations/DemoProcessFiles.java -import onjava.ProcessFiles; -public class DemoProcessFiles { - public static void main(String[] args) { - new ProcessFiles(file -> System.out.println(file), - "java").start(args); - } -} -``` - -输出为: - -```java -.\AtUnitExample1.java -.\AtUnitExample2.java -.\AtUnitExample3.java -.\AtUnitExample4.java -.\AtUnitExample5.java -.\AUComposition.java -.\AUExternalTest.java -.\database\Constraints.java -.\database\DBTable.java -.\database\Member.java -.\database\SQLInteger.java -.\database\SQLString.java -.\database\TableCreator.java -.\database\Uniqueness.java -.\DemoProcessFiles.java -.\HashSetTest.java -.\ifx\ExtractInterface.java -.\ifx\IfaceExtractorProcessor.java -.\ifx\Multiplier.java -.\PasswordUtils.java -.\simplest\Simple.java -.\simplest\SimpleProcessor.java -.\simplest\SimpleTest.java -.\SimulatingNull.java -.\StackL.java -.\StackLStringTst.java -.\Testable.java -.\UseCase.java -.\UseCaseTracker.java -``` - -如果没有命令行参数,这个程序会遍历当前的目录树。你还可以提供多个参数,这些参数可以是类文件(带或不带.class扩展名)或目录。 - -回到我们对 **AtUnit.java** 的讨论,因为 **@Unit** 会自动找到可测试的类和方法,所以不需要“套件”机制。 - -**AtUnit.java** 中存在的一个我们必须要解决的问题是,当它发现类文件时,类文件名中的限定类名(包括包)不明显。为了发现这个信息,必须解析类文件 - 这不是微不足道的,但也不是不可能的。 找到 .class 文件时,会打开它并读取其二进制数据并将其传递给 `ClassNameFinder.thisClass()`。 在这里,我们正在进入“字节码工程”领域,因为我们实际上正在分析类文件的内容: - -```java -// onjava/atunit/ClassNameFinder.java -// {java onjava.atunit.ClassNameFinder} -package onjava.atunit; -import java.io.*; -import java.nio.file.*; -import java.util.*; -import onjava.*; -public class ClassNameFinder { - public static String thisClass(byte[] classBytes) { - Map offsetTable = new HashMap<>(); - Map classNameTable = new HashMap<>(); - try { - DataInputStream data = new DataInputStream( - new ByteArrayInputStream(classBytes)); - int magic = data.readInt(); // 0xcafebabe - int minorVersion = data.readShort(); - int majorVersion = data.readShort(); - int constantPoolCount = data.readShort(); - int[] constantPool = new int[constantPoolCount]; - for(int i = 1; i < constantPoolCount; i++) { - int tag = data.read(); - // int tableSize; - switch(tag) { - case 1: // UTF - int length = data.readShort(); - char[] bytes = new char[length]; - for(int k = 0; k < bytes.length; k++) - bytes[k] = (char)data.read(); - String className = new String(bytes); - classNameTable.put(i, className); - break; - case 5: // LONG - case 6: // DOUBLE - data.readLong(); // discard 8 bytes - i++; // Special skip necessary - break; - case 7: // CLASS - int offset = data.readShort(); - offsetTable.put(i, offset); - break; - case 8: // STRING - data.readShort(); // discard 2 bytes - break; - case 3: // INTEGER - case 4: // FLOAT - case 9: // FIELD_REF - case 10: // METHOD_REF - case 11: // INTERFACE_METHOD_REF - case 12: // NAME_AND_TYPE - case 18: // Invoke Dynamic - data.readInt(); // discard 4 bytes - break; - case 15: // Method Handle - data.readByte(); - data.readShort(); - break; - case 16: // Method Type - data.readShort(); - break; - default: - throw - new RuntimeException("Bad tag " + tag); - } - } - short accessFlags = data.readShort(); - String access = (accessFlags & 0x0001) == 0 ? - "nonpublic:" : "public:"; - int thisClass = data.readShort(); - int superClass = data.readShort(); - return access + classNameTable.get( - offsetTable.get(thisClass)).replace('/', '.'); - } catch(IOException | RuntimeException e) { - throw new RuntimeException(e); - } - } - // Demonstration: - public static void main(String[] args) throws Exception { - PathMatcher matcher = FileSystems.getDefault() - .getPathMatcher("glob:**/*.class"); -// Walk the entire tree: - Files.walk(Paths.get(".")) - .filter(matcher::matches) - .map(p -> { - try { - return thisClass(Files.readAllBytes(p)); - } catch(Exception e) { - throw new RuntimeException(e); - } - }) - .filter(s -> s.startsWith("public:")) -// .filter(s -> s.indexOf('$') >= 0) - .map(s -> s.split(":")[1]) - .filter(s -> !s.startsWith("enums.")) - .filter(s -> s.contains(".")) - .forEach(System.out::println); - } -} -``` - -输出为: - -```java -onjava.ArrayShow -onjava.atunit.AtUnit$TestMethods -onjava.atunit.AtUnit -onjava.atunit.ClassNameFinder -onjava.atunit.Test -onjava.atunit.TestObjectCleanup -onjava.atunit.TestObjectCreate -onjava.atunit.TestProperty -onjava.BasicSupplier -onjava.CollectionMethodDifferences -onjava.ConvertTo -onjava.Count$Boolean -onjava.Count$Byte -onjava.Count$Character -onjava.Count$Double -onjava.Count$Float -onjava.Count$Integer -onjava.Count$Long -onjava.Count$Pboolean -onjava.Count$Pbyte -onjava.Count$Pchar -onjava.Count$Pdouble -onjava.Count$Pfloat -onjava.Count$Pint -onjava.Count$Plong -onjava.Count$Pshort -onjava.Count$Short -onjava.Count -onjava.CountingIntegerList -onjava.CountMap -onjava.Countries -onjava.Enums -onjava.FillMap -onjava.HTMLColors -onjava.MouseClick -onjava.Nap -onjava.Null -onjava.Operations -onjava.OSExecute -onjava.OSExecuteException -onjava.Pair -onjava.ProcessFiles$Strategy -onjava.ProcessFiles -onjava.Rand$Boolean -onjava.Rand$Byte -onjava.Rand$Character -onjava.Rand$Double -onjava.Rand$Float -onjava.Rand$Integer -onjava.Rand$Long -onjava.Rand$Pboolean -onjava.Rand$Pbyte -onjava.Rand$Pchar -onjava.Rand$Pdouble -onjava.Rand$Pfloat -onjava.Rand$Pint -onjava.Rand$Plong -onjava.Rand$Pshort -onjava.Rand$Short -onjava.Rand$String -onjava.Rand -onjava.Range -onjava.Repeat -onjava.RmDir -onjava.Sets -onjava.Stack -onjava.Suppliers -onjava.TimedAbort -onjava.Timer -onjava.Tuple -onjava.Tuple2 -onjava.Tuple3 -onjava.Tuple4 -onjava.Tuple5 -onjava.TypeCounter -``` - - 虽然无法在这里介绍其中所有的细节,但是每个类文件都必须遵循一定的格式,而我已经尽力用有意义的字段来表示这些从 **ByteArrayInputStream** 中提取出来的数据片段。通过施加在输入流上的读操作,你能看出每个信息片的大小。例如每一个类的头 32 个 bit 总是一个 “神秘数字” **0xcafebabe**,而接下来的两个 **short** 值是版本信息。常量池包含了程序的常量,所以这是一个可变的值。接下来的 **short** 告诉我们这个常量池有多大,然后我们为其创建一个尺寸合适的数组。常量池中的每一个元素,其长度可能是固定式,也可能是可变的值,因此我们必须检查每一个常量的起始标记,然后才能知道该怎么做,这就是 switch 语句的工作。我们并不打算精确的分析类中所有的数据,仅仅是从文件的起始一步一步的走,直到取得我们所需的信息,因此你会发现,在这个过程中我们丢弃了大量的数据。关于类的信息都保存在 **classNameTable** 和 **offsetTable** 中。在读取常量池之后,就找到了 **this_class** 信息,这是 **offsetTable** 的一个坐标,通过它可以找到进入 **classNameTable** 的坐标,然后就可以得到我们所需的类的名字了。 - -现在让我们回到 **AtUtil.java** 中,process() 方法中拥有了类的名字,然后检查它是否包含“.”,如果有就表示该类定义于一个包中。没有包的类会被忽略。如果一个类在包中,那么我们就可以使用标准的类加载器通过 `Class.forName()` 将其加载进来。现在我们可以对这个类进行 **@Unit** 注解的分析工作了。 - -我们只需要关注三件事:首先是 **@Test** 方法,它们被保存在 **TestMehtods** 列表中,然后检查其是否具有 @TestObjectCreate 和 **@TestObjectCleanup****** 方法。从代码中可以看到,我们通过调用相应的方法来查询注解从而找到这些方法。 - -每找到一个 @Test 方法,就打印出来当前类的名字,于是观察者立刻就可以知道发生了什么。接下来开始执行测试,也就是打印出方法名,然后调用 createTestObject() (如果存在一个加了 @TestObjectCreate 注解的方法),或者调用默认构造器。一旦创建出来测试对象,如果调用其上的测试方法。如果测试的返回值为 boolean,就捕获该结果。如果测试方法没有返回值,那么就没有异常发生,我们就假设测试成功,反之,如果当 assert 失败或者有任何异常抛出的时候,就说明测试失败,这时将异常信息打印出来以显示错误的原因。如果有失败的测试发生,那么还要统计失败的次数,并将失败所属的类和方法加入到 failedTests 中,以便最后报告给用户。 - - - -## 本章小结 - -注解是 Java 引入的一项非常受欢迎的补充,它提供了一种结构化,并且具有类型检查能力的新途径,从而使得你能够为代码中加入元数据,而且不会导致代码杂乱并难以阅读。使用注解能够帮助我们避免编写累赘的部署描述性文件,以及其他的生成文件。而 Javadoc 中的 @deprecated 被 @Deprecated 注解所替代的事实也说明,与注释性文字相比,注解绝对更适用于描述类相关的信息。 - -Java 提供了很少的内置注解。这意味着如果你在别处找不到可用的类库,那么就只能自己创建新的注解以及相应的处理器。通过将注解处理器链接到 javac,你可以一步完成编译新生成的文件,简化了构造过程。 - -API 的提供方和框架将会将注解作为他们工具的一部分。通过 @Unit 系统,我们可以想象,注解会极大的改变我们的 Java 编程体验。 - - - -

- -[^3 ]: The Java designers coyly suggest that a mirror is where you find a reflection. \ No newline at end of file diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/AQS\345\216\237\347\220\206\345\210\206\346\236\220.md" "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/AQS\345\216\237\347\220\206\346\265\205\346\236\220.md" similarity index 57% rename from "JDK/\345\271\266\345\217\221\347\274\226\347\250\213/AQS\345\216\237\347\220\206\345\210\206\346\236\220.md" rename to "JDK/\345\271\266\345\217\221\347\274\226\347\250\213/AQS\345\216\237\347\220\206\346\265\205\346\236\220.md" index 21ce449031..e1261e2308 100644 --- "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/AQS\345\216\237\347\220\206\345\210\206\346\236\220.md" +++ "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/AQS\345\216\237\347\220\206\346\265\205\346\236\220.md" @@ -1,79 +1,80 @@ +# 1 AQS(AbstractQueuedSynchronizer) +## 1.1 基本模型 +![](https://upload-images.jianshu.io/upload_images/4685968-a7056d4c8afa2bfa.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-28eb1053dd96343e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-02e979128dd1a9fa.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![CLH 队列( FIFO)](https://upload-images.jianshu.io/upload_images/4685968-fb7f2d5bbff78f38.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 1.2 类型 -AQS定义两种资源共享方式: -### 独占锁 +![](https://upload-images.jianshu.io/upload_images/4685968-4694e4866f31515a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 独占锁 每次只能有一个线程能持有锁,ReentrantLock就是以独占方式实现的互斥锁。 - -独占锁是一种悲观保守的加锁策略,它避免了读/读冲突。若某只读线程获取锁,则其它读线程都只能等待,这就限制了不必要的并发性,因为读操作并不会影响数据的一致性。 -### 共享锁 -允许多个线程同时获取锁,并发访问共享资源。 - -共享锁则是一种乐观锁,放宽加锁策略,允许多个执行读操作的线程同时访问共享资源。 ReadWriteLock,读-写锁,就允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但二者不能同时进行。 - -AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,分别标识AQS队列中等待线程的锁获取模式。 -![](https://img-blog.csdnimg.cn/img_convert/5f278a7e90f48d306a954cf6aded59fa.png) -![](https://img-blog.csdnimg.cn/img_convert/aacccb326883560e0b5001362ff1ee6c.png) +独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性 +- 共享锁 +允许多个线程同时获取锁,并发访问共享资源 +共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。 java的并发包中提供了ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行 +AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,他们分别标识 AQS队列中等待线程的锁获取模式。 +![](https://upload-images.jianshu.io/upload_images/4685968-3ceb1ec59d70d65b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-f19f67a533d4500a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 1.3 锁的公平与非公平 -指线程请求获取锁的过程中,是否允许插队。 - -- 在公平锁上,线程将按他们发出请求的顺序来获得锁 -- 非公平锁则允许在线程发出请求后立即尝试获取锁,若可用则可直接获取锁,尝试失败才进行排队等待 - -比如ReentrantLock,就提供了两种锁获取方式 +锁的公平与非公平,是指线程请求获取锁的过程中,是否允许插队。 +在公平锁上,线程将按他们发出请求的顺序来获得锁;而非公平锁则允许在线程发出请求后立即尝试获取锁,如果可用则可直接获取锁,尝试失败才进行排队等待。 +ReentrantLock提供了两种锁获取方式 - FairSyn - NofairSync +结论:ReentrantLock是以独占锁的加锁策略实现的互斥锁,同时它提供了公平和非公平两种锁获取方式。最初看源码时竟然把这两个概念弄混了 ## 1.4 架构 AQS提供了独占锁和共享锁必须实现的方法 - 具有独占锁功能的子类 它必须实现tryAcquire、tryRelease、isHeldExclusively等,ReentrantLock是一种独占锁 - 共享锁功能的子类 必须实现tryAcquireShared和tryReleaseShared等方法,带有Shared后缀的方法都是支持共享锁加锁的语义。Semaphore是一种共享锁 -![](https://img-blog.csdnimg.cn/img_convert/55ba6b977b57640f28d7e1bc9dfae7dc.png) +![](https://upload-images.jianshu.io/upload_images/4685968-4b7cf4a30adb76b1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 这里就以`ReentrantLock`排它锁为例开始讲解如何利用AQS # 2 ReentrantLock ## 2.1 构造方法 -![](https://img-blog.csdnimg.cn/img_convert/2da34fb550bbcc114d91caa9eb1809f8.png) +![](https://upload-images.jianshu.io/upload_images/4685968-dc5fef0586950cf3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 显然,对象中有一个属性叫`sync` -![](https://img-blog.csdnimg.cn/img_convert/346181cafa2488e883433dddb20b48da.png) +![](https://upload-images.jianshu.io/upload_images/4685968-6044db1e72a633b5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 有两种不同的实现类 - `NonfairSync`(默认实现) - `FairSync` 它们都是排它锁的内部类,不论用哪一个都能实现排它锁,只是内部可能有点原理上的区别。先以`NonfairSync`为例 -![](https://img-blog.csdnimg.cn/img_convert/85eba502320fd9826fb8fbf0e3061a3e.png) +![](https://upload-images.jianshu.io/upload_images/4685968-9e0903ecdcf73bf8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) `lock()`先通过CAS尝试将状态从0修改为1 - 若直接修改成功,前提条件自然是锁的状态为0,则直接将线程的OWNER修改为当前线程,这是一种理想情况,如果并发粒度设置适当也是一种乐观情况 - 若该动作未成功,则会间接调用`acquire(1)` -![](https://img-blog.csdnimg.cn/img_convert/d86afb34609f0e42d3cbb2df9b579c20.png) +![](https://upload-images.jianshu.io/upload_images/4685968-a5d664e36dbcf27e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) `acquire(int)`就定义在`AbstractQueuedSynchronizer` ## tryAcquire / tryRelease 首先看`tryAcquire(arg)`的调用(传入的参数是1),在`NonfairSync`中,会这样来实现 -![](https://img-blog.csdnimg.cn/img_convert/c89725bcf7870eb715c3a55b543a9b75.png) -![](https://img-blog.csdnimg.cn/img_convert/ddd5bbf4995e9da1896f79f596e524bf.png) +![](https://upload-images.jianshu.io/upload_images/4685968-7c66ff929e8dbac6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-933f1ae5b6b3a8b1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 首先获取这个锁的状态 - 若状态为0,则尝试设置状态为传入的参数(这里就是1),若设置成功就代表自己获取到了锁,返回`true` 状态为0设置1的动作在外部就有做过一次,内部再一次做只是提升概率,而且这样的操作相对锁来讲不占开销 - 若状态非0,则判定当前线程是否为排它锁的Owner,如果是Owner则尝试将状态增加acquires(也就是增加1),如果这个状态值溢出,则抛异常,否则即将状态设置进去后返回true(实现类似于偏向的功能,可重入,但是无需进一步征用) - 如果状态不是0,且自身不是owner,则返回false -![image.png](https://img-blog.csdnimg.cn/img_convert/8b12f2bc8eee2ccdeab36151f3e825a2.png) +![image.png](https://upload-images.jianshu.io/upload_images/4685968-9bf53d5d8bfadc0b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 回到对`acquire()`的调用判定中是通过`if(!tryAcquire())`作为第1个条件的,如果返回true,则判定就不会成立了,自然后面的acquireQueued动作就不会再执行了,如果发生这样的情况是最理想的。 无论多么乐观,征用是必然存在的,如果征用存在则owner自然不会是自己,`tryAcquire()`会返回false,接着就会再调用方法:`acquireQueued(addWaiter(Node.EXCLUSIVE), arg)` 这个方法的调用的代码更不好懂,需要从里往外看,这里的Node.EXCLUSIVE是节点的类型 -![](https://img-blog.csdnimg.cn/img_convert/83ce028ec3196994fd58dd0e82e58a4c.png) +![](https://upload-images.jianshu.io/upload_images/4685968-b7654e6a89dfa0ec.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 看名称应该清楚是排它类型的意思。接着调用addWaiter()来增加一个排它锁类型的节点,这个addWaiter()的代码是这样写的: -![](https://img-blog.csdnimg.cn/img_convert/95519b69b2739093402801b4bd73e6d2.png) +![](https://upload-images.jianshu.io/upload_images/4685968-a1955d0e941422c5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 这里创建了一个Node的对象,将当前线程和`Node.EXCLUSIVE`模式传入,也就是说Node节点理论上包含了这两项信息 代码中的tail是AQS的一个属性 -![](https://img-blog.csdnimg.cn/img_convert/f7a6ad5be62368c272e187d91fc2bd69.png) +![](https://upload-images.jianshu.io/upload_images/4685968-9a14f387f33cf1fb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 刚开始的时候肯定是为`null`,也就是不会进入第一层if判定的区域,而直接会进入`enq(node)`的代码 -![](https://img-blog.csdnimg.cn/img_convert/b1232f867932d50262785c4789d0e376.png) -AQS是链表,而且它还应该有一个head引用来指向链表的头节点。AQS在初始化时head = tail = null,在运行时来回移动。 - -AQS是一个基于状态(state)的链表管理方式: -```java +![](https://upload-images.jianshu.io/upload_images/4685968-361894babed357bd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +看到了`tail`就应该猜到了`AQS是链表`,而且它还应该有一个head引用来指向链表的头节点 +AQS在初始化的时候head、tail都是null,在运行时来回移动 +AQS是一个基于状态(state)的链表管理方式 +``` /** * 这个插入会检测head tail 的初始化, 必要的话会初始化一个 dummy 节点, 这个和 ConcurrentLinkedQueue 一样的 */ @@ -86,18 +87,14 @@ AQS是一个基于状态(state)的链表管理方式: * 3. 这时再在tail后面添加节点(这一步可能失败, 可能发生竞争被其他的线程抢占) * 这里为什么要加入一个 dummy 节点呢? * 这里的 Sync Queue 是CLH lock的一个变种, 线程节点 node 能否获取lock的判断通过其前继节点 - * 而且这里在当前节点想获取lock时通常给前继节点 打SIGNAL标识(表示当【前继节点】释放lock,需要通知我来获取lock) + * 而且这里在当前节点想获取lock时通常给前继节点 打上 signal 的标识(表示当前继节点释放lock需要通知我来获取lock) * 若这里不清楚的同学, 请先看看 CLH lock的资料 (这是理解 AQS 的基础) */ private Node enq(final Node node){ for(;;){ Node t = tail; - // 1. 队列为空 - // 初始化一个 dummy 节点 其实和 ConcurrentLinkedQueue 一样 - if(t == null){ // Must initialize - // 2. 初始化 head 与 tail - // 这个CAS成功后, head 就有值了 - if(compareAndSetHead(new Node())){ + if(t == null){ // Must initialize // 1. 队列为空 初始化一个 dummy 节点 其实和 ConcurrentLinkedQueue 一样 + if(compareAndSetHead(new Node())){ // 2. 初始化 head 与 tail (这个CAS成功后, head 就有值了, 详情将 Unsafe 操作) tail = head; } }else{ @@ -114,22 +111,30 @@ private Node enq(final Node node){ 首先这个是一个死循环,而且本身没有锁,因此可以有多个线程进来,假如某个线程进入方法 此时head、tail都是null,自然会进入 `if(t == null)`创建一个Node,这个Node没有像开始那样给予类型和线程,很明显是一个空的Node对象 -![此时传入的node和某一个线程创建的Node对象](https://img-blog.csdnimg.cn/img_convert/7daa504e41d2dfd372b50fddbcb44e7f.png) +![此时传入的node和某一个线程创建的Node对象](https://upload-images.jianshu.io/upload_images/4685968-d7c4592ccdc0cdef.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 刚才我们很理想的认为只有一个线程会出现这种情况,如果有多个线程并发进入这个if判定区域,可能就会同时存在多个这样的数据结构,在各自形成数据结构后,多个线程都会去做`compareAndSetHead(new Node())`的动作,也就是尝试将这个临时节点设置为head -![](https://img-blog.csdnimg.cn/img_convert/e5dc4e25643df72f4964da00e93b9f01.png) -显然并发时只有一个线程会成功,因此成功的那个线程会执行`tail = head`,整个AQS的链表就成为![AQS被第一个请求成功的线程初始化后](https://img-blog.csdnimg.cn/img_convert/a04559886cf63928b976020b8e49555e.png) +![](https://upload-images.jianshu.io/upload_images/4685968-e26e3ab3046baead.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +显然并发时只有一个线程会成功,因此成功的那个线程会执行`tail = head`,整个AQS的链表就成为![AQS被第一个请求成功的线程初始化后](https://upload-images.jianshu.io/upload_images/4685968-c116fb5dfd8cfd5f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 有一个线程会成功修改head和tail的值,其它的线程会继续循环,再次循环会进入else 在else语句所在的逻辑中 - 第一步是`node.prev = t`,这个t就是tail的临时值,也就是首先让尝试写入的node节点的prev指针指向原来的结束节点 - 然后尝试通过CAS替换掉AQS中的tail的内容为当前线程的Node -![](https://img-blog.csdnimg.cn/img_convert/00fb6be2016e81e5cb3162dc52e6d2a5.png) +![](https://upload-images.jianshu.io/upload_images/4685968-05e2c8e3d92d064f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - 无论有多少个线程并发到这里,依然只会有一个能成功,成功者执行 `t.next = node`,也就是让原先的tail节点的next引用指向现在的node,现在的node已经成为了最新的结束节点,不成功者则会继续循环 -![插入一个节点步骤前后动作](https://img-blog.csdnimg.cn/img_convert/d8f71898fb7fbd75671e44fafdc4e8b4.png) +![插入一个节点步骤前后动作](https://upload-images.jianshu.io/upload_images/4685968-7bb84a7833688a43.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 总之节点都是在链表尾部写入的,而且是线程安全的 知道了AQS大致的写入是一种双向链表的插入操作,但插入链表节点对锁有何用途呢,我们还得退回到`addWaiter`最终返回了要写入的node节点, 再回退到`acquire ()`中所在的代码中需要将这个返回的node节点作为`acquireQueued`入口参数,并传入另一个参数(依然是1),看看它里面到底做了些什么 -```java +``` +/** + * Acquires in exclusive uninterruptible mode for thread already in + * queue. Used by condition wait methods as well as acquire + * + * @param node the node + * @param arg the acquire argument + * @return {@code} if interrupted while waiting + */ /** * 不支持中断的获取锁 */ @@ -145,92 +150,45 @@ final boolean acquireQueued(final Node node, int arg){ failed = false; return interrupted; // 4. 返回在整个获取的过程中是否被中断过 ; 但这又有什么用呢? 若整个过程中被中断过, 则最后我在 自我中断一下 (selfInterrupt), 因为外面的函数可能需要知道整个过程是否被中断过 } - // 5. 调用 shouldParkAfterFailedAcquire 判断是否需要中断 - // 这里可能会一开始返回 false,但在此进去后直接返回true - // 主要和前继节点的状态是否是SIGNAL有关 - if(shouldParkAfterFailedAcquire(p, node) && - // 6. 现在lock还是被其他线程占用 那就睡一会, 返回值判断是否这次线程的唤醒是被中断唤醒 - parkAndCheckInterrupt()){ + if(shouldParkAfterFailedAcquire(p, node) && // 5. 调用 shouldParkAfterFailedAcquire 判断是否需要中断(这里可能会一开始 返回 false, 但在此进去后直接返回 true(主要和前继节点的状态是否是 signal)) + parkAndCheckInterrupt()){ // 6. 现在lock还是被其他线程占用 那就睡一会, 返回值判断是否这次线程的唤醒是被中断唤醒 interrupted = true; } } }finally { - // 7. 在整个获取中出错 - if(failed){ + if(failed){ // 7. 在整个获取中出错 cancelAcquire(node); // 8. 清除 node 节点(清除的过程是先给 node 打上 CANCELLED标志, 然后再删除) } } } ``` 这里也是一个死循环,除非进入`if(p == head && tryAcquire(arg))`,而p为`node.predcessor()`得到 -![](https://img-blog.csdnimg.cn/img_convert/dab1a1ccffa05d17a1f81fb1f89350a5.png) -返回node节点的前一个节点,也就是说只有当前一个节点是head时,进一步尝试通过`tryAcquire(arg)`来征用才有机会成功。 - -`tryAcquire(arg)`成立的条件为:锁的状态为0,且通过CAS尝试设置状态成功或线程的持有者本身是当前线程才会返回true。 - -如果这个条件成功后,发生的几个动作包含: -- 首先调用setHead(Node) -![](https://img-blog.csdnimg.cn/img_convert/4cf5b7eac5225d4e42aafa53a29dbece.png) +![](https://upload-images.jianshu.io/upload_images/4685968-5c4353c6ca9b2caf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-212c321db4092bad.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +这个方法返回node节点的前一个节点,也就是说只有当前一个节点是head时,进一步尝试通过`tryAcquire(arg)`来征用才有机会成功 +`tryAcquire(arg)`成立的条件为:锁的状态为0,且通过CAS尝试设置状态成功或线程的持有者本身是当前线程才会返回true,我们现在来详细拆分这部分代码。 +○ 如果这个条件成功后,发生的几个动作包含: +(1) 首先调用setHead(Node) +![](https://upload-images.jianshu.io/upload_images/4685968-b22e59333dd2bd1d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 这个操作会将传入的node节点作为AQS的head所指向的节点。线程属性设置为空(因为现在已经获取到锁,不再需要记录下这个节点所对应的线程了) -![](https://img-blog.csdnimg.cn/img_convert/a4c954d8d68618e639e807b685747b42.png) +![](https://upload-images.jianshu.io/upload_images/4685968-680ccec19b2e7ce6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 再将这个节点的prev引用赋值为null - -- 进一步将前一个节点的next引用赋值为null。 -在进行了这样的修改后,队列的结构就变成了以下这种情况了,这样就可让执行完的节点释放掉内存区域,而不是无限制增长队列,也就真正形成FIFO了: -![CAS成功获取锁后,队列的变化](https://img-blog.csdnimg.cn/img_convert/8f4952a09cad9e12582731e12cef71e7.png) - -如果这个判定条件失败,会首先判定: -### shouldParkAfterFailedAcquire -```java -shouldParkAfterFailedAcquire(p , node) -``` -```java -private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { - int ws = pred.waitStatus; - // 判断前一个节点的状态是否为 Node.SIGNAL - // 是则返回true,不是则返回false - if (ws == Node.SIGNAL) - /* - * This node has already set status asking a release - * to signal it, so it can safely park. - * - */ - return true; - if (ws > 0) { - /* - * Predecessor was cancelled. Skip over predecessors and - * indicate retry. - */ - do { - node.prev = pred = pred.prev; - } while (pred.waitStatus > 0); - pred.next = node; - } else { - /* - * waitStatus 必须为 0 或 PROPAGATE。 - * 表明我们需要一个信号,但不要park。 - * 调用者将需要重试,以确保在park前无法获取 - */ - compareAndSetWaitStatus(pred, ws, Node.SIGNAL); - } - return false; -} -``` -判定节点的状态是否大于0,若大于0则认为被“CANCELLED”掉了(大于0的只可能CANCELLED态),因此会从前一个节点开始逐步循环找到一个没有被“CANCELLED”节点,然后与这个节点的next、prev的引用相互指向;如果前一个节点的状态不是大于0的,则通过CAS尝试将状态修改为“Node.SIGNAL”,自然的如果下一轮循环的时候会返回值应该会返回true。 - -若该方法返回了true,则执行parkAndCheckInterrupt(),它通过 -```java -LockSupport.park(this) -``` -将当前线程挂起到WATING态,它需要等待一个中断、unpark方法来唤醒它,通过这样一种FIFO机制的等待,实现Lock操作。 -### unlock() -如果获取到了锁不释放,那自然就成了死锁,所以必须要释放,来看看它内部是如何释放的。同样从排它锁(ReentrantLock)中的unlock()方法开始 -![](https://img-blog.csdnimg.cn/img_convert/3a3e17f0bea5a86fe6bd5600a2fe41ae.png) -![unlock方法间接调用AQS的release(1)来完成](https://img-blog.csdnimg.cn/img_convert/35da5c344ffe2c300138715b0f5f5eca.png) +(2) 进一步将前一个节点的next引用赋值为null。 +在进行了这样的修改后,队列的结构就变成了以下这种情况了,通过这样的方式,就可以让执行完的节点释放掉内存区域,而不是无限制增长队列,也就真正形成FIFO了: +![CAS成功获取锁后,队列的变化](https://upload-images.jianshu.io/upload_images/4685968-f4d36150337c045d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +○ 如果这个判定条件失败 +会首先判定:“shouldParkAfterFailedAcquire(p , node)”,这个方法内部会判定前一个节点的状态是否为:“Node.SIGNAL”,若是则返回true,若不是都会返回false,不过会再做一些操作:判定节点的状态是否大于0,若大于0则认为被“CANCELLED”掉了(我们没有说明几个状态的值,不过大于0的只可能被CANCELLED的状态),因此会从前一个节点开始逐步循环找到一个没有被“CANCELLED”节点,然后与这个节点的next、prev的引用相互指向;如果前一个节点的状态不是大于0的,则通过CAS尝试将状态修改为“Node.SIGNAL”,自然的如果下一轮循环的时候会返回值应该会返回true。 +如果这个方法返回了true,则会执行:“parkAndCheckInterrupt()”方法,它是通过LockSupport.park(this)将当前线程挂起到WATING状态,它需要等待一个中断、unpark方法来唤醒它,通过这样一种FIFO的机制的等待,来实现了Lock的操作。 + +相应的,可以自己看看FairSync实现类的lock方法,其实区别不大,有些细节上的区别可能会决定某些特定场景的需求,你也可以自己按照这样的思路去实现一个自定义的锁。 + +接下来简单看看unlock()解除锁的方式,如果获取到了锁不释放,那自然就成了死锁,所以必须要释放,来看看它内部是如何释放的。同样从排它锁(ReentrantLock)中的unlock()方法开始 +![](https://upload-images.jianshu.io/upload_images/4685968-f5d927da4d032bb9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![unlock方法间接调用AQS的release(1)来完成](https://upload-images.jianshu.io/upload_images/4685968-186a5d604097f75d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 通过tryRelease(int)方法进行了某种判定,若它成立则会将head传入到unparkSuccessor(Node)方法中并返回true,否则返回false。 首先来看看tryRelease(int)方法 -![](https://img-blog.csdnimg.cn/img_convert/9ca048731c45896138cf62356a8aeb24.png) +![](https://upload-images.jianshu.io/upload_images/4685968-dc1d26d5062071f3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 就是一个设置锁状态的操作,而且是将状态减掉传入的参数值(参数是1),如果结果状态为0,就将排它锁的Owner设置为null,以使得其它的线程有机会进行执行。 在排它锁中,加锁的时候状态会增加1(当然可以自己修改这个值),在解锁的时候减掉1,同一个锁,在可以重入后,可能会被叠加为2、3、4这些值,只有unlock()的次数与lock()的次数对应才会将Owner线程设置为空,而且也只有这种情况下才会返回true。 @@ -238,39 +196,12 @@ LockSupport.park(this) 这一点大家写代码要注意了哦,如果是在循环体中lock()或故意使用两次以上的lock(),而最终只有一次unlock(),最终可能无法释放锁。 在方法unparkSuccessor(Node)中,就意味着真正要释放锁了 -```java -private void unparkSuccessor(Node node) { - /* - * If status is negative (i.e., possibly needing signal) try - * to clear in anticipation of signalling. It is OK if this - * fails or if status is changed by waiting thread. - */ - int ws = node.waitStatus; - if (ws < 0) - compareAndSetWaitStatus(node, ws, 0); - - /* - * Thread to unpark is held in successor, which is normally - * just the next node. But if cancelled or apparently null, - * traverse backwards from tail to find the actual - * non-cancelled successor. - */ - Node s = node.next; - if (s == null || s.waitStatus > 0) { - s = null; - for (Node t = tail; t != null && t != node; t = t.prev) - if (t.waitStatus <= 0) - s = t; - } - if (s != null) - LockSupport.unpark(s.thread); -} -``` -传入的是head节点(head节点是已执行完的节点,在后面阐述该方法的body时,都叫head节点),内部首先会发生的动作是获取head节点的next节点, +![](https://upload-images.jianshu.io/upload_images/4685968-10dc69128dc1c76f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +它传入的是head节点(head节点是已经执行完的节点,在后面阐述这个方法的body的时候都叫head节点),内部首先会发生的动作是获取head节点的next节点, 如果获取到的节点不为空,则直接通过`LockSupport.unpark()`释放对应的被挂起的线程,这样一来将会有一个节点唤醒后继续进入`acquireQueued`的循环进一步尝试 `tryAcquire()`来获取锁,但是也未必能完全获取到哦,因为此时也可能有一些外部的请求正好与之征用,而且还奇迹般的成功了,那这个线程的运气就有点悲剧了,不过通常乐观认为不会每一次都那么悲剧。 再看看共享锁,从前面的排它锁可以看得出来是用一个状态来标志锁的,而共享锁也不例外,但是Java不希望去定义两个状态,所以它与排它锁的第一个区别就是在`锁的状态`,它用int来标志锁的状态,int有4个字节,它用高16位标志读锁(共享锁),低16位标志写锁(排它锁),高16位每次增加1相当于增加65536(通过1 << 16得到),自然的在这种读写锁中,读锁和写锁的个数都不能超过65535个(条件是每次增加1的,如果递增是跳跃的将会更少)。在计算读锁数量的时候将状态左移16位,而计算排它锁会与65535“按位求与”操作,如下图所示。 -![读写锁中的数量计算及限制](https://img-blog.csdnimg.cn/img_convert/b01d5ac76ebafd1d49aa534ecf23ef41.png) +![读写锁中的数量计算及限制](https://upload-images.jianshu.io/upload_images/4685968-61ce30bfe4b6daca.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 写锁的功能与“ReentrantLock”基本一致,区域在于它会在`tryAcquire`时,判定状态的时候会更加复杂一些(因此有些时候它的性能未必好)。 @@ -283,39 +214,34 @@ private void unparkSuccessor(Node node) { 在本节中我们除了学习到AQS的内在,这个是Java层面的队列模型,其实我们也可以利用许多队列模型来解决自己的问题,甚至于可以改写模型模型来满足自己的需求 -## 2.4 ConditionObject +##  2.4  ConditionObject ReentrantLock是独占锁,而且AQS的ConditionObject只能与ReentrantLock一起使用,它是为了支持条件队列的锁更方便 ConditionObject的signal和await方法都是基于独占锁的,如果线程非锁的独占线程,则会抛出IllegalMonitorStateException 例如signalAll源码: -```java -public final void signalAll() { - if (!isHeldExclusively()) - throw new IllegalMonitorStateException(); - Node first = firstWaiter; - if (first != null) - doSignalAll(first); -} ``` - -既然Condtion是为了支持Lock的,为什么ConditionObject不作为ReentrantLock的内部类呢?对于实现锁功能的子类,直接扩展它就可以实现对条件队列的支持。但是,对于其它非锁语义的实现类如Semaphore、CountDownLatch等类来说,条件队列是无用的,也会给开发者扩展AQS带来困惑。总之,是各有利弊 -# FAQ + public final void signalAll() { + if (!isHeldExclusively()) + throw new IllegalMonitorStateException(); + Node first = firstWaiter; + if (first != null) + doSignalAll(first); + } +``` +      我在想,既然Condtion是为了支持Lock的,为什么ConditionObject不作为ReentrantLock的内部类呢?对于实现锁功能的子类,直接扩展它就可以实现对条件队列的支持。但是,对于其它非锁语义的实现类如Semaphore、CountDownLatch等类来说,条件队列是无用的,也会给开发者扩展AQS带来困惑。总之,是各有利弊,大师们的思想,还需要仔细揣摩啊! +#关于Lock及AQS的一些补充: - Lock的操作不仅仅局限于lock()/unlock(),因为这样线程可能进入WAITING状态,这个时候如果没有unpark()就没法唤醒它,可能会一直“睡”下去,可以尝试用tryLock()、tryLock(long , TimeUnit)来做一些尝试加锁或超时来满足某些特定场景的需要。 例如有些时候发现尝试加锁无法加上,先释放已经成功对其它对象添加的锁,过一小会再来尝试,这样在某些场合下可以避免“死锁”哦。 - lockInterruptibly() 它允许抛出`InterruptException`,也就是当外部发起了中断操作,程序内部有可能会抛出这种异常,但是并不是绝对会抛出异常的,大家仔细看看代码便清楚了。 - newCondition()操作,是返回一个Condition的对象,Condition只是一个接口,它要求实现await()、awaitUninterruptibly()、awaitNanos(long)、await(long , TimeUnit)、awaitUntil(Date)、signal()、signalAll()方法,`AbstractQueuedSynchronizer`中有一个内部类叫做`ConditionObject`实现了这个接口,它也是一个类似于队列的实现,具体可以参考源码。大多数情况下可以直接使用,当然觉得自己比较牛逼的话也可以参考源码自己来实现。 - -AQS的Node中有每个Node自己的状态(waitStatus)分别包含: -- SIGNAL -是前面有线程在运行,需要前面线程结束后,调用unpark()才能激活自己,值为:-1 -- CANCELLED -当AQS发起取消或fullyRelease()时,会是这个状态。值为1,也是几个状态中唯一一个大于0的状态,所以前面判定状态大于0就基本等价于是CANCELLED -- CONDITION -线程基于Condition对象发生了等待,进入了相应的队列,自然也需要Condition对象来激活,值为-2 +- 在AQS的Node中有每个Node自己的状态(waitStatus),我们这里归纳一下,分别包含: + - SIGNAL 从前面的代码状态转换可以看得出是前面有线程在运行,需要前面线程结束后,调用unpark()方法才能激活自己,值为:-1 + - CANCELLED 当AQS发起取消或fullyRelease()时,会是这个状态。值为1,也是几个状态中唯一一个大于0的状态,所以前面判定状态大于0就基本等价于是CANCELLED的意思。 + - CONDITION 线程基于Condition对象发生了等待,进入了相应的队列,自然也需要Condition对象来激活,值为-2。 PROPAGATE 读写锁中,当读锁最开始没有获取到操作权限,得到后会发起一个doReleaseShared()动作,内部也是一个循环,当判定后续的节点状态为0时,尝试通过CAS自旋方式将状态修改为这个状态,表示节点可以运行。 状态0 初始化状态,也代表正在尝试去获取临界资源的线程所对应的Node的状态。 -# 羊群效应 +#羊群效应 说一下羊群效应,当有多个线程去竞争同一个锁的时候,假设锁被某个线程占用,那么如果有成千上万个线程在等待锁,有一种做法是同时唤醒这成千上万个线程去去竞争锁,这个时候就发生了羊群效应,海量的竞争必然造成资源的剧增和浪费,因此终究只能有一个线程竞争成功,其他线程还是要老老实实的回去等待。 AQS的FIFO的等待队列给解决在锁竞争方面的羊群效应问题提供了一个思路:保持一个FIFO队列,队列每个节点只关心其前一个节点的状态,线程唤醒也只唤醒队头等待线程。 -这个思路已经被应用到了分布式锁的实践中 \ No newline at end of file +这个思路已经被应用到了分布式锁的实践中 diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\22713-\346\230\276\345\274\217\351\224\201.md" "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230(13)-\346\230\276\345\274\217\351\224\201.md" similarity index 100% rename from "JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\22713-\346\230\276\345\274\217\351\224\201.md" rename to "JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230(13)-\346\230\276\345\274\217\351\224\201.md" diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\22714\344\271\213\346\236\204\345\273\272\350\207\252\345\256\232\344\271\211\347\232\204\345\220\214\346\255\245\345\267\245\345\205\267-(Building-Custom-Synchronizers).md" "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\22714\344\271\213\346\236\204\345\273\272\350\207\252\345\256\232\344\271\211\347\232\204\345\220\214\346\255\245\345\267\245\345\205\267-(Building-Custom-Synchronizers).md" new file mode 100644 index 0000000000..15002909d5 --- /dev/null +++ "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\22714\344\271\213\346\236\204\345\273\272\350\207\252\345\256\232\344\271\211\347\232\204\345\220\214\346\255\245\345\267\245\345\205\267-(Building-Custom-Synchronizers).md" @@ -0,0 +1,783 @@ +类库中包含了许多存在状态依赖的类,例如FutureTask、Semaphore和BlockingQueue,他们的一些操作都有前提条件,例如非空,或者任务已完成等。 + +创建状态依赖类的最简单的房就是在JDK提供了的状态依赖类基础上构造。例如第八章的ValueLactch,如果这些不满足,可以使用Java语言或者类库提供的底层机制来构造,包括 + +* 内置的条件队列 +* condition +* AQS + +这一章就介绍这些。 + +### [](#141-%E7%8A%B6%E6%80%81%E4%BE%9D%E8%B5%96%E6%80%A7%E7%9A%84%E7%AE%A1%E7%90%86-state-dependence)14.1 状态依赖性的管理 State Dependence + +在14.2节会介绍使用条件队列来解决阻塞线程运行的问题。下面先介绍通过轮询和休眠的方式(勉强)的解决。 + +下面是一个标准的模板, + +``` +void blockingAction() throws InterruptedException { + acquire lock on object state + while (precondition does not hold) { + release lock + wait until precondition might hold + optionally fail if interrupted or timeout expires + reacquire lock + } + perform action +} + +``` + +下面介绍阻塞有界队列的集中实现方式。依赖的前提条件是: + +* 不能从空缓存中获取元素 +* 不能将元素放入已满的缓存中 + +不满足条件时候,依赖状态的操作可以 + +* 抛出异常 +* 返回一个错误状态(码) +* 阻塞直到进入正确的状态 + +下面是基类,线程安全,但是非阻塞。 + +``` +@ThreadSafe +public abstract class BaseBoundedBuffer { + @GuardedBy("this") private final V[] buf; + @GuardedBy("this") private int tail; + @GuardedBy("this") private int head; + @GuardedBy("this") private int count; + + protected BaseBoundedBuffer(int capacity) { + this.buf = (V[]) new Object[capacity]; + } + + protected synchronized final void doPut(V v) { + buf[tail] = v; + if (++tail == buf.length) + tail = 0; + ++count; + } + + protected synchronized final V doTake() { + V v = buf[head]; + buf[head] = null; + if (++head == buf.length) + head = 0; + --count; + return v; + } + + public synchronized final boolean isFull() { + return count == buf.length; + } + + public synchronized final boolean isEmpty() { + return count == 0; + } +} + +``` + +“先检查再运行”的逻辑解决方案如下,调用者必须自己处理前提条件失败的情况。当然也可以返回错误消息。 + +当然调用者可以不Sleep,而是直接重试,这种方法叫做**忙等待或者自旋等待(busy waiting or spin waiting. )**,如果换成很长时间都不变,那么这将会消耗大量的CPU时间!!!所以调用者自己休眠,sleep让出CPU。但是这个时间就很尴尬了,sleep长了万一一会前提条件就满足了岂不是白等了从而响应性低,sleep短了浪费CPU时钟周期。另外可以试试yield,但是这也不靠谱。 + +``` +@ThreadSafe + public class GrumpyBoundedBuffer extends BaseBoundedBuffer { + public GrumpyBoundedBuffer() { + this(100); + } + + public GrumpyBoundedBuffer(int size) { + super(size); + } + + public synchronized void put(V v) throws BufferFullException { + if (isFull()) + throw new BufferFullException(); + doPut(v); + } + + public synchronized V take() throws BufferEmptyException { + if (isEmpty()) + throw new BufferEmptyException(); + return doTake(); + } +} + +class ExampleUsage { + private GrumpyBoundedBuffer buffer; + int SLEEP_GRANULARITY = 50; + + void useBuffer() throws InterruptedException { + while (true) { + try { + String item = buffer.take(); + // use item + break; + } catch (BufferEmptyException e) { + Thread.sleep(SLEEP_GRANULARITY); + } + } + } +} + +``` + +下一步改进下,首先让客户端舒服些。 + +``` +@ThreadSafe +public class SleepyBoundedBuffer extends BaseBoundedBuffer { + int SLEEP_GRANULARITY = 60; + + public SleepyBoundedBuffer() { + this(100); + } + + public SleepyBoundedBuffer(int size) { + super(size); + } + + public void put(V v) throws InterruptedException { + while (true) { + synchronized (this) { + if (!isFull()) { + doPut(v); + return; + } + } + Thread.sleep(SLEEP_GRANULARITY); + } + } + + public V take() throws InterruptedException { + while (true) { + synchronized (this) { + if (!isEmpty()) + return doTake(); + } + Thread.sleep(SLEEP_GRANULARITY); + } + } +} + +``` + +这种方式测试失败,那么释放锁,让别人做,自己休眠下,然后再检测,不断的重复这个过程,当然可以解决,但是还是需要做权衡,CPU使用率与响应性之间的抉择。 + +那么我们想如果这种轮询和休眠的dummy方式不用,而是存在某种挂起线程的方案,并且这种方法能够确保党某个条件成真时候立刻唤醒线程,那么将极大的简化实现工作,这就是条件队列的实现。 + +Condition Queues的名字来源:it gives a group of threads called the **wait set** a way to wait for a specific condition to become true. Unlike typical queues in which the elements are data items, the elements of a condition queue are the threads waiting for the condition. + +每个Java对象都可以是一个锁,每个对象同样可以作为一个条件队列,并且Object的wait、notify和notifyAll就是内部条件队列的API。对象的内置锁(intrinsic lock )和内置条件队列是关联的,**要调用X中的条件队列的任何一个方法,都必须持有对象X上的锁。** + +Object.wait自动释放锁,并且请求操作系统挂起当前线程,从而其他线程可以获得这个锁并修改对象状态。当被挂起的线程唤醒时。它将在返回之前重新获取锁。 + +``` +@ThreadSafe +public class BoundedBuffer extends BaseBoundedBuffer { + // CONDITION PREDICATE: not-full (!isFull()) + // CONDITION PREDICATE: not-empty (!isEmpty()) + public BoundedBuffer() { + this(100); + } + + public BoundedBuffer(int size) { + super(size); + } + + // BLOCKS-UNTIL: not-full + public synchronized void put(V v) throws InterruptedException { + while (isFull()) + wait(); + doPut(v); + notifyAll(); + } + + // BLOCKS-UNTIL: not-empty + public synchronized V take() throws InterruptedException { + while (isEmpty()) + wait(); + V v = doTake(); + notifyAll(); + return v; + } + + // BLOCKS-UNTIL: not-full + // Alternate form of put() using conditional notification + public synchronized void alternatePut(V v) throws InterruptedException { + while (isFull()) + wait(); + boolean wasEmpty = isEmpty(); + doPut(v); + if (wasEmpty) + notifyAll(); + } +} + +``` + +注意,如果某个功能无法通过“轮询和休眠"来实现,那么条件队列也无法实现。 + +### [](#142-using-condition-queues)14.2 Using Condition Queues + +#### [](#1421-%E6%9D%A1%E4%BB%B6%E8%B0%93%E8%AF%8Dthe-condition-predicate)14.2.1 条件谓词The Condition Predicate + +The Condition Predicate 是使某个操作成为状态依赖操作的前提条件。take方法的条件谓词是”缓存不为空“,take方法在执行之前必须首先测试条件谓词。同样,put方法的条件谓词是”缓存不满“。 + +在条件等待中存在一种重要的三元关系,包括 + +* 加锁 +* wait方法 +* 条件谓词 + +条件谓词中包含多个状态变量,而状态变量由一个锁来保护,因此在测试条件谓词之前必须先持有这个锁。锁对象和条件队列对象必须是同一个对象。wait释放锁,线程挂起阻塞,等待知道超时,然后被另外一个线程中断或者被一个通知唤醒。唤醒后,wait在返回前还需要重新获取锁,当线程从wait方法中唤醒,它在重新请求锁时不具有任何特殊的优先级,和其他人一起竞争。 + +#### [](#1422-%E8%BF%87%E6%97%A9%E5%94%A4%E9%86%92)14.2.2 过早唤醒 + +其他线程中间插足了,获取了锁,并且修改了遍历,这时候线程获取锁需要重新检查条件谓词。 + +``` +wait block ----------race to get lock ------------------------------------------get lock ----- + ^ +wait block --------> race to get lock ------get lock------> perform action ---> release lock + ^ + notifyAll + +``` + +当然有的时候,比如一个你根本不知道为什么别人调用了notify或者notifyAll,也许条件谓词压根就没满足,但是线程还是获取了锁,然后test条件谓词,释放所,其他线程都来了这么一趟,发生这就是“谎报军情”啊。 + +基于以上这两种情况,都必须重新测试条件谓词。 + +When using condition waits (Object.wait or Condition.await): + +* Always have a condition predicate——some test of object state that must hold before proceeding; +* Always test the condition predicate before calling wait, and again after returning from wait; +* Always call wait in a loop; +* Ensure that the state variables making up the condition predicate are guarded by the lock associated with the condition queue; +* Hold the lock associated with the the condition queue when calling wait, notify, or notifyAll +* Do not release the lock after checking the condition predicate but before acting on it. + +模板就是: + +``` +void stateDependentMethod() throws InterruptedException { + // condition predicate must be guarded by lock + synchronized(lock) { + while (!conditionPredicate()) //一定在循环里面做条件谓词 + lock.wait(); //确保和synchronized的是一个对象 + // object is now in desired state //不要释放锁 + } +} + +``` + +#### [](#1423-%E4%B8%A2%E5%A4%B1%E7%9A%84%E4%BF%A1%E5%8F%B7)14.2.3 丢失的信号 + +保证notify一定在wait之后 + +#### [](#1424-%E9%80%9A%E7%9F%A5)14.2.4 通知 + +下面介绍通知。 + +调用notify和notifyAll也得持有与条件队列对象相关联的锁。调用notify,JVM Thread Scheduler在这个条件队列上等待的多个线程中选择一个唤醒,而notifyAll则会唤醒所有线程。因此一旦notify了那么就需要尽快的释放锁,否则别人都竞争等着拿锁,都会进行blocked的状态,而不是线程挂起waiting状态,竞争都了不是好事,但是这是你考了性能因素和安全性因素的一个矛盾,具体问题要具体分析。 + +下面的方法可以进来减少竞争,但是确然程序正确的实现有些难写,所以这个折中还得自己考虑: + +``` +public synchronized void alternatePut(V v) throws InterruptedException { + while (isFull()) + wait(); + boolean wasEmpty = isEmpty(); + doPut(v); + if (wasEmpty) + notifyAll(); + } + +``` + +使用notify容易丢失信号,所以大多数情况下用notifyAll,比如take notify,却通知了另外一个take,没有通知put,那么这就是信号丢失,是一种“被劫持的”信号。 + +因此只有满足下面两个条件,才能用notify,而不是notifyAll: + +* 所有等待线程的类型都相同 +* 单进单出 + +#### [](#1425-%E7%A4%BA%E4%BE%8B%E9%98%80%E9%97%A8%E7%B1%BBa-gate-class)14.2.5 示例:阀门类A Gate Class + +和第5章的那个TestHarness中使用CountDownLatch类似,完全可以使用wait/notifyAll做阀门。 + +``` +@ThreadSafe +public class ThreadGate { + // CONDITION-PREDICATE: opened-since(n) (isOpen || generation>n) + @GuardedBy("this") private boolean isOpen; + @GuardedBy("this") private int generation; + + public synchronized void close() { + isOpen = false; + } + + public synchronized void open() { + ++generation; + isOpen = true; + notifyAll(); + } + + // BLOCKS-UNTIL: opened-since(generation on entry) + public synchronized void await() throws InterruptedException { + int arrivalGeneration = generation; + while (!isOpen && arrivalGeneration == generation) + wait(); + } +} + +``` + +### [](#143-explicit-condition-objects)14.3 Explicit Condition Objects + +Lock是一个内置锁的替代,而Condition也是一种广义的**内置条件队列**。 + +Condition的API如下: + +``` +public interface Condition { + void await() throws InterruptedException; + boolean await(long time, TimeUnit unit)throws InterruptedException; + long awaitNanos(long nanosTimeout) throws InterruptedException; + void awaitUninterruptibly(); + boolean awaitUntil(Date deadline) throws InterruptedException; + void signal(); + void signalAll(); +} + +``` + +内置条件队列存在一些缺陷,每个内置锁都只能有一个相关联的条件队列,记住是**一个**。所以在BoundedBuffer这种累中,**多个线程可能在同一个条件队列上等待不同的条件谓词**,所以notifyAll经常通知不是同一个类型的需求。如果想编写一个带有多个条件谓词的并发对象,或者想获得除了条件队列可见性之外的更多的控制权,可以使用Lock和Condition,而不是内置锁和条件队列,这更加灵活。 + +一个Condition和一个lock关联,想象一个条件队列和内置锁关联一样。在Lock上调用newCondition就可以新建无数个条件谓词,这些condition是可中断的、可有时间限制的,公平的或者非公平的队列操作。 + +The equivalents of wait, notify, and notifyAll for Condition objects are await, signal, and signalAll。 + +下面的例子就是改造后的BoundedBuffer, + +``` +@ThreadSafe +public class ConditionBoundedBuffer { + protected final Lock lock = new ReentrantLock(); + // CONDITION PREDICATE: notFull (count < items.length) + private final Condition notFull = lock.newCondition(); + // CONDITION PREDICATE: notEmpty (count > 0) + private final Condition notEmpty = lock.newCondition(); + private static final int BUFFER_SIZE = 100; + @GuardedBy("lock") private final T[] items = (T[]) new Object[BUFFER_SIZE]; + @GuardedBy("lock") private int tail, head, count; + + // BLOCKS-UNTIL: notFull + public void put(T x) throws InterruptedException { + lock.lock(); + try { + while (count == items.length) + notFull.await(); + items[tail] = x; + if (++tail == items.length) + tail = 0; + ++count; + notEmpty.signal(); + } finally { + lock.unlock(); + } + } + + // BLOCKS-UNTIL: notEmpty + public T take() throws InterruptedException { + lock.lock(); + try { + while (count == 0) + notEmpty.await(); + T x = items[head]; + items[head] = null; + if (++head == items.length) + head = 0; + --count; + notFull.signal(); + return x; + } finally { + lock.unlock(); + } + } +} + +``` + +注意这里使用了signal而不是signalll,能极大的减少每次缓存操作中发生的上下文切换和锁请求次数。 + +使用condition和内置锁和条件队列一样,必须保卫在lock里面。 + +### [](#144-synchronizer%E5%89%96%E6%9E%90)14.4 Synchronizer剖析 + +看似ReentrantLock和Semaphore功能很类似,每次只允许一定的数量线程通过,到达阀门时 + +* 可以通过 lock或者acquire +* 等待,阻塞住了 +* 取消tryLock,tryAcquire +* 可中断的,限时的 +* 公平等待和非公平等待 + +下面的程序是使用Lock做一个Mutex也就是持有一个许可的Semaphore。 + +``` +@ThreadSafe +public class SemaphoreOnLock { + private final Lock lock = new ReentrantLock(); + // CONDITION PREDICATE: permitsAvailable (permits > 0) + private final Condition permitsAvailable = lock.newCondition(); + @GuardedBy("lock") private int permits; + + SemaphoreOnLock(int initialPermits) { + lock.lock(); + try { + permits = initialPermits; + } finally { + lock.unlock(); + } + } + + // BLOCKS-UNTIL: permitsAvailable + public void acquire() throws InterruptedException { + lock.lock(); + try { + while (permits <= 0) + permitsAvailable.await(); + --permits; + } finally { + lock.unlock(); + } + } + + public void release() { + lock.lock(); + try { + ++permits; + permitsAvailable.signal(); + } finally { + lock.unlock(); + } + } +} + +``` + +实际上很多J.U.C下面的类都是基于AbstractQueuedSynchronizer (AQS)构建的,例如CountDownLatch, ReentrantReadWriteLock, SynchronousQueue,and FutureTask(java7之后不是了)。AQS解决了实现同步器时设计的大量细节问题,例如等待线程采用FIFO队列操作顺序。AQS不仅能极大极少实现同步器的工作量,并且也不必处理竞争问题,基于AQS构建只可能在一个时刻发生阻塞,从而降低上下文切换的开销,提高吞吐量。在设计AQS时,充分考虑了可伸缩性,可谓大师Doug Lea的经典作品啊! + +### [](#145-abstractqueuedsynchronizer-aqs)14.5 AbstractQueuedSynchronizer (AQS) + +基于AQS构建的同步器勒种,最进步的操作包括各种形式的获取操作和释放操作。获取操作是一种依赖状态的操作,并且通常会阻塞。 + +如果一个类想成为状态依赖的类,它必须拥有一些状态,AQS负责管理这些状态,通过getState,setState, compareAndSetState等protected类型方法进行操作。这是设计模式中的模板模式。 + +使用AQS的模板如下: + +获取锁:首先判断当前状态是否允许获取锁,如果是就获取锁,否则就阻塞操作或者获取失败,也就是说如果是独占锁就可能阻塞,如果是共享锁就可能失败。另外如果是阻塞线程,那么线程就需要进入阻塞队列。当状态位允许获取锁时就修改状态,并且如果进了队列就从队列中移除。 + +释放锁:这个过程就是修改状态位,如果有线程因为状态位阻塞的话就唤醒队列中的一个或者更多线程。 + +``` +boolean acquire() throws InterruptedException { + while (state does not permit acquire) { + if (blocking acquisition requested) { + enqueue current thread if not already queued + block current thread + } + else + return failure + } + possibly update synchronization state + dequeue thread if it was queued + return success +} +void release() { + update synchronization state + if (new state may permit a blocked thread to acquire) + unblock one or more queued threads +} + +``` + +要支持上面两个操作就必须有下面的条件: + +* 原子性操作同步器的状态位 +* 阻塞和唤醒线程 +* 一个有序的队列 + +**1 状态位的原子操作** + +这里使用一个32位的整数来描述状态位,前面章节的原子操作的理论知识整好派上用场,在这里依然使用CAS操作来解决这个问题。事实上这里还有一个64位版本的同步器(AbstractQueuedLongSynchronizer),这里暂且不谈。 + +**2 阻塞和唤醒线程** + +标准的JAVA API里面是无法挂起(阻塞)一个线程,然后在将来某个时刻再唤醒它的。JDK 1.0的API里面有Thread.suspend和Thread.resume,并且一直延续了下来。但是这些都是过时的API,而且也是不推荐的做法。 + +HotSpot在Linux中中通过调用pthread_mutex_lock函数把线程交给系统内核进行阻塞。 + +在JDK 5.0以后利用JNI在LockSupport类中实现了此特性。 + +> LockSupport.park() LockSupport.park(Object) LockSupport.parkNanos(Object, long) LockSupport.parkNanos(long) LockSupport.parkUntil(Object, long) LockSupport.parkUntil(long) LockSupport.unpark(Thread) + +上面的API中park()是在当前线程中调用,导致线程阻塞,带参数的Object是挂起的对象,这样监视的时候就能够知道此线程是因为什么资源而阻塞的。由于park()立即返回,所以通常情况下需要在循环中去检测竞争资源来决定是否进行下一次阻塞。park()返回的原因有三: + +* 其他某个线程调用将当前线程作为目标调用 [`unpark`](http://www.blogjava.net/xylz/java/util/concurrent/locks/LockSupport.html#unpark(java.lang.Thread)); +* 其他某个线程[中断](http://www.blogjava.net/xylz/java/lang/Thread.html#interrupt())当前线程; +* 该调用不合逻辑地(即毫无理由地)返回。 + +其实第三条就决定了需要循环检测了,类似于通常写的while(checkCondition()){Thread.sleep(time);}类似的功能。 + +**3 有序队列** + +在AQS中采用CHL列表来解决有序的队列的问题。 + +AQS采用的CHL模型采用下面的算法完成FIFO的入队列和出队列过程。该队列的操作均通过Lock-Free(CAS)操作. + +自己实现的CLH SpinLock如下: + +``` +class ClhSpinLock { + private final ThreadLocal prev; + private final ThreadLocal node; + private final AtomicReference tail = new AtomicReference(new Node()); + + public ClhSpinLock() { + this.node = new ThreadLocal() { + protected Node initialValue() { + return new Node(); + } + }; + + this.prev = new ThreadLocal() { + protected Node initialValue() { + return null; + } + }; + } + + public void lock() { + final Node node = this.node.get(); + node.locked = true; + // 一个CAS操作即可将当前线程对应的节点加入到队列中, + // 并且同时获得了前继节点的引用,然后就是等待前继释放锁 + Node pred = this.tail.getAndSet(node); + this.prev.set(pred); + while (pred.locked) {// 进入自旋 + } + } + + public void unlock() { + final Node node = this.node.get(); + node.locked = false; + this.node.set(this.prev.get()); + } + + private static class Node { + private volatile boolean locked; + } +} + +``` + +对于入队列(*enqueue):*采用CAS操作,每次比较尾结点是否一致,然后插入的到尾结点中。 + +``` +do { +        pred = tail; +}while ( !compareAndSet(pred,tail,node) ); + +``` + +对于出队列(*dequeue*):由于每一个节点也缓存了一个状态,决定是否出队列,因此当不满足条件时就需要自旋等待,一旦满足条件就将头结点设置为下一个节点。 + +AQS里面有三个核心字段: + +> private volatile int state; +> +> private transient volatile Node head; +> +> private transient volatile Node tail; + +其中state描述的有多少个线程取得了锁,对于互斥锁来说state<=1。head/tail加上CAS操作就构成了一个CHL的FIFO队列。下面是Node节点的属性。 + +独占操作的API都是不带有shared,而共享的包括semaphore和countdownlatch都是使用带有shared字面的API。 + +一些有用的参考资料: + +**java.util.concurrent.locks.AbstractQueuedSynchronizer - **AQS + +[http://gee.cs.oswego.edu/dl/papers/aqs.pdf](http://gee.cs.oswego.edu/dl/papers/aqs.pdf)论文 + +[http://www.blogjava.net/xylz/archive/2010/07/08/325587.html](http://www.blogjava.net/xylz/archive/2010/07/08/325587.html) 一个比较全面的另外一个人的解读 + +[http://suo.iteye.com/blog/1329460](http://suo.iteye.com/blog/1329460) + +[http://www.infoq.com/cn/articles/jdk1.8-abstractqueuedsynchronizer](http://www.infoq.com/cn/articles/jdk1.8-abstractqueuedsynchronizer) + +[http://www.cnblogs.com/zhanjindong/p/java-concurrent-package-aqs-overview.html](http://www.cnblogs.com/zhanjindong/p/java-concurrent-package-aqs-overview.html) + +[http://www.cnblogs.com/zhanjindong/p/java-concurrent-package-aqs-clh-and-spin-lock.html](http://www.cnblogs.com/zhanjindong/p/java-concurrent-package-aqs-clh-and-spin-lock.html) + +[http://www.cnblogs.com/zhanjindong/p/java-concurrent-package-aqs-locksupport-and-thread-interrupt.html](http://www.cnblogs.com/zhanjindong/p/java-concurrent-package-aqs-locksupport-and-thread-interrupt.html) + +独占的就用TRyAcquire, TRyRelease, and isHeldExclusively,共享的就用 tryAcquireShared and TRyReleaseShared. 带有try前缀的方法都是模板方法,AQS用于判断是否可以继续,例如如果tryAcquireShared返回一个负值,那么表示获取锁失败,失败的就需要进入CLH队列,并且挂起线程。 + +举一个例子,一个简单的闭锁。 + +``` +@ThreadSafe +public class OneShotLatch { + private final Sync sync = new Sync(); + + public void signal() { + sync.releaseShared(0); + } + + public void await() throws InterruptedException { + sync.acquireSharedInterruptibly(0); + } + + private class Sync extends AbstractQueuedSynchronizer { + protected int tryAcquireShared(int ignored) { + // Succeed if latch is open (state == 1), else fail + return (getState() == 1) ? 1 : -1; + } + + protected boolean tryReleaseShared(int ignored) { + setState(1); // Latch is now open + return true; // Other threads may now be able to acquire + + } + } +} + +``` + +下面是自己实现的一个Mutex。 + +``` +/** + * Lock free的互斥锁,简单实现,不可重入锁 + */ +public class Mutex implements Lock { + + private static final int FREE = 0; + private static final int BUSY = 1; + + private static class LockSync extends AbstractQueuedSynchronizer { + + private static final long serialVersionUID = 4689388770786922019L; + + protected boolean isHeldExclusively() { + return getState() == BUSY; + } + + public boolean tryAcquire(int acquires) { + return compareAndSetState(FREE, BUSY); + } + + protected boolean tryRelease(int releases) { + if (getState() == FREE) { + throw new IllegalMonitorStateException(); + } + + setState(FREE); + return true; + } + + Condition newCondition() { + return new ConditionObject(); + } + + } + + private final LockSync sync = new LockSync(); + + public void lock() { + sync.acquire(0); + } + + public boolean tryLock() { + return sync.tryAcquire(0); + } + + public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { + return sync.tryAcquireNanos(1, unit.toNanos(timeout)); + } + + public void unlock() { + sync.release(0); + } + + public Condition newCondition() { + return sync.newCondition(); + } + + public boolean isLocked() { + return sync.isHeldExclusively(); + } + + public boolean hasQueuedThreads() { + return sync.hasQueuedThreads(); + } + + public void lockInterruptibly() throws InterruptedException { + sync.acquireInterruptibly(0); + } + +} + +``` + +### [](#146-juc%E5%90%8C%E6%AD%A5%E5%99%A8%E5%8B%92%E7%A7%8D%E7%9A%84aqs)14.6 J.U.C同步器勒种的AQS + +* ReentrantLock + +``` +protected boolean tryAcquire(int ignored) { + final Thread current = Thread.currentThread(); + int c = getState(); + if (c == 0) { + if (compareAndSetState(0, 1)) { + owner = current; + return true; + } + } else if (current == owner) { + setState(c+1); + return true; + } + return false; +} + +``` + +* Semaphore和CountDownLatch + +``` +protected int tryAcquireShared(int acquires) { + while (true) { + int available = getState(); + int remaining = available - acquires; + if (remaining < 0 + || compareAndSetState(available, remaining)) + return remaining; + } +} +protected boolean tryReleaseShared(int releases) { + while (true) { + int p = getState(); + if (compareAndSetState(p, p + releases)) + return true; + } +} +``` diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\227(15)-\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266.md" "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\22715\344\271\213\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266(Atomic-Variables-and-Non-blocking-Synchron.md" similarity index 56% rename from "JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\227(15)-\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266.md" rename to "JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\22715\344\271\213\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266(Atomic-Variables-and-Non-blocking-Synchron.md" index 5aece1fb28..270e953ff1 100644 --- "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\227(15)-\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266.md" +++ "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\22715\344\271\213\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266(Atomic-Variables-and-Non-blocking-Synchron.md" @@ -1,30 +1,33 @@ -非阻塞算法,用底层的原子机器指令代替锁,确保数据在并发访问中的一致性。 -非阻塞算法被广泛应用于OS和JVM中实现线程/进程调度机制和GC及锁,并发数据结构中。 +近年在并发算法领域的大多数研究都侧重于非阻塞算法,这种算法用底层的原子机器指令来代替锁来确保数据在并发访问中的一致性,非阻塞算法被广泛应用于OS和JVM中实现线程/进程调度机制和GC以及锁,并发数据结构中。 + +与锁的方案相比,非阻塞算法都要复杂的多,他们在可伸缩性和活跃性上(避免死锁)都有巨大优势。 + +非阻塞算法,顾名思义,多个线程竞争相同的数据时不会发生阻塞,因此他能在粒度更细的层次上进行协调,而且极大的减少调度开销。 -与锁相比,非阻塞算法复杂的多,在可伸缩性和活跃性上(避免死锁)有巨大优势。 -非阻塞算法,即多个线程竞争相同的数据时不会发生阻塞,因此能更细粒度的层次上进行协调,而且极大减少调度开销。 # 1 锁的劣势 独占,可见性是锁要保证的。 -许多JVM都对非竞争的锁获取和释放做了很多优化,性能很不错。 -但若一些线程被挂起然后稍后恢复运行,当线程恢复后还得等待其他线程执行完他们的时间片,才能被调度,所以挂起和恢复线程存在很大开销。 -其实很多锁的粒度很小,很简单,若锁上存在激烈竞争,那么 调度开销/工作开销 比值就会非常高,降低业务吞吐量。 +许多JVM都对非竞争的锁获取和释放做了很多优化,性能很不错了。 + +但是如果一些线程被挂起然后稍后恢复运行,当线程恢复后还得等待其他线程执行完他们的时间片,才能被调度,所以挂起和恢复线程存在很大的开销,其实很多锁的力度很小的,很简单,如果锁上存在着激烈的竞争,那么多调度开销/工作开销比值就会非常高。 + +与锁相比volatile是一种更轻量的同步机制,因为使用volatile不会发生上下文切换或者线程调度操作,但是volatile的指明问题就是虽然保证了可见性,但是原子性无法保证,比如i++的字节码就是N行。 -而与锁相比,volatile是一种更轻量的同步机制,因为使用volatile不会发生上下文切换或线程调度操作,但volatile的指明问题就是虽然保证了可见性,但是原子性无法保证。 +如果一个线程正在等待锁,它不能做任何事情,如果一个线程在持有锁的情况下呗延迟执行了,例如发生了缺页错误,调度延迟,那么就没法执行。如果被阻塞的线程优先级较高,那么就会出现priority invesion的问题,被永久的阻塞下去。 -- 若一个线程正在等待锁,它不能做任何事情 -- 若一个线程在持有锁情况下被延迟执行了,如发生缺页错误,调度延迟,就没法执行 -- 若被阻塞的线程优先级较高,就会出现priority invesion问题,被永久阻塞 # 2 硬件对并发的支持 -独占锁是悲观锁,对细粒度的操作,更高效的应用是乐观锁,这种方法需要借助**冲突监测机制,来判断更新过程中是否存在来自其他线程的干扰,若存在,则失败重试**。 -几乎所有现代CPU都有某种形式的原子读-改-写指令,如compare-and-swap等,JVM就是使用这些指令来实现无锁并发。 +独占锁是悲观所,对于细粒度的操作,更高效的应用是乐观锁,这种方法需要借助**冲突监测机制来判断更新过程中是否存在来自其他线程的干扰,如果存在则失败重试**。 + +几乎所有的现代CPU都有某种形式的原子读-改-写指令,例如compare-and-swap等,JVM就是使用这些指令来实现无锁并发。 + ## 2.1 比较并交换 + CAS(Compare and set)乐观的技术。Java实现的一个compare and set如下,这是一个模拟底层的示例: + ```java @ThreadSafe public class SimulatedCAS { - @GuardedBy("this") private int value; public synchronized int get() { @@ -45,8 +48,11 @@ public class SimulatedCAS { == compareAndSwap(expectedValue, newValue)); } } + ``` + ## 2.2 非阻塞的计数器 + ```java public class CasCounter { private SimulatedCAS value; @@ -63,36 +69,62 @@ public class CasCounter { return v + 1; } } + ``` + Java中使用AtomicInteger。 -竞争激烈一般时,CAS性能远超基于锁的计数器。看起来他的指令更多,但无需上下文切换和线程挂起,JVM内部的代码路径实际很长,所以反而好些。 +首先在竞争激烈一般时候,CAS性能远超过第三章基于锁的计数器。看起来他的指令更多,但是无需上下文切换和线程挂起,JVM内部的代码路径实际很长,所以反而好些。但是激烈程度比较高的时候,它的开销还是比较大,但是你会发生这种激烈程度非常高的情况只是理论,实际生产环境很难遇到。况且JIT很聪明,这种操作往往能非常大的优化。 -但激烈程度较高时,开销还是较大,但会发生这种激烈程度非常高的情况只是理论,实际生产环境很难遇到。况且JIT很聪明,这种操作往往能非常大的优化。 +为了确保正常更新,可能得将CAS操作放到for循环里,从语法结构上来看,使用**CAS**比使用锁更加复杂,得考虑失败的情况(锁会挂起线程,直到恢复);但是基于**CAS**的原子操作,在性能上基本超过了基于锁的计数器,即使只有很小的竞争或者不存在竞争! -为确保正常更新,可能得将CAS操作放到for循环,从语法结构看,使用**CAS**比使用锁更加复杂,得考虑失败情况(锁会挂起线程,直到恢复)。 -但基于**CAS**的原子操作,性能基本超过基于锁的计数器,即使只有很小的竞争或不存在竞争! +在轻度到中度的争用情况下,非阻塞算法的性能会超越阻塞算法,因为 CAS 的多数时间都在第一次尝试时就成功,而发生争用时的开销也不涉及**线程挂起**和**上下文切换**,只多了几个循环迭代。没有争用的 CAS 要比没有争用的锁便宜得多(这句话肯定是真的,因为没有争用的锁涉及 CAS 加上额外的处理,加锁至少需要一个CAS,在有竞争的情况下,需要操作队列,线程挂起,上下文切换),而争用的 CAS 比争用的锁获取涉及更短的延迟。 -在轻度到中度争用情况下,非阻塞算法的性能会超越阻塞算法,因为 CAS 的多数时间都在第一次尝试时就成功,而发生争用时的开销也不涉及**线程挂起**和**上下文切换**,只多了几个循环迭代。 -没有争用的 CAS 要比没有争用的锁轻量得多(因为没有争用的锁涉及 CAS 加上额外的处理,加锁至少需要一个CAS,在有竞争的情况下,需要操作队列,线程挂起,上下文切换),而争用的 CAS 比争用的锁获取涉及更短的延迟。 +CAS的缺点是它使用调用者来处理竞争问题,通过重试、回退、放弃,而锁能自动处理竞争问题,例如阻塞。 -CAS的缺点是,它使用调用者来处理竞争问题,通过重试、回退、放弃,而锁能自动处理竞争问题,例如阻塞。 +原子变量可以看做更好的volatile类型变量。 + +AtomicInteger在JDK8里面做了改动。 + +```java +public final int getAndIncrement() { + return unsafe.getAndAddInt(this, valueOffset, 1); +} + +``` -原子变量可看做更好的volatile类型变量。AtomicInteger在JDK8里面做了改动。 -![](https://img-blog.csdnimg.cn/0f94ab5e4b6045e5aa83d99cbc9c03c4.png) JDK7里面的实现如下: -![](https://img-blog.csdnimg.cn/d2f94066894a4501b6dd5e6d9ad4a8c1.png) -Unsafe是经过特殊处理的,不能理解成常规的Java代码,1.8在调用getAndAddInt时,若系统底层: -- 支持fetch-and-add,则执行的就是native方法,使用fetch-and-add -- 不支持,就按照上面getAndAddInt那样,以Java代码方式执行,使用compare-and-swap + +```java +public final int getAndAdd(int delta) { + for(;;) { + intcurrent= get(); + intnext=current+delta; + if(compareAndSet(current,next)) + returncurrent; + } + } + +``` + +Unsafe是经过特殊处理的,不能理解成常规的Java代码,区别在于: + +- 1.8在调用getAndAddInt的时候,如果系统底层支持fetch-and-add,那么它执行的就是native方法,使用的是fetch-and-add +- 如果不支持,就按照上面的所看到的getAndAddInt方法体那样,以java代码的方式去执行,使用的是compare-and-swap 这也正好跟openjdk8中Unsafe::getAndAddInt上方的注释相吻合: -以下包含在不支持本机指令的平台上使用的基于 CAS 的 Java 实现 -![](https://img-blog.csdnimg.cn/327bda8392cf4158ab94049e67f9b169.png) + +```java +// The following contain CAS-based Java implementations used on +// platforms not supporting native instructions +``` + # 3 原子变量类 -J.U.C的AtomicXXX。 + +J.U.C的AtomicXXX 例如一个AtomictReference的使用如下: + ```java public class CasNumberRange { @Immutable @@ -140,11 +172,18 @@ public class CasNumberRange { } } } + ``` + + # 4 非阻塞算法 + Lock-free算法,可以实现栈、队列、优先队列或者散列表。 + ## 4.1 非阻塞的栈 -Trebier算法,1986年提出。 + +Trebier算法,1986年提出的。 + ```java public class ConcurrentStack { AtomicReference> top = new AtomicReference>(); @@ -179,9 +218,13 @@ Trebier算法,1986年提出。 } } } + ``` + ## 4.2 非阻塞的链表 -J.U.C的ConcurrentLinkedQueue也是参考这个由Michael and Scott,1996年实现的算法。 + +有点复杂哦,实际J.U.C的ConcurrentLinkedQueue也是参考了这个由Michael and Scott,1996年实现的算法。 + ```java public class LinkedQueue { @@ -222,14 +265,19 @@ public class LinkedQueue { } } } + ``` + ## 4.3 原子域更新 -AtomicReferenceFieldUpdater,一个基于反射的工具类,能对指定类的指定的volatile引用字段进行原子更新。(该字段不能是private的) + +AtomicReferenceFieldUpdater,一个基于反射的工具类,它能对指定类的指定的volatile引用字段进行原子更新。(注意这个字段不能是private的) 通过调用AtomicReferenceFieldUpdater的静态方法newUpdater就能创建它的实例,该方法要接收三个参数: + * 包含该字段的对象的类 * 将被更新的对象的类 * 将被更新的字段的名称 + ```java AtomicReferenceFieldUpdater updater=AtomicReferenceFieldUpdater.newUpdater(Dog.class,String.class,"name"); Dog dog1=new Dog(); @@ -238,5 +286,7 @@ AtomicReferenceFieldUpdater updater=AtomicReferenceFieldUpdater.newUpdater(Dog.c class Dog { volatile String name="dog1"; + } -``` \ No newline at end of file + +``` diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\347\237\245\350\257\206\347\202\271\345\205\250\346\200\273\347\273\223.md" "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\347\237\245\350\257\206\347\202\271\345\205\250\346\200\273\347\273\223.md" index 3c9eb32787..7f0adf7cdb 100644 --- "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\347\237\245\350\257\206\347\202\271\345\205\250\346\200\273\347\273\223.md" +++ "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\347\237\245\350\257\206\347\202\271\345\205\250\346\200\273\347\273\223.md" @@ -1,14 +1,18 @@ -# 1 基本概念 -## 1.1 并发 +![](https://upload-images.jianshu.io/upload_images/4685968-f12a51fabb09287a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![高并发处理的思路及手段](https://upload-images.jianshu.io/upload_images/4685968-881327a0ac18f5a0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-f6439f7c997bfcf8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +#1 基本概念 +##1.1 并发 同时拥有两个或者多个线程,如果程序在单核处理器上运行多个线程将交替地换入或者换出内存,这些线程是同时“存在"的,每个线程都处于执行过程中的某个状态,如果运行在多核处理器上,此时,程序中的每个线程都将分配到一个处理器核上,因此可以同时运行. -## 1.2 高并发( High Concurrency) +##1.2 高并发( High Concurrency) 互联网分布式系统架构设计中必须考虑的因素之一,通常是指,通过设计保证系统能够同时并行处理很多请求. -## 1.3 区别与联系 +##1.3 区别与联系 - 并发: 多个线程操作相同的资源,保证线程安全,合理使用资源 - 高并发:服务能同时处理很多请求,提高程序性能 -# 2 CPU -## 2.1 CPU 多级缓存 -![](https://img-blog.csdnimg.cn/img_convert/03859736ec299060001e2702e49ce90f.png) +#2 CPU +##2.1 CPU 多级缓存 +![](https://upload-images.jianshu.io/upload_images/4685968-6fc8b5b6509ca6d7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - 为什么需要CPU cache CPU的频率太快了,快到主存跟不上 如此,在处理器时钟周期内,CPU常常需要等待主存,浪费资源。所以cache的出现,是为了缓解CPU和内存之间速度的不匹配问题(结构:cpu-> cache-> memory ). @@ -17,7 +21,7 @@ CPU的频率太快了,快到主存跟不上 如果某个数据被访问,那么在不久的将来它很可能被再次访问 2) 空间局部性 如果某个数据被访问,那么与它相邻的数据很快也可能被访问 -## 2.2 缓存一致性(MESI) +##2.2 缓存一致性(MESI) 用于保证多个 CPU cache 之间缓存共享数据的一致 - M-modified被修改 该缓存行只被缓存在该 CPU 的缓存中,并且是被修改过的,与主存中数据是不一致的,需在未来某个时间点写回主存,该时间是允许在其他CPU 读取主存中相应的内存之前,当这里的值被写入主存之后,该缓存行状态变为 E @@ -27,31 +31,30 @@ CPU的频率太快了,快到主存跟不上 - S-shared共享 该缓存行可被多个 CPU 缓存,与主存中数据一致 - I-invalid无效 -![](https://img-blog.csdnimg.cn/img_convert/6f134b3e29210e5efab39c87b65330f4.png) +![](https://upload-images.jianshu.io/upload_images/4685968-d2325f1e0e5b7786.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - 乱序执行优化 处理器为提高运算速度而做出违背代码原有顺序的优化 -## 并发的优势与风险 -![](https://img-blog.csdnimg.cn/img_convert/611324b1cf6a4e73e374489d631ba506.png) -# 3 项目准备 -## 3.1 项目初始化 -![自定义4个基本注解](https://img-blog.csdnimg.cn/img_convert/39ce4a664f6c0aae2ac8254813397adf.png) -![随手写个测试类](https://img-blog.csdnimg.cn/img_convert/bb06219a24cbb1e50b01982299566c07.png) -![运行正常](https://img-blog.csdnimg.cn/img_convert/3bdb11cf5def6daa6196b1a57c5d2d17.png) -## 3.2 并发模拟-Jmeter压测 -![](https://img-blog.csdnimg.cn/img_convert/bc910009c5ef0fdeedd010888f792947.png) -![](https://img-blog.csdnimg.cn/img_convert/a40c37633be6b1ada9013773224d7998.png) -![添加"查看结果数"和"图形结果"监听器](https://img-blog.csdnimg.cn/img_convert/e21935009955b121f641ccaba344edab.png) -![log view 下当前日志信息](https://img-blog.csdnimg.cn/img_convert/0c48834ab0b6c22746d638967d62bc20.png) -![图形结果](https://img-blog.csdnimg.cn/img_convert/c0603ace9b85444880b66e7493a3736f.png) +##并发的优势与风险 +![](https://upload-images.jianshu.io/upload_images/4685968-e083e9bf164b7d73.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +#3 项目准备 +##3.1 项目初始化 +![自定义4个基本注解](https://upload-images.jianshu.io/upload_images/4685968-9ea512f5c1b3b4ea.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![随手写个测试类](https://upload-images.jianshu.io/upload_images/4685968-7bc23c076be35936.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![运行正常](https://upload-images.jianshu.io/upload_images/4685968-c7fba914bceb792e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +##3.2 并发模拟-Jmeter压测 +![](https://upload-images.jianshu.io/upload_images/4685968-8c994437f5663dd6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-0a6f7c5e0217aa17.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![添加"查看结果数"和"图形结果"监听器](https://upload-images.jianshu.io/upload_images/4685968-646e66ad4e2c7ffc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![log view 下当前日志信息](https://upload-images.jianshu.io/upload_images/4685968-cc70555c9f3b78b2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![图形结果](https://upload-images.jianshu.io/upload_images/4685968-01c21bb9069bcd28.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 3.3 并发模拟-代码 ### CountDownLatch -- 可阻塞线程,并保证当满足特定条件时可继续执行 -### Semaphore(信号量) -可阻塞线程,控制同一时间段内的并发量。 - -以上二者通常和线程池搭配。 +![可阻塞线程,并保证当满足特定条件时可继续执行](https://upload-images.jianshu.io/upload_images/4685968-0d0481ec5302cadb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +###Semaphore(信号量) +![可阻塞线程,控制同一时间段内的并发量](https://upload-images.jianshu.io/upload_images/4685968-3133af3b8e419a39.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +以上二者通常和线程池搭配 -并发模拟: +下面开始做并发模拟 ```java package com.mmall.concurrency; @@ -195,7 +198,17 @@ public class AtomicExample2 { } ``` -```java +``` +package com.mmall.concurrency.example.atomic; + +import com.mmall.concurrency.annoations.ThreadSafe; +import lombok.extern.slf4j.Slf4j; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author JavaEdge + * @date 18/4/3 + */ @Slf4j @ThreadSafe public class AtomicExample4 { @@ -217,21 +230,21 @@ public class AtomicExample4 { } } ``` -![](https://img-blog.csdnimg.cn/img_convert/ed21be49eb204a082bdf73e6f454169e.png) +![输出结果](https://upload-images.jianshu.io/upload_images/4685968-b60dac7453bd0904.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - AtomicReference,AtomicReferenceFieldUpdater -![](https://img-blog.csdnimg.cn/img_convert/035f5f4e875de5f6acc4c825d60b19fc.png) +![](https://upload-images.jianshu.io/upload_images/4685968-d726108a34669255.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - AtomicBoolean -![](https://img-blog.csdnimg.cn/img_convert/b761bb82385e4e8750092328ac1cbf44.png) +![](https://upload-images.jianshu.io/upload_images/4685968-b5fb1eaf9938c163.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - AtomicStampReference : CAS的 ABA 问题 ### 4.2.2 锁 synchronized:依赖 JVM - 修饰代码块:大括号括起来的代码,作用于调用的对象 - 修饰方法: 整个方法,作用于调用的对象 -![](https://img-blog.csdnimg.cn/img_convert/dc5f64990fe69a62f9c722c790447e62.png) +![](https://upload-images.jianshu.io/upload_images/4685968-88b9935f21beef0e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - 修饰静态方法:整个静态方法,作用于所有对象 -![](https://img-blog.csdnimg.cn/img_convert/8cfd5c38ff9079fa2a9025ae6cc2573c.png) -```java +![](https://upload-images.jianshu.io/upload_images/4685968-f0b3530e761400d1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +``` package com.mmall.concurrency.example.count; import com.mmall.concurrency.annoations.ThreadSafe; @@ -318,9 +331,9 @@ JMM关于synchronized的规定 屏障指令,将本地内存中的共享变量值刷新到主内存 - 对volatile变量读操作时,会在读操作前加入一条load 屏障指令,从主内存中读取共享变量 -![volatile 写](https://img-blog.csdnimg.cn/img_convert/9b5f34400031011ab65c6f0535fd4bcb.png) -![volatile 读](https://img-blog.csdnimg.cn/img_convert/d7340f94c160e7f53785d31ee9b4094a.png) -![计数类之 volatile 版,非线程安全的](https://img-blog.csdnimg.cn/img_convert/c949a42943079f294d9d5162988829e5.png) +![volatile 写](https://upload-images.jianshu.io/upload_images/4685968-1b52dfe716f14632.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![volatile 读](https://upload-images.jianshu.io/upload_images/4685968-fe9dd1b03c64c7f4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![计数类之 volatile 版,非线程安全的](https://upload-images.jianshu.io/upload_images/4685968-977262be174e2dbc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - volatile使用 ```java volatile boolean inited = false; @@ -352,19 +365,16 @@ JMM允许编译器和处理器对指令进行重排序,但是重排序过程 - 对象逸出 一种错误的发布。当-个对象还没有构造完成时,就使它被其他线程所见 -![发布对象](https://img-blog.csdnimg.cn/img_convert/584fdb52d87f3c3250dbabf80b68909b.png) +![发布对象](https://upload-images.jianshu.io/upload_images/4685968-b368f6fe5b350cbe.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -![对象逸出](https://img-blog.csdnimg.cn/img_convert/03a669241b1b109b5c3aedbdb6e8e0e6.png) +![对象逸出](https://upload-images.jianshu.io/upload_images/4685968-88d207fcc6bf1866.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 5.1 安全发布对象 -- 在静态初始化函数中初始化一个对象引用 -- 将对象的引用保存到volatile类型域或者AtomicReference对象中 -- 将对象的引用保存到某个正确构造对象的final类型域中 -- 将对象的引用保存到一个由锁保护的域中 -![非线程安全的懒汉模式](https://img-blog.csdnimg.cn/img_convert/53083bac102f7277c08ff5f57bbe9c34.png) -![饿汉模式](https://img-blog.csdnimg.cn/img_convert/393a83dc5077c07536f2efba418bd3df.png) -![线程安全的懒汉模式](https://img-blog.csdnimg.cn/img_convert/77fca2c11aaebe47cc5e1324f30c2c7f.png) +![](https://upload-images.jianshu.io/upload_images/4685968-7400ab2abe1dbbfb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![非线程安全的懒汉模式](https://upload-images.jianshu.io/upload_images/4685968-ba18bdbe3a3c4ed1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![饿汉模式](https://upload-images.jianshu.io/upload_images/4685968-be2854c290143094.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![线程安全的懒汉模式](https://upload-images.jianshu.io/upload_images/4685968-e632243a5a97281a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ```java package com.mmall.concurrency.example.singleton; @@ -420,12 +430,12 @@ public class SingletonExample4 { } } ``` -![](https://img-blog.csdnimg.cn/img_convert/049d7847d6ba8e5add4aa1749d61d503.png) -![](https://img-blog.csdnimg.cn/img_convert/532650dc170a88ce1476c315d0f999a7.png) +![](https://upload-images.jianshu.io/upload_images/4685968-823166cdf7936293.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-9002671b71096f6c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 7 AQS ## 7.1 介绍 -![数据结构](https://img-blog.csdnimg.cn/img_convert/a31bb8a3d2d938be38bac2a2f46f6baa.png) +![数据结构](https://upload-images.jianshu.io/upload_images/4685968-918dcaea77d556e9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - 使用Node实现FIFO队列,可以用于构建锁或者其他同步装置的基础框架 - 利用了一个int类型表示状态 - 使用方法是继承 @@ -532,11 +542,11 @@ public class CountDownLatchExample2 { } ``` ##Semaphore用法 -![](https://img-blog.csdnimg.cn/img_convert/750059f485bfac67c031496937c2c678.png) -![](https://img-blog.csdnimg.cn/img_convert/455407acb625c57b1de26351afc395d1.png) -![](https://img-blog.csdnimg.cn/img_convert/32149d53141d77efe6906ead30215c51.png) -## CycliBarrier -```java +![](https://upload-images.jianshu.io/upload_images/4685968-e6cbcd4254c642c5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-dbefbf2c76ad5a2a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-41f5f5a5fd135804.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +##CycliBarrier +``` package com.mmall.concurrency.example.aqs; import lombok.extern.slf4j.Slf4j; @@ -579,8 +589,8 @@ public class CyclicBarrierExample1 { } } ``` -![](https://img-blog.csdnimg.cn/img_convert/61b5f12aed68f4879eaf421921180919.png) -```java +![](https://upload-images.jianshu.io/upload_images/4685968-4fb51fa4926fd70e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +``` package com.mmall.concurrency.example.aqs; import lombok.extern.slf4j.Slf4j; @@ -628,8 +638,8 @@ public class CyclicBarrierExample2 { } } ``` -![await 超时导致程序抛异常](https://img-blog.csdnimg.cn/img_convert/fd7b571f566476e03b76b10f2a4eff35.png) -```java +![await 超时导致程序抛异常](https://upload-images.jianshu.io/upload_images/4685968-0f899c23531f8ee8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +``` package com.mmall.concurrency.example.aqs; import lombok.extern.slf4j.Slf4j; @@ -677,93 +687,85 @@ public class SemaphoreExample3 { } ``` -# 9 线程池 -## 9.1 newCachedThreadPool -![](https://img-blog.csdnimg.cn/img_convert/f62ed32ba431e25c4e6a8a3a111a571b.png) -## 9.2 newFixedThreadPool -![](https://img-blog.csdnimg.cn/img_convert/bc0c48352c23a3dba6719b375e71f972.png) -## 9.3 newSingleThreadExecutor +#9 线程池 +##9.1 newCachedThreadPool +![](https://upload-images.jianshu.io/upload_images/4685968-1122da7a48223ba1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +##9.2 newFixedThreadPool +![](https://upload-images.jianshu.io/upload_images/4685968-0ea942bf12e5210f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +##9.3 newSingleThreadExecutor 看出是顺序执行的 -![](https://img-blog.csdnimg.cn/img_convert/01743428143ee985c8b77ef540b4f7d6.png) +![](https://upload-images.jianshu.io/upload_images/4685968-989d59429f589403.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 9.4 newScheduledThreadPool -![](https://img-blog.csdnimg.cn/img_convert/fbd0a4e6d07f448eb40fd7b8f1cd25c6.png) -![](https://img-blog.csdnimg.cn/img_convert/ff45eb82a16ae5504cc575cb44ed51e8.png) +![](https://upload-images.jianshu.io/upload_images/4685968-f7536ec7a1cf6ecc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-c90e09d5bfe707e6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 10 死锁 -### 必要条件 -互斥条件 -请求和保持条件 -不剥夺条件 -环路等待条件 +![](https://upload-images.jianshu.io/upload_images/4685968-461f6a4251ae8ca4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-46d58773e597195f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 11 高并发之扩容思路 ## 11.1 扩容 -垂直扩容(纵向扩展) :提高系统部件能力 -水平扩容(横向扩展) :增加更多系统成员来实现 +![](https://upload-images.jianshu.io/upload_images/4685968-8fabe01a7a9073ba.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 11.1 扩容 - 数据库 -读操作扩展: memcache、redis、 CDN等缓存 -写操作扩展: Cassandra、Hbase等 +![](https://upload-images.jianshu.io/upload_images/4685968-f1b501aa5f49953d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 12 高并发之缓存思路 ## 12.1 缓存 -![](https://img-blog.csdnimg.cn/img_convert/76f907d1cc784020d995a7cf10dbd94f.png) +![](https://upload-images.jianshu.io/upload_images/4685968-c5b8fa643df25009.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ### 1 缓存特征 -命中率:命中数/(命中数+没有命中数) -最大元素(空间) -清空策略:FIFO,LFU,LRU,过期时间,随机等 -### 2 缓存分类和应用场景 -本地缓存:编程实现(成员变量、局部变量、静态变量)、Guava Cache +![](https://upload-images.jianshu.io/upload_images/4685968-272a9236ceace316.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +### 2 缓存命中率影响因素 +![](https://upload-images.jianshu.io/upload_images/4685968-302f50ee02ba8cf5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +### 3 缓存分类和应用场景 +![](https://upload-images.jianshu.io/upload_images/4685968-52b2f576007d9b70.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -分布式缓存:Memcache、Redis +## 12.2 高并发之缓存-特征、场景及组件介绍 -## 12.2 高并发之缓存-特征、场景及组件 ### 1 Guava Cache -![](https://img-blog.csdnimg.cn/img_convert/d067a93f221947fa484f9ee907f7eb86.png) +![](https://upload-images.jianshu.io/upload_images/4685968-23d4a01198f33926.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + ### 2 缓存 - Memchche -![](https://img-blog.csdnimg.cn/img_convert/9b11cc84e7057677c5a96ddeee3fccac.png) -![](https://img-blog.csdnimg.cn/img_convert/f020c59264775ab2879c0033b0f99e23.png) -![](https://img-blog.csdnimg.cn/img_convert/b9ee8bc6ce9fd595c8d6ccf1ff8c3b30.png) +![](https://upload-images.jianshu.io/upload_images/4685968-ddde5e19b73fd570.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-7659178fc344a005.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-59c0c75277c9f31f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ### 3 缓存 - Redis -![](https://img-blog.csdnimg.cn/img_convert/979b05e24d7c4bd42e8a670be0b2cb7c.png) +![](https://upload-images.jianshu.io/upload_images/4685968-c8267b97a065b4b7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 12.3 redis的使用 - 配置类 -![](https://img-blog.csdnimg.cn/img_convert/0028d7224be1324243cccf0c8f7bfc1b.png) -![](https://img-blog.csdnimg.cn/img_convert/7a999a064f2d5b68f10307dbc0294af8.png) +![](https://upload-images.jianshu.io/upload_images/4685968-4134748d1b3181f2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-8634e07d480b8546.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - 服务类 -![](https://img-blog.csdnimg.cn/img_convert/832c747fdc3815b4236f15fe49c7e326.png) -![](https://img-blog.csdnimg.cn/img_convert/021f2e1f7504f684fabc7fa8ff705c82.png) -![](https://img-blog.csdnimg.cn/img_convert/26917bd4ba7dd415543215aff2e443e7.png) -![](https://img-blog.csdnimg.cn/img_convert/7c8093cefa4e7345caa275623cb300fe.png) +![](https://upload-images.jianshu.io/upload_images/4685968-fdb9daccd4778227.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-0c8d8a14d833b9e5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-461ff2bf851c25ab.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-a6efafc286fd0119.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 12.4 高并发场景问题及实战 ### 缓存一致性 -![](https://img-blog.csdnimg.cn/img_convert/3394e70367765d18207a4c843e343b27.png) +![](https://upload-images.jianshu.io/upload_images/4685968-8897cf0ea46fd70e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 13 高并发之消息队列思路 ## 13.1 业务案例 -![](https://img-blog.csdnimg.cn/img_convert/6f36c1d234f9df2a3965e1165e83229d.png) +![](https://upload-images.jianshu.io/upload_images/4685968-269e8e4d643ab023.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 将发短信封装成一条消息放进消息队列中,若发生短信过多,队列已满,需要控制发送的频率. 通过将事件封装成消息放入队列,实现了业务解耦,异步设计,确保了短信服务只要正常后,一定会将短信成功发到用户. ## 13.2 消息队列的特性 -业务无关:只做消息分发 -FIFO :先投递先到达 -容灾:节点的动态增删和消息的持久化 -性能:吞吐量提升,系统内部通信效率提高 - +![](https://upload-images.jianshu.io/upload_images/4685968-4153efed9fa882d2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - 为何需要消息队列呢 -[生产]和[消费]的速度或稳定性等因素不一致 +![](https://upload-images.jianshu.io/upload_images/4685968-4d0a52ea405e86af.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - 优点 -业务解耦 -最终一致性 -广播 -错峰与流控 +![](https://upload-images.jianshu.io/upload_images/4685968-6ff5a7fbc96173b9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-5b2f547310cfd06f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + ## 队列 ### kafka -![](https://img-blog.csdnimg.cn/img_convert/3f5b237b1a8054b8f19b5ead0161874d.png) \ No newline at end of file +![](https://upload-images.jianshu.io/upload_images/4685968-f387cab7dbc62cb3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +、 diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\347\232\204Condition\346\216\245\345\217\243.md" "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\347\232\204Condition\346\216\245\345\217\243.md" index 8d7c46edac..270f510f60 100644 --- "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\347\232\204Condition\346\216\245\345\217\243.md" +++ "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\347\232\204Condition\346\216\245\345\217\243.md" @@ -1,31 +1,40 @@ +> 这天,我还在安详的看书学习,学妹突然找我,问到:好学长,你懂 Condition 接口嘛?能教教我嘛? + + ![](https://img-blog.csdnimg.cn/20210421150717227.png) + + +看到学妹来了,我立马也精神了起来,说到: +![](https://img-blog.csdnimg.cn/20210421145352544.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + Condition就是实现了管程里面的条件变量。 Java 语言内置的管程里只有一个条件变量,而Lock&Condition实现的管程支持多个条件变量。 -支持多个条件变量,能让代码可读性更好,实现也更容易。例如,你看我这里实现一个阻塞队列,就需要两个条件变量: -- 队列不空 -空队列自然没有元素能出队 -- 队列不满 -队列已满,当然也不可有元素再入队 +因为支持多个条件变量,能让代码可读性更好,实现也更容易。 +例如,你看我这里实现一个阻塞队列,就需要两个条件变量。 + +> 可爱的学妹,又真诚发问到:那如何利用两个条件变量实现阻塞队列呢? + +一个阻塞队列,需要两个条件变量: +- 队列不空(空队列不可出队) +- 队列不满(队列已满不可入队) + ![](https://img-blog.csdnimg.cn/20210421150133653.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + Lock和Condition实现的管程,线程等待和通知需要调用await()、signal()、signalAll(),它们的语义和wait()、notify()、notifyAll()相同。 - Lock&Condition实现的管程里只能使用await()、signal()、signalAll() - synchronized实现的管程才能使用wait()、notify()、notifyAll() -如果在Lock&Condition实现的管程里调用wait()、notify()、notifyAll(),你距离离职就更近一步了。 +如果在Lock&Condition实现的管程里调用wait()、notify()、notifyAll(),你距离离职就更近一步。 -### Thread.sleep() V.S Condition.await() -Object.wait()和Condition.await()的原理是基本一致的,不同在于Condition.await()底层是调用LockSupport.park()实现阻塞当前线程。它在阻塞当前线程前,其实还做了: -1. 把当前线程添加到条件队列 -2. **完全**释放锁,即让state=0,然后才调用`LockSupport.park()`阻塞当前线程 +JDK的Lock和Condition不过就是管程的一种实现,看看在Dubbo中,Lock和Condition是怎么用的。 -JDK的Lock和Condition不过就是管程的一种实现,一般如何使用呢? +> 我们先要清楚,什么是同步与异步呢? -> 什么是同步与异步? - 同步 调用方需要等待结果 - 异步 不需要等待结果 -> 代码里如何实现异步? +> 那代码里如何实现异步呢? - 调用方创建一个子线程,在子线程中执行方法调用,即异步调用 - 方法实现时,创建一个新的线程执行主要逻辑,主线程直接return,即异步方法。 @@ -34,7 +43,7 @@ JDK的Lock和Condition不过就是管程的一种实现,一般如何使用呢 > 是不是好奇了,明明日常使用的RPC调用都是同步的呀?这到底是同步还是异步? -这肯定有人帮忙实现了异步转同步。比如RPC框架Dubbo,具体它是怎么做到的呢? +很好想象,肯定有人帮忙实现了异步转同步。比如RPC框架Dubbo,具体它是怎么做到的呢? 对于下面一个简单的RPC调用,默认情况下sayHello()是个同步方法,即执行service.sayHello(“dubbo”)时,线程会停下来等结果。 @@ -55,6 +64,7 @@ DefaultFuture.get()之前发生了什么呢: > - RPC返回结果前,阻塞调用线程,让调用线程等待 > - RPC返回结果后,唤醒调用线程,让调用线程重新执行 -这就是经典的等待-通知机制,即管程的实现方案。 -- 看看Dubbo是怎么实现的。 -![](https://img-blog.csdnimg.cn/20210421170330667.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) \ No newline at end of file +这就是经典的等待-通知机制。也就想到了管程的实现方案。看看远古版本的Dubbo是怎么实现的。 +![](https://img-blog.csdnimg.cn/20210421170330667.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +好了,下课! \ No newline at end of file diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\347\272\277\347\250\213\346\261\240ThreadPoolExecutor.md" "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\347\272\277\347\250\213\346\261\240ThreadPoolExecutor.md" deleted file mode 100644 index 9bba52ac2a..0000000000 --- "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\347\272\277\347\250\213\346\261\240ThreadPoolExecutor.md" +++ /dev/null @@ -1,1267 +0,0 @@ -# 1 为什么要用线程池 -## 1.1 线程the more, the better? -1、线程在java中是一个对象,更是操作系统的资源,线程创建、销毁都需要时间。 -如果创建时间+销毁时间>执行任务时间就很不合算。 -2、Java对象占用堆内存,操作系统线程占用系统内存,根据JVM规范,一个线程默认最大栈 -大小1M,这个栈空间是需要从系统内存中分配的。线程过多,会消耗很多的内存。 -3、操作系统需要频繁切换线程上下文(大家都想被运行),影响性能。 - -线程使应用能够更加充分合理地协调利用CPU、内存、网络、I/O等系统资源. -线程的创建需要开辟虚拟机栈、本地方法栈、程序计数器等线程私有的内存空间; -在线程销毁时需要回收这些系统资源. -频繁地创建和销毁线程会浪费大量的系统资源,增加并发编程风险. - -在服务器负载过大的时候,如何让新的线程等待或者友好地拒绝服务? - -这些都是线程自身无法解决的; -所以需要通过线程池协调多个线程,并实现类似主次线程隔离、定时执行、周期执行等任务. - -# 2 线程池的作用 -● 利用线程池管理并复用线程、控制最大并发数等 - -● 实现任务线程队列缓存策略和拒绝机制 - -● 实现某些与时间相关的功能 -如定时执行、周期执行等 - -● 隔离线程环境 -比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大; -因此,通过配置独立的线程池,将较慢的交易服务与搜索服务隔离开,避免各服务线程相互影响. - -在开发中,合理地使用线程池能够带来3个好处 - - **降低资源消耗** 通过重复利用已创建的线程,降低创建和销毁线程造成的系统资源消耗 - - **提高响应速度** 当任务到达时,任务可以不需要等到线程创建就能立即执行 - - **提高线程的可管理性** 线程是稀缺资源,如果过多地创建,不仅会消耗系统资源,还会降低系统的稳定性,导致使用线程池可以进行统一分配、调优和监控。 - -# 3 概念 -1、**线程池管理器** -用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务; -2、**工作线程** -线程池中线程,在没有任务时处于等待状态,可以循环的执行任务; -3、**任务接口** -每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等; -4、**任务队列** -用于存放没有处理的任务。提供缓冲机制。. - -- 原理示意图 -![](https://img-blog.csdnimg.cn/20191009015833132.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -# 4 线程池API -## 4.1 接口定义和实现类 -![](https://img-blog.csdnimg.cn/2019100901595683.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -### 继承关系图 -![线程池相关类图](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyOTA5XzQ2ODU5NjgtZWFhYWY4ZmQ4ODQ5Nzc1Ny5wbmc?x-oss-process=image/format,png) -可以认为ScheduledThreadPoolExecutor是最丰富的实现类! - -## 4.2 方法定义 -### 4.2.1 ExecutorService -![](https://img-blog.csdnimg.cn/20191009020347726.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -### 4.2.2 ScheduledExecutorService -#### public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit); -![](https://img-blog.csdnimg.cn/20191013013014872.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -#### public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit); -![](https://img-blog.csdnimg.cn/20191013013113751.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -#### 以上两种都是创建并执行一个一次性任务, 过了延迟时间就会被执行 -#### public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); - -![](https://img-blog.csdnimg.cn/20191013013412305.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -创建并执行一个周期性任务 -过了给定的初始延迟时间,会第一次被执行 -执行过程中发生了异常,那么任务就停止 - -一次任务 执行时长超过了周期时间,下一次任务会等到该次任务执行结束后,立刻执行,这也是它和`scheduleWithFixedDelay`的重要区别 - -#### public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); -创建并执行一个周期性任务 -过了初始延迟时间,第一次被执行,后续以给定的周期时间执行 -执行过程中发生了异常,那么任务就停止 - -一次任务执行时长超过了周期时间,下一 次任务 会在该次任务执 -行结束的时间基础上,计算执行延时。 -对于超过周期的长时间处理任务的不同处理方式,这是它和`scheduleAtFixedRate`的重要区别。 - -### 实例 -- 测试例子 -![](https://img-blog.csdnimg.cn/20191013153615841.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 测试实现 -![](https://img-blog.csdnimg.cn/20191013153730175.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 运行结果 -![](https://img-blog.csdnimg.cn/2019101315391641.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -可以看出超过core的线程都在等待,线程池线程数量为何达不到最大线程数呢?那这个参数还有什么意义, 让我们继续往下阅读吧! - - -### 4.2.2 Executors工具类 -你可以自己实例化线程池,也可以用`Executors`创建线程池的工厂类,常用方法如下: - -`ExecutorService` 的抽象类`AbstractExecutorService `提供了`submit`、`invokeAll` 等方法的实现; -但是核心方法`Executor.execute()`并没有在这里实现. -因为所有的任务都在该方法执行,不同实现会带来不同的执行策略. - -通过`Executors`的静态工厂方法可以创建三个线程池的包装对象 -- ForkJoinPool、 -- ThreadPoolExecutor -- ScheduledThreadPoolExecutor - -● Executors.newWorkStealingPool -JDK8 引入,创建持有足够线程的线程池支持给定的并行度; -并通过使用多个队列减少竞争; -构造方法中把CPU数量设置为默认的并行度. -返回`ForkJoinPool` ( JDK7引入)对象,它也是`AbstractExecutorService` 的子类 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyOTA2XzQ2ODU5NjgtM2I0YThlOGMxNDA4Zjg5Mi5wbmc?x-oss-process=image/format,png) - - -● Executors.newCachedThreadPool -创建的是一个无界的缓冲线程池。它的任务队列是一个同步队列。 -任务加入到池中 -- 如果池中有空闲线程,则用空闲线程执行 -- 如无, 则创建新线程执行。 - -池中的线程空闲超过60秒,将被销毁。线程数随任务的多少变化。 -`适用于执行耗时较小的异步任务`。池的核心线程数=0 ,最大线程数= Integer.MAX_ _VALUE -`maximumPoolSize` 最大可以至`Integer.MAX_VALUE`,是高度可伸缩的线程池. -若达到该上限,相信没有服务器能够继续工作,直接OOM. -`keepAliveTime` 默认为60秒; -工作线程处于空闲状态,则回收工作线程; -如果任务数增加,再次创建出新线程处理任务. - -● Executors.newScheduledThreadPool -能定时执行任务的线程池。该池的核心线程数由参数指定,线程数最大至`Integer.MAX_ VALUE`,与上述相同,存在OOM风险. -`ScheduledExecutorService`接口的实现类,支持**定时及周期性任务执行**; -相比`Timer`,` ScheduledExecutorService` 更安全,功能更强大. -与`newCachedThreadPool`的区别是**不回收工作线程**. - -● Executors.newSingleThreadExecutor -创建一个单线程的线程池,相当于单线程串行执行所有任务,保证按任务的提交顺序依次执行. -只有-个线程来执行无界任务队列的单-线程池。该线程池确保任务按加入的顺序一个一 -个依次执行。当唯一的线程因任务 异常中止时,将创建一个新的线程来继续执行 后续的任务。 -与newFixedThreadPool(1)的区别在于,单线程池的池大小在`newSingleThreadExecutor`方法中硬编码,不能再改变的。 - - -● Executors.newFixedThreadPool -创建一个固定大小任务队列容量无界的线程池 -输入的参数即是固定线程数; -既是核心线程数也是最大线程数; -不存在空闲线程,所以`keepAliveTime`等于0. -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyODE5XzQ2ODU5NjgtOGNkOTFmM2M2ZWFkYTlkZS5wbmc?x-oss-process=image/format,png) -其中使用了 LinkedBlockingQueue, 但是没有设置上限!!!,堆积过多任务!!! - -下面介绍`LinkedBlockingQueue`的构造方法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyOTEwXzQ2ODU5NjgtZmNlMjYxZGJlMzBkZWY3MS5wbmc?x-oss-process=image/format,png) -使用这样的无界队列,如果瞬间请求非常大,会有OOM的风险; -除`newWorkStealingPool` 外,其他四个创建方式都存在资源耗尽的风险. - -不推荐使用其中的任何创建线程池的方法,因为都没有任何限制,存在安全隐患. - - `Executors`中默认的线程工厂和拒绝策略过于简单,通常对用户不够友好. -线程工厂需要做创建前的准备工作,对线程池创建的线程必须明确标识,就像药品的生产批号一样,为线程本身指定有意义的名称和相应的序列号. -拒绝策略应该考虑到业务场景,返回相应的提示或者友好地跳转. -以下为简单的ThreadFactory 示例 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzk3XzQ2ODU5NjgtZDIwMjUyODdhODJhZGQ5NS5wbmc?x-oss-process=image/format,png) - -上述示例包括线程工厂和任务执行体的定义; -通过newThread方法快速、统一地创建线程任务,强调线程一定要有特定意义的名称,方便出错时回溯. - -- 单线程池:newSingleThreadExecutor()方法创建,五个参数分别是ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())。含义是池中保持一个线程,最多也只有一个线程,也就是说这个线程池是顺序执行任务的,多余的任务就在队列中排队。 -- 固定线程池:newFixedThreadPool(nThreads)方法创建 -[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NZEi0e3y-1570557031347)(https://uploadfiles.nowcoder.com/images/20190625/5088755_1561474494512_5D0DD7BCB7171E9002EAD3AEF42149E6 "图片标题")] - -池中保持nThreads个线程,最多也只有nThreads个线程,多余的任务也在队列中排队。 -[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SId8FBO1-1570557031347)(https://uploadfiles.nowcoder.com/images/20190625/5088755_1561476084467_4A47A0DB6E60853DEDFCFDF08A5CA249 "图片标题")] - -[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6uzv6UAk-1570557031348)(https://uploadfiles.nowcoder.com/images/20190625/5088755_1561476102425_FB5C81ED3A220004B71069645F112867 "图片标题")] -线程数固定且线程不超时 -- 缓存线程池:newCachedThreadPool()创建,五个参数分别是ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue())。 -含义是池中不保持固定数量的线程,随需创建,最多可以创建Integer.MAX_VALUE个线程(说一句,这个数量已经大大超过目前任何操作系统允许的线程数了),空闲的线程最多保持60秒,多余的任务在SynchronousQueue(所有阻塞、并发队列在后续文章中具体介绍)中等待。 - -为什么单线程池和固定线程池使用的任务阻塞队列是LinkedBlockingQueue(),而缓存线程池使用的是SynchronousQueue()呢? -因为单线程池和固定线程池中,线程数量是有限的,因此提交的任务需要在LinkedBlockingQueue队列中等待空余的线程;而缓存线程池中,线程数量几乎无限(上限为Integer.MAX_VALUE),因此提交的任务只需要在SynchronousQueue队列中同步移交给空余线程即可。 - -- 单线程调度线程池:newSingleThreadScheduledExecutor()创建,五个参数分别是 (1, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue())。含义是池中保持1个线程,多余的任务在DelayedWorkQueue中等待。 -- 固定调度线程池:newScheduledThreadPool(n)创建,五个参数分别是 (n, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue())。含义是池中保持n个线程,多余的任务在DelayedWorkQueue中等待。 - -有一项技术可以缓解执行时间较长任务造成的影响,即限定任务等待资源的时间,而不要无限的等待 - -先看第一个例子,测试单线程池、固定线程池和缓存线程池(注意增加和取消注释): - -``` -public class ThreadPoolExam { - public static void main(String[] args) { - //first test for singleThreadPool - ExecutorService pool = Executors.newSingleThreadExecutor(); - //second test for fixedThreadPool -// ExecutorService pool = Executors.newFixedThreadPool(2); - //third test for cachedThreadPool -// ExecutorService pool = Executors.newCachedThreadPool(); - for (int i = 0; i < 5; i++) { - pool.execute(new TaskInPool(i)); - } - pool.shutdown(); - } -} - -class TaskInPool implements Runnable { - private final int id; - - TaskInPool(int id) { - this.id = id; - } - - @Override - public void run() { - try { - for (int i = 0; i < 5; i++) { - System.out.println("TaskInPool-["+id+"] is running phase-"+i); - TimeUnit.SECONDS.sleep(1); - } - System.out.println("TaskInPool-["+id+"] is over"); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } -} -``` - -如图为排查底层公共缓存调用出错时的截图 -![有意义的线程命名](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzQ5XzQ2ODU5NjgtODU1MDI1MzM5MDZjMzNmMi5wbmc?x-oss-process=image/format,png) -绿色框采用自定义的线程工厂,明显比蓝色框默认的线程工厂创建的线程名称拥有更多的额外信息:如调用来源、线程的业务含义,有助于快速定位到死锁、StackOverflowError 等问题. - -# 5 创建线程池 -首先从`ThreadPoolExecutor`构造方法讲起,学习如何自定义`ThreadFactory`和`RejectedExecutionHandler`; -并编写一个最简单的线程池示例. -然后,通过分析`ThreadPoolExecutor`的`execute`和`addWorker`两个核心方法; -学习如何把任务线程加入到线程池中运行. - -- ThreadPoolExecutor 的构造方法如下 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzMyXzQ2ODU5NjgtYTVmOTU1Yjc5MmJkNDUzZS5wbmc?x-oss-process=image/format,png) - -- 第1个参数: corePoolSize 表示常驻核心线程数 -如果等于0,则任务执行完之后,没有任何请求进入时销毁线程池的线程; -如果大于0,即使本地任务执行完毕,核心线程也不会被销毁. -这个值的设置非常关键; -设置过大会浪费资源; -设置过小会导致线程频繁地创建或销毁. - -- 第2个参数: maximumPoolSize 表示线程池能够容纳同时执行的最大线程数 -从第1处来看,必须>=1. -如果待执行的线程数大于此值,需要借助第5个参数的帮助,缓存在队列中. -如果`maximumPoolSize = corePoolSize`,即是固定大小线程池. - -- 第3个参数: keepAliveTime 表示线程池中的线程空闲时间 -当空闲时间达到`keepAliveTime`时,线程会被销毁,直到只剩下`corePoolSize`个线程; -避免浪费内存和句柄资源. -在默认情况下,当线程池的线程数大于`corePoolSize`时,`keepAliveTime`才起作用. -但是当`ThreadPoolExecutor`的`allowCoreThreadTimeOut = true`时,核心线程超时后也会被回收. - -- 第4个参数: TimeUnit表示时间单位 -keepAliveTime的时间单位通常是TimeUnit.SECONDS. - -- 第5个参数: workQueue 表示缓存队列 -当请求的线程数大于`maximumPoolSize`时,线程进入`BlockingQueue`. -后续示例代码中使用的LinkedBlockingQueue是单向链表,使用锁来控制入队和出队的原子性; -两个锁分别控制元素的添加和获取,是一个生产消费模型队列. - -- 第6个参数: threadFactory 表示线程工厂 -它用来生产一组相同任务的线程; -线程池的命名是通过给这个factory增加组名前缀来实现的. -在虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的. - -- 第7个参数: handler 表示执行拒绝策略的对象 -当超过第5个参数`workQueue`的任务缓存区上限的时候,即可通过该策略处理请求,属于一种简单的限流保护。 -友好的拒绝策略可以是如下: -1. 保存到数据库进行削峰填谷,空闲时再提取出来执行 -2. 转向某个提示页面 -3. 打印日志 - -### 2.1.1 corePoolSize(核心线程数量) -线程池中应该保持的主要线程的数量.即使线程处于空闲状态,除非设置了`allowCoreThreadTimeOut`这个参数,当提交一个任务到线程池时,若线程数量Integer 有32位; -最右边29位表工作线程数; -最左边3位表示线程池状态,可表示从0至7的8个不同数值 -线程池的状态用高3位表示,其中包括了符号位. -五种状态的十进制值按从小到大依次排序为 -RUNNING < SHUTDOWN < STOP < TIDYING =核心线程数 或线程创建失败,则将当前任务放到工作队列中 - // 只有线程池处于 RUNNING 态,才执行后半句 : 置入队列 - if (isRunning(c) && workQueue.offer(command)) { - int recheck = ctl.get(); - - // 只有线程池处于 RUNNING 态,才执行后半句 : 置入队列 - if (! isRunning(recheck) && remove(command)) - reject(command); - // 若之前的线程已被消费完,新建一个线程 - else if (workerCountOf(recheck) == 0) - addWorker(null, false); - // 核心线程和队列都已满,尝试创建一个新线程 - } - else if (!addWorker(command, false)) - // 抛出RejectedExecutionException异常 - // 若 addWorker 返回是 false,即创建失败,则唤醒拒绝策略. - reject(command); - } -``` -发生拒绝的理由有两个 -( 1 )线程池状态为非RUNNING状态 -(2)等待队列已满。 - -下面继续分析`addWorker` - -## addWorker 源码解析 - -根据当前线程池状态,检查是否可以添加新的任务线程,若可以则创建并启动任务; -若一切正常则返回true; -返回false的可能性如下 -1. 线程池没有处于`RUNNING`态 -2. 线程工厂创建新的任务线程失败 -### 参数 -- firstTask -外部启动线程池时需要构造的第一个线程,它是线程的母体 -- core -新增工作线程时的判断指标 - - true -需要判断当前`RUNNING`态的线程是否少于`corePoolsize` - - false -需要判断当前`RUNNING`态的线程是否少于`maximumPoolsize` -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUzMjIwXzQ2ODU5NjgtMjg2MDRmYjVkYTE5MjJlNC5wbmc?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzg5XzQ2ODU5NjgtOTk1ZmFlOTQyOTQwMjFjNy5wbmc?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyODMwXzQ2ODU5NjgtM2Y3NzViOWQ1MThmMzc4My5wbmc?x-oss-process=image/format,png) - -这段代码晦涩难懂,部分地方甚至违反代码规约,但其中蕴含丰富的编码知识点 - -- 第1处,配合循环语句出现的label,类似于goto 作用 -label 定义时,必须把标签和冒号的组合语句紧紧相邻定义在循环体之前,否则会编译出错. -目的是 在实现多重循环时能够快速退出到任何一层; -出发点似乎非常贴心,但在大型软件项目中,滥用标签行跳转的后果将是灾难性的. -示例代码中在`retry`下方有两个无限循环; -在`workerCount`加1成功后,直接退出两层循环. - -- 第2处,这样的表达式不利于阅读,应如是 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzg1XzQ2ODU5NjgtMDg2ZTlkNWY5ZGEyYWZkNC5wbmc?x-oss-process=image/format,png) - -- 第3处,与第1处的标签呼应,`AtomicInteger`对象的加1操作是原子性的; -`break retry`表 直接跳出与`retry` 相邻的这个循环体 - -- 第4处,此`continue`跳转至标签处,继续执行循环. -如果条件为false,则说明线程池还处于运行状态,即继续在`for(;)`循环内执行. - -- 第5处,`compareAndIncrementWorkerCount `方法执行失败的概率非常低. -即使失败,再次执行时成功的概率也是极高的,类似于自旋原理. -这里是先加1,创建失败再减1,这是轻量处理并发创建线程的方式; -如果先创建线程,成功再加1,当发现超出限制后再销毁线程,那么这样的处理方式明显比前者代价要大. - -- 第6处,`Worker `对象是工作线程的核心类实现,部分源码如下 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUzMjMyXzQ2ODU5NjgtYzkzNTI3ODJjNTZjM2Q2Ny5wbmc?x-oss-process=image/format,png) -它实现了`Runnable`接口,并把本对象作为参数输入给`run()`中的`runWorker (this)`; -所以内部属性线程`thread`在`start`的时候,即会调用`runWorker`. - -# 总结 -线程池的相关源码比较精炼,还包括线程池的销毁、任务提取和消费等,与线程状态图一样,线程池也有自己独立的状态转化流程,本节不再展开。 -总结一下,使用线程池要注意如下几点: -(1)合理设置各类参数,应根据实际业务场景来设置合理的工作线程数。 -(2)线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 -(3)创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。 - -线程池不允许使用Executors,而是通过ThreadPoolExecutor的方式创建,这样的处理方式能更加明确线程池的运行规则,规避资源耗尽的风险。 - - - - - -进一步查看源码发现,这些方法最终都调用了ThreadPoolExecutor和ScheduledThreadPoolExecutor的构造函数 -而ScheduledThreadPoolExecutor继承自ThreadPoolExecutor - -## 0.2 ThreadPoolExecutor 自定义线程池 -[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5A6eRvc8-1570557031390)(https://uploadfiles.nowcoder.com/images/20190625/5088755_1561476436402_10FB15C77258A991B0028080A64FB42D "图片标题")] -它们都是某种线程池,可以控制线程创建,释放,并通过某种策略尝试复用线程去执行任务的一个管理框架 - -,因此最终所有线程池的构造函数都调用了Java5后推出的ThreadPoolExecutor的如下构造函数 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyODEwXzQ2ODU5NjgtYmY0MTAwOTU5Nzk4NjA1OC5wbmc?x-oss-process=image/format,png) - -## Java默认提供的线程池 -Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUzMDc1XzQ2ODU5NjgtNGYxOGI1ZTk2ZWIxZDkzMC5wbmc?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyOTg5XzQ2ODU5NjgtYjdlYzU5YTgwMDQ0MmIyNi5wbmc?x-oss-process=image/format,png) - -我们只需要将待执行的方法放入 run 方法中,将 Runnable 接口的实现类交给线程池的 -execute 方法,作为他的一个参数,比如: -```java -Executor e=Executors.newSingleThreadExecutor(); -e.execute(new Runnable(){ //匿名内部类 public void run(){ -//需要执行的任务 -} -}); - -``` -# 线程池原理 - 任务execute过程 - - 流程图 -![](https://img-blog.csdnimg.cn/20191014020916959.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 示意图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzMwXzQ2ODU5NjgtYTA3YjhiMzIzMzMxYzE1ZS5wbmc?x-oss-process=image/format,png) - -ThreadPoolExecutor执行execute()分4种情况 - - 若当前运行的线程少于`corePoolSize`,则创建新线程来执行任务(该步需要获取全局锁) - - 若运行的线程多于或等于`corePoolSize`,且工作队列没满,则将新提交的任务存储在工作队列里。即, 将任务加入`BlockingQueue` - - 若无法将任务加入`BlockingQueue`,且没达到线程池最大数量, 则创建新的线程来处理任务(该步需要获取全局锁) - - 若创建新线程将使当前运行的线程超出`maximumPoolSize`,任务将被拒绝,并调用`RejectedExecutionHandler.rejectedExecution()` - -采取上述思路,是为了在执行`execute()`时,尽可能避免获取全局锁 -在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁 - -## 实例 -![](https://img-blog.csdnimg.cn/20191013231005279.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 结果 -![](https://img-blog.csdnimg.cn/20191014015653362.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -# **源码分析** -```java - /** - * 检查是否可以根据当前池状态和给定的边界(核心或最大) - * 添加新工作线程。如果是这样,工作线程数量会相应调整,如果可能的话,一个新的工作线程创建并启动 - * 将firstTask作为其运行的第一项任务。 - * 如果池已停止此方法返回false - * 如果线程工厂在被访问时未能创建线程,也返回false - * 如果线程创建失败,或者是由于线程工厂返回null,或者由于异常(通常是在调用Thread.start()后的OOM)),我们干净地回滚。 - * - * @param core if true use corePoolSize as bound, else - * maximumPoolSize. (A boolean indicator is used here rather than a - * value to ensure reads of fresh values after checking other pool - * state). - * @return true if successful - */ - private boolean addWorker(Runnable firstTask, boolean core) { - retry: - for (;;) { - int c = ctl.get(); - int rs = runStateOf(c); - - - /** - * Check if queue empty only if necessary. - * - * 如果线程池已关闭,并满足以下条件之一,那么不创建新的 worker: - * 1. 线程池状态大于 SHUTDOWN,也就是 STOP, TIDYING, 或 TERMINATED - * 2. firstTask != null - * 3. workQueue.isEmpty() - * 简单分析下: - * 状态控制的问题,当线程池处于 SHUTDOWN ,不允许提交任务,但是已有任务继续执行 - * 当状态大于 SHUTDOWN ,不允许提交任务,且中断正在执行任务 - * 多说一句:若线程池处于 SHUTDOWN,但 firstTask 为 null,且 workQueue 非空,是允许创建 worker 的 - * - */ - if (rs >= SHUTDOWN && - ! (rs == SHUTDOWN && - firstTask == null && - ! workQueue.isEmpty())) - return false; - - for (;;) { - int wc = workerCountOf(c); - if (wc >= CAPACITY || - wc >= (core ? corePoolSize : maximumPoolSize)) - return false; - // 如果成功,那么就是所有创建线程前的条件校验都满足了,准备创建线程执行任务 - // 这里失败的话,说明有其他线程也在尝试往线程池中创建线程 - if (compareAndIncrementWorkerCount(c)) - break retry; - // 由于有并发,重新再读取一下 ctl - c = ctl.get(); // Re-read ctl - // 正常如果是 CAS 失败的话,进到下一个里层的for循环就可以了 - // 可如果是因为其他线程的操作,导致线程池的状态发生了变更,如有其他线程关闭了这个线程池 - // 那么需要回到外层的for循环 - if (runStateOf(c) != rs) - continue retry; - // else CAS failed due to workerCount change; retry inner loop - } - } - - /* * - * 到这里,我们认为在当前这个时刻,可以开始创建线程来执行任务 - */ - - // worker 是否已经启动 - boolean workerStarted = false; - // 是否已将这个 worker 添加到 workers 这个 HashSet 中 - boolean workerAdded = false; - Worker w = null; - try { - // 把 firstTask 传给 worker 的构造方法 - w = new Worker(firstTask); - // 取 worker 中的线程对象,Worker的构造方法会调用 ThreadFactory 来创建一个新的线程 - final Thread t = w.thread; - if (t != null) { - //先加锁 - final ReentrantLock mainLock = this.mainLock; - // 这个是整个类的全局锁,持有这个锁才能让下面的操作“顺理成章”, - // 因为关闭一个线程池需要这个锁,至少我持有锁的期间,线程池不会被关闭 - mainLock.lock(); - try { - // Recheck while holding lock. - // Back out on ThreadFactory failure or if - // shut down before lock acquired. - int rs = runStateOf(ctl.get()); - - // 小于 SHUTTDOWN 即 RUNNING - // 如果等于 SHUTDOWN,不接受新的任务,但是会继续执行等待队列中的任务 - if (rs < SHUTDOWN || - (rs == SHUTDOWN && firstTask == null)) { - // worker 里面的 thread 不能是已启动的 - if (t.isAlive()) // precheck that t is startable - throw new IllegalThreadStateException(); - // 加到 workers 这个 HashSet 中 - workers.add(w); - int s = workers.size(); - if (s > largestPoolSize) - largestPoolSize = s; - workerAdded = true; - } - } finally { - mainLock.unlock(); - } - // 若添加成功 - if (workerAdded) { - // 启动线程 - t.start(); - workerStarted = true; - } - } - } finally { - // 若线程没有启动,做一些清理工作,若前面 workCount 加了 1,将其减掉 - if (! workerStarted) - addWorkerFailed(w); - } - // 返回线程是否启动成功 - return workerStarted; - } -``` -看下 `addWorkFailed` -![workers 中删除掉相应的 worker,workCount 减 1 -private void addWor](https://upload-images.jianshu.io/upload_images/4685968-77abdc7bff21cca6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -![记录 workers 中的个数的最大值,因为 workers 是不断增加减少的,通过这个值可以知道线程池的大小曾经达到的最大值](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzA4XzQ2ODU5NjgtMDc4NDcyYjY4MmZjYzljZC5wbmc?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUzNjMzXzQ2ODU5NjgtMzNmNTE0NTc3ZTk3ZGMzNS5wbmc?x-oss-process=image/format,png) - - - - -`worker` 中的线程 `start` 后,其 `run` 方法会调用 `runWorker ` -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyODgwXzQ2ODU5NjgtYTAwOWJjMDJhMjI0ZGNlMi5wbmc?x-oss-process=image/format,png) -继续往下看 `runWorker` -```java -// worker 线程启动后调用,while 循环(即自旋!)不断从等待队列获取任务并执行 -// worker 初始化时,可指定 firstTask,那么第一个任务也就可以不需要从队列中获取 -final void runWorker(Worker w) { - Thread wt = Thread.currentThread(); - // 该线程的第一个任务(若有) - Runnable task = w.firstTask; - w.firstTask = null; - // 允许中断 - w.unlock(); - - boolean completedAbruptly = true; - try { - // 循环调用 getTask 获取任务 - while (task != null || (task = getTask()) != null) { - w.lock(); - // 若线程池状态大于等于 STOP,那么意味着该线程也要中断 - /** - * 若线程池STOP,请确保线程 已被中断 - * 如果没有,请确保线程未被中断 - * 这需要在第二种情况下进行重新检查,以便在关中断时处理shutdownNow竞争 - */ - if ((runStateAtLeast(ctl.get(), STOP) || - (Thread.interrupted() && - runStateAtLeast(ctl.get(), STOP))) && - !wt.isInterrupted()) - wt.interrupt(); - try { - // 这是一个钩子方法,留给需要的子类实现 - beforeExecute(wt, task); - Throwable thrown = null; - try { - // 到这里终于可以执行任务了 - task.run(); - } catch (RuntimeException x) { - thrown = x; throw x; - } catch (Error x) { - thrown = x; throw x; - } catch (Throwable x) { - // 这里不允许抛出 Throwable,所以转换为 Error - thrown = x; throw new Error(x); - } finally { - // 也是一个钩子方法,将 task 和异常作为参数,留给需要的子类实现 - afterExecute(task, thrown); - } - } finally { - // 置空 task,准备 getTask 下一个任务 - task = null; - // 累加完成的任务数 - w.completedTasks++; - // 释放掉 worker 的独占锁 - w.unlock(); - } - } - completedAbruptly = false; - } finally { - // 到这里,需要执行线程关闭 - // 1. 说明 getTask 返回 null,也就是说,这个 worker 的使命结束了,执行关闭 - // 2. 任务执行过程中发生了异常 - // 第一种情况,已经在代码处理了将 workCount 减 1,这个在 getTask 方法分析中说 - // 第二种情况,workCount 没有进行处理,所以需要在 processWorkerExit 中处理 - processWorkerExit(w, completedAbruptly); - } -} -``` -看看 `getTask() ` -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzgwXzQ2ODU5NjgtNWU5NDc3MzE5M2Q5Y2Y0OS5wbmc?x-oss-process=image/format,png) -```java -// 此方法有三种可能 -// 1. 阻塞直到获取到任务返回。默认 corePoolSize 之内的线程是不会被回收的,它们会一直等待任务 -// 2. 超时退出。keepAliveTime 起作用的时候,也就是如果这么多时间内都没有任务,那么应该执行关闭 -// 3. 如果发生了以下条件,须返回 null -// 池中有大于 maximumPoolSize 个 workers 存在(通过调用 setMaximumPoolSize 进行设置) -// 线程池处于 SHUTDOWN,而且 workQueue 是空的,前面说了,这种不再接受新的任务 -// 线程池处于 STOP,不仅不接受新的线程,连 workQueue 中的线程也不再执行 -private Runnable getTask() { - boolean timedOut = false; // Did the last poll() time out? - - for (;;) { - // 允许核心线程数内的线程回收,或当前线程数超过了核心线程数,那么有可能发生超时关闭 - - // 这里 break,是为了不往下执行后一个 if (compareAndDecrementWorkerCount(c)) - // 两个 if 一起看:如果当前线程数 wc > maximumPoolSize,或者超时,都返回 null - // 那这里的问题来了,wc > maximumPoolSize 的情况,为什么要返回 null? - // 换句话说,返回 null 意味着关闭线程。 - // 那是因为有可能开发者调用了 setMaximumPoolSize 将线程池的 maximumPoolSize 调小了 - - // 如果此 worker 发生了中断,采取的方案是重试 - // 解释下为什么会发生中断,这个读者要去看 setMaximumPoolSize 方法, - // 如果开发者将 maximumPoolSize 调小了,导致其小于当前的 workers 数量, - // 那么意味着超出的部分线程要被关闭。重新进入 for 循环,自然会有部分线程会返回 null - int c = ctl.get(); - int rs = runStateOf(c); - - // Check if queue empty only if necessary. - if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { - // CAS 操作,减少工作线程数 - decrementWorkerCount(); - return null; - } - - int wc = workerCountOf(c); - - // Are workers subject to culling? - boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; - - if ((wc > maximumPoolSize || (timed && timedOut)) - && (wc > 1 || workQueue.isEmpty())) { - if (compareAndDecrementWorkerCount(c)) - return null; - continue; - } - - try { - Runnable r = timed ? - workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : - workQueue.take(); - if (r != null) - return r; - timedOut = true; - } catch (InterruptedException retry) { - // 如果此 worker 发生了中断,采取的方案是重试 - // 解释下为什么会发生中断,这个读者要去看 setMaximumPoolSize 方法, - // 如果开发者将 maximumPoolSize 调小了,导致其小于当前的 workers 数量, - // 那么意味着超出的部分线程要被关闭。重新进入 for 循环,自然会有部分线程会返回 null - timedOut = false; - } - } -} -``` -到这里,基本上也说完了整个流程,回到 execute(Runnable command) 方法,看看各个分支,我把代码贴过来一下: -```java -/** - * Executes the given task sometime in the future. The task - * may execute in a new thread or in an existing pooled thread. - * - * If the task cannot be submitted for execution, either because this - * executor has been shutdown or because its capacity has been reached, - * the task is handled by the current {@code RejectedExecutionHandler}. - * - * @param command the task to execute - * @throws RejectedExecutionException at discretion of - * {@code RejectedExecutionHandler}, if the task - * cannot be accepted for execution - * @throws NullPointerException if {@code command} is null - */ - public void execute(Runnable command) { - if (command == null) - throw new NullPointerException(); - /* - * Proceed in 3 steps: - * - * 1. If fewer than corePoolSize threads are running, try to - * start a new thread with the given command as its first - * task. The call to addWorker atomically checks runState and - * workerCount, and so prevents false alarms that would add - * threads when it shouldn't, by returning false. - * - * 2. If a task can be successfully queued, then we still need - * to double-check whether we should have added a thread - * (because existing ones died since last checking) or that - * the pool shut down since entry into this method. So we - * recheck state and if necessary roll back the enqueuing if - * stopped, or start a new thread if there are none. - * - * 3. If we cannot queue task, then we try to add a new - * thread. If it fails, we know we are shut down or saturated - * and so reject the task. - */ - //表示 “线程池状态” 和 “线程数” 的整数 - int c = ctl.get(); - // 如果当前线程数少于核心线程数,直接添加一个 worker 执行任务, - // 创建一个新的线程,并把当前任务 command 作为这个线程的第一个任务(firstTask) - if (workerCountOf(c) < corePoolSize) { - // 添加任务成功,即结束 - // 执行的结果,会包装到 FutureTask - // 返回 false 代表线程池不允许提交任务 - if (addWorker(command, true)) - return; - - c = ctl.get(); - } - - // 到这说明,要么当前线程数大于等于核心线程数,要么刚刚 addWorker 失败 - - // 如果线程池处于 RUNNING ,把这个任务添加到任务队列 workQueue 中 - if (isRunning(c) && workQueue.offer(command)) { - /* 若任务进入 workQueue,我们是否需要开启新的线程 - * 线程数在 [0, corePoolSize) 是无条件开启新线程的 - * 若线程数已经大于等于 corePoolSize,则将任务添加到队列中,然后进到这里 - */ - int recheck = ctl.get(); - // 若线程池不处于 RUNNING ,则移除已经入队的这个任务,并且执行拒绝策略 - if (! isRunning(recheck) && remove(command)) - reject(command); - // 若线程池还是 RUNNING ,且线程数为 0,则开启新的线程 - // 这块代码的真正意图:担心任务提交到队列中了,但是线程都关闭了 - else if (workerCountOf(recheck) == 0) - addWorker(null, false); - } - // 若 workQueue 满,到该分支 - // 以 maximumPoolSize 为界创建新 worker, - // 若失败,说明当前线程数已经达到 maximumPoolSize,执行拒绝策略 - else if (!addWorker(command, false)) - reject(command); - } -``` -**工作线程**:线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会循环获取工作队列里的任务来执行.我们可以从Worker类的run()方法里看到这点 - -```java - public void run() { - try { - Runnable task = firstTask; - firstTask = null; - while (task != null || (task = getTask()) != null) { - runTask(task); - task = null; - } - } finally { - workerDone(this); - } - } - boolean workerStarted = false; - boolean workerAdded = false; - Worker w = null; - try { - w = new Worker(firstTask); - - final Thread t = w.thread; - if (t != null) { - //先加锁 - final ReentrantLock mainLock = this.mainLock; - mainLock.lock(); - try { - // Recheck while holding lock. - // Back out on ThreadFactory failure or if - // shut down before lock acquired. - int rs = runStateOf(ctl.get()); - - if (rs < SHUTDOWN || - (rs == SHUTDOWN && firstTask == null)) { - if (t.isAlive()) // precheck that t is startable - throw new IllegalThreadStateException(); - workers.add(w); - int s = workers.size(); - if (s > largestPoolSize) - largestPoolSize = s; - workerAdded = true; - } - } finally { - mainLock.unlock(); - } - if (workerAdded) { - t.start(); - workerStarted = true; - } - } - } finally { - if (! workerStarted) - addWorkerFailed(w); - } - return workerStarted; - } -``` -线程池中的线程执行任务分两种情况 - - 在execute()方法中创建一个线程时,会让这个线程执行当前任务 - - 这个线程执行完上图中 1 的任务后,会反复从BlockingQueue获取任务来执行 - -# 线程池的使用 - -## 向线程池提交任务 - 可以使用两个方法向线程池提交任务 -### execute() -用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功.通过以下代码可知execute()方法输入的任务是一个Runnable类的实例. -```java - threadsPool.execute(new Runnable() { - @Override - public void run() { - // TODO Auto-generated method stub - } - }); -``` -从运行结果可以看出,单线程池中的线程是顺序执行的。固定线程池(参数为2)中,永远最多只有两个线程并发执行。缓存线程池中,所有线程都并发执行。 -第二个例子,测试单线程调度线程池和固定调度线程池。 - -```java -public class ScheduledThreadPoolExam { - public static void main(String[] args) { - //first test for singleThreadScheduledPool - ScheduledExecutorService scheduledPool = Executors.newSingleThreadScheduledExecutor(); - //second test for scheduledThreadPool -// ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2); - for (int i = 0; i < 5; i++) { - scheduledPool.schedule(new TaskInScheduledPool(i), 0, TimeUnit.SECONDS); - } - scheduledPool.shutdown(); - } -} - -class TaskInScheduledPool implements Runnable { - private final int id; - - TaskInScheduledPool(int id) { - this.id = id; - } - - @Override - public void run() { - try { - for (int i = 0; i < 5; i++) { - System.out.println("TaskInScheduledPool-["+id+"] is running phase-"+i); - TimeUnit.SECONDS.sleep(1); - } - System.out.println("TaskInScheduledPool-["+id+"] is over"); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } -} -``` -从运行结果可以看出,单线程调度线程池和单线程池类似,而固定调度线程池和固定线程池类似。 -总结: - -- 如果没有特殊要求,使用缓存线程池总是合适的; -- 如果只能运行一个线程,就使用单线程池。 -- 如果要运行调度任务,则按需使用调度线程池或单线程调度线程池 -- 如果有其他特殊要求,则可以直接使用ThreadPoolExecutor类的构造函数来创建线程池,并自己给定那五个参数。 - -### submit() -用于提交需要返回值的任务.线程池会返回一个future类型对象,通过此对象可以判断任务是否执行成功 -并可通过get()获取返回值,get()会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候可能任务没有执行完. - -```java - Future future = executor.submit(harReturnValuetask); - try { - Object s = future.get(); - } catch (InterruptedException e) { - // 处理中断异常 - } catch (ExecutionException e) { - // 处理无法执行任务异常 - } finally { - // 关闭线程池 - executor.shutdown(); - } -``` -## 关闭线程池 -可通过调用线程池的**shutdown**或**shutdownNow**方法来关闭线程池. -它们的原理是遍历线程池中的工作线程,然后逐个调用线程的**interrupt**方法来中断线程,所以无法响应中断的任务可能永远无法终止. -但是它们存在一定的区别 - - - **shutdownNow**首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表 - - **shutdown**只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程. - -只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true. -当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true. -至于应该调用哪一种方法,应该由提交到线程池的任务的特性决定,通常调用shutdown方法来关闭线程池,若任务不一定要执行完,则可以调用shutdownNow方法. - -## 合理配置 - -要想合理地配置线程池,就必须首先 - -### 分析任务特性 - -可从以下几个角度来分析 - - 任务的性质:CPU密集型任务、IO密集型任务和混合型任务 - - 任务的优先级:高、中和低 - - 任务的执行时间:长、中和短 - - 任务的依赖性:是否依赖其他系统资源,如数据库连接。 - -### 任务性质 -可用不同规模的线程池分开处理 - -#### CPU密集型任务(计算型任务) -应配置尽可能小的线程,配置 - ` N(CPU)+1 `或 `N(CPU) * 2` - -#### I/O密集型任务 -相对比计算型任务,需多一些线程,根据具体 I/O 阻塞时长考量 - -> 如Tomcat中默认最大线程数: 200。 - -也可考虑根据需要在一个最小数量和最大数量间自动增减线程数。 - -业务读取较多,线程并不是一直在执行任务,则应配置尽可能多的线程 -`N(CPU)/1 - 阻塞系数(0.8~0.9)` - -一般,生产环境下,CPU使用率达到80,说明被充分利用 - -#### 混合型的任务 -如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量.如果这两个任务执行时间相差太大,则没必要进行分解. - -可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数. - -优先级不同的任务可以使用PriorityBlockingQueue处理.它可以让优先级高 -的任务先执行. - -> 注意 如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行 - -执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行. - -依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU. - -**建议使用有界队列** 有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千. -假如系统里后台任务线程池的队列和线程池全满了,不断抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞,任务积压在线程池里. -如果我们设置成无界队列,那么线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题. -## 2.5 线程池的监控 -如果在系统中大量使用线程池,则有必要对线程池进行监控,方便在出现问题时,可以根据线程池的使用状况快速定位问题.可通过线程池提供的参数进行监控,在监控线程池的时候可以使用以下属性: - - - taskCount:线程池需要执行的任务数量 - - completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。 - - largestPoolSize:线程池里曾经创建过的最大线程数量.通过这个数据可以知道线程池是否曾经满过.如该数值等于线程池的最大大小,则表示线程池曾经满过. - - getPoolSize:线程池的线程数量.如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减. - - getActiveCount:获取活动的线程数. - -通过扩展线程池进行监控.可以通过继承线程池来自定义线程池,重写线程池的 -beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控.例如,监控任务的平均执行时间、最大执行时间和最小执行时间等. -这几个方法在线程池里是空方法. -```java -protected void beforeExecute(Thread t, Runnable r) { } -``` -## 2.6 线程池的状态 -1.当线程池创建后,初始为 running 状态 -2.调用 shutdown 方法后,处 shutdown 状态,此时不再接受新的任务,等待已有的任务执行完毕 -3.调用 shutdownnow 方法后,进入 stop 状态,不再接受新的任务,并且会尝试终止正在执行的任务。 -4.当处于 shotdown 或 stop 状态,并且所有工作线程已经销毁,任务缓存队列已清空,线程池被设为 terminated 状态。 - -# 总结 -## java 线程池有哪些关键属性? -- corePoolSize 到 maximumPoolSize 之间的线程会被回收,当然 corePoolSize 的线程也可以通过设置而得到回收(allowCoreThreadTimeOut(true))。 -- workQueue 用于存放任务,添加任务的时候,如果当前线程数超过了 corePoolSize,那么往该队列中插入任务,线程池中的线程会负责到队列中拉取任务。 -- keepAliveTime 用于设置空闲时间,如果线程数超出了 corePoolSize,并且有些线程的空闲时间超过了这个值,会执行关闭这些线程的操作 -- rejectedExecutionHandler 用于处理当线程池不能执行此任务时的情况,默认有抛出 RejectedExecutionException 异常、忽略任务、使用提交任务的线程来执行此任务和将队列中等待最久的任务删除,然后提交此任务这四种策略,默认为抛出异常。 -##线程池中的线程创建时机? -- 如果当前线程数少于 corePoolSize,那么提交任务的时候创建一个新的线程,并由这个线程执行这个任务; -- 如果当前线程数已经达到 corePoolSize,那么将提交的任务添加到队列中,等待线程池中的线程去队列中取任务; -- 如果队列已满,那么创建新的线程来执行任务,需要保证池中的线程数不会超过 maximumPoolSize,如果此时线程数超过了 maximumPoolSize,那么执行拒绝策略。 - -## 任务执行过程中发生异常怎么处理? -如果某个任务执行出现异常,那么执行任务的线程会被关闭,而不是继续接收其他任务。然后会启动一个新的线程来代替它。 - -## 什么时候会执行拒绝策略? -- workers 的数量达到了 corePoolSize,任务入队成功,以此同时线程池被关闭了,而且关闭线程池并没有将这个任务出队,那么执行拒绝策略。这里说的是非常边界的问题,入队和关闭线程池并发执行,读者仔细看看 execute 方法是怎么进到第一个 reject(command) 里面的。 -- workers 的数量大于等于 corePoolSize,准备入队,可是队列满了,任务入队失败,那么准备开启新的线程,可是线程数已经达到 maximumPoolSize,那么执行拒绝策略。 - -# 参考 -- 《码出高效》 - -- 《Java并发编程的艺术》 \ No newline at end of file diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\347\272\277\347\250\213\351\200\232\344\277\241.md" "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\347\272\277\347\250\213\351\200\232\344\277\241.md" deleted file mode 100644 index 423ce8a991..0000000000 --- "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\347\272\277\347\250\213\351\200\232\344\277\241.md" +++ /dev/null @@ -1,68 +0,0 @@ -要想实现多个线程之间的协同,如:线程执行先后顺序、获取某个线程执行的结果等。 -涉及到线程之间相互通信,分为如下四类: -# 1 文件共享 -![](https://img-blog.csdnimg.cn/20191008023446691.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -# 2 网络共享 -socket编程 - -# 3 共享变量 -![](https://img-blog.csdnimg.cn/20191008023621435.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -# 4 线程协作(JDK API) -细分为: ~~suspend/resume~~ 、 wait/notify、 park/unpark - -JDK中对于需要多线程协作完成某一任务的场景,提供了对应API支持。 -多线程协作的典型场景是:生产者-消费者模型。(线程阻塞、 线程唤醒) - -## 示例 -- 线程-1去买包子,没有包子,则不再执行 -- 线程-2生产出包子,通知线程-1继续执行 -![](https://img-blog.csdnimg.cn/2019100802383347.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -## 4.1 suspend、resume(废弃) -- 调用suspend挂起目标线程 -- resume恢复线程执行 -![](https://img-blog.csdnimg.cn/2021071910523538.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -但该组合很容易写出 -### 死锁 -- 同步代码中使用 -![](https://img-blog.csdnimg.cn/2019100802572960.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20210719133903312.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -- 先后顺序:suspend比resume后执行 -![](https://img-blog.csdnimg.cn/2019100803021180.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20191008030515787.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -所以用如下机制替代 -## 4.2 wait/notify -这些方法只能由**同一对象锁**的持有者线程调用,也就是写在同步块里面,否则抛IllegalMonitorStateException。 - -**wait** 方法导致当前线程等待,加入该对象的等待集合中,并且**放弃当前持有的对象锁**。 - -**notify**/**notifyAll** 方法唤醒**一个**/**所有**正在等待这个对象锁的线程。 - -> 虽然wait会自动解锁,但**对顺序有要求**。若在notify被调用后, 才调用wait,则线程会永远处于**WAITING**态。 - -### 正常使用 -![](https://img-blog.csdnimg.cn/20191008031659529.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -### 死锁 -![](https://img-blog.csdnimg.cn/20191008032042659.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20191008032138968.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -## 4.3 park/unpark -LockSupport用来创建锁和其他同步类的基本线程阻塞原语: -- 线程调用`LockSupport.park`,则等待“许可” -- 线程调用`LockSupport.unpark`,必须把等待获得许可的线程作为参数进行传递,好让此线程继续运行,为指定线程提供“许可(permit)” - -**不要求park和unpark方法的调用顺序**。 - -多次调用unpark之后,再调用park,线程会直接运行,**不会叠加**,即连续多次调用park,第一次会拿到“许可”直接运行,后续调用会进入等待。 - -### 正常![](https://img-blog.csdnimg.cn/20191008033156471.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -### 死锁 -![](https://img-blog.csdnimg.cn/20191008033327469.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -# 5 伪唤醒 -**之前代码中用if语句来判断,是否进入等待状态,是错误的**。 - -官方推荐`应该在循环中检查等待条件`,因为处于等待状态的线程可能会收到**错误警报和伪唤醒**,如果不在循环中检查等待条件,程序就可能在没有满足结束条件的情况下退出。 - -伪唤醒是指线程并非因为notify、notifyall、 unpark等API调用而唤醒,而是更底层原因导致的。 -![](https://img-blog.csdnimg.cn/20191008034349825.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) \ No newline at end of file diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\351\253\230\346\200\247\350\203\275\347\274\226\347\250\213 - \345\217\257\351\207\215\345\205\245\350\257\273\345\206\231\351\224\201 ReentrantReadWriteLock.md" "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\351\253\230\346\200\247\350\203\275\347\274\226\347\250\213 - \345\217\257\351\207\215\345\205\245\350\257\273\345\206\231\351\224\201 ReentrantReadWriteLock.md" new file mode 100644 index 0000000000..1e25c9b409 --- /dev/null +++ "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\351\253\230\346\200\247\350\203\275\347\274\226\347\250\213 - \345\217\257\351\207\215\345\205\245\350\257\273\345\206\231\351\224\201 ReentrantReadWriteLock.md" @@ -0,0 +1,497 @@ +# 1 读写锁维护了一对相关的锁,一个用于只读操作,一个用于写入操作。 + +只要没有writer,读锁可以由多个reader线程同时保持。 +写锁是独占的。 + +- 互斥锁一次只允许一个线程访问共享数据,哪怕进行的是只读操作 +- 读写锁允许对共享数据进行更高级别的并发访问 + - 对于写操作,一次只有一个线程(write线程)可以修改共享数据 + - 对于读操作,允许任意数量的线程同时进行读取。 + +与互斥锁相比,使用读写锁能否提升性能则取决于读写操作期间`读取数据相对于修改数据的频率,以及数据的争用,即在同一时间试图对该数据执行读取或写入操作的线程数`。 + +读写锁适用于`读多写少`的情况。 + +# 2 可重入读写锁 ReentrantReadWriteLock + +## 2.1 属性 + +`ReentrantReadWriteLock` 也是基于 `AbstractQueuedSynchronizer `实现的,具有下面这些属性 + +- 获取顺序:此类不会将读/写者优先强加给锁访问的排序 + + - 非公平模式(默认) +连续竞争的非公平锁可能无限期地推迟一个或多个reader或writer线程,但吞吐量通常要高于公平锁 + - 公平模式 +线程利用一个近似到达顺序的策略来争夺进入。 +当释放当前保持的锁时,可以为等待时间最长的单个writer线程分配写锁,如果有一组等待时间大于所有正在等待的writer线程的reader,将为该组分配读锁。 +试图获得公平写入锁的非重入的线程将会阻塞,除非读取锁和写入锁都自由(这意味着没有等待线程)。 +- 重入:此锁允许reader和writer按照 ReentrantLock 的样式重新获取读/写锁。在写线程保持的所有写锁都已释放后,才允许重入reader使用读锁 +writer可以获取读取锁,但reader不能获取写入锁。 +- 锁降级:重入还允许从写锁降级为读锁,实现方式是:先获取写锁,然后获取读取锁,最后释放写锁。但是,`从读取锁升级到写入锁是不可能的`。 +- 锁获取的中断:读锁和写锁都支持锁获取期间的中断。 +- Condition 支持:写锁提供了一个 Condition 实现,对于写锁来说,该实现的行为与 `ReentrantLock.newCondition()` 提供的 Condition 实现对 ReentrantLock 所做的行为相同。当然,`此 Condition 只能用于写锁`。 +读锁不支持 Condition,readLock().newCondition() 会抛UnsupportedOperationException +- 监测:此类支持一些确定是读锁还是写锁的方法。这些方法设计用于监视系统状态,而不是同步控制。 + +# 3 ReentrantLock加锁和释放锁的底层原理 - AQS + +## 3.1 回顾 + +AQS对象内部以单个 int 类型的原子变量来表示其状态, 代表了加锁的状态。 +初始状态下,这个state的值是0 + +AQS内部还有一个关键变量,记录当前加锁的是哪个线程 +初始化状态下,这个变量是null![](https://img-blog.csdnimg.cn/20191018234504480.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +接着线程1跑过来调用ReentrantLock的*lock*()方法尝试进行加锁,这个加锁的过程,直接就是用CAS操作将state值从0变为1。 +如果之前没人加过锁,那么state的值肯定是0,此时线程1就可以加锁成功。 +一旦线程1加锁成功了之后,就可以设置当前加锁线程是自己。 + +- 线程1跑过来加锁的一个过程 +![](https://img-blog.csdnimg.cn/20191018235617253.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +说白了,就是并发包里的一个核心组件,里面有state变量、加锁线程变量等核心的东西,维护了加锁状态。 + +ReentrantLock 这种东西只是一个外层的API,内核中的锁机制实现其实都是依赖AQS组件 + +Reentrant打头,意思是一个可重入锁。 +可重入锁就是你可以对一个ReentrantLock对象多次执行lock()加锁和unlock()释放锁,也就是可以对一个锁加多次,叫做可重入加锁。 +看明白了那个state变量之后,就知道了如何进行可重入加锁! + + +其实每次线程1可重入加锁一次,会判断一下当前加锁线程就是自己,那么他自己就可以可重入多次加锁,每次加锁就是把state的值给累加1,别的没啥变化。 + +接着,如果线程1加锁了之后,线程2跑过来加锁会怎么样呢? + +## 3.2 我们来看看锁的互斥是如何实现的 +线程2跑过来一下看到,哎呀!state的值不是0啊?所以CAS操作将state从0变为1的过程会失败,因为state的值当前为1,说明已经有人加锁了! + +接着线程2会看一下,是不是自己之前加的锁啊?当然不是了,“加锁线程”这个变量明确记录了是线程1占用了这个锁,所以线程2此时就是加锁失败。 + +- 一起来感受一下线程2的绝望心路 +![](https://img-blog.csdnimg.cn/20191019000850859.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +接着,线程2会将自己放入AQS中的一个等待队列,因为自己尝试加锁失败了,此时就要将自己放入队列中来等待,等待线程1释放锁之后,自己就可以重新尝试加锁了 + +所以大家可以看到,AQS是如此的核心!AQS内部还有一个等待队列,专门放那些加锁失败的线程! + +- 同样,给大家来一张图,一起感受一下: +![](https://img-blog.csdnimg.cn/20191019001308340.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +接着,线程1在执行完自己的业务逻辑代码之后,就会释放锁! +他释放锁的过程非常的简单,就是将AQS内的state变量的值递减1,如果state值为0,则彻底释放锁,会将“加锁线程”变量也设置为null! +- 附图 +![](https://img-blog.csdnimg.cn/20191019001520803.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +接下来,会从等待队列的队头唤醒线程2重新尝试加锁。 +好!线程2现在就重新尝试加锁,这时还是用CAS操作将state从0变为1,此时就会成功,成功之后代表加锁成功,就会将state设置为1。 +此外,还要把“加锁线程”设置为线程2自己,同时线程2自己就从等待队列中出队了。 +- 最后再来一张图,大家来看看这个过程。 +![](https://img-blog.csdnimg.cn/20191019001626741.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + + +## 3.3 个抽象方法 + +- tryAcquire(int) +- tryRelease(int) +- tryAcquireShared(int) +- tryReleaseShared(int) + + +前两个方法用于独占/排他模式,后两个用于共享模式 ,留给子类实现,自定义同步器的行为以实现特定的功能。 + +- ReentrantLock,它是可重入的独占锁,内部的 Sync 类实现了 tryAcquire(int)、tryRelease(int) 方法,并用状态的值来表示重入次数,加锁或重入锁时状态加 1,释放锁时状态减 1,状态值等于 0 表示锁空闲。 + +- CountDownLatch,它是一个关卡,在条件满足前阻塞所有等待线程,条件满足后允许所有线程通过。内部类 Sync 把状态初始化为大于 0 的某个值,当状态大于 0 时所有wait线程阻塞,每调用一次 countDown 方法就把状态值减 1,减为 0 时允许所有线程通过。利用了AQS的共享模式。 + +现在,要用AQS来实现 ReentrantReadWriteLock。 + +# 4 AQS只有一个状态,那么如何表示 多个读锁 与 单个写锁 +ReentrantLock 里,状态值表示重入计数 +- 现在如何在AQS里表示每个读锁、写锁的重入次数呢 +- 如何实现读锁、写锁的公平性呢 + +一个状态是没法既表示读锁,又表示写锁的,显然不够用啊,那就辦成两份用了! +**状态的高位部分表示读锁,低位表示写锁** +由于写锁只有一个,所以写锁的重入计数也解决了,这也会导致写锁可重入的次数减小。 + +由于读锁可以同时有多个,肯定不能再用辦成两份用的方法来处理了 +但我们有 `ThreadLocal`,可以把线程重入读锁的次数作为值存在 `ThreadLocal ` + +对于公平性的实现,可以通过AQS的等待队列和它的抽象方法来控制 +在状态值的另一半里存储当前持有读锁的线程数。 +- 如果读线程申请读锁,当前写锁重入次数不为 0 时,则等待,否则可以马上分配 +- 如果是写线程申请写锁,当前状态为 0 则可以马上分配,否则等待。 + +# 5 源码分析 +AQS 的状态是32位(int 类型)的 +## 5.1 一分为二 +读锁用高16位,表示持有读锁的线程数(sharedCount),写锁低16位,表示写锁的重入次数 (exclusiveCount)。状态值为 0 表示锁空闲,sharedCount不为 0 表示分配了读锁,exclusiveCount 不为 0 表示分配了写锁,sharedCount和exclusiveCount 肯定不会同时不为 0。 +![](https://img-blog.csdnimg.cn/20191019003604724.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +## 读锁重入计数 +``` + /** + * A counter for per-thread read hold counts. + * Maintained as a ThreadLocal; cached in cachedHoldCounter + * 每个线程特定的 read 持有计数。存放在ThreadLocal,缓存在cachedHoldCounter + * 不需要是线程安全的。 + */ + static final class HoldCounter { + int count = 0; + + // 使用id而不是引用是为了避免保留垃圾。注意这是个常量。 + // Use id, not reference, to avoid garbage retention + final long tid = Thread.currentThread().getId(); + } + + /** + * ThreadLocal subclass. Easiest to explicitly define for sake + * of deserialization mechanics. + * 采用继承是为了重写 initialValue 方法,这样就不用进行这样的处理: + * 如果ThreadLocal没有当前线程的计数,则new一个,再放进ThreadLocal里。 + * 可以直接调用 get + */ + static final class ThreadLocalHoldCounter + extends ThreadLocal { + public HoldCounter initialValue() { + return new HoldCounter(); + } + } + + /** + * The number of reentrant read locks held by current thread. + * Initialized only in constructor and readObject. + * Removed whenever a thread's read hold count drops to 0. + * 保存当前线程重入读锁的次数的容器。在读锁重入次数为 0 时移除 + */ + private transient ThreadLocalHoldCounter readHolds; + + /** + * The hold count of the last thread to successfully acquire + * readLock. This saves ThreadLocal lookup in the common case + * where the next thread to release is the last one to + * acquire. This is non-volatile since it is just used + * as a heuristic, and would be great for threads to cache. + * + *

Can outlive the Thread for which it is caching the read + * hold count, but avoids garbage retention by not retaining a + * reference to the Thread. + * + *

Accessed via a benign data race; relies on the memory + * model's final field and out-of-thin-air guarantees. + */ + /** + * 最近一个成功获取读锁的线程的计数。这省却了ThreadLocal查找, + * 通常情况下,下一个要释放的线程是最后一个获取的线程。 + * 这不是 volatile 的,因为它仅用于试探的,线程进行缓存也是极好的 + * (因为判断是否是当前线程是通过线程id来比较的)。 + */ + private transient HoldCounter cachedHoldCounter; + + /** + * firstReader is the first thread to have acquired the read lock. + * firstReaderHoldCount is firstReader's hold count. + * + *

More precisely, firstReader is the unique thread that last + * changed the shared count from 0 to 1, and has not released the + * read lock since then; null if there is no such thread. + * + *

Cannot cause garbage retention unless the thread terminated + * without relinquishing its read locks, since tryReleaseShared + * sets it to null. + * + *

Accessed via a benign data race; relies on the memory + * model's out-of-thin-air guarantees for references. + * + *

This allows tracking of read holds for uncontended read + * locks to be very cheap. + */ + /** + * firstReader是第一个获取读锁的线程,更加确切地说是最后一个把 共享计数 从 0 改为 1 的(在锁空闲的时候),而且在那之后还没有释放读锁的独特的线程!如果不存在这样的线程则为null + * firstReaderHoldCount 是 firstReader 的重入计数。 + * + * firstReader 不会导致垃圾存留,因此在 tryReleaseShared 里设置为null, + * 除非线程异常终止,没有释放读锁 + * + * 这使得在跟踪无竞争的读锁计数时代价非常低 + * + * firstReader及其计数firstReaderHoldCount是不会放入 readHolds 的 + */ + private transient Thread firstReader = null; + private transient int firstReaderHoldCount; + + Sync() { + readHolds = new ThreadLocalHoldCounter(); + // ensures visibility of readHolds + setState(getState()); // 确保 readHolds 的内存可见性,利用 volatile 写的内存语义 + } +} +``` +## 写锁的获取与释放 +通过 tryAcquire 和 tryRelease 实现,源码里有这么一段说明 +![](https://img-blog.csdnimg.cn/20191019003704206.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +tryRelease 和 tryAcquire 可能被 Conditions 调用。 +因此可能出现参数里包含在条件等待和用 tryAcquire 重新获取到锁的期间内已经释放的 读和写 计数 + +这说明看起来像是在 tryAcquire 里设置状态时要考虑方法参数(acquires)的高位部分,其实是不需要的。由于写锁是独占的,acquires 表示的只能是写锁的计数,如果当前线程成功获取写锁,只需要简单地把当前状态加上 acquires 的值即可,tryRelease 里直接减去其参数值即可。 +``` +protected final boolean tryAcquire(int acquires) { + /* + * Walkthrough: + * 1. If read count nonzero or write count nonzero + * and owner is a different thread, fail. + * 2. If count would saturate, fail. (This can only + * happen if count is already nonzero.) + * 3. Otherwise, this thread is eligible for lock if + * it is either a reentrant acquire or + * queue policy allows it. If so, update state + * and set owner. + */ + Thread current = Thread.currentThread(); + int c = getState(); + int w = exclusiveCount(c); + if (c != 0) { // 状态不为0,表示锁被分配出去了 + // (Note: if c != 0 and w == 0 then shared count != 0) + // c != 0 且 w == 0 : 分配了读锁 + // w != 0 && current != getExclusiveOwnerThread() 表示其他线程获取了写锁。 + if (w == 0 || current != getExclusiveOwnerThread()) + return false ; + // 写锁重入 + // 检测是否超过最大重入次数。 + if (w + exclusiveCount(acquires) > MAX_COUNT) + throw new Error("Maximum lock count exceeded"); + + // 更新写锁重入次数,写锁在低位,直接加上 acquire 即可。 + // Reentrant acquire + setState(c + acquires); + return true ; + } + // writerShouldBlock 留给子类实现,用于实现公平性策略 + // 如果允许获取写锁,则用 CAS 更新状态 + if (writerShouldBlock() || + !compareAndSetState(c, c + acquires)) + return false ; // 不允许获取锁 或 CAS 失败。 + // 获取写锁,设置独占线程 + setExclusiveOwnerThread(current); + return true; +} + +protected final boolean tryRelease(int releases) { + if (!isHeldExclusively()) // 是否是当前线程持有写锁 + throw new IllegalMonitorStateException(); + // 这里不考虑高16位是因为高16位肯定是 0 + int nextc = getState() - releases; + boolean free = exclusiveCount(nextc) == 0; + if (free) + setExclusiveOwnerThread( null); // 写锁完全释放,设置独占线程为null + setState(nextc); + return free; +} +``` + +## 读锁获取与释放 +``` +// 参数变为 unused 是因为读锁的重入计数是内部维护的 +protected final int tryAcquireShared(int unused) { +/* + * Walkthrough: + * 1. If write lock held by another thread, fail. + * 2. Otherwise, this thread is eligible for + * lock wrt state, so ask if it should block + * because of queue policy. If not, try + * to grant by CASing state and updating count. + * Note that step does not check for reentrant + * acquires, which is postponed to full version + * to avoid having to check hold count in + * the more typical non-reentrant case. + * 3. If step 2 fails either because thread + * apparently not eligible or CAS fails or count + * saturated, chain to version with full retry loop. + */ + Thread current = Thread.currentThread(); + int c = getState(); + // 持有写锁的线程可以获取读锁 + if (exclusiveCount(c) != 0 && // 已分配了写锁 + getExclusiveOwnerThread() != current) // 且当前线程不是持有写锁的线程 + return -1; + int r = sharedCount(c); // 获取读锁的计数 + if (!readerShouldBlock() && // 由子类根据其公平策略决定是否允许获取读锁 + r < MAX_COUNT && // 读锁数量尚未达到最大值 + // 尝试获取读锁,注意读线程计数的单位是 2^16 + compareAndSetState(c, c + SHARED_UNIT)) { + // 成功获取读锁 + + // 注意下面对firstReader的处理:firstReader是不会放到readHolds里的 + // 这样,在读锁只有一个的情况下,就避免了查找readHolds + if (r == 0) { // 是 firstReader,计数不会放入 readHolds + firstReader = current; + firstReaderHoldCount = 1; + } else if (firstReader == current) { // firstReader 重入 + firstReaderHoldCount++; + } else { + // 非 firstReader 读锁重入计数更新 + HoldCounter rh = cachedHoldCounter; // 首先访问缓存 + if (rh == null || rh.tid != current.getId()) + cachedHoldCounter = rh = readHolds.get(); + else if (rh.count == 0) + readHolds.set(rh); + rh.count++; + } + return 1; + } + // 获取读锁失败,放到循环里重试。 + return fullTryAcquireShared(current); +} +/** + * Full version of acquire for reads, that handles CAS misses + * and reentrant reads not dealt with in tryAcquireShared. + */ +final int fullTryAcquireShared(Thread current) { + /* + * This code is in part redundant with that in + * tryAcquireShared but is simpler overall by not + * complicating tryAcquireShared with interactions between + * retries and lazily reading hold counts. + */ + HoldCounter rh = null; + for (;;) { + int c = getState(); + if (exclusiveCount(c) != 0) { + if (getExclusiveOwnerThread() != current) + // 写锁被分配,非写锁线程获取读锁失败 + return -1; + // else we hold the exclusive lock; blocking here + // would cause deadlock. + // 否则,当前线程持有写锁,在这里阻塞将导致死锁 + } else if (readerShouldBlock()) { + // Make sure we're not acquiring read lock reentrantly + // 写锁空闲 且 公平策略决定 线程应当被阻塞 + // 如果是已获取读锁的线程重入读锁时, + // 即使公平策略指示应当阻塞也不会阻塞。 + // 否则也会导致死锁 + if (firstReader == current) { + // assert firstReaderHoldCount > 0; + } else { + if (rh == null) { + rh = cachedHoldCounter; + if (rh == null || rh.tid != current.getId()) { + rh = readHolds.get(); + if (rh.count == 0) + readHolds.remove(); + } + } + // 需要阻塞且是非重入(还未获取读锁的),获取失败。 + if (rh.count == 0) + return -1; + } + } + + // 写锁空闲 且 公平策略决定线程可以获取读锁 + if (sharedCount(c) == MAX_COUNT) + throw new Error( "Maximum lock count exceeded"); + if (compareAndSetState(c, c + SHARED_UNIT)) { + // 申请读锁成功,下面的处理跟tryAcquireShared类似 + if (sharedCount(c) == 0) { + firstReader = current; + firstReaderHoldCount = 1; + } else if (firstReader == current) { + firstReaderHoldCount++; + } else { + // 设定最后一次获取读锁的缓存 + if (rh == null) + rh = cachedHoldCounter; + + if (rh == null || rh.tid != current.getId()) + rh = readHolds.get(); + else if (rh.count == 0) + readHolds.set(rh); + rh.count++; + + cachedHoldCounter = rh; // 缓存起来用于释放 cache for release + } + return 1; + } + } +} + +protected final boolean tryReleaseShared(int unused) { + Thread current = Thread.currentThread(); + // 清理firstReader缓存 或 readHolds里的重入计数 + if (firstReader == current) { + // assert firstReaderHoldCount > 0; + if (firstReaderHoldCount == 1) + firstReader = null; + else + firstReaderHoldCount--; + } else { + HoldCounter rh = cachedHoldCounter; + if (rh == null || rh.tid != current.getId()) + rh = readHolds.get(); + int count = rh.count; + if (count <= 1) { + // 完全释放读锁 + readHolds.remove(); + if (count <= 0) + throw unmatchedUnlockException(); + } + --rh.count; // 主要用于重入退出 + } + + // 循环在CAS更新状态值,主要是把读锁数量减 1 + for (;;) { + int c = getState(); + int nextc = c - SHARED_UNIT; + if (compareAndSetState(c, nextc)) + // 释放读锁对其他读线程没有任何影响, + // 但可以允许等待的写线程继续,如果读锁、写锁都空闲。 + return nextc == 0; + } +} +``` +## 公平性策略 +策略由 Sync 的子类 FairSync 和 NonfairSync 实现 +``` +/** + * 这个非公平策略的同步器是写锁优先的,申请写锁时总是不阻塞。 + */ +static final class NonfairSync extends Sync { + private static final long serialVersionUID = -8159625535654395037L; + final boolean writerShouldBlock() { + return false; // 写线程总是可以突入 + } + final boolean readerShouldBlock() { + /* 作为一个启发用于避免写线程饥饿,如果线程临时出现在等待队列的头部则阻塞, + * 如果存在这样的,则是写线程。 + */ + return apparentlyFirstQueuedIsExclusive(); + } +} + +/** + * 公平的 Sync,它的策略是:如果线程准备获取锁时, + * 同步队列里有等待线程,则阻塞获取锁,不管是否是重入 + * 这也就需要tryAcqire、tryAcquireShared方法进行处理。 + */ +static final class FairSync extends Sync { + private static final long serialVersionUID = -2274990926593161451L; + final boolean writerShouldBlock() { + return hasQueuedPredecessors(); + } + final boolean readerShouldBlock() { + return hasQueuedPredecessors(); + } +} +``` +现在用奇数表示申请读锁的读线程,偶数表示申请写锁的写线程,每个数都表示一个不同的线程,存在下面这样的申请队列,假设开始时锁空闲: + +1 3 5 0 7 9 2 4 +读线程1申请读锁时,锁是空闲的,马上分配,读线程3、5申请时,由于已分配读锁,它们也可以马上获取读锁。 +假设此时有线程11申请读锁,由于它不是读锁重入,只能等待。而线程1再次申请读锁是可以的,因为它的重入。 +写线程0申请写锁时,由于分配了读锁,只能等待,当读线程1、3、5都释放读锁后,线程0可以获取写锁。 +线程0释放后,线程7、9获取读锁,它们释放后,线程2获取写锁,此时线程4必须等待线程2释放。 +线程4在线程2释放写锁后获取写锁,它释放写锁后,锁恢复空闲 + + + +# 参考 +https://blog.csdn.net/qq_42046105/article/details/102384342 \ No newline at end of file diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\351\253\230\346\200\247\350\203\275\347\274\226\347\250\213\345\256\236\346\210\230 - \347\272\277\347\250\213\351\200\232\344\277\241.md" "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\351\253\230\346\200\247\350\203\275\347\274\226\347\250\213\345\256\236\346\210\230 - \347\272\277\347\250\213\351\200\232\344\277\241.md" new file mode 100644 index 0000000000..8aa6e4f3ea --- /dev/null +++ "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\351\253\230\346\200\247\350\203\275\347\274\226\347\250\213\345\256\236\346\210\230 - \347\272\277\347\250\213\351\200\232\344\277\241.md" @@ -0,0 +1,76 @@ +要想实现多个线程之间的协同,如:线程执行先后顺序、获取某个线程执行的结果等等。 +涉及到线程之间相互通信,分为下面四类: + +# 1 文件共享 +![](https://img-blog.csdnimg.cn/20191008023446691.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +# 2 网络共享 +socket编程问题,非本文重点,不再赘述 + +# 3 共享变量 +![](https://img-blog.csdnimg.cn/20191008023621435.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +# 4 线程协作 - JDK API +细分为: ~~suspend/resume~~ 、 wait/notify、 park/unpark + +JDK中对于需要多线程协作完成某一任务的场景,提供了对应API支持。 +多线程协作的典型场景是:生产者-消费者模型。(线程阻塞、 线程唤醒) + +示例:线程1去买包子,没有包子,则不再执行。线程-2生产出包子,通知线程-1继续执行。 +![](https://img-blog.csdnimg.cn/2019100802383347.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +## 4.1 API - 被弃用的suspend和resume +作用:调用suspend挂起目标线程,通过resume可以恢复线程执行 +![](https://img-blog.csdnimg.cn/20191008025019964.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +被弃用的主要原因是,容易写出 +### 死锁代码 +- 同步代码中使用 +![](https://img-blog.csdnimg.cn/2019100802572960.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20191008030038265.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +- 先后顺序:suspend比resume后执行 +![](https://img-blog.csdnimg.cn/2019100803021180.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +![](https://img-blog.csdnimg.cn/20191008030515787.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +所以用wait/notify和park/unpark机制对它进行替代 + +## 4.2 wait/notify +这些方法只能由**同一对象锁**的持有者线程调用,也就是写在同步块里面,否则会抛IllegalMonitorStateException + +**wait** 方法导致当前线程等待,加入该对象的等待集合中,并且**放弃当前持有的对象锁** + +**notify**/**notifyAll** 方法唤醒一个 或所有正在等待这个对象锁的线程。 + +> 虽然wait会自动解锁,但是**对顺序有要求**,如果在notify被调用之后, 才开始wait方法的调用,线程会永远处于**WAITING**状态。 + +- 正常使用 +![](https://img-blog.csdnimg.cn/20191008031659529.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +- 死锁 +![](https://img-blog.csdnimg.cn/20191008032042659.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20191008032138968.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +- 小结 +![](https://img-blog.csdnimg.cn/20191008031954289.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +## 4.3 park/unpark +线程调用park则等待“许可”,unpark方法为指定线程提供“许可(permit)” 。 + +**不要求park和unpark方法的调用顺序** + +多次调用unpark之后,再调用park, 线程会直接运行。 +**但不会叠加**,即连续多次调用park方法,第一次会拿到“许可”直接运行,后续调 +用会进入等待。 + +- 正常![](https://img-blog.csdnimg.cn/20191008033156471.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +- 死锁 +![](https://img-blog.csdnimg.cn/20191008033327469.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +# 5 伪唤醒 +** 之前代码中用if语句来判断,是否进入等待状态,是错误的! ** + +官方建议`应该在循环中检查等待条件`,原因是处于等待状态的线程可能会收到**错误警报和伪 +唤醒**,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。 + +伪唤醒是指线程并非因为notify、notifyall、 unpark等 api调用而唤醒,是更底层原因导致的。 +![](https://img-blog.csdnimg.cn/20191008034349825.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +# 6 总结 + 涉及很多JDK多线程开发工具类及其底层实现的原理 \ No newline at end of file diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/\345\217\257\351\207\215\345\205\245\350\257\273\345\206\231\351\224\201ReentrantReadWriteLock.md" "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/\345\217\257\351\207\215\345\205\245\350\257\273\345\206\231\351\224\201ReentrantReadWriteLock.md" deleted file mode 100644 index 15bf8bc6e0..0000000000 --- "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/\345\217\257\351\207\215\345\205\245\350\257\273\345\206\231\351\224\201ReentrantReadWriteLock.md" +++ /dev/null @@ -1,87 +0,0 @@ -# 1 读写锁,一个用于只读,一个用于写入 -只要没有writer,读锁可以由多个reader线程同时保持。 -写锁是独占的。 - -- 互斥锁一次只允许一个线程访问共享数据,哪怕是只读 -- 读写锁允许对共享数据进行更高性能的并发访问 - - 对于写操作,一次只有一个线程(write线程)可修改共享数据 - - 对于读操作,允许任意数量的线程同时读取 - -与互斥锁相比,使用读写锁能否提升性能则取决于读写操作期间`读取数据相对于修改数据的频率,以及数据的争用,即在同一时间试图对该数据执行读取或写入操作的线程数`。 - -读写锁适用于`读多写少`的场景。 - -# 2 可重入读写锁 ReentrantReadWriteLock -可重入锁,又名递归锁。 -## 2.1 属性 -`ReentrantReadWriteLock` 基于 `AbstractQueuedSynchronizer `实现,具有如下属性 - -### 获取顺序 -此类不会将读/写者优先强加给锁访问的排序。 - -- 非公平模式(默认) -连续竞争的非公平锁可能无限期推迟一或多个读或写线程,但吞吐量通常要高于公平锁。 - -- 公平模式 -线程利用一个近似到达顺序的策略来竞争进入。 -当释放当前持有的锁时,可以为等待时间最长的单个writer线程分配写锁,如果有一组等待时间大于所有正在等待的writer线程的reader,将为该组分配读锁。 -试图获得公平写入锁的非重入的线程将会阻塞,除非读取锁和写入锁都自由(这意味着没有等待线程)。 -### 重入 -此锁允许reader和writer按照 ReentrantLock 的样式重新获取读/写锁。在写线程保持的所有写锁都已释放后,才允许重入reader使用读锁 -writer可以获取读取锁,但reader不能获取写入锁。 -### 锁降级 -重入还允许从写锁降级为读锁,实现方式是:先获取写锁,然后获取读取锁,最后释放写锁。但是,`从读取锁升级到写入锁是不可能的`。 -### 锁获取的中断 -读锁和写锁都支持锁获取期间的中断。 -### Condition 支持 -写锁提供了一个 Condition 实现,对于写锁来说,该实现的行为与 `ReentrantLock.newCondition()` 提供的 Condition 实现对 ReentrantLock 所做的行为相同。当然,`此 Condition 只能用于写锁`。 -读锁不支持 Condition,readLock().newCondition() 会抛UnsupportedOperationException -### 监测 -此类支持一些确定是读锁还是写锁的方法。这些方法设计用于监视系统状态,而不是同步控制。 - -# 3 AQS -- 记录当前加锁的是哪个线程,初始化状态下,这个变量是null![](https://img-blog.csdnimg.cn/20191018234504480.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -接着线程1跑过来调用ReentrantLock#lock()尝试加锁:直接用CAS操作将state值从0变为1。 -如果之前没人加过锁,那么state的值肯定是0,此时线程1就可以加锁成功。 -一旦线程1加锁成功了之后,就可以设置当前加锁线程是自己。 - -- 线程1跑过来加锁的一个过程 -![](https://img-blog.csdnimg.cn/20191018235617253.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -加锁线程变量 - -其实每次线程1可重入加锁一次,会判断一下当前加锁线程就是自己,那么他自己就可以可重入多次加锁,每次加锁就是把state的值给累加1,别的没啥变化。 - -接着,如果线程1加锁了之后,线程2跑过来加锁会怎么样呢? - -## 锁的互斥是如何实现的? -线程2跑过来一下看到 state≠0,所以CAS将state 01=》失败,因为state的值当前为1,已有人加锁了! - -接着线程2会看一下,是不是自己之前加的锁啊?当然不是了,“加锁线程”这个变量明确记录了是线程1占用了这个锁,所以线程2此时就是加锁失败。 -![](https://img-blog.csdnimg.cn/20191019000850859.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -接着,线程2会将自己放入AQS中的一个等待队列,因为自己尝试加锁失败了,此时就要将自己放入队列中来等待,等待线程1释放锁之后,自己就可以重新尝试加锁了。 -![](https://img-blog.csdnimg.cn/20191019001308340.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -接着,线程1在执行完自己的业务逻辑代码之后,就会释放锁:将AQS内的state变量的值减1,若state值为0,则彻底释放锁,会将“加锁线程”变量也设置为null。 -![](https://img-blog.csdnimg.cn/20191019001520803.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -接下来,会从等待队列的队头唤醒线程2重新尝试加锁。线程2现在就重新尝试加锁:CAS将state从0变为1,此时就会成功,成功之后代表加锁成功,就会将state设置为1。还要把“加锁线程”设置为线程2自己,同时线程2自己就从等待队列中出队了。 -![](https://img-blog.csdnimg.cn/20191019001626741.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -- ReentrantLock,它是可重入的独占锁,内部的 Sync 类实现了 tryAcquire(int)、tryRelease(int) 方法,并用状态的值来表示重入次数,加锁或重入锁时状态加 1,释放锁时状态减 1,状态值等于 0 表示锁空闲。 - -- CountDownLatch,它是一个关卡,在条件满足前阻塞所有等待线程,条件满足后允许所有线程通过。内部类 Sync 把状态初始化为大于 0 的某个值,当状态大于 0 时所有wait线程阻塞,每调用一次 countDown 方法就把状态值减 1,减为 0 时允许所有线程通过。利用了AQS的**共享模式**。 - -# 4 AQS只有一个状态,那么如何表示多个读锁与单个写锁 -ReentrantLock 里,状态值表示重入计数 -- 现在如何在AQS里表示每个读锁、写锁的重入次数呢 -- 如何实现读锁、写锁的公平性呢 - -一个状态是没法既表示读锁,又表示写锁的,那就辦成两份用了! -**状态的高位部分表示读锁,低位表示写锁**。由于写锁只有一个,所以写锁的重入计数也解决了,这也会导致写锁可重入的次数减小。 - -由于读锁可以同时有多个,肯定不能再用辦成两份用的方法来处理了。但我们有 `ThreadLocal`,可以把线程重入读锁的次数作为值存在 `ThreadLocal `。 - -对于公平性的实现,可以通过AQS的等待队列和它的抽象方法来控制。在状态值的另一半里存储当前持有读锁的线程数。 -- 如果读线程申请读锁,当前写锁重入次数不为 0 时,则等待,否则可以马上分配 -- 如果是写线程申请写锁,当前状态为 0 则可以马上分配,否则等待。 \ No newline at end of file diff --git "a/JavaScript/VueJS/RESTful\346\236\266\346\236\204.md" "b/JavaScript/VueJS/RESTful\346\236\266\346\236\204.md" new file mode 100644 index 0000000000..84fa273c99 --- /dev/null +++ "b/JavaScript/VueJS/RESTful\346\236\266\346\236\204.md" @@ -0,0 +1,82 @@ +![image.png](http://upload-images.jianshu.io/upload_images/4685968-39f29a1ad477cdb7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![image.png](http://upload-images.jianshu.io/upload_images/4685968-6fdd998f69fb84be.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![zzz](http://upload-images.jianshu.io/upload_images/4685968-daab1cabc5a51e35.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![image.png](http://upload-images.jianshu.io/upload_images/4685968-ea1df6e21fd0aa44.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![image.png](http://upload-images.jianshu.io/upload_images/4685968-146fc2ec7c746710.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](http://upload-images.jianshu.io/upload_images/4685968-bf61836dd3ce965f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](http://upload-images.jianshu.io/upload_images/4685968-ab6dfa795b8589b6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +0\. REST不是"rest"这个单词,而是几个单词缩写。 +1\. REST描述的是在网络中client和server的一种交互形式;REST本身不实用,实用的是如何设计 RESTful API(REST风格的网络接口); +2\. Server提供的RESTful API中,URL中只使用名词来指定资源,原则上不使用动词。“资源”是REST架构或者说整个网络处理的核心。比如: +[http://api.qc.com/v1/newsfeed](http://api.qc.com/v1/newsfeed): 获取某人的新鲜; +[http://api.qc.com/v1/friends](http://api.qc.com/v1/friends): 获取某人的好友列表; +[http://api.qc.com/v1/profile](http://api.qc.com/v1/profile): 获取某人的详细信息;3\. 用HTTP协议里的动词来实现资源的添加,修改,删除等操作。即通过HTTP动词来实现资源的状态扭转: +GET 用来获取资源, +POST 用来新建资源(也可以用于更新资源), +PUT 用来更新资源, +DELETE 用来删除资源。比如: +DELETE [http://api.qc.com/v1/](http://api.qc.com/v1/friends)friends: 删除某人的好友 (在http parameter指定好友id) +POST [http://api.qc.com/v1/](http://api.qc.com/v1/friends)friends: 添加好友 +UPDATE [http://api.qc.com/v1/profile](http://api.qc.com/v1/profile): 更新个人资料 + +禁止使用: GET [http://api.qc.com/v1/deleteFriend](http://api.qc.com/v1/deleteFriend) 图例: +4\. Server和Client之间传递某资源的一个表现形式,比如用JSON,XML传输文本,或者用JPG,WebP传输图片等。当然还可以压缩HTTP传输时的数据(on-wire data compression)。 +5\. 用 HTTP Status Code传递Server的状态信息。比如最常用的 200 表示成功,500 表示Server内部错误等。 + +主要信息就这么点。最后是要解放思想,Web端不再用之前典型的PHP或JSP架构,而是改为前段渲染和附带处理简单的商务逻辑(比如AngularJS或者BackBone的一些样例)。Web端和Server只使用上述定义的API来传递数据和改变数据状态。格式一般是JSON。iOS和Android同理可得。由此可见,Web,iOS,Android和第三方开发者变为平等的角色通过一套API来共同消费Server提供的服务。 + +#**REST名称** +REST -- REpresentational State Transfer +全称 Resource Representational State Transfer:资源在网络中以某种表现形式进行状态转移 +- Resource:资源,即数据(前面说过网络的核心)。比如 newsfeed,friends +- Representational:某种表现形式,比如用JSON,XML,JPEG +- State Transfer:状态变化。通过HTTP动词实现 +#**REST的出处** +Roy Fielding的毕业论文。参与设计HTTP协议,也是Apache Web Server项目(可惜现在已经是 nginx 的天下)的co-founder。PhD的毕业学校是 UC Irvine,Irvine在加州,有着充裕的阳光和美丽的海滩,是著名的富人区。Oculus VR 的总部就坐落于此(虚拟现实眼镜,被FB收购,CTO为Quake和Doom的作者 John Carmack) +论文地址:[Architectural Styles and the Design of Network-based Software Architectures](http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm) +REST章节:[Fielding Dissertation: CHAPTER 5: Representational State Transfer (REST)](http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) +#**RESTful API** +**为什么要用RESTful结构呢?** +大家都知道"古代"网页是前端后端融在一起的,比如之前的PHP,JSP等。在之前的桌面时代问题不大,但是近年来移动互联网的发展,各种类型的Client层出不穷,RESTful可以通过一套统一的接口为 Web,iOS和Android提供服务。另外对于广大平台来说,比如Facebook platform,微博开放平台,微信公共平台等,它们不需要有显式的前端,只需要一套提供服务的接口,于是RESTful更是它们最好的选择。 +在RESTful架构下: +![](http://upload-images.jianshu.io/upload_images/4685968-bbab8b0bbb38d049.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +**Server的API如何设计才满足RESTful要求?** +URL中只使用名词来指定资源,原则上不使用动词 +best practices: +1\. URL root: +[https://example.org/api/v1/](https://example.org/api/v1/) +[https://api.example.com/v1/](https://api.example.com/v1/) +2\. API versioning: +可以放在URL里面,也可以用HTTP的header: +/api/v1/ +3\. URI使用名词而不是动词,且推荐用复数。 +BAD +* /getProducts +* /listOrders +* /retrieveClientByOrder?orderId=1 + +GOOD +* GET /products : will return the list of all products +* POST /products : will add a product to the collection +* GET /products/4 : will retrieve product #4 +* PATCH/PUT /products/4 : will update product #4 + +4\. 保证 HEAD 和 GET 方法是安全的,不会对资源状态有所改变(污染)。比如严格杜绝如下情况: +GET /deleteProduct?id=1 +5\. 资源的地址推荐用嵌套结构。比如: +GET /friends/10375923/profile +UPDATE /profile/primaryAddress/city6\. 警惕返回结果的大小。如果过大,及时进行分页(pagination)或者加入限制(limit)。HTTP协议支持分页(Pagination)操作,在Header中使用 Link 即可。 +7\. 使用正确的HTTP Status Code表示访问状态:[HTTP/1.1: Status Code Definitions](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html) +8\. 在返回结果用明确易懂的文本(String。注意返回的错误是要给人看的,避免用 1001 这种错误信息),而且适当地加入注释。 +9\. 关于安全:自己的接口就用https,加上一个key做一次hash放在最后即可。考虑到国情,HTTPS在无线网络里不稳定,可以使用Application Level的加密手段把整个HTTP的payload加密。有兴趣的朋友可以用手机连上电脑的共享Wi-Fi,然后用Charles监听微信的网络请求(发照片或者刷朋友圈)。 +如果是平台的API,可以用成熟但是复杂的OAuth2,新浪微博这篇:[授权机制说明](http://open.weibo.com/wiki/%E6%8E%88%E6%9D%83%E6%9C%BA%E5%88%B6%E8%AF%B4%E6%98%8E) + +#**各端的具体实现** +如上面的图所示,Server统一提供一套RESTful API,web+ios+android作为同等公民调用API。各端发展到现在,都有一套比较成熟的框架来帮开发者事半功倍。 + +-- Server -- +推荐: Spring MVC +教程: +[Getting Started · Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) diff --git "a/Netty/NIO\347\275\221\347\273\234\347\274\226\347\250\213/NIO\347\275\221\347\273\234\346\225\231\347\250\213(8)-SocketChannel.md" "b/Netty/NIO\347\275\221\347\273\234\347\274\226\347\250\213/NIO\347\275\221\347\273\234\346\225\231\347\250\213(8)-SocketChannel.md" deleted file mode 100644 index e309870d9b..0000000000 --- "a/Netty/NIO\347\275\221\347\273\234\347\274\226\347\250\213/NIO\347\275\221\347\273\234\346\225\231\347\250\213(8)-SocketChannel.md" +++ /dev/null @@ -1,75 +0,0 @@ -Java NIO中的SocketChannel是一个连接到TCP 网络套接字的通道。 - -可通过如下方式创建SocketChannel: -- 打开一个SocketChannel,并连接到网络上的某台服务器 -- 一个新连接到达ServerSocketChannel时,会创建一个SocketChannel - -# 打开 SocketChannel -下面是SocketChannel的打开方式的简单用法: -```java -SocketChannel socketChannel = SocketChannel.open(); -socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80)); -``` -看看Tomcat#NioEndpoint是如何使用的 -```java -ServerSocketChannel serverSock = ServerSocketChannel.open(); -socketProperties.setProperties(serverSock.socket()); -InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset()); -serverSock.socket().bind(addr,getAcceptCount()); -``` - -# 关闭 SocketChannel -当用完SocketChannel之后调用`SocketChannel.close()`关闭SocketChannel: -```java -socketChannel.close(); -``` -# 从 SocketChannel 读取数据 -要从SocketChannel中读取数据,调用一个read()的方法之一。如下: -```java -ByteBuffer buf = ByteBuffer.allocate(48); - -int bytesRead = socketChannel.read(buf); -``` -首先,分配一个Buffer。从SocketChannel读取到的数据将会放到这个Buffer中。 - -然后,调用SocketChannel.read()。该方法将数据从SocketChannel 读到Buffer中。read()方法返回的int值表示读了多少字节进Buffer里。如果返回的是-1,表示已经读到了流的末尾(连接关闭了)。 - -# 写入 SocketChannel -写数据到SocketChannel用的是`SocketChannel.write()`,该方法以一个Buffer作为参数。 - -如下: -```java -String newData = "New String to write to file..." + System.currentTimeMillis(); -ByteBuffer buf = ByteBuffer.allocate(48); -buf.clear(); -buf.put(newData.getBytes()); -buf.flip(); -while(buf.hasRemaining()) { - channel.write(buf); -} -``` - -注意SocketChannel.write()方法的调用是在一个while循环中的。Write()方法无法保证能写多少字节到SocketChannel。所以,我们重复调用write()直到Buffer没有要写的字节为止。 - -# 非阻塞模式 -可以设置 SocketChannel 为非阻塞模式(non-blocking mode)。设置后,就可以在异步模式下调用connect(), read() 和write()。 - -## connect() -如果SocketChannel在非阻塞模式下,此时调用connect(),该方法可能在连接建立之前就返回了。为了确定连接是否建立,可以调用finishConnect()的方法。像这样: - -```java -socketChannel.configureBlocking(false); -socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80)); -while(! socketChannel.finishConnect() ){ - //wait, or do something else... -} -``` - -## write() -非阻塞模式下,write()方法在尚未写出任何内容时可能就返回了。所以需要在循环中调用write()。前面已经有例子了,这里就不赘述了。 - -## read() -非阻塞模式下,read()方法在尚未读取到任何数据时可能就返回了。所以需要关注它的int返回值,它会告诉你读取了多少字节。 - -# 非阻塞模式与Selector -非阻塞模式与Selectors搭配会工作的更好,通过将一或多个SocketChannel注册到Selector,可以询问选择器哪个通道已经准备好了读取,写入等。 \ No newline at end of file diff --git "a/Netty/Netty\346\272\220\347\240\201\350\247\243\346\236\220/Netty\346\272\220\347\240\201\350\247\243\346\236\220\345\256\236\346\210\230(9)-\347\274\226\347\240\201.md" "b/Netty/Netty\346\272\220\347\240\201\350\247\243\346\236\220/Netty\346\272\220\347\240\201\350\247\243\346\236\220\345\256\236\346\210\230(9)-\347\274\226\347\240\201.md" index e69de29bb2..d4dade4ff2 100644 --- "a/Netty/Netty\346\272\220\347\240\201\350\247\243\346\236\220/Netty\346\272\220\347\240\201\350\247\243\346\236\220\345\256\236\346\210\230(9)-\347\274\226\347\240\201.md" +++ "b/Netty/Netty\346\272\220\347\240\201\350\247\243\346\236\220/Netty\346\272\220\347\240\201\350\247\243\346\236\220\345\256\236\346\210\230(9)-\347\274\226\347\240\201.md" @@ -0,0 +1,350 @@ +# 概述 +## 一个问题 +![](https://upload-images.jianshu.io/upload_images/4685968-84804df4d0671c4b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-19eb33dede2bb8e9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +编码器实现了` ChannelOutboundHandler`,并将出站数据从 一种格式转换为另一种格式,和我们方才学习的解码器的功能正好相反。Netty 提供了一组类, 用于帮助你编写具有以下功能的编码器: +- 将消息编码为字节 +- 将消息编码为消息 +我们将首先从抽象基类 MessageToByteEncoder 开始来对这些类进行考察 +# 1 抽象类 MessageToByteEncoder +![MessageToByteEncoder API](https://upload-images.jianshu.io/upload_images/4685968-fc2c1f94df8cb05e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +解码器通常需要在` Channel `关闭之后产生最后一个消息(因此也就有了 `decodeLast()`方法) +这显然不适于编码器的场景——在连接被关闭之后仍然产生一个消息是毫无意义的 + +## 1.1 ShortToByteEncoder +其接受一` Short` 型实例作为消息,编码为`Short`的原子类型值,并写入`ByteBuf`,随后转发给`ChannelPipeline`中的下一个 `ChannelOutboundHandler` +每个传出的 Short 值都将会占用 ByteBuf 中的 2 字节 + ![ShortToByteEncoder](https://upload-images.jianshu.io/upload_images/4685968-97f463faa149d584.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-8e071d7bafdfc0a9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## 1.2 Encoder +![](https://upload-images.jianshu.io/upload_images/4685968-729af7889f8ae41c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +Netty 提供了一些专门化的 `MessageToByteEncoder`,可基于此实现自己的编码器 +`WebSocket08FrameEncoder `类提供了一个很好的实例 +![](https://upload-images.jianshu.io/upload_images/4685968-69f5cff063544148.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 2 抽象类 MessageToMessageEncoder +你已经看到了如何将入站数据从一种消息格式解码为另一种 +为了完善这幅图,将展示 对于出站数据将如何从一种消息编码为另一种。`MessageToMessageEncoder `类的 `encode() `方法提供了这种能力 +![MessageToMessageEncoderAPI](https://upload-images.jianshu.io/upload_images/4685968-c2eba110d2a993ab.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +为了演示,使用` IntegerToStringEncoder` 扩展了 `MessageToMessageEncoder` +- 编码器将每个出站 Integer 的 String 表示添加到了该 List 中 +![](https://upload-images.jianshu.io/upload_images/4685968-4299c17d7c63ef14.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![IntegerToStringEncoder的设计](https://upload-images.jianshu.io/upload_images/4685968-f7ec09a39121010e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +关于有趣的 MessageToMessageEncoder 的专业用法,请查看 `io.netty.handler. codec.protobuf.ProtobufEncoder `类,它处理了由 Google 的 Protocol Buffers 规范所定义 的数据格式。 +# 一个java对象最后是如何转变成字节流,写到socket缓冲区中去的 +![](https://upload-images.jianshu.io/upload_images/4685968-24fca24338bf2433.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +pipeline中的标准链表结构 +java对象编码过程 +write:写队列 +flush:刷新写队列 +writeAndFlush: 写队列并刷新 +## pipeline中的标准链表结构 +![标准的pipeline链式结构](https://upload-images.jianshu.io/upload_images/4685968-e31d12a7a18ae15d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +数据从head节点流入,先拆包,然后解码成业务对象,最后经过业务`Handler`处理,调用`write`,将结果对象写出去 +而写的过程先通过`tail`节点,然后通过`encoder`节点将对象编码成`ByteBuf`,最后将该`ByteBuf`对象传递到`head`节点,调用底层的Unsafe写到JDK底层管道 +## Java对象编码过程 +为什么我们在pipeline中添加了encoder节点,java对象就转换成netty可以处理的ByteBuf,写到管道里? + +我们先看下调用write的code +![](https://upload-images.jianshu.io/upload_images/4685968-d0f990dfe8feec5e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +业务处理器接受到请求之后,做一些业务处理,返回一个`user` +- 然后,user在pipeline中传递 +![AbstractChannel#](https://upload-images.jianshu.io/upload_images/4685968-73ef914536864393.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![DefaultChannelPipeline#](https://upload-images.jianshu.io/upload_images/4685968-120b0e1792d9fb6c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![AbstractChannelHandlerContext#](https://upload-images.jianshu.io/upload_images/4685968-9eb7d051da8c055e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![AbstractChannelHandlerContext#](https://upload-images.jianshu.io/upload_images/4685968-c19b9919da807791.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 情形一 +![AbstractChannelHandlerContext#](https://upload-images.jianshu.io/upload_images/4685968-b0a3e8ee071ee4ce.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![AbstractChannelHandlerContext#](https://upload-images.jianshu.io/upload_images/4685968-7d045ca0ab92822f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 情形二 +![AbstractChannelHandlerContext#](https://upload-images.jianshu.io/upload_images/4685968-a9a361e8b6b0c0b5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-800871bb8d968ecb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![AbstractChannelHandlerContext#invokeWrite0](https://upload-images.jianshu.io/upload_images/4685968-6bbc148ee05a7145.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![AbstractChannelHandlerContext#invokeFlush0](https://upload-images.jianshu.io/upload_images/4685968-8aa6125ace28d5ce.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +handler 如果不覆盖 flush 方法,就会一直向前传递直到 head 节点 +![](https://upload-images.jianshu.io/upload_images/4685968-bddea4d884dbbbd5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +落到 `Encoder`节点,下面是 `Encoder` 的处理流程 +![](https://upload-images.jianshu.io/upload_images/4685968-74ef3d309d447ade.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +按照简单自定义协议,将Java对象 User 写到传入的参数 out中,这个out到底是什么? + +需知` User `对象,从`BizHandler`传入到 `MessageToByteEncoder`时,首先传到 `write` +![](https://upload-images.jianshu.io/upload_images/4685968-667d8c9562155645.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +### 1. 判断当前Handelr是否能处理写入的消息(匹配对象) +![](https://upload-images.jianshu.io/upload_images/4685968-716c1fa479aeed87.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-4a1d643f04edb25b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-d46fa8bcf1ce03a3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + - 判断该对象是否是该类型参数匹配器实例可匹配到的类型 +![TypeParameterMatcher#](https://upload-images.jianshu.io/upload_images/4685968-c2ee03c7c6e9b816.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![具体实例](https://upload-images.jianshu.io/upload_images/4685968-9e5abcf80fc0d548.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +### 2 分配内存 +![](https://upload-images.jianshu.io/upload_images/4685968-4d65f0f5674af21d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-5476a7fdb230815b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +### 3 编码实现 +- 调用`encode`,这里就调回到 `Encoder` 这个`Handler`中 +![](https://upload-images.jianshu.io/upload_images/4685968-b9df1d4be3dfabe3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 其为抽象方法,因此自定义实现类实现编码方法 +![](https://upload-images.jianshu.io/upload_images/4685968-6f47b5610e0afa2f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-357ce06307b4b0cd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +### 4 释放对象 +- 既然自定义Java对象转换成`ByteBuf`了,那么这个对象就已经无用,释放掉 (当传入的`msg`类型是`ByteBuf`时,就不需要自己手动释放了) +![](https://upload-images.jianshu.io/upload_images/4685968-933bdca4347cbc93.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-ac7d7e987a786b0e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +### 5 传播数据 +//112 如果buf中写入了数据,就把buf传到下一个节点,直到 header 节点 +![](https://upload-images.jianshu.io/upload_images/4685968-72ef8f66aaced620.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +### 6 释放内存 +//115 否则,释放buf,将空数据传到下一个节点 +// 120 如果当前节点不能处理传入的对象,直接扔给下一个节点处理 +// 127 当buf在pipeline中处理完之后,释放 +![](https://upload-images.jianshu.io/upload_images/4685968-10b7d01223813755.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +#### Encoder处理传入的Java对象 +- 判断当前`Handler`是否能处理写入的消息 + - 如果能处理,进入下面的流程 + - 否则,直接扔给下一个节点处理 +- 将对象强制转换成`Encoder ` 可以处理的 `Response`对象 +- 分配一个`ByteBuf` +- 调用`encoder`,即进入到 Encoder 的 encode方法,该方法是用户代码,用户将数据写入ByteBuf +- 既然自定义Java对象转换成ByteBuf了,那么这个对象就已经无用了,释放掉(当传入的msg类型是ByteBuf时,无需自己手动释放) +- 如果buf中写入了数据,就把buf传到下一个节点,否则,释放buf,将空数据传到下一个节点 +- 最后,当buf在pipeline中处理完之后,释放节点 + +总结就是,`Encoder`节点分配一个`ByteBuf`,调用`encode`方法,将Java对象根据自定义协议写入到ByteBuf,然后再把ByteBuf传入到下一个节点,在我们的例子中,最终会传入到head节点 +``` +public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + unsafe.write(msg, promise); +} +``` +这里的msg就是前面在Encoder节点中,载有java对象数据的自定义ByteBuf对象 +## write - 写buffer队列 +![](https://upload-images.jianshu.io/upload_images/4685968-307c73b5dfd72d23.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![ChannelOutboundInvoker#](https://upload-images.jianshu.io/upload_images/4685968-96833b7477fefe2c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-1ae909693fc84ebf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![write(Object msg, boolean flush, ChannelPromise promise)](https://upload-images.jianshu.io/upload_images/4685968-c08cff4eba7e1db2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-e4253f83c5e64716.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-8af14874dc519026.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-7d5e1e346cf7cacb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![HeadContext in DefaultChannelPipeline#write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)](https://upload-images.jianshu.io/upload_images/4685968-82841997d30ade96.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![Unsafe in Channel#write(Object msg, ChannelPromise promise)](https://upload-images.jianshu.io/upload_images/4685968-ef289b46a21326ee.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +以下过程分三步讲解 +![](https://upload-images.jianshu.io/upload_images/4685968-0685a9bb2338e8d3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +#### direct ByteBuf +![](https://upload-images.jianshu.io/upload_images/4685968-0c4f5243677578a7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![AbstractChannel#filterOutboundMessage(Object msg)](https://upload-images.jianshu.io/upload_images/4685968-045f2edbcc3ac462.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 首先,调用` assertEventLoop `确保该方法的调用是在`reactor`线程中 +- 然后,调用 `filterOutboundMessage() `,将待写入的对象过滤,把非`ByteBuf`对象和`FileRegion`过滤,把所有的非直接内存转换成直接内存`DirectBuffer` +![](https://upload-images.jianshu.io/upload_images/4685968-6624e78825702577.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![AbstractNioChannel#newDirectBuffer](https://upload-images.jianshu.io/upload_images/4685968-e3b5ee7cc58e8fba.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +#### 插入写队列 +- 接下来,估算出需要写入的ByteBuf的size + +![](https://upload-images.jianshu.io/upload_images/4685968-7c49f8cfc8abb137.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 最后,调用 ChannelOutboundBuffer 的addMessage(msg, size, promise) 方法,所以,接下来,我们需要重点看一下这个方法干了什么事情 +![ChannelOutboundBuffer](https://upload-images.jianshu.io/upload_images/4685968-5d5799967e956bf8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +想要理解上面这段代码,须掌握写缓存中的几个消息指针 +![](https://upload-images.jianshu.io/upload_images/4685968-de798f9d24c6ca9d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +ChannelOutboundBuffer 里面的数据结构是一个单链表结构,每个节点是一个 Entry,Entry 里面包含了待写出ByteBuf 以及消息回调 promise下面分别是 +### 三个指针的作用 +- flushedEntry +表第一个被写到OS Socket缓冲区中的节点 +![ChannelOutboundBuffer](https://upload-images.jianshu.io/upload_images/4685968-07a5e15190603fb2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- unFlushedEntry +表第一个未被写入到OS Socket缓冲区中的节点 +![ChannelOutboundBuffer](https://upload-images.jianshu.io/upload_images/4685968-87709ad85523cc40.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- tailEntry +表`ChannelOutboundBuffer`缓冲区的最后一个节点 +![ChannelOutboundBuffer](https://upload-images.jianshu.io/upload_images/4685968-8c220a874c9b76b6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +### 图解过程 +- 初次调用write 即 `addMessage` 后 +![](https://upload-images.jianshu.io/upload_images/4685968-01ff08665e5fb3d5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +`fushedEntry`指向空,`unFushedEntry`和 `tailEntry `都指向新加入节点 + +- 第二次调用 `addMessage`后 +![](https://upload-images.jianshu.io/upload_images/4685968-c3f5e605fcabd30d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 第n次调用 `addMessage`后 +![](https://upload-images.jianshu.io/upload_images/4685968-f3340e19079c3e27.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +可得,调用n次`addMessage`后 +- `flushedEntry`指针一直指向`null`,表此时尚未有节点需写到Socket缓冲区 +- `unFushedEntry`后有n个节点,表当前还有n个节点尚未写到Socket缓冲区 + +#### 设置写状态 +![ChannelOutboundBuffer#addMessage](https://upload-images.jianshu.io/upload_images/4685968-6f2250e10cdbbd9c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 统计当前有多少字节需要需要被写出 +![ChannelOutboundBuffer#addMessage(Object msg, int size, ChannelPromise promise)](https://upload-images.jianshu.io/upload_images/4685968-5543bf8e6d4579e9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 当前缓冲区中有多少待写字节 +![ChannelOutboundBuffer#](https://upload-images.jianshu.io/upload_images/4685968-0171d03c0fc61952.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +![](https://upload-images.jianshu.io/upload_images/4685968-f0219d7b269f9a6e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![ChannelConfig#getWriteBufferHighWaterMark()](https://upload-images.jianshu.io/upload_images/4685968-b61cc201e20ffe40.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-c8732e196722e9d1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-17856713165403bf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 所以默认不能超过64k +![WriteBufferWaterMark](https://upload-images.jianshu.io/upload_images/4685968-5d7ec5b3129fcadb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +![](https://upload-images.jianshu.io/upload_images/4685968-d19715897fdf72b5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 自旋锁+CAS 操作,通过 pipeline 将事件传播到channelhandler 中监控 +![](https://upload-images.jianshu.io/upload_images/4685968-bccfb48e0b7c36c0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +## flush:刷新buffer队列 +### 添加刷新标志并设置写状态 +- 不管调用`channel.flush()`,还是`ctx.flush()`,最终都会落地到`pipeline`中的`head`节点 +![DefaultChannelPipeline#flush](https://upload-images.jianshu.io/upload_images/4685968-ecb565a2254297d0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 之后进入到`AbstractUnsafe` +![AbstractChannel#flush()](https://upload-images.jianshu.io/upload_images/4685968-6a2b9b242520c3ef.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- flush方法中,先调用 +![ChannelOutboundBuffer#addFlush](https://upload-images.jianshu.io/upload_images/4685968-c14e8c30f202db83.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![ChannelOutboundBuffer#decrementPendingOutboundBytes(long size, boolean invokeLater, boolean notifyWritability)](https://upload-images.jianshu.io/upload_images/4685968-09b36f5b96a13a2d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-a01a7b4688477996.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![和之前那个实例相同,不再赘述](https://upload-images.jianshu.io/upload_images/4685968-04c753ec32f58d92.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 结合前面的图来看,上述过程即 +首先拿到 `unflushedEntry` 指针,然后将` flushedEntry `指向`unflushedEntry`所指向的节点,调用完毕后 +![](https://upload-images.jianshu.io/upload_images/4685968-28a4e504c8b9a158.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +### 遍历 buffer 队列,过滤bytebuf +- 接下来,调用 `flush0()` +![](https://upload-images.jianshu.io/upload_images/4685968-37c6cb089cce16d1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 发现这里的核心代码就一个 `doWrite` +![AbstractChannel#](https://upload-images.jianshu.io/upload_images/4685968-669bf7e04ff0e99a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +# AbstractNioByteChannel +- 继续跟 +``` +protected void doWrite(ChannelOutboundBuffer in) throws Exception { + int writeSpinCount = -1; + + boolean setOpWrite = false; + for (;;) { + // 拿到第一个需要flush的节点的数据 + Object msg = in.current(); + + if (msg instanceof ByteBuf) { + boolean done = false; + long flushedAmount = 0; + // 拿到自旋锁迭代次数 + if (writeSpinCount == -1) { + writeSpinCount = config().getWriteSpinCount(); + } + // 自旋,将当前节点写出 + for (int i = writeSpinCount - 1; i >= 0; i --) { + int localFlushedAmount = doWriteBytes(buf); + if (localFlushedAmount == 0) { + setOpWrite = true; + break; + } + + flushedAmount += localFlushedAmount; + if (!buf.isReadable()) { + done = true; + break; + } + } + + in.progress(flushedAmount); + + // 写完之后,将当前节点删除 + if (done) { + in.remove(); + } else { + break; + } + } + } +} +``` +- 第一步,调用`current()`先拿到第一个需要`flush`的节点的数据 +![ChannelOutboundBuffer#current](https://upload-images.jianshu.io/upload_images/4685968-d3e67e72eef65f8f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 第二步,拿到自旋锁的迭代次数 +![](https://upload-images.jianshu.io/upload_images/4685968-1d6de61347280fb4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +- 第三步 调用 JDK 底层 API 进行自旋写 +自旋的方式将`ByteBuf`写到JDK NIO的`Channel` +强转为ByteBuf,若发现没有数据可读,直接删除该节点 +![](https://upload-images.jianshu.io/upload_images/4685968-4d37a05bf05ffe07.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 拿到自旋锁迭代次数 + +![image.png](https://upload-images.jianshu.io/upload_images/4685968-d210766988601cf9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 在并发编程中使用自旋锁可以提高内存使用率和写的吞吐量,默认值为16 +![ChannelConfig](https://upload-images.jianshu.io/upload_images/4685968-a8475bef6ecf13e2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 继续看源码 +![](https://upload-images.jianshu.io/upload_images/4685968-ecafbd59548a6fd0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![AbstractNioByteChannel#](https://upload-images.jianshu.io/upload_images/4685968-dce60c0a5bf11941.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- `javaChannel()`,表明 JDK NIO Channel 已介入此次事件 +![NioSocketChannel#](https://upload-images.jianshu.io/upload_images/4685968-a4397658d76d4698.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![ByteBuf#readBytes(GatheringByteChannel out, int length)](https://upload-images.jianshu.io/upload_images/4685968-cbb0d3b5f759ee4b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 得到向JDK 底层已经写了多少字节 +![PooledDirectByteBuf#](https://upload-images.jianshu.io/upload_images/4685968-3c02f943cdd66670.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-e7b52e9aa0e733c9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 从 Netty 的 bytebuf 写到 JDK 底层的 bytebuffer +![](https://upload-images.jianshu.io/upload_images/4685968-ecf02ba9cc614c0f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-ccdaa9ea3aff5b95.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 第四步,删除该节点 +节点的数据已经写入完毕,接下来就需要删除该节点 +![](https://upload-images.jianshu.io/upload_images/4685968-624394ec70aace1b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +首先拿到当前被`flush`掉的节点(`flushedEntry`所指) +然后拿到该节点的回调对象 `ChannelPromise`, 调用 `removeEntry()`移除该节点 +![](https://upload-images.jianshu.io/upload_images/4685968-331da9841bbbdc9b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +这里是逻辑移除,只是将flushedEntry指针移到下个节点,调用后 +![](https://upload-images.jianshu.io/upload_images/4685968-a3ee365cf099451f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +随后,释放该节点数据的内存,调用` safeSuccess `回调,用户代码可以在回调里面做一些记录,下面是一段Example +``` +ctx.write(xx).addListener(new GenericFutureListener>() { + @Override + public void operationComplete(Future future) throws Exception { + // 回调 + } +}) +``` +最后,调用 `recycle`,将当前节点回收 +## writeAndFlush: 写队列并刷新 +`writeAndFlush`在某个`Handler`中被调用之后,最终会落到 `TailContext `节点 +![](https://upload-images.jianshu.io/upload_images/4685968-7de9bca98e530e24.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +``` + +public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) { + write(msg, true, promise); + + return promise; +} + +``` +![AbstractChannelHandlerContext#](https://upload-images.jianshu.io/upload_images/4685968-2d386c037c13aa70.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![AbstractChannelHandlerContext#](https://upload-images.jianshu.io/upload_images/4685968-3f7d85e213c1523e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +最终,通过一个`boolean`变量,表示是调用` invokeWriteAndFlush`,还是` invokeWrite`,`invokeWrite`便是我们上文中的write过程 +![AbstractChannelHandlerContext#](https://upload-images.jianshu.io/upload_images/4685968-8d312140e4d95ba0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +可以看到,最终调用的底层方法和单独调用` write `和` flush `一样的 +![](https://upload-images.jianshu.io/upload_images/4685968-d6e4563b1ff4f249.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-579fcadc81f33348.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +由此看来,`invokeWriteAndFlush`基本等价于`write`之后再来一次`flush` + +# 总结 +- 调用`write`并没有将数据写到Socket缓冲区中,而是写到了一个单向链表的数据结构中,`flush`才是真正的写出 +- `writeAndFlush`等价于先将数据写到netty的缓冲区,再将netty缓冲区中的数据写到Socket缓冲区中,写的过程与并发编程类似,用自旋锁保证写成功 +- netty中的缓冲区中的ByteBuf为DirectByteBuf +![](https://upload-images.jianshu.io/upload_images/4685968-42527f4855e71919.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## 当 BizHandler 通过 writeAndFlush 方法将自定义对象往前传播时,其实可以拆分成两个过程 +- 通过 pipeline逐渐往前传播,传播到其中的一个 encode 节点后,其负责重写 write 方法将自定义的对象转化为 ByteBuf,接着继续调用 write 向前传播 + +- pipeline中的编码器原理是创建一个`ByteBuf`,将Java对象转换为`ByteBuf`,然后再把`ByteBuf`继续向前传递,若没有再重写了,最终会传播到 head 节点,其中缓冲区列表拿到缓存写到 JDK 底层 ByteBuffer \ No newline at end of file diff --git "a/Python/\347\210\254\350\231\253/python\351\253\230\347\272\247\347\210\254\350\231\253\345\256\236\346\210\230\344\271\213Headers\344\277\241\346\201\257\346\240\241\351\252\214-Cookie.md" "b/Python/\347\210\254\350\231\253/python\351\253\230\347\272\247\347\210\254\350\231\253\345\256\236\346\210\230\344\271\213Headers\344\277\241\346\201\257\346\240\241\351\252\214-Cookie.md" deleted file mode 100644 index fccea8ba37..0000000000 --- "a/Python/\347\210\254\350\231\253/python\351\253\230\347\272\247\347\210\254\350\231\253\345\256\236\346\210\230\344\271\213Headers\344\277\241\346\201\257\346\240\241\351\252\214-Cookie.md" +++ /dev/null @@ -1,68 +0,0 @@ -### python高级爬虫实战之Headers信息校验-Cookie - -#### 一、什么是cookie - -​ 上期我们了解了User-Agent,这期我们来看下如何利用Cookie进行用户模拟登录从而进行网站数据的爬取。 - -首先让我们来了解下什么是Cookie: - -​ Cookie指某些网站为了辨别用户身份、从而储存在用户本地终端上的数据。当客户端在第一次请求网站指定的首页或登录页进行登录之后,服务器端会返回一个Cookie值给客户端。如果客户端为浏览器,将自动将返回的cookie存储下来。当再次访问改网页的其他页面时,自动将cookie值在Headers里传递过去,服务器接受值后进行验证,如合法处理请求,否则拒绝请求。 - -### 二、如何利用cookie - -​ 举个例子我们要去微博爬取相关数据,首先我们会遇到登录的问题,当然我们可以利用python其他的功能模块进行模拟登录,这里可能会涉及到验证码等一些反爬手段。 - -![截屏2024-03-04 下午7.53.55](https://s2.loli.net/2024/03/04/j7RxseHBKSlGMD5.png) - -换个思路,我们登录好了,通过开发者工具“右击” 检查(或者按F12) 获取到对应的cookie,那我们就可以绕个登录的页面,利用cookie继续用户模拟操作从而直接进行操作了。 - -![截屏2024-03-04 下午8.02.39](https://s2.loli.net/2024/03/04/qLygJpvH6RYTlzE.png) - -利用cookie实现模拟登录的两种方法: - -- [ ] 将cookie插入Headers请求头 - - ``` - Headers={"cookie":"复制的cookie值"} - ``` - - - -- [ ] 将cookie直接作为requests方法的参数 - -``` -cookie={"cookie":"复制的cookie值"} -requests.get(url,cookie=cookie) -``` - -#### 三、利用selenium获取cookie,实现用户模拟登录 - -实现方法:利用selenium模拟浏览器操作,输入用户名,密码 或扫码进行登录,获取到登录的cookie保存成文件,加载文件解析cookie实现用户模拟登录。 - -```python -from selenium import webdriver -from time import sleep -import json -#selenium模拟浏览器获取cookie -def getCookie: - driver = webdriver.Chrome() - driver.maximize_window() - driver.get('https://weibo.co m/login.php') - sleep(20) # 留时间进行扫码 - Cookies = driver.get_cookies() # 获取list的cookies - jsCookies = json.dumps(Cookies) # 转换成字符串保存 - with open('cookies.txt', 'w') as f: - f.write(jsCookies) - -def login: - filename = 'cookies.txt' - #创建MozillaCookieJar实例对象 - cookie = cookiejar.MozillaCookieJar() - #从文件中读取cookie内容到变量 - cookie.load(filename, ignore_discard=True, ignore_expires=True) - response = requests.get('https://weibo.co m/login.php',cookie=cookie) -``` - -#### 四、拓展思考 - -​ 如果频繁使用一个账号进行登录爬取网站数据有可能导致服务器检查到异常,对当前账号进行封禁,这边我们就需要考虑cookie池的引入了。 \ No newline at end of file diff --git "a/Python/\347\210\254\350\231\253/python\351\253\230\347\272\247\347\210\254\350\231\253\345\256\236\346\210\230\344\271\213Headers\344\277\241\346\201\257\346\240\241\351\252\214-User-Agent.md" "b/Python/\347\210\254\350\231\253/python\351\253\230\347\272\247\347\210\254\350\231\253\345\256\236\346\210\230\344\271\213Headers\344\277\241\346\201\257\346\240\241\351\252\214-User-Agent.md" deleted file mode 100644 index 0fa29af29d..0000000000 --- "a/Python/\347\210\254\350\231\253/python\351\253\230\347\272\247\347\210\254\350\231\253\345\256\236\346\210\230\344\271\213Headers\344\277\241\346\201\257\346\240\241\351\252\214-User-Agent.md" +++ /dev/null @@ -1,61 +0,0 @@ -### python高级爬虫实战之Headers信息校验-User-Agent - -​ User-agent 是当前网站反爬策略中最基础的一种反爬技术,服务器通过接收请求头中的user-agen的值来判断是否为正常用户访问还是爬虫程序。 - -​ 下面举一个简单的例子 爬取我们熟悉的豆瓣网: - -```python -import requests -url='https://movie.douban.com/' -resp=requests.get(url) -print(resp.status_code) -``` - -运行结果得到status_code:418 - -说明我们爬虫程序已经被服务器所拦截,无法正常获取相关网页数据。 - -我们可以通过返回的状态码来了解服务器的相应情况 - -- 100–199:信息反馈 -- 200–299:成功反馈 -- 300–399:重定向消息 -- 400–499:客户端错误响应 -- 500–599:服务器错误响应 - -现在我们利用google chrome浏览器来打开豆瓣网,查看下网页。 - -正常打开网页后,我们在页面任意地方右击“检查” 打开开发者工具。 - -image-20240301205014592 - - - -选择:Network-在Name中随便找一个文件点击后,右边Headers显示内容,鼠标拉到最下面。 - -![截屏2024-03-01 下午8.53.05](https://s2.loli.net/2024/03/01/XdjyBL5ClIYnT9F.png) - -User-Agent: - -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 - -我们把这段带到程序中再试下看效果如何。 - -```python -import requests -url='https://movie.douban.com/' -headers={ -"user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36" -} -resp=requests.get(url,headers=headers) -print(resp.status_code) -``` - -完美,执行后返回状态码200 ,说明已经成功骗过服务器拿到了想要的数据。 - -​ 对于User-agent 我们可以把它当做一个身份证,这个身份证中会包含很多信息,通过这些信息可以识别出访问者。所以当服务器开启了user-agent认证时,就需要像服务器传递相关的信息进行核对。核对成功,服务器才会返回给用户正确的内容,否则就会拒绝服务。 - -当然,对于Headers的相关信息还有很多,后续我们再一一讲解,下期见。 - - - diff --git "a/Python/\347\210\254\350\231\253/\345\210\251\347\224\250python\345\256\236\347\216\260\345\260\217\350\257\264\350\207\252\347\224\261.md" "b/Python/\347\210\254\350\231\253/\345\210\251\347\224\250python\345\256\236\347\216\260\345\260\217\350\257\264\350\207\252\347\224\261.md" deleted file mode 100644 index deb2f6262f..0000000000 --- "a/Python/\347\210\254\350\231\253/\345\210\251\347\224\250python\345\256\236\347\216\260\345\260\217\350\257\264\350\207\252\347\224\261.md" +++ /dev/null @@ -1,91 +0,0 @@ -### 利用python实现小说自由 - -#### 一、用到的相关模块 - -1.reuqests模块 - -安装reuqest模块,命令行输入: - -``` -pip install requests -``` - -2.xpath解析 - -​ XPath 即为 XML 路径语言,它是一种用来确定 XML (标准通用标记语言子集)文档中某部分位置的语言。XPath 基于 XML 的树状结构,提供在数据结构树中找寻节点的能力。起初 XPath 的提出的初衷是将其作为一个通用的、介于 XPointer 与 XSL 间的语法模型。但是 XPath 很快的被开发者采用来当作小型查询语言。 - -​ 简单的来说:Xpath(XML Path Language)是一门在 XML 和 HTML 文档中查找信息的语言,可用来在 XML 和 HTML 文档中对元素和属性进行遍历。 - -​ xml 是 一个HTML/XML的解析器,主要的功能是如何解析和提取 HTML/XML 数据。 - -安装xml: - -``` -pip install lxml -``` - - - -#### 二、实现步骤 - -1.首先我们打开一个小说的网址:https://www.qu-la.com/booktxt/17437775116/ - -2.右击“检查” 查看下这个网页的相关代码情况 - - - -我们可以发现所有的内容都被包裹在

    - -通过xpath 解析出每个章节的标题,和链接。 - -3.根据对应的链接获取每个章节的文本。同样的方法找到章节文本的具体位置 - -//*[@id="txt"] - - - -3.for 循环获取所有链接的文本,保存为Txt文件。 - -#### 三、代码展示 - -```python -import requests -from lxml import etree -def getNovel(): - headers = {'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36'}; - html = requests.get('https://www.qu-la.com/booktxt/17437775116/',headers=headers).content - doc=etree.HTML(html) - contents=doc.xpath('//*[ @id="list"]/div[3]/ul[2]') #获取到小说的所有章节 - for content in contents: - links=content.xpath('li/a/@href') #获取每个章节的链接 - for link in links: #循环处理每个章节 - url='https://www.qu-la.com'+link #拼接章节url - html=requests.get(url).text - doc=etree.HTML(html) - content = doc.xpath('//*[@id="txt"]/text()') #获取章节的正文 - title = doc.xpath('//*[@id="chapter-title"]/h1/text()') #获取标题 - #所有的保存到一个文件里面 - with open('books/凡人修仙之仙界篇.txt', 'a') as file: - file.write(title[0]) - print('正在下载{}'.format(title[0])) - for items in content: - file.write(item) - - print('下载完成') -getNovel() #调用函数 -``` - -#### 四、拓展思考 - -1.写一个搜索界面,用户输入书名自主下载对应的小说。 - -2.引入多进程异步下载,提高小说的下载速度。 - - - diff --git a/README.md b/README.md index 8ad293ad89..8461ee5d80 100755 --- a/README.md +++ b/README.md @@ -1,29 +1,38 @@ -## 1 Java程序员充电、求职必备的核心知识库 +## 1 JavaEdge - Java程序员充电、求职必备的核心知识库 全方位详细深入阐述从入门到高级Java程序员必备的知识技能。按照现有计划,主要研究如下方面知识点: -0. Java SE重难点、包含但不限于集合、多线程、泛型、反射、I/O -1. Java Web重难点,包含但不限于Servlet、Tomcat -2. Java 后端开发流行框架,包含但不限于Spring、MyBatis -3. 计算机理论基础,包含但不限于计算机操作系统(Linux)、计算机网络、常见的数据结构与算法(Java实现) -4. 数据存储组件的基本操作与原理探究,包含但不限于MySQL、Redis、Kafka、Hive、HBase -5. 分布式、微服务时下流行框架及理论,包含但不限于Spring Cloud Alibaba、Dubbo全家桶 -6. 设计模式思想及应用 -7. 云原生相关,包含但不限于 Docker、k8s +0. Java SE重难点、包含但不限于集合、多线程、泛型、反射、I/O; +1. Java Web重难点,包含但不限于Servlet、JSP、Tomcat、Jetty ; +2. Java EE开发流行框架,包含但不限于Spring、MyBatis、Hibernate、Vert.X; +3. 计算机理论基础,包含但不限于计算机操作系统(Linux)、计算机网络、常见的数据结构与算法(Java实现) ; +4. 数据存储组件的基本操作与原理探究,包含但不限于MySQL、Redis、Kafka、Hive、HBase ; +5. 分布式、微服务时下流行框架及理论,包含但不限于CAP理论及其相关算法、Zookeeper、Spring Cloud Alibaba、Dubbo; +6. 编程设计模式思想及其在各种框架中的实际应用(Java实现) +7. 云原生时代的宠儿,包含但不限于 Docker、k8s ## 2 公众号 更多精彩内容将发布在公众号 **JavaEdge**,公众号提供大量求职面试资料,后台回复 "面试" 即可领取。 -本号系统整理了Java高级工程师必备技能点,帮你理清纷杂面试知识点,有的放矢。 +本号系统整理了Java 高级工程师必备技能点,帮你理清纷杂面试知识点,有的放矢。我本人也是基于这些知识体系,在各种求职征途中拿到百度、携程、华为、中兴、顺丰、帆软等offer。 + + ## 3 笔者简介 + ### [阿里云栖社区博客专家](https://yq.aliyun.com/users/article?spm=a2c4e.8091938.headeruserinfo.3.65993d6eqaQ0O6) +![](https://img-blog.csdnimg.cn/20190712131824494.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + ### [腾讯云自媒体邀约计划作者](https://cloud.tencent.com/developer/user/1752328) + +![](https://img-blog.csdnimg.cn/20190712140323352.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +
    -## 4 目录结构 +## 4 目录结构(不断优化中) |  数据结构与算法  | 操作系统 |  网络  | 面向对象 |   数据存储   |    Java    | 架构设计 |    框架    | 编程规范 |    职业规划    | | :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:| @@ -83,6 +92,15 @@ ### :memo: 职业规划 +## QQ 技术交流群 + +为大家提供一个学习交流平台,在这里你可以自由地讨论技术问题。 + + + +## 微信交流群 + + ### 本人微信 @@ -95,17 +113,4 @@ ### 绘图工具 - [draw.io](https://www.draw.io/) -- keynote - -再分享我整理汇总的一些 Java 面试相关资料(亲自验证,严谨科学!别再看网上误导人的垃圾面试题!!!),助你拿到更多 offer! - -![](https://img-blog.csdnimg.cn/35dcdea77d6d4845a18ef4780309a2a6.png) - -[点击获取更多经典必读电子书!](https://mp.weixin.qq.com/s?__biz=MzUzNTY5MzA3MQ==&mid=2247497273&idx=1&sn=b0f1e2e03cd7de3ce5d93cc8793d6d88&chksm=fa832459cdf4ad4fb046c0beb7e87ecea48f338278846679ef65238af45f0a135720e7061002&token=766333302&lang=zh_CN#rd) - -2023年最新Java学习路线一条龙: - -[![](https://img-blog.csdnimg.cn/0fe00585e984406fbd9c22cedbf4b239.png)](https://www.nowcoder.com/discuss/353159357007339520?sourceSSR=users) - - -再给大家推荐一个学习 前后端软件开发 和准备Java 面试的公众号[【JavaEdge】](https://mp.weixin.qq.com/s?__biz=MzUzNTY5MzA3MQ==&mid=2247498257&idx=1&sn=b09d88691f9bfd715e000b69ef61227e&chksm=fa832871cdf4a1675d4491727399088ca488fa13e0a3cdf2ece3012265e5a3ef273dff540879&token=766333302&lang=zh_CN#rd)(强烈推荐!) +- keynote \ No newline at end of file diff --git a/Spring/Spring Boot/Spring Boot Actuator.md b/Spring/Spring Boot/Spring Boot Actuator.md deleted file mode 100644 index b594d64889..0000000000 --- a/Spring/Spring Boot/Spring Boot Actuator.md +++ /dev/null @@ -1,77 +0,0 @@ -# 整合 -- 添加依赖 -![](https://img-blog.csdnimg.cn/20190916234856333.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 启动应用 -![](https://img-blog.csdnimg.cn/2019091623500231.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- [打开链接](http://localhost:8080/actuator) -![](https://img-blog.csdnimg.cn/20190916235411576.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190917001217324.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -## 健康信息 -健康信息可以检查应用的运行状态,它经常被监控软件用来提醒人们生产环境是否存在问题。health端点暴露的默认信息取决于端点是如何被访问的。对于一个非安全,未认证的连接只返回一个简单的'status'信息。对于一个安全或认证过的连接其他详细信息也会展示 -Spring Boot包含很多自动配置的HealthIndicators,你也可以写自己的。 - -### 自动配置的HealthIndicators -Spring Boot在合适的时候会自动配置以下HealthIndicators: -![](https://img-blog.csdnimg.cn/20190917002830278.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 下表显示了内置状态的默认状态映射: -![](https://img-blog.csdnimg.cn/20190917003004321.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -- 让我们配置一下health节点,并重启应用 -![](https://img-blog.csdnimg.cn/20190917001814832.png) -- 可看到对于磁盘的监控信息 -![](https://img-blog.csdnimg.cn/20190917001849686.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -## 应用信息 -应用信息会暴露所有InfoContributor beans收集的各种信息,Spring Boot包含很多自动配置的InfoContributors,你也可以编写自己的实现。 -### 自动配置的InfoContributors -Spring Boot会在合适的时候自动配置以下InfoContributors: -![](https://img-blog.csdnimg.cn/20190917003252246.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -> 注 使用management.info.defaults.enabled属性可禁用以上所有InfoContributors。 - -### 自定义应用info信息 -通过设置Spring属性info.*,你可以定义info端点暴露的数据。所有在info关键字下的Environment属性都将被自动暴露,例如,你可以将以下配置添加到application.properties: -``` -info.app.encoding=UTF-8 -info.app.java.source=1.8 -info.app.java.target=1.8 -``` -注 你可以在构建时扩展info属性,而不是硬编码这些值。假设使用Maven,你可以按以下配置重写示例: -``` -info.app.encoding=@project.build.sourceEncoding@ -info.app.java.source=@java.version@ -info.app.java.target=@java.version@ -``` -### Git提交信息 -info端点的另一个有用特性是,在项目构建完成后发布git源码仓库的状态信息。如果GitProperties bean可用,Spring Boot将暴露git.branch,git.commit.id和git.commit.time属性。 - -> 注 如果classpath根目录存在git.properties文件,Spring Boot将自动配置GitProperties bean。查看Generate git information获取更多详细信息。 - -使用management.info.git.mode属性可展示全部git信息(比如git.properties全部内容): -``` -management.info.git.mode=full -``` -### 构建信息 -如果BuildProperties bean存在,info端点也会发布你的构建信息。 - -注 如果classpath下存在META-INF/build-info.properties文件,Spring Boot将自动构建BuildProperties bean。Maven和Gradle都能产生该文件 - -- 配置info -![](https://img-blog.csdnimg.cn/20190917003634110.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 启动观察输出信息![](https://img-blog.csdnimg.cn/20190917003721284.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -- SpringBoot支持很多端点,除了默认显示的几个,还可以激活暴露所有端点 -![](https://img-blog.csdnimg.cn/20190917004116115.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190917004052711.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 如果只想暴露某个端点也是可以的 -![](https://img-blog.csdnimg.cn/2019091700435537.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 查看JVM最大内存 -![](https://img-blog.csdnimg.cn/20190917004447618.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -## 3 Beans -Bean 端点提供有关应用程序 bean 的信息。 -### 获取 Beans -- /actuator/beans GET 请求 -![](https://img-blog.csdnimg.cn/9f6178ded60f4b8ba018bc2bdef864de.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_12,color_FFFFFF,t_70,g_se,x_16) -响应的结构: -![](https://img-blog.csdnimg.cn/2f2e8b7d5b0643c5a18f25968242c948.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) diff --git "a/Spring/Spring Boot/\351\235\242\350\257\225\345\256\230\351\227\256\357\274\232\344\275\240\344\273\254\346\234\215\345\212\241\346\234\200\345\244\247\347\232\204\345\271\266\345\217\221\351\207\217\346\230\257\345\244\232\345\260\221\357\274\237.md" "b/Spring/Spring Boot/\351\235\242\350\257\225\345\256\230\351\227\256\357\274\232\344\275\240\344\273\254\346\234\215\345\212\241\346\234\200\345\244\247\347\232\204\345\271\266\345\217\221\351\207\217\346\230\257\345\244\232\345\260\221\357\274\237.md" deleted file mode 100644 index dff1968478..0000000000 --- "a/Spring/Spring Boot/\351\235\242\350\257\225\345\256\230\351\227\256\357\274\232\344\275\240\344\273\254\346\234\215\345\212\241\346\234\200\345\244\247\347\232\204\345\271\266\345\217\221\351\207\217\346\230\257\345\244\232\345\260\221\357\274\237.md" +++ /dev/null @@ -1,63 +0,0 @@ -Spring Boot 能支持的最大并发量主要看其对Tomcat的设置。由于现在都使用的是springboot服务,配置文件中也没有配置Tomcat 相关参数,基本都是使用默认的Tomcat的线程配置。 -![](https://img-blog.csdnimg.cn/20210703192756544.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -默认设置中,Tomcat的最大线程数200,最大连接数10000。 - -# 并发量指的是连接数,还是线程数? -连接数。 - -# 200个线程如何处理10000条连接? - Tomcat有两种处理连接的模式 -## BIO -一个线程只处理一个Socket连接 -## NIO -一个线程处理多个Socket连接。由于HTTP请求不会太耗时,而且多个连接一般不会同时来消息,所以一个线程处理多个连接没有太大问题。 - -# 为什么不多开几个线程? -多开线程的代价就是,增加上下文切换的时间,浪费CPU时间,另外还有就是线程数增多,每个线程分配到的时间片就变少。 -多开线程≠提高处理效率。 - -# 为何不增大最大连接数? -增大最大连接数,支持的并发量确实可以上去。但是在没有改变硬件条件的情况下,这种并发量的提升必定以牺牲响应时间为代价。 - -# 配置文件为空,这些默认配置哪来的? -Spring Boot的默认配置信息,都在 spring-boot-autoconfigure-版本号.jar 这个包中。 - -Tomcat配置:`org.springframework.boot.autoconfigure.web.ServerProperties.java` -![](https://img-blog.csdnimg.cn/20210703195025204.png) - - -最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目。 - - -最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目, - -# CPU密集型 -操作内存处理的业务,一般线程数设置为:CPU核数 + 1 或者 CPU核数*2。核数为4的话,一般设置 5 或 8 。 - -# IO密集型 -文件操作,网络操作,数据库操作,一般线程设置为:cpu核数 / (1-0.9),核数为4的话,一般设置 40 - -```bash -maxThreads="8" //最大并发数 -minSpareThreads="100"///初始化时创建的线程数 -maxSpareThreads="500"///一旦创建的线程超过这个值,Tomcat就会关闭不再需要的socket线程。 -acceptCount="700"// 指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理 - -maxThreads 客户请求最大线程数 -minSpareThreads Tomcat初始化时创建的 socket 线程数 -maxSpareThreads Tomcat连接器的最大空闲 socket 线程数 -enableLookups 若设为true, 则支持域名解析,可把 ip 地址解析为主机名 -redirectPort 在需要基于安全通道的场合,把客户请求转发到基于SSL 的 redirectPort 端口 -acceptAccount 监听端口队列最大数,满了之后客户请求会被拒绝(不能小于maxSpareThreads ) -connectionTimeout 连接超时 -minProcessors 服务器创建时的最小处理线程数 -maxProcessors 服务器同时最大处理线程数 -URIEncoding URL统一编码 - -maxThreads:处理的最大并发请求数,默认值200 -minSpareThreads:最小线程数始终保持运行,默认值10 -maxConnections:在给定时间接受和处理的最大连接数,默认值10000 -``` - -> 参考 -> - http://tomcat.apache.org/tomcat-8.0-doc/config/http.html#HTTP/1.1_and_HTTP/1.0_Support \ No newline at end of file diff --git "a/Spring/Spring RestTemplate\344\270\272\344\275\225\345\277\205\351\241\273\346\220\255\351\205\215MultiValueMap\357\274\237.md" "b/Spring/Spring RestTemplate\344\270\272\344\275\225\345\277\205\351\241\273\346\220\255\351\205\215MultiValueMap\357\274\237.md" deleted file mode 100644 index 0281c13f2c..0000000000 --- "a/Spring/Spring RestTemplate\344\270\272\344\275\225\345\277\205\351\241\273\346\220\255\351\205\215MultiValueMap\357\274\237.md" +++ /dev/null @@ -1,53 +0,0 @@ -微服务之间的大多都是使用 HTTP 通信,这自然少不了使用 HttpClient。 -在不适用 Spring 前,一般使用 Apache HttpClient 和 Ok HttpClient 等,而一旦引入 Spring,就有了更好选择 - RestTemplate。 - -接口: -![](https://img-blog.csdnimg.cn/f7633a690cec471787c55a5b34722db1.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -想接受一个 Form 表单请求,读取表单定义的两个参数 para1 和 para2,然后作为响应返回给客户端。 - -定义完接口后,使用 RestTemplate 来发送一个这样的表单请求,代码示例如下: -![](https://img-blog.csdnimg.cn/96362640fa3b4354b5b0a32687c302b1.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -上述代码定义了一个 Map,包含了 2 个表单参数,然后使用 RestTemplate 的 postForObject 提交这个表单。 -执行代码提示 400 错误,即请求出错: -![](https://img-blog.csdnimg.cn/a2dfa488fb0f415aacb08441bd35e76d.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -就是缺少 para1 表单参数,why? -# 解析 -RestTemplate 提交的表单,最后提交请求啥样? -Wireshark 抓包: -![](https://img-blog.csdnimg.cn/94de06901df043ac9fa9a8de77ed9fd6.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -实际上是将定义的表单数据以 JSON 提交过去了,所以我们的接口处理自然取不到任何表单参数。 -why?怎么变成 JSON 请求体提交数据呢?注意 RestTemplate 执行调用栈: -![](https://img-blog.csdnimg.cn/63895ac7bac84bde8dce1d208938af54.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -最终使用的 Jackson 工具序列化了表单 -![](https://img-blog.csdnimg.cn/8f4582ece1c244baabe31b43463db638.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -用到 JSON 的关键原因在 -### RestTemplate.HttpEntityRequestCallback#doWithRequest -![](https://img-blog.csdnimg.cn/c779397a10a14cf3a9b5fe3635b22b8b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -根据当前要提交的 Body 内容,遍历当前支持的所有编解码器: -- 若找到合适编解码器,用之完成 Body 转化 - -看下 JSON 的编解码器对是否合适的判断 -### AbstractJackson2HttpMessageConverter#canWrite -![](https://img-blog.csdnimg.cn/cf45893c65b5498eb2d16638ce7873e5.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -可见,当使用的 Body 为 HashMap,是可完成 JSON 序列化的。 -所以后续将这个表单序列化为请求 Body了。 - -但我还是疑问,为何适应表单处理的编解码器不行? -那就该看编解码器判断是否支持的实现: -### FormHttpMessageConverter#canWrite![](https://img-blog.csdnimg.cn/ee0f19a3ed6c43269a58ddb3179591f5.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -可见只有当我们发送的 Body 是 MultiValueMap 才能使用表单来提交。 -原来使用 RestTemplate 提交表单必须是 MultiValueMap! -而我们案例定义的就是普通的 HashMap,最终是按请求 Body 的方式发送出去的。 -# 修正 -换成 MultiValueMap 类型存储表单数据即可: -![](https://img-blog.csdnimg.cn/ace1283803104462bab2261d6f9789d3.png) -修正后,表单数据最终使用下面的代码进行了编码: -### FormHttpMessageConverter#write -![](https://img-blog.csdnimg.cn/312d71a80572443b8bb153dcdbd15e0e.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -发送出的数据截图如下: -![](https://img-blog.csdnimg.cn/244f8819935244d59083e66ca6b16ea4.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -这就对了!其实官方文档也说明了: -![](https://img-blog.csdnimg.cn/7020d620d5b544a29d022877dcfac1a8.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -> 参考: -> - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html \ No newline at end of file diff --git "a/Spring/Spring \346\263\250\345\205\245\351\233\206\345\220\210\347\261\273\345\236\213.md" "b/Spring/Spring \346\263\250\345\205\245\351\233\206\345\220\210\347\261\273\345\236\213.md" deleted file mode 100644 index a03f12b62c..0000000000 --- "a/Spring/Spring \346\263\250\345\205\245\351\233\206\345\220\210\347\261\273\345\236\213.md" +++ /dev/null @@ -1,111 +0,0 @@ -集合类型的自动注入是Spring提供的另外一个强大功能。我们在方便的使用依赖注入的特性时,必须要思考对象从哪里注入、怎么创建、为什么是注入这一个对象的。虽然编写框架的目的是让开发人员无需关心太多底层细节,能专心业务逻辑的开发,但是作为开发人员不能真的无脑去使用框架。 -务必学会注入集合等高级用法,让自己有所提升! - -现在有一需求:存在多个用户Bean,找出来存储到一个List。 -# 1 注入方式 -## 1.1 收集方式 -多个用户Bean定义: -![](https://img-blog.csdnimg.cn/ac65da5cd0bd43818389f5deab4803e6.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -有了集合类型的自动注入后,即可收集零散的用户Bean: -![](https://img-blog.csdnimg.cn/20d0ea3055e348a2860e82ceeca9f36f.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -这样即可完成集合类型注入: -![](https://img-blog.csdnimg.cn/1d3ae50349f743738a2d7473936e2a3a.png) - - -但当持续增加一些user时,可能就不喜欢用上述的注入集合类型了,而是这样: -## 1.2 直接装配方式 -![](https://img-blog.csdnimg.cn/b3cf4f059ebe48a2b352ea48b274fe9d.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/b1cea2e7b6fb4f7b9ac241002cc2e953.png) - -分开玩,大家应该不会有啥问题,若两种方式共存了,会咋样? -运行程序后发现直接装配方式的未生效: -![](https://img-blog.csdnimg.cn/ce05a1948eaa414082d8f18685dfdad1.png) - -这是为啥呢? -# 2 源码解析 -就得精通这两种注入风格在Spring分别如何实现的。 -## 2.1 收集装配 -### DefaultListableBeanFactory#resolveMultipleBeans -```java -private Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable String beanName, - @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) { - final Class type = descriptor.getDependencyType(); - if (descriptor instanceof StreamDependencyDescriptor) { - // 装配stream - return stream; - } - else if (type.isArray()) { - // 装配数组 - return result; - } - else if (Collection.class.isAssignableFrom(type) && type.isInterface()) { - // 装配集合 - // 获取集合的元素类型 - Class elementType = descriptor.getResolvableType().asCollection().resolveGeneric(); - if (elementType == null) { - return null; - } - // 根据元素类型查找所有的bean - Map matchingBeans = findAutowireCandidates(beanName, elementType, - new MultiElementDescriptor(descriptor)); - if (matchingBeans.isEmpty()) { - return null; - } - if (autowiredBeanNames != null) { - autowiredBeanNames.addAll(matchingBeans.keySet()); - } - // 转化查到的所有bean放置到集合并返回 - TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); - Object result = converter.convertIfNecessary(matchingBeans.values(), type); - // ... - return result; - } - else if (Map.class == type) { - // 解析map - return matchingBeans; - } - else { - return null; - } -} -``` -#### 1 获取集合类型的elementType -目标类型定义为List users,所以元素类型为User: -![](https://img-blog.csdnimg.cn/f8d0c4f475bb431ea81cd62c880a34cf.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -#### 2 根据元素类型找出所有Bean -有了elementType,即可据其找出所有Bean: -![](https://img-blog.csdnimg.cn/a2d520efa38749b79dc754c4eccaf158.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -#### 3 将匹配的所有的Bean按目标类型转化 -上一步获取的所有的Bean都以`java.util.LinkedHashMap.LinkedValues`存储,和目标类型大不相同,所以最后按需转化。 - -本案例中,需转化为List: -![](https://img-blog.csdnimg.cn/4fa60adda03b4c9e8ca483a4729d32bf.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -## 2.2 直接装配方式 -#### DefaultListableBeanFactory#findAutowireCandidates -不再赘述。 - -最后就是根据目标类型直接寻找匹配Bean名称为users的`List`装配给userController#users属性。 - -当同时满足这两种装配方式时,Spring会如何处理呢? -## DefaultListableBeanFactory#doResolveDependency -![](https://img-blog.csdnimg.cn/c644acc0147a4c09adb3c18d3325083b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -显然这两种装配集合的方式不能同存,结合本案例: -- 当使用收集装配时,能找到任一对应Bean,则返回 -- 若一个都没找到,才采用直接装配 - -所以后期以List方式直接添加的user Bean都不生效! -# 3 修正 -务必避免两种方式共存去装配集合!只选用一种方式即可。 -比如只使用直接装配: -![](https://img-blog.csdnimg.cn/73198359e18e441ab5c2bbc0337eb2ac.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16)只使用收集方式: -![](https://img-blog.csdnimg.cn/16d5c6659cd94bb68a4fad26224cbcf0.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -如何做到让用户2优先输出呢? -控制spring bean加载顺序: -1. Bean上使用@Order注解,如@Order(2)。数值越小表示优先级越高。默认优先级最低。 -2. @DependsOn 使用它,可使得依赖的Bean如果未被初始化会被优先初始化。 -3. 添加@Order(number)注解,number越小优先级越高,越靠前 -4. 声明user这些Bean时将id=2的user提到id=1之前 \ No newline at end of file diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/.md" b/Spring/SpringFramework/.md similarity index 100% rename from "JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/.md" rename to Spring/SpringFramework/.md diff --git "a/Spring/SpringFramework/InitializingBean\344\275\277\347\224\250.md" "b/Spring/SpringFramework/InitializingBean\344\275\277\347\224\250.md" new file mode 100644 index 0000000000..d2f4ce1464 --- /dev/null +++ "b/Spring/SpringFramework/InitializingBean\344\275\277\347\224\250.md" @@ -0,0 +1,78 @@ +InitializingBean接口为bean提供了初始化方法的方式 + +它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候会自动执行该方法。 + +``` + +package org.springframework.beans.factory; + +/** + * 所有由BeanFactory设置的所有属性都需要响应的bean的接口:例如,执行自定义初始化,或者只是检查所有强制属性是否被设置。 + *实现InitializingBean的替代方法是指定一个自定义init方法,例如在XML bean定义中。 + */ +public interface InitializingBean { + + /** + * 在BeanFactory设置了提供的所有bean属性后,由BeanFactory调用。 + *这个方法允许bean实例在所有的bean属性被设置时才能执行 + */ + void afterPropertiesSet() throws Exception; + +} +``` + +在spring初始化bean的时候,如果该bean是实现了InitializingBean接口,并且同时在配置文件中指定了init-method,系统则先调用afterPropertiesSet方法,再调用init-method中指定的方法。 +``` + +``` +这种方式在spring中是怎么实现的呢? +通过查看spring加载bean的源码类(AbstractAutowireCapableBeanFactory)可以看出玄机 +AbstractAutowireCapableBeanFactory类中的invokeInitMethods方法讲解的非常清楚,源码如下: + +``` +protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd) + throws Throwable { + //首先看该bean是否实现了实现了InitializingBean接口,若已实现,则直接调用bean的afterPropertiesSet方法 + boolean isInitializingBean = (bean instanceof InitializingBean); + if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) { + if (logger.isDebugEnabled()) { + logger.debug("Invoking afterPropertiesSet() on bean with name '" + beanName + "'"); + } + if (System.getSecurityManager() != null) { + try { + AccessController.doPrivileged(new PrivilegedExceptionAction() { + @Override + public Object run() throws Exception { + //直接调用afterPropertiesSet + ((InitializingBean) bean).afterPropertiesSet(); + return null; + } + }, getAccessControlContext()); + } + catch (PrivilegedActionException pae) { + throw pae.getException(); + } + } + else { + //直接调用afterPropertiesSet + ((InitializingBean) bean).afterPropertiesSet(); + } + } + + if (mbd != null) { + String initMethodName = mbd.getInitMethodName(); + //判断是否指定了init-method方法,如果指定了init-method方法,则再调用制定的init-method + if (initMethodName != null && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) && + !mbd.isExternallyManagedInitMethod(initMethodName)) { + //进一步查看该方法的源码,可以发现init-method方法中指定的方法是通过反射实现的 + invokeCustomInitMethod(beanName, bean, mbd); + } + } + } +``` + +观察源码可得出以下结论 + + 1. spring为bean提供了两种初始化bean的方式,实现InitializingBean接口而实现afterPropertiesSet方法,或者在配置文件中同过init-method指定,两种方式可以同时使用 + 2. 实现InitializingBean接口是直接调用afterPropertiesSet方法,比通过反射调用init-method指定的方法相对来说效率要高。但init-method方式消除了对spring的依赖 + 3. 若调用afterPropertiesSet方法时产生异常,则也不会再调用init-method指定的方法 diff --git "a/Spring/SpringFramework/Spring AOP\345\210\233\345\273\272Proxy\347\232\204\350\277\207\347\250\213.md" "b/Spring/SpringFramework/Spring AOP\345\210\233\345\273\272Proxy\347\232\204\350\277\207\347\250\213.md" deleted file mode 100644 index e1984562a7..0000000000 --- "a/Spring/SpringFramework/Spring AOP\345\210\233\345\273\272Proxy\347\232\204\350\277\207\347\250\213.md" +++ /dev/null @@ -1,124 +0,0 @@ -> 全是干货的技术号: -> 本文已收录在github,欢迎 star/fork: -> [https://github.com/Wasabi1234/Java-Interview-Tutorial](https://github.com/Wasabi1234/Java-Interview-Tutorial) - -Spring在程序运行期,就能帮助我们把切面中的代码织入Bean的方法内,让开发者能无感知地在容器对象方法前后随心添加相应处理逻辑,所以AOP其实就是个代理模式。 -但凡是代理,由于代码不可直接阅读,也是初级程序员们 bug 的重灾区。 -# 1 案例 -某游戏系统,含负责点券充值的类CouponService,它含有一个充值方法deposit(): -![](https://img-blog.csdnimg.cn/d92cc2a3908e4c0f8468ef42440bb940.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -deposit()会使用微信支付充值。因此在这个方法中,加入pay()。 - -由于微信支付是第三方接口,需记录接口调用时间。 -引入 **@Around** 增强 ,分别记录在pay()方法执行前后的时间,并计算pay()执行耗时。 -![](https://img-blog.csdnimg.cn/82ec220929644cc7b751b148279a9c92.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -Controller: -![](https://img-blog.csdnimg.cn/815d2b60a2ad49e888132a0da33fdbad.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -访问接口,会发现这段计算时间的切面并没有执行到,输出日志如下: -![](https://img-blog.csdnimg.cn/36db9a5c4f584a6098a9f65be4a76646.png) -切面类明明定义了切面对应方法,但却没执行到。说明在类的内部,通过this调用的方法,不会被AOP增强。 -# 2 源码解析 -- this对应的对象就是一个普通CouponService对象: -![](https://img-blog.csdnimg.cn/cfb4ccb9412c4f28baba84a0ff75f5b8.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -- 而在Controller层中自动装配的CouponService对象: -![](https://img-blog.csdnimg.cn/24bcddfe649c427cad718d9e0a7fadea.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -是个被Spring增强过的Bean,所以执行deposit()时,会执行记录接口调用时间的增强操作。而this对应的对象只是一个普通的对象,并无任何额外增强。 - -为什么this引用的对象只是一个普通对象? -要从Spring AOP增强对象的过程来看。 -## 实现 -AOP的底层是动态代理,创建代理的方式有两种: -- JDK方式 -只能对实现了接口的类生成代理,不能针对普通类 -- CGLIB方式 -可以针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,来实现代理对象。 -![](https://img-blog.csdnimg.cn/ddb123e788dc47e083433f5ccd24ce3a.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -针对非Spring Boot程序,除了添加相关AOP依赖项外,还会使用 **@EnableAspectJAutoProxy** 开启AOP功能。 -这个注解类引入AspectJAutoProxyRegistrar,它通过实现ImportBeanDefinitionRegistrar接口完成AOP相关Bean准备工作。 - -现在来看下创建代理对象的过程。先来看下调用栈: -![](https://img-blog.csdnimg.cn/44c0f0955fa74569abd9baa3496a07a8.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -- **创建代理对象的时机** -创建一个Bean时 - -创建的的关键工作由AnnotationAwareAspectJAutoProxyCreator完成 -## AnnotationAwareAspectJAutoProxyCreator -一种BeanPostProcessor。所以它的执行是在完成原始Bean构建后的初始化Bean(initializeBean)过程中 -##### AbstractAutoProxyCreator#postProcessAfterInitialization -```java -public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { - if (bean != null) { - Object cacheKey = getCacheKey(bean.getClass(), beanName); - if (this.earlyProxyReferences.remove(cacheKey) != bean) { - return wrapIfNecessary(bean, beanName, cacheKey); - } - } - return bean; -} -``` -关键方法wrapIfNecessary:在需要使用AOP时,它会把创建的原始Bean对象wrap成代理对象,作为Bean返回。 -## AbstractAutoProxyCreator#wrapIfNecessary -```java -protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { - // 省略非关键代码 - Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); - if (specificInterceptors != DO_NOT_PROXY) { - this.advisedBeans.put(cacheKey, Boolean.TRUE); - Object proxy = createProxy( - bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); - this.proxyTypes.put(cacheKey, proxy.getClass()); - return proxy; - } - // 省略非关键代码 -} -``` -## createProxy -创建代理对象的关键: -```java -protected Object createProxy(Class beanClass, @Nullable String beanName, - @Nullable Object[] specificInterceptors, TargetSource targetSource) { - // ... - // 1. 创建一个代理工厂 - ProxyFactory proxyFactory = new ProxyFactory(); - if (!proxyFactory.isProxyTargetClass()) { - if (shouldProxyTargetClass(beanClass, beanName)) { - proxyFactory.setProxyTargetClass(true); - } - else { - evaluateProxyInterfaces(beanClass, proxyFactory); - } - } - Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); - // 2. 将通知器(advisors)、被代理对象等信息加入到代理工厂 - proxyFactory.addAdvisors(advisors); - proxyFactory.setTargetSource(targetSource); - customizeProxyFactory(proxyFactory); - // ... - // 3. 通过代理工厂获取代理对象 - return proxyFactory.getProxy(getProxyClassLoader()); -} -``` -经过这样一个过程,一个代理对象就被创建出来了。我们从Spring中获取到的对象都是这个代理对象,所以具有AOP功能。而之前直接使用this引用到的只是一个普通对象,自然也就没办法实现AOP的功能了。 -# 3 修正 -经过前面分析可知,**只有引用的是被 `动态代理` 所创对象,才能被Spring增强,实现期望的AOP功能**。 - -那得怎么处理对象,才具备这样的条件? -### 被@Autowired注解 -通过 **@Autowired**,在类的内部,自己引用自己: -![](https://img-blog.csdnimg.cn/6c83d422014c47588a10659ebef5e4ae.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/c4ba19274d3e457a8705790e9ef7d94f.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_18,color_FFFFFF,t_70,g_se,x_16) - -### 直接从AopContext获取当前Proxy -AopContext,就是通过一个ThreadLocal来将Proxy和线程绑定起来,这样就可以随时拿出当前线程绑定的Proxy。 - -使用该方案有个前提,需要在 **@EnableAspectJAutoProxy** 加配置项 **exposeProxy = true** ,表示将代理对象放入到ThreadLocal,这才可以直接通过 -```java -AopContext.currentProxy() -``` -获取到,否则报错: -![](https://img-blog.csdnimg.cn/8af06b17823a4c8dbec6ca6238807d92.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -于是修改代码: -![](https://img-blog.csdnimg.cn/562eef29a2bb416c85b378a754ef0257.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -勿忘修改**EnableAspectJAutoProxy** 的 **exposeProxy**属性: -![](https://img-blog.csdnimg.cn/11175d6f95ab462a8d444cca182493d3.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) \ No newline at end of file diff --git "a/Spring/SpringFramework/Spring AOP\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210\357\274\237.md" "b/Spring/SpringFramework/Spring AOP\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210\357\274\237.md" deleted file mode 100644 index 5625f5ce76..0000000000 --- "a/Spring/SpringFramework/Spring AOP\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210\357\274\237.md" +++ /dev/null @@ -1,78 +0,0 @@ -IoC和AOP生而就是为了解耦和扩展。 - -# 什么是 IoC? - -一种设计思想,将设计好的对象交给Spring容器控制,而非直接在对象内部控制。 - -> 为啥要让容器来管理对象呢?你这程序员咋就知道甩锅呢? -![](https://img-blog.csdnimg.cn/20210512150222541.png) - -拥有初级趣味的码农,可能只是觉着使用IoC方便,就是个用来解耦的,但这还远非容器的益处。 -利用容器管理所有的框架、业务对象,我们可以做到: -- 无侵入调整对象的关系 -- 无侵入地随时调整对象的属性 -- 实现对象的替换 - -这使得框架开发者在后续实现一些扩展就很容易。 - -# 什么是AOP? - -AOP实现了高内聚、低耦合,在切面集中实现横切关注点(缓存、权限、日志等),然后通过切点配置把代码注入到合适的位置。 - -# 基本概念 -- **连接点(Join point)** -就是方法执行 -- **切点(Pointcut)** -Spring AOP默认使用AspectJ查询表达式,通过在连接点运行查询表达式来匹配切点 -- **增强(Advice)** -也叫作通知,定义了切入切点后增强的方式,包括前、后、环绕等。Spring AOP中,把增强定义为拦截器 -- **切面(Aspect)** -切面=切点+增强 - -### Declaring Advice -有如下三类增强: -#### @Before -#### @After -#### @Around -**环绕通知**运行 **around** 匹配方法的执行。它有机会在方法运行之前和之后都工作,并确定方法实际运行何时、如何甚至是否执行。若你需要以线程安全的方式(例如启动和停止计时器)在方法执行前后共享状态,则通常会使用Around advice。 - -使用案例: -```java -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.ProceedingJoinPoint; - -@Aspect -public class AroundExample { - - @Around("com.xyz.myapp.CommonPointcuts.businessService()") - public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { - // start stopwatch - Object retVal = pjp.proceed(); - // stop stopwatch - return retVal; - } -} -``` -around advice 返回的值就是方法调用者看到的返回值。例如,一个简单的缓存aspect可以返回一个值从缓存(如果它有)或调用`procedd`如果它没有。请注意,可以多次调用`procedd`,或者根本不在around advice的主体内调用,这都是合法的。 - -推荐始终使用最不强大的advice形式,以满足需求。 - -使用 **@Around** 注解声明环绕通知时,第一个参数必须是ProceedingJoinPoint类型。 -在通知的方法体中,调用 `proceed()` 会导致基础方法运行。 `proceed()` 也可以在Object[]中传递。数组中的值在进行时用作方法执行的参数。 -#### Advice参数 -Spring 提供全种类的通知,这意味着你在通知的方法签名中声明所需参数,而非和`Object[]`协作。 -如何编写通用的通知,以便了解通知方法当前在通知啥玩意。 -#### Access to the Current JoinPoint -任何通知方法都可能声明类型`org.aspectj.lang.JoinPoint` 的参数(请注意,围绕建议需要申报类型'继续JoinPoint'的第一参数,该参数是 JoinPoint 的子类。 -JoinPoint 接口提供了许多有用的方法: -- getArgs() -返回方法的参数 -- getThis() -返回代理对象 -- getTarget() -返回目标对象 -- getSignature() -Returns a description of the method that is being advised. -- toString() -Prints a useful description of the method being advised. \ No newline at end of file diff --git "a/Spring/SpringFramework/Spring Bean \344\276\235\350\265\226\346\263\250\345\205\245\345\270\270\350\247\201\351\224\231\350\257\257.md" "b/Spring/SpringFramework/Spring Bean \344\276\235\350\265\226\346\263\250\345\205\245\345\270\270\350\247\201\351\224\231\350\257\257.md" deleted file mode 100644 index 7afc37ce21..0000000000 --- "a/Spring/SpringFramework/Spring Bean \344\276\235\350\265\226\346\263\250\345\205\245\345\270\270\350\247\201\351\224\231\350\257\257.md" +++ /dev/null @@ -1,103 +0,0 @@ -有时我们会使用@Value自动注入,同时也存在注入到集合、数组等复杂类型的场景。这都是方便写 bug 的场景。 - -# 1 @Value未注入预期值 -在字段或方法/构造函数参数级别使用,指示带注释元素的默认值表达式。 -通常用于表达式驱动或属性驱动的依赖注入。 还支持处理程序方法参数的动态解析 -例如,在 Spring MVC 中,一个常见的用例是使用#{systemProperties.myProp} systemProperties.myProp #{systemProperties.myProp}样式的 SpEL(Spring 表达式语言)表达式注入值。 -或可使用${my.app.myProp}样式属性占位符注入值。 - -**@Value**实际处理由BeanPostProcessor执行,这意味着不能在BeanPostProcessor或BeanFactoryPostProcessor类型中使用 **@Value**。 -## V.S Autowired -在装配对象成员属性时,常使用@Autowired来装配。但也使用@Value进行装配: -- 使用@Autowired一般都不会设置属性值 -- @Value必须指定一个字符串值,因其定义做了要求: -![](https://img-blog.csdnimg.cn/3182d920dc254a90a519f4691a4f2b6c.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -一般都会因 **@Value** 常用于String类型的装配,误以为其不能用于非内置对象的装配。 - -可用如下方式注入一个属性成员: -![](https://img-blog.csdnimg.cn/5404d551f42f4937bbc915911e007e58.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -使用 **@Value**更多是用来装配String,而且支持多种强大的装配方式 -![](https://img-blog.csdnimg.cn/84ffc52fd3db4f89b14ca60c2beebccb.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_16,color_FFFFFF,t_70,g_se,x_16) -application.properties配置了这样一个属性: -```java -user=admin -password=pass -``` - -然后我们在一个Bean中,分别定义两个属性来引用它们: -![](https://img-blog.csdnimg.cn/12b3b5bc840f4133b41d71ac8af8e2da.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -password返回了配置值,但user却不是配置文件的指定值,而是PC用户名。 -## 答疑 -有一个正确的,说明 **@Value**使用姿势没问题,但user为啥不正确? -这就得精通Spring到底如何根据 **@Value**查询值。 -### @Value的核心工作流程 -#### DefaultListableBeanFactory#doResolveDependency -```java -@Nullable -public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName, - @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException { - // ... - Class type = descriptor.getDependencyType(); - // 寻找@Value - Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor); - if (value != null) { - if (value instanceof String) { - // 解析Value值 - String strVal = resolveEmbeddedValue((String) value); - BeanDefinition bd = (beanName != null && containsBean(beanName) ? - getMergedBeanDefinition(beanName) : null); - value = evaluateBeanDefinitionString(strVal, bd); - } - - // 转化Value解析的结果到装配的类型 - TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); - try { - return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor()); - } - catch (UnsupportedOperationException ex) {} - } - // ... - } -``` -**@Value** 的工作大体分为以下三个核心步骤。 -#### 1 寻找@Value -判断这个属性字段是否标记为@Value: -##### QualifierAnnotationAutowireCandidateResolver#findValue -- valueAnnotationType就是 **@Value** -![](https://img-blog.csdnimg.cn/8588a95243e1400d82181d711b97b1ef.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/46f640ee047c4f3d8d85c085770e64c9.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -#### 2 解析@Value的字符串值 -若一个字段标记了 **@Value**,则可拿到对应字符串值,然后根据字符串值解析,最终解析的结果可能是一个字符串or对象,取决于字符串怎么写。 - -#### 3 将解析结果转化为待装配的对象的类型 -当拿到上一步生成的结果后,我们会发现可能和我们要装配的类型不匹配。 -比如定义的是UUID,而结果是个字符串,此时就会根据目标类型来寻找转化器执行转化: -![](https://img-blog.csdnimg.cn/18ed8af280474e6887aa5da59ef53c9d.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -分析可得问题关键在第二步,执行过程: -![](https://img-blog.csdnimg.cn/728d4ba5c3154e2eb952d32609d53995.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -这里是在解析嵌入的值,替换掉占位符。使用PropertySourcesPlaceholderConfigurer根据PropertySources替换。 - -当使用 `${user}` 获取替换值时,最终执行的查找并非只在application.property文件。 -可以发现如下“源”都是替换的依据: -![](https://img-blog.csdnimg.cn/3b8888a22217457b8e04c0da90afe8e8.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -而具体的查找执行,通过 -##### PropertySourcesPropertyResolver#getProperty -获取执行方式 -![](https://img-blog.csdnimg.cn/ed8efa9caf61487795c573a75b13d67b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -在解析Value字符串有顺序,源都存在CopyOnWriteArrayList,启动时就被按序固定下来了,一个一个“源”顺序查找,在其中一源找到后,就直接返回。 - -查看systemEnvironment源,发现刚好有个user和自定义的重合,且值不是admin。 -![](https://img-blog.csdnimg.cn/24b383c3c8a84d7a8edf3f98dfd42e78.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -所以这真是冤家路窄了,刚好系统环境变量(systemEnvironment)含同名配置。若没有意识到它们的存在,起了同名字符串作为 **@Value**,就容易引发这类问题。 -## 修正 -避免使用同一个名称,具体修改如下: -```java -user.name=admin -user.password=pass -``` -其实还是不行。 -在systemProperties这个PropertiesPropertySource源中刚好存在user.name,真是无巧不成书。所以命名时,我们一定要注意不仅要避免和环境变量冲突,也要注意避免和系统变量等其他变量冲突,才能从根本解决该问题。 - -Spring给我们提供了很多好用的功能,但是这些功能交织到一起后,就有可能让我们误入一些坑,只有了解它的运行方式,我们才能迅速定位问题、解决问题。 \ No newline at end of file diff --git "a/Spring/SpringFramework/Spring Bean\345\237\272\347\241\200.md" "b/Spring/SpringFramework/Spring Bean\345\237\272\347\241\200.md" index b946fba568..9b5d16377c 100644 --- "a/Spring/SpringFramework/Spring Bean\345\237\272\347\241\200.md" +++ "b/Spring/SpringFramework/Spring Bean\345\237\272\347\241\200.md" @@ -186,3 +186,5 @@ bean元数据定义中的指定类只是初始类引用,可能结合使用的 该方法可确定给定名称bean的类型。 更确切地,返回针对相同bean名称的`BeanFactory.getBean`调用将返回的对象的类型。 且该方法的实现考虑了前面穷举的所有情况,并针对于FactoryBean ,返回FactoryBean所创建的对象类型,和`FactoryBean.getObjectType()`返回一致。 ![](https://img-blog.csdnimg.cn/20200828121006639.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) + +![](https://img-blog.csdnimg.cn/20200825235213822.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) diff --git "a/Spring/SpringFramework/Spring Exception\344\271\213\345\260\217\345\277\203\350\277\207\346\273\244\345\231\250\345\274\202\345\270\270.md" "b/Spring/SpringFramework/Spring Exception\344\271\213\345\260\217\345\277\203\350\277\207\346\273\244\345\231\250\345\274\202\345\270\270.md" deleted file mode 100644 index fe83f65d7b..0000000000 --- "a/Spring/SpringFramework/Spring Exception\344\271\213\345\260\217\345\277\203\350\277\207\346\273\244\345\231\250\345\274\202\345\270\270.md" +++ /dev/null @@ -1,67 +0,0 @@ -# 错误场景![](https://img-blog.csdnimg.cn/e2023dfa73674c659fe29797527f1a26.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -验证请求的Token合法性的Filter。Token校验失败时,直接抛自定义异常,移交给Spring处理: -![](https://img-blog.csdnimg.cn/122fe2d581fc4684a2ad6deb491987e7.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/016ce84b7da3488fa65148e1e9a27ee7.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/87e550844437408f88f254387b0b2cd6.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -测试HTTP请求: -![](https://img-blog.csdnimg.cn/18d36bd55d484550a4ad5bbee58fa49c.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -日志输出如下:说明**IllegalRequestExceptionHandler**未生效。 -![](https://img-blog.csdnimg.cn/81a7b9c33b7b48b5bc4353d11f6a4089.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -why? -这就需要精通Spring异常处理流程了。 -# 解析 -![](https://img-blog.csdnimg.cn/2c00379066d1490891b909c4013d1fdf.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -当所有Filter被执行完毕,Spring才会处理Servlet相关,而**DispatcherServlet**才是整个Servlet处理核心,它是前端控制器设计模式,提供 Spring Web MVC 的集中访问点并负责职责的分派。 - -在这,Spring处理了请求和处理器的对应关系及**统一异常处理**。 - -Filter内异常无法被统一处理,就是因为异常处理发生在 ***DispatcherServlet#doDispatch()*** -![](https://img-blog.csdnimg.cn/13642402d76c4f2fbadf4da28d7239c4.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -但此时,**过滤器已全部执行完**。 -# Spring异常统一处理 -## ControllerAdvice如何被Spring加载并对外暴露? -#### WebMvcConfigurationSupport#handlerExceptionResolver() -实例化并注册一个ExceptionHandlerExceptionResolver 的实例 -![](https://img-blog.csdnimg.cn/32e6572a6cfd4d93bacc992871875f24.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -最终按下图调用栈,Spring 实例化了ExceptionHandlerExceptionResolver类。 -![](https://img-blog.csdnimg.cn/2055644a405542698006d57e0fba1f50.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -ExceptionHandlerExceptionResolver实现了**InitializingBean** -![](https://img-blog.csdnimg.cn/916c98b8d2dc4144b3ba690d27f19091.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -重写 ***afterPropertiesSet()*** ![](https://img-blog.csdnimg.cn/09a7b4e7ca2b48ef811b35f3c90089f1.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -#### initExceptionHandlerAdviceCache -完成所有 ControllerAdvice 中的ExceptionHandler 初始化:查找所有 **@ControllerAdvice** 注解的 Bean,把它们放入exceptionHandlerAdviceCache。 -这里即指自定义的IllegalRequestExceptionHandler -![](https://img-blog.csdnimg.cn/c1102552aa0a4d478d2ed537ba72cf61.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/184207f02e31465ba45de946e5a1b73c.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -所有被 **@ControllerAdvice** 注解的异常处理器,都会在 **ExceptionHandlerExceptionResolver** 实例化时自动扫描并装载在其exceptionHandlerAdviceCache。 -#### initHandlerExceptionResolvers -当第一次请求发生时,***DispatcherServlet#initHandlerExceptionResolvers()*** 将获取所有注册到 Spring 的 HandlerExceptionResolver 实例(ExceptionHandlerExceptionResolver正是),存到**handlerExceptionResolvers** -![](https://img-blog.csdnimg.cn/9560e3a409254a82af3b655a12fe24cb.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/be51ad0247c24552837631caf23b2004.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -## ControllerAdvice如何被Spring消费并处理异常? -### DispatcherServlet -#### doDispatch() -![](https://img-blog.csdnimg.cn/90c9d7b1254b49da8f865323eeb5e42a.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -执行用户请求时,当查找、执行请求对应的 handler 过程中异常时: -1. 会把异常值赋给 dispatchException -2. 再移交 processDispatchResult() -#### processDispatchResult -![](https://img-blog.csdnimg.cn/287b3c8dae8e48c4a5ec8dccc7143dad.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -当Exception非空时,继续移交 -#### processHandlerException -![](https://img-blog.csdnimg.cn/6b18557adc6844adaaac2562955ef9ef.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -从 handlerExceptionResolvers 获取有效的异常解析器以解析异常。 - -这里的 handlerExceptionResolvers 一定包含声明的IllegalRequestExceptionHandler#IllegalRequestException 的异常处理器的 ExceptionHandlerExceptionResolver 包装类。 -# 修正 -为利用到 Spring MVC 的异常处理机制,改造Filter: -- 手动捕获异常 -- 将异常通过 HandlerExceptionResolver 进行解析处理 - -据此,修改 PermissionFilter,注入 HandlerExceptionResolver: -![](https://img-blog.csdnimg.cn/1b50cf3672214f418f908fd8f736a120.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -然后,在 doFilter 捕获异常并移交 HandlerExceptionResolver: -![](https://img-blog.csdnimg.cn/b83601b115024554854786652e2a2f92.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -现在再用错误 Token 请求,日志输出如下: -![](https://img-blog.csdnimg.cn/0f7cb17d0ccb4b3caf84ff6c78227656.png) 响应体: -![](https://img-blog.csdnimg.cn/e69723db189b4eca9231dc06fe3c7059.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) \ No newline at end of file diff --git "a/Spring/SpringFramework/Spring\345\237\272\344\272\216\346\263\250\350\247\243\347\232\204\345\256\271\345\231\250\351\205\215\347\275\256.md" "b/Spring/SpringFramework/Spring-5-0\344\270\255\346\226\207\347\211\210-3-9.md" similarity index 97% rename from "Spring/SpringFramework/Spring\345\237\272\344\272\216\346\263\250\350\247\243\347\232\204\345\256\271\345\231\250\351\205\215\347\275\256.md" rename to "Spring/SpringFramework/Spring-5-0\344\270\255\346\226\207\347\211\210-3-9.md" index 4fed3adf96..7d17d999e3 100644 --- "a/Spring/SpringFramework/Spring\345\237\272\344\272\216\346\263\250\350\247\243\347\232\204\345\256\271\345\231\250\351\205\215\347\275\256.md" +++ "b/Spring/SpringFramework/Spring-5-0\344\270\255\346\226\207\347\211\210-3-9.md" @@ -1,3 +1,5 @@ +### 3.9 基于注解的容器配置 + > **在配置Spring时注解是否比XML更好?** > > 基于注解配置的引入引出了一个问题——这种方式是否比基于XML的配置更好。简短的回答是视情况而定。长一点的回答是每种方法都有它的优点和缺点,通常是由开发者决定哪一种策略更适合他们。由于注解的定义方式,注解在它们的声明中提供了许多上下文,导致配置更简短更简洁。然而,XML擅长连接组件而不必接触源代码或重新编译它们。一些开发者更喜欢接近源代码,而另一些人则认为基于注解的类不再是POJOs,此外,配置变的去中心化,而且更难控制。 @@ -10,7 +12,8 @@ > 注解注入在XML注入之前进行,因此对于通过两种方法进行组装的属性后者的配置会覆盖前者。 跟以前一样,你可以作为单独的bean定义来注册它们,但也可以通过在一个基于XML的Spring配置(注入包含上下文命名空间)中包含下面的标签来隐式的注册它们: -```xml + +``` 在下面的例子中JSR 330的`@Inject`注解可以用来代替Spring的`@Autowired`注解。 你可以将`@Autowired`注解应用到构造函数上。 -```java +``` public class MovieRecommender { private final CustomerPreferenceDao customerPreferenceDao; @@ -75,7 +78,7 @@ public class MovieRecommender { 正如预料的那样,你也可以将`@Autowired`注解应用到“传统的”setter方法上: -```java +``` public class SimpleMovieLister { private MovieFinder movieFinder; @@ -92,7 +95,7 @@ public class SimpleMovieLister { 你也可以应用注解到具有任何名字和/或多个参数的方法上: -```java +``` public class MovieRecommender { private MovieCatalog movieCatalog; @@ -113,7 +116,7 @@ public class MovieRecommender { 你也可以应用`@Autowired`到字段上,甚至可以与构造函数混合用: -```java +``` public class MovieRecommender { private final CustomerPreferenceDao customerPreferenceDao; @@ -133,7 +136,7 @@ public class MovieRecommender { 通过给带有数组的字段或方法添加`@Autowired`注解,也可以从`ApplicationContext`中提供一组特定类型的bean: -```java +``` public class MovieRecommender { @Autowired @@ -146,7 +149,7 @@ public class MovieRecommender { 同样也可以应用到具有同一类型的集合上: -```java +``` public class MovieRecommender { private Set movieCatalogs; @@ -165,7 +168,7 @@ public class MovieRecommender { 只要期望的key是`String`,那么类型化的Maps就可以自动组装。Map的值将包含所有期望类型的beans,key将包含对应的bean名字: -```java +``` public class MovieRecommender { private Map movieCatalogs; @@ -182,7 +185,7 @@ public class MovieRecommender { 默认情况下,当没有候选beans可获得时,自动组装会失败;默认的行为是将注解的方法,构造函数和字段看作指明了需要的依赖。这个行为也可以通过下面的方式去改变。 -```java +``` public class SimpleMovieLister { private MovieFinder movieFinder; @@ -202,7 +205,8 @@ public class SimpleMovieLister { > 建议在`@Required`注解之上使用`@Autowired`的`required`特性。`required`特性表明这个属性自动装配是不需要的,如果这个属性不能被自动装配,它会被忽略。另一方面`@Required`是更强大的,在它强制这个属性被任何容器支持的bean设置。如果没有值注入,会抛出对应的异常。 你也可以对那些已知的具有可解析依赖的接口使用`@Autowired`:`BeanFactory`,`ApplicationContext`,`Environment`, `ResourceLoader`,`ApplicationEventPublisher`和`MessageSource`。这些接口和它们的扩展接口,例如`ConfigurableApplicationContext`或`ResourcePatternResolver`,可以自动解析,不需要特别的设置。 -```java + +``` public class MovieRecommender { @Autowired @@ -215,16 +219,16 @@ public class MovieRecommender { } ``` -`@Autowired`、`@Inject`、`@Resource`和`@Value`都是通过Spring的`BeanPostProcessor`实现处理的,这反过来意味着,你不能在自己的`BeanPostProcessor`或`BeanFactoryPostProcessor`中使用这些注解。 -这些类型必须显式通过XML或使用Spring的`@Bean`方法来装配。 -# 3 用@Primary微调基于注解的自动装配 +> `@Autowired`,`@Inject`,`@Resource`和`@Value`注解是通过Spring `BeanPostProcessor`实现处理,这反过来意味着你不能在你自己的`BeanPostProcessor`或`BeanFactoryPostProcessor`中应用这些注解(如果有的话)。这些类型必须显式的通过XML或使用Spring的`@Bean`方法来’wired up’。 + +#### 3.9.3 用@Primary微调基于注解的自动装配 因为根据类型的自动装配可能会导致多个候选目标,所以在选择过程中进行更多的控制经常是有必要的。一种方式通过Spring的`@Primary`注解来完成。当有个多个候选bean要组装到一个单值的依赖时,`@Primary`表明指定的bean应该具有更高的优先级。如果确定一个’primary’ bean位于候选目标中间,它将是那个自动装配的值。 假设我们具有如下配置,将`firstMovieCatalog`定义为主要的`MovieCatalog`。 -```java +``` @Configuration public class MovieConfiguration { @@ -242,7 +246,7 @@ public class MovieConfiguration { 根据这样的配置,下面的`MovieRecommender`将用`firstMovieCatalog`进行自动装配。 -```java +``` public class MovieRecommender { @Autowired @@ -255,7 +259,7 @@ public class MovieRecommender { 对应的bean定义如下: -```xml +``` `标记作为``标记的子元素,然后指定匹配你的定制限定符注解的类型和值。类型用来匹配注解的全限定类名称。或者,如果没有名称冲突的风险,为了方便,你可以使用简写的类名称。下面的例子证实了这些方法。 -```xml +``` @@ -459,7 +463,7 @@ public class MovieRecommender { 你也可以定义接收命名属性之外的定制限定符注解或代替简单的值属性。如果要注入的字段或参数指定了多个属性值,bean定义必须匹配所有的属性值才会被认为是一个可自动装配的候选目标。作为一个例子,考虑下面的注解定义: -```java +``` @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Qualifier @@ -474,7 +478,7 @@ public @interface MovieQualifier { 这种情况下`Format`是枚举类型: -```java +``` public enum Format { VHS, DVD, BLURAY } @@ -482,7 +486,7 @@ public enum Format { 要自动装配的字段使用定制限定符进行注解,并且包含了两个属性值:`genre`和`format`。 -```java +``` public class MovieRecommender { @Autowired @@ -508,7 +512,7 @@ public class MovieRecommender { 最后,bean定义应该包含匹配的限定符值。这个例子也证实了bean元属性可以用来代替``子元素。如果可获得``,它和它的属性优先级更高,如果当前没有限定符,自动装配机制会将``内的值作为备用,正如下面的例子中的最后两个bean定义。 -```xml +``` ``` -# 5 使用泛型作为自动装配限定符 +#### 3.9.5 使用泛型作为自动装配限定符 除了`@Qualifier`注解外,也可以使用Java的泛型类型作为限定符的一种暗示方式。例如,假设你有如下配置: -```java +``` @Configuration public class MyConfiguration { @@ -574,7 +578,7 @@ public class MyConfiguration { 假设上面的beans实现了一个泛型接口,例如,`Store`和`Store`,你可以`@Autowire` `Store`接口,泛型将作为限定符使用: -```java +``` @Autowired private Store s1; // qualifier, injects the stringStore bean @@ -584,15 +588,18 @@ private Store s2; // qualifier, injects the integerStore bean 当自动装配`Lists`,`Maps`和`Arrays`时,也会应用泛型限定符: -```java +``` // Inject all Store beans as long as they have an generic // Store beans will not appear in this list @Autowired private List> s; ``` -### 3.9.6 CustomAutowireConfigurer -`CustomAutowireConfigurer`是一个能使你注册自己的定制限定符注解类型的`BeanFactoryPostProcessor`,即使它们不使用Spring的`@Qualifier` -```xml + +#### 3.9.6 CustomAutowireConfigurer + +`CustomAutowireConfigurer`是一个能使你注册自己的定制限定符注解类型的`BeanFactoryPostProcessor`,即使它们不使用Spring的`@Qualifier`注解进行注解。 + +``` @@ -613,13 +620,13 @@ private List> s; 当多个beans符合条件成为自动装配的候选目标时,”primary” bean的决定如下:如果在候选目标中某个确定的bean中的`primary`特性被设为`true`,它将被选为目标bean。 -# 7 @Resource +#### 3.9.7 @Resource Spring也支持使用JSR-250 `@Resource`对字段或bean属性setter方法进行注入。这是在Java EE 5和6中的一种通用模式,例如在JSF 1.2管理的beans或JAX-WS 2.0的端点。Spring对它管理的对象也支持这种模式。 `@Resource`采用名字属性,默认情况下Spring将名字值作为要注入的bean的名字。换句话说,它遵循*by-name*语义,下面的例子证实了这一点: -```java +``` public class SimpleMovieLister { private MovieFinder movieFinder; @@ -634,7 +641,7 @@ public class SimpleMovieLister { 如果没有显式的指定名字,默认名字从字段名或setter方法中取得。在字段情况下,它采用字段名称;在setter方法情况下,它采用bean的属性名。因此下面的例子将名字为`movieFinder`的bean注入到它的setter方法中: -```java +``` public class SimpleMovieLister { private MovieFinder movieFinder; @@ -670,10 +677,10 @@ public class MovieRecommender { } ``` -# 8 @PostConstruct和@PreDestroy +#### 3.9.8 @PostConstruct和@PreDestroy `CommonAnnotationBeanPostProcessor`不仅识别`@Resource`注解,而且识别JSR-250生命周期注解。在Spring 2.5引入了对这些注解的支持,也提供了在初始化回调函数和销毁回调函数中描述的那些注解的一种可替代方式。假设`CommonAnnotationBeanPostProcessor`在Spring的`ApplicationContext`中注册,执行这些注解的方法在生命周期的同一点被调用,作为对应的Spring生命周期接口方法或显式声明的回调方法。在下面的例子中,缓存会预先放置接近初始化之前,并在销毁之前清除。 -```java +``` public class CachingMovieLister { @PostConstruct diff --git "a/Spring/SpringFramework/Spring Bean\344\275\234\347\224\250\345\237\237\347\256\241\347\220\206.md" "b/Spring/SpringFramework/Spring-Bean\347\232\204\344\275\234\347\224\250\345\237\237\347\256\241\347\220\206.md" similarity index 100% rename from "Spring/SpringFramework/Spring Bean\344\275\234\347\224\250\345\237\237\347\256\241\347\220\206.md" rename to "Spring/SpringFramework/Spring-Bean\347\232\204\344\275\234\347\224\250\345\237\237\347\256\241\347\220\206.md" diff --git "a/Spring/SpringFramework/Spring Bean\347\224\237\345\221\275\345\221\250\346\234\237\347\256\241\347\220\206.md" "b/Spring/SpringFramework/Spring-Bean\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md" similarity index 54% rename from "Spring/SpringFramework/Spring Bean\347\224\237\345\221\275\345\221\250\346\234\237\347\256\241\347\220\206.md" rename to "Spring/SpringFramework/Spring-Bean\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md" index 0f32a53bfe..daf177acef 100644 --- "a/Spring/SpringFramework/Spring Bean\347\224\237\345\221\275\345\221\250\346\234\237\347\256\241\347\220\206.md" +++ "b/Spring/SpringFramework/Spring-Bean\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md" @@ -1,77 +1,34 @@ -# 1 Spring简介 -- 轻量级容器,提供集中式,自动配置与装配应用业务对象功能 -- 提供统一的事务管理抽象,基于插件式的事务管理(声明性事务管理)能够很容易的实现事务层管理,而无需了解底层事务实现 -- 提供统一的数据访问抽象,包括简单和有效率的JDBC框架,极大的改进了效率(大大减少了开发的代码量)并且减少了可能的错误 +#Spring简介 +- 轻量级的容器,提供集中式,自动配置与装配应用业务对象功能 +- 提供了统一的事务管理抽象,基于插件式的事务管理(声明性事务管理)能够很容易的实现事务层管理,而无需了解各种底层事务实现 +- 提供了统一的数据访问抽象,包括简单和有效率的JDBC框架,极大的改进了效率(大大减少了开发的代码量)并且减少了可能的错误 - Spring的数据访问层集成了Toplink,Hibernate,JDO,and iBATIS SQL Maps等O/R mapping解决方案,其目的是提供统一的DAO支持类实现和事务管理策略 - Spring提供了一个用标准Java编写的AOP框架(也能集成AspectJ),提供基于POJOs的声明式的事务管理和其他企业事务 - 提供可以与IoC容器集成的强大而灵活的MVCWeb框架 -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtNGFmYzBlYjAzMTVhNDBhYS5wbmc?x-oss-process=image/format,png) +![](http://upload-images.jianshu.io/upload_images/4685968-4afc0eb0315a40aa.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -# 2 bean对象生命周期管理 -![生命周期](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtZGJhOTAyYjE0NzlhN2U4Mi5wbmc?x-oss-process=image/format,png) +#bean对象生命周期管理 +![生命周期](http://upload-images.jianshu.io/upload_images/4685968-dba902b1479a7e82.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +1.Spring对Bean进行实例化(相当于程序中的new Class()) +2.Spring将值和Bean的引用注入进Bean对应的属性中 +3.如果Bean实现了BeanNameAware接口,Spring将Bean的ID传递给setBeanName()方法(实现BeanNameAware主要是为了通过Bean的引用来获得Bean的ID,一般业务中是很少有用到Bean的ID的) +4.如果Bean实现了BeanFactoryAware接口,Spring将调用setBeanDactory(BeanFactory bf)方法并把BeanFactory容器实例作为参数传入(实现BeanFactoryAware 主要目的是为了获取Spring容器,如Bean通过Spring容器发布事件等) +5.如果Bean实现了ApplicationContextAwaer接口,Spring容器将调用setApplicationContext(ApplicationContext ctx)方法,把应用上下文作为参数传入(作用与BeanFactory类似都是为了获取Spring容器,不同的是Spring容器在调用setApplicationContext方法时会把它自己作为setApplicationContext 的参数传入,而Spring容器在调用setBeanDactory前需要程序员自己指定(注入)setBeanDactory里的参数BeanFactory ) +6.如果Bean实现了BeanPostProcess接口,Spring将调用它们的postProcessBeforeInitialization(预初始化)方法(作用是在Bean实例创建成功后对进行增强处理,如对Bean进行修改,增加某个功能) +7.如果Bean实现了InitializingBean接口,Spring将调用它们的afterPropertiesSet方法,作用与在配置文件中对Bean使用init-method声明初始化的作用一样,都是在Bean的全部属性设置成功后执行的初始化方法。 +8.如果Bean实现了BeanPostProcess接口,Spring将调用它们的postProcessAfterInitialization(后初始化)方法(作用与6的一样,只不过6是在Bean初始化前执行的,而这个是在Bean初始化后执行的,时机不同 ) +9.经过以上的工作后,Bean将一直驻留在应用上下文中给应用使用,直到应用上下文被销毁 +10.如果Bean实现了DispostbleBean接口,Spring将调用它的destory方法,作用与在配置文件中对Bean使用destory-method属性的作用一样,都是在Bean实例销毁前执行的方法 -## 1 Spring对Bean进行实例化 -相当于new Class() -## 2 Spring将值和Bean的引用注进Bean对应的属性 +# 7 bean的生命周期 -## 3 实现BeanNameAware接口 -- Spring将Bean的ID传递给setBeanName()方法 -- 实现BeanNameAware主要为了通过Bean的引用获得Bean的ID,一般业务中是很少有用到Bean的ID -## 4 实现BeanFactoryAware接口 -- Spring将调用setBeanDactory(BeanFactory bf),并把BeanFactory容器实例作为参数传入 -- 实现BeanFactoryAware 主要为了获取Spring容器,如Bean通过Spring容器发布事件 -## 5 实现ApplicationContextAwaer接口 -- Spring容器将调用setApplicationContext(ApplicationContext ctx),把应用上下文作为参数传入。 -与BeanFactory类似都为获取Spring容器,不同是 -Spring容器在调用setApplicationContext方法时会把它自己作为setApplicationContext 的参数传入 -而Spring容器在调用setBeanDactory前需要程序员自己指定(注入)setBeanDactory里的参数BeanFactory - -## 6 BeanPostProcess接口 -Spring将调用它们的postProcessBeforeInitialization(预初始化)方法,在Bean实例创建成功后对进行增强处理,如对Bean进行修改,增加某个功能。 - -## 7 InitializingBean接口 -InitializingBean接口为bean提供了初始化方法的方式。 -它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候会自动执行该方法。 - -Spring将调用它们的afterPropertiesSet方法,作用与在配置文件中对Bean使用init-method声明初始化的作用一样,都是在Bean的全部属性设置成功后执行的初始化方法。 - -- 实现InitializingBean接口是直接调用afterPropertiesSet方法,比通过反射调用init-method指定的方法相对来说效率要高。但init-method方式消除了对spring的依赖 -- 若调用afterPropertiesSet方法时产生异常,则也不会再调用init-method指定的方法 -```java -package org.springframework.beans.factory; - -/** - * 所有由BeanFactory设置的所有属性都需要响应的bean的接口:例如,执行自定义初始化,或者只是检查所有强制属性是否被设置。 - * 实现InitializingBean的替代方法是指定一个自定义init方法,例如在XML bean定义中。 - */ -public interface InitializingBean { +Spring Bean是Spring应用中最最重要的部分了。所以来看看Spring容器在初始化一个bean的时候会做那些事情,顺序是怎样的,在容器关闭的时候,又会做哪些事情。 - /** - * 在BeanFactory设置了提供的所有bean属性后,由BeanFactory调用。 - *这个方法允许bean实例在所有的bean属性被设置时才能执行 - */ - void afterPropertiesSet() throws Exception; +> spring版本:4.2.3.RELEASE +鉴于Spring源码是用gradle构建的,我也决定舍弃我大maven,尝试下洪菊推荐过的gradle。运行beanLifeCycle模块下的junit test即可在控制台看到如下输出,可以清楚了解Spring容器在创建,初始化和销毁Bean的时候依次做了那些事情。 -} ``` -若class中实现该接口,在Spring Container中的bean生成之后,自动调用函数afterPropertiesSet()。 - -因其实现了InitializingBean接口,其中只有一个方法,且在Bean加载后就执行。该方法可被用来检查是否所有的属性都已设置好。 -## 8 BeanPostProcess接口 -Spring将调用它们的postProcessAfterInitialization(后初始化)方法,作用与6一样,只不过6是在Bean初始化前执行,而这是在Bean初始化后执行。 - - -> 经过以上工作,Bean将一直驻留在应用上下文中给应用使用,直到应用上下文被销毁。 - -## 9 DispostbleBean接口 -- Spring将调用它的destory方法 -- 作用与在配置文件中对Bean使用destory-method属性的作用一样,都是在Bean实例销毁前执行的方法 - - -Spring Bean是Spring应用中最最重要的部分了。所以来看看Spring容器在初始化一个bean的时候会做那些事情,顺序是怎样的,在容器关闭的时候,又会做哪些事情。 - -```bash Spring容器初始化 ===================================== 调用GiraffeService无参构造函数 @@ -91,51 +48,59 @@ GiraffeService中利用set方法设置属性值 Spring容器初始化完毕 ===================================== 从容器中获取Bean -giraffe Name=JavaEdge +giraffe Name=李光洙 ===================================== 调用preDestroy注解标注的方法 执行DisposableBean接口的destroy方法 执行配置的destroy-method Spring容器关闭 ``` + 先来看看,Spring在Bean从创建到销毁的生命周期中可能做得事情。 + # initialization 和 destroy -有时需要在Bean属性值set好后、Bean销毁前搞事情,比如检查Bean中某个属性是否被正常设值。 +有时我们需要在Bean属性值set好之后和Bean销毁之前做一些事情,比如检查Bean中某个属性是否被正常的设置好值 Spring提供了多种方法让我们可以在 Bean 的生命周期中执行initialization和pre-destroy方法 -## 1 实现InitializingBean/DisposableBean接口 -这两个接口都只包含一个方法: -- 实现InitializingBean#afterPropertiesSet(),可在Bean属性值设置好后操作 -- 实现DisposableBean#destroy(),可在销毁Bean前操作 +## **1.实现InitializingBean和DisposableBean接口** + +这两个接口都只包含一个方法。 +- 实现InitializingBean接口的afterPropertiesSet()方法可以在Bean属性值设置好之后做一些操作 +- 实现DisposableBean接口的destroy()方法可以在销毁Bean之前做一些操作。 + ### 案例 + ```java public class GiraffeService implements InitializingBean,DisposableBean { - @Override public void afterPropertiesSet() throws Exception { System.out.println("执行InitializingBean接口的afterPropertiesSet方法"); } - @Override public void destroy() throws Exception { System.out.println("执行DisposableBean接口的destroy方法"); } } ``` -这种使用比较简单,但不推荐,会将Bean实现和Spring框架耦合。 -## 2 bean配置文件指定init-method、destroy-method -Spring允许我们创建自己的 init 方法和 destroy 方法。只要在 Bean 的配置文件中指定 `init-method` 和 `destroy-method` 的值就可以在 Bean 初始化时和销毁之前执行一些操作。 +这种方法比较简单,但是不建议使用。因为这样会将Bean的实现和Spring框架耦合在一起。 + +## **2.在bean的配置文件中指定init-method和destroy-method方法** + +Spring允许我们创建自己的 init 方法和 destroy 方法 + +只要在 Bean 的配置文件中指定 `init-method` 和 `destroy-method` 的值就可以在 Bean 初始化时和销毁之前执行一些操作。 + ### 案例 + ```java public class GiraffeService { - // 通过的destroy-method属性指定的销毁方法 + //通过的destroy-method属性指定的销毁方法 public void destroyMethod() throws Exception { System.out.println("执行配置的destroy-method"); } - - // 通过的init-method属性指定的初始化方法 + //通过的init-method属性指定的初始化方法 public void initMethod() throws Exception { System.out.println("执行配置的init-method"); } @@ -143,25 +108,28 @@ public class GiraffeService { ``` 配置文件中的配置: -```xml + +``` ``` -自定义的init-method和post-method方法可以抛异常,但不能有参数。 -这种方式比较推荐,因为可以自己创建方法,无需将Bean的实现直接依赖于Spring框架。 +需要注意的是自定义的init-method和post-method方法可以抛异常但是不能有参数。 + +这种方式比较推荐,因为可以自己创建方法,无需将Bean的实现直接依赖于spring的框架。 -## @PostConstruct、@PreDestroy -这两个注解均在`javax.annotation` 包。 -Spring 支持用 `@PostConstruct`和 `@PreDestroy`注解指定 `init` 和 `destroy` 方法。 -为使注解生效,需在配置文件中定义 +## **3.使用@PostConstruct和@PreDestroy注解** + +除了xml配置的方式,Spring 也支持用 `@PostConstruct`和 `@PreDestroy`注解来指定 `init` 和 `destroy` 方法。 +这两个注解均在`javax.annotation` 包中。 +为了注解可以生效,需要在配置文件中定义 - org.springframework.context.annotation.CommonAnnotationBeanPostProcessor - 或context:annotation-config ### 案例 + ```java public class GiraffeService { - @PostConstruct public void initPostConstruct(){ System.out.println("执行PostConstruct注解标注的方法"); @@ -172,12 +140,16 @@ public class GiraffeService { } } ``` + 配置文件: + ```xml + + ``` -# 实现Aware接口 -在Bean中使用Spring框架的一些对象 + +# 实现Aware接口 在Bean中使用Spring框架的一些对象 有些时候我们需要在 Bean 的初始化中使用 Spring 框架自身的一些对象来执行一些操作,比如 - 获取 ServletContext 的一些参数 @@ -189,27 +161,8 @@ public class GiraffeService { 这些接口均继承于`org.springframework.beans.factory.Aware`标记接口,并提供一个将由 Bean 实现的set方法,Spring通过基于setter的依赖注入方式使相应的对象可以被Bean使用。 介绍一些重要的Aware接口: -## **ApplicationContextAware** -获得ApplicationContext对象,可以用来获取所有Bean definition的名字。 - -任何希望被通知它运行的ApplicationContext对象要实现的接口。 -例如,当一个对象需要访问一组协作 bean 时。通过 bean 引用进行配置比仅为了bean=查找而实现此接口更有意义! -如果对象需要访问文件资源,即想要调用getResource ,想要发布应用程序事件,或者需要访问 MessageSource,也可以实现此接口。 但是,在这种特定场景中,最好实现更具体的ResourceLoaderAware 、 ApplicationEventPublisherAware或MessageSourceAware接口。 -请注意,文件资源依赖项也可以作为org.springframework.core.io.Resource类型的 bean 属性公开,通过字符串填充,并由 bean 工厂进行自动类型转换。 这消除了为了访问特定文件资源而实现任何回调接口的需要。 -org.springframework.context.support.ApplicationObjectSupport是应用程序对象的一个​​方便的基类,实现了这个接口。 - -实现该接口的类,通过方法setApplicationContext()获得该对象所运行在的ApplicationContext。一般用于初始化object。 -![](https://img-blog.csdnimg.cn/20190702041941650.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -在填充普通 bean 属性之后但在初始化回调之前调用,例如: -- org.springframework.beans.factory.InitializingBean.afterPropertiesSet() -- 或自定义初始化方法 - -在: -- ResourceLoaderAware.setResourceLoader -- ApplicationEventPublisherAware.setApplicationEventPublisher -- 和MessageSourceAware之后调用(如果适用)。 - +- **ApplicationContextAware**: 获得ApplicationContext对象,可以用来获取所有Bean definition的名字。 - **BeanFactoryAware**:获得BeanFactory对象,可以用来检测Bean的作用域。 - **BeanNameAware**:获得Bean在配置文件中定义的名字。 - **ResourceLoaderAware**:获得ResourceLoader对象,可以获得classpath中某个文件。 @@ -258,32 +211,16 @@ public class GiraffeService implements ApplicationContextAware, } } ``` + ### BeanPostProcessor -允许自定义修改新 bean 实例的工厂钩子——如检查标记接口或用代理包装 bean。 - -- 通过标记接口或类似方式填充bean的后置处理器将实现postProcessBeforeInitialization(java.lang.Object,java.lang.String) -- 而用代理包装bean的后置处理器通常会实现postProcessAfterInitialization(java.lang.Object,java.lang.String) -#### Registration -一个ApplicationContext可在其 Bean 定义中自动检测 BeanPostProcessor Bean,并将这些后置处理器应用于随后创建的任何 Bean。 -普通的BeanFactory允许对后置处理器进行编程注册,将它们应用于通过Bean工厂创建的所有Bean。 -#### Ordering -在 ApplicationContext 中自动检测的 OrderBeanPostProcessor Bean 将根据 PriorityOrdered 和 Ordered 语义进行排序。 -相比之下,在BeanFactory以编程方式注册的BeanPostProcessor bean将按注册顺序应用 -对于以编程方式注册的后处理器,通过实现 PriorityOrdered 或 Ordered 接口表达的任何排序语义都将被忽略。 -对于 BeanPostProcessor bean,并不考虑 **@Order** 注解。 -![](https://img-blog.csdnimg.cn/05510c239a6e41a683eb806181b90347.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -Aware接口是针对某个 **实现这些接口的Bean** 定制初始化的过程, -Spring还可针对容器中 **所有Bean** 或 **某些Bean** 定制初始化过程,只需提供一个实现BeanPostProcessor接口的实现类。 - -该接口包含如下方法: -- postProcessBeforeInitialization -在容器中的Bean初始化之前执行 -- postProcessAfterInitialization -在容器中的Bean初始化之后执行 -#### 实例 + +上面的*Aware接口是针对某个实现这些接口的Bean定制初始化的过程, +Spring同样可以针对容器中的所有Bean,或者某些Bean定制初始化过程,只需提供一个实现BeanPostProcessor接口的类即可。 该接口中包含两个方法,postProcessBeforeInitialization和postProcessAfterInitialization。 postProcessBeforeInitialization方法会在容器中的Bean初始化之前执行, postProcessAfterInitialization方法在容器中的Bean初始化之后执行。 + +例子如下: + ```java public class CustomerBeanPostProcessor implements BeanPostProcessor { - @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("执行BeanPostProcessor的postProcessBeforeInitialization方法,beanName=" + beanName); @@ -296,34 +233,44 @@ public class CustomerBeanPostProcessor implements BeanPostProcessor { } } ``` + 要将BeanPostProcessor的Bean像其他Bean一样定义在配置文件中 + ```xml - + ``` + # 总结 -Spring Bean的生命周期 + +所以。。。结合第一节控制台输出的内容,Spring Bean的生命周期是这样的: - Bean容器找到配置文件中 Spring Bean 的定义。 -- Bean容器利用反射创建一个Bean的实例。 -- 如果涉及到一些属性值,利用set方法设置一些属性值。 -- 如果Bean实现了BeanNameAware接口,调用setBeanName()方法,传入Bean的名字 -- 如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例 -- 如果Bean实现了BeanFactoryAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例 +- Bean容器利用Java Reflection API创建一个Bean的实例。 +- 如果涉及到一些属性值 利用set方法设置一些属性值。 +- 如果Bean实现了BeanNameAware接口,调用setBeanName()方法,传入Bean的名字。 +- 如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 +- 如果Bean实现了BeanFactoryAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 - 与上面的类似,如果实现了其他Aware接口,就调用相应的方法。 - 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执行postProcessBeforeInitialization()方法 -- 若Bean实现InitializingBean接口,执行afterPropertiesSet() -- 若Bean定义包含init-method属性,执行指定方法 +- 如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。 +- 如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 - 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执行postProcessAfterInitialization()方法 - 当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy()方法。 - 当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 -![](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC05LTE3LzQ4Mzc2MjcyLmpwZw?x-oss-process=image/format,png) +用图表示一下(图来源:http://www.jianshu.com/p/d00539babca5): + +![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-17/48376272.jpg) 与之比较类似的中文版本: -![](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC05LTE3LzU0OTY0MDcuanBn?x-oss-process=image/format,png) -很多时候我们并不会真的去实现上面说描述的那些接口,那么下面我们就除去那些接口,针对bean的单例和非单例来描述下bean的生命周期: +![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-17/5496407.jpg) + + +**其实很多时候我们并不会真的去实现上面说描述的那些接口,那么下面我们就除去那些接口,针对bean的单例和非单例来描述下bean的生命周期:** + ### 单例管理的对象 + 当scope=”singleton”,即默认情况下,会在启动容器时(即实例化容器时)时实例化。但我们可以指定Bean节点的lazy-init=”true”来延迟初始化bean,这时候,只有在第一次获取bean时才会初始化bean,即第一次请求该bean时才初始化。如下配置: ```xml @@ -363,12 +310,15 @@ public class LifeBean { } } ``` -life.xml配置如下: + life.xml配置如下: + ```xml ``` + 测试代码: + ```java public class LifeTest { @Test @@ -384,7 +334,7 @@ public class LifeTest { 运行结果: -```bash +``` LifeBean()构造函数 this is init of lifeBean com.bean.LifeBean@573f2bb1 @@ -393,13 +343,17 @@ this is destory of lifeBean com.bean.LifeBean@573f2bb1 ``` ### 非单例管理的对象 + 当`scope=”prototype”`时,容器也会延迟初始化 bean,Spring 读取xml 文件的时候,并不会立刻创建对象,而是在第一次请求该 bean 时才初始化(如调用getBean方法时)。在第一次请求每一个 prototype 的bean 时,Spring容器都会调用其构造器创建这个对象,然后调用`init-method`属性值中所指定的方法。对象销毁的时候,Spring 容器不会帮我们调用任何方法,因为是非单例,这个类型的对象有很多个,Spring容器一旦把这个对象交给你之后,就不再管理这个对象了。 -为了测试prototype bean的生命周期,life.xml配置如下: +为了测试prototype bean的生命周期life.xml配置如下: + ```xml ``` + 测试程序: + ```java public class LifeTest { @Test @@ -417,7 +371,7 @@ public class LifeTest { 运行结果: -```bash +``` LifeBean()构造函数 this is init of lifeBean com.bean.LifeBean@573f2bb1 @@ -430,10 +384,10 @@ this is destory of lifeBean com.bean.LifeBean@573f2bb1 可以发现,对于作用域为 prototype 的 bean ,其`destroy`方法并没有被调用。**如果 bean 的 scope 设为prototype时,当容器关闭时,`destroy` 方法不会被调用。对于 prototype 作用域的 bean,有一点非常重要,那就是 Spring不能对一个 prototype bean 的整个生命周期负责:容器在初始化、配置、装饰或者是装配完一个prototype实例后,将它交给客户端,随后就对该prototype实例不闻不问了。** 不管何种作用域,容器都会调用所有对象的初始化生命周期回调方法。但对prototype而言,任何配置好的析构生命周期回调方法都将不会被调用。**清除prototype作用域的对象并释放任何prototype bean所持有的昂贵资源,都是客户端代码的职责**(让Spring容器释放被prototype作用域bean占用资源的一种可行方式是,通过使用bean的后置处理器,该处理器持有要被清除的bean的引用)。谈及prototype作用域的bean时,在某些方面你可以将Spring容器的角色看作是Java new操作的替代者,任何迟于该时间点的生命周期事宜都得交由客户端来处理。 -Spring 容器可以管理 singleton 作用域下 bean 的生命周期,在此作用域下,Spring 能够精确地知道bean何时被创建,何时初始化完成,以及何时被销毁。而对于 prototype 作用域的bean,Spring只负责创建,当容器创建了 bean 的实例后,bean 的实例就交给了客户端的代码管理,Spring容器将不再跟踪其生命周期,并且不会管理那些被配置成prototype作用域的bean的生命周期。 +**Spring 容器可以管理 singleton 作用域下 bean 的生命周期,在此作用域下,Spring 能够精确地知道bean何时被创建,何时初始化完成,以及何时被销毁。而对于 prototype 作用域的bean,Spring只负责创建,当容器创建了 bean 的实例后,bean 的实例就交给了客户端的代码管理,Spring容器将不再跟踪其生命周期,并且不会管理那些被配置成prototype作用域的bean的生命周期。** + +# 参考 -> 参考 -> - https://blog.csdn.net/fuzhongmin05/article/details/73389779 -> - https://yemengying.com/2016/07/14/spring-bean-life-cycle/ -> - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/config/BeanPostProcessor.html \ No newline at end of file +- https://blog.csdn.net/fuzhongmin05/article/details/73389779 +- https://yemengying.com/2016/07/14/spring-bean-life-cycle/ diff --git "a/Spring/SpringFramework/Spring-\344\272\213\345\212\241\347\256\241\347\220\206.md" "b/Spring/SpringFramework/Spring-\344\272\213\345\212\241\347\256\241\347\220\206.md" new file mode 100644 index 0000000000..f24f8e4bd6 --- /dev/null +++ "b/Spring/SpringFramework/Spring-\344\272\213\345\212\241\347\256\241\347\220\206.md" @@ -0,0 +1,42 @@ +# 1 目标 +![](https://upload-images.jianshu.io/upload_images/4685968-eed053d251275855.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 2事务回顾 +## 什么是事务 +![](https://upload-images.jianshu.io/upload_images/4685968-83d81f1c7b75ae07.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## 具体案例 +![](https://upload-images.jianshu.io/upload_images/4685968-1028f8592a327682.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![钱转了,李四却没收到!!!](https://upload-images.jianshu.io/upload_images/4685968-ce870c99003f98c5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![需要事务!!!](https://upload-images.jianshu.io/upload_images/4685968-a102d0e9734d4216.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## 事务的特性 +![](https://upload-images.jianshu.io/upload_images/4685968-3a6300f87c2a1953.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![原子性](https://upload-images.jianshu.io/upload_images/4685968-6cc65f05555dd413.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![一致性](https://upload-images.jianshu.io/upload_images/4685968-f76d2f7cab3ca00c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-889156dab97b9c97.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-226a7bf9ea65ab08.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-ec18c46d3c08b495.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 3 事务的API +## Spring 接口介绍 +![](https://upload-images.jianshu.io/upload_images/4685968-96ed77d556799458.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-01dd1056fff16546.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-2172687122971173.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-5c61cb9956587e6e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## PlatformTransactionManager +![](https://upload-images.jianshu.io/upload_images/4685968-e13c0def9040a4c9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## TransactionDefinition +![](https://upload-images.jianshu.io/upload_images/4685968-d7ca52c82a556b36.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +### 脏读 +![](https://upload-images.jianshu.io/upload_images/4685968-d722077c8547ab50.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +### 不可重复读 +![](https://upload-images.jianshu.io/upload_images/4685968-c78a6f53c4ec2014.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +### 幻读 +![](https://upload-images.jianshu.io/upload_images/4685968-c3a2b2720333ad2b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +### 事务隔离级别 +![](https://upload-images.jianshu.io/upload_images/4685968-d7e2033751124d7b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-133d4393a443117a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-738039998b94ad7f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## TransactionDefinition事务传播行为 +![](https://upload-images.jianshu.io/upload_images/4685968-937d0ca6d13ec7d9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## TransactionStatus + +# 4 环境搭建 +![](https://upload-images.jianshu.io/upload_images/4685968-d705cb27a349cc84.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) diff --git "a/Spring/SpringFramework/SpringHTTP\347\212\266\346\200\201\347\240\201.md" "b/Spring/SpringFramework/SpringHTTP\347\212\266\346\200\201\347\240\201.md" index 0a1d6c6471..ac65c21c7e 100644 --- "a/Spring/SpringFramework/SpringHTTP\347\212\266\346\200\201\347\240\201.md" +++ "b/Spring/SpringFramework/SpringHTTP\347\212\266\346\200\201\347\240\201.md" @@ -1,4 +1,3 @@ -```java /* * Copyright 2002-2017 the original author or authors. * @@ -540,4 +539,3 @@ public enum HttpStatus { } } -``` \ No newline at end of file diff --git "a/Spring/SpringFramework/SpringMVC\346\213\246\346\210\252\345\244\204\347\220\206\345\231\250.md" "b/Spring/SpringFramework/SpringMVC-\347\232\204\345\244\204\347\220\206\346\213\246\346\210\252\345\231\250.md" similarity index 58% rename from "Spring/SpringFramework/SpringMVC\346\213\246\346\210\252\345\244\204\347\220\206\345\231\250.md" rename to "Spring/SpringFramework/SpringMVC-\347\232\204\345\244\204\347\220\206\346\213\246\346\210\252\345\231\250.md" index 55df7dae0c..dea63a4a11 100644 --- "a/Spring/SpringFramework/SpringMVC\346\213\246\346\210\252\345\244\204\347\220\206\345\231\250.md" +++ "b/Spring/SpringFramework/SpringMVC-\347\232\204\345\244\204\347\220\206\346\213\246\346\210\252\345\231\250.md" @@ -1,21 +1,27 @@ -# 1 工作原理流程图 -![](https://img-blog.csdnimg.cn/img_convert/e0096bfcb38005ce8fe0e648a274777b.png) -# 2 Spring Web MVC 的处理器拦截器 -- HandlerInterceptor -![](https://img-blog.csdnimg.cn/5940620ea2f342e99890b8c551300f73.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -类似Servlet 开发中的过滤器Filter,用于对处理器进行预处理和后处理。 +#0 目录 +![](http://upload-images.jianshu.io/upload_images/4685968-0e96e02f95af818c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](http://upload-images.jianshu.io/upload_images/4685968-14563a0520de7e61.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](http://upload-images.jianshu.io/upload_images/4685968-48c508160847ed78.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](http://upload-images.jianshu.io/upload_images/4685968-060046ab31685f80.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + ![](http://upload-images.jianshu.io/upload_images/4685968-174b6d94bc2397c2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 1 Spring MVC拦截器流程图 +![](http://upload-images.jianshu.io/upload_images/4685968-ca4e9021f653c954.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +#2 Spring Web MVC 的处理器拦截器 +![](https://upload-images.jianshu.io/upload_images/4685968-1b14ff00f1e4ab85.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -HandlerInterceptor接口定义了如下方法: -## preHandle -该方法将在**请求处理之前进行调用**,只有当该方法返回true时,才会继续调用下一个`Interceptor`的`preHandle()`,如果已是最后一个`Interceptor`就会是调用当前请求的`Controller` -## postHandle +类似于Servlet 开发中的过滤器Filter,用于对处理器进行预处理和后处理 + +HandlerInterceptor接口定义了三个方法 +##2.1 preHandle +该方法将在**请求处理之前进行调用**,只有当该方法返回true时,才会继续调用下一个`Interceptor`的`preHandle()`,如果已是最后一个`Interceptor`就会是调用当前请求的`Controller` +##2.2 postHandle 该方法将在请求处理后,`DispatcherServlet`进行视图返回**渲染之前进行调用**,可以在这个方法中对`Controller`处理之后的`ModelAndView`对象进行操作(比如这里加入公用信息以便页面显示) -## 2.3 afterCompletion +##2.3 afterCompletion 该方法也是需要当前对应的`Interceptor`的`preHandle`方法的返回值为`true`时才会执行,该方法将在整个请求结束之后,也就是在`DispatcherServlet` 渲染了对应的视图之后执行 **用于资源清理** -# 3 拦截器配置 -## 3.1 针对某种mapping拦截器配置 +#3 拦截器配置 +##3.1 针对某种mapping拦截器配置 ```xml @@ -29,7 +35,7 @@ HandlerInterceptor接口定义了如下方法: ``` -## 3.2 针对所有mapping配置全局拦截器 +##3.2 针对所有mapping配置全局拦截器 ```xml @@ -44,7 +50,7 @@ HandlerInterceptor接口定义了如下方法: ``` -# 4 实践 +#4 实践 用户访问其他页面时,从Seesion中获取到用户,未登录则重定向到登录页面。 ```java Public class LoginInterceptor implements HandlerInterceptor{ @@ -66,4 +72,4 @@ Public class LoginInterceptor implements HandlerInterceptor{ return false; } -``` \ No newline at end of file +``` diff --git "a/Spring/SpringFramework/Spring\347\232\204IoC\343\200\201AOP.md" "b/Spring/SpringFramework/Spring\347\232\204IoC\343\200\201AOP.md" deleted file mode 100644 index 27a2795c12..0000000000 --- "a/Spring/SpringFramework/Spring\347\232\204IoC\343\200\201AOP.md" +++ /dev/null @@ -1,206 +0,0 @@ -Spring AOP通过CGlib、JDK动态代理实现运行期的动态方法增强,以抽取出业务无关代码,使其不与业务代码耦合,从而降低系统耦合性,提高代码可重用性和开发效率。 -所以AOP广泛应用在日志记录、监控管理、性能统计、异常处理、权限管理、统一认证等方面。 - -# 单例Bean如何注入Prototype的Bean? - -要为单例Bean注入Prototype Bean,不只是修改Scope属性。由于单例Bean在容器启动时就会完成一次性初始化。所以最简单的,把Prototype Bean设置为通过代理注入,即把proxyMode属性设为**TARGET_CLASS**。 - -比如抽象类LearnService,可认为是有状态的,如果LearnService是单例的话,那必然会OOM -![](https://img-blog.csdnimg.cn/20210512164658255.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -实际开发时,很多人不假思索把LearnGo和LearnJava类加上 **@Service**,让它们成为Bean,也没有考虑到父类其实有状态: -```java -@Service -@Slf4j -public class LearnJava extends LearnService { - - @Override - public void learn() { - super.learn(); - log.info("java"); - } -} - -@Service -@Slf4j -public class LearnGo extends LearnService { - - @Override - public void learn() { - super.learn(); - log.info("go"); - } -} -``` -相信大多数同学都认为 **@Service** 的意义就在于Spring能通过 **@Autowired** 自动注入对象,就比如可以直接使用注入的List获取到LearnJava和LearnGo,而没想过类的生命周期: -```java -@Autowired -List learnServiceList; - -@GetMapping("test") -public void test() { - log.info("===================="); - learnServiceList.forEach(LearnService::learn); -} -``` -- 当年开发父类的人将父类设计为有状态的,但不关心子类怎么使用父类的 -- 而开发子类的同学,没多想就直接添加 **@Service**,让类成为Bean,再通过 **@Autowired**注入该服务 -这样设置后,有状态的父类就可能产生内存泄漏或线程安全问题。 - -## 最佳实践 - -在给类添加 **@Service**把类型交由容器管理前,首先考虑类是否有状态,再为Bean设置合适Scope。 -比如该案例,就为我们的两个类添加 **@Scope**即可,设为**PROTOTYPE**生命周期: -![](https://img-blog.csdnimg.cn/20210512170638576.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -但这样还是会内存泄漏,说明修改无用! - -观察日志可得,第一次调用和第二次调用时,SayBye对象都是4c0bfe9e,SayHello也是一样问题。从日志第7到10行还可以看到,第二次调用后List的元素个数变为了2,说明父类SayService维护的List在不断增长,不断调用必然出现OOM: -```java -[17:12:45.798] [http-nio-30666-exec-1] [INFO ] [c.j.s.beansingletonandorder.LearnService:26 ] - I'm com.javaedge.springpart1.beansingletonandorder.LearnGo@4a5fe6a2 size:1 -[17:12:45.798] [http-nio-30666-exec-1] [INFO ] [c.j.s.beansingletonandorder.LearnGo:20 ] - go -[17:12:45.839] [http-nio-30666-exec-1] [INFO ] [c.j.s.beansingletonandorder.LearnService:26 ] - I'm com.javaedge.springpart1.beansingletonandorder.LearnJava@6cb46b0 size:1 -[17:12:45.840] [http-nio-30666-exec-1] [INFO ] [c.j.s.beansingletonandorder.LearnJava:17 ] - java -[17:12:57.380] [http-nio-30666-exec-2] [INFO ] [c.j.s.b.BeanSingletonAndOrderController:25 ] - ==================== -[17:12:57.416] [http-nio-30666-exec-2] [INFO ] [c.j.s.beansingletonandorder.LearnService:26 ] - I'm com.javaedge.springpart1.beansingletonandorder.LearnGo@b859c00 size:2 -[17:12:57.416] [http-nio-30666-exec-2] [INFO ] [c.j.s.beansingletonandorder.LearnGo:20 ] - go -[17:12:57.452] [http-nio-30666-exec-2] [INFO ] [c.j.s.beansingletonandorder.LearnService:26 ] - I'm com.javaedge.springpart1.beansingletonandorder.LearnJava@5426300 size:2 -[17:12:57.452] [http-nio-30666-exec-2] [INFO ] [c.j.s.beansingletonandorder.LearnJava:17 ] - java -``` -所以,问题就是: -### 单例Bean如何注入Prototype Bean? -Controller标记了 **@RestController** -**@RestController** = **@Controller** + **@ResponseBody**,又因为 **@Controller**标记了 **@Component**元注解,所以 **@RestController**也是一个Spring Bean。 - -Bean默认是单例的,所以单例的Controller注入的Service也是一次性创建的,即使Service本身标识了**prototype**的范围,也不会起作用。 - -修复方案就是让Service以代理方式注入。这样虽然Controller是单例的,但每次都能从代理获取Service。这样一来,prototype范围的配置才能真正生效: -![](https://img-blog.csdnimg.cn/20210512172431727.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -调试发现,注入的Service都是Spring生成的代理类: -![](https://img-blog.csdnimg.cn/20210512172721106.png) - -如果不希望走代理,还有一种方案,每次直接从ApplicationContext中获取Bean: -![](https://img-blog.csdnimg.cn/20210512172928970.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -这里Spring注入的LearnService的List,第一个元素是LearnGo,第二个元素是LearnJava。但我们更希望的是先执行Java再执行Go,所以注入一个List Bean时, 还要能控制Bean的顺序。 - -一般来说,顺序如何都无所谓,但对AOP,顺序可能会引发致命问题。 -# 监控切面顺序导致的Spring事务失效 -通过AOP实现一个整合日志记录、异常处理和方法耗时打点为一体的统一切面。但后来发现,使用了AOP切面后,这个应用的声明式事务处理居然都是无效的。 - - -现在分析AOP实现的监控组件和事务失效有什么关系,以及通过AOP实现监控组件是否还有其他坑。 - -先定义一个自定义注解Metrics,打上该注解的方法可以实现各种监控功能: -![](https://img-blog.csdnimg.cn/20210512190554206.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -然后,实现一个切面完成Metrics注解提供的功能。这个切面可以实现标记了@RestController注解的Web控制器的自动切入,如果还需要对更多Bean进行切入的话,再自行标记@Metrics注解。 - -测试MetricsAspect的功能。 - -Service中实现创建用户的时候做了事务处理,当用户名包含test字样时会抛出异常,导致事务回滚。为Service中的createUser添加@Metrics注解。 -还可以手动为类或方法添加@Metrics注解,实现Controller之外的其他组件的自动监控。 - -```java -@Slf4j -@RestController //自动进行监控 -@RequestMapping("metricstest") -public class MetricsController { - @Autowired - private UserService userService; - @GetMapping("transaction") - public int transaction(@RequestParam("name") String name) { - try { - userService.createUser(new UserEntity(name)); - } catch (Exception ex) { - log.error("create user failed because {}", ex.getMessage()); - } - return userService.getUserCount(name); - } -} - -@Service -@Slf4j -public class UserService { - @Autowired - private UserRepository userRepository; - @Transactional - @Metrics //启用方法监控 - public void createUser(UserEntity entity) { - userRepository.save(entity); - if (entity.getName().contains("test")) - throw new RuntimeException("invalid username!"); - } - - public int getUserCount(String name) { - return userRepository.findByName(name).size(); - } -} - -@Repository -public interface UserRepository extends JpaRepository { - List findByName(String name); -} -``` - -使用用户名“test”测试一下注册功能,自行测试可以观察到日志中打出了整个调用的出入参、方法耗时: - -但之后性能分析觉得默认的 **@Metrics**配置不太好,优化点: -- Controller的自动打点,不要自动记录入参和出参日志,避免日志量过大 -- Service中的方法,最好可以自动捕获异常 - -优化调整: -- MetricsController手动添加 **@Metrics**,设置logParameters和logReturn为false -![](https://img-blog.csdnimg.cn/20210513134317508.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -- Service中的createUser方法的@Metrics注解,设置了ignoreException属性为true -![](https://img-blog.csdnimg.cn/20210513134421950.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -可是实际上线发现日志量并没有减少,而且事务回滚还失效了,从输出看到最后查询到了名为test的用户。 - -执行Service的createUser方法时,Spring 的 TransactionAspectSupport并没有捕获到异常,所以自然无法回滚事务。因为异常被MetricsAspect吞了。 - -切面本身是一个Bean,Spring对不同切面增强的执行顺序是由Bean优先级决定的,具体规则是: -- 入操作(Around(连接点执行前)、Before),切面优先级越高,越先执行 -一个切面的入操作执行完,才轮到下一切面,所有切面入操作执行完,才开始执行连接点(方法) -- 出操作(Around(连接点执行后)、After、AfterReturning、AfterThrowing) -切面优先级越低,越先执行。一个切面的出操作执行完,才轮到下一切面,直到返回到调用点。 -- 同一切面的Around比After、Before先执行 - -对于Bean可以通过 **@Order** 设置优先级:默认情况下Bean的优先级为最低优先级,其值是Integer的最大值。值越大优先级越低。 -![](https://img-blog.csdnimg.cn/20210513142450888.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20210513142719486.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -## 通知 -### 执行顺序 -新建一个TestAspectWithOrder10切面,通过 **@Order**注解设置优先级为10,做简单的日志输出,切点是TestController所有方法; -![](https://img-blog.csdnimg.cn/20210513143248251.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -然后再定义一个类似的TestAspectWithOrder20切面,设置优先级为20: -![](https://img-blog.csdnimg.cn/20210513143327567.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -调用TestController的方法,观察日志输出: -```bash -TestAspectWithOrder10 @Around before -TestAspectWithOrder10 @Before -TestAspectWithOrder20 @Around before -TestAspectWithOrder20 @Before -TestAspectWithOrder20 @Around after -TestAspectWithOrder20 @After -TestAspectWithOrder10 @Around after -TestAspectWithOrder10 @After -``` -Spring的事务管理同样基于AOP,默认,优先级最低,会先执行出操作,但自定义切面MetricsAspect默认情况下也是最低优先级。 -这时就会产生问题:若出操作先执行捕获了异常,则Spring事务就会因为无法catch异常而无法回滚。 - -所以要指定MetricsAspect的优先级,可设置为最高优先级,即最先执行入操作最后执行出操作: -![](https://img-blog.csdnimg.cn/20210513150407535.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -切入的连接点是方法,注解定义在类上是无法直接从方法上获取到注解的。所以要改为优先从方法获取,若方法上获取不到再从类获取,若还是获取不到则使用默认注解: -```java -Metrics metrics = signature.getMethod().getAnnotation(Metrics.class); -if (metrics == null) { - metrics = signature.getMethod().getDeclaringClass().getAnnotation(Metrics.class); -} -``` -修正完后,事务就可以正常回滚了,并且Controller的监控日志也不再出现入参、出参。 - -监控平台如果想生产可用,需修改: -- 日志打点,改为对接Metrics监控系统 -- 各种监控开关,从注解属性获取改为通过配置中心实时获取 \ No newline at end of file diff --git "a/Spring/SpringFramework/Spring\347\274\226\347\250\213\345\274\217\344\272\213\345\212\241.md" "b/Spring/SpringFramework/Spring\347\274\226\347\250\213\345\274\217\344\272\213\345\212\241.md" deleted file mode 100644 index a26d65f407..0000000000 --- "a/Spring/SpringFramework/Spring\347\274\226\347\250\213\345\274\217\344\272\213\345\212\241.md" +++ /dev/null @@ -1,218 +0,0 @@ -为了更细粒度的事务划分,Spring提供如下两种方式的编程式事务管理: -# 1 PlatformTransactionManager -你也可以使用 org.springframework.transaction.PlatformTransactionManager 来直接管理你的事务。只需通过bean的引用,简单的把你在使用的PlatformTransactionManager 传递给你的bean。 然后,使用TransactionDefinition和TransactionStatus对象, 你可以启动,回滚和提交事务。 - -```java -DefaultTransactionDefinition def = new DefaultTransactionDefinition(); -// explicitly setting the transaction name is something that can only be done programmatically -def.setName("SomeTxName"); -def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); - -TransactionStatus status = txManager.getTransaction(def); -try { - // 执行业务逻辑 -} -catch (MyException ex) { - txManager.rollback(status); - throw ex; -} -txManager.commit(status); -``` - -# 2 TransactionTemplate -PlatformTransactionManager中部分代码是可以重用的,所以spring对其进行了优化,采用模板方法模式就其进行封装,主要省去了提交或者回滚事务的代码。 - -若你选择编程式事务管理,Spring推荐使用 TransactionTemplate。 类似使用JTA的 UserTransaction API (除了异常处理的部分稍微简单点)。 -## 2.1 简介 -TransactionTemplate 采用与Spring中别的模板同样的方法,如 JdbcTemplate 。 -使用回调机制,将应用代码从样板式的资源获取和释放代码中解放。 - -同一个事务管理的代码调用逻辑中,每次执行 SQL,都是基于同一连接,所有连接都是从一个公共地方TransactionSynchronizationManager获取。 -所以获取连接操作不应该在 ORM 层框架,而是由 Spring 维护。 -### JdbcTemplate源码 -![](https://img-blog.csdnimg.cn/20210704183829725.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -![](https://img-blog.csdnimg.cn/20210704183726428.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -```java -@Override -@Nullable -public T execute(ConnectionCallback action) throws DataAccessException { - Assert.notNull(action, "Callback object must not be null"); - - Connection con = DataSourceUtils.getConnection(obtainDataSource()); - try { - // Create close-suppressing Connection proxy, also preparing returned Statements. - Connection conToUse = createConnectionProxy(con); - return action.doInConnection(conToUse); - } - catch (SQLException ex) { - // Release Connection early, to avoid potential connection pool deadlock - // in the case when the exception translator hasn't been initialized yet. - String sql = getSql(action); - DataSourceUtils.releaseConnection(con, getDataSource()); - con = null; - throw translateException("ConnectionCallback", sql, ex); - } - finally { - DataSourceUtils.releaseConnection(con, getDataSource()); - } -} -``` - -从给定的数据源获取连接。 知道绑定到当前线程的相应连接,例如在使用DataSourceTransactionManager 。 如果事务同步处于活动状态,例如在JTA事务中运行时,则将连接绑定到线程。 -![](https://img-blog.csdnimg.cn/20210704182450795.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -mybatis 中的各种 Mapper,其实底层就是通过使用 sqlSession 执行的。 -而不管是 jdbcTemplate 还是 mybatis,获取连接都是通过 Spring的TransactionSynchronizationManager: -![](https://img-blog.csdnimg.cn/20210704201230242.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -#### 为何使用ThreadLocal存储资源? -若一个线程请求事务处理过程还在正常进行,但同时另一个线程并发也请求了同一事务的处理方法而且出现了异常要回滚了,若两个线程共用同一个数据库连接,就会导致回滚时影响原来正常执行的线程!所以连接对象是使用 ThreadLocal 存储的。就是为了避免多线程共用同一个连接,导致回滚时出现数据错乱。 -![](https://img-blog.csdnimg.cn/20210704202442776.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - - -> 使用 TransactionTemplate 会增加你的代码与Spring的事务框架和API间的耦合。所以是否使用编程式事务管理由你自己决定。 - -应用代码必须在一个事务性的上下文中执行,这样就会像这样一样显式的使用TransactionTemplate。作为应用程序员, 会写一个 TransactionCallback 的实现, (通常会用匿名类实现 )这样的实现会包含所以你需要在该事务上下文中执行的代码。 -然后你会把一个你自己实现TransactionCallback的实例传递给TransactionTemplate暴露的execute 方法。 -```java -public class SimpleService implements Service { - - // single TransactionTemplate shared amongst all methods in this instance - private final TransactionTemplate transactionTemplate; - - // use constructor-injection to supply the PlatformTransactionManager - public SimpleService(PlatformTransactionManager transactionManager) { - Assert.notNull(transactionManager, "The 'transactionManager' argument must not be null."); - this.transactionTemplate = new TransactionTemplate(transactionManager); - } - - public Object someServiceMethod() { - return transactionTemplate.execute(new TransactionCallback() { - - // the code in this method executes in a transactional context - public Object doInTransaction(TransactionStatus status) { - updateOperation1(); - return resultOfUpdateOperation2(); - } - }); - } -} -``` -如果不需要返回值,更方便的是创建一个 TransactionCallbackWithoutResult 匿名类 -```java -transactionTemplate.execute(new TransactionCallbackWithoutResult() { - @Override - protected void doInTransactionWithoutResult(TransactionStatus status) { - updateOperation1(); - updateOperation2(); - } -}); -``` - -回调方法内的代码可以通过调用 TransactionStatus 对象的 setRollbackOnly() 方法来回滚事务。 - -```java -transactionTemplate.execute(new TransactionCallbackWithoutResult() { - - protected void doInTransactionWithoutResult(TransactionStatus status) { - try { - updateOperation1(); - updateOperation2(); - } catch (SomeBusinessExeption ex) { - status.setRollbackOnly(); - } - } -}); -``` - -## 2.2 指定事务设置 -诸如传播模式、隔离等级、超时等等的事务设置都可以在TransactionTemplate中或者通过配置或者编程式地实现。 -TransactionTemplate实例默认继承了默认事务设置。 下面有个编程式的为一个特定的TransactionTemplate定制事务设置的例子。 -```java -public class SimpleService implements Service { - - private final TransactionTemplate transactionTemplate; - - public SimpleService(PlatformTransactionManager transactionManager) { - Assert.notNull(transactionManager, "The 'transactionManager' argument must not be null."); - this.transactionTemplate = new TransactionTemplate(transactionManager); - - // the transaction settings can be set here explicitly if so desired - this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED); - this.transactionTemplate.setTimeout(30); // 30 seconds - // and so forth... - } -} -``` - -使用Spring XML配置来定制TransactionTemplate的事务属性。 sharedTransactionTemplate 可以被注入到所有需要的服务中去。 -```xml - - - -" -``` -TransactionTemplate 类的实例是线程安全的,任何状态都不会被保存。 TransactionTemplate 实例的确会维护配置状态,所以当一些类选择共享一个单独的 TransactionTemplate实例时,如果一个类需要使用不同配置的TransactionTemplate(比如,不同的隔离等级), 那就需要创建和使用两个不同的TransactionTemplate。 -## 2.3 案例 - -```java -@Test -public void test1() throws Exception { - //定义一个数据源 - org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource(); - dataSource.setDriverClassName("com.mysql.jdbc.Driver"); - dataSource.setUrl("jdbc:mysql://localhost:3306/mydb?characterEncoding=UTF-8"); - dataSource.setUsername("root"); - dataSource.setPassword("root123"); - dataSource.setInitialSize(5); - //定义一个JdbcTemplate,用来方便执行数据库增删改查 - JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); - //1.定义事务管理器,给其指定一个数据源(可以把事务管理器想象为一个人,这个人来负责事务的控制操作) - PlatformTransactionManager platformTransactionManager = new DataSourceTransactionManager(dataSource); - //2.定义事务属性:TransactionDefinition,TransactionDefinition可以用来配置事务的属性信息,比如事务隔离级别、事务超时时间、事务传播方式、是否是只读事务等等。 - DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition(); - transactionDefinition.setTimeout(10);//如:设置超时时间10s - //3.创建TransactionTemplate对象 - TransactionTemplate transactionTemplate = new TransactionTemplate(platformTransactionManager, transactionDefinition); - /** - * 4.通过TransactionTemplate提供的方法执行业务操作 - * 主要有2个方法: - * (1).executeWithoutResult(Consumer action):没有返回值的,需传递一个Consumer对象,在accept方法中做业务操作 - * (2). T execute(TransactionCallback action):有返回值的,需要传递一个TransactionCallback对象,在doInTransaction方法中做业务操作 - * 调用execute方法或者executeWithoutResult方法执行完毕之后,事务管理器会自动提交事务或者回滚事务。 - * 那么什么时候事务会回滚,有2种方式: - * (1)transactionStatus.setRollbackOnly();将事务状态标注为回滚状态 - * (2)execute方法或者executeWithoutResult方法内部抛出异常 - * 什么时候事务会提交? - * 方法没有异常 && 未调用过transactionStatus.setRollbackOnly(); - */ - transactionTemplate.executeWithoutResult(new Consumer() { - @Override - public void accept(TransactionStatus transactionStatus) { - jdbcTemplate.update("insert into t_user (name) values (?)", "transactionTemplate-1"); - jdbcTemplate.update("insert into t_user (name) values (?)", "transactionTemplate-2"); - - } - }); - System.out.println("after:" + jdbcTemplate.queryForList("SELECT * from t_user")); -} - -output: -after:[{id=1, name=transactionTemplate-1}, {id=2, name=transactionTemplate-2}] -``` -### executeWithoutResult:无返回值场景 -executeWithoutResult(Consumer action):没有返回值的,需传递一个Consumer对象,在accept方法中做业务操作 -![](https://img-blog.csdnimg.cn/baaf82eaee1949ce852206771682c3dc.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -```java -transactionTemplate.executeWithoutResult(new Consumer() { - @Override - public void accept(TransactionStatus transactionStatus) { - //执行业务操作 - } -}); -``` -### execute:有返回值场景 -T execute(TransactionCallback action):有返回值的,需要传递一个TransactionCallback对象,在doInTransaction方法中做业务操作 \ No newline at end of file diff --git "a/Spring/SpringFramework/\344\270\272\344\273\200\344\271\210private\346\226\271\346\263\225\345\212\240\344\272\206@Transactional\357\274\214\344\272\213\345\212\241\344\271\237\346\262\241\346\234\211\347\224\237\346\225\210\357\274\237.md" "b/Spring/SpringFramework/\344\270\272\344\273\200\344\271\210private\346\226\271\346\263\225\345\212\240\344\272\206@Transactional\357\274\214\344\272\213\345\212\241\344\271\237\346\262\241\346\234\211\347\224\237\346\225\210\357\274\237.md" deleted file mode 100644 index 72d83a972c..0000000000 --- "a/Spring/SpringFramework/\344\270\272\344\273\200\344\271\210private\346\226\271\346\263\225\345\212\240\344\272\206@Transactional\357\274\214\344\272\213\345\212\241\344\271\237\346\262\241\346\234\211\347\224\237\346\225\210\357\274\237.md" +++ /dev/null @@ -1,75 +0,0 @@ -现在产品期望用户创建和保存逻辑分离:把User实例的创建和保存逻辑拆到两个方法分别进行。 -然后,把事务的注解 **@Transactional** 加在保存数据库的方法上。 -![](https://img-blog.csdnimg.cn/2f29be1be44f4029a8ed70152d34de03.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -执行程序,异常正常抛出 -![](https://img-blog.csdnimg.cn/5952728511b443f38eb79cb3aae0f0b9.png) -事务未回滚 -![](https://img-blog.csdnimg.cn/4c7e43e4f9944f1fac26d6a7ff83342e.png) -# 源码解析 - debug: -![](https://img-blog.csdnimg.cn/dcfa7c2e44ca4048b5f889b4ab14a542.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -前一段是 Spring 创建 Bean 的过程。当 Bean 初始化之后,开始尝试代理操作,这是从如下方法开始处理的: -### AbstractAutoProxyCreator#postProcessAfterInitialization -```java -public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { - if (bean != null) { - Object cacheKey = getCacheKey(bean.getClass(), beanName); - if (this.earlyProxyReferences.remove(cacheKey) != bean) { - return wrapIfNecessary(bean, beanName, cacheKey); - } - } - return bean; -} -``` -继续 debug,直到 -### AopUtils#canApply -针对切面定义里的条件,确定这个方法是否可被应用创建成代理。 -有段 `methodMatcher.matches(method, targetClass)` 判断这个方法是否符合这样的条件: -```java -public static boolean canApply(Pointcut pc, Class targetClass, boolean hasIntroductions) { - // ... - for (Class clazz : classes) { - Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz); - for (Method method : methods) { - if (introductionAwareMethodMatcher != null ? - introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) : - methodMatcher.matches(method, targetClass)) { - return true; - } - } - } - return false; -} -``` -![](https://img-blog.csdnimg.cn/f6729281ab6443cca152a37c2b1dbb76.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -从 matches() 调用到 -##### AbstractFallbackTransactionAttributeSource#getTransactionAttribute -获取注解中的事务属性,根据属性确定事务的策略。 -![](https://img-blog.csdnimg.cn/7996b41bf8054f28be00f67fd5473cdc.png) -接着调用到 -#### computeTransactionAttribute -根据方法和类的类型确定是否返回事务属性: -![](https://img-blog.csdnimg.cn/ab5481cb131f41b4956021c582b4a78a.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -当上图中条件判断结果为 true,则返回 null,表明该方法不会被代理,从而导致事务注解不会生效。 - -那到底是不是 true 呢? -##### 条件1:allowPublicMethodsOnly() -AnnotationTransactionAttributeSource#publicMethodsOnly属性值 -![](https://img-blog.csdnimg.cn/108a1106eb5946ecab4e182bef6de966.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -publicMethodsOnly 是通过 AnnotationTransactionAttributeSource 的构造方法初始化的,默认为 true。 -![](https://img-blog.csdnimg.cn/591bfd6571814ed29b0b610021724da0.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -##### 条件2:Modifier.isPublic() -根据传入的 `method.getModifiers()` 获取方法的修饰符,该修饰符是 java.lang.reflect.Modifier 的静态属性,对应的几类修饰符分别是: -- PUBLIC: 1 -- PRIVATE: 2 -- PROTECTED: 4 - -这里做了一个位运算,只有当传入的方法修饰符是 public 类型的时候,才返回 true -![](https://img-blog.csdnimg.cn/adafc6c4f16d44478625252d9ac12006.png) -综上两个条件,只有当注解为事务方法为 public 才会被 Spring 处理。 -# 修正 -只需将修饰符从 private 改成 public,其实该问题 IDEA 也会告警,一般都会避免。 -![](https://img-blog.csdnimg.cn/ee7ded3ee775473a8094d28a1d2d0e08.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -调用这个加了事务注解的方法,必须是调用被 Spring AOP 代理过的方法:不能通过类的内部调用或通过 this 调用。 -所以我们的案例的UserService,它Autowired了自身(UserService)的一个实例来完成代理方法的调用。 \ No newline at end of file diff --git "a/Spring/SpringFramework/\344\270\272\344\273\200\344\271\210\345\212\240\344\272\206@WebFilter\346\263\250\350\247\243\357\274\214Spring\345\215\264\346\262\241\346\234\211\347\273\231\346\210\221\350\207\252\345\212\250\346\263\250\345\205\245\350\257\245\350\277\207\346\273\244\345\231\250?.md" "b/Spring/SpringFramework/\344\270\272\344\273\200\344\271\210\345\212\240\344\272\206@WebFilter\346\263\250\350\247\243\357\274\214Spring\345\215\264\346\262\241\346\234\211\347\273\231\346\210\221\350\207\252\345\212\250\346\263\250\345\205\245\350\257\245\350\277\207\346\273\244\345\231\250?.md" deleted file mode 100644 index c0f3fcec3b..0000000000 --- "a/Spring/SpringFramework/\344\270\272\344\273\200\344\271\210\345\212\240\344\272\206@WebFilter\346\263\250\350\247\243\357\274\214Spring\345\215\264\346\262\241\346\234\211\347\273\231\346\210\221\350\207\252\345\212\250\346\263\250\345\205\245\350\257\245\350\277\207\346\273\244\345\231\250?.md" +++ /dev/null @@ -1,82 +0,0 @@ -在 Spring 编程中,主要配合如下注解构建过滤器: -- @ServletComponentScan -- @WebFilter - -那这看起来只是用上这俩注解就能继续摸鱼了呀。但上了生产后,还是能遇到花式问题: -- 工作不起来 -- 顺序不对 -- 执行多次等 - -大多因为想当然觉得使用简单,没有上心。还是有必要精通过滤器执行的流程和原理。 -# @WebFilter 过滤器无法被自动注入 -为统计接口耗时,实现一个过滤器: -![](https://img-blog.csdnimg.cn/e1f8f6ce7b4c47cab322388c54309807.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -该过滤器标记了 **@WebFilter**。所以启动程序加上扫描注解 **@ServletComponentScan** 让其生效: -![](https://img-blog.csdnimg.cn/6aef56e0ecf742aba96c9a3a83353992.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -然后,提供一个 UserController: -![](https://img-blog.csdnimg.cn/6a4d432b3f9b4232a8abf44e8c35a697.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -发现应用启动失败 -![](https://img-blog.csdnimg.cn/7d5b595b43024955aa17314dd55f7890.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -TimeCostFilter 看起来是个普通 Bean啊,为何不能被自动注入? -# 源码解析 -本质上,过滤器被 **@WebFilter** 修饰后,TimeCostFilter 只会被包装为 FilterRegistrationBean,而 TimeCostFilter 本身只会作为一个 InnerBean 被实例化,这意味着 TimeCostFilter 实例并不会作为 Bean 注册到 Spring 容器。 -![](https://img-blog.csdnimg.cn/6af1a896b40b48e7997aab35fe393ef1.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -所以当我们想自动注入 TimeCostFilter 时,就会失败。知道这个结论后,我们可以带着两个问题去理清一些关键的逻辑: -### FilterRegistrationBean 是什么?它是如何被定义的 -javax.servlet.annotation.WebFilter -![](https://img-blog.csdnimg.cn/d50c1424b7ca4aedaeb5c176d108246c.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -所以它不属 Spring,而是 Servlet 规范。 -Spring Boot 项目使用它时,Spring Boot 使用了 `org.springframework.boot.web.servlet.FilterRegistrationBean` 包装 @WebFilter 标记的实例。 -实现上来说,即 FilterRegistrationBean#Filter 属性就是 @WebFilter 标记的实例。这点我们可以从之前给出的截图中看出端倪。 - -定义一个 Filter 类时,我们可能想的是,会自动生成它的实例,然后以 Filter 的名称作为 Bean 名来指向它。 -但调试发现,在 Spring Boot 中,Bean 名字确实是对的,只是 Bean 实例其实是 FilterRegistrationBean。 - -> 这 FilterRegistrationBean 最早是如何获取的呢? - -得追溯到 @WebFilter 注解是如何被处理的。 -### @WebFilter 是如何工作的 -使用 **@WebFilter** 时,Filter 被加载有两个条件: -- 声明了 **@WebFilter** -- 在能被 **@ServletComponentScan** 扫到的路径下 - -直接搜索对 **@WebFilter** 的使用,可发现 **WebFilterHandler** 使用了它: -![](https://img-blog.csdnimg.cn/f54dc1da4efb42088f36b65cd49dc4d1.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -因此,我们选择在 **doHandle()** 打断点 -![](https://img-blog.csdnimg.cn/3f62fe089b9b4b56a89c2ff3fd44ab74.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -debug启动,观察调用栈: -![](https://img-blog.csdnimg.cn/a3a423af4efb4123bef27cddd97f0997.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -可见对 **@WebFilter** 的处理是在SB启动时,在**ServletComponentRegisteringPostProcessor**被触发,实现对如下注解的的扫描和处理: -- **@WebFilter** -- **@WebListener** -- **@WebServlet** - -WebFilterHandler则负责处理 **@WebFilter** 的使用: - -最后,**WebServletHandler** 通过父类 **ServletComponentHandler** 的模版方法模式,处理了所有被 **@WebFilter** 注解的类: -![](https://img-blog.csdnimg.cn/c87248e1484644efb912bf3e74513419.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16)可见最终注册的 FilterRegistrationBean就是自定义的WebFilter。 - -看第二个问题: -# 何时实例化TimeCostFilter -TimeCostFilter 是何时实例化的呢?为什么它没有成为一个普通 Bean? -可在 TimeCostFilter 构造器中加断点,便于快速定位初始化时机: -![](https://img-blog.csdnimg.cn/5e0390b6343046a6812e69da17266688.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -结合源码,可发现: -- Tomcat 启动时(***onstartUp***),才会创建 FilterRegistrationBean -- FilterRegistrationBean 在被创建时(***createBean***)会创建 **TimeCostFilter** 装配自身,而 **TimeCostFilter** 是通过 ***ResolveInnerBean*** 创建的 -- **TimeCostFilter** 实例最终是一种 **InnerBean** -![](https://img-blog.csdnimg.cn/4e37161a2335439da027266872c8118f.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -所以最终 TimeCostFilter 实例是一种 **InnerBean**,也就无法自动注入了。 -# 修正 -找到根因,就知道如何解决了。 - -前文解析可知,使用 **@WebFilter** 修饰过滤器时,TimeCostFilter 类型的 Bean 并没有注册至 Spring 容器,真正注册的是 FilterRegistrationBean。 -考虑到还可能存在多个 Filter,可这样修改: -![](https://img-blog.csdnimg.cn/a233451d6ac14ffe9154042be51aa677.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -- 注入 **FilterRegistrationBean** 类型,而非 **TimeCostFilter** 类型 -- 注入的名称是包含包名的全限定名,不能直接用 `TimeCostFilter`,以便存在多个过滤器时,能精确匹配。 - -# 总结 -**@WebFilter** 这种方式构建的 Filter 无法直接根据过滤器定义类型自动注入,因为这种Filter本身是以内部Bean呈现,最终是通过**FilterRegistrationBean**呈现给Spring。 -所以可通过自动注入**FilterRegistrationBean**类型完成自动装配。 \ No newline at end of file diff --git "a/Spring/SpringFramework/\350\207\252\345\256\232\344\271\211Filter\345\220\216,\346\210\221\347\232\204\344\270\232\345\212\241\344\273\243\347\240\201\346\200\216\344\271\210\350\242\253\346\211\247\350\241\214\344\272\206\345\244\232\346\254\241\357\274\237.md" "b/Spring/SpringFramework/\350\207\252\345\256\232\344\271\211Filter\345\220\216,\346\210\221\347\232\204\344\270\232\345\212\241\344\273\243\347\240\201\346\200\216\344\271\210\350\242\253\346\211\247\350\241\214\344\272\206\345\244\232\346\254\241\357\274\237.md" deleted file mode 100644 index 6de9747607..0000000000 --- "a/Spring/SpringFramework/\350\207\252\345\256\232\344\271\211Filter\345\220\216,\346\210\221\347\232\204\344\270\232\345\212\241\344\273\243\347\240\201\346\200\216\344\271\210\350\242\253\346\211\247\350\241\214\344\272\206\345\244\232\346\254\241\357\274\237.md" +++ /dev/null @@ -1,125 +0,0 @@ -实际生产过程中,若要求构建的过滤器针对全局路径有效,且无任何特殊需求(主要针对 Servlet 3.0 的一些异步特性支持),则完全可直接使用 Filter 接口(或继承 Spring 对 Filter 接口的包装类 OncePerRequestFilter),并使用**@Component** 将其包装为 Spring 中的普通 Bean,也可达到预期需求。 - -不过不管使用哪种方式,可能都遇到问题:业务代码重复执行多次。 - -这里以 **@Component** + Filter 接口实现方式呈现案例。 -创建一SB应用: -![](https://img-blog.csdnimg.cn/c456333c13e5482d90a3875ac5c6d8e2.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -UserController: -![](https://img-blog.csdnimg.cn/3f732602dad946daac2ef662858d1174.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -DemoFilter: -![](https://img-blog.csdnimg.cn/931f474caedf46739b60b272a07154e9.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -查看调用接口后的日志: -![](https://img-blog.csdnimg.cn/c2ee9c2675f84407a078fe7c93abecf6.png)业务代码竟然被执行两次??? - -预期是 Filter 的业务执行不会影响核心业务,所以当抛异常时,还是会调用`chain.doFilter`。 -但有时,我们会忘记及时返回而误闯其它`chain.doFilter`,最终导致自定义过滤器被执行多次。 - -而检查代码时,往往不能光速看出问题,所以这是类典型错误,虽然原因很简单。 -现在我们来分析为何会执行两次,以精通 Filter 的执行。 -# 源码解析 -首先我们要搞清 -## 责任链模式 -我们看Tomcat的Filter实现ApplicationFilterChain,采用责任链模式,像递归调用,区别在于: -- 递归调用,同一对象把子任务交给同一方法本身 -- 责任链,一个对象把子任务交给**其它对象**的同名方法 - -核心在于上下文 FilterChain 在不同对象 Filter 间的传递与状态的改变,通过这种链式串联,即可对同种对象资源实现不同业务场景的处理,实现业务解耦。 -### FilterChain 结构 -![](https://img-blog.csdnimg.cn/69f2e0298d26439c9d2ca42a19adbd83.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -1. 请求来临时,会执行到 ***StandardWrapperValve#invoke()*** ,该方法会创建 ApplicationFilterChain,并通过 ***ApplicationFilterChain#doFilter()*** 触发过滤器执行 -2. ***ApplicationFilterChain#doFilter()*** 会执行其私有方法 internalDoFilter -3. 在 internalDoFilter 方法中获取下一个Filter,并使用 request、response、this(当前ApplicationFilterChain 实例)作为参数来调用 doFilter(): -public void doFilter(ServletRequest request, ServletResponse response, -FilterChain chain) throws IOException, ServletException -4. 在 Filter 类的 doFilter() 中,执行Filter定义的动作并继续传递,获取第三个参数 ApplicationFilterChain,并执行其 doFilter() -5. 此时会循环执行进入第 2 步、第 3 步、第 4 步,直到第3步中所有的 Filter 类都被执行完毕为止 -6. 所有的Filter过滤器都被执行完毕后,会执行 servlet.service(request, response) 方法,最终调用对应的 Controller 层方法 - -现在,让我们先看负责请求处理的触发时机: -### StandardWrapperValve#invoke() -FilterChain 在何处被创建,又是在何处进行初始化调用,从而激活责任链开始链式调用? -```java -public final void invoke(Request request, Response response) - throws IOException, ServletException { - // ... - // 创建filterChain - ApplicationFilterChain filterChain = - ApplicationFilterFactory.createFilterChain(request, wrapper, servlet); -// ... -try { - if ((servlet != null) && (filterChain != null)) { - // Swallow output if needed - if (context.getSwallowOutput()) { - // ... - // 执行责任链 - filterChain.doFilter(request.getRequest(), - response.getResponse()); - // ... - } -// ... -} -``` -那FilterChain为何能被链式调用,调用细节如何?查阅: -### ApplicationFilterFactory.createFilterChain() -```java -public static ApplicationFilterChain createFilterChain(ServletRequest request, - Wrapper wrapper, Servlet servlet) { - // ... - ApplicationFilterChain filterChain = null; - if (request instanceof Request) { - // ... - // 创建FilterChain - filterChain = new ApplicationFilterChain(); - // ... - } - // ... - // Add the relevant path-mapped filters to this filter chain - for (int i = 0; i < filterMaps.length; i++) { - // ... - ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) - context.findFilterConfig(filterMaps[i].getFilterName()); - if (filterConfig == null) { - continue; - } - // 增加filterConfig到Chain - filterChain.addFilter(filterConfig); - } - - // ... - return filterChain; -} -``` -它创建 FilterChain,并将所有 Filter 逐一添加到 FilterChain 中。 - -继续查看 -### ApplicationFilterChain -**javax.servlet.FilterChain** 的实现类 -![](https://img-blog.csdnimg.cn/a086671f1117459e820cbb768553fe26.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -用于管理特定请求的一组过滤器的执行。 当所有定义的过滤器都执行完毕后,对 ***doFilter()*** 的下一次调用将执行 ***servlet#service()*** 本身。 -#### 实例变量 -过滤器集 -![](https://img-blog.csdnimg.cn/9dafeaf064244ad980dee5e22c61ce8d.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16)过滤器链中当前位置 -![](https://img-blog.csdnimg.cn/a82237385d1141e581a28cc736521cdd.png)链中当前的过滤器数 -![](https://img-blog.csdnimg.cn/6532e64173a04b31a8bb842e9aa767e7.png) -#### addFilter![](https://img-blog.csdnimg.cn/9a120216a76f46dd8053bfa8c6f3a059.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -每个被初始化的 Filter 都会通过 ***filterChain.addFilter()*** ,加入Filters,并同时更新n,使其等于 Filters数组长度。 - -至此,Spring 完成对 FilterChain 创建准备工作。 -#### doFilter() -调用此链中的下一个过滤器,传递指定请求、响应。 若此链无更多过滤器,则调用 ***servlet#service()*** -![](https://img-blog.csdnimg.cn/fb6fb766642040749904839912e9b6af.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -被委派到当前类的私有方法 -#### internalDoFilter(过滤器逻辑的核心) -每被调用一次,pos 变量值自增 1,即从类成员变量 Filters 中取下一个 Filter: -![](https://img-blog.csdnimg.cn/6d2e361f3d344aeba4c2dd6eb93f7328.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -***filter.doFilter(request, response, this)*** 会调用过滤器实现的 doFilter(),第三个参数为 this,即当前ApplicationFilterChain实例 ,这意味着用户需要在过滤器中显式调用一次 ***javax.servlet.FilterChain#doFilter***,才能完成整个链路。 - -当`pos < n`,说明已执行完所有过滤器,才调用 ***servlet.service(request, response)*** 执行真正业务。从 ***internalDoFilter()*** 执行到 ***Controller#saveUser()*** 。 -![](https://img-blog.csdnimg.cn/0011ff410ca545e29d0a0cc736212663.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -回到案例,***DemoFilter#doFilter()*** 捕获异常的部分执行了一次,随后在 try 外面又执行一次,因而当抛出异常时,doFilter() 会被执行两次,相应的 ***servlet.service(request, response)*** 方法及对应的 Controller 处理方法也被执行两次。 -# 修正 -只需除去重复的 ***filterChain.doFilter(request, response)*** : -![](https://img-blog.csdnimg.cn/1773dfaf02d84824b5bcfe8276876c6f.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -使用过滤器时,切忌多次调用 ***FilterChain#doFilter()*** 。 \ No newline at end of file diff --git "a/Spring/Spring\347\250\213\345\272\217\351\205\215\347\275\256\344\274\230\345\205\210\347\272\247.md" "b/Spring/Spring\347\250\213\345\272\217\351\205\215\347\275\256\344\274\230\345\205\210\347\272\247.md" deleted file mode 100644 index c5fa1169c4..0000000000 --- "a/Spring/Spring\347\250\213\345\272\217\351\205\215\347\275\256\344\274\230\345\205\210\347\272\247.md" +++ /dev/null @@ -1,81 +0,0 @@ -我们一般使用`application.yml`实现Spring Boot应用参数配置。但Spring配置有优先级,实际开发中要避免重复配置项的覆盖,就必须清晰这个优先级。 - -Spring通过Environment抽象出: -- Profile -规定场景。定义诸如dev、test、prod等环境 -- Property -PropertySources,各种配置源。一个环境中可能有多个配置源,每个配置源有许多配置项。查询配置信息时,按配置源优先级进行查询 -![](https://img-blog.csdnimg.cn/2021051910232476.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -> Property是如何查询配置的? - -首先看下配置的优先级: - -```java -env.getPropertySources().stream() - .forEach(System.out::println); -``` -如下共九个配置源: -![](https://img-blog.csdnimg.cn/3b8888a22217457b8e04c0da90afe8e8.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - - -- systemProperties -系统配置 -- applicationConfig -配置文件,我们的 yml 文件配置。如 application.yml 和 bootstrap.yml,首先加载bootstrap.yml。 -![](https://img-blog.csdnimg.cn/20210709155626321.png) - - -其中的**OriginAwareSystemEnvironmentPropertySource**就是我们的`application.yml`。 - -StandardEnvironment,继承自 -# AbstractEnvironment -- MutablePropertySources#propertySources -所有的配置源 -- getProperty -通过PropertySourcesPropertyResolver类进行查询配置 -![](https://img-blog.csdnimg.cn/20210519120036370.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 实例化PropertySourcesPropertyResolver时,传入了当前的MutablePropertySources -![](https://img-blog.csdnimg.cn/20210519115930680.png) - -那就来具体看看该类: -# MutablePropertySources -![](https://img-blog.csdnimg.cn/20210519120800958.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -# PropertySourcesPropertyResolver -构造器传入后面用来遍历的**propertySources**。 -![](https://img-blog.csdnimg.cn/20210519130713181.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -结合**AbstractEnvironment**,这个**propertySources**就是**AbstractEnvironment#MutablePropertySources**。 - -遍历时,若发现配置源中有对应K的V,则使用该V。 -所以**MutablePropertySources**中的配置源顺序很关键。 -- 真正查询配置的方法 -![](https://img-blog.csdnimg.cn/20210519130402101.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -在查询所有配置源时,NO.1的 **ConfigurationPropertySourcesPropertySource**并非一个实际存在的配置源,而是一个代理。debug 下查看到"user.name"的值是由它提供并返回,且没有再遍历后面的PropertySource看看有无"user.name" -![](https://img-blog.csdnimg.cn/2021051913313372.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -# ConfigurationPropertySourcesPropertySource -- getProperty()最终还调用findConfigurationProperty查询对应配置 -![](https://img-blog.csdnimg.cn/20210516153002620.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -- 上图中**getSource**()结果就是**SpringConfigurationPropertySources** -![](https://img-blog.csdnimg.cn/20210515204232227.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 其中包含的配置源列表 -![](https://img-blog.csdnimg.cn/20210515204514320.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -第一个就是ConfigurationPropertySourcesPropertySource,这遍历不会导致死循环吗? - -注意到configurationProperty的实际配置是从系统属性来的。 -![](https://img-blog.csdnimg.cn/20210515205923497.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -# SpringConfigurationPropertySources -![](https://img-blog.csdnimg.cn/20210515222936881.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -**ConfigurationPropertySourcesPropertySource**是所有配置源的NO.1,其早就知道了**PropertySourcesPropertyResolver**的遍历逻辑。 - -> 那知道遍历逻辑后,如何暗箱操作可以让自己成为南波湾配置源? - -**ConfigurationPropertySourcesPropertySource**实例化时 -![](https://img-blog.csdnimg.cn/20210515223624562.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70)**ConfigurationPropertySourcesPropertySource**是在**ConfigurationPropertySources**#attach中被 new 出来的。 - -获得MutablePropertySources,把自己加入成为第一 -![](https://img-blog.csdnimg.cn/20210516152639338.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -这个attach方法就是在Spring应用程序启动时准备环境的时候调用的。 -![](https://img-blog.csdnimg.cn/20210516153250188.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) \ No newline at end of file diff --git "a/Spring/SpringFramework/Spring\347\232\204@Transaction\346\263\250\350\247\243\345\244\261\346\225\210\345\234\272\346\231\257.md" "b/Spring/\344\275\240\347\232\204\346\225\260\346\215\256\345\272\223\344\272\213\345\212\241\345\212\240\344\270\212Spring\347\232\204@Transaction\346\263\250\350\247\243\344\270\272\345\225\245\350\277\230\346\262\241\347\224\237\346\225\210?.md" similarity index 100% rename from "Spring/SpringFramework/Spring\347\232\204@Transaction\346\263\250\350\247\243\345\244\261\346\225\210\345\234\272\346\231\257.md" rename to "Spring/\344\275\240\347\232\204\346\225\260\346\215\256\345\272\223\344\272\213\345\212\241\345\212\240\344\270\212Spring\347\232\204@Transaction\346\263\250\350\247\243\344\270\272\345\225\245\350\277\230\346\262\241\347\224\237\346\225\210?.md" diff --git "a/TODO/uml/Java\346\200\273\347\273\223\347\237\245\350\257\206\347\202\271.pos" "b/TODO/uml/Java\346\200\273\347\273\223\347\237\245\350\257\206\347\202\271.pos" deleted file mode 100644 index 9dea4e81b1..0000000000 --- "a/TODO/uml/Java\346\200\273\347\273\223\347\237\245\350\257\206\347\202\271.pos" +++ /dev/null @@ -1 +0,0 @@ -{"diagram":{"image":{"x":0,"width":200,"y":0,"pngdata":"iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAsUlEQVR4nO3BAQEAAACCIP+vbkhAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8GXHmAAFMgHIEAAAAAElFTkSuQmCC","height":200},"elements":{"leftChildren":[],"note":"","watermark":"","children":[{"parent":"root","lineStyle":{"randomLineColor":"#4D69FD"},"children":[{"parent":"800ecb4c0776","children":[{"parent":"36d002b102e2","children":[{"parent":"5351911e121f","children":[],"id":"da9b17a0be4d","title":"数据结构: 数组"},{"parent":"5351911e121f","children":[],"id":"00dd5a2b370d","title":"由于数据结构的特点, 查找访问效率高, 增删效率低"},{"parent":"5351911e121f","children":[],"id":"89a058828311","title":"默认数组大小10"},{"parent":"5351911e121f","children":[],"id":"dc80c006aa14","title":"ArrayList存在指定index新增与直接新增,在新增前会有一步校验长度的判断ensureCapacityInternal,如果长度不够需要扩容
    "},{"parent":"5351911e121f","children":[],"id":"d16b9b53101f","title":"ArrayList线程不安全,线程安全版本的数组容器是Vector
    "},{"parent":"5351911e121f","children":[],"id":"5eb59595d8fd","title":"与LinkedList遍历效率对比,性能高很多,ArrayList遍历最大的优势在于内存的连续性,CPU的内部缓存结构会缓存连续的内存片段,可以大幅降低读取内存的性能开销
    "}],"id":"5351911e121f","title":"ArrayList"},{"parent":"36d002b102e2","children":[{"parent":"f817a30f5e08","children":[],"id":"e3e17fe90642","title":"数据结构: 双向链表"},{"parent":"f817a30f5e08","children":[],"id":"3846d194c83e","title":"适合插入删除频繁的情况  内部维护了链表的长度
    "}],"id":"f817a30f5e08","title":"LinkedList
    "}],"id":"36d002b102e2","title":"List"},{"parent":"800ecb4c0776","children":[{"parent":"24f4a3146f29","children":[{"parent":"f4b8a675a029","children":[{"parent":"2353a0a1da34","children":[],"id":"8838e06b887a","title":"数据结构: 数组+链表
    "},{"parent":"2353a0a1da34","children":[],"id":"7a82749e87f8","title":"头插法: 新来的值会取代原有的值,原有的值就顺推到链表中去
    "},{"parent":"2353a0a1da34","children":[],"id":"c1c3800b1ead","title":"Java7在多线程操作HashMap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系, 可能形成环形链表
    "}],"id":"2353a0a1da34","title":"1.7"},{"parent":"f4b8a675a029","children":[{"parent":"cb2e0e4bf56a","children":[],"id":"a743dde50473","title":"数据结构: 数组+链表+红黑树 
    "},{"parent":"cb2e0e4bf56a","children":[{"parent":"4bb6868f0cc3","children":[],"id":"4753ca9df7aa","title":"根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表
    "}],"id":"4bb6868f0cc3","title":"Hashmap中的链表大小超过八个时会自动转化为红黑树,当删除小于六时重新变为链表
    "},{"parent":"cb2e0e4bf56a","children":[],"id":"1c07c2a415b7","title":"尾插法
    "},{"parent":"cb2e0e4bf56a","children":[],"id":"94b8ffa2573f","title":"Java8在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的引用关系
    "}],"id":"cb2e0e4bf56a","title":"1.8"},{"parent":"f4b8a675a029","children":[{"parent":"6d54a11adce4","children":[],"id":"210d702bbf4a","title":"LoadFactory 默认0.75
    "},{"parent":"6d54a11adce4","children":[],"id":"e4055d669e46","title":"
    • 扩容:创建一个新的Entry空数组,长度是原数组的2倍。
    • ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组

    "},{"parent":"6d54a11adce4","children":[{"parent":"af3ec421dc31","children":[],"id":"0c6cfee11d49","title":"因为长度扩大以后,Hash的规则也随之改变
    "},{"parent":"af3ec421dc31","children":[],"id":"316f5ac25324","title":"Hash的公式---> index = HashCode(Key) & (Length - 1)
    原来长度(Length)是8你位运算出来的值是2 ,新的长度是16位运算出来的值不同
    "},{"parent":"af3ec421dc31","children":[],"id":"ff2cf93209dc","title":"HashMap是通过key的HashCode去寻找index的, 如果不进行重写,会出现在一个index中链表的HashCode相等情况,所以要确保相同的对象返回相同的hash值,不同的对象返回不同的hash值,必须要重写equals
    "}],"id":"af3ec421dc31","title":"为什么要ReHash而不进行复制? 
    "}],"id":"6d54a11adce4","title":"扩容机制"},{"parent":"f4b8a675a029","children":[{"parent":"c6437e8e4f45","children":[],"id":"cb7132ed82f1","title":"HashMap源码中put/get方法都没有加同步锁, 无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全无法保证
    "},{"parent":"c6437e8e4f45","children":[{"parent":"9d6423986311","children":[],"id":"b40b3d560950","title":"Collections.synchronizedMap(Map)"},{"parent":"9d6423986311","children":[{"parent":"cdcaf61438fc","children":[{"parent":"404e074072c9","children":[{"parent":"b926b8dd14c9","children":[],"id":"6bf370941ab4","title":"安全失败机制: 这种机制会使你此次读到的数据不一定是最新的数据。
    如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理"}],"id":"b926b8dd14c9","title":"Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null"},{"parent":"404e074072c9","children":[],"id":"c4669cca3ca2","title":"Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类"},{"parent":"404e074072c9","children":[],"id":"63a75cf7717b","title":"HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75"},{"parent":"404e074072c9","children":[],"id":"8e6e339c6351","title":"当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1"},{"parent":"404e074072c9","children":[{"parent":"13aa27708fb1","children":[],"id":"e72d018a877c","title":"快速失败(fail—fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出ConcurrentModificationException"}],"id":"13aa27708fb1","title":"HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的. 所以,当其他线程改变了HashMap 的结构,如:增加、删除元素,将会抛出ConcurrentModificationException 异常,而 Hashtable 则不会"}],"id":"404e074072c9","title":"与HahsMap的区别"}],"id":"cdcaf61438fc","title":"Hashtable"},{"parent":"9d6423986311","children":[],"id":"3aa13f74a283","title":"ConcurrentHashMap"}],"id":"9d6423986311","title":"确保线程安全的方式"}],"id":"c6437e8e4f45","title":"线程不安全
    "},{"parent":"f4b8a675a029","children":[{"parent":"83e802bbbf44","children":[],"id":"6689f5a28d8d","title":"创建HashMap时最好赋初始值, 而且最好为2的幂,为了位运算的方便
    "},{"parent":"83e802bbbf44","children":[{"parent":"33f1a38f62cd","children":[],"id":"245253ad18f8","title":"实现均匀分布, 在使用不是2的幂的数字的时候,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值,只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的
    "}],"id":"33f1a38f62cd","title":"默认初始化大小为16
    "}],"id":"83e802bbbf44","title":"初始化
    "},{"parent":"f4b8a675a029","children":[],"id":"1bf62446136a","title":"重写equals必须重写HashCode
    "}],"id":"f4b8a675a029","title":"HashMap
    "},{"parent":"24f4a3146f29","children":[{"parent":"099533450f79","children":[{"parent":"6c4d02753b0b","children":[],"id":"1bf9ad750241","title":"这种机制会使你此次读到的数据不一定是最新的数据。如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,HashTable同理
    "}],"id":"6c4d02753b0b","title":"安全失败机制
    "},{"parent":"099533450f79","children":[{"parent":"238f89a01639","children":[],"id":"9a49309bdc28","title":"数据结构: 数组+链表 (Segment 数组、HashEntry 组成)
    "},{"parent":"238f89a01639","children":[{"parent":"940aa3e4031c","children":[],"id":"6d0c1ad1257f","title":"HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next
    "}],"id":"940aa3e4031c","title":"HashEntry
    "},{"parent":"238f89a01639","children":[{"parent":"214133bb3335","children":[{"parent":"c94b076d3d2d","children":[],"id":"ccbe178d4298","title":"继承了ReentrantLock
    "},{"parent":"c94b076d3d2d","children":[],"id":"8e8ef1826155","title":"每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
    如果容量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。
    "}],"id":"c94b076d3d2d","title":"segment分段锁
    "},{"parent":"214133bb3335","children":[{"parent":"d93302980fe7","children":[],"id":"26bfa3a12ad8","title":"尝试自旋获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁
    "},{"parent":"d93302980fe7","children":[],"id":"b933436ce63c","title":"如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功
    "}],"id":"d93302980fe7","title":"put
    "},{"parent":"214133bb3335","children":[{"parent":"e791448ffbae","children":[],"id":"65b8e0356631","title":"由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值
    "},{"parent":"e791448ffbae","children":[],"id":"1748a490b610","title":"ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁
    "}],"id":"e791448ffbae","title":"get
    "}],"id":"214133bb3335","title":"并发度高的原因
    "}],"id":"238f89a01639","title":"1.7
    "},{"parent":"099533450f79","children":[{"parent":"bb9fcd3cdf24","children":[],"id":"cefe93491aa8","title":"数组+链表+红黑树
    "},{"parent":"bb9fcd3cdf24","children":[{"parent":"7f1ff4d86665","children":[],"id":"122b03888ad2","title":"抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性
    "}],"id":"7f1ff4d86665","title":"区别
    "},{"parent":"bb9fcd3cdf24","children":[{"parent":"292fc2483f4c","children":[],"id":"1181a0ece609","title":"根据 key 计算出 hashcode
    "},{"parent":"292fc2483f4c","children":[],"id":"17e33984c1e5","title":"判断是否需要进行初始化
    "},{"parent":"292fc2483f4c","children":[],"id":"d0bfa1371c32","title":"即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功
    "},{"parent":"292fc2483f4c","children":[],"id":"1ddaa88f7963","title":"如果当前位置的 hashcode == MOVED == -1,则需要进行扩容
    "},{"parent":"292fc2483f4c","children":[],"id":"5735982de5ef","title":"如果都不满足,则利用 synchronized 锁写入数据
    "},{"parent":"292fc2483f4c","children":[],"id":"4e006a2b0550","title":"如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树
    "}],"id":"292fc2483f4c","title":"put操作
    "},{"parent":"bb9fcd3cdf24","children":[{"parent":"92654a95d638","children":[],"id":"02394dd149e1","title":"根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值
    "},{"parent":"92654a95d638","children":[],"id":"d7b487f3b69b","title":"如果是红黑树那就按照树的方式获取值
    "},{"parent":"92654a95d638","children":[],"id":"1c6f9d2d5016","title":"就不满足那就按照链表的方式遍历获取值
    "}],"id":"92654a95d638","title":"get操作
    "}],"collapsed":false,"id":"bb9fcd3cdf24","title":"1.8
    "}],"id":"099533450f79","title":"ConcurrentHashMap
    "}],"collapsed":false,"id":"24f4a3146f29","title":"Map"},{"parent":"800ecb4c0776","children":[{"parent":"e7046be1aafc","children":[{"parent":"bb49195cb4a7","children":[],"link":{"title":"https://mp.weixin.qq.com/s/0cMrE87iUxLBw_qTBMYMgA","type":"url","value":"https://mp.weixin.qq.com/s/0cMrE87iUxLBw_qTBMYMgA"},"id":"5448b1ead28d","title":"同步容器(如Vector)的所有操作一定是线程安全的吗?"}],"id":"bb49195cb4a7","title":"相关文档"}],"id":"e7046be1aafc","title":"Vector"},{"parent":"800ecb4c0776","children":[{"parent":"6062a460b43c","children":[{"parent":"d695fc5d8846","children":[],"id":"84bbc18befc1","title":"底层实现的就是HashMap,所以是根据HashCode来判断是否是重复元素
    "},{"parent":"d695fc5d8846","children":[],"id":"27e06a58782e","title":"初始化容量是:16, 因为底层实现的是HashMap。加载因子是0.75
    "},{"parent":"d695fc5d8846","children":[],"id":"dbcb9b76077a","title":"无序的
    "},{"parent":"d695fc5d8846","children":[],"id":"2c468ef34635","title":"HashSet不能根据索引去数据,所以不能用普通的for循环来取出数据,应该用增强for循环,查询性能不好
    "}],"id":"d695fc5d8846","title":"HashSet
    "},{"parent":"6062a460b43c","children":[{"parent":"3e5fc407007b","children":[],"id":"34347150ba65","title":"底层是实现的TreeMap
    "},{"parent":"3e5fc407007b","children":[],"id":"3ead4a788049","title":"元素不能够重复,可以有一个null值,并且这个null值一直在第一个位置上
    "},{"parent":"3e5fc407007b","children":[],"id":"432c37f69b42","title":"默认容量:16,加载因子是0.75
    "},{"parent":"3e5fc407007b","children":[],"id":"1bf8f3a4b2c3","title":"TreeMap是有序的,这个有序不是存入的和取出的顺序是一样的,而是根据自然规律拍的序
    "}],"id":"3e5fc407007b","title":"TreeSet
    "}],"id":"6062a460b43c","title":"Set"}],"collapsed":true,"id":"800ecb4c0776","title":"集合"},{"parent":"root","lineStyle":{"randomLineColor":"#F4325C"},"children":[],"id":"ef690530e935","title":"基础"},{"parent":"root","lineStyle":{"randomLineColor":"#A04AFB"},"children":[{"parent":"d61da867cb10","children":[{"parent":"c56b85ccc7f0","children":[],"id":"cd07e14ad850","title":"虚拟机堆
    "},{"parent":"c56b85ccc7f0","children":[],"id":"5f3d1d3f67c2","title":"虚拟机栈
    "},{"parent":"c56b85ccc7f0","children":[],"id":"b44fa08a79e3","title":"方法区
    "},{"parent":"c56b85ccc7f0","children":[],"id":"38e4ffe62590","title":"本地方法栈
    "},{"parent":"c56b85ccc7f0","children":[],"id":"6b76f89e7451","title":"程序计数器
    "}],"id":"c56b85ccc7f0","title":"Java内存区域
    "},{"parent":"d61da867cb10","children":[{"parent":"08f416fb625f","children":[],"id":"95995454f91e","title":"加载->验证->准备->解析->初始化->使用->卸载
    "},{"parent":"08f416fb625f","children":[{"parent":"28edebbeb4d6","children":[],"id":"fddd8578d67e","title":"父类加载 不重复加载
    "}],"id":"28edebbeb4d6","title":"双亲委派原则
    "},{"parent":"08f416fb625f","children":[{"parent":"ff18f9026678","children":[],"id":"ea8db2744206","title":"第一次,在JDK1.2以前,双亲委派模型在JDK1.2引入,ClassLoder在最初已经存在了,为了兼容已有代码,添加了findClass()方法,如果父类加载失败会自动调用findClass()来完成加载
    "},{"parent":"ff18f9026678","children":[],"id":"5c240d502f24","title":"第二次,由双亲委派模型缺陷导致,由于双亲委派越基础的类由越上层的加载器进行加载,如果有基础类型调回用户代码回无法解决而产生,出现线程上下文类加载器,会出现父类加载器请求子类加载器完成类加载的行为
    "},{"parent":"ff18f9026678","children":[],"id":"5d62862bf3e0","title":"第三次,代码热替换、模块热部署,典型:OSGi每一个程序模块都有一个自己的类加载器
    "}],"id":"ff18f9026678","title":"破坏双亲委派模型
    "}],"id":"08f416fb625f","title":"类得加载机制"},{"parent":"d61da867cb10","children":[{"parent":"52bb18b06c6e","children":[],"id":"f61579e4d22a","title":"新生代/年轻代
    "},{"parent":"52bb18b06c6e","children":[],"id":"72db835ecada","title":"老年代
    "},{"parent":"52bb18b06c6e","children":[{"parent":"559cb847a3c3","children":[{"parent":"48946b525dfd","children":[],"id":"b7b6b8d8f03d","title":"字符串存在永久代中,容易出现性能问题和内存溢出
    "},{"parent":"48946b525dfd","children":[],"id":"2f7df7f20543","title":"类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
    "},{"parent":"48946b525dfd","children":[],"id":"5439cd0f53c8","title":"永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
    "},{"parent":"48946b525dfd","children":[],"id":"9c36c0817672","title":"将 HotSpot 与 JRockit 合二为一
    "}],"id":"48946b525dfd","title":"为什么要使用元空间取代永久代的实现?
    "},{"parent":"559cb847a3c3","children":[{"parent":"1d6773397050","children":[],"id":"c33f0e9fe53f","title":"元空间并不在虚拟机中,而是使用本地内存。因此默认情况下,元空间的大小仅受本地内存限制
    "}],"id":"1d6773397050","title":"元空间与永久代区别
    "},{"parent":"559cb847a3c3","children":[{"parent":"afb5323c83d0","children":[],"id":"fbf6f9baf099","title":"-XX:MetaspaceSize:初始空间大小,达到该值会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值
    "},{"parent":"afb5323c83d0","children":[],"id":"8e0c80084aef","title":"-XX:MaxMetaspaceSize:最大空间,默认是没有限制的
    "}],"id":"afb5323c83d0","title":"元空间空间大小设置
    "}],"id":"559cb847a3c3","title":"永久代/元空间
    "},{"parent":"52bb18b06c6e","children":[{"parent":"34170f8e7332","children":[],"id":"9d5df7c5e2eb","title":"根据存活时间
    "}],"id":"34170f8e7332","title":"晋升机制
    "}],"id":"52bb18b06c6e","title":"分代回收
    "},{"parent":"d61da867cb10","children":[{"parent":"c465e3a953e3","children":[{"parent":"2e9e41cbb0e4","children":[],"id":"6ff93afab3be","title":"绝大多数对象都是朝生熄灭的
    "}],"id":"2e9e41cbb0e4","title":"弱分代假说
    "},{"parent":"c465e3a953e3","children":[{"parent":"759979ad09cd","children":[],"id":"a152f857c10b","title":"熬过越多次垃圾收集过程的对象就越难以消亡
    "}],"id":"759979ad09cd","title":"强分代假说
    "},{"parent":"c465e3a953e3","children":[{"parent":"4843a46b8196","children":[],"id":"6481cbce8487","title":"跨代引用相对于同代引用来说仅占极少数
    "}],"id":"4843a46b8196","title":"跨代引用假说
    "}],"id":"c465e3a953e3","title":"分代收集理论
    "},{"parent":"d61da867cb10","children":[{"parent":"85ab22cf15b4","children":[],"id":"0c42af972e09","title":"“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,如果某个对象到GC Roots间没有任何引用链相连接,则证明此对象是不可能再被使用的
    "}],"id":"85ab22cf15b4","title":"可达性分析算法
    "},{"parent":"d61da867cb10","children":[{"parent":"133b15c6580f","children":[{"parent":"9bd71d4114f3","children":[],"id":"e602b5dbf42a","title":"强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它; 当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
    "}],"id":"9bd71d4114f3","title":"强引用 (StrongReference)
    "},{"parent":"133b15c6580f","children":[{"parent":"a4d69c685996","children":[],"id":"aaedb29f6e8f","title":"如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
    "}],"id":"a4d69c685996","title":"软引用 (SoftReference)
    "},{"parent":"133b15c6580f","children":[{"parent":"6cdc46b73fd3","children":[],"id":"7acc452f65ab","title":"在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
    "}],"id":"6cdc46b73fd3","title":"弱引用 (WeakReference)
    "},{"parent":"133b15c6580f","children":[{"parent":"5f58e345e898","children":[],"id":"d189c9591839","title":"如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收
    "}],"id":"5f58e345e898","title":"虚引用 (PhantomReference)
    "}],"id":"133b15c6580f","title":"引用
    "},{"parent":"d61da867cb10","children":[{"parent":"7a6e56139d11","children":[{"parent":"39f66c681caf","children":[{"parent":"5303c5abf34b","children":[],"id":"e983d3f5fd4a","title":"对象存活较多情况
    "},{"parent":"5303c5abf34b","children":[],"id":"b9065a228c05","title":"老年代
    "}],"id":"5303c5abf34b","title":"适用场景
    "},{"parent":"39f66c681caf","children":[{"parent":"733f3bc0b7ab","children":[],"id":"4f9fdc7ae82e","title":"内存空间碎片化
    "},{"parent":"733f3bc0b7ab","children":[],"id":"2a97115e02e3","title":"由于空间碎片导致的提前GC
    "},{"parent":"733f3bc0b7ab","children":[{"parent":"706cff5bc691","children":[],"id":"7a5e8f6db9e2","title":"标记需清除或存货对象
    "},{"parent":"706cff5bc691","children":[],"id":"3da1d4f2af71","title":"清除标记或未标记对象
    "}],"id":"706cff5bc691","title":"扫描了两次
    "}],"id":"733f3bc0b7ab","title":"缺点
    "}],"id":"39f66c681caf","title":"标记请除
    "},{"parent":"7a6e56139d11","children":[{"parent":"463d39f131e0","children":[{"parent":"cc3d3a846262","children":[],"id":"ad8382025fe4","title":"存活对象少比较高效
    "},{"parent":"cc3d3a846262","children":[],"id":"fb2a5786103c","title":"扫描了整个空间(标记存活对象并复制移动)
    "},{"parent":"cc3d3a846262","children":[],"id":"efebc696f64e","title":"年轻代
    "}],"id":"cc3d3a846262","title":"适用场景
    "},{"parent":"463d39f131e0","children":[{"parent":"4a07efc633b8","children":[],"id":"ca57a2a2591a","title":"需要空闲空间
    "},{"parent":"4a07efc633b8","children":[],"id":"74b68744b877","title":"老年代作为担保空间
    "},{"parent":"4a07efc633b8","children":[],"id":"9a2b53e3948c","title":"复制移动对象
    "}],"id":"4a07efc633b8","title":"缺点
    "}],"id":"463d39f131e0","title":"标记复制
    "},{"parent":"7a6e56139d11","children":[{"parent":"6e91691d76fb","children":[{"parent":"ca79fb0161d2","children":[],"id":"3093a8344bf8","title":"对象存活较多情况
    "},{"parent":"ca79fb0161d2","children":[],"id":"171500353dee","title":"老年代
    "}],"id":"ca79fb0161d2","title":"适用场景
    "},{"parent":"6e91691d76fb","children":[{"parent":"2e00b69db6db","children":[],"id":"9cc7fd616235","title":"移动存活对象并更新对象引用
    "},{"parent":"2e00b69db6db","children":[],"id":"9f173f5f0a02","title":"Stop The World
    "}],"id":"2e00b69db6db","title":"缺点
    "}],"id":"6e91691d76fb","title":"标记整理
    "},{"parent":"7a6e56139d11","children":[{"parent":"873b4393cb04","children":[],"id":"e55f55cabd04","title":"没办法解决循环引用的问题
    "}],"id":"873b4393cb04","title":"引用计数
    "}],"id":"7a6e56139d11","title":"垃圾回收机制
    "},{"parent":"d61da867cb10","children":[{"parent":"94c48a9cef88","children":[{"parent":"1871a079226a","children":[{"parent":"ec2e2387d0dc","children":[{"parent":"1e82c0b474da","children":[],"id":"4ae444f631e6","title":"Eden
    "},{"parent":"1e82c0b474da","children":[],"id":"2e6db7cc3c33","title":"Survivor1
    "},{"parent":"1e82c0b474da","children":[],"id":"c07a1cd16c8f","title":"Survivor2
    "},{"parent":"1e82c0b474da","children":[{"parent":"150da6938c96","children":[],"id":"79ab826115b2","title":"通过阈值晋升
    "}],"id":"150da6938c96","title":"Minor GC
    "}],"id":"1e82c0b474da","title":"年轻代"},{"parent":"ec2e2387d0dc","children":[{"parent":"c2b0ef70d896","children":[],"id":"69438d9521b8","title":"Major GC 等价于 Full GC
    "}],"id":"c2b0ef70d896","title":"老年代
    "},{"parent":"ec2e2387d0dc","children":[],"id":"bb363f2d06eb","title":"永久"}],"id":"ec2e2387d0dc","title":"分代情况
    "},{"parent":"1871a079226a","children":[{"parent":"ef9b2adfad54","children":[],"id":"f8a938786281","title":"对CPU资源敏感
    "},{"parent":"ef9b2adfad54","children":[],"id":"f34402ad150a","title":"无法处理浮动垃圾
    "},{"parent":"ef9b2adfad54","children":[],"id":"1b39a82e9c8c","title":"基于标记清除算法 大量空间碎片
    "}],"id":"ef9b2adfad54","title":"缺点
    "}],"id":"1871a079226a","title":"CMS
    "},{"parent":"94c48a9cef88","children":[{"parent":"9b2c5a694136","children":[],"id":"d3247834f765","title":"分区概念 弱化分代
    "},{"parent":"9b2c5a694136","children":[{"parent":"0ac46d282191","children":[],"id":"1eba7428e805","title":"不会产生碎片空间,分配大对象不会提前Full GC
    "}],"id":"0ac46d282191","title":"标记整理算法
    "},{"parent":"9b2c5a694136","children":[{"parent":"20775041ea09","children":[],"id":"df43ec84ba90","title":"使用参数-XX:MaxGCPauseMills,默认为200毫秒,优先处理回收价值收集最大的Region
    "}],"id":"20775041ea09","title":"允许用户设置收集的停顿时间
    "},{"parent":"9b2c5a694136","children":[],"id":"0831e78ce2d4","title":"利用CPU多核条件,缩短STW时间
    "},{"parent":"9b2c5a694136","children":[],"id":"00fc277cb4e8","title":"原始快照算法(SATB)保证收集线程与用户线程互不干扰,避免标记结果出错
    "},{"parent":"9b2c5a694136","children":[{"parent":"64ddc0929330","children":[{"parent":"6bda8393ce9e","children":[],"id":"2194b30b8023","title":"标记STW从GC Roots开始直接可达的对象,借用Minor GC时同步完成
    "}],"id":"6bda8393ce9e","title":"初始标记
    "},{"parent":"64ddc0929330","children":[{"parent":"8954c96bf47e","children":[],"id":"11402483ace1","title":"从GC Roots开始对堆对象进行可达性分析,找出要回收的对象,与用户程序并发执行,重新处理SATB记录下的并发时引用变动对象
    "}],"id":"8954c96bf47e","title":"并发标记
    "},{"parent":"64ddc0929330","children":[{"parent":"dcca5f39bc8c","children":[],"id":"2044b2d42f4a","title":"处理并发阶段结束后遗留下来的少量SATB记录
    "}],"id":"dcca5f39bc8c","title":"最终标记
    "},{"parent":"64ddc0929330","children":[{"parent":"32a05d0acfd4","children":[],"id":"1c5bed528018","title":"根据用户期待的GC停顿时间制定回收计划
    "}],"id":"32a05d0acfd4","title":"筛选回收
    "}],"id":"64ddc0929330","title":"收集步骤
    "},{"parent":"9b2c5a694136","children":[{"parent":"144086fdf009","children":[{"parent":"70dcc7b7411a","children":[{"parent":"bf3ba1d7ab1a","children":[],"id":"6d91b336c4b7","title":"复制一些存活对象到Old区、Survivor区
    "}],"id":"bf3ba1d7ab1a","title":"回收所有Eden、Survivor区
    "}],"id":"70dcc7b7411a","title":"Minor GC/Young GC
    "},{"parent":"144086fdf009","children":[],"id":"44c8c447963a","title":"Mixed GC
    "}],"id":"144086fdf009","title":"回收模式
    "}],"id":"9b2c5a694136","title":"G1
    "},{"parent":"94c48a9cef88","children":[{"parent":"8e74a22eafd1","children":[],"id":"642ff9bd8dbb","title":"G1分区域 每个区域是有老年代概念的,但是收集器以整个区域为单位收集
    "},{"parent":"8e74a22eafd1","children":[],"id":"affafbc6a4da","title":"G1回收后马上合并空闲内存,而CMS会在STW的时候合并
    "}],"id":"8e74a22eafd1","title":"CMS与G1的区别
    "}],"id":"94c48a9cef88","title":"垃圾回收器
    "},{"parent":"d61da867cb10","children":[{"parent":"1dfd2da1ccb2","children":[],"id":"09de7462c062","title":"老年代空间不足
    "},{"parent":"1dfd2da1ccb2","children":[],"id":"7681c3724945","title":"system.gc()通知JVM进行Full GC
    "},{"parent":"1dfd2da1ccb2","children":[],"id":"d1038ecedd23","title":"持久代空间不足
    "}],"id":"1dfd2da1ccb2","title":"Full GC
    "},{"parent":"d61da867cb10","children":[{"parent":"a7686c6448fb","children":[],"id":"750a3f631db0","title":"在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起。是Java中一种全局暂停现象,全局停顿,所有Java代码停止,Native代码可以执行,但不能与JVM交互
    "}],"id":"a7686c6448fb","title":"STW(Stop The World)
    "},{"parent":"d61da867cb10","children":[{"parent":"a7d301228e2a","children":[],"id":"92f20719ac8a","title":"设置堆的最大最小值 -xms -xmx
    "},{"parent":"a7d301228e2a","children":[{"parent":"0715cab0f70c","children":[{"parent":"59b27c4fd939","children":[],"id":"e21f787b02c5","title":"防止年轻代堆收缩:老年代同理
    "}],"id":"59b27c4fd939","title":"-XX:newSize设置绝对大小
    "}],"id":"0715cab0f70c","title":"调整老年和年轻代的比例
    "},{"parent":"a7d301228e2a","children":[],"id":"ff8918fe5f06","title":"主要看是否存在更多持久对象和临时对象
    "},{"parent":"a7d301228e2a","children":[],"id":"24446fac73b9","title":"观察一段时间 看峰值老年代如何 不影响gc就加大年轻代
    "},{"parent":"a7d301228e2a","children":[],"id":"9204d8b66e5f","title":"配置好的机器可以用 并发收集算法
    "},{"parent":"a7d301228e2a","children":[],"id":"b58af9729fb9","title":"每个线程默认会开启1M的堆栈 存放栈帧 调用参数 局部变量 太大了  500k够了
    "},{"parent":"a7d301228e2a","children":[],"id":"c001d0fcef30","title":"原则 就是减少GC STW
    "}],"id":"a7d301228e2a","title":"性能调优
    "},{"parent":"d61da867cb10","children":[{"parent":"febcbf010f6e","children":[],"id":"0e938c11383a","title":"jasvism
    "},{"parent":"febcbf010f6e","children":[],"id":"64f7c1d4e0c1","title":"dump
    "},{"parent":"febcbf010f6e","children":[],"id":"3ae88d77a8e0","title":"监控配置 自动dump
    "}],"id":"febcbf010f6e","title":"FullGC 内存泄露排查
    "},{"parent":"d61da867cb10","children":[{"parent":"163f2b3e6279","children":[{"parent":"341ea9d0a8b1","children":[{"parent":"7b2c9c719a5b","children":[],"id":"52bb357adfd0","title":"开启逃逸分析:-XX:+DoEscapeAnalysis
    关闭逃逸分析:-XX:-DoEscapeAnalysis
    显示分析结果:-XX:+PrintEscapeAnalysis
    "}],"id":"7b2c9c719a5b","title":"Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术
    "}],"id":"341ea9d0a8b1","title":"概念
    "},{"parent":"163f2b3e6279","children":[{"parent":"2951be8a5ab7","children":[{"parent":"19414f3dfc6b","children":[],"id":"b8919c87025b","title":"即一个对象的作用范围逃出了当前方法或者当前线程
    "},{"parent":"19414f3dfc6b","children":[{"parent":"f3f5a4d41ca7","children":[],"id":"78e7003b3c6c","title":"对象是一个静态变量
    "},{"parent":"f3f5a4d41ca7","children":[],"id":"57d7fe2eba87","title":"对象是一个已经发生逃逸的对象
    "},{"parent":"f3f5a4d41ca7","children":[],"id":"f72de2af0484","title":"对象作为当前方法的返回值
    "}],"id":"f3f5a4d41ca7","title":"场景
    "}],"id":"19414f3dfc6b","title":"全局逃逸
    "},{"parent":"2951be8a5ab7","children":[{"parent":"078cd7effe02","children":[],"id":"c4a9820ede62","title":"即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸
    "}],"id":"078cd7effe02","title":"参数级逃逸
    "},{"parent":"2951be8a5ab7","children":[{"parent":"dfb6626fa13e","children":[],"id":"a1f76aa518e6","title":"即方法中的对象没有发生逃逸
    "}],"id":"dfb6626fa13e","title":"没有逃逸
    "}],"id":"2951be8a5ab7","title":"逃逸状态
    "},{"parent":"163f2b3e6279","children":[{"parent":"74538b6f0667","children":[{"parent":"22052ecf3777","children":[],"id":"1bb3c11ad593","title":"开启锁消除:-XX:+EliminateLocks
    关闭锁消除:-XX:-EliminateLocks
    "}],"id":"22052ecf3777","title":"锁消除
    "},{"parent":"74538b6f0667","children":[{"parent":"d19fda364ab3","children":[],"id":"618f15119b55","title":"开启标量替换:-XX:+EliminateAllocations
    关闭标量替换:-XX:-EliminateAllocations
    显示标量替换详情:-XX:+PrintEliminateAllocations
    "}],"id":"d19fda364ab3","title":"标量替换
    "},{"parent":"74538b6f0667","children":[{"parent":"58e0672d4093","children":[],"id":"852d0a874d5c","title":"当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能
    "}],"id":"58e0672d4093","title":"栈上分配
    "}],"id":"74538b6f0667","title":"逃逸分析优化
    "},{"parent":"163f2b3e6279","children":[{"parent":"0c9fe2ae4750","children":[],"id":"d4e70d6dd352","title":"在平时开发过程中尽可能的控制变量的作用范围了,变量范围越小越好,让虚拟机尽可能有优化的空间
    "}],"id":"0c9fe2ae4750","title":"结论
    "}],"id":"163f2b3e6279","title":"逃逸分析
    "},{"parent":"d61da867cb10","children":[{"parent":"f01a444aff49","children":[{"parent":"e5b794712d9f","children":[{"parent":"69d4722f6204","children":[],"id":"9058d548b851","title":"当堆内存(Heap Space)没有足够空间存放新创建的对象时,会抛出
    "},{"parent":"69d4722f6204","children":[{"parent":"82077529709e","children":[],"id":"e5efc4b324bd","title":"请求创建一个超大对象,通常是一个大数组
    "},{"parent":"82077529709e","children":[],"id":"f6c6d999b2e2","title":"超出预期的访问量/数据量,通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值
    "},{"parent":"82077529709e","children":[],"id":"810ed21d175e","title":"过度使用终结器(Finalizer),该对象没有立即被 GC
    "},{"parent":"82077529709e","children":[],"id":"12f8716631c0","title":"内存泄漏(Memory Leak),大量对象引用没有释放,JVM 无法对其自动回收,常见于使用了 File 等资源没有回收
    "}],"id":"82077529709e","title":"场景
    "},{"parent":"69d4722f6204","children":[{"parent":"bb0032b74d7a","children":[],"id":"4508b1e1415b","title":"针对大部分情况,通常只需要通过 -Xmx 参数调高 JVM 堆内存空间即可
    "},{"parent":"bb0032b74d7a","children":[],"id":"8a21793fb810","title":"如果是超大对象,可以检查其合理性,比如是否一次性查询了数据库全部结果,而没有做结果数限制
    "},{"parent":"bb0032b74d7a","children":[],"id":"7970de1c13ce","title":"如果是业务峰值压力,可以考虑添加机器资源,或者做限流降级
    "},{"parent":"bb0032b74d7a","children":[],"id":"61721dad15fd","title":"如果是内存泄漏,需要找到持有的对象,修改代码设计,比如关闭没有释放的连接
    "}],"id":"bb0032b74d7a","title":"解决方案
    "}],"id":"69d4722f6204","title":"Java heap space
    "},{"parent":"e5b794712d9f","children":[{"parent":"16ea8d379757","children":[],"id":"089b25ad7859","title":"当 Java 进程花费 98% 以上的时间执行 GC,但只恢复了不到 2% 的内存,且该动作连续重复了 5 次,就会抛出
    "},{"parent":"16ea8d379757","children":[],"id":"4cccd7722816","title":"场景与解决方案与Java heap space类似
    "}],"id":"16ea8d379757","title":"GC overhead limit exceeded
    "},{"parent":"e5b794712d9f","children":[{"parent":"27f250898d47","children":[],"id":"b5735a767281","title":"该错误表示永久代(Permanent Generation)已用满,通常是因为加载的 class 数目太多或体积太大
    "},{"parent":"27f250898d47","children":[{"parent":"7adba9e7ee50","children":[],"id":"f92f654bc6f2","title":"程序启动报错,修改 -XX:MaxPermSize 启动参数,调大永久代空间
    "},{"parent":"7adba9e7ee50","children":[],"id":"a1ba084f84ae","title":"应用重新部署时报错,很可能是没有应用没有重启,导致加载了多份 class 信息,只需重启 JVM 即可解决
    "},{"parent":"7adba9e7ee50","children":[],"id":"ac1dd7e1efe5","title":"运行时报错,应用程序可能会动态创建大量 class,而这些 class 的生命周期很短暂,但是 JVM 默认不会卸载 class,可以设置 -XX:+CMSClassUnloadingEnabled 和 -XX:+UseConcMarkSweepGC这两个参数允许 JVM 卸载 class
    "},{"parent":"7adba9e7ee50","children":[],"id":"9908b8d183cb","title":"如果上述方法无法解决,可以通过 jmap 命令 dump 内存对象 jmap-dump:format=b,file=dump.hprof<process-id> ,然后利用 Eclipse MAT功能逐一分析开销最大的 classloader 和重复 class
    "}],"id":"7adba9e7ee50","title":"解决方案
    "}],"id":"27f250898d47","title":"Permgen space
    "},{"parent":"e5b794712d9f","children":[{"parent":"5d9794eb44e3","children":[],"id":"ff167c68cd26","title":"该错误表示 Metaspace 已被用满,通常是因为加载的 class 数目太多或体积太大
    "},{"parent":"5d9794eb44e3","children":[],"id":"9e30222f9cdd","title":"场景与解决方案与Permgen space类似,需注意调整元空间大小参数为 -XX:MaxMetaspaceSize
    "}],"id":"5d9794eb44e3","title":"Metaspace(元空间)
    "},{"parent":"e5b794712d9f","children":[{"parent":"88733d6fd9e1","children":[],"id":"4b3170dc49e7","title":"当 JVM 向底层操作系统请求创建一个新的 Native 线程时,如果没有足够的资源分配就会报此类错误
    "},{"parent":"88733d6fd9e1","children":[{"parent":"2de99f106fd9","children":[],"id":"d2894deebffe","title":"线程数超过操作系统最大线程数 ulimit 限制
    "},{"parent":"2de99f106fd9","children":[],"id":"6b4a4957f1f7","title":"线程数超过 kernel.pid_max(只能重启)
    "},{"parent":"2de99f106fd9","children":[],"id":"697aa13d5593","title":"Native 内存不足
    "}],"id":"2de99f106fd9","title":"场景
    "},{"parent":"88733d6fd9e1","children":[{"parent":"d6e225b4cdbd","children":[],"id":"8f7d0718c783","title":"升级配置,为机器提供更多的内存
    "},{"parent":"d6e225b4cdbd","children":[],"id":"fd893dac8e71","title":"降低 Java Heap Space 大小
    "},{"parent":"d6e225b4cdbd","children":[],"id":"96d3dc50bfcf","title":"修复应用程序的线程泄漏问题
    "},{"parent":"d6e225b4cdbd","children":[],"id":"0ab47fa12a24","title":"限制线程池大小
    "},{"parent":"d6e225b4cdbd","children":[],"id":"28f3069b69cc","title":"使用 -Xss 参数减少线程栈的大小
    "},{"parent":"d6e225b4cdbd","children":[],"id":"5b123c27cf17","title":"调高 OS 层面的线程最大数:执行 ulimia-a 查看最大线程数限制,使用 ulimit-u xxx 调整最大线程数限制
    "}],"id":"d6e225b4cdbd","title":"解决方案
    "}],"id":"88733d6fd9e1","title":"Unable to create new native thread
    "},{"parent":"e5b794712d9f","children":[{"parent":"a207e8502e59","children":[],"id":"ee75aeab507e","title":"虚拟内存(Virtual Memory)由物理内存(Physical Memory)和交换空间(Swap Space)两部分组成。当运行时程序请求的虚拟内存溢出时就会报 Outof swap space? 错误
    "},{"parent":"a207e8502e59","children":[{"parent":"e9aff14f85ee","children":[],"id":"f5d926e1a54d","title":"地址空间不足
    "},{"parent":"e9aff14f85ee","children":[],"id":"0ab3e7bd4ce0","title":"物理内存已耗光
    "},{"parent":"e9aff14f85ee","children":[],"id":"a719e6915e62","title":"应用程序的本地内存泄漏(native leak),例如不断申请本地内存,却不释放
    "},{"parent":"e9aff14f85ee","children":[],"id":"95178fc0d9c2","title":"执行 jmap-histo:live<pid> 命令,强制执行 Full GC;如果几次执行后内存明显下降,则基本确认为 Direct ByteBuffer 问题
    "}],"id":"e9aff14f85ee","title":"场景
    "},{"parent":"a207e8502e59","children":[{"parent":"69838d20af49","children":[],"id":"f6eb75443db8","title":"升级地址空间为 64 bit
    "},{"parent":"69838d20af49","children":[],"id":"4c2d62f88cd0","title":"使用 Arthas 检查是否为 Inflater/Deflater 解压缩问题,如果是,则显式调用 end 方法
    "},{"parent":"69838d20af49","children":[],"id":"8149b8d47a40","title":"Direct ByteBuffer 问题可以通过启动参数 -XX:MaxDirectMemorySize 调低阈值
    "},{"parent":"69838d20af49","children":[],"id":"67cf9ef9b29d","title":"升级服务器配置/隔离部署,避免争用
    "}],"id":"69838d20af49","title":"解决方案
    "}],"id":"a207e8502e59","title":"Out of swap space?
    "},{"parent":"e5b794712d9f","children":[{"parent":"fc5d0b351a1a","children":[],"id":"dcbc57e34a45","title":"有一种内核作业(Kernel Job)名为 Out of Memory Killer,它会在可用内存极低的情况下“杀死”(kill)某些进程。OOM Killer 会对所有进程进行打分,然后将评分较低的进程“杀死”,Killprocessorsacrifice child 错误不是由 JVM 层面触发的,而是由操作系统层面触发的
    "},{"parent":"fc5d0b351a1a","children":[{"parent":"959ac5e8a270","children":[],"id":"a97b65d73f88","title":"默认情况下,Linux 内核允许进程申请的内存总量大于系统可用内存,通过这种“错峰复用”的方式可以更有效的利用系统资源。
    然而,这种方式也会无可避免地带来一定的“超卖”风险。例如某些进程持续占用系统内存,然后导致其他进程没有可用内存。此时,系统将自动激活 OOM Killer,寻找评分低的进程,并将其“杀死”,释放内存资源
    "}],"id":"959ac5e8a270","title":"场景
    "},{"parent":"fc5d0b351a1a","children":[{"parent":"a7ada05bbd00","children":[],"id":"2b1d922217cf","title":"升级服务器配置/隔离部署,避免争用
    "},{"parent":"a7ada05bbd00","children":[],"id":"b9502a1fe60b","title":"OOM Killer 调优
    "}],"id":"a7ada05bbd00","title":"解决方案
    "}],"id":"fc5d0b351a1a","title":"Kill process or sacrifice child
    "},{"parent":"e5b794712d9f","children":[{"parent":"4dfd99c9bc90","children":[],"id":"52471cca5e33","title":"JVM 限制了数组的最大长度,该错误表示程序请求创建的数组超过最大长度限制
    "},{"parent":"4dfd99c9bc90","children":[{"parent":"7a7846a84d01","children":[],"id":"ef42a376573a","title":"检查代码,确认业务是否需要创建如此大的数组,是否可以拆分为多个块,分批执行
    "}],"id":"7a7846a84d01","title":"解决方案
    "}],"id":"4dfd99c9bc90","title":"Requested array size exceeds VM limit
    "},{"parent":"e5b794712d9f","children":[{"parent":"926260b12712","children":[],"id":"7deb1188a580","title":"Direct ByteBuffer 的默认大小为 64 MB,一旦使用超出限制,就会抛出 Directbuffer memory 错误
    "},{"parent":"926260b12712","children":[{"parent":"c8bb4e5de982","children":[],"id":"558937529cce","title":"Java 只能通过 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通过 Arthas 等在线诊断工具拦截该方法进行排查
    "},{"parent":"c8bb4e5de982","children":[],"id":"04d0566021d0","title":"检查是否直接或间接使用了 NIO,如 netty,jetty 等
    "},{"parent":"c8bb4e5de982","children":[],"id":"a5ff4af8fc72","title":"通过启动参数 -XX:MaxDirectMemorySize 调整 Direct ByteBuffer 的上限值
    "},{"parent":"c8bb4e5de982","children":[],"id":"eb97fd5380b3","title":"检查 JVM 参数是否有 -XX:+DisableExplicitGC 选项,如果有就去掉,因为该参数会使 System.gc()失效
    "},{"parent":"c8bb4e5de982","children":[],"id":"e19382936700","title":"检查堆外内存使用代码,确认是否存在内存泄漏;或者通过反射调用 sun.misc.Cleaner 的 clean() 方法来主动释放被 Direct ByteBuffer 持有的内存空间
    "},{"parent":"c8bb4e5de982","children":[],"id":"b338bbfe6fec","title":"内存容量确实不足,升级配置
    "}],"id":"c8bb4e5de982","title":"解决方案
    "}],"id":"926260b12712","title":"Direct buffer memory
    "}],"collapsed":false,"id":"e5b794712d9f","title":"OOM
    "},{"parent":"f01a444aff49","children":[],"id":"4fca236e0053","title":"内存泄露"},{"parent":"f01a444aff49","children":[],"id":"31b72a753814","title":"线程死锁
    "},{"parent":"f01a444aff49","children":[],"id":"f5310ade47dc","title":"锁争用
    "},{"parent":"f01a444aff49","children":[],"id":"be43a23fb08e","title":"Java进程消耗CPU过高
    "}],"id":"f01a444aff49","title":"JVM调优
    "},{"parent":"d61da867cb10","children":[{"parent":"5c6067b84b1e","children":[],"id":"03fed9bf17f7","title":"Jconsole"},{"parent":"5c6067b84b1e","children":[],"id":"b0ec90eb542b","title":"Jprofiler"},{"parent":"5c6067b84b1e","children":[],"id":"baa86063ec3e","title":"jvisualvm"},{"parent":"5c6067b84b1e","children":[],"id":"c7c342ec412e","title":"MAT"}],"id":"5c6067b84b1e","title":"JVM性能检测工具"},{"parent":"d61da867cb10","children":[{"parent":"9a58c2256f22","children":[],"id":"541fa6a430b7","title":"help dump"},{"parent":"9a58c2256f22","children":[],"id":"63ed06e60104","title":"生产机 dump"},{"parent":"9a58c2256f22","children":[],"id":"29df5ce1b166","title":"mat"},{"parent":"9a58c2256f22","children":[],"id":"a2815d62f70f","title":"jmap"},{"parent":"9a58c2256f22","children":[],"id":"aa872dbc3855","title":"-helpdump"}],"id":"9a58c2256f22","title":"内存泄露"},{"parent":"d61da867cb10","children":[{"parent":"dbe9489e1531","children":[],"id":"30af4ef10e6d","title":"topc -c"},{"parent":"dbe9489e1531","children":[],"id":"f413c8870538","title":"top -Hp pid"},{"parent":"dbe9489e1531","children":[{"parent":"d7a7bb2d1ce7","children":[],"id":"3d609323d047","title":"进制转换"}],"id":"d7a7bb2d1ce7","title":"jstack"},{"parent":"dbe9489e1531","children":[],"id":"28dc67e9bcc8","title":"cat
    "}],"id":"dbe9489e1531","title":"CPU100%"}],"collapsed":true,"id":"d61da867cb10","title":"JVM"},{"parent":"root","lineStyle":{"randomLineColor":"#FF8502"},"children":[],"id":"ecbf53fba47d","title":"多线程"},{"parent":"root","lineStyle":{"randomLineColor":"#F5479C"},"children":[{"parent":"d1383b5af44b","children":[{"parent":"6846db773567","children":[{"parent":"9cfcc064de09","children":[],"id":"3cc1cea3b5d2","title":"Spring 中的 Bean 默认都是单例的"}],"id":"9cfcc064de09","title":"单例模式"},{"parent":"6846db773567","children":[{"parent":"70e04d48b002","children":[],"id":"79b9ad909b14","title":"Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象"}],"id":"70e04d48b002","title":"工厂模式"},{"parent":"6846db773567","children":[{"parent":"18539d17e1a2","children":[],"id":"f10b087651f0","title":"Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller"}],"id":"18539d17e1a2","title":"适配器模式
    "},{"parent":"6846db773567","children":[{"parent":"aa0d772a111e","children":[],"id":"458c4c117473","title":"Spring AOP 功能的实现"}],"id":"aa0d772a111e","title":"代理设计模式
    "},{"parent":"6846db773567","children":[{"parent":"cf993fcce1fa","children":[],"id":"5a9fd93671a5","title":"Spring 事件驱动模型就是观察者模式很经典的一个应用"}],"id":"cf993fcce1fa","title":"观察者模式
    "},{"parent":"6846db773567","children":[],"id":"4761f7394f26","title":"... ..."}],"id":"6846db773567","title":"设计模式"},{"parent":"d1383b5af44b","children":[{"parent":"6be9b379c857","children":[{"image":{"w":721,"h":306,"url":"http://cdn.processon.com/60d5841c07912920c8095f17?e=1624609325&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:vvIb8jvxokB9aizRUTjAy8Dz4NI="},"parent":"8218ea06bbce","children":[{"parent":"f7aea39c3b24","children":[],"id":"408ac2ef4844","title":"Bean 容器找到配置文件中 Spring Bean 的定义"},{"parent":"f7aea39c3b24","children":[],"id":"34992c358614","title":"Bean 容器利用 Java Reflection API 创建一个Bean的实例"},{"parent":"f7aea39c3b24","children":[],"id":"e7981f427e63","title":"如果涉及到一些属性值 利用 set()方法设置一些属性值"},{"parent":"f7aea39c3b24","children":[],"id":"dbf75a139571","title":"如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入Bean的名字"},{"parent":"f7aea39c3b24","children":[],"id":"787517f23307","title":"如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例"},{"parent":"f7aea39c3b24","children":[],"id":"6466197a3ddf","title":"如果Bean实现了 BeanFactoryAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader 对象的实例"},{"parent":"f7aea39c3b24","children":[],"id":"feac889b1b8d","title":"与上面的类似,如果实现了其他 *.Aware接口,就调用相应的方法"},{"parent":"f7aea39c3b24","children":[],"id":"91b027b10223","title":"如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessBeforeInitialization() 方法"},{"parent":"f7aea39c3b24","children":[],"id":"b1af63401bf5","title":"如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法
    "},{"parent":"f7aea39c3b24","children":[],"id":"37fa23a9c16a","title":"如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法"},{"parent":"f7aea39c3b24","children":[],"id":"b57d01a401e0","title":"如果有和加载这个 Bean的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessAfterInitialization() 方法"},{"parent":"f7aea39c3b24","children":[],"id":"baf7bd961172","title":"当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法"},{"parent":"f7aea39c3b24","children":[],"id":"da536275f445","title":"当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法"}],"style":{"text-align":"center"},"id":"f7aea39c3b24","title":"Spring Bean 生命周期"}],"id":"8218ea06bbce","title":"生命周期"},{"parent":"6be9b379c857","children":[{"parent":"780f6b30ba11","children":[{"parent":"69fa69a6315a","children":[],"id":"847478f4b37e","title":"唯一 bean 实例,Spring 中的 bean 默认都是单例的
    "}],"id":"69fa69a6315a","title":"singleton
    "},{"parent":"780f6b30ba11","children":[{"parent":"61779d764f15","children":[],"id":"1b7c8dc3e783","title":"每次请求都会创建一个新的 bean 实例
    "}],"id":"61779d764f15","title":"prototype
    "},{"parent":"780f6b30ba11","children":[{"parent":"b84f40579383","children":[],"id":"a2c0221f2a17","title":"每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效
    "}],"id":"b84f40579383","title":"request
    "},{"parent":"780f6b30ba11","children":[{"parent":"2980654747ac","children":[],"id":"df20b0568d9b","title":"每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效
    "}],"id":"2980654747ac","title":"session
    "}],"id":"780f6b30ba11","title":"作用域"},{"parent":"6be9b379c857","children":[{"parent":"94d2392b941d","children":[],"id":"f6facb5c7bdf","title":"单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题
    "},{"parent":"94d2392b941d","children":[{"parent":"c50f84f9dd07","children":[],"id":"6a052cae7ac3","title":"在Bean对象中尽量避免定义可变的成员变量(不太现实)"},{"parent":"c50f84f9dd07","children":[],"id":"be83d8ee0d9c","title":"在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)"}],"id":"c50f84f9dd07","title":"解决方案"}],"id":"94d2392b941d","title":"单例Bean线程不安全"}],"id":"6be9b379c857","title":"Bean"},{"parent":"d1383b5af44b","children":[{"parent":"2bd94a15f271","children":[{"parent":"db24e1d06ef6","children":[],"id":"c975e1461b17","title":"循环依赖其实就是循环引用,一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用"}],"id":"db24e1d06ef6","title":"定义"},{"parent":"2bd94a15f271","children":[{"parent":"6429ee8470d0","children":[{"parent":"b444d530df33","children":[],"id":"fe4447498b81","title":"可以解决"}],"id":"b444d530df33","title":"单例setter注入
    "},{"parent":"6429ee8470d0","children":[{"parent":"a628a0c088fe","children":[],"id":"7d2affdf5098","title":"不能解决"}],"id":"a628a0c088fe","title":"多例setter注入"},{"parent":"6429ee8470d0","children":[{"parent":"52ccb73aaa1c","children":[],"id":"c5d72e9224fc","title":"不能解决"}],"id":"52ccb73aaa1c","title":"构造器注入"},{"parent":"6429ee8470d0","children":[{"parent":"e0ddcb98872d","children":[],"id":"8bc315c0e4f4","title":"有可能解决"}],"id":"e0ddcb98872d","title":"单例的代理对象注入
    "},{"parent":"6429ee8470d0","children":[{"parent":"c1883b1d1d04","children":[],"id":"30c4d71d67b7","title":"不能解决"}],"id":"c1883b1d1d04","title":"DependOn循环依赖
    "}],"id":"6429ee8470d0","title":"主要场景"},{"parent":"2bd94a15f271","children":[{"parent":"34656efe2153","children":[],"id":"07eedf3c5055","title":"一级缓存: 用于保存实例化、注入、初始化完成的bean实例
    "},{"parent":"34656efe2153","children":[],"id":"5389148434c4","title":"二级缓存: 用于保存实例化完成的bean实例"},{"parent":"34656efe2153","children":[],"id":"de57eefcdc3f","title":"三级缓存: 用于保存bean创建工厂,以便于后面扩展有机会创建代理对象"}],"id":"34656efe2153","title":"三级缓存"},{"parent":"2bd94a15f271","children":[{"image":{"w":720,"h":309,"url":"http://cdn.processon.com/60d58be3e0b34d7f1166296f?e=1624611315&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:Szb4rjzK4-RP973_dTYTIc1x1yg="},"parent":"7537dcdb4537","children":[],"style":{"text-align":"center"},"id":"813c5d07800b","title":"Spring解决循环依赖"}],"id":"7537dcdb4537","title":"Spring如何解决循环依赖?"},{"parent":"2bd94a15f271","children":[{"parent":"6809a28c84fb","children":[{"parent":"0b65efef98be","children":[],"id":"edb8657d8633","title":"使用@Lazy注解,延迟加载
    "},{"parent":"0b65efef98be","children":[],"id":"713bcafda8f8","title":"使用@DependsOn注解,指定加载先后关系
    "},{"parent":"0b65efef98be","children":[],"id":"ecde0ac2eff5","title":"修改文件名称,改变循环依赖类的加载顺序
    "}],"id":"0b65efef98be","title":"生成代理对象产生的循环依赖
    "},{"parent":"6809a28c84fb","children":[{"parent":"19fc35c94a10","children":[],"id":"a898194da78e","title":"找到@DependsOn注解循环依赖的地方,迫使它不循环依赖
    "}],"id":"19fc35c94a10","title":"使用@DependsOn产生的循环依赖
    "},{"parent":"6809a28c84fb","children":[{"parent":"67569155e5e1","children":[],"id":"fbefaf05f15b","title":"把bean改成单例"}],"id":"67569155e5e1","title":"多例循环依赖"},{"parent":"6809a28c84fb","children":[{"parent":"d1721015c62e","children":[],"id":"086c2cf4a1f1","title":"使用@Lazy注解解决"}],"id":"d1721015c62e","title":"构造器循环依赖"}],"id":"6809a28c84fb","title":"Spring无法解决的循环依赖怎么解决?"}],"id":"2bd94a15f271","title":"循环依赖"},{"parent":"d1383b5af44b","children":[{"parent":"7525c1be5308","children":[],"id":"2efd2dd7603f","title":"Spring是父容器,SpringMVC是子容器,Spring父容器中注册的Bean对SpringMVC子容器是可见的,反之则不行"}],"id":"7525c1be5308","title":"父子容器"},{"parent":"d1383b5af44b","children":[{"parent":"2d661386d933","children":[],"id":"9dedda0f87c3","title":"采用不同的连接器"},{"parent":"2d661386d933","children":[{"parent":"9478703f6033","children":[],"id":"906c6c2186d9","title":"共享链接"}],"id":"9478703f6033","title":"用AOP 新建立了一个 链接"},{"parent":"2d661386d933","children":[],"id":"c0c888325014","title":"ThreadLocal 当前事务"},{"parent":"2d661386d933","children":[],"id":"cdf3bf19398a","title":"前提是 关闭AutoCommit"}],"id":"2d661386d933","title":"事务实现原理"},{"parent":"d1383b5af44b","children":[{"parent":"3fc3209ef908","children":[{"parent":"00348542df62","children":[{"parent":"7e65680e758d","children":[],"id":"9d4a3af2166d","title":"如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务"}],"id":"7e65680e758d","title":"PROPAGATION_REQUIRED"},{"parent":"00348542df62","children":[{"parent":"fbcd20add78f","children":[],"id":"bb981fa897b5","title":"如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行"}],"id":"fbcd20add78f","title":"PROPAGATION_SUPPORTS"},{"parent":"00348542df62","children":[{"parent":"fa9c9e3e91b3","children":[],"id":"f0bd2ab03cb3","title":"如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)"}],"id":"fa9c9e3e91b3","title":"PROPAGATION_MANDATORY"}],"id":"00348542df62","title":"支持当前事务的情况"},{"parent":"3fc3209ef908","children":[{"parent":"be43db2ade73","children":[{"parent":"274fe413f9a9","children":[],"id":"a9835b9de879","title":"创建一个新的事务,如果当前存在事务,则把当前事务挂起"}],"id":"274fe413f9a9","title":"PROPAGATION_REQUIRES_NEW"},{"parent":"be43db2ade73","children":[{"parent":"6de124ee0323","children":[],"id":"73808be05652","title":"以非事务方式运行,如果当前存在事务,则把当前事务挂起"}],"id":"6de124ee0323","title":"PROPAGATION_NOT_SUPPORTED"},{"parent":"be43db2ade73","children":[{"parent":"b7f5bd597d94","children":[],"id":"aade3bc122d2","title":"以非事务方式运行,如果当前存在事务,则抛出异常"}],"id":"b7f5bd597d94","title":"PROPAGATION_NEVER"}],"id":"be43db2ade73","title":"不支持当前事务的情况"},{"parent":"3fc3209ef908","children":[{"parent":"c633fc129e54","children":[{"parent":"7f933d08995d","children":[],"id":"29d2241160ac","title":"如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED"}],"id":"7f933d08995d","title":"PROPAGATION_NESTED"}],"id":"c633fc129e54","title":"其他情况"}],"id":"3fc3209ef908","title":"事务的传播行为"},{"parent":"d1383b5af44b","children":[{"parent":"d09326c239bc","children":[{"parent":"0ddcc4fc0973","children":[],"id":"2b143ba58aa6","title":"实现类"}],"id":"0ddcc4fc0973","title":"静态代理"},{"parent":"d09326c239bc","children":[{"parent":"e88e55b4f4a1","children":[{"parent":"93e7c1779448","children":[{"parent":"2b790def7035","children":[{"parent":"8aadeb882937","children":[],"id":"b8325d0d990f","title":"调用具体方法的时候调用invokeHandler"}],"id":"8aadeb882937","title":"java反射机制生成一个代理接口的匿名类"}],"id":"2b790def7035","title":"实现接口"}],"id":"93e7c1779448","title":"JDK动态代理"},{"parent":"e88e55b4f4a1","children":[{"parent":"ad4aa9aa8708","children":[{"parent":"d62f18134c3b","children":[],"id":"f4f870f8a078","title":"修改字节码生成子类去处理"}],"id":"d62f18134c3b","title":"asm字节码编辑技术动态创建类 基于classLoad装载"}],"id":"ad4aa9aa8708","title":"cglib"}],"id":"e88e55b4f4a1","title":"动态代理"}],"id":"d09326c239bc","title":"AOP"},{"parent":"d1383b5af44b","children":[],"id":"321b75efaafa","title":"IOC"}],"collapsed":true,"id":"d1383b5af44b","title":"Spring
    "},{"parent":"root","lineStyle":{"randomLineColor":"#4D69FD"},"children":[{"parent":"93b7f849fef3","children":[{"parent":"2d833717c2db","children":[{"parent":"688bd93ca78f","children":[{"parent":"adb794ebf462","children":[],"id":"28b48a3fc7d9","title":"MVCC支持高并发、四个隔离级别(默认为可重复读)、支持事务操作、聚簇索引
    "}],"id":"adb794ebf462","title":"InnoDB
    "},{"parent":"688bd93ca78f","children":[{"parent":"9851e7c98465","children":[],"id":"6cda6ad38022","title":"全文索引、压缩、空间函数、崩溃后无法安全恢复
    "}],"id":"9851e7c98465","title":"MyISAM
    "}],"id":"688bd93ca78f","title":"常见"},{"parent":"2d833717c2db","children":[{"parent":"259d8b591849","children":[{"parent":"8af8eb9d1749","children":[],"id":"faac28b5b4bf","title":"只支持insert、select操作,适合日志和数据采集
    "}],"id":"8af8eb9d1749","title":"Archive
    "},{"parent":"259d8b591849","children":[{"parent":"246578695747","children":[],"id":"6b252c309014","title":"会丢弃所有插入数据,不做保存,记录Blackhole日志,可以用于复制数据库到备份库
    "}],"id":"246578695747","title":"Blackhole
    "},{"parent":"259d8b591849","children":[{"parent":"22c9f95864a3","children":[],"id":"c91fe24e6951","title":"可以将CSV文件作为MySQL表处理,不支持索引
    "}],"id":"22c9f95864a3","title":"CSV
    "},{"parent":"259d8b591849","children":[{"parent":"4c62ccaebd05","children":[],"id":"1707d3b04d53","title":"访问MySQL服务器的一个代理,创建远程到MySQL服务器的客户端连接,默认禁用
    "}],"id":"4c62ccaebd05","title":"Federated
    "},{"parent":"259d8b591849","children":[{"parent":"7c87e4ff16d1","children":[],"id":"66796cc84a6b","title":"数据保存在内存中,不需要磁盘I/O,重启后数据会丢失但是表结构会保留
    "}],"id":"7c87e4ff16d1","title":"Memory
    "},{"parent":"259d8b591849","children":[{"parent":"7e8ec900b7d9","children":[],"id":"260f221a82a2","title":"MyISAM变种,可以用于日志或数据仓库,已被放弃
    "}],"id":"7e8ec900b7d9","title":"Merge
    "},{"parent":"259d8b591849","children":[{"parent":"deaf720f74d6","children":[],"id":"f24022cd65fd","title":"集群引擎
    "}],"id":"deaf720f74d6","title":"NDB
    "}],"id":"259d8b591849","title":"其他(可做了解)"}],"collapsed":false,"id":"2d833717c2db","title":"存储引擎"},{"parent":"93b7f849fef3","children":[{"parent":"a5966d5788f2","children":[{"parent":"7adc8a1c18d2","children":[],"id":"a4370f67b89b","title":"binlog记录了数据库表结构和表数据变更,比如update/delete/insert/truncate/create
    "},{"parent":"7adc8a1c18d2","children":[],"id":"4aade95fc08c","title":"主要用来复制和恢复数据"}],"id":"7adc8a1c18d2","title":"binlog
    "},{"parent":"a5966d5788f2","children":[{"parent":"7577f673f85c","children":[],"id":"31aeaae43467","title":"在写入内存后会产生redo log,记录本次在某个页上做了什么修改
    "},{"parent":"7577f673f85c","children":[],"id":"142d34087f0c","title":"恢复写入内存但数据还没真正写到磁盘的数据,redo log记载的是物理变化,文件的体积很小,恢复速度很快
    "}],"id":"7577f673f85c","title":"redo log
    "},{"parent":"a5966d5788f2","children":[{"parent":"4814832d3190","children":[],"id":"c90806ae3f83","title":"undo log是逻辑日志,存储着修改之前的数据,相当于一个前版本
    "},{"parent":"4814832d3190","children":[],"id":"99805bbdffab","title":"用来回滚和多版本控制"}],"id":"4814832d3190","title":"undo log
    "},{"parent":"a5966d5788f2","children":[{"parent":"5dfacdc162c1","children":[],"id":"8d3250892fd5","title":"redo log 记录的是数据的物理变化,binlog 记录的是数据的逻辑变化"},{"parent":"5dfacdc162c1","children":[],"id":"42cefe6fd536","title":"redo log的作用是为持久化而生的,仅存储写入内存但还未刷到磁盘的数据;binlog的作用是复制和恢复而生的,保持主从数据库的一致性,如果整个数据库的数据都被删除了,可以通过binlog恢复,而redo log则不能
    "},{"parent":"5dfacdc162c1","children":[],"id":"5590b5b15841","title":"redo log是MySQL的InnoDB引擎所产生的;binlog无论MySQL用什么引擎,都会有的"},{"parent":"5dfacdc162c1","children":[{"parent":"fa7e2894f9c9","children":[{"parent":"b2f9cb6a36f3","children":[{"parent":"be9575df9c26","children":[],"id":"8b3e3c83afb7","title":"如果写redo log失败了,那我们就认为这次事务有问题,回滚,不再写binlog"},{"parent":"be9575df9c26","children":[],"id":"a1220b7a6966","title":"如果写redo log成功了,写binlog,写binlog写一半了,但失败了怎么办?我们还是会对这次的事务回滚,将无效的binlog给删除(因为binlog会影响从库的数据,所以需要做删除操作)"},{"parent":"be9575df9c26","children":[],"id":"122c9e026c22","title":"如果写redo log和binlog都成功了,那这次算是事务才会真正成功"}],"id":"be9575df9c26","title":"解析"},{"parent":"b2f9cb6a36f3","children":[{"parent":"be4d6b8613d4","children":[{"parent":"b17221822991","children":[],"id":"9383dc884572","title":"如果redo log写失败了,而binlog写成功了。那假设内存的数据还没来得及落磁盘,机器就挂掉了。那主从服务器的数据就不一致了。(从服务器通过binlog得到最新的数据,而主服务器由于redo log没有记载,没法恢复数据)"},{"parent":"b17221822991","children":[],"id":"4f824c63c2b0","title":"如果redo log写成功了,而binlog写失败了。那从服务器就拿不到最新的数据了"}],"id":"b17221822991","title":"MySQL需要保证redo log和binlog的数据是一致的"}],"id":"be4d6b8613d4","title":"结论"},{"parent":"b2f9cb6a36f3","children":[{"parent":"4c6de79ecd53","children":[{"parent":"9f54d23f7748","children":[{"parent":"4387d2b41401","children":[],"id":"a4c2899d7ffa","title":"阶段1:InnoDBredo log 写盘,InnoDB 事务进入 prepare(做好准备) 状态"},{"parent":"4387d2b41401","children":[],"id":"05dea48990c1","title":"阶段2:binlog 写盘,InooDB 事务进入 commit(提交) 状态"},{"parent":"4387d2b41401","children":[],"id":"7e939c251666","title":"每个事务binlog的末尾,会记录一个 XID event,标志着事务是否提交成功,也就是说,恢复过程中,binlog 最后一个 XID event 之后的内容都应该被 purge(清除)"}],"id":"4387d2b41401","title":"过程"}],"id":"9f54d23f7748","title":"MySQL通过两阶段提交来保证redo log和binlog的数据是一致的"}],"id":"4c6de79ecd53","title":"保持一致性的方法"}],"id":"b2f9cb6a36f3","title":"引申问题:在写入某一个log,失败了,那会怎么办?比如先写redo log,再写binlog"}],"id":"fa7e2894f9c9","title":"redo log事务开始的时候,就开始记录每次的变更信息,而binlog是在事务提交的时候才记录"}],"id":"5dfacdc162c1","title":"binlog与redo log的区别"},{"parent":"a5966d5788f2","children":[{"parent":"cea3499ae4ce","children":[{"parent":"e50c2afacca9","children":[],"id":"18fce611a383","title":"默认不开启,需要手动将参数设置为ON"}],"id":"e50c2afacca9","title":"慢查询日志,记录所有执行时间超过long_query_time的所有查询或不使用索引的查询
    "}],"id":"cea3499ae4ce","title":"slow log
    "}],"collapsed":false,"id":"a5966d5788f2","title":"log"},{"parent":"93b7f849fef3","children":[{"parent":"5210cc3e2201","children":[{"parent":"9901dd2630fa","children":[{"parent":"f2714fc25cf8","children":[{"parent":"72e55f7388bc","children":[],"id":"efa64c497a6e","title":"B树在提高了IO性能的同时并没有解决元素遍历的效率低下的问题,为了解决这个问题产生了B+树。B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而在数据库中基于范围的查询是非常频繁的,但B树不支持这样的操作或者说效率太低"}],"id":"72e55f7388bc","title":"为什么选用B+Tree不选择B-Tree?"},{"parent":"f2714fc25cf8","children":[{"parent":"118574a529fa","children":[{"parent":"e98c2b0bce63","children":[],"id":"70ba3b2ec259","title":"和索引中的所有列进行匹配"}],"id":"e98c2b0bce63","title":"全值匹配"},{"parent":"118574a529fa","children":[{"parent":"f6fe4bbe8879","children":[],"id":"6650dea74984","title":"只使用索引的第一列"}],"id":"f6fe4bbe8879","title":"匹配最左前缀"},{"parent":"118574a529fa","children":[{"parent":"a8e32170dad9","children":[],"id":"0736d8171b6b","title":"只匹配某一列值的开头部分"}],"id":"a8e32170dad9","title":"匹配列前缀"},{"parent":"118574a529fa","children":[{"parent":"c3ecae94375c","children":[],"id":"d6ce88217808","title":"查找范围区间"}],"id":"c3ecae94375c","title":"匹配范围值"},{"parent":"118574a529fa","children":[{"parent":"f7a85f0b29a2","children":[],"id":"f2c263321a5b","title":"第一列全匹配,第二列匹配范围区间"}],"id":"f7a85f0b29a2","title":"精确匹配某一列并范围匹配另外一列"},{"parent":"118574a529fa","children":[{"parent":"00e02f358513","children":[],"id":"7eaacfcc4407","title":"覆盖索引"}],"id":"00e02f358513","title":"只访问索引的查询"}],"id":"118574a529fa","title":"适用范围"}],"id":"f2714fc25cf8","title":"B+Tree 索引"},{"parent":"9901dd2630fa","children":[{"parent":"079407222137","children":[{"parent":"2961e3444d75","children":[],"id":"9d63f5055547","title":"只有精确匹配索引所有列的查询才有效"}],"id":"2961e3444d75","title":"等值查询"}],"id":"079407222137","title":"Hash 索引"},{"parent":"9901dd2630fa","children":[{"parent":"ac2bb051dc41","children":[],"id":"b1fc3ee864fd","title":"MyISAM表支持,可以用作地理数据存储"}],"id":"ac2bb051dc41","title":"R- Tree 索引(空间数据索引)"},{"parent":"9901dd2630fa","children":[{"parent":"98bc7018f2e8","children":[],"id":"74d8f80862a6","title":"MyISAM表支持,查找文本中的关键字"}],"id":"98bc7018f2e8","title":"全文索引"}],"id":"9901dd2630fa","title":"常见索引"},{"parent":"5210cc3e2201","children":[{"parent":"eb2a05c840d5","children":[],"id":"38870688a881","title":"InnoDB通过主键聚集数据,若没有主键则会选择一个唯一非空索引代替,若都不存在则会隐式定义一个主键来作为聚簇索引"}],"id":"eb2a05c840d5","title":"聚簇索引"},{"parent":"5210cc3e2201","children":[{"parent":"c1563189924d","children":[],"id":"13d6a347d2e2","title":"会多进行一次扫描(回表操作)"}],"id":"c1563189924d","title":"非聚簇索引"},{"parent":"5210cc3e2201","children":[{"parent":"1c121f6da170","children":[{"parent":"e0e8ecd756d7","children":[],"id":"f3837dae5ad5","title":"索引不能是表达式的一部分,也不能是函数的参数
    "}],"id":"e0e8ecd756d7","title":"独立的列"},{"parent":"1c121f6da170","children":[{"parent":"a3bd55e14de1","children":[],"id":"043d3ebd14c6","title":"在需要使用多列作为查询条件时,联合索引比使用多个单列索引性能更好
    "}],"id":"a3bd55e14de1","title":"多列索引"},{"parent":"1c121f6da170","children":[{"parent":"d28f309facf7","children":[],"id":"cf867ae6885b","title":"将选择行最强的索引列放在最前面
    "},{"parent":"d28f309facf7","children":[],"id":"977d01e8182f","title":"索引的选择性:不重复的索引值和记录总数的对比
    "}],"id":"d28f309facf7","title":"索引列的顺序"},{"parent":"1c121f6da170","children":[{"parent":"6ed32658245b","children":[],"id":"a735cff03984","title":"对BLOG、TEXT、VARCHAR类型的列,使用前缀索引,索引开始的部分字符
    "},{"parent":"6ed32658245b","children":[],"id":"9af199030361","title":"前缀索引的长度选取,需根据索引的选择性来确定
    "}],"id":"6ed32658245b","title":"前缀索引"},{"parent":"1c121f6da170","children":[{"parent":"861978fd7ef6","children":[],"id":"94eacfaadf23","title":"索引包含所有需要查询的字段值"},{"parent":"861978fd7ef6","children":[{"parent":"5d4e9d8774fd","children":[],"id":"5d9d32175efc","title":"索引远小于数据行的大小,只读取索引能够减少数据访问量
    "},{"parent":"5d4e9d8774fd","children":[],"id":"858036b36f18","title":"不用回表"}],"id":"5d4e9d8774fd","title":"优点"}],"id":"861978fd7ef6","title":"覆盖索引"}],"id":"1c121f6da170","title":"索引优化"},{"parent":"5210cc3e2201","children":[{"parent":"a88af8d55657","children":[],"id":"d05b0626264c","title":"大大减少了服务器需要扫描的数据行数"},{"parent":"a88af8d55657","children":[],"id":"f685636c3846","title":"帮助服务器避免进行排序和分组,以及避免创建临时表"},{"parent":"a88af8d55657","children":[],"id":"3df58d5d4a3b","title":"将随机 I/O 变为顺序 I/O"}],"id":"a88af8d55657","title":"索引的优点"},{"parent":"5210cc3e2201","children":[{"parent":"1792cb752801","children":[{"parent":"cef88efe00e5","children":[],"id":"c1e59b66fefb","title":"因为如果不是覆盖索引需要回表"}],"id":"cef88efe00e5","title":"对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效"},{"parent":"1792cb752801","children":[],"id":"64956008bace","title":"对于中到大型的表,索引非常有效"},{"parent":"1792cb752801","children":[],"id":"4070060fdb7d","title":"对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术"}],"id":"1792cb752801","title":"使用条件"}],"collapsed":false,"id":"5210cc3e2201","title":"索引"},{"parent":"93b7f849fef3","children":[{"parent":"1a7dcec2ca47","children":[{"parent":"85c67aaf34a6","children":[{"parent":"35c936f7bdfa","children":[],"id":"a596da0b2488","title":"SIMPLE 简单查询"},{"parent":"35c936f7bdfa","children":[],"id":"f4b9fe60c553","title":"UNION 联合查询"},{"parent":"35c936f7bdfa","children":[],"id":"5f4c581eaea4","title":"SUBQUERY 子查询"}],"id":"35c936f7bdfa","title":"select_type"},{"parent":"85c67aaf34a6","children":[{"parent":"46d559313322","children":[],"id":"c4ee393f6a07","title":"查询的表"}],"id":"46d559313322","title":"table"},{"parent":"85c67aaf34a6","children":[{"parent":"a7b85d2883a5","children":[],"id":"b616d85f45b2","title":"system"},{"parent":"a7b85d2883a5","children":[{"parent":"35007f4ba57f","children":[],"id":"f1988a49fc81","title":"只有一条查询结果&主键/唯一索引"}],"id":"35007f4ba57f","title":"const"},{"parent":"a7b85d2883a5","children":[{"parent":"ad8c20fd30de","children":[],"id":"96881f188e51","title":"链接查询&主键/唯一索引&只有一条查询结果"}],"id":"ad8c20fd30de","title":"eq_ref"},{"parent":"a7b85d2883a5","children":[{"parent":"f42e8c179697","children":[],"id":"f02141ac36a6","title":"非唯一索引"}],"id":"f42e8c179697","title":"ref"},{"parent":"a7b85d2883a5","children":[{"parent":"43897738682a","children":[],"id":"910cd850cb27","title":"使用索引进行范围查询时"}],"id":"43897738682a","title":"range"},{"parent":"a7b85d2883a5","children":[{"parent":"8fef129a816a","children":[],"id":"200bdd342f1e","title":"查询的字段时索引的一部分,覆盖索引"},{"parent":"8fef129a816a","children":[],"id":"462b089e56d4","title":"使用主键排序"}],"id":"8fef129a816a","title":"index"},{"parent":"a7b85d2883a5","children":[{"parent":"1bb5aaa0dd0d","children":[],"id":"764244bd2a17","title":"全表扫描"}],"id":"1bb5aaa0dd0d","title":"all"}],"id":"a7b85d2883a5","title":"type"},{"parent":"85c67aaf34a6","children":[{"parent":"b31b3a54b63b","children":[],"id":"dde0162686c2","title":"可选择的索引"}],"id":"b31b3a54b63b","title":"possible_keys"},{"parent":"85c67aaf34a6","children":[{"parent":"81ad0f7b17de","children":[],"id":"905080a6f852","title":"实际使用的索引"}],"id":"81ad0f7b17de","title":"key"},{"parent":"85c67aaf34a6","children":[{"parent":"e313359e3213","children":[],"id":"9094ee686fb6","title":"扫描的行数"}],"id":"e313359e3213","title":"rows"}],"id":"85c67aaf34a6","title":"使用 Explain 分析 Select 查询语句
    "},{"parent":"1a7dcec2ca47","children":[{"parent":"766730b3f63f","children":[{"parent":"e4c6eb7dee1d","children":[],"id":"f00841c92b2d","title":"只查询必要的列,对使用*永远持怀疑态度"},{"parent":"e4c6eb7dee1d","children":[],"id":"6396af513501","title":"只返回必要的行,使用Limit限制返回行数"},{"parent":"e4c6eb7dee1d","children":[],"id":"e69aa7cfb00a","title":"缓存重复查询的数据,例如使用redis"}],"id":"e4c6eb7dee1d","title":"减少请求的数据量"},{"parent":"766730b3f63f","children":[{"parent":"64889a19144a","children":[],"id":"e3d6b04e7263","title":"使用索引覆盖查询"}],"id":"64889a19144a","title":"减少数据库扫描的行数"}],"id":"766730b3f63f","title":"优化数据访问"},{"parent":"1a7dcec2ca47","children":[{"parent":"879ad88fe515","children":[],"id":"4fcee3d0c287","title":"有时候一个复杂的查询并没有多个简单查询执行迅速"},{"parent":"879ad88fe515","children":[],"id":"344a37829ef0","title":"对于大查询,可以使用切分查询,每次只返回一小部分"},{"parent":"879ad88fe515","children":[],"id":"fd9207da3a3a","title":"对于关联查询,可根据情况进行分解,在应用程序中进行关联"},{"parent":"879ad88fe515","children":[],"id":"27dc6dbe2891","title":"对于大分页等场景, 可采用延迟关联
    "}],"id":"879ad88fe515","title":"重构查询方式"}],"collapsed":false,"id":"1a7dcec2ca47","title":"查询性能优化"},{"parent":"93b7f849fef3","children":[{"parent":"f349bf5b5c84","children":[{"parent":"8a0fefaa1a67","children":[],"id":"9efbed658957","title":"原子性
    "},{"parent":"8a0fefaa1a67","children":[],"id":"1fa9df39ec26","title":"一致性"},{"parent":"8a0fefaa1a67","children":[],"id":"814aae5b441a","title":"隔离性"},{"parent":"8a0fefaa1a67","children":[],"id":"98bb1a37de0d","title":"持久性"}],"collapsed":false,"id":"8a0fefaa1a67","title":"ACID"},{"parent":"f349bf5b5c84","children":[{"parent":"d4c5e6c7b97d","children":[{"parent":"46a0e40a540b","children":[],"id":"97fc28724d63","title":"事务读取未提交的数据"}],"id":"46a0e40a540b","title":"脏读"},{"parent":"d4c5e6c7b97d","children":[{"parent":"0a6b6f451a92","children":[],"id":"80ae6dfb6f8c","title":"一个事务内,多次读同一数据。A事务未结束,B事务访问同一数据,A事务读取两次可能不一致
    "}],"id":"0a6b6f451a92","title":"不可重复读"},{"parent":"d4c5e6c7b97d","children":[{"parent":"c653b25154d9","children":[],"id":"dd1fc239cca6","title":"一个事务在前后两次查询同一范围的时候,后一次查询看到了前一次查询没有看到的行。事务A 按照一定条件进行数据读取, 期间事务B 插入了相同搜索条件的新数据,事务A再次按照原先条件进行读取时,发现了事务B 新插入的数据
    "}],"id":"c653b25154d9","title":"幻读"},{"parent":"d4c5e6c7b97d","children":[{"parent":"ca82f84e0c45","children":[],"id":"0bae386e19f5","title":"一个事务的更新操作会被另一个事务的更新操作所覆盖"},{"parent":"ca82f84e0c45","children":[],"id":"b317c67d4337","title":"例如:T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改"}],"id":"ca82f84e0c45","title":"丢失更新"}],"collapsed":false,"id":"d4c5e6c7b97d","title":"脏读、不可重复读、幻读、丢失更新
    "},{"parent":"f349bf5b5c84","children":[{"parent":"54cad68df1e1","children":[{"parent":"187fad689217","children":[],"id":"40001f009aca","title":"事务可以读取未提交数据(脏读)
    "},{"parent":"187fad689217","children":[],"id":"74d2f0b7e8ff","title":"存在脏读、不可重复读、幻读"}],"id":"187fad689217","title":"读未提交
    "},{"parent":"54cad68df1e1","children":[{"parent":"831af13c1fe3","children":[],"id":"7e9293ed801e","title":"事务只可以读取已经提交的事务所做的修改
    "},{"parent":"831af13c1fe3","children":[],"id":"7214bb4bdc0e","title":"存在不可重复读、幻读"}],"id":"831af13c1fe3","title":"读已提交
    "},{"parent":"54cad68df1e1","children":[{"parent":"ddabe3557266","children":[],"id":"a6098ce57c11","title":"同一个事务多次读取同样记录结果一致
    "},{"parent":"ddabe3557266","children":[],"id":"82158205dff5","title":"存在幻读"},{"parent":"ddabe3557266","children":[],"id":"2d11fbd7845e","title":"InnoDB默认级别
    "}],"id":"ddabe3557266","title":"可重复读
    "},{"parent":"54cad68df1e1","children":[{"parent":"329045116b43","children":[],"id":"f201ca426fb5","title":"读取每一行数据上都加锁
    "},{"parent":"329045116b43","children":[],"id":"9040c49f6995","title":"存在加锁读"}],"id":"329045116b43","title":"可串行化
    "}],"collapsed":false,"id":"54cad68df1e1","title":"事务隔离级别"},{"parent":"f349bf5b5c84","children":[{"parent":"0c3fe336f085","children":[{"parent":"e1911d4cb48b","children":[{"parent":"010c32dd6618","children":[],"id":"7abe6c3095a9","title":"允许事务读一行数据"}],"id":"010c32dd6618","title":"共享锁(读锁)"},{"parent":"e1911d4cb48b","children":[{"parent":"e440236253d1","children":[],"id":"27e41f2eeea3","title":"允许事务删除或更新一行数据"}],"id":"e440236253d1","title":"排他锁(写锁)"},{"parent":"e1911d4cb48b","children":[{"parent":"ae79dcf5ed09","children":[],"id":"b09a0093dd92","title":"事务想要获取一张表中某几行的共享锁
    "}],"id":"ae79dcf5ed09","title":"意向共享锁"},{"parent":"e1911d4cb48b","children":[{"parent":"a843c2d1177e","children":[],"id":"470c8fe9653b","title":"事务想要获取一张表中某几行的排他锁"}],"id":"a843c2d1177e","title":"意向排他锁"}],"id":"e1911d4cb48b","title":"锁类型
    "},{"parent":"0c3fe336f085","children":[{"parent":"3a31e35e17d1","children":[{"parent":"8458c31623f5","children":[],"id":"2fb5f24d4ca4","title":"锁定整张表,是开销最小的策略"},{"parent":"8458c31623f5","children":[],"id":"fc59fa7dd010","title":"开销小,加锁快;无死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低"}],"id":"8458c31623f5","title":"表锁"},{"parent":"3a31e35e17d1","children":[{"parent":"85be4bb1b97c","children":[],"id":"7a8a1338b899","title":"行级锁只对用户正在访问的行进行锁定"},{"parent":"85be4bb1b97c","children":[],"id":"1fe3439f48cb","title":"开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高"},{"parent":"85be4bb1b97c","children":[{"parent":"767fe9aba9f7","children":[],"id":"e87c3357ae96","title":"当行锁涉及到索引失效的时候,会触发表锁的行为"}],"id":"767fe9aba9f7","title":"升级行为"}],"id":"85be4bb1b97c","title":"行锁"},{"parent":"3a31e35e17d1","children":[],"id":"30d6e73385cf","title":"间隙锁"}],"id":"3a31e35e17d1","title":"锁粒度"}],"id":"0c3fe336f085","title":"锁"},{"parent":"f349bf5b5c84","children":[{"parent":"eed5a9459b5a","children":[{"parent":"ce09c69be50f","children":[{"parent":"eed8dbe525bf","children":[],"id":"8e7afdfa1b51","title":"像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读。它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁"}],"id":"eed8dbe525bf","title":"当前读"},{"parent":"ce09c69be50f","children":[{"parent":"64325b6f0da8","children":[],"id":"2025e92bff9a","title":"不加锁的select操作就是快照读。快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。快照读基于MVCC,为了提高并发性能的考虑
    "}],"id":"64325b6f0da8","title":"快照读"}],"id":"ce09c69be50f","title":"当前读与快照读"},{"parent":"eed5a9459b5a","children":[{"parent":"287cd593da0c","children":[{"parent":"9d63e35bb5f6","children":[],"id":"2c3c0a528cb3","title":"存储的每次对某条聚簇索引记录进行修改的时候的事务id"}],"id":"9d63e35bb5f6","title":"trx_id"},{"parent":"287cd593da0c","children":[{"parent":"01688368c063","children":[],"id":"1a6030a34904","title":"一个指针,它指向这条聚簇索引记录的上一个版本的位置
    "}],"id":"01688368c063","title":"roll_pointer
    "}],"id":"287cd593da0c","title":"聚簇索引中的隐藏列"},{"parent":"eed5a9459b5a","children":[{"parent":"629025098291","children":[],"id":"fd5bc24b1ab0","title":"Read View 保存了当前事务开启时所有活跃的事务列表"}],"id":"629025098291","title":"ReadView
    "},{"parent":"eed5a9459b5a","children":[{"parent":"0a9f4b6b3fd8","children":[],"id":"2fda9cd25a51","title":"获取事务自己的版本号,即 事务ID
    "},{"parent":"0a9f4b6b3fd8","children":[],"id":"69d637f03ec4","title":"获取 ReadView
    "},{"parent":"0a9f4b6b3fd8","children":[{"parent":"d234974b39a2","children":[{"parent":"00c306141cc3","children":[],"id":"6793592fd10e","title":"直接读取最新版本ReadView
    "}],"id":"00c306141cc3","title":"读未提交
    "},{"parent":"d234974b39a2","children":[{"parent":"cd3375057c84","children":[],"id":"d9b62e5bee77","title":"每次查询的开始都会生成一个独立的ReadView
    "}],"id":"cd3375057c84","title":"读已提交
    "},{"parent":"d234974b39a2","children":[{"parent":"b9c1beaa17b7","children":[],"id":"37c1b07a6706","title":"可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView
    "}],"id":"b9c1beaa17b7","title":"可重复读
    "}],"id":"d234974b39a2","title":"查询得到的数据,然后与 ReadView 中的事务版本号进行比较
    "},{"parent":"0a9f4b6b3fd8","children":[],"id":"0931e9c7e939","title":"如果不符合 ReadView 规则, 那么就需要 UndoLog 中历史快照
    "},{"parent":"0a9f4b6b3fd8","children":[],"id":"ed2a683bfd62","title":"最后返回符合规则的数据
    "}],"id":"0a9f4b6b3fd8","title":"实现原理"}],"id":"eed5a9459b5a","title":"MVCC"}],"collapsed":false,"id":"f349bf5b5c84","title":"事务"},{"parent":"93b7f849fef3","children":[{"parent":"7bb42e3d47e8","children":[{"parent":"f04740c1a26a","children":[{"parent":"6344bab34462","children":[],"id":"e844fe3ee99d","title":"借助第三方工具pt-online-schema-change"}],"id":"6344bab34462","title":"修改为bigint"},{"parent":"f04740c1a26a","children":[],"id":"c9cc52e3755f","title":"一般用不完就分库分表了, 使用全局唯一id"}],"id":"f04740c1a26a","title":"自增id用完了"},{"parent":"7bb42e3d47e8","children":[{"parent":"827ace408eb2","children":[],"id":"0662d7098e60","title":"设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count参数,减少binlog的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。

    将sync_binlog 设置为大于1的值(比较常见是100~1000)。这样做的风险是,主机掉电时会丢binlog日志。

    将innodb_flush_log_at_trx_commit设置为2。这样做的风险是,主机掉电的时候会丢数据。"}],"id":"827ace408eb2","title":"IO性能瓶颈"}],"id":"7bb42e3d47e8","title":"常见问题"}],"collapsed":true,"id":"93b7f849fef3","title":"MySQL"},{"parent":"root","lineStyle":{"randomLineColor":"#F5479C"},"children":[{"parent":"68e2d53f78c0","children":[{"parent":"a2ba7f2839e7","children":[],"id":"332a1e1ff9de","title":"秒杀的库存扣减"},{"parent":"a2ba7f2839e7","children":[],"id":"fdc0cda55b5c","title":"APP首页的访问流量高峰"},{"parent":"a2ba7f2839e7","children":[],"id":"235928a55a4d","title":"避免数据库打崩"}],"id":"a2ba7f2839e7","title":"为什么使用Redis"},{"parent":"68e2d53f78c0","children":[{"parent":"1bbcd515493d","children":[{"parent":"d6e64200f130","children":[{"parent":"c1b235ccc3a0","children":[],"id":"af253a7e312d","title":"key-value,类似ArrayList"}],"id":"c1b235ccc3a0","title":"String"},{"parent":"d6e64200f130","children":[],"id":"805ec0ae37ce","title":"Hash"},{"parent":"d6e64200f130","children":[],"id":"18c40debca54","title":"set"},{"parent":"d6e64200f130","children":[],"id":"5aba119e78a1","title":"zset"},{"parent":"d6e64200f130","children":[],"id":"466e3ae93922","title":"List"}],"id":"d6e64200f130","title":"必会项"},{"parent":"1bbcd515493d","children":[{"parent":"e08a90c96b17","children":[{"parent":"aac9ee1c9de2","children":[],"id":"6529bc24a01c","title":"统计网站UV(独立访客,每个用户每天只记录一次)"}],"id":"aac9ee1c9de2","title":"HyperLogLog
    "},{"parent":"e08a90c96b17","children":[{"parent":"e9b70d2f5e4a","children":[],"id":"09ba2789d093","title":"计算地理位置,类似功能附近的人"}],"id":"e9b70d2f5e4a","title":"Geo"},{"parent":"e08a90c96b17","children":[{"parent":"36770e0d1f7f","children":[],"id":"951025427dd0","title":"消息的多播,发布/订阅,无法持久化"}],"id":"36770e0d1f7f","title":"Pub/Sub"},{"parent":"e08a90c96b17","children":[{"parent":"504b0480286f","children":[],"id":"ff3c30b3b218","title":"用户签到,短视频属性(特效,加锁),用户在线状态,活跃状态
    "}],"id":"504b0480286f","title":"BitMap"},{"parent":"e08a90c96b17","children":[{"parent":"750183397ab0","children":[{"parent":"d912173e6c29","children":[],"id":"c0d25c9698a3","title":"缓存穿透"},{"parent":"d912173e6c29","children":[],"id":"cea22ee9fb6b","title":"海量数据去重"}],"id":"d912173e6c29","title":"使用场景"},{"parent":"750183397ab0","children":[{"parent":"d1cce1828064","children":[{"parent":"1c2955281250","children":[],"id":"ebe498b35f4e","title":"可以通过建立一个白名单来存储可能会误判的元素"}],"id":"1c2955281250","title":"存在误判"},{"parent":"d1cce1828064","children":[{"parent":"e4d933c94529","children":[],"id":"0f690c41bf85","title":"可以采用Counting Bloom Filter"}],"id":"e4d933c94529","title":"删除困难"}],"id":"d1cce1828064","title":"缺点"},{"parent":"750183397ab0","children":[{"parent":"3ee30b7a6a80","children":[],"id":"c672a0977842","title":"redisson"},{"parent":"3ee30b7a6a80","children":[],"id":"3525627855c4","title":"guava"}],"id":"3ee30b7a6a80","title":"现成的轮子"},{"parent":"750183397ab0","children":[{"parent":"e23b985c75be","children":[{"parent":"015aee9efba2","children":[],"id":"7a36e20faba7","title":"可进行设置, 默认值为0.03"}],"id":"015aee9efba2","title":"预估数据量n以及期望的误判率fpp"},{"parent":"e23b985c75be","children":[{"parent":"2aa25fd1f61c","children":[{"parent":"2b742b98e5af","children":[],"style":{"font-size":19},"id":"6e1979c1417e","title":""}],"id":"2b742b98e5af","title":"Bit数组大小"},{"parent":"2aa25fd1f61c","children":[{"parent":"744b38ebf3de","children":[],"style":{"font-size":20},"id":"19690dd504f2","title":""}],"id":"744b38ebf3de","title":"哈希函数选择"}],"id":"2aa25fd1f61c","title":"hash函数的选取以及bit数组的大小"}],"id":"e23b985c75be","title":"实现"}],"id":"750183397ab0","title":"BloomFilter (布隆过滤器)"},{"parent":"e08a90c96b17","children":[{"parent":"a3817cda1299","children":[],"id":"74b95271d80f","title":"可持久化的消息队列,支持多播"}],"id":"a3817cda1299","title":"Stream"}],"id":"e08a90c96b17","title":"加分项"}],"id":"1bbcd515493d","title":"数据结构"},{"parent":"68e2d53f78c0","children":[{"parent":"7742814a28a8","children":[{"parent":"a5a4136392f3","children":[{"parent":"dac8a72c8af3","children":[],"id":"2e0bddd4ebac","title":"Redis中大量key同一时间失效, 导致大量请求落库
    "}],"id":"dac8a72c8af3","title":"概念"},{"parent":"a5a4136392f3","children":[{"parent":"4f3f99b22654","children":[{"parent":"cae81e94657c","children":[],"id":"f458c0739724","title":"加随机过期时间, 避免大量key同时失效"},{"parent":"cae81e94657c","children":[],"id":"daaa9f606c2b","title":"设置热点数据永不过期
    "},{"parent":"cae81e94657c","children":[],"id":"1d88c6e21c3c","title":"如果Redis是集群部署, 将热点数据均匀分布在不同的Redis库中也能避免全部失效的问题"}],"id":"cae81e94657c","title":"预防方案"},{"parent":"4f3f99b22654","children":[{"parent":"c3bea9f91a7e","children":[],"id":"a7313b4d8479","title":"设置本地缓存(ehcache)+限流(hystrix),尽量避免请求过多打入数据库导致数据库宕机
    "}],"id":"c3bea9f91a7e","title":"万一发生"},{"parent":"4f3f99b22654","children":[{"parent":"88f98afe7a1c","children":[],"id":"921be9f40b92","title":"redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据
    "}],"id":"88f98afe7a1c","title":"挂掉以后"}],"id":"4f3f99b22654","title":"解决方案"}],"id":"a5a4136392f3","title":"缓存雪崩"},{"parent":"7742814a28a8","children":[{"parent":"16f48041dfe8","children":[{"parent":"18dad091e32b","children":[],"id":"d4a29ef74f33","title":"缓存和数据库中都没有的数据,而用户不断发起请求, 导致数据库压力过大,严重会击垮数据库
    "}],"id":"18dad091e32b","title":"概念"},{"parent":"16f48041dfe8","children":[{"parent":"c83b9f04cec3","children":[{"parent":"f1ca0b7df56c","children":[],"id":"06abfdcea127","title":"接口层增加校验,比如用户鉴权校验,参数做校验,存在不合法的参数直接做短路操作"},{"parent":"f1ca0b7df56c","children":[{"parent":"0a8f16c78524","children":[],"id":"77c829f772d4","title":"不存在则不请求"}],"id":"0a8f16c78524","title":"布隆过滤器"}],"id":"f1ca0b7df56c","title":"过滤方式"},{"parent":"c83b9f04cec3","children":[{"parent":"e9827ebf2fbc","children":[],"id":"f58a9e45bbfb","title":"将对应Key的Value对写为null、或其他提示信息,缓存有效时间可以设置短点,如30秒"}],"id":"e9827ebf2fbc","title":"临时返回"}],"id":"c83b9f04cec3","title":"解决方案"}],"id":"16f48041dfe8","title":"缓存穿透"},{"parent":"7742814a28a8","children":[{"parent":"89e2121fac66","children":[{"parent":"72782b925676","children":[],"id":"ccdc7563e860","title":"一个非常热点的Key,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库
    "}],"id":"72782b925676","title":"概念"},{"parent":"89e2121fac66","children":[{"parent":"f2b90513c9c0","children":[],"id":"48a839f3015b","title":"互斥锁"},{"parent":"f2b90513c9c0","children":[],"id":"217261d1bab5","title":"设置热点数据永不过期"}],"id":"f2b90513c9c0","title":"解决方案"}],"id":"89e2121fac66","title":"缓存击穿"},{"parent":"7742814a28a8","children":[{"parent":"796744a8869c","children":[{"parent":"9cb88abbc2df","children":[],"id":"d4df0f21db8b","title":"数据库与缓存内数据不一致问题"}],"id":"9cb88abbc2df","title":"定义"},{"parent":"796744a8869c","children":[{"parent":"cd7299545586","children":[{"parent":"2a161277de43","children":[],"id":"f7e64d411a34","title":"保持最终一致性, 当缓存中没有时会从数据库读取"}],"id":"2a161277de43","title":"设置过期时间"},{"parent":"cd7299545586","children":[{"parent":"787b437c7b36","children":[{"parent":"6a186a44f666","children":[],"id":"71f5b508690c","title":"第一步成功(操作数据库),第二步失败(删除缓存),会导致数据库里是新数据,而缓存里是旧数据"},{"parent":"6a186a44f666","children":[],"id":"26959b0b8788","title":"如果第一步(操作数据库)就失败了,直接返回错误(Exception),不会出现数据不一致
    "},{"parent":"6a186a44f666","children":[{"parent":"2daec7e7e0d5","children":[{"parent":"cc61025444c8","children":[],"id":"27c9e98de34b","title":"不一致,但出现概率很低"}],"id":"cc61025444c8","title":"
    • 缓存刚好失效
    • 线程A查询数据库,得一个旧值
    • 线程B将新值写入数据库
    • 线程B删除缓存
    • 线程A将查到的旧值写入缓存

    "}],"id":"2daec7e7e0d5","title":"并发场景"}],"id":"6a186a44f666","title":"原子性被破坏情景"},{"parent":"787b437c7b36","children":[{"parent":"a9947ca1b3c6","children":[],"id":"221e14e48d66","title":"将需要删除的key发送到消息队列中
    "},{"parent":"a9947ca1b3c6","children":[],"id":"d340f9c73068","title":"自己消费消息,获得需要删除的key
    "},{"parent":"a9947ca1b3c6","children":[],"id":"cdd19d4a6cd3","title":"不断重试删除操作,直到成功
    "}],"id":"a9947ca1b3c6","title":"删除缓存失败的解决思路"},{"parent":"787b437c7b36","children":[{"parent":"7b07db7a011b","children":[],"id":"51870fc09c5a","title":"高并发下表现优异,在原子性被破坏时表现不如意"}],"id":"7b07db7a011b","title":"结论"}],"id":"787b437c7b36","title":"先更新数据库,再删除缓存"},{"parent":"cd7299545586","children":[{"parent":"99a36d602e49","children":[{"parent":"42e69abbccbe","children":[],"id":"4d13edae29bf","title":"第一步成功(删除缓存),第二步失败(更新数据库),数据库和缓存的数据还是一致的
    "},{"parent":"42e69abbccbe","children":[],"id":"362019fddab4","title":"如果第一步(删除缓存)就失败了,我们可以直接返回错误(Exception),数据库和缓存的数据还是一致的"},{"parent":"42e69abbccbe","children":[{"parent":"2217b4dc2182","children":[{"parent":"a9df8552a646","children":[],"id":"9d945a5b687c","title":"不一致"}],"id":"a9df8552a646","title":"
    • 线程A删除了缓存
    • 线程B查询,发现缓存已不存在
    • 线程B去数据库查询得到旧值
    • 线程B将旧值写入缓存
    • 线程A将新值写入数据库
    "}],"id":"2217b4dc2182","title":"并发场景"}],"id":"42e69abbccbe","title":"原子性被破坏情景"},{"parent":"99a36d602e49","children":[{"parent":"bb57d06a2d89","children":[],"id":"4925061129d7","title":"将删除缓存、修改数据库、读取缓存等的操作积压到队列里边,实现串行化
    "}],"id":"bb57d06a2d89","title":"并发下解决数据库与缓存不一致的思路"},{"parent":"99a36d602e49","children":[{"parent":"db6def4c48a6","children":[],"id":"8def4023ab41","title":"高并发下表现不如意,在原子性被破坏时表现优异"}],"id":"db6def4c48a6","title":"结论"}],"id":"99a36d602e49","title":"先删除缓存,再更新数据库
    "},{"parent":"cd7299545586","children":[{"parent":"f9b9c73e9e93","children":[{"parent":"f55f8ee1671a","children":[],"id":"626ca445c252","title":"先删除缓存
    "},{"parent":"f55f8ee1671a","children":[],"id":"315c5d4f0763","title":"再写数据库"},{"parent":"f55f8ee1671a","children":[{"parent":"23aabe812e01","children":[],"id":"fae527875f52","title":"延时时间要大于数据库一次写操作的时间
    "},{"parent":"23aabe812e01","children":[],"id":"774f81a5a8a0","title":"需要考虑Redis和数据库的主从同步时间
    "}],"id":"23aabe812e01","title":"休眠一段时间"},{"parent":"f55f8ee1671a","children":[],"id":"10db51291f20","title":"再次删除缓存"}],"id":"f55f8ee1671a","title":"先删除缓存,再更新数据库"}],"id":"f9b9c73e9e93","title":"延时双删
    "}],"id":"cd7299545586","title":"解决方案"}],"id":"796744a8869c","title":"双写一致性"},{"parent":"7742814a28a8","children":[{"parent":"13012e650d03","children":[{"parent":"66c624a52b7f","children":[{"parent":"3f21b7a753e0","children":[{"parent":"07452410b695","children":[],"id":"f1badcd09c76","title":"可重入锁"},{"parent":"07452410b695","children":[],"id":"e220542f12fa","title":"乐观锁
    "},{"parent":"07452410b695","children":[],"id":"bd7cd43665fd","title":"公平锁"},{"parent":"07452410b695","children":[],"id":"28b6c9db34d6","title":"读写锁"},{"parent":"07452410b695","children":[],"id":"0f0046ee2fb4","title":"Redlock"},{"parent":"07452410b695","children":[],"id":"e3e5074fe0ee","title":"BloomFilter (布隆过滤器)"}],"id":"07452410b695","title":"功能概括"}],"id":"3f21b7a753e0","title":"Redisson"},{"parent":"66c624a52b7f","children":[{"parent":"c34fec094423","children":[{"parent":"d2c674dedd38","children":[],"id":"2b3737289a9b","title":"setnx key 不存在,才会设置它的值,否则什么也不做
    "}],"id":"d2c674dedd38","title":"加锁"},{"parent":"c34fec094423","children":[{"parent":"5c1eb865ad35","children":[],"id":"8132cd2aac6c","title":"expire 设置过期时间
    "}],"id":"5c1eb865ad35","title":"避免死锁"},{"parent":"c34fec094423","children":[{"parent":"6c746237d083","children":[],"id":"4cd136dcf59a","title":"Redis 2.6.12扩展了 set 命令
    "}],"id":"6c746237d083","title":"保证setnx与expire的原子性
    "}],"id":"c34fec094423","title":"操作"},{"parent":"66c624a52b7f","children":[{"parent":"3b3a67a4f68f","children":[{"parent":"945ee50567d1","children":[{"parent":"dfad1fb29323","children":[{"parent":"750122fd9d6d","children":[],"id":"2fce14e8226c","title":"加锁时,先设置一个过期时间,然后我们开启一个守护线程,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行续期,重新设置过期时间"},{"parent":"750122fd9d6d","children":[],"id":"c000eca69ea1","title":"Redisson的看门狗"}],"id":"750122fd9d6d","title":"评估操作共享资源的时间不准确"}],"id":"dfad1fb29323","title":"客户端 1 操作共享资源耗时太久,导致锁被自动释放,之后被客户端 2 持有"}],"id":"945ee50567d1","title":"锁过期"},{"parent":"3b3a67a4f68f","children":[{"parent":"bf25f0444016","children":[{"parent":"bbb2d7133150","children":[{"parent":"ea7ea3f77745","children":[{"parent":"fc68dca74343","children":[],"id":"6310889577fc","title":"UUID"},{"parent":"fc68dca74343","children":[],"id":"7462c19e34f5","title":"自己的线程 ID"}],"id":"fc68dca74343","title":"添加唯一标识"},{"parent":"ea7ea3f77745","children":[{"parent":"bb6006e00c6e","children":[{"parent":"6b33de287b91","children":[],"id":"37d6c65470fd","title":"Redis 处理每一个请求是单线程执行的,在执行一个 Lua 脚本时,其它请求必须等待"}],"id":"6b33de287b91","title":"Lua 脚本"}],"id":"bb6006e00c6e","title":"原子性"},{"parent":"ea7ea3f77745","children":[{"parent":"bbadb586bfed","children":[],"id":"59c6c1ed4a41","title":"唯一标识加锁"},{"parent":"bbadb586bfed","children":[],"id":"80b475a0dbea","title":"操作共享资源"},{"parent":"bbadb586bfed","children":[],"id":"b2b7aee63272","title":"释放锁:Lua 脚本,先 GET 判断锁是否归属自己,再 DEL 释放锁"}],"id":"bbadb586bfed","title":"严谨的流程"}],"id":"ea7ea3f77745","title":"没有检查这把锁是否归自己持有"}],"id":"bbb2d7133150","title":"客户端 1 操作共享资源过程中GC或因其他原因超时释放,  导致释放了客户端 2 的锁
    "}],"id":"bf25f0444016","title":"释放别人的锁"},{"parent":"3b3a67a4f68f","children":[{"parent":"b05bbbfca458","children":[{"parent":"1e47c82d5213","children":[],"id":"d41070261ebd","title":"客户端 1 在主库上执行 SET 命令,加锁成功
    "},{"parent":"1e47c82d5213","children":[],"id":"ff99a0fb95f0","title":"此时,主库异常宕机,SET 命令还未同步到从库上(主从复制是异步的)"},{"parent":"1e47c82d5213","children":[],"id":"b585e9c1979d","title":"从库被哨兵提升为新主库,这个锁在新的主库上,丢失了"}],"id":"1e47c82d5213","title":"场景"},{"parent":"b05bbbfca458","children":[{"parent":"6d09d47be5d2","children":[{"parent":"4cab76cdd425","children":[{"parent":"2a920be28d4e","children":[],"id":"f784806d22dc","title":"不再需要部署从库和哨兵实例,只部署主库
    "},{"parent":"2a920be28d4e","children":[],"id":"5b380f8d976a","title":"但主库要部署多个,官方推荐至少 5 个实例"}],"id":"2a920be28d4e","title":"使用前提"},{"parent":"4cab76cdd425","children":[{"parent":"50ee74e743dd","children":[],"id":"964b265a6b2d","title":"客户端先获取「当前时间戳T1」"},{"parent":"50ee74e743dd","children":[],"id":"314294fbf295","title":"客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁"},{"parent":"50ee74e743dd","children":[],"id":"ca66b200ce03","title":"如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败"},{"parent":"50ee74e743dd","children":[],"id":"6f679cab274f","title":"加锁成功,去操作共享资源"},{"parent":"50ee74e743dd","children":[],"id":"e71de8c8d3b3","title":"加锁失败,向「全部节点」发起释放锁请求( Lua 脚本)"}],"id":"50ee74e743dd","title":"使用流程"},{"parent":"4cab76cdd425","children":[{"parent":"c8b84a886178","children":[],"id":"0dca2da045c0","title":"网络延迟"},{"parent":"c8b84a886178","children":[],"id":"0794df0823fa","title":"进程暂停(GC)"},{"parent":"c8b84a886178","children":[],"id":"5485ba1922fd","title":"时钟漂移"}],"id":"c8b84a886178","title":"请考虑好分布式系统的NPC问题"}],"id":"4cab76cdd425","title":"Redlock"}],"id":"6d09d47be5d2","title":"解决方案"}],"id":"b05bbbfca458","title":"主从发生切换分布锁安全问题
    "}],"id":"3b3a67a4f68f","title":"问题"}],"id":"66c624a52b7f","title":"分布式锁"}],"id":"13012e650d03","title":"并发竞争"},{"parent":"7742814a28a8","children":[{"parent":"b36a30a83fbc","children":[],"id":"65abd363741b","title":"bigkey命令 找到干掉"},{"parent":"b36a30a83fbc","children":[],"id":"e9ff6a38dec0","title":"Redis 4.0引入了memory usage命令和lazyfree机制"}],"id":"b36a30a83fbc","title":"大Key"},{"parent":"7742814a28a8","children":[{"parent":"c7346bf92b6a","children":[],"id":"e74b5d1c63a1","title":"设置缓存时间不失效"},{"parent":"c7346bf92b6a","children":[],"id":"69508638ffc6","title":"多级缓存"},{"parent":"c7346bf92b6a","children":[],"id":"bdc6ecf6b73c","title":"布隆过滤器"},{"parent":"c7346bf92b6a","children":[],"id":"30abdb86e98d","title":"读写分离"}],"id":"c7346bf92b6a","title":"热点key"},{"parent":"7742814a28a8","children":[{"parent":"29f0b4fd4759","children":[{"parent":"7ce1965bb4b7","children":[{"parent":"5d0a0294f45d","children":[],"id":"3367ed8b7920","title":"没有offset、limit参数,会一次查出全部"},{"parent":"5d0a0294f45d","children":[],"id":"47780bc0614e","title":"keys算法是遍历算法,复杂度为O(n),因为Redis单线程的特性,会顺序执行指令,其他指令必须等待keys执行结束才可以执行"}],"id":"5d0a0294f45d","title":"缺点"}],"id":"7ce1965bb4b7","title":"keys"},{"parent":"29f0b4fd4759","children":[{"parent":"b7569f19ae71","children":[{"parent":"c9d9d6881a2c","children":[],"id":"b5bc3f2b03ab","title":"同样可以正则表达式匹配,limit可控条数,游标分布进行不会阻塞"}],"id":"c9d9d6881a2c","title":"优点"},{"parent":"b7569f19ae71","children":[{"parent":"477b5ab0ad6c","children":[],"id":"bab36d1d140e","title":"返回结果会重复"},{"parent":"477b5ab0ad6c","children":[],"id":"8f7198524c90","title":"如果遍历过程出现数据修改,不能确定改动后的数据能不能遍历到"}],"id":"477b5ab0ad6c","title":"缺点"}],"id":"b7569f19ae71","title":"scan"}],"id":"29f0b4fd4759","title":"搜索海量key"}],"id":"7742814a28a8","title":"常见问题"},{"parent":"68e2d53f78c0","children":[{"parent":"3fd6a16f4f94","children":[{"parent":"10c0fbbaf18c","children":[],"id":"3ff27aab32ff","title":"zset滑动窗口实现"},{"parent":"10c0fbbaf18c","children":[],"id":"c2637409763a","title":"在量大时会消耗很多存储空间"}],"id":"10c0fbbaf18c","title":"简单限流"},{"parent":"3fd6a16f4f94","children":[{"parent":"f5de183ac704","children":[{"parent":"d5400b7edf80","children":[],"id":"b4fb7a7e554e","title":"漏斗Funnel、漏斗算法实现makeSpace"},{"parent":"d5400b7edf80","children":[],"id":"76106014ff32","title":"在每次灌水前调用makeSpace,给漏斗腾出空间,腾出的空间取决于水流的速度
    "}],"id":"d5400b7edf80","title":"单体实现"},{"parent":"f5de183ac704","children":[{"parent":"d62e25e3e13c","children":[],"id":"b04c03869678","title":"将漏斗对象按字段存储到hash结构中,灌水时将hash结构字段去出进行逻辑运算后,再将新值重填到hash结构中,完成频度检测"},{"parent":"d62e25e3e13c","children":[{"parent":"6d2dccb3f68e","children":[],"id":"c6c702826f75","title":"加锁处理,如果失败重试会导致性能下降"},{"parent":"6d2dccb3f68e","children":[],"id":"a030aea687cd","title":"Redis4.0提供的Redis-Cell解决此问题"}],"id":"6d2dccb3f68e","title":"无法保证这三个操作的原子性"}],"id":"d62e25e3e13c","title":"分布式实现"}],"id":"f5de183ac704","title":"漏斗限流"}],"id":"3fd6a16f4f94","title":"限流操作"},{"parent":"68e2d53f78c0","children":[{"parent":"ada2330a2425","children":[{"parent":"668b1454d74f","children":[{"parent":"4d48d557b5e0","children":[],"id":"7fbadfbbcfcc","title":"5分钟一次"},{"parent":"4d48d557b5e0","children":[],"id":"d00795ceede8","title":"冷备"},{"parent":"4d48d557b5e0","children":[],"id":"f8401d16c024","title":"恢复的时候比较快"},{"parent":"4d48d557b5e0","children":[],"id":"3bbfd52a1687","title":"快照文件生成时间久,消耗cpu"},{"parent":"4d48d557b5e0","children":[],"id":"ccc6b9dcf9fb","title":"采用COW机制来实现持久化,fork进程处理"}],"id":"4d48d557b5e0","title":"RDB"},{"parent":"668b1454d74f","children":[{"parent":"9e12decdb68e","children":[],"id":"1d0805523949","title":"appendOnly"},{"parent":"9e12decdb68e","children":[],"id":"89f5517d6396","title":"数据齐全"},{"parent":"9e12decdb68e","children":[],"id":"7491a69d002c","title":"只对内存进行修改的指令记录,进行恢复时相当于‘重放’所有执行指令"},{"parent":"9e12decdb68e","children":[{"parent":"91d9756055c8","children":[],"id":"5d34419b1718","title":"定期做AOF瘦身"}],"id":"91d9756055c8","title":"回复慢文件大"}],"id":"9e12decdb68e","title":"AOF"},{"parent":"668b1454d74f","children":[{"parent":"0aa38f4c9668","children":[],"id":"7f7ef8d8748a","title":"同时将RDB与AOF存放在一起,AOF只存储自持久化开始到持久化结束期间的AOF日志"},{"parent":"0aa38f4c9668","children":[],"id":"a52194b87860","title":"在重启时先加载RDB的内容,然后重放AOF日志,提升效率"}],"id":"0aa38f4c9668","title":"混合持久化"}],"id":"668b1454d74f","title":"持久化"},{"parent":"ada2330a2425","children":[{"parent":"58b8b6e3fee6","children":[],"id":"b7cb33dbb795","title":"主从数据采取异步同步"},{"parent":"58b8b6e3fee6","children":[],"id":"f596df0cfe11","title":"保证最终一致性,从节点会努力追赶主节点,保证最终情况下从节点与主节点一致"},{"parent":"58b8b6e3fee6","children":[{"parent":"c9582a62c7b9","children":[{"parent":"09c563a5c389","children":[],"id":"b12a602a099d","title":"Redis将对自己状态产生修改影响的指令记录在本地内存buffer中,然后异步将buffer中的指令同步到从节点"},{"parent":"09c563a5c389","children":[],"id":"6985e3bc5bf5","title":"复制内存buffer是定长的环形数组,如果数组内容满了,就会从头开始覆盖"},{"parent":"09c563a5c389","children":[],"id":"e0065aa5e0d6","title":"在网络不好的情况下,可能导致无法通过指令流来同步"}],"id":"09c563a5c389","title":"增量同步"},{"parent":"c9582a62c7b9","children":[{"parent":"7e28cef0ac53","children":[],"id":"923021194d4e","title":"快照同步十分耗费资源"},{"parent":"7e28cef0ac53","children":[],"id":"b6a46fbc2eea","title":"首先在主节点进行bgsave,将当前内存快照到磁盘文件,然后再将快照传送到从节点"},{"parent":"7e28cef0ac53","children":[],"id":"19da982e6ab9","title":"快照文件接受完毕后,先将当前内存数据清空,然后执行全量加载,加载后通知主节点进行增量同步"},{"parent":"7e28cef0ac53","children":[],"id":"4954c29c2a50","title":"在此期间buffer依旧向前移动,如果快照同步时间过长或复制buffer太小,就会导致快照同步后依旧无法增量同步,导致死循环"},{"parent":"7e28cef0ac53","children":[],"id":"d4a0e45138a3","title":"所以务必要配置合适的复制buffer大小,避免快照复制死循环"}],"id":"7e28cef0ac53","title":"快照同步"}],"id":"c9582a62c7b9","title":"同步方式"}],"id":"58b8b6e3fee6","title":"数据同步机制"},{"parent":"ada2330a2425","children":[{"parent":"14d4ebf6fb81","children":[],"id":"6d287264815f","title":"哨兵负责监控主从节点健康,当主节点挂掉时,自动选择最优节点成为主节点,原主节点恢复后会变为从节点"},{"parent":"14d4ebf6fb81","children":[{"parent":"22cf1ee3be06","children":[],"id":"0ec54367a481","title":"Redis采用异步复制,所以当主节点挂掉,从节点可能没有收到全部的同步消息,产生消息丢失"},{"parent":"22cf1ee3be06","children":[{"parent":"fea18cac6d2a","children":[],"id":"bb55da53f849","title":"表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服务"}],"id":"fea18cac6d2a","title":"min-slaves-to-write 1"},{"parent":"22cf1ee3be06","children":[{"parent":"9d3ebc781908","children":[],"id":"85f361fff22e","title":"表示如果10s内没有收到从节点的反馈,就意味着同步不正常"}],"id":"9d3ebc781908","title":"min-slaves-max-lag 10"}],"id":"22cf1ee3be06","title":"消息丢失"}],"id":"14d4ebf6fb81","title":"哨兵"},{"parent":"ada2330a2425","children":[],"id":"6a204b455e04","title":"集群"}],"id":"ada2330a2425","title":"高可用"},{"parent":"68e2d53f78c0","children":[{"parent":"0bbbfddb1bdf","children":[],"id":"1c62574854ce","title":"合并读、写命令,减少网络开销"}],"id":"0bbbfddb1bdf","title":"管道"},{"parent":"68e2d53f78c0","children":[{"parent":"550cc736d1a3","children":[{"parent":"27b517127004","children":[],"id":"697a6405075e","title":"创建一个定时器,当key设置有过期时间,且到达过期时间,由定时器任务立即删除"},{"parent":"27b517127004","children":[],"id":"fbf01411fe0d","title":"可以快速释放掉不必要的内存占用 , 但是CPU压力很大
    "}],"id":"27b517127004","title":"定时删除"},{"parent":"550cc736d1a3","children":[{"parent":"5ad26b89a5fb","children":[],"id":"1954cef851e8","title":"在客户端访问key的时候,进行过期检查,如果过期了立即删除"}],"id":"5ad26b89a5fb","title":"惰性策略(惰性删除)"},{"parent":"550cc736d1a3","children":[{"parent":"1494d4d816a6","children":[],"id":"80ed3dbfa838","title":"Redis会将设置了过期时间的key放入一个独立的字典中,定期遍历(默认每秒进行10次扫描)来删除到底的key"},{"parent":"1494d4d816a6","children":[{"parent":"3c1730cc5539","children":[],"id":"418e5db9ebc8","title":"定期遍历不会遍历所有的key,而是采取贪心策略"},{"parent":"3c1730cc5539","children":[],"id":"c03a6231598c","title":"从过期字典中随机选出20个key"},{"parent":"3c1730cc5539","children":[],"id":"d50e70020d97","title":"删除这20个key中已经过期的key"},{"parent":"3c1730cc5539","children":[],"id":"2de36c1ef778","title":"如果过期的key的比例超过1/4,则重复步骤"},{"parent":"3c1730cc5539","children":[],"id":"c646a5a92ec1","title":"为保证不会循环过度,扫描时间上线默认不会超过25ms"}],"id":"3c1730cc5539","title":"贪心策略"}],"id":"1494d4d816a6","title":"定时扫描(定时删除)"},{"parent":"550cc736d1a3","children":[{"parent":"15e0714a86f6","children":[],"id":"0d3ad6052dae","title":"从节点不会进行过期扫描,对过期的处理时被动的,主节点在key到期时,会在AOF里增加一条del指令,同步到从节点,从节点通过指令删除key"}],"id":"15e0714a86f6","title":"从节点过期策略"}],"id":"550cc736d1a3","title":"过期策略"},{"parent":"68e2d53f78c0","children":[{"parent":"1c19ee20a68c","children":[{"parent":"35311e5d526f","children":[],"id":"6ff15aa765c3","title":"采用key/value+链表实现,当字典元素被访问时移动到表头,当空间满的时候踢掉链表尾部的元素"},{"parent":"35311e5d526f","children":[{"parent":"9b7510be7f91","children":[],"id":"dbcf0b911044","title":"Redis的LRU采用一种近似LRU的算法,为每个key增加一个额外字段长度为24bit,为最后一次访问的时间戳"},{"parent":"9b7510be7f91","children":[],"id":"3f37f69602f0","title":"采取懒惰方式处理,当执行写入操作时如果超出最大内存就执行一次LRU淘汰算法,随机采样5(数量可设置)个key,淘汰掉最旧的key,如果淘汰后依旧超出最大内存则继续淘汰"}],"id":"9b7510be7f91","title":"Redis的LRU"}],"id":"35311e5d526f","title":"LRU"}],"id":"1c19ee20a68c","title":"淘汰机制"},{"parent":"68e2d53f78c0","children":[{"parent":"ef4cb7bfd92a","children":[],"id":"77a7955156bf","title":"在调用套接字方法的时候默认是阻塞的,比如,read方法需要读取规定字节后返回,如果没有线程就会卡在那里,直到新数据来或者链接关闭
    "},{"parent":"ef4cb7bfd92a","children":[],"id":"f329207efe37","title":"write方法一般不会阻塞,除非内核为套接字分配的写缓冲区已经满了"},{"parent":"ef4cb7bfd92a","children":[],"id":"9bf3cb96e12d","title":"非阻塞的IO提供了一个选项Non_Blocking,打开时读写都不会阻塞 读多少写多少取决于内核的套接字字节分配"},{"parent":"ef4cb7bfd92a","children":[],"id":"9235ef78e11c","title":"非阻塞IO也有问题线程要读数据读了一点就返回了线程什么时候知道继续读?写一样"},{"parent":"ef4cb7bfd92a","children":[],"id":"eb7e9d548e40","title":"一般都是select解决但是性能低现在都是epoll"}],"id":"ef4cb7bfd92a","title":"多路IO复用"}],"collapsed":true,"id":"68e2d53f78c0","title":"Redis"},{"parent":"root","lineStyle":{"randomLineColor":"#13A3ED"},"children":[{"parent":"27d73b325f5b","children":[{"parent":"c126505c846f","children":[{"parent":"3e6fdbd260dc","children":[{"parent":"e3bbe630c77b","children":[],"id":"a52aafb21566","title":"所有节点拥有数据的最新版本"}],"id":"e3bbe630c77b","title":"数据一致性(consistency)"},{"parent":"3e6fdbd260dc","children":[{"parent":"16f0be57497c","children":[],"id":"13e807d7a20d","title":"数据具备高可用性"}],"id":"16f0be57497c","title":"可用性(availability)"},{"parent":"3e6fdbd260dc","children":[{"parent":"7394b794bc3c","children":[],"id":"6f3f4c1e3e1f","title":"容忍网络出现分区,分区之间网络不可达"}],"id":"7394b794bc3c","title":"分区容错性(partition-tolerance)"}],"id":"3e6fdbd260dc","title":"概念"},{"parent":"c126505c846f","children":[{"parent":"ea8b927aa992","children":[],"id":"ea6785f04b60","title":"在容忍网络分区的条件下,“强一致性”和“极致可用性”无法同时达到"}],"id":"ea8b927aa992","title":"定义"}],"id":"c126505c846f","title":"CAP理论"},{"parent":"27d73b325f5b","children":[{"parent":"d63e14aab5d1","children":[{"parent":"94404e1c447b","children":[],"id":"dd9e2dc4af2a","title":"用来维护各个服务的注册信息 , 各个服务通过注册清单的服务名获取服务具体位置(IP地址会变,服务名一般不会变)
    "}],"id":"94404e1c447b","title":"服务治理:Eureka"},{"parent":"d63e14aab5d1","children":[{"parent":"885fa0bda765","children":[],"id":"f1bd36f604b7","title":"客户端可以从Eureka Server中得到一份服务清单,在发送请求时通过负载均衡算法,在多个服务器之间选择一个进行访问"}],"id":"885fa0bda765","title":"客户端负载均衡:Ribbon"},{"parent":"d63e14aab5d1","children":[{"parent":"84ec94898586","children":[],"id":"320c4c3f6ad0","title":"在远程服务出现延迟, 宕机情况时提供断路器、线程隔离等功能"}],"id":"84ec94898586","title":"服务容错保护:Hystrix"},{"parent":"d63e14aab5d1","children":[{"parent":"3f725be6c509","children":[],"id":"5d6df34cab52","title":"整合了Ribbon与Hystrix"}],"id":"3f725be6c509","title":"声明式服务调用:Feign"},{"parent":"d63e14aab5d1","children":[{"parent":"b19ae03b802a","children":[],"id":"1cee4018ba68","title":"解决路由规则与服务实例的维护间题, 签名校验、 登录校验冗余问题
    "}],"id":"b19ae03b802a","title":"API网关服务:Zuul"},{"parent":"d63e14aab5d1","children":[{"parent":"261553fe2960","children":[{"parent":"510230e16d5b","children":[],"id":"605eb3a4eb2a","title":"通过接口获取数据、并依据此数据初始化自己的应用"}],"id":"510230e16d5b","title":"Client
    "},{"parent":"261553fe2960","children":[{"parent":"f97a17502384","children":[],"id":"f0fc3067f1d7","title":"提供配置文件的存储、以接口的形式将配置文件的内容提供出去"}],"id":"f97a17502384","title":"Server"}],"id":"261553fe2960","title":"分布式配置中心:Config"}],"id":"d63e14aab5d1","title":"基础功能"}],"collapsed":true,"id":"27d73b325f5b","title":"Spring Cloud"},{"parent":"root","lineStyle":{"randomLineColor":"#A04AFB"},"children":[],"id":"bc354f9b6903","title":"Zookeeper"},{"parent":"root","lineStyle":{"randomLineColor":"#74C11F"},"children":[],"id":"96167bd3313f","title":"Dubbo"},{"parent":"root","lineStyle":{"randomLineColor":"#F4325C"},"children":[{"parent":"2967bbb0ce6c","children":[{"parent":"12f64511f568","children":[{"parent":"75e771d94bbf","children":[{"parent":"53caf8da86e2","children":[],"id":"696233005cce","title":"NameServer是几乎无状态的, 可以横向扩展, 节点之间相互无通信, 可以通过部署多态机器来标记自己是一个伪集群"},{"parent":"53caf8da86e2","children":[],"id":"20cf4d49dd61","title":"NameServer的压力不会太大, 主要开销在维持心跳和提供Topic-Broker的关系数据"},{"parent":"53caf8da86e2","children":[],"id":"c96708a3cb62","title":"Broker向NameServer发送心跳时, 会携带上当前自己所负责的所有Topic信息(万级别), 若Topic个数很多会导致一次心跳中,就Topic的数据就几十M, 网络情况差的情况下, 网络传输失败, 心跳失败, 导致NameServer误认为Broker心跳失败"}],"id":"53caf8da86e2","title":"主要负责对于源数据的管理,包括了对于Topic和路由信息的管理"}],"id":"75e771d94bbf","title":"NameServer"},{"parent":"12f64511f568","children":[{"parent":"daad2e0ee47a","children":[{"parent":"a3a4b2fa4290","children":[],"id":"a876bb50d0bb","title":"Broker是具体提供业务的服务器, 单个Broker节点与所有NameServer节点保持长连接及心跳, 并会定时将Topic信息注册到NameServer"},{"parent":"a3a4b2fa4290","children":[],"id":"888065bfed04","title":"底层通信和连接基于Netty实现"},{"parent":"a3a4b2fa4290","children":[],"id":"6ff21d8f55f8","title":"Broker负责消息存储, 以Topic为维度支持轻量级队列, 单机可以支撑上万队列规模, 支持消息推拉模型"}],"id":"a3a4b2fa4290","title":"消息中转角色,负责存储消息,转发消息"}],"id":"daad2e0ee47a","title":"Broker"},{"parent":"12f64511f568","children":[{"parent":"5c20008f41b6","children":[{"parent":"cfa99e651913","children":[],"id":"ecf9bb3145fc","title":"同步发送: 发送者向MQ执行发送消息API,同步等待,直到消息服务器返回发送结果。一般用于重要通知消息,例如重要通知邮件、营销短信
    "},{"parent":"cfa99e651913","children":[],"id":"8f1cefc4cec3","title":"异步发送: 发送这向MQ执行发送消息API,指定消息发送成功后的回调函数,然后调用消息发送API后,立即返回.消息发送者线程不阻塞,直到运行结束,消息发送成功或失败的回调任务在一个新线程执行,一般用于可能链路耗时较长而对响应时间敏感的业务场景,例如用户视频上传后通知启动转码服务"},{"parent":"cfa99e651913","children":[],"id":"93201d04d89e","title":"单向发送: 发送者MQ执行发送消息API时,直接返回,不等待消息服务器的结果,也不注册回调函数,简单的说就是只管发送,不在乎消息是否成功存储在消息服务器上,适用于某些耗时非常短但对可靠性要求并不高的场景,例如日志收集"}],"id":"cfa99e651913","title":"消息生产者,负责产生消息,一般由业务系统负责产生消息"}],"id":"5c20008f41b6","title":"Producer"},{"parent":"12f64511f568","children":[{"parent":"d7f396e0831a","children":[{"parent":"2167651c5963","children":[],"id":"eac97c7046ef","title":"Pull: 拉取型消费,主动从消息服务器拉去信息, 只要批量拉取到消息, 用户应用就会启动消费过程, 所以Pull成为主动消费型"},{"parent":"2167651c5963","children":[],"id":"e1d302c81ee1","title":"Push: 推送型消费者,封装了消息的拉去,消费进度和其他的内部维护工作, 将消息到达时执行回调接口留给用户程序来实现. 所以Push成为被动消费型, 但从实现上看还是从消息服务器中拉取消息, 不同于Pull的时Push首先要注册消费监听器, 当监听器触发后才开始消费消息"}],"id":"2167651c5963","title":"消息消费者,负责消费消息,一般是后台系统负责异步消费"}],"id":"d7f396e0831a","title":"Consumer"}],"id":"12f64511f568","title":"基础组成"},{"parent":"2967bbb0ce6c","children":[{"parent":"604a2b077915","children":[{"parent":"5f5dd32d8097","children":[{"parent":"ee7415aca487","children":[{"parent":"e8d48c3eb98c","children":[],"id":"c36cc866bf9a","title":"配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢失(异步刷盘丢失少量消息,同步刷盘一条不丢)。性能最高"}],"id":"e8d48c3eb98c","title":"优点"},{"parent":"ee7415aca487","children":[{"parent":"043f2943a836","children":[],"id":"df0bc145fab6","title":"单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响"}],"id":"043f2943a836","title":"缺点"}],"id":"ee7415aca487","title":"一个集群无Slave,全是Master"}],"id":"5f5dd32d8097","title":"多Master"},{"parent":"604a2b077915","children":[{"parent":"1257c8b9aac5","children":[{"parent":"ea64577686e3","children":[{"parent":"90097b5c030f","children":[],"id":"e53091b80fab","title":"即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,因为Master 宕机后,消费者仍然可以从Slave消费,此过程对应用透明。不需要人工干预。性能同多 Master 模式几乎一样"}],"id":"90097b5c030f","title":"优点"},{"parent":"ea64577686e3","children":[{"parent":"4ef440c8e995","children":[],"id":"27e509d14ece","title":"Master宕机,磁盘损坏情况,会丢失少量消息"}],"id":"4ef440c8e995","title":"缺点"}],"id":"ea64577686e3","title":"每个Master配置一个Slave,有多对Master-Slave,HA采用异步复制方式,主备有短暂消息延迟,毫秒级"}],"id":"1257c8b9aac5","title":"多Master, 多Salve, 异步复制"},{"parent":"604a2b077915","children":[{"parent":"4d53b0661b8f","children":[{"parent":"e4a1b95dba5b","children":[{"parent":"9f79418a5eed","children":[],"id":"a5fc7ac37b70","title":"数据与服务都无单点,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高"}],"id":"9f79418a5eed","title":"优点"},{"parent":"e4a1b95dba5b","children":[{"parent":"6dd53df1911c","children":[],"id":"57d816d31bf1","title":"性能比异步复制模式略低,大约低10%左右,发送单个消息的RT会略高。目前主宕机后,备机不能自动切换为主机,后续会支持自动切换功能"}],"id":"6dd53df1911c","title":"缺点"}],"id":"e4a1b95dba5b","title":"每个Master配置一个Slave,有多对Master-Slave,HA采用同步双写方式,主备都写成功,向应用才返回成功"}],"id":"4d53b0661b8f","title":"多Master, 多Salve, 双写一致"}],"id":"604a2b077915","title":"支持集群模式"},{"parent":"2967bbb0ce6c","children":[{"parent":"43da161a94fa","children":[],"id":"1a296e9626d0","title":"只有发送成功后返回CONSUME_SUCCESS, 消费才是完成的"},{"parent":"43da161a94fa","children":[{"parent":"8fc82e05be66","children":[{"parent":"d99a334f4878","children":[],"id":"40fed29109ad","title":"当确认批次消息消费失败 (RECONSUME_LATER) 时, RocketMQ会把这批消息重发回Broker (此处非原Topic 而是这个消费者组的 RETRY Topic)
    "},{"parent":"d99a334f4878","children":[],"id":"ec26b776e2f4","title":"在延迟的某个时间点(默认是10秒,业务可设置)后,再次投递到这个ConsumerGroup"},{"parent":"d99a334f4878","children":[],"id":"c226bf1b7a9e","title":"如果一直这样重复消费都持续失败到一定次数(默认16次),就会投递到死信队列。人工干预解决"}],"id":"d99a334f4878","title":"顺序消息重试"},{"parent":"8fc82e05be66","children":[],"id":"813946fc3144","title":"无序消息重试"}],"id":"8fc82e05be66","title":"消息重试"},{"parent":"43da161a94fa","children":[{"parent":"a38425631022","children":[],"id":"0964ff7a4b1c","title":"RocketMQ 以 Consumer Group (消费者组)+ Queue (队列) 为单位管理消费进度, 通过 Consumer Offset 标记这个这个消费组在这条Queue上的消费进度"},{"parent":"a38425631022","children":[],"id":"76e7f5c236a6","title":"如果已存在的消费组出现了新消费实例的时候,依靠这个组的消费进度,可以判断第一次是从哪里开始拉取的"},{"parent":"a38425631022","children":[],"id":"65266364b4b8","title":"每次消息成功后,本地的消费进度会被更新,然后由定时器定时同步到broker,以此持久化消费进度"},{"parent":"a38425631022","children":[],"id":"ddcbe167cfd1","title":"但每次记录消费进度时,只会将一批消息中最小的offset值更新为消费进度值"}],"id":"a38425631022","title":"ACK机制"},{"parent":"43da161a94fa","children":[{"parent":"ce692c70d0c6","children":[],"id":"8c1e99f1aebf","title":"由于消费进度只记录了一个下标,就可能出现拉取了100条消息如 100 - 200 的消息,后面99条都消费结束了,只有101消费一直没有结束的情况"},{"parent":"ce692c70d0c6","children":[],"id":"88f2e23d3c86","title":"RocketMQ为了保证消息肯定被消费成功,消费进度只能维持在101,直到101也消费结束,本地消费进度才能标记200消费结束"},{"parent":"ce692c70d0c6","children":[],"id":"df1747541718","title":"在这种情况下,如果RocketMQ机器断电,或者被kill, 此处的消费进度就还是101, 当队列重新分配实例时, 从broker获取的消费进度维持在101, 就会出现重复消费的情况
    "},{"parent":"ce692c70d0c6","children":[],"id":"3528a1c8bcec","title":"对于这个场景,RocketMQ暂时无能为力,所以业务必须要保证消息消费的幂等性"}],"id":"ce692c70d0c6","title":"重复消费"},{"parent":"43da161a94fa","children":[{"parent":"b6fc9dbb9580","children":[],"id":"939f53dff9c0","title":"RocketMQ支持按照时间回溯消费,时间维度精确到毫秒,可以向前回溯,也可以向后回溯"}],"id":"b6fc9dbb9580","title":"消息回溯"}],"id":"43da161a94fa","title":"消费保证"},{"parent":"2967bbb0ce6c","children":[{"parent":"403b9cd18917","children":[{"parent":"4439dd28bd16","children":[{"parent":"40d9bda89e49","children":[],"id":"07d61c838390","title":"由于 NameServer 节点是无状态的,且各个节点直接的数据是一致的,故存在多个 NameServer 节点的情况下,部分 NameServer 不可用也可以保证 MQ 服务正常运行"}],"id":"40d9bda89e49","title":"NameServer 集群"},{"parent":"4439dd28bd16","children":[{"parent":"5e1a97f2162e","children":[{"parent":"2abd6a7fd911","children":[],"id":"f4b8c1adb5df","title":"由于Slave只负责读,当 Master 不可用,它对应的 Slave 仍能保证消息被正常消费"},{"parent":"2abd6a7fd911","children":[],"id":"8139525d3db2","title":"由于配置多组 Master-Slave 组,其他的 Master-Slave 组也会保证消息的正常发送和消费"}],"id":"2abd6a7fd911","title":"一个 Master 可以配置多个 Slave,同时也支持配置多个 Master-Slave 组"}],"id":"5e1a97f2162e","title":"Broker 主从, 多主从"},{"parent":"4439dd28bd16","children":[{"parent":"f82965d473fb","children":[],"id":"7e71895d91ef","title":"Consumer 的高可用是依赖于 Master-Slave 配置的,由于 Master 能够支持读写消息,Slave 支持读消息,当 Master 不可用或繁忙时, Consumer 会被自动切换到从 Slave 读取(自动切换,无需配置)"}],"id":"f82965d473fb","title":"Consumer 自动切换"},{"parent":"4439dd28bd16","children":[{"parent":"f3c6854c53d6","children":[],"id":"ad3cb7405f89","title":"在创建Topic时, 将Topic的多个 Message Queue 创建在多个 Broker组 上, 这样当一个Broker组的Master不可用后,其他组的Master仍然可用,Producer仍然可以发送消息
    "}],"id":"f3c6854c53d6","title":"Producer 连接多个 Broker"}],"id":"4439dd28bd16","title":"集群"},{"parent":"403b9cd18917","children":[{"parent":"a4cb838b16ea","children":[{"parent":"c2592c074d4c","children":[],"id":"07ef479a1045","title":"在返回写成功状态时,消息已经被写入磁盘中。即消息被写入内存的PAGECACHE 中后,立刻通知刷新线程刷盘,等待刷盘完成,才会唤醒等待的线程并返回成功状态, 超时会返回错误
    "}],"id":"c2592c074d4c","title":"同步刷盘"},{"parent":"a4cb838b16ea","children":[{"parent":"3d7e8bfce714","children":[],"id":"4fe92f30b6e8","title":"在返回写成功状态时,消息可能只是被写入内存的 PAGECACHE 中。当内存的消息量积累到一定程度时,触发写操作快速写入, 不返回错误"}],"id":"3d7e8bfce714","title":"异步刷盘"}],"id":"a4cb838b16ea","title":"刷盘机制"},{"parent":"403b9cd18917","children":[{"parent":"55e8e026c976","children":[{"parent":"7e5670f14a09","children":[],"id":"8859c3e13bfe","title":"Master 和 Slave 均写成功后才反馈给客户端写成功状态"}],"id":"7e5670f14a09","title":"同步复制"},{"parent":"55e8e026c976","children":[{"parent":"953b201d922e","children":[],"id":"c97878eed758","title":"只要 Master 写成功,就反馈客户端写成功状态"}],"id":"953b201d922e","title":"异步复制"}],"id":"55e8e026c976","title":"消息的主从复制"}],"id":"403b9cd18917","title":"高可用"},{"parent":"2967bbb0ce6c","children":[{"parent":"e56a055b5068","children":[{"parent":"16961909ae98","children":[],"id":"944ff5256f15","title":"RocketMQ提供了MessageQueueSelector队列选择机制"},{"parent":"16961909ae98","children":[],"id":"68d213356c50","title":"顺序发送 顺序消费由 消费者保证"}],"id":"16961909ae98","title":"Hash取模法"}],"id":"e56a055b5068","title":"顺序消费"},{"parent":"2967bbb0ce6c","children":[{"parent":"7ce52753f364","children":[{"parent":"4fb6ee988efc","children":[],"id":"8c2657879e36","title":"使用业务端逻辑保持幂等性"}],"id":"4fb6ee988efc","title":"原则"},{"parent":"7ce52753f364","children":[{"parent":"1bfaf765406c","children":[],"id":"b0239da60ed7","title":"对于同一操作发起的一次请求或者多次请求的结果是一致的"}],"id":"1bfaf765406c","title":"幂等性"},{"parent":"7ce52753f364","children":[{"parent":"56d747f52657","children":[],"id":"e39d27d3a0ab","title":"保证每条消息都有唯一编号(比如唯一流水号),重复消费时主键冲突不再处理消息"}],"id":"56d747f52657","title":"去重策略"}],"id":"7ce52753f364","title":"消息去重"},{"parent":"2967bbb0ce6c","children":[{"parent":"09e95e6eab93","children":[{"parent":"c8a5405fb7c5","children":[],"id":"3570c5920213","title":"暂不能被Consumer消费的消息, 需要 Producer 对消息的二次确认后,Consumer才能去消费它"}],"id":"c8a5405fb7c5","title":"Half Message (半消息)"},{"parent":"09e95e6eab93","children":[{"parent":"332f741948f5","children":[],"id":"be923878148d","title":"A服务先发送个 Half Message 给 Broker 端,消息中携带 B服务"},{"parent":"332f741948f5","children":[],"id":"55ea8b7092e3","title":"当A服务知道 Half Message 发送成功后, 执行本地事务"},{"parent":"332f741948f5","children":[],"id":"4187239edfc5","title":"如果本地事务成功,那么 Producer 像 Broker 服务器发送 Commit , 这样B服务就可以消费该 Message"},{"parent":"332f741948f5","children":[],"id":"08a1fccc842a","title":"如果本地事务失败,那么 Producer 像 Broker 服务器发送 Rollback , 那么就会直接删除上面这条半消息"},{"parent":"332f741948f5","children":[],"id":"4da11302aa7f","title":"如果因为网络等原因迟迟没有返回失败还是成功,那么会执行RocketMQ的回调接口,来进行事务的回查"}],"id":"332f741948f5","title":"流程"},{"parent":"09e95e6eab93","children":[],"id":"a5095a559452","title":"最终一致性"},{"parent":"09e95e6eab93","children":[],"id":"5c502c270ff1","title":"最大努力通知"}],"id":"09e95e6eab93","title":"事务消息"},{"parent":"2967bbb0ce6c","children":[{"parent":"539927f8e2c1","children":[],"id":"88eabed09a58","title":"Producer 和 NameServer 节点建立一个长连接"},{"parent":"539927f8e2c1","children":[],"id":"fc3d758dc575","title":"定期从 NameServer 获取 Topic 信息 
    "},{"parent":"539927f8e2c1","children":[],"id":"e2e50215339f","title":"并且向 Broker Master 建立链接 发送心跳"},{"parent":"539927f8e2c1","children":[],"id":"2063f1f65200","title":"发送消息给 Broker Master"},{"parent":"539927f8e2c1","children":[],"id":"0d92c5a8e5e8","title":"Consumer 从 Mater 和 Slave 一起订阅消息"}],"id":"539927f8e2c1","title":"一次完整的通信流程"},{"parent":"2967bbb0ce6c","children":[{"parent":"9081c42a0f2a","children":[],"id":"1219a6e079fa","title":"不再被正常消费 "},{"parent":"9081c42a0f2a","children":[],"id":"5e70a2a44651","title":"保存3天"},{"parent":"9081c42a0f2a","children":[],"id":"50aa84db0020","title":"面向消费者组 "},{"parent":"9081c42a0f2a","children":[],"id":"d4a503883c07","title":"控制台 重发 重写消费者 单独消费"}],"id":"9081c42a0f2a","title":"死信队列"},{"parent":"2967bbb0ce6c","children":[{"parent":"cc10915c102e","children":[{"parent":"712c8d74cc5a","children":[],"id":"e8d44e34f882","title":"生产者将消息发送给Rocket MQ的时候,如果出现了网络抖动或者通信异常等问题,消息就有可能会丢失"},{"parent":"712c8d74cc5a","children":[{"parent":"e4ba8bf00b71","children":[],"id":"4d4c85447a76","title":"如果消息还没有完成异步刷盘,RocketMQ中的Broker宕机的话,就会导致消息丢失"},{"parent":"e4ba8bf00b71","children":[],"id":"a16ee1c4706e","title":"如果消息已经被刷入了磁盘中,但是数据没有做任何备份,一旦磁盘损坏,那么消息也会丢失"}],"id":"e4ba8bf00b71","title":"消息需要持久化到磁盘中,这时会有两种情况导致消息丢失"},{"parent":"712c8d74cc5a","children":[],"id":"f521198db453","title":"消费者成功从RocketMQ中获取到了消息,还没有将消息完全消费完的时候,就通知RocketMQ我已经将消息消费了,然后消费者宕机,但是RocketMQ认为消费者已经成功消费了数据,所以数据依旧丢失了"}],"id":"712c8d74cc5a","title":"常见场景"},{"parent":"cc10915c102e","children":[{"parent":"6e0281cde36d","children":[],"id":"d1b9b4c61dab","title":"事务消息"},{"parent":"6e0281cde36d","children":[],"id":"d5c1c1f6fd31","title":"同步刷盘"},{"parent":"6e0281cde36d","children":[],"id":"dd9b94bf2957","title":"主从机构的话,需要Leader将数据同步给Followe"},{"parent":"6e0281cde36d","children":[],"id":"29c1212c50c6","title":"消费时无法异步消费,只能等待消费完成再通知RocketMQ消费完成"}],"id":"6e0281cde36d","title":"确保消息零丢失"},{"parent":"cc10915c102e","children":[],"id":"f6dabcb64d3a","title":"上述方案会使性能和吞吐量大幅下降, 需按场景谨慎使用"}],"id":"cc10915c102e","title":"消息丢失"},{"parent":"2967bbb0ce6c","children":[{"parent":"ce3c57e6ab49","children":[{"parent":"12b15ec01705","children":[{"parent":"c74aa299e8ad","children":[],"id":"24987ee3c4db","title":"在业务允许的情况下, 根据一定的丢弃策略来丢弃消息"},{"parent":"c74aa299e8ad","children":[],"id":"036abd6de052","title":"修复Consumer不消费问题,使其恢复正常消费,根据业务需要看是否要暂停"},{"parent":"c74aa299e8ad","children":[],"id":"81368743808e","title":"停止消费 加机器 加Topic, 编写临时处理分发程序消费
    "}],"id":"c74aa299e8ad","title":"丢弃, 扩容"}],"id":"12b15ec01705","title":"解决思想"}],"id":"ce3c57e6ab49","title":"消息堆积"},{"parent":"2967bbb0ce6c","children":[{"parent":"8a5fb1ce1bf4","children":[],"id":"05af140b142a","title":"RocketMQ支持定时消息,但是不支持任意时间精度,支持特定的level,例如定时5s,10s,1m等"}],"id":"8a5fb1ce1bf4","title":"定时消息"},{"parent":"2967bbb0ce6c","children":[{"parent":"4aa9ff9ad75c","children":[{"parent":"954e7ff26b10","children":[],"id":"e4f3f3d220ec","title":"单机吞吐量:十万级"},{"parent":"954e7ff26b10","children":[],"id":"c4af3edfc858","title":"可用性:非常高,分布式架构"},{"parent":"954e7ff26b10","children":[],"id":"6a45885fa4c5","title":"消息可靠性:经过参数优化配置,消息可以做到零丢失"},{"parent":"954e7ff26b10","children":[],"id":"77955f421cf0","title":"功能支持:MQ功能较为完善,还是分布式的,扩展性好"},{"parent":"954e7ff26b10","children":[],"id":"2159848b2077","title":"支持10亿级别的消息堆积,不会因为堆积导致性能下降"},{"parent":"954e7ff26b10","children":[],"id":"8d732d9ac0d4","title":"源码是java,我们可以自己阅读源码,定制自己公司的MQ,可以掌控"},{"parent":"954e7ff26b10","children":[],"id":"079ef0e7e12f","title":"天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况"},{"parent":"954e7ff26b10","children":[],"id":"24933c880163","title":"RoketMQ在稳定性上可能更值得信赖,这些业务场景在阿里双11已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择RocketMQ"}],"id":"954e7ff26b10","title":"优点"},{"parent":"4aa9ff9ad75c","children":[{"parent":"6e817ad0d7e2","children":[],"id":"5bc9518a9f6f","title":"支持的客户端语言不多,目前是java及c++,其中c++不成熟"},{"parent":"6e817ad0d7e2","children":[],"id":"bee9d1ce399f","title":"社区活跃度不是特别活跃那种"},{"parent":"6e817ad0d7e2","children":[],"id":"9e4046411e3c","title":"没有在 mq 核心中去实现JMS等接口,有些系统要迁移需要修改大量代码"}],"id":"6e817ad0d7e2","title":"缺点"}],"id":"4aa9ff9ad75c","title":"优缺点总结"}],"collapsed":true,"id":"2967bbb0ce6c","title":"RocketMQ"},{"parent":"root","lineStyle":{"randomLineColor":"#7754F6"},"children":[],"id":"6b402feebcdf","title":"分布式锁"},{"parent":"root","lineStyle":{"randomLineColor":"#FFCA01"},"children":[{"parent":"426e047d8f9d","children":[{"parent":"4b90dd9864c9","children":[{"parent":"057add18a139","children":[{"parent":"3097acf2ab15","children":[],"id":"df6d551163bc","title":"假如在第一阶段所有参与者都返回准备成功,那么协调者则向所有参与者发送提交事务命令,等待所有事务都提交成功之后,返回事务执行成功"},{"parent":"3097acf2ab15","children":[],"id":"d16589167a2c","title":"假如在第一阶段有一个参与者返回失败,那么协调者就会向所有参与者发送回滚事务的请求,即分布式事务执行失败"}],"id":"3097acf2ab15","title":"准备阶段: 协调者(事务管理器)给每个参与者发送Prepare消息,参与者要么直接返回失败,
    要么在本地执行事务,写本地的redo和undo日志,但不做提交"},{"parent":"057add18a139","children":[{"parent":"8c5177c0c885","children":[],"id":"ab3d2da5a1c6","title":"如果第二阶段提交失败, 执行的是回滚事务操作, 那么会不断重试, 直到所有参与者全部回滚, 不然在第一阶段准备成功的参与者会一直阻塞"},{"parent":"8c5177c0c885","children":[],"id":"acc12723768c","title":"如果第二阶段提交失败, 执行的是提交事务, 也会不断重试, 因为有可能一些参与者已经提交成功, 所以只能不断重试, 甚至人工介入处理
    "}],"id":"8c5177c0c885","title":"提交阶段: 协调者收到参与者的失败消息或者超时,直接给每个参与者发送回滚消息;
    否则,发送提交消息. 参与者根据协调者的指令执行提交或回滚操作,释放锁资源"}],"id":"057add18a139","title":"流程"},{"parent":"4b90dd9864c9","children":[],"id":"fb30798578aa","title":"同步阻塞协议"},{"parent":"4b90dd9864c9","children":[{"parent":"7a977c8d1831","children":[],"id":"1cce93f1a7a6","title":"同步阻塞导致长久资源锁定, 效率低"},{"parent":"7a977c8d1831","children":[],"id":"ffb01d2f2712","title":"协调者是一个单点, 存在单点故障问题, 参与者将一直处于锁定状态"},{"parent":"7a977c8d1831","children":[],"id":"143f109fefcf","title":"脑裂问题, 在提交阶段,如果只有部分参与者接收并执行了提交请求,会导致节点数据不一致
    "}],"id":"7a977c8d1831","title":"缺点"},{"parent":"4b90dd9864c9","children":[],"id":"75a98189b051","title":"是数据库层面解决方案"}],"id":"4b90dd9864c9","title":"2PC(两段式提交)"},{"parent":"426e047d8f9d","children":[{"parent":"e8856f8bce0a","children":[{"parent":"1eee8b12a46d","children":[],"id":"623a1ea6da3e","title":"CanCommit阶段: 协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应
    "},{"parent":"1eee8b12a46d","children":[{"parent":"1a7358388525","children":[],"id":"21e1c7f27605","title":"假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行"},{"parent":"1a7358388525","children":[],"id":"3e2483073c0d","title":"假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断"}],"id":"1a7358388525","title":"PreCommit阶段: 协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作"},{"parent":"1eee8b12a46d","children":[{"parent":"24711faa09db","children":[{"parent":"28223e1f26b4","children":[],"id":"7ccba5096def","title":"发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求"},{"parent":"28223e1f26b4","children":[],"id":"2dd11bddf0c0","title":"事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源"},{"parent":"28223e1f26b4","children":[],"id":"b7a3a303e9f7","title":"响应反馈 事务提交完之后,向协调者发送Ack响应"},{"parent":"28223e1f26b4","children":[],"id":"211c7b4aa22e","title":"完成事务 协调者接收到所有参与者的ack响应之后,完成事务"}],"id":"28223e1f26b4","title":"执行提交"},{"parent":"24711faa09db","children":[{"parent":"8abf9b8b22a0","children":[],"id":"f7664385e997","title":"发送中断请求 协调者向所有参与者发送abort请求"},{"parent":"8abf9b8b22a0","children":[],"id":"8f46d4cf1026","title":"事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源"},{"parent":"8abf9b8b22a0","children":[],"id":"18777ca3eaf1","title":"反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息"},{"parent":"8abf9b8b22a0","children":[],"id":"d27ccae9a9b6","title":"中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断"}],"id":"8abf9b8b22a0","title":"中断事务: 协调者没有接收到参与者发送的ACK响应, 或响应超时
    "}],"id":"24711faa09db","title":"doCommit阶段: 该阶段进行真正的事务提交"}],"id":"1eee8b12a46d","title":"流程"},{"parent":"e8856f8bce0a","children":[{"parent":"bef994d87c61","children":[],"id":"cce362d1b257","title":"降低了阻塞范围,在等待超时后协调者或参与者会中断事务"},{"parent":"bef994d87c61","children":[],"id":"c30827441ec6","title":"避免了协调者单点问题,doCommit阶段中协调者出现问题时,参与者会继续提交事务"}],"id":"bef994d87c61","title":"优点"},{"parent":"e8856f8bce0a","children":[{"parent":"0c7767c1b90d","children":[],"id":"5942b8825222","title":"脑裂问题依然存在,即在参与者收到PreCommit请求后等待最终指令,如果此时协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致"}],"id":"0c7767c1b90d","title":"缺点"},{"parent":"e8856f8bce0a","children":[],"id":"5bb33349d790","title":"是数据库层面解决方案"}],"id":"e8856f8bce0a","title":"3PC(三段式提交)"},{"parent":"426e047d8f9d","children":[{"parent":"b0b8679322ca","children":[{"parent":"006e94b9f913","children":[],"id":"59b9eb0f94d6","title":"Try阶段: 完成所有业务检查, 预留必须业务资源"},{"parent":"006e94b9f913","children":[],"id":"c5313a0fb055","title":"Confirm阶段: 真正执行业务, 不作任何业务检查, 只使用Try阶段预留的业务资源, Confirm操作必须保证幂等性
    "},{"parent":"006e94b9f913","children":[],"id":"471ecf6d09dc","title":"Cancel阶段: 释放Try阶段预留的业务资源, Cancel操作必须保证幂等性
    "}],"id":"006e94b9f913","title":"流程"},{"parent":"b0b8679322ca","children":[{"parent":"23870fd0159e","children":[],"id":"752def670283","title":"因为Try阶段检查并预留了资源,所以confirm阶段一般都可以执行成功"},{"parent":"23870fd0159e","children":[],"id":"a233a7cbf2e3","title":"资源锁定都是在业务代码中完成,不会block住DB,可以做到对db性能无影响"},{"parent":"23870fd0159e","children":[],"id":"7d19b1ea9c94","title":"TCC的实时性较高,所有的DB写操作都集中在confirm中,写操作的结果实时返回(失败时因为定时程序执行时间的关系,略有延迟)"}],"id":"23870fd0159e","title":"优点"},{"parent":"b0b8679322ca","children":[{"parent":"2ab2502c2344","children":[],"id":"c8b2dc57628d","title":"因为事务状态管理,将产生多次DB操作,这将损耗一定的性能,并使得整个TCC事务时间拉长"},{"parent":"2ab2502c2344","children":[],"id":"4d1e258645f4","title":"事务涉及方越多,Try、Confirm、Cancel中的代码就越复杂,可复用性就越底"},{"parent":"2ab2502c2344","children":[],"id":"56449003ad04","title":"涉及方越多,这几个阶段的处理时间越长,失败的可能性也越高"}],"id":"2ab2502c2344","title":"缺点"},{"parent":"b0b8679322ca","children":[],"id":"4c6d4fa72f28","title":"是业务层面解决方案"}],"id":"b0b8679322ca","title":"TCC(Try、Confirm、Cancel)"},{"parent":"426e047d8f9d","children":[],"id":"121687a469fc","title":"XA"},{"parent":"426e047d8f9d","children":[],"id":"48c137315bda","title":"最大努力通知"},{"parent":"426e047d8f9d","children":[],"id":"c6b7184c14c3","title":"本地消息表(ebay研发出的)"},{"parent":"426e047d8f9d","children":[{"parent":"3b31deded74d","children":[],"id":"84f89c2ef8a1","title":"事务消息"}],"id":"3b31deded74d","title":"半消息/最终一致性(RocketMQ)"}],"collapsed":true,"id":"426e047d8f9d","title":"分布式事务"}],"root":true,"theme":"dark_caihong","id":"root","title":"Java","structure":"mind_right"}},"meta":{"exportTime":"2022-02-16 17:35:03","member":"60b8501a63768975c7bcc153","diagramInfo":{"creator":"60b8501a63768975c7bcc153","created":"2021-06-23 17:55:24","modified":"2022-02-16 17:28:13","title":"Java知识点","category":"mind_free"},"id":"60d3050c1e08532a43b7f737","type":"ProcessOn Schema File","version":"1.0"}} \ No newline at end of file diff --git a/TODO/uml/ReplicaManager#appendRecordstxt b/TODO/uml/ReplicaManager#appendRecordstxt deleted file mode 100644 index cf9eb42aa7..0000000000 --- a/TODO/uml/ReplicaManager#appendRecordstxt +++ /dev/null @@ -1,20 +0,0 @@ -@startuml -title: ReplicaManager#appendRecords - -actor ReplicaManager as ReplicaManager - -alt requiredAcks值合法 -ReplicaManager -> ReplicaManager : 写入消息集到本地日志 -ReplicaManager -> ReplicaManager : 构建写入结果状态 -alt 等待其他副本完成写入 -ReplicaManager -> ReplicaManager : 创建延时请求对象 -ReplicaManager -> ReplicaManager : 交由 Puratory 管理 -else -ReplicaManager -> ReplicaManager : 调用回调逻辑 -end -else requiredAcks值非法 -ReplicaManager -> ReplicaManager : 构造特定异常对象 -ReplicaManager -> ReplicaManager : 封装进回调函数执行 -end - -@enduml \ No newline at end of file diff --git a/TODO/uml/index.js b/TODO/uml/index.js new file mode 100644 index 0000000000..d28a940534 --- /dev/null +++ b/TODO/uml/index.js @@ -0,0 +1,6 @@ +import React from 'react' +import ReactDOM from 'react-dom' + +const mydiv = React.createElement('div',{id:'mydiv', title: 'div aaa'}, '这是一个 div') + +ReactDOM.render(mydiv, document.getElementById('app')) \ No newline at end of file diff --git a/TODO/uml/mysql.xmind b/TODO/uml/mysql.xmind deleted file mode 100644 index 2c439b7a2e..0000000000 Binary files a/TODO/uml/mysql.xmind and /dev/null differ diff --git "a/TODO/uml/redis\344\274\230\345\214\226.xmind" "b/TODO/uml/redis\344\274\230\345\214\226.xmind" deleted file mode 100644 index f4dfaa071a..0000000000 Binary files "a/TODO/uml/redis\344\274\230\345\214\226.xmind" and /dev/null differ diff --git a/TODO/uml/spring.xmind b/TODO/uml/spring.xmind deleted file mode 100644 index f9fd71b68e..0000000000 Binary files a/TODO/uml/spring.xmind and /dev/null differ diff --git a/TODO/uml/test.java b/TODO/uml/test.java new file mode 100644 index 0000000000..7b6947aefa --- /dev/null +++ b/TODO/uml/test.java @@ -0,0 +1,321 @@ + +===================================== +2021-06-07 23:34:29 0x7f9e65cb4700 INNODB MONITOR OUTPUT +===================================== +Per second averages calculated from the last 7 seconds +----------------- +BACKGROUND THREAD +----------------- +srv_master_thread loops: 5114424 srv_active, 0 srv_shutdown, 14722045 srv_idle +srv_master_thread log flush and writes: 19832170 +---------- +SEMAPHORES +---------- +OS WAIT ARRAY INFO: reservation count 36041107 +OS WAIT ARRAY INFO: signal count 32703412 +RW-shared spins 0, rounds 11645805, OS waits 4127499 +RW-excl spins 0, rounds 45779914, OS waits 950903 +RW-sx spins 187503, rounds 4165206, OS waits 80783 +Spin rounds per wait: 11645805.00 RW-shared, 45779914.00 RW-excl, 22.21 RW-sx +------------------------ +LATEST DETECTED DEADLOCK +------------------------ +2021-06-01 22:00:51 0x7f9e9f6db700 +*** (1) TRANSACTION: +TRANSACTION 4385633832, ACTIVE 0 sec updating or deleting +mysql tables in use 1, locked 1 +LOCK WAIT 6 lock struct(s), heap size 1136, 6 row lock(s), undo log entries 2 +MySQL thread id 20231571, OS thread handle 140318965561088, query id 3402074420 10.100.249.217 dev update +REPLACE into sub_order_index (member_level, crs_no, sub_crs_no, + arrival_date, departure_date, profile_status, + multiple_value, multiple_type, hotel_id, + member_id, is_self_checkin, deleted, + create_time, create_user, modify_time, + modify_user, resv_profile_no) + values + + ('NON', '200001421060120412988600PX04PLZD', 'S2000014210601204129957001X043XMV', '2021-06-01 20:01:20.0', '2021-06-01 20:09:33.0', 'HOTEL_ROOM_CHECK_OUT', '8C064A283FF2A94E6DEF4B74B0BE7D72', '9', '2000014', '', 0, 0, '2021-06-01 22:00:51.331', 'SyncPms', '2021-06-01 22:00:51.331', 'SyncPms', 'T2000014055281679001') +*** (1) WAITING FOR THIS LOCK TO BE GRANTED: +RECORD LOCKS space id 19517 page no 28 n bits 312 index idx_multiple_value_multiple_type_resv_profile_no of table `orderdb_26`.`sub_order_index` trx id 4385633832 lock_mode X locks gap before rec insert intention waiting +Record lock, heap no 177 PHYSICAL RECORD: n_fields 4; compact format; info bits 32 + 0: len 30; hex 384330363441323833464632413934453644454634423734423042453744; asc 8C064A283FF2A94E6DEF4B74B0BE7D; (total 32 bytes); + 1: len 1; hex 39; asc 9;; + 2: len 20; hex 5432303030303134303535323831363739303031; asc T2000014055281679001;; + 3: len 4; hex 80010a72; asc r;; + +*** (2) TRANSACTION: +TRANSACTION 4385633839, ACTIVE 0 sec inserting +mysql tables in use 1, locked 1 +4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1 +MySQL thread id 20231673, OS thread handle 140319256327936, query id 3402074427 10.100.249.217 dev update +REPLACE into sub_order_index (member_level, crs_no, sub_crs_no, + arrival_date, departure_date, profile_status, + multiple_value, multiple_type, hotel_id, + member_id, is_self_checkin, deleted, + create_time, create_user, modify_time, + modify_user, resv_profile_no) + values + + ('NON', '200001421060120412988600PX04PLZD', 'S2000014210601204129957001X043XMV', '2021-06-01 20:01:20.0', '2021-06-01 20:09:33.0', 'HOTEL_ROOM_CHECK_OUT', '8C064A283FF2A94E6DEF4B74B0BE7D72', '9', '2000014', '', 0, 0, '2021-06-01 22:00:51.363', 'SyncPms', '2021-06-01 22:00:51.363', 'SyncPms', 'T2000014055281679001') +*** (2) HOLDS THE LOCK(S): +RECORD LOCKS space id 19517 page no 28 n bits 312 index idx_multiple_value_multiple_type_resv_profile_no of table `orderdb_26`.`sub_order_index` trx id 4385633839 lock_mode X locks gap before rec +Record lock, heap no 177 PHYSICAL RECORD: n_fields 4; compact format; info bits 32 + 0: len 30; hex 384330363441323833464632413934453644454634423734423042453744; asc 8C064A283FF2A94E6DEF4B74B0BE7D; (total 32 bytes); + 1: len 1; hex 39; asc 9;; + 2: len 20; hex 5432303030303134303535323831363739303031; asc T2000014055281679001;; + 3: len 4; hex 80010a72; asc r;; + +*** (2) WAITING FOR THIS LOCK TO BE GRANTED: +RECORD LOCKS space id 19517 page no 28 n bits 312 index idx_multiple_value_multiple_type_resv_profile_no of table `orderdb_26`.`sub_order_index` trx id 4385633839 lock_mode X waiting +Record lock, heap no 177 PHYSICAL RECORD: n_fields 4; compact format; info bits 32 + 0: len 30; hex 384330363441323833464632413934453644454634423734423042453744; asc 8C064A283FF2A94E6DEF4B74B0BE7D; (total 32 bytes); + 1: len 1; hex 39; asc 9;; + 2: len 20; hex 5432303030303134303535323831363739303031; asc T2000014055281679001;; + 3: len 4; hex 80010a72; asc r;; + +*** WE ROLL BACK TRANSACTION (2) +------------ +TRANSACTIONS +------------ +Trx id counter 4464613238 +Purge done for trx's n:o < 4464613238 undo n:o < 0 state: running but idle +History list length 2 +LIST OF TRANSACTIONS FOR EACH SESSION: +---TRANSACTION 421800976044544, not started +0 lock struct(s), heap size 1136, 0 row lock(s) +---TRANSACTION 421800976043632, not started +0 lock struct(s), heap size 1136, 0 row lock(s) +---TRANSACTION 421800976045456, not started +0 lock struct(s), heap size 1136, 0 row lock(s) +---TRANSACTION 421800976049104, not started +0 lock struct(s), heap size 1136, 0 row lock(s) +---TRANSACTION 421800976048192, not started +0 lock struct(s), heap size 1136, 0 row lock(s) +---TRANSACTION 421800976047280, not started +0 lock struct(s), heap size 1136, 0 row lock(s) +---TRANSACTION 421800976046368, not started +0 lock struct(s), heap size 1136, 0 row lock(s) +---TRANSACTION 421800976042720, not started +0 lock struct(s), heap size 1136, 0 row lock(s) +---TRANSACTION 421800976041808, not started +0 lock struct(s), heap size 1136, 0 row lock(s) +---TRANSACTION 421800976052752, not started +0 lock struct(s), heap size 1136, 0 row lock(s) +---TRANSACTION 421800976079200, not started +0 lock struct(s), heap size 1136, 0 row lock(s) +---TRANSACTION 421800976076464, not started +0 lock struct(s), heap size 1136, 0 row lock(s) +---TRANSACTION 421800976075552, not started +0 lock struct(s), heap size 1136, 0 row lock(s) +---TRANSACTION 421800976073728, not started +0 lock struct(s), heap size 1136, 0 row lock(s) +---TRANSACTION 421800976185904, not started +0 lock struct(s), heap size 1136, 0 row lock(s) +---TRANSACTION 421800976050016, not started +0 lock struct(s), heap size 1136, 0 row lock(s) +-------- +FILE I/O +-------- +I/O thread 0 state: waiting for completed aio requests (insert buffer thread) +I/O thread 1 state: waiting for completed aio requests (log thread) +I/O thread 2 state: waiting for completed aio requests (read thread) +I/O thread 3 state: waiting for completed aio requests (read thread) +I/O thread 4 state: waiting for completed aio requests (read thread) +I/O thread 5 state: waiting for completed aio requests (read thread) +I/O thread 6 state: waiting for completed aio requests (write thread) +I/O thread 7 state: waiting for completed aio requests (write thread) +I/O thread 8 state: waiting for completed aio requests (write thread) +I/O thread 9 state: waiting for completed aio requests (write thread) +Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] , + ibuf aio reads:, log i/o's:, sync i/o's: +Pending flushes (fsync) log: 0; buffer pool: 0 +5505530 OS file reads, 951355475 OS file writes, 818298558 OS fsyncs +0.00 reads/s, 0 avg bytes/read, 0.29 writes/s, 0.29 fsyncs/s +------------------------------------- +INSERT BUFFER AND ADAPTIVE HASH INDEX +------------------------------------- +Ibuf: size 1, free list len 6642, seg size 6644, 83407 merges +merged operations: + insert 84507, delete mark 177929, delete 20750 +discarded operations: + insert 0, delete mark 0, delete 0 +Hash table size 553193, node heap has 37 buffer(s) +Hash table size 553193, node heap has 106 buffer(s) +Hash table size 553193, node heap has 150 buffer(s) +Hash table size 553193, node heap has 141 buffer(s) +Hash table size 553193, node heap has 98 buffer(s) +Hash table size 553193, node heap has 415 buffer(s) +Hash table size 553193, node heap has 503 buffer(s) +Hash table size 553193, node heap has 39 buffer(s) +0.00 hash searches/s, 0.00 non-hash searches/s +--- +LOG +--- +Log sequence number 1213523098520 +Log flushed up to 1213523098520 +Pages flushed up to 1213523098520 +Last checkpoint at 1213523098511 +0 pending log flushes, 0 pending chkp writes +755158342 log i/o's done, 0.29 log i/o's/second +---------------------- +BUFFER POOL AND MEMORY +---------------------- +Total large memory allocated 2198863872 +Dictionary memory allocated 6066331 +Buffer pool size 131056 +Free buffers 8201 +Database pages 121366 +Old database pages 44638 +Modified db pages 0 +Pending reads 0 +Pending writes: LRU 0, flush list 0, single page 0 +Pages made young 10467337, not young 105475546 +0.00 youngs/s, 0.00 non-youngs/s +Pages read 5502201, created 761687, written 168169615 +0.00 reads/s, 0.00 creates/s, 0.00 writes/s +Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000 +Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s +LRU len: 121366, unzip_LRU len: 0 +I/O sum[5944]:cur[0], unzip sum[0]:cur[0] +---------------------- +INDIVIDUAL BUFFER POOL INFO +---------------------- +---BUFFER POOL 0 +Buffer pool size 16382 +Free buffers 1025 +Database pages 15171 +Old database pages 5580 +Modified db pages 0 +Pending reads 0 +Pending writes: LRU 0, flush list 0, single page 0 +Pages made young 1172041, not young 12676848 +0.00 youngs/s, 0.00 non-youngs/s +Pages read 667343, created 92295, written 21898861 +0.00 reads/s, 0.00 creates/s, 0.00 writes/s +No buffer pool page gets since the last printout +Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s +LRU len: 15171, unzip_LRU len: 0 +I/O sum[743]:cur[0], unzip sum[0]:cur[0] +---BUFFER POOL 1 +Buffer pool size 16382 +Free buffers 1025 +Database pages 15163 +Old database pages 5577 +Modified db pages 0 +Pending reads 0 +Pending writes: LRU 0, flush list 0, single page 0 +Pages made young 1431640, not young 13962711 +0.00 youngs/s, 0.00 non-youngs/s +Pages read 736157, created 95590, written 8880005 +0.00 reads/s, 0.00 creates/s, 0.00 writes/s +No buffer pool page gets since the last printout +Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s +LRU len: 15163, unzip_LRU len: 0 +I/O sum[743]:cur[0], unzip sum[0]:cur[0] +---BUFFER POOL 2 +Buffer pool size 16382 +Free buffers 1025 +Database pages 15171 +Old database pages 5580 +Modified db pages 0 +Pending reads 0 +Pending writes: LRU 0, flush list 0, single page 0 +Pages made young 1349112, not young 13505819 +0.00 youngs/s, 0.00 non-youngs/s +Pages read 690852, created 95765, written 11865138 +0.00 reads/s, 0.00 creates/s, 0.00 writes/s +No buffer pool page gets since the last printout +Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s +LRU len: 15171, unzip_LRU len: 0 +I/O sum[743]:cur[0], unzip sum[0]:cur[0] +---BUFFER POOL 3 +Buffer pool size 16382 +Free buffers 1025 +Database pages 15174 +Old database pages 5581 +Modified db pages 0 +Pending reads 0 +Pending writes: LRU 0, flush list 0, single page 0 +Pages made young 1138937, not young 11821041 +0.00 youngs/s, 0.00 non-youngs/s +Pages read 624793, created 94616, written 37067605 +0.00 reads/s, 0.00 creates/s, 0.00 writes/s +No buffer pool page gets since the last printout +Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s +LRU len: 15174, unzip_LRU len: 0 +I/O sum[743]:cur[0], unzip sum[0]:cur[0] +---BUFFER POOL 4 +Buffer pool size 16382 +Free buffers 1025 +Database pages 15176 +Old database pages 5582 +Modified db pages 0 +Pending reads 0 +Pending writes: LRU 0, flush list 0, single page 0 +Pages made young 1434122, not young 14038054 +0.00 youngs/s, 0.00 non-youngs/s +Pages read 729050, created 96300, written 29291343 +0.00 reads/s, 0.00 creates/s, 0.00 writes/s +No buffer pool page gets since the last printout +Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s +LRU len: 15176, unzip_LRU len: 0 +I/O sum[743]:cur[0], unzip sum[0]:cur[0] +---BUFFER POOL 5 +Buffer pool size 16382 +Free buffers 1025 +Database pages 15162 +Old database pages 5576 +Modified db pages 0 +Pending reads 0 +Pending writes: LRU 0, flush list 0, single page 0 +Pages made young 1327458, not young 13579773 +0.00 youngs/s, 0.00 non-youngs/s +Pages read 702086, created 95588, written 18332748 +0.00 reads/s, 0.00 creates/s, 0.00 writes/s +No buffer pool page gets since the last printout +Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s +LRU len: 15162, unzip_LRU len: 0 +I/O sum[743]:cur[0], unzip sum[0]:cur[0] +---BUFFER POOL 6 +Buffer pool size 16382 +Free buffers 1025 +Database pages 15185 +Old database pages 5585 +Modified db pages 0 +Pending reads 0 +Pending writes: LRU 0, flush list 0, single page 0 +Pages made young 1349030, not young 13303436 +0.00 youngs/s, 0.00 non-youngs/s +Pages read 673951, created 95080, written 17220572 +0.00 reads/s, 0.00 creates/s, 0.00 writes/s +No buffer pool page gets since the last printout +Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s +LRU len: 15185, unzip_LRU len: 0 +I/O sum[743]:cur[0], unzip sum[0]:cur[0] +---BUFFER POOL 7 +Buffer pool size 16382 +Free buffers 1026 +Database pages 15164 +Old database pages 5577 +Modified db pages 0 +Pending reads 0 +Pending writes: LRU 0, flush list 0, single page 0 +Pages made young 1264997, not young 12587864 +0.00 youngs/s, 0.00 non-youngs/s +Pages read 677969, created 96453, written 23613343 +0.00 reads/s, 0.00 creates/s, 0.00 writes/s +Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000 +Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s +LRU len: 15164, unzip_LRU len: 0 +I/O sum[743]:cur[0], unzip sum[0]:cur[0] +-------------- +ROW OPERATIONS +-------------- +0 queries inside InnoDB, 0 queries in queue +0 read views open inside InnoDB +Process ID=26515, Main thread ID=140323700393728, state: sleeping +Number of rows inserted 33434277, updated 739038616, deleted 695767, read 6545664881069 +0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 142.12 reads/s +---------------------------- +END OF INNODB MONITOR OUTPUT +============================ diff --git "a/Tomcat/HttpServlet\344\270\272\344\273\200\344\271\210\350\246\201\345\256\236\347\216\260serializable\357\274\237.md" "b/Tomcat/HttpServlet\344\270\272\344\273\200\344\271\210\350\246\201\345\256\236\347\216\260serializable\357\274\237.md" deleted file mode 100644 index 0d6bacd6c7..0000000000 --- "a/Tomcat/HttpServlet\344\270\272\344\273\200\344\271\210\350\246\201\345\256\236\347\216\260serializable\357\274\237.md" +++ /dev/null @@ -1,8 +0,0 @@ -HttpServlet为什么要实现serializable?在什么情况下,servlet会被序列化? -如果未显示定义serialVersionUID,系统会用什么算法给指定一个? -![](https://img-blog.csdnimg.cn/d745c434cdc8418e9519cb821fbd9317.png) -![](https://img-blog.csdnimg.cn/71dc3f2b6ef14d1099605a75c4412639.png) -Serializable是可序列化。 -简单点将,就是实现了这个接口后,实例就可以转化为数据流了。 - -Servlet 是有状态的,所以需要持久化到本地(钝化),然后当 Tomcat 重启时,重新加载出来。比如Servlet存储了一些用户登录信息,而当时分布式缓存 redis 也还没流行,所以需要支持可序列化。 \ No newline at end of file diff --git "a/Tomcat/Servlet\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md" "b/Tomcat/Servlet\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md" deleted file mode 100644 index c3343e881b..0000000000 --- "a/Tomcat/Servlet\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md" +++ /dev/null @@ -1,67 +0,0 @@ -Servlet从创建直到毁灭的整个过程: -- Servlet 初始化后调用 init () 方法 -- Servlet 调用 service() 方法来处理客户端的请求 -- Servlet 销毁前调用 destroy() 方法 -- 最后,Servlet 是由 JVM 的垃圾回收器进行GC -# init() -只调用一次。在第一次创建 Servlet 时被调用,在后续每次用户请求时不再调用。因此,它是用于一次性初始化。 - -Servlet 创建于用户第一次调用对应于该 Servlet 的 URL 时,但是您也可以指定 Servlet 在服务器第一次启动时被加载。 - -当用户调用一个 Servlet 时,就会创建一个 Servlet 实例,每一个用户请求都会产生一个新的线程,适当的时候移交给 doGet 或 doPost 方法。init() 方法简单地创建或加载一些数据,这些数据将被用于 Servlet 的整个生命周期。 -```java -public void init() throws ServletException { - // 初始化代码... -} -``` -## service() -执行实际任务的主要方法。Servlet 容器(即 Web 服务器)调用 service() 方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。 - -每次服务器接收到一个 Servlet 请求时,服务器会产生一个新的线程并调用服务。service() 方法检查 HTTP 请求类型(GET、POST、PUT、DELETE 等),并在适当的时候调用 doGet、doPost、doPut,doDelete 等方法。 -```java -public void service(ServletRequest request, - ServletResponse response) - throws ServletException, IOException{ -} -``` -service() 方法由容器调用,service 方法在适当的时候调用 doGet、doPost、doPut、doDelete 等方法。所以,您不用对 service() 方法做任何动作,您只需要根据来自客户端的请求类型来重写 doGet() 或 doPost() 即可。 - -doGet() 和 doPost() 方法是每次服务请求中最常用的方法。下面是这两种方法的特征。 -## doGet() -GET 请求来自于一个 URL 的正常请求,或者来自于一个未指定 METHOD 的 HTML 表单,它由 doGet() 方法处理。 -```java -public void doGet(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException { - // Servlet 代码 -} -``` -## doPost() -POST 请求来自于一个特别指定了 METHOD 为 POST 的 HTML 表单,它由 doPost() 方法处理。 -```java -public void doPost(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException { - // Servlet 代码 -} -``` -## destroy() 方法 -destroy() 方法只会被调用一次,在 Servlet 生命周期结束时被调用。destroy() 方法可以让您的 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘,并执行其他类似的清理活动。 - -在调用 destroy() 方法之后,servlet 对象被标记为垃圾回收。destroy 方法定义如下所示: -```java - public void destroy() { - // 终止化代码... - } -``` -# 架构 -![](https://img-blog.csdnimg.cn/64b79221818a4830add2e3333b09355c.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_13,color_FFFFFF,t_70,g_se,x_16) -- 第一个到达服务器的 HTTP 请求被委派到 Servlet 容器 -- Servlet 容器在调用 service() 方法之前加载 Servlet -- 然后 Servlet 容器处理由多个线程产生的多个请求,每个线程执行一个单一的 Servlet 实例的 service() 方法 - -![](https://img-blog.csdnimg.cn/4a408cce640d4fccae2efb410bab50e1.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -Spring 设计成了枚举 -![](https://img-blog.csdnimg.cn/7267c2be0c0d4f03b7b93405ea8779d5.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -Tomat 是设计成普通常量,没有范围,可随便突破。 -区别在于,枚举进行了范围限制。 \ No newline at end of file diff --git "a/Tomcat/Servlet\350\247\204\350\214\203\345\217\212\345\256\271\345\231\250.md" "b/Tomcat/Servlet\350\247\204\350\214\203\345\217\212\345\256\271\345\231\250.md" deleted file mode 100644 index e98a8c06de..0000000000 --- "a/Tomcat/Servlet\350\247\204\350\214\203\345\217\212\345\256\271\345\231\250.md" +++ /dev/null @@ -1,247 +0,0 @@ -# 1 遥远的CGI -实现Web动态内容的技术,最早使用的是CGI(Common Gateway Interface,通用网关接口)技术,根据用户输入的请求动态地传送HTML数据。 -CGI并不是开发语言,而只是能够利用为它编写的程序来实现Web服务器的一种协议。 -可用来实现电子商务网站、搜索引擎处理和在线登记等功能。当用户在Web页面中提交输入的数据时,Web浏览器就会将用户输入的数据发送到Web服务器上。在服务器上,CGI程序对输入的数据进行格式化,并将这个信息发送给数据库或服务器上运行的其他程序,然后将结果返回给Web服务器。最后,Web服务器将结果发送给Web浏览器,这些结果有时使用新的Web页面显示,有时在当前Web页面中显示。 - -编写自定义CGI脚本需要相当多的编程技巧,多数CGI脚本是由Perl,Java,C和C++等语言编写的,服务器上通常很少运行用JavaScript编写的服务器脚本,不管使用何种语言,Web页面设计者都需要控制服务器,包括所需要的后台程序(如数据库),这些后台程序提供结果或来自用户的消息。即使拥有基于服务器的网站设计工具,编写CGI程序也要求程序设计者有一定的经验。 - -由于每一次对于动态内容的请求都需要启动一个新的CGI程序,因而会增加Web服务器的负担,所以CGI的一个很大缺陷是容易影响Web服务器的运行速度。 -## 脚本编程 -由于CGI程序与HTML文档需要分开编写、分开运行,要将两者融合在一起并不容易,因此,CGI程序的维护与编写比较困难。为了解决这一问题,一些厂商推出了脚本语言来增强网页开发功能。 - -脚本语言是一种文本型编程语言,可嵌入到HTML文档中。脚本语言分客户端和服务器端两种类型,分别在Web浏览器和Web服务器中运行。 - -客户端脚本语言主要有JavaScript、Jscript(Microsoft公司的JavaSCript版本)和VBscript等。当Web浏览器需要浏览使用客户端脚本语言编写的Web页面时,Web服务器将客户端脚本连同Web页面一起传送到Web浏览器,Web浏览器同时显示HTML的显示效果和客户端脚本的运行效果, 客户端脚本可减轻Web服务器的处理负担,提高Web页面的响应速度。 - -服务器端脚本语言主要有ASP,JSP,PHP和LiveWire等。当Web浏览器需要浏览使用服务器端脚本语言编写的Web页面时,Web服务器运行Web页面中的服务器端脚本,将由脚本语言的运行结果与Web页面的HTML部分生成的新的Web页面传送到Web浏览器,Web浏览器显示生成的新的Web页面, 服务器端脚本可减少不同Web浏览器的运行差异,提高Web页面的实用性。 - -这期间,Java 的 Servlet模型也就诞生了。 -# 2 Servlet -一种基于Java技术的Web组件,用于生成动态内容,由容器管理。类似于其它Java技术组件,Servlet 是平台无关的Java类组成,并且由Java Web服务器加载执行。 - -通常由Servlet容器提供运行时环境。Servlet 容器,有时候也称作为Servlet引擎,作为Web服务器或应用服务器的一部分 。通过请求和响应对话,提供Web客户端与Servlets 交互的能力。容器管理Servlets实例以及它们的生命周期。 - -## 主要版本 -![](https://img-blog.csdnimg.cn/9bdc4d3780174fe6a963291dfea900bb.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -## 核心组件 -|核心组件 API|说明 | 起始版本| Spring framework 代表实现 | -|--|--|--|--| -|javax. servlet .Servlet | 动态内容组件| 1.0| DispatcherServlet| -|javax. servlet .Filter | Servlet过滤器| 2.3| CharacterEncodingFilter| -|javax. servlet .ServletContext | Servlet应用上下文| | | -|javax. servlet .AsyncContext | 异步上下文|3.0 | 无| -|javax. servlet .ServletContextistener | ServletContext生命周期监听器| 2.3 | ContextLoaderListener | -|javax . servlet . ServletRequestlistener | ServletRequest生命周期监听器| 2.3 | RequestContextListener | -|javax. servlet .http.HttpSessionListener | 生命周期监听器| 2.3| HttpSessionMutexListener | -|javax. servlet .Asynclistener | 异步上下文监听器| 3.0| StandardServletAsyncWebRequest | -|javax. servlet .ServletContainerInitializer | Servlet容器初始化器|3.0 | SpringServletContainerInitializer | - - -浏览器发给服务端的是一个HTTP请求,HTTP服务器收到请求后,需调用服务端程序处理请求。 - -> HTTP服务器怎么知道要调用哪个处理器方法? - -最简单的就是在HTTP服务器代码写一堆if/else:若是A请求就调x类m1方法,若是B请求就调o类的m2方法。 -这种设计的致命点在于HTTP服务器代码跟业务逻辑耦合,若你新增了业务方法,竟然还得改HTTP服务器代码。 -**面向接口编程**算得上是解决耦合问题的银弹,我们可定义一个接口,各业务类都实现该接口,没错,他就是Servlet接口,实现了Servlet接口的业务类也叫作Servlet。 - -解决了业务逻辑和HTTP服务器的耦合问题,那又有问题了:对特定请求,HTTP服务器又如何知道: -- 哪个Servlet负责处理请求? -- 谁负责实例化Servlet? - -显然HTTP服务器不适合负责这些,否则又要和业务逻辑耦合。 - -于是,诞生了Servlet容器。 -# 3 Servlet容器 -用于加载和管理业务类。 - -HTTP服务器不直接和业务类交互,而是把请求先交给Servlet容器,Servlet容器内部将请求转发到具体Servlet。 -若该Servlet还没创建,就加载并实例化之,然后调用该Servlet的接口方法。 - -因此Servlet接口其实是Servlet容器跟具体业务类之间的接口: -![](https://img-blog.csdnimg.cn/20210716140239681.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -左:HTTP服务器直接调用具体业务类,但紧耦合。 -右:HTTP服务器不直接调用业务类,而是把请求移交给容器,容器通过Servlet接口调用业务类。因此Servlet接口和Servlet容器,实现了HTTP服务器与业务类的解耦。 - -**Servlet接口和Servlet容器**这一整套规范叫作**Servlet规范**。Tomcat按Servlet规范要求实现了Servlet容器,又兼备HTTP服务器功能。 -若实现新业务,只需实现一个Servlet,并把它注册到Tomcat(Servlet容器),剩下的事情就由Tomcat帮忙。 -# 4 Servlet接口 -Servlet接口定义了下面五个方法: -![](https://img-blog.csdnimg.cn/20210716140638840.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -具体业务类在service方法实现处理逻辑,入参这两个类是对通信协议的封装: -- ServletRequest -封装请求信息 -- ServletResponse -封装响应信息 - -HTTP协议中的请求和响应就对应 -- HttpServletRequest -获取所有请求相关的信息,包括请求路径、Cookie、HTTP头、请求参数等。还能创建和获取Session -- HttpServletResponse -封装HTTP响应 - -生命周期相关方法: -- init -Servlet容器在加载Servlet类的时候会调用,可能会在init方法里初始化一些资源。比如Spring MVC中的DispatcherServlet,就是在init方法里创建了自己的Spring容器。 -- destroy -卸载时会调用,可能在destroy方法里释放这些资源 - -## ServletConfig -ServletConfig的作用就是封装Servlet的初始化参数。 -可以在`web.xml`给Servlet配置参数,并在程序通过getServletConfig拿到这些参数。 - -有接口一般就有抽象类,抽象类用来实现接口和封装通用的逻辑,因此Servlet规范提供了GenericServlet抽象类,可以通过扩展它来实现Servlet。虽然Servlet规范并不在乎通信协议是什么,但是大多数的Servlet都是在HTTP环境中处理的,因此Servet规范还提供了HttpServlet来继承GenericServlet,并且加入了HTTP特性。这样我们通过继承HttpServlet类来实现自己的Servlet,只需要重写两个方法:doGet和doPost。 -![](https://img-blog.csdnimg.cn/20210716162226262.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -# 5 Servlet容器 -## 工作流程 -当客户请求某个资源时 -- HTTP服务器用ServletRequest对象封装客户的请求信息 -- 然后调用Servlet容器的service方法 -- Servlet容器拿到请求后,根据请求的URL和Servlet的映射关系,找到相应的Servlet -- 如果Servlet还没有被加载,就用反射创建该Servlet -- 调用Servlet的init方法来完成初始化 -- 调用Servlet的service方法来处理请求 -- 把ServletResponse对象返回给HTTP服务器,HTTP服务器会把响应发送给客户端 - -![](https://img-blog.csdnimg.cn/20210716165416791.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -## Web应用 -Servlet容器会实例化和调用Servlet,那Servlet怎么注册到Servlet容器? - -根据Servlet规范,Web应用程序有一定目录结构,放置了 -- Servlet的类文件 -- 配置文件 -- 静态资源 - -Servlet容器通过读取配置文件,就能找到并加载Servlet。Web应用的大致目录结构: -```bash -| - MyWebApp - | - WEB-INF/web.xml -- 配置文件,用来配置Servlet等 - | - WEB-INF/lib/ -- 存放Web应用所需各种JAR包 - | - WEB-INF/classes/ -- 存放你的应用类,比如Servlet类 - | - META-INF/ -- 目录存放工程的一些信息 -``` -# 6 ServletContext - -> 定义了一系列servlet用于与其servlet容器通信的方法。如获取文件的 MIME 类型、调度请求或写入日志文件。 -> 每个JVM的Web应用程序都有一个上下文。(Web 应用程序是安装在服务器 URL 名称空间(如 -> `/catalog`)的特定子集下并可能通过 。war 文件安装的服务和内容的集合。如果在部署描述符中标 -> -> 分布式系统下,则每个机器节点都有一个上下文实例。在这种情况下,上下文不能用作共享全局信息的位置(因为信息不会是真正的全局的)。应该改用数据库等外部资源。ServletContext -> 对象包含在 ServletConfig 对象中,当服务器初始化时,Web 服务器会提供该对象。 - -Servlet规范定义了ServletContext接口对应一个Web应用。比如一个 SpringBoot 应用,那就只有一个ServletContext。 -不要和 Spring 的 applicationContext 混为一谈,因为一个应用中,可以有多个Spring 的 applicationContext。 - -Web应用部署好后,Servlet容器在启动时会加载Web应用,并为每个Web应用创建一个全局的上下文环境ServletContext对象,为后面的Spring容器提供宿主环境。 - -可将**ServletContext**看做一个全局对象,一个Web应用可能有多个Servlet,这些Servlet可通过全局ServletContext共享数据: -- Web应用的初始化参数 -- Web应用目录下的文件资源等 - -**ServletContext** 持有所有Servlet实例,所以也能实现Servlet请求的转发。 - -Tomcat&Jetty在启动过程中触发容器初始化事件,Spring的ContextLoaderListener会监听到这个事件,它的contextInitialized方法会被调用,在这个方法中,Spring会初始化全局的Spring根容器,这个就是Spring的IoC容器。 -IoC容器初始化完毕后,Spring将其存储到**ServletContext**,便于以后获取。 -![](https://img-blog.csdnimg.cn/20210716220353539.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -ServletContext就是用来共享数据的,比如SpringMVC需要从ServletContext拿到全局的Spring容器,把它设置成自己的父容器。 - -Tomcat&Jetty在启动过程中还会扫描Servlet,一个Web应用中的Servlet可以有多个,以SpringMVC中的DispatcherServlet为例,这个Servlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个Servlet请求。 - -Servlet一般会延迟加载,当第一个请求达到时,Tomcat&Jetty发现DispatcherServlet还没有被实例化,就调用DispatcherServlet的init方法,DispatcherServlet在初始化的时候会建立自己的容器,叫做SpringMVC 容器,用来持有Spring MVC相关的Bean。同时,Spring MVC还会通过ServletContext拿到Spring根容器,并将Spring根容器设为SpringMVC容器的父容器,请注意,Spring MVC容器可以访问父容器中的Bean,但是父容器不能访问子容器的Bean, 也就是说Spring根容器不能访问SpringMVC容器里的Bean。说的通俗点就是,在Controller里可以访问Service对象,但是在Service里不可以访问Controller对象。 -# 初始化工作 -Tomcat/Jetty启动,对于每个WebApp,依次进行初始化工作: -1、对每个WebApp,都有一个WebApp ClassLoader,和一个ServletContext -2、ServletContext启动时,会扫描web.xml配置文件,找到Filter、Listener和Servlet配置 - -3、如果Listener中配有spring的ContextLoaderListener -3.1、ContextLoaderListener就会收到webapp的各种状态信息。 -3.3、在ServletContext初始化时,ContextLoaderListener也就会将Spring IOC容器进行初始化,管理Spring相关的Bean。 -3.4、ContextLoaderListener会将Spring IOC容器存放到ServletContext中 - -4、如果Servlet中配有SpringMVC的DispatcherServlet -4.1、DispatcherServlet初始化时(其一次请求到达)。 -4.2、其中,DispatcherServlet会初始化自己的SpringMVC容器,用来管理Spring MVC相关的Bean。 -4.3、SpringMVC容器可以通过ServletContext获取Spring容器,并将Spring容器设置为自己的根容器。而子容器可以访问父容器,从而在Controller里可以访问Service对象,但是在Service里不可以访问Controller对象。 -4.2、初始化完毕后,DispatcherServlet开始处理MVC中的请求映射关系。 - -Servlet默认单例模式,Spring的Bean默认也是单例模式,则Spring MVC是如何处理并发请求? -DispatcherServlet中的成员变量都是初始化好后就不会被改变了,所以是线程安全的,那“可见性”怎么保证呢? -由Web容器比如Tomcat保证,Tomcat在调用Servlet的init方法时,用synchronized。 - -- 若还没有至少一个已初始化的实例,则加载并初始化该 servlet 的一个实例。 例如,这可用于加载deployment descriptor中标记为在服务器启动时加载的 servlet。 -实现说明:类名以`org.apache.catalina.`开头的 Servlet org.apache.catalina. (所谓的 servlet容器)由加载此类的同一类加载器加载,而非由当前 Web 应用程序的类加载器加载。 这使此类可以访问 Catalina 内部结构,而对于为 Web 应用程序加载的类,这种访问权限是被阻止的 -![](https://img-blog.csdnimg.cn/20210716204054195.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20210716210859693.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20210716210934704.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -# 扩展机制 -引入了Servlet规范后,无需关心Socket网络通信、HTTP协议或你的业务类是如何被实例化和调用的,因为这些都被Servlet规范标准化了,我们只需关心怎么实现业务逻辑。 - -有规范看着很方便,但若规范不能满足你的个性需求,就没法用了,因此设计一个规范或者一个中间件,要充分考虑到可扩展性。 -Servlet规范提供了两种扩展机制:Filter和Listener。 -## Filter -过滤器,该接口允许你对请求和响应做一些统一的定制化处理。Filter是基于过程的,它是过程的一部分,是基于过程行为的。比如: -- 根据请求频率限制访问 -- 根据地区不同修改响应内容 - - 过滤器是 Servlet 的重要标准之一: -- 请求和响应的统一处理 -- 访问日志记录 -- 请求权限审核 - ...... -都发挥重要作用 - -### 工作原理 -Web应用部署完成后,Servlet容器需要实例化Filter并把Filter链接成一个FilterChain。当请求进来时,获取第一个Filter并调用doFilter方法,doFilter方法负责调用这个FilterChain中的下一个Filter。 - -## Listener -监听器,Listener是基于状态的,任何行为改变同一个状态,触发的事件是一致的。当Web应用在Servlet容器中运行时,Servlet容器内部会不断的发生各种事件,如Web应用的启动和停止、用户请求到达等。 Servlet容器提供了一些默认的监听器来监听这些事件,当事件发生时,Servlet容器会负责调用监听器的方法。当然,你可以定义自己的监听器去监听你感兴趣的事件,将监听器配置在web.xml中。比如Spring就实现了自己的监听器,来监听ServletContext的启动事件,目的是当Servlet容器启动时,创建并初始化全局的Spring容器。 - -# X FAQ -**service方法为什么把request和response都当作输入参数,而不是输入参数只有request,response放到返回值里呢?** -方便责任链模式下层层传递。 - -在SpringBoot项目中,为什么没有web.xml了? -SpringBoot是以嵌入式的方式来启动Tomcat。对于SpringBoot来说,Tomcat只是个JAR包。SpringBoot通过Servlet3.0规范中 **@WebServlet** 注解或者API直接向Servlet容器添加Servlet,无需web.xml。 - -## 分不清的xxx容器 -### Servlet容器 -用于管理Servlet生命周期。 -### SpringMVC容器 -管理SpringMVC Bean生命周期。 -### Spring容器 -用于管理Spring Bean生命周期。包含许多子容器,其中SpringMVC容器就是其中常用的,DispatcherServlet就是SpringMVC容器中的servlet接口,也是SpringMVC容器的核心类。 - -Spring容器主要用于整个Web应用程序需要共享的一些组件,比如DAO、数据库的ConnectionFactory等,SpringMVC容器主要用于和该Servlet相关的一些组件,比如Controller、ViewResovler等。 -至此就清楚了Spring容器内部的关系。 - -**Spring和SpringMVC分别有自己的IOC容器或上下文,为何分成俩容器?** -隔离管理的Bean,各管各的,职责明确。SpringMVC的容器直接管理跟DispatcherServlet相关的Bean,也就是Controller,ViewResolver等,并且SpringMVC容器是在DispacherServlet的init方法里创建的。而Spring容器管理其他的Bean比如Service和DAO。 - -并且SpringMVC容器是Spring容器的子容器,所谓的父子关系意味着什么呢,就是你通过子容器去拿某个Bean时,子容器先在自己管理的Bean中去找这个Bean,如果找不到再到父容器中找。但是父容器不能到子容器中去找某个Bean。 - -其实这个套路跟JVM的类加载器设计有点像,不同的类加载器也为了隔离,不过加载顺序是反的,子加载器总是先委托父加载器去加载某个类,加载不到再自己来加载。 - -并且通过父子关系,使得SpringMVC容器可以从父亲Spring容器那里拿Bean,因为Spring容器管理的是公共的Bean。 -当然可以用同一个容器来管理,SpringBoot就是这样做的。 - -Spring和SpringMVC是通过配置文件来明确指定各自管理的Bean。 - -**Servlet容器跟Spring容器又有什么关系呢?** -有人说spring容器是servlet容器的子容器,但是这个servlet容器到底是tomcat实现的容器呢,还是jetty实现的容器呢?所以spring容器与servlet容器他们之间并没有直接的血缘关系,可以说spring容器依赖了servlet容器,spring容器的实现遵循了Servlet 规范。 - -spring容器只是servlet容器上下文(ServletContext)的一个属性,web容器启动时通过ServletContextListener机制构建出来。SpringBoot中只有一个Spring上下文: - -```bash -org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext -``` - -![](https://img-blog.csdnimg.cn/20210716201457871.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70)servlet容器初始化成功后被spring监听,创建spring容器放入servlet容器中,访问到达,初始化dispatcher servlet时创建springmvc容器,通过servletContext拿到spring容器,并将其作为自己的父容器,spring mvc容器会定义controller相关的bean,spring会定义业务逻辑相关的bean。 - - -> 参考 -> - https://blog.csdn.net/zhanglf02/article/details/89791797 -> - https://docs.oracle.com/cd/E19146-01/819-2634/abxbh/index.html -> - https://matthung0807.blogspot.com/2020/09/tomcat-how-servlet-construct.html -> - https://github.com/heroku/devcenter-embedded-tomcat -> - https://biang.io/blog/mixed/cgi \ No newline at end of file diff --git "a/Tomcat/Spring Boot\345\246\202\344\275\225\345\220\257\345\212\250\345\265\214\345\205\245\345\274\217Tomcat\357\274\237.md" "b/Tomcat/Spring Boot\345\246\202\344\275\225\345\220\257\345\212\250\345\265\214\345\205\245\345\274\217Tomcat\357\274\237.md" deleted file mode 100644 index 2d5fd6dc79..0000000000 --- "a/Tomcat/Spring Boot\345\246\202\344\275\225\345\220\257\345\212\250\345\265\214\345\205\245\345\274\217Tomcat\357\274\237.md" +++ /dev/null @@ -1,207 +0,0 @@ -Spring Boot在内部启动了一个嵌入式Web容器。 -Tomcat是组件化设计,所以就是启动这些组件。 - -> Tomcat独立部署模式是通过startup脚本启动,Tomcat中的Bootstrap和Catalina会负责初始化类加载器,并解析server.xml和启动这些组件。 - -内嵌模式,Bootstrap和Catalina的工作由Spring Boot代劳,Spring Boot调用Tomcat API启动这些组件。 -# Spring Boot中Web容器相关接口 -### WebServer -为支持各种Web容器,Spring Boot抽象出嵌入式Web容过滤器器,定义WebServer接口: -![](https://img-blog.csdnimg.cn/b42b8ef2454f4977a53d6a1a36399ebb.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -Web容器比如Tomcat、Jetty去实现该接口 -![](https://img-blog.csdnimg.cn/7de9b2055c9d48a1b998ae5ba9109d1e.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -### ServletWebServerFactory -创建Web容器,返回的就是上面提到的WebServer。 -```java -public interface ServletWebServerFactory { - WebServer getWebServer(ServletContextInitializer... initializers); -} -``` -ServletContextInitializer入参表示ServletContext的初始化器,用于ServletContext中的一些配置: -```java -public interface ServletContextInitializer { - void onStartup(ServletContext servletContext) throws ServletException; -} -``` - -getWebServer会调用ServletContextInitializer#onStartup,即若想在Servlet容器启动时做一些事情,比如注册自己的Servlet,可以实现一个ServletContextInitializer,在Web容器启动时,Spring Boot会把所有实现ServletContextInitializer接口的类收集起来,统一调其onStartup。 -### WebServerFactoryCustomizerBeanPostProcessor -一个BeanPostProcessor,为定制化嵌入式Web容器,在postProcessBeforeInitialization过程中去寻找Spring容器中WebServerFactoryCustomizer类型的Bean,并依次调用WebServerFactoryCustomizer接口的customize方法做一些定制化。 -```java -public interface WebServerFactoryCustomizer { - void customize(T factory); -} -``` -# 创建、启动嵌入式Web容器 -Spring的ApplicationContext,其抽象实现类AbstractApplicationContext#refresh -![](https://img-blog.csdnimg.cn/8305733360bd48069e8306f6d492e664.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -用来新建或刷新一个ApplicationContext,在refresh中会调用onRefresh,AbstractApplicationContext的子类可以重写onRefresh实现Context刷新逻辑。 - -因此重写 **ServletWebServerApplicationContext#onRefresh** 创建嵌入式Web容器: -![](https://img-blog.csdnimg.cn/54541ff6f69047f5ba05da0f6811e0e4.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -重写onRefresh方法,调用createWebServer创建和启动Tomcat。 -### createWebServer -```java -private void createWebServer() { - // WebServer是Spring Boot抽象出来的接口,具体实现类就是不同Web容器 - WebServer webServer = this.webServer; - ServletContext servletContext = this.getServletContext(); - - // 若Web容器尚未创建 - if (webServer == null && servletContext == null) { - // 通过Web容器工厂创建 - ServletWebServerFactory factory = this.getWebServerFactory(); - // 传入一个"SelfInitializer" - this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()}); - - } else if (servletContext != null) { - try { - this.getSelfInitializer().onStartup(servletContext); - } catch (ServletException var4) { - ... - } - } - - this.initPropertySources(); -} -``` - -### getWebServer -以Tomcat为例,主要调用Tomcat的API去创建各种组件: -```java -public WebServer getWebServer(ServletContextInitializer... initializers) { - // 1.实例化一个Tomcat【Server组件】 - Tomcat tomcat = new Tomcat(); - - // 2. 创建一个临时目录 - File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat"); - tomcat.setBaseDir(baseDir.getAbsolutePath()); - - // 3.初始化各种组件 - Connector connector = new Connector(this.protocol); - tomcat.getService().addConnector(connector); - this.customizeConnector(connector); - tomcat.setConnector(connector); - tomcat.getHost().setAutoDeploy(false); - this.configureEngine(tomcat.getEngine()); - - // 4. 创建定制版的"Context"组件 - this.prepareContext(tomcat.getHost(), initializers); - return this.getTomcatWebServer(tomcat); -} -``` -prepareContext的Context指Tomcat的Context组件,为控制Context组件行为,Spring Boot自定义了TomcatEmbeddedContext类,继承Tomcat的StandardContext: -![](https://img-blog.csdnimg.cn/29ea42ba2130426f8a4d3d96dbdbf03d.png) -# 注册Servlet -- 有@RestController,为什么还要自己去注册Servlet给Tomcat? -可能有些场景需要注册你自己写的一个Servlet提供辅助功能,与主程序分开。 - -- Sprong Boot 不注册Servlet 给Tomcat 直接用 **@Controller** 就能实现Servlet功能是为啥呢? -因为Sprong Boot默认给我们注册了DispatcherSetvlet。 - -## Servlet注解 -在Spring Boot启动类上加上 **@ServletComponentScan** 注解后,使用@WebServlet、@WebFilter、@WebListener标记的Servlet、Filter、Listener就可以自动注册到Servlet容器。 -![](https://img-blog.csdnimg.cn/f30ba0ce2d2242ab972a8fb937fcf0eb.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/9366a2ca7df24abdb7d68987ed91e8cd.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -在Web应用的入口类上加上@ServletComponentScan,并且在Servlet类上加上@WebServlet,这样Spring Boot会负责将Servlet注册到内嵌的Tomcat中。 - -## ServletRegistrationBean -Spring Boot提供了 -- ServletRegistrationBean -- FilterRegistrationBean -- ServletListenerRegistrationBean - -分别用来注册Servlet、Filter、Listener。 -假如要注册一个Servlet: -![](https://img-blog.csdnimg.cn/61a11f64e3b74f47af071b9661b0e425.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -返回一个ServletRegistrationBean,并将它当作Bean注册到Spring,因此你需要把这段代码放到Spring Boot自动扫描的目录中,或者放到**@Configuration**标识的类中。 -Spring会把这种类型的Bean收集起来,根据Bean里的定义向Tomcat注册Servlet。 - -## 动态注册 -可以创建一个类去实现ServletContextInitializer接口,并把它注册为一个Bean,Spring Boot会负责调用这个接口的onStartup。 - -> 实现ServletContextInitializer接口的类会被spring管理,而不是被Servlet容器管理。 - -```java -@Component -public class MyServletRegister implements ServletContextInitializer { - - @Override - public void onStartup(ServletContext servletContext) { - - // Servlet 3.0规范新的API - ServletRegistration myServlet = servletContext - .addServlet("HelloServlet", HelloServlet.class); - - myServlet.addMapping("/hello"); - - myServlet.setInitParameter("name", "Hello Servlet"); - } - -} -``` -ServletRegistrationBean也是通过ServletContextInitializer实现的,它实现了ServletContextInitializer接口。 -注意到onStartup方法的参数是我们熟悉的ServletContext,可以通过调用它的addServlet方法来动态注册新的Servlet,这是Servlet 3.0以后才有的功能。 - -通过 ServletContextInitializer 接口可以向 Web 容器注册 Servlet,实现 ServletContextInitializer 接口的Bean被speing管理,但是在什么时机触发其onStartup()方法的呢? -通过 Tomcat 中的 ServletContainerInitializer 接口实现者,如TomcatStarter,创建tomcat时设置了该类,在tomcat启动时会触发ServletContainerInitializer实现者的onStartup()方法,在这个方法中触发ServletContextInitializer接口的onStartup()方法,如注册DispatcherServlet。 - -DispatcherServletRegistrationBean实现了ServletContextInitializer接口,它的作用就是向Tomcat注册DispatcherServlet,那它是在什么时候、如何被使用的呢? -prepareContext方法调用了另一个私有方法configureContext,这个方法就包括了往Tomcat的Context添加ServletContainerInitializer对象: -```java -context.addServletContainerInitializer(starter, NO_CLASSES); -``` -其中有DispatcherServletRegistrationBean。 - -## 定制Web容器 -如何在Spring Boot中定制Web容器。在Spring Boot 2.0中可通过如下方式: -### ConfigurableServletWebServerFactory -通用的Web容器工厂,定制Web容器通用参数: -```java -@Component -public class MyGeneralCustomizer implements - WebServerFactoryCustomizer { - - public void customize(ConfigurableServletWebServerFactory factory) { - factory.setPort(8081); - factory.setContextPath("/hello"); - } -} -``` -### TomcatServletWebServerFactory -通过特定Web容器工厂进一步定制。 - -给Tomcat增加一个Valve,这个Valve的功能是向请求头里添加traceid,用于分布式追踪。 -```java -class TraceValve extends ValveBase { - @Override - public void invoke(Request request, Response response) throws IOException, ServletException { - - request.getCoyoteRequest().getMimeHeaders(). - addValue("traceid").setString("1234xxxxabcd"); - - Valve next = getNext(); - if (null == next) { - return; - } - - next.invoke(request, response); - } - -} -``` -跟方式一类似,再添加一个定制器: -```java -@Component -public class MyTomcatCustomizer implements - WebServerFactoryCustomizer { - - @Override - public void customize(TomcatServletWebServerFactory factory) { - factory.setPort(8081); - factory.setContextPath("/hello"); - factory.addEngineValves(new TraceValve() ); - - } -} -``` \ No newline at end of file diff --git "a/Tomcat/Tomcat\343\200\201Nginx\345\222\214Apache\347\232\204\345\214\272\345\210\253.md" "b/Tomcat/Tomcat\343\200\201Nginx\345\222\214Apache\347\232\204\345\214\272\345\210\253.md" deleted file mode 100644 index 077d829508..0000000000 --- "a/Tomcat/Tomcat\343\200\201Nginx\345\222\214Apache\347\232\204\345\214\272\345\210\253.md" +++ /dev/null @@ -1,35 +0,0 @@ -- 这三者都是web server,那他们各自有什么特点呢? -- 他们之间的区别是什么呢? -- nginx 和 tomcat在性能上面有何异同? -- tomcat用在java后台程序上,java后台程序难道不能用apache和nginx吗? - -Apache HTTP Server Project、Nginx都是开源的HTTP服务器软件。 - -HTTP服务器本质上也是一种应用程序——它通常运行在服务器之上,绑定服务器的IP地址并监听某一个TCP端口来接收并处理HTTP请求,这样客户端(如Firefox,Chrome这样的浏览器)就能通过HTTP协议获取服务器上的网页(HTML格式)、文档(PDF格式)、音频(MP4格式)、视频(MOV格式)等资源。 - -下图描述的就是这一过程: -![](https://img-blog.csdnimg.cn/20210628155422614.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -不仅仅是Apache HTTP Server和Nginx,编程语言比如 Java的类库中也实现了简单的HTTP服务器方便开发者使用: -- HttpServer (Java HTTP Server ) - -使用这些类库能够非常容易的运行一个HTTP服务器,它们都能够通过绑定IP地址并监听tcp端口来提供HTTP服务。 - -Apache Tomcat与Apache HTTP Server相比,Tomcat能够动态生成资源并返回到客户端。Apache HTTP Server和Nginx都能够将某一文本文件内容通过HTTP协议返回到客户端,但该文本文件的内容固定——即无论何时、任何人访问它得到的内容都完全相同,这就是`静态资源`。 - -动态资源则在不同时间、客户端访问得到的内容不同,例如: -- 包含显示当前时间的页面 -- 显示当前IP地址的页面 - -Apache HTTP Server和Nginx本身不支持生成动态页面,但它们可以通过其他模块来支持(例如通过Shell、PHP、Python脚本程序来动态生成内容)。若想要使用Java程序动态生成资源内容,使用这一类HTTP服务器很难做到。Java Servlet以及衍生的JSP可以让Java程序也具有处理HTTP请求并且返回内容(由程序动态控制)的能力,Tomcat正是支持运行Servlet/JSP应用程序的容器(Container): -![](https://img-blog.csdnimg.cn/2021062816011444.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -Tomcat运行在JVM之上,和HTTP服务器一样,绑定IP地址并监听TCP端口,同时还包含如下职责: -- 管理Servlet程序的生命周期 -- 将URL映射到指定Servlet进行处理 -- 与Servlet程序合作处理HTTP请求 -根据HTTP请求生成HttpServletRequest对象并传递给Servlet进行处理,将Servlet中的HttpServletResponse对象生成的内容返回给浏览器 - -虽然Tomcat也可以认为是HTTP服务器,但通常它仍然会和Nginx配合在一起使用: -- 动静态资源分离 -运用Nginx的反向代理功能分发请求:所有动态资源的请求交给Tomcat,而静态资源的请求(例如图片、视频、CSS、JavaScript文件等)则直接由Nginx返回到浏览器,大大减轻Tomcat压力 -- 负载均衡 -当业务压力增大时,可能一个Tomcat的实例不足以处理,那么这时可以启动多个Tomcat实例进行水平扩展,而Nginx的负载均衡功能可以把请求通过算法分发到各个不同的实例进行处理 \ No newline at end of file diff --git "a/Tomcat/Tomcat\345\244\232\345\261\202\345\256\271\345\231\250\347\232\204\350\256\276\350\256\241.md" "b/Tomcat/Tomcat\345\244\232\345\261\202\345\256\271\345\231\250\347\232\204\350\256\276\350\256\241.md" deleted file mode 100644 index e75660de59..0000000000 --- "a/Tomcat/Tomcat\345\244\232\345\261\202\345\256\271\345\231\250\347\232\204\350\256\276\350\256\241.md" +++ /dev/null @@ -1,114 +0,0 @@ -Tomcat的容器用来装载Servlet。那Tomcat的Servlet容器是如何设计的呢? - -# 容器的层次结构 -Tomcat设计了4种容器:Engine、Host、Context和Wrapper -![](https://img-blog.csdnimg.cn/2021071923583247.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -Tomcat通过这种分层,使得Servlet容器具有很好的灵活性。 -- Context表示一个Web应用程序 -- Wrapper表示一个Servlet,一个Web应用程序中可能会有多个Servlet -- Host代表一个虚拟主机,或一个站点,可以给Tomcat配置多个虚拟主机地址,而一个虚拟主机下可以部署多个Web应用程序 -- Engine表示引擎,用来管理多个虚拟站点,一个Service最多只能有一个Engine - -观察Tomcat的server.xml配置文件。Tomcat采用了组件化设计,最外层即是Server -![](https://img-blog.csdnimg.cn/20210719235935386.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -这些容器具有父子关系,形成一个树形结构,Tomcat用组合模式来管理这些容器。 - -所有容器组件都实现Container接口,因此组合模式可以使得用户对 -- 单容器对象 -最底层的Wrapper -- 组合容器对象 -上面的Context、Host或者Engine -的使用具有一致性。 - -Container接口定义: - -```java -public interface Container extends Lifecycle { - public void setName(String name); - public Container getParent(); - public void setParent(Container container); - public void addChild(Container child); - public void removeChild(Container child); - public Container findChild(String name); -} -``` - -# 请求定位Servlet的过程 -搞这么多层次的容器,Tomcat是怎么确定请求是由哪个Wrapper容器里的Servlet来处理的呢? -Tomcat用Mapper组件完成这个任务。 - -Mapper就是将用户请求的URL定位到一个Servlet -## 工作原理 -Mapper组件保存了Web应用的配置信息:容器组件与访问路径的映射关系,比如 -- Host容器里配置的域名 -- Context容器里的Web应用路径 -- Wrapper容器里Servlet映射的路径 - -这些配置信息就是一个多层次的Map。 - -当一个请求到来时,Mapper组件通过解析请求URL里的域名和路径,再到自己保存的Map里去查找,就能定位到一个Servlet。 -一个请求URL最后只会定位到一个Wrapper容器,即一个Servlet。 - -假如有一网购系统,有 -- 面向B端管理人员的后台管理系统 -- 面向C端用户的在线购物系统 - -这俩系统跑在同一Tomcat,为隔离它们的访问域名,配置两个虚拟域名: -- manage.shopping.com -管理人员通过该域名访问Tomcat去管理用户和商品,而用户管理和商品管理是两个单独的Web应用 -- user.shopping.com -C端用户通过该域名去搜索商品和下订单,搜索功能和订单管理也是两个独立Web应用 - -这样部署,Tomcat会创建一个Service组件和一个Engine容器组件,在Engine容器下创建两个Host子容器,在每个Host容器下创建两个Context子容器。由于一个Web应用通常有多个Servlet,Tomcat还会在每个Context容器里创建多个Wrapper子容器。每个容器都有对应访问路径 -![](https://img-blog.csdnimg.cn/20210720111550336.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -Tomcat如何将URL定位到一个Servlet呢? -- 首先,根据协议和端口号选定Service和Engine -Tomcat的每个连接器都监听不同的端口,比如Tomcat默认的HTTP连接器监听8080端口、默认的AJP连接器监听8009端口。该URL访问8080端口,因此会被HTTP连接器接收,而一个连接器是属于一个Service组件的,这样Service组件就确定了。一个Service组件里除了有多个连接器,还有一个Engine容器,因此Service确定了,Engine也确定了。 -- 根据域名选定Host。 -Mapper组件通过URL中的域名去查找相应的Host容器,比如user.shopping.com,因此Mapper找到Host2容器。 -- 根据URL路径找到Context组件 -Host确定以后,Mapper根据URL的路径来匹配相应的Web应用的路径,比如例子中访问的是/order,因此找到了Context4这个Context容器。 -- 最后,根据URL路径找到Wrapper(Servlet) -Context确定后,Mapper再根据web.xml中配置的Servlet映射路径来找到具体Wrapper和Servlet。 - -并非只有Servlet才会去处理请求,查找路径上的父子容器都会对请求做一些处理: -- 连接器中的Adapter会调用容器的Service方法执行Servlet -- 最先拿到请求的是Engine容器,Engine容器对请求做一些处理后,会把请求传给自己子容器Host继续处理,依次类推 -- 最后这个请求会传给Wrapper容器,Wrapper会调用最终的Servlet来处理 - -这个调用过程使用的Pipeline-Valve管道,责任链模式,在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。 - -Valve表示一个处理点,比如权限认证和记录日志。 -```java -public interface Valve { - public Valve getNext(); - public void setNext(Valve valve); - public void invoke(Request request, Response response) -} -``` -由于Valve是一个处理点,因此invoke方法就是来处理请求的。 -Pipeline接口: -```java -public interface Pipeline extends Contained { - public void addValve(Valve valve); - public Valve getBasic(); - public void setBasic(Valve valve); - public Valve getFirst(); -} -``` -所以Pipeline中维护了Valve链表,Valve可插入到Pipeline。 -Pipeline中没有invoke方法,因为整个调用链的触发是Valve完成自己的处理后,调用getNext.invoke调用下一个Valve。 - -每个容器都有一个Pipeline对象,只要触发这个Pipeline的第一个Valve,这个容器里Pipeline中的Valve就都会被调用到。但不同容器的Pipeline如何链式触发? -比如Engine中Pipeline需要调用下层容器Host中的Pipeline。 -Pipeline有个getBasic方法。这个BasicValve处于Valve链尾,负责调用下层容器的Pipeline里的第一个Valve。 -![](https://img-blog.csdnimg.cn/20210720135942222.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -整个调用过程由连接器中的Adapter触发的,它会调用Engine的第一个Valve: -![](https://img-blog.csdnimg.cn/20210720141234571.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70)Wrapper容器的最后一个Valve会创建一个Filter链,并调用doFilter方法,最终会调到Servlet的service方法。 - -Valve和Filter有什么区别呢? -- Valve是Tomcat的私有机制,与Tomcat紧耦合。Servlet API是公有标准,所有Web容器包括Jetty都支持Filter -- Valve工作在Web容器级别,拦截所有应用的请求。Servlet Filter工作在应用级别,只拦截某个Web应用的所有请求。若想做整个Web容器的拦截器,必须使用Valve。 \ No newline at end of file diff --git "a/Tomcat/Tomcat\345\246\202\344\275\225\346\211\223\347\240\264\345\217\214\344\272\262\345\247\224\346\264\276\346\234\272\345\210\266\345\256\236\347\216\260\351\232\224\347\246\273Web\345\272\224\347\224\250\347\232\204\357\274\237.md" "b/Tomcat/Tomcat\345\246\202\344\275\225\346\211\223\347\240\264\345\217\214\344\272\262\345\247\224\346\264\276\346\234\272\345\210\266\345\256\236\347\216\260\351\232\224\347\246\273Web\345\272\224\347\224\250\347\232\204\357\274\237.md" deleted file mode 100644 index ccb7deb634..0000000000 --- "a/Tomcat/Tomcat\345\246\202\344\275\225\346\211\223\347\240\264\345\217\214\344\272\262\345\247\224\346\264\276\346\234\272\345\210\266\345\256\236\347\216\260\351\232\224\347\246\273Web\345\272\224\347\224\250\347\232\204\357\274\237.md" +++ /dev/null @@ -1,100 +0,0 @@ -Tomcat通过自定义类加载器WebAppClassLoader打破双亲委派,即重写了JVM的类加载器ClassLoader的findClass方法和loadClass方法,以优先加载Web应用目录下的类。 - -Tomcat负责加载我们的Servlet类、加载Servlet所依赖的JAR包。Tomcat本身也是个Java程序,因此它需要加载自己的类和依赖的JAR包。 - -若在Tomcat运行两个Web应用程序,它们有功能不同的同名Servlet,Tomcat需同时加载和管理这两个同名的Servlet类,保证它们不会冲突。所以Web应用之间的类需要隔离 - -若两个Web应用都依赖同一三方jar,比如Spring,则Spring jar被加载到内存后,Tomcat要保证这两个Web应用能共享之,即Spring jar只被加载一次,否则随着三方jar增多,JVM的内存会占用过大。 -所以,和 JVM 一样,需要隔离Tomcat本身的类和Web应用的类。 -# Tomcat类加载器的层次结构 -- Tomcat的类加载器层次结构 -![](https://img-blog.csdnimg.cn/06ef794a266541b89089131a8c51d8f6.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -- 前三个是加载器实例名,不是类名。![](https://img-blog.csdnimg.cn/f59d56811c574170a2969f620d4be934.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -## WebAppClassLoader -若使用JVM默认的AppClassLoader加载Web应用,AppClassLoader只能加载一个Servlet类,在加载第二个同名Servlet类时,AppClassLoader会返回第一个Servlet类的Class实例。 -因为在AppClassLoader眼里,同名Servlet类只能被加载一次。 - -于是,Tomcat自定义了一个类加载器**WebAppClassLoader**, 并为每个Web应用创建一个**WebAppClassLoader**实例。 - -每个Web应用自己的Java类和依赖的JAR包,分别放在`WEB-INF/classes`和`WEB-INF/lib`目录下,都是WebAppClassLoader加载的。 - -Context容器组件对应一个Web应用,因此,每个Context容器创建和维护一个WebAppClassLoader加载器实例。 -不同加载器实例加载的类被认为是不同的类,即使类名相同。这就相当于在JVM内部创建相互隔离的Java类空间,每个Web应用都有自己的类空间,Web应用之间通过各自的类加载器互相隔离。 - -## SharedClassLoader -两个Web应用之间怎么共享库类,并且不能重复加载相同的类? - -双亲委派机制的各子加载器都能通过父加载器去加载类,于是考虑把需共享的类放到父加载器的加载路径。 - -应用程序即是通过该方式共享JRE核心类。 -Tomcat搞了个类加载器SharedClassLoader,作为WebAppClassLoader的父加载器,以加载Web应用之间共享的类。 - -若WebAppClassLoader未加载到某类,就委托父加载器SharedClassLoader去加载该类,SharedClassLoader会在指定目录下加载共享类,之后返回给WebAppClassLoader,即可解决共享问题。 - -## CatalinaClassLoader -如何隔离Tomcat本身的类和Web应用的类? - -兄弟关系:两个类加载器是平行的,它们可能拥有同一父加载器,但两个兄弟类加载器加载的类是隔离的。 - -于是,Tomcat搞了CatalinaClassLoader,专门加载Tomcat自身的类。 - -问题是,当Tomcat和各Web应用之间需要共享一些类时该怎么办? - -## CommonClassLoader -共享依旧靠父子关系。 -再增加个CommonClassLoader,作为CatalinaClassLoader和SharedClassLoader的父加载器。 - -CommonClassLoader能加载的类都可被CatalinaClassLoader、SharedClassLoader 使用,而CatalinaClassLoader和SharedClassLoader能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。 - -# Spring的加载问题 -JVM默认情况下,若一个类由类加载器A加载,则该类的依赖类也由相同的类加载器加载。 -比如Spring作为一个Bean工厂,它需要创建业务类的实例,并且在创建业务类实例之前需要加载这些类。Spring是通过调用Class.forName来加载业务类的,我们来看一下forName的源码: -```java -public static Class forName(String className) { - Class caller = Reflection.getCallerClass(); - return forName0(className, true, ClassLoader.getClassLoader(caller), caller); -} -``` -会使用调用者,即Spring的加载器去加载业务类。 - -Web应用之间共享的jar可交给SharedClassLoader加载,以避免重复加载。Spring作为共享的三方jar,本身由SharedClassLoader加载,Spring又要去加载业务类,按照前面那条规则,加载Spring的类加载器也会用来加载业务类,但是业务类在Web应用目录下,不在SharedClassLoader的加载路径下,这该怎么办呢? -## 线程上下文加载器 -于是有了线程上下文加载器,一种类加载器传递机制。因为该类加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中就能把这个类加载器取出来用。因此Tomcat为每个Web应用创建一个WebAppClassLoader类加载器,并在启动Web应用的线程里设置线程上下文加载器,这样Spring在启动时就将线程上下文加载器取出来,用来加载Bean。Spring取线程上下文加载的代码如下: -```java -cl = Thread.currentThread().getContextClassLoader(); -``` - -在StandardContext的启动方法,会将当前线程的上下文加载器设置为WebAppClassLoader。 -![](https://img-blog.csdnimg.cn/fc59c5e9cfd6473183c67cf7bd91329a.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -启动方法结束时,会恢复线程的上下文加载器: -```java -Thread.currentThread().setContextClassLoader(originalClassLoader); -``` - -> 这是为什么呢? - -线程上下文加载器其实是线程的一个私有数据,跟线程绑定,这个线程完成启动Context组件后,会被回收到线程池,之后被用来做其他事情,为了不影响其他事情,需恢复之前的线程上下文加载器。 -优先加载web应用的类,当加载完了再改回原来的。 - -线程上下文的加载器就是指定子类加载器来加载具体的某个桥接类,比如JDBC的Driver的加载。 -# 总结 -Tomcat的Context组件为每个Web应用创建一个WebAppClassLoader类加载器,由于不同类加载器实例加载的类是互相隔离的,因此达到了隔离Web应用的目的,同时通过CommonClassLoader等父加载器来共享第三方JAR包。而共享的第三方JAR包怎么加载特定Web应用的类呢?可以通过设置线程上下文加载器来解决。 - -多个应用共享的Java类文件和JAR包,分别放在Web容器指定的共享目录: -- CommonClassLoader -对应 `/common/*` -- CatalinaClassLoader -对应 `/server/*` -- SharedClassLoader -对应 `/shared/*` -- WebAppClassloader -对应 `/webapps//WEB-INF/*` - -可以在Tomcat conf目录下的Catalina.properties文件里配置各种类加载器的加载路径。 - - -当出现ClassNotFound错误时,应该检查你的类加载器是否正确。 -线程上下文加载器不仅仅可以用在Tomcat和Spring类加载的场景里,核心框架类需要加载具体实现类时都可以用到它,比如我们熟悉的JDBC就是通过上下文类加载器来加载不同的数据库驱动的。 \ No newline at end of file diff --git "a/Tomcat/Tomcat\345\257\271Servlet\350\247\204\350\214\203\347\232\204Filter\345\217\212Listener\345\256\236\347\216\260.md" "b/Tomcat/Tomcat\345\257\271Servlet\350\247\204\350\214\203\347\232\204Filter\345\217\212Listener\345\256\236\347\216\260.md" deleted file mode 100644 index e44d50f004..0000000000 --- "a/Tomcat/Tomcat\345\257\271Servlet\350\247\204\350\214\203\347\232\204Filter\345\217\212Listener\345\256\236\347\216\260.md" +++ /dev/null @@ -1,171 +0,0 @@ -加载Servlet的类不等于创建Servlet实例,Tomcat先加载Servlet的类,然后还得在Java堆创建Servlet实例。 - -一个Web应用里往往有多个Servlet,而在Tomcat中一个Web应用对应一个Context容器,即一个Context容器需管理多个Servlet实例。 -但Context容器并不直接持有Servlet实例,而是通过子容器Wrapper管理Servlet,可以把Wrapper容器看作Servlet的包装。 - -> 为何需要Wrapper?Context容器直接维护一个Servlet数组还不满足? - -Servlet不仅是个类实例,还有相关配置信息,比如URL映射、初始化参数,因此设计出了一个包装器,把Servlet本身和它相关的数据包起来。 - -> 管理好Servlet就够了吗? - -Listener和Filter也是Servlet规范,Tomcat也要创建它们的实例,在合适时机调用它们的方法。 -# Servlet管理 -Tomcat用Wrapper容器管理Servlet -```java -protected volatile Servlet instance = null; -``` -它拥有一个Servlet实例,Wrapper#loadServlet实例化Servlet: -```java -public synchronized Servlet loadServlet() throws ServletException { - Servlet servlet; - - // 1. 创建Servlet实例 - servlet = (Servlet) instanceManager.newInstance(servletClass); - - // 2.调用了Servlet#init【Servlet规范要求】 - initServlet(servlet); - - return servlet; -} -``` - -为加快系统启动速度,一般采取资源延迟加载,所以Tomcat默认情况下Tomcat在启动时不会加载你的Servlet,除非把Servlet loadOnStartup参数置true。 - -虽然Tomcat在启动时不会创建Servlet实例,但会创建Wrapper容器。当有请求访问某Servlet,才会创建该Servlet实例。 - -Servlet是被谁调用呢? -Wrapper作为一个容器组件,有自己的Pipeline和BasicValve,其BasicValve为StandardWrapperValve。 - -当请求到来,Context容器的BasicValve会调用Wrapper容器中Pipeline中的第一个Valve,然后调用到StandardWrapperValve: -```java -public final void invoke(Request request, Response response) { - - // 1.实例化Servlet - servlet = wrapper.allocate(); - - // 2.给当前请求创建一个Filter链 - ApplicationFilterChain filterChain = - ApplicationFilterFactory.createFilterChain(request, wrapper, servlet); - - // 3. 调用这个Filter链,Filter链中的最后一个Filter会调用Servlet - filterChain.doFilter(request.getRequest(), response.getResponse()); - -} -``` - -StandardWrapperValve的invoke就三步: -1. 创建Servlet实例 -2. 给当前请求创建一个Filter链 -3. 调用Filter链 - -### 为何要给每个请求创建Filter链 -每个请求的请求路径不同,而Filter都有相应路径映射,因此不是所有Filter都需要处理当前请求,要根据请求路径选择特定的一些Filter。 - -### 为何没调用Servlet#service -Filter链的最后一个Filter会负责调用Servlet。 - -# Filter管理 -跟Servlet一样,Filter也可在`web.xml`配置。 -但Filter的作用域是整个Web应用,因此Filter的实例维护在Context容器:Map里存的是filterDef(filter定义),而非filter类实例 -![](https://img-blog.csdnimg.cn/a3042975203c43a1bb10f19d23110602.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -Filter链存活期很短,它跟每个请求对应。一个新请求来了,就动态创建一个Filter链,请求处理完,Filter链就被回收。 -```java -public final class ApplicationFilterChain implements FilterChain { - - // Filter链的Filter数组 - private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0]; - - // Filter链的当前的调用位置 - private int pos = 0; - - // Filter总数 - private int n = 0; - - // 每个Filter链最终要调用的Servlet - private Servlet servlet = null; - - public void doFilter(ServletRequest req, ServletResponse res) { - internalDoFilter(request,response); - } - - private void internalDoFilter(ServletRequest req, - ServletResponse res){ - - if (pos < n) { - ApplicationFilterConfig filterConfig = filters[pos++]; - Filter filter = filterConfig.getFilter(); - - filter.doFilter(request, response, this); - return; - } - - servlet.service(request, response); - -} -``` -internalDoFilter里会做个判断: -- 若当前Filter位置 < Filter数组长度,即Filter还没调完,就从Filter数组取下一个Filter,调用其doFilter -- 否则,说明已调用完所有Filter,该调用Servlet#service了。service方法是留给程序员实现业务逻辑的,比如CRUD -```java -public void doFilter(ServletRequest request, ServletResponse response, - FilterChain chain){ - - ... - - //调用Filter的方法 - chain.doFilter(request, response); - - } -``` -Filter#doFilter的FilterChain参数,就是Filter链。每个Filter#doFilter里必须调用Filter链的doFilter,而Filter链中保存当前Filter位置,会调用下一个Filter的doFilter方法,这样就能完成链式调用。 - -### 对应的filter是怎么注册到Servlet的呢? -filter是注册到Servlet容器中,Tomcat的StandardContext类中维护了一个Filter列表,所谓注册就是把你写的filter类实例加到这个列表。 - -# Listener管理 -Listener可以监听容器内部发生的事件: -- 生命状态的变化 -比如Context容器启动和停止、Session的创建和销毁。 -- 属性变化 -比如Context容器某个属性值变了、Session的某个属性值变了以及新的请求来了 - -## 怎么添加监听器 -在web.xml配置或注解添加,在监听器里实现业务逻辑。 -Tomcat需读取配置文件,拿到监听器的类名,将它们实例化,并适时调用这些监听器方法。 - -Tomcat是通过Context容器来管理这些监听器的。Context容器将两类事件分开来管理,分别用不同的集合来存放不同类型事件的监听器: -```java -//监听属性值变化的监听器 -private List applicationEventListenersList = new CopyOnWriteArrayList<>(); - -//监听生命事件的监听器 -private Object applicationLifecycleListenersObjects[] = new Object[0]; -剩下的事情就是触发监听器了,比如在Context容器的启动方法里,就触发了所有的ServletContextListener: - -// 1 拿到所有生命周期监听器 -Object instances[] = getApplicationLifecycleListeners(); - -for (int i = 0; i < instances.length; i++) { - // 2 判断Listener的类型是否为ServletContextListener - if (!(instances[i] instanceof ServletContextListener)) - continue; - - // 3 触发Listener方法 - ServletContextListener lr = (ServletContextListener) instances[i]; - lr.contextInitialized(event); -} -``` -这里的ServletContextListener接口是留给用户的扩展机制,用户可以实现该接口定义自己的监听器,监听Context容器的启停事件。 -ServletContextListener跟Tomcat自己的生命周期事件LifecycleListener是不同的。LifecycleListener定义在生命周期管理组件中,由基类LifecycleBase统一管理。 - -可定制监听器监听Tomcat内部发生的各种事件:比如Web应用、Session级别或请求级别的。Tomcat中的Context容器统一维护了这些监听器,并负责触发。 - -Context组件通过自定义类加载器来加载Web应用,并实现了Servlet规范,直接跟Web应用打交道。 -# FAQ -Context容器分别用了CopyOnWriteArrayList和对象数组来存储两种不同的监听器,为什么要这样设计呢? -因为: -- 属性值变化listener能动态配置,所以用CopyOnWriteArray -写不会那么频繁,读取比较频繁 -- 生命周期事件listener,不能动态改变,无线程安全问题 -生命周期相关的类比如session一个用户分配一个,用完了就会销毁,用对象数组,可以适应增删改操作 \ No newline at end of file diff --git "a/Tomcat/Tomcat\346\213\222\347\273\235\350\277\236\346\216\245\345\216\237\345\233\240\345\210\206\346\236\220\345\217\212\347\275\221\347\273\234\344\274\230\345\214\226.md" "b/Tomcat/Tomcat\346\213\222\347\273\235\350\277\236\346\216\245\345\216\237\345\233\240\345\210\206\346\236\220\345\217\212\347\275\221\347\273\234\344\274\230\345\214\226.md" deleted file mode 100644 index ac3c2efd9f..0000000000 --- "a/Tomcat/Tomcat\346\213\222\347\273\235\350\277\236\346\216\245\345\216\237\345\233\240\345\210\206\346\236\220\345\217\212\347\275\221\347\273\234\344\274\230\345\214\226.md" +++ /dev/null @@ -1,123 +0,0 @@ -Java Socket网络编程常见的异常有哪些,然后通过一个实验来重现其中的Connection reset异常,并且通过配置Tomcat的参数来解决这个问题。 - -# 异常场景 -### java.net.SocketTimeoutException -超时异常,超时分为 -- 连接超时 -在调用Socket.connect方法的时候超时,大多因为网络不稳定 -- 读取超时 -调用Socket.read方法时超时。不一定是因为网络延迟,很可能下游服务的响应时间过长 - -### java.net.BindException: Address already in use: JVM_Bind -端口被占用。 -当服务器端调用 -- new ServerSocket(port) -- 或Socket.bind函数 - -若端口已被占用,就会抛该异常。 - -可以用 - -```bash -netstat –an -``` -![](https://img-blog.csdnimg.cn/982f7e6b965a44ceb3733716134b387e.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -查看端口被谁占用了,换个空闲端口即可。 - -### java.net.ConnectException: Connection refused: connect -连接被拒绝。 -当客户端调用 -- new Socket(ip, port) -- 或Socket.connect函数 - -原因是: -- 未找到指定IP的机器 -- 机器存在,但该机器上没有开启指定监听端口 -#### 解决方案 -从客户端机器ping一下服务端IP: -- ping不通,看看IP是不是写错了? -- ping通,需要确认服务端的服务是不是挂了? - -### java.net.SocketException: Socket is closed -连接已关闭。 - -通信的一方主动关闭了Socket连接(调用了Socket的close方法),接着又对Socket连接进行了读写操作,这时os会报“Socket连接已关闭”。 -### java.net.SocketException: Connection reset/Connect reset by peer: Socket write error -连接被重置。 -- 通信的一方已将Socket关闭,可能是主动关闭或是因为异常退出,这时如果通信的另一方还在写数据,就会触发这个异常(Connect reset by peer) -- 若对方还在尝试从TCP连接中读数据,则会抛出Connection reset异常。 - -为了避免这些异常发生,在编写网络通信程序时要确保: -- 程序退出前要主动关闭所有的网络连接 -- 检测通信的另一方的关闭连接操作,当发现另一方关闭连接后自己也要关闭该连接。 - -### java.net.SocketException: Broken pipe -通信管道已坏。 - -发生这个异常的场景是,通信的一方在收到“Connect reset by peer: Socket write error”后,如果再继续写数据则会抛出Broken pipe异常,解决方法同上。 - -### java.net.SocketException: Too many open files -进程打开文件句柄数超过限制。 - -当并发用户数比较大时,服务器可能会报这个异常。这是因为每创建一个Socket连接就需要一个文件句柄,此外服务端程序在处理请求时可能也需要打开一些文件。 - -可以通过lsof -p pid命令查看进程打开了哪些文件,是不是有资源泄露,即进程打开的这些文件本应该被关闭,但由于程序的Bug而没有被关闭。 - -如果没有资源泄露,可以通过设置增加最大文件句柄数。具体方法是通过ulimit -a来查看系统目前资源限制,通过ulimit -n 10240修改最大文件数。 - -# Tomcat网络参数 -- maxConnections -- acceptCount - -## TCP连接的建立过程 -客户端向服务端发送SYN包,服务端回复**SYN+ACK**,同时将这个处于**SYN_RECV**状态的连接保存到半连接队列。 - -客户端返回ACK包完成三次握手,服务端将ESTABLISHED状态的连接移入accept队列,等待应用程序(Tomcat)调用accept方法将连接取走。 -这里涉及两个队列: -- 半连接队列:保存SYN_RECV状态的连接 -队列长度由`net.ipv4.tcp_max_syn_backlog`设置 -- accept队列:保存ESTABLISHED状态的连接 -队列长度为`min(net.core.somaxconn,backlog)`。其中backlog是我们创建ServerSocket时指定的参数,最终会传递给listen方法: -```c -int listen(int sockfd, int backlog); -``` -若设置的backlog大于`net.core.somaxconn`,accept队列的长度将被设置为`net.core.somaxconn`,而这个backlog参数就是Tomcat中的acceptCount参数,默认值100,但请注意`net.core.somaxconn`默认值128。 -在高并发情况下当Tomcat来不及处理新连接时,这些连接都被堆积在accept队列,而acceptCount参数可以控制accept队列长度。超过该长度,内核会向客户端发送RST,这样客户端会触发“Connection reset”异常。 - -**Tomcat#maxConnections** 指Tomcat在任意时刻接收和处理的最大连接数。 -当Tomcat接收的连接数达到maxConnections时,Acceptor线程不会再从accept队列取走连接,这时accept队列中的连接会越积越多。 - -maxConnections的默认值与连接器类型有关:NIO的默认值是10000,APR默认是8192。 - -所以Tomcat -```bash -最大并发连接数 = maxConnections + acceptCount -``` -若acceptCount- -设置得过大,请求等待时间会比较长;如果acceptCount设置过小,高并发情况下,客户端会立即触发Connection reset异常。 - -# Tomcat网络调优实战 -接下来我们通过一个直观的例子来加深对上面两个参数的理解。我们先重现流量高峰时accept队列堆积的情况,这样会导致客户端触发“Connection reset”异常,然后通过调整参数解决这个问题。主要步骤有: - -1. JMeter 创建一个测试计划、一个线程组、一个请求。 -测试计划: -![](https://img-blog.csdnimg.cn/d3a1aff7976b460da92a6cdb74928677.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -线程组(线程数这里设置为1000,模拟大流量): -![](https://img-blog.csdnimg.cn/d8311ca4a98540d98b61b381315d0df2.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -请求(请求的路径是Tomcat自带的例子程序): -![](https://img-blog.csdnimg.cn/915ac9b81cfd4a7c91ed93f9a74f3d76.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -2.启动Tomcat。 -3.开启JMeter测试,在View Results Tree中会看到大量失败的请求,请求的响应里有“Connection reset”异常,也就是前面提到的,当accept队列溢出时,服务端的内核发送了RST给客户端,使得客户端抛出了这个异常。 -![](https://img-blog.csdnimg.cn/eba2bf60725848548186bc6378b0c363.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -4.修改内核参数,在/etc/sysctl.conf中增加一行net.core.somaxconn=2048,然后执行命令sysctl -p。 -5.修改Tomcat参数acceptCount为2048,重启Tomcat -![](https://img-blog.csdnimg.cn/49394caf8a8941f4ae49b6c16fca7514.png) - -6.再次启动JMeter测试,这一次所有的请求会成功,也看不到异常了。我们可以通过下面的命令看到系统中ESTABLISHED的连接数增大了,这是因为我们加大了accept队列的长度。 -![](https://img-blog.csdnimg.cn/2aa712c36fbb4b2d8fb166763369f2ed.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - -如果一时找不到问题代码,还可以通过网络抓包工具来分析数据包。 \ No newline at end of file diff --git "a/Tomcat/Tomcat\346\236\266\346\236\204\347\256\200\344\273\213.md" "b/Tomcat/Tomcat\346\236\266\346\236\204\350\247\243\346\236\220\344\271\2131-\346\236\266\346\236\204\347\256\200\344\273\213.md" similarity index 100% rename from "Tomcat/Tomcat\346\236\266\346\236\204\347\256\200\344\273\213.md" rename to "Tomcat/Tomcat\346\236\266\346\236\204\350\247\243\346\236\220\344\271\2131-\346\236\266\346\236\204\347\256\200\344\273\213.md" diff --git "a/Tomcat/Tomcat\347\232\204Connector-BIO.md" "b/Tomcat/Tomcat\346\236\266\346\236\204\350\247\243\346\236\220\344\271\2132-connector-BIO.md" similarity index 100% rename from "Tomcat/Tomcat\347\232\204Connector-BIO.md" rename to "Tomcat/Tomcat\346\236\266\346\236\204\350\247\243\346\236\220\344\271\2132-connector-BIO.md" diff --git "a/Tomcat/Tomcat\347\232\204Connector-NIO.md" "b/Tomcat/Tomcat\346\236\266\346\236\204\350\247\243\346\236\220\344\271\2133-Connector-NIO.md" similarity index 99% rename from "Tomcat/Tomcat\347\232\204Connector-NIO.md" rename to "Tomcat/Tomcat\346\236\266\346\236\204\350\247\243\346\236\220\344\271\2133-Connector-NIO.md" index 60053206aa..ad8d6748d1 100644 --- "a/Tomcat/Tomcat\347\232\204Connector-NIO.md" +++ "b/Tomcat/Tomcat\346\236\266\346\236\204\350\247\243\346\236\220\344\271\2133-Connector-NIO.md" @@ -38,7 +38,7 @@ Worker线程拿到Poller传过来的socket后,将socket封装在SocketProcesso # NioSelectorPool NioEndpoint对象中维护了一个NioSelecPool对象,这个NioSelectorPool中又维护了一个BlockPoller线程,这个线程就是基于辅Selector进行NIO的逻辑。以执行servlet后,得到response,往socket中写数据为例,最终写的过程调用NioBlockingSelector的write方法。  -```java +``` public int write(ByteBuffer buf, NioChannel socket, long writeTimeout,MutableInteger lastWrite) throws IOException { SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector()); if ( key == null ) throw new IOException("Key no longer registered"); @@ -97,8 +97,8 @@ public int write(ByteBuffer buf, NioChannel socket, long writeTimeout,MutableInt ``` 也就是说当socket.write()返回0时,说明网络状态不稳定,这时将socket注册OP_WRITE事件到辅Selector,由BlockPoller线程不断轮询这个辅Selector,直到发现这个socket的写状态恢复了,通过那个倒数计数器,通知Worker线程继续写socket动作。 -BlockSelector线程逻辑: -```java +看一下BlockSelector线程的逻辑;  +``` public void run() { while (run) { try { @@ -140,7 +140,7 @@ public void run() { 使用这个辅Selector主要是减少线程间的切换,同时还可减轻主Selector的负担。以上描述了NIO connector工作的主要逻辑,可以看到在设计上还是比较精巧的。NIO connector还有一块就是Comet,有时间再说吧。需要注意的是,上面从Acceptor开始,有很多对象的封装,NioChannel及其KeyAttachment,PollerEvent和SocketProcessor对象,这些不是每次都重新生成一个新的,都是NioEndpoint分别维护了它们的对象池;  -```java +``` ConcurrentLinkedQueue processorCache = new ConcurrentLinkedQueue() ConcurrentLinkedQueue keyCache = new ConcurrentLinkedQueue() ConcurrentLinkedQueue eventCache = new ConcurrentLinkedQueue() diff --git "a/Tomcat/Tomcat\347\232\204\345\220\204\347\272\247\345\256\271\345\231\250\344\273\254\347\232\204\350\201\214\350\264\243.md" "b/Tomcat/Tomcat\347\232\204\345\220\204\347\272\247\345\256\271\345\231\250\344\273\254\347\232\204\350\201\214\350\264\243.md" deleted file mode 100644 index 958a15c53e..0000000000 --- "a/Tomcat/Tomcat\347\232\204\345\220\204\347\272\247\345\256\271\345\231\250\344\273\254\347\232\204\350\201\214\350\264\243.md" +++ /dev/null @@ -1,230 +0,0 @@ -> 通过startup.sh启动Tomcat后会发生什么呢? - -![](https://img-blog.csdnimg.cn/20210720163627669.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -1. Tomcat也是Java程序,因此startup.sh脚本会启动一个JVM运行Tomcat的启动类Bootstrap -2. Bootstrap主要负责初始化Tomcat的类加载器,并创建Catalina -3. Catalina是个启动类,解析server.xml、创建相应组件,并调用Server#start -4. Server组件负责管理Service组件,会调用Service#start -5. Service组件负责管理连接器和顶层容器Engine,因此会调用连接器和Engine的start() - -这些启动类或组件不处理具体的请求,它们主要是“管理”,管理下层组件的生命周期,并给下层组件分配任务,即路由请求到应负责的组件。 -# Catalina -主要负责创建Server,并非直接new个Server实例就完事了,而是: -- 解析server.xml,将里面配的各种组件创建出来 -- 接着调用Server组件的init、start方法,这样整个Tomcat就启动起来了 - -Catalina还需要处理各种“异常”,比如当通过“Ctrl + C”关闭Tomcat时, - -> Tomcat会如何优雅停止并清理资源呢? - -因此Catalina在JVM中注册一个 **关闭钩子**。 -```java -public void start() { - // 1. 如果持有的Server实例为空,就解析server.xml创建出来 - if (getServer() == null) { - load(); - } - // 2. 如果创建失败,报错退出 - if (getServer() == null) { - log.fatal(sm.getString("catalina.noServer")); - return; - } - - // 3.启动Server - try { - getServer().start(); - } catch (LifecycleException e) { - return; - } - - // 创建并注册关闭钩子 - if (useShutdownHook) { - if (shutdownHook == null) { - shutdownHook = new CatalinaShutdownHook(); - } - Runtime.getRuntime().addShutdownHook(shutdownHook); - } - - // 监听停止请求 - if (await) { - await(); - stop(); - } -} -``` -## 关闭钩子 -若需在JVM关闭时做一些清理,比如: -- 将缓存数据刷盘 -- 清理一些临时文件 - -就可以向JVM注册一个关闭钩子,其实就是个线程,JVM在停止之前会尝试执行该线程的run()。 - -Tomcat的**关闭钩子** 就是CatalinaShutdownHook: -![](https://img-blog.csdnimg.cn/eeb4c65673154a25876ec027c1f488df.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -Tomcat的“关闭钩子”实际上就执行了Server#stop,会释放和清理所有资源。 -# Server组件 -Server组件具体实现类StandardServer。 - -Server继承了LifecycleBase,它的生命周期被统一管理 -![](https://img-blog.csdnimg.cn/20210720165154608.png) -它的子组件是Service,因此它还需要管理Service的生命周期,即在启动时调用Service组件的启动方法,在停止时调用它们的停止方法。Server在内部维护了若干Service组件,它是以数组来保存的,那Server是如何添加一个Service到数组中的呢? -```java -@Override -public void addService(Service service) { - - service.setServer(this); - - synchronized (servicesLock) { - // 长度+1的数组并没有一开始就分配一个很长的数组 - // 而是在添加的过程中动态地扩展数组长度,当添加一个新的Service实例时 - // 会创建一个新数组并把原来数组内容复制到新数组,节省内存 - Service results[] = new Service[services.length + 1]; - - // 复制老数据 - System.arraycopy(services, 0, results, 0, services.length); - results[services.length] = service; - services = results; - - // 启动Service组件 - if (getState().isAvailable()) { - try { - service.start(); - } catch (LifecycleException e) { - // Ignore - } - } - - // 触发监听事件 - support.firePropertyChange("service", null, service); - } - -} -``` - -Server组件还需要启动一个Socket来监听停止端口,所以才能通过shutdown命令关闭Tomcat。 -上面Catalina的启动方法最后一行代码就是调用Server#await。 - -在await方法里会创建一个Socket监听8005端口,并在一个死循环里接收Socket上的连接请求,如果有新的连接到来就建立连接,然后从Socket中读取数据;如果读到的数据是停止命令“SHUTDOWN”,就退出循环,进入stop流程。 - -# Service组件 -Service组件的具体实现类StandardService -```java -public class StandardService extends LifecycleBase implements Service { - //名字 - private String name = null; - - //Server实例 - private Server server = null; - - //连接器数组 - protected Connector connectors[] = new Connector[0]; - private final Object connectorsLock = new Object(); - - //对应的Engine容器 - private Engine engine = null; - - //映射器及其监听器 - protected final Mapper mapper = new Mapper(); - protected final MapperListener mapperListener = new MapperListener(this); -``` - -StandardService继承了LifecycleBase抽象类,此外StandardService中还有一些我们熟悉的组件,比如Server、Connector、Engine和Mapper。 - -Tomcat支持热部署,当Web应用的部署发生变化,Mapper中的映射信息也要跟着变化,MapperListener就是监听器,监听容器的变化,并把信息更新到Mapper。 - -## Service启动方法 -```java -protected void startInternal() throws LifecycleException { - - // 1. 触发启动监听器 - setState(LifecycleState.STARTING); - - // 2. 先启动Engine,Engine会启动它子容器 - if (engine != null) { - synchronized (engine) { - engine.start(); - } - } - - // 3. 再启动Mapper监听器 - mapperListener.start(); - - // 4.最后启动连接器,连接器会启动它子组件,比如Endpoint - synchronized (connectorsLock) { - for (Connector connector: connectors) { - if (connector.getState() != LifecycleState.FAILED) { - connector.start(); - } - } - } -} -``` -Service先后启动Engine、Mapper监听器、连接器。 -内层组件启动好了才能对外提供服务,才能启动外层的连接器组件。而Mapper也依赖容器组件,容器组件启动好了才能监听它们的变化,因此Mapper和MapperListener在容器组件之后启动。 -# Engine组件 -最后我们再来看看顶层的容器组件Engine具体是如何实现的。Engine本质是一个容器,因此它继承了ContainerBase基类,并且实现了Engine接口。 - -```java -public class StandardEngine extends ContainerBase implements Engine { -} -``` -Engine的子容器是Host,所以它持有了一个Host容器的数组,这些功能都被抽象到了ContainerBase,ContainerBase中有这样一个数据结构: -```java -protected final HashMap children = new HashMap<>(); -``` -ContainerBase用HashMap保存了它的子容器,并且ContainerBase还实现了子容器的“增删改查”,甚至连子组件的启动和停止都提供了默认实现,比如ContainerBase会用专门的线程池来启动子容器。 -```java -for (int i = 0; i < children.length; i++) { - results.add(startStopExecutor.submit(new StartChild(children[i]))); -} -``` -所以Engine在启动Host子容器时就直接重用了这个方法。 -## Engine自己做了什么? -容器组件最重要的功能是处理请求,而Engine容器对请求的“处理”,其实就是把请求转发给某一个Host子容器来处理,具体是通过Valve来实现的。 - -每个容器组件都有一个Pipeline,而Pipeline中有一个基础阀(Basic Valve)。 -Engine容器的基础阀定义如下: - -```java -final class StandardEngineValve extends ValveBase { - - public final void invoke(Request request, Response response) - throws IOException, ServletException { - - // 拿到请求中的Host容器 - Host host = request.getHost(); - if (host == null) { - return; - } - - // 调用Host容器中的Pipeline中的第一个Valve - host.getPipeline().getFirst().invoke(request, response); - } - -} -``` -把请求转发到Host容器。 -处理请求的Host容器对象是从请求中拿到的,请求对象中怎么会有Host容器? -因为请求到达Engine容器前,Mapper组件已对请求进行路由处理,Mapper组件通过请求URL定位了相应的容器,并且把容器对象保存到请求对象。 - -所以当我们在设计这样的组件时,需考虑: -- 用合适的数据结构来保存子组件,比如 -Server用数组来保存Service组件,并且采取动态扩容的方式,这是因为数组结构简单,占用内存小 -ContainerBase用HashMap来保存子容器,虽然Map占用内存会多一点,但是可以通过Map来快速的查找子容器 -- 根据子组件依赖关系来决定它们的启动和停止顺序,以及如何优雅的停止,防止异常情况下的资源泄漏。 - -# 总结 -- Server 组件, 实现类 StandServer -- 继承了 LifeCycleBase -- 子组件是 Service, 需要管理其生命周期(调用其 LifeCycle 的方法), 用数组保存多个 Service 组件, 动态扩容数组来添加组件 -- 启动一个 socket Listen停止端口, Catalina 启动时, 调用 Server await 方法, 其创建 socket Listen 8005 端口, 并在死循环中等连接, 检查到 shutdown 命令, 调用 stop 方法 -- Service 组件, 实现类 StandService -- 包含 Server, Connector, Engine 和 Mapper 组件的成员变量 -- 还包含 MapperListener 成员变量, 以支持热部署, 其Listen容器变化, 并更新 Mapper, 是观察者模式 -- 需注意各组件启动顺序, 根据其依赖关系确定 -- 先启动 Engine, 再启动 Mapper Listener, 最后启动连接器, 而停止顺序相反. -- Engine 组件, 实现类 StandEngine 继承 ContainerBase -- ContainerBase 实现了维护子组件的逻辑, 用 HaspMap 保存子组件, 因此各层容器可重用逻辑 -- ContainerBase 用专门线程池启动子容器, 并负责子组件启动/停止, "增删改查" -- 请求到达 Engine 之前, Mapper 通过 URL 定位了容器, 并存入 Request 中. Engine 从 Request 取出 Host 子容器, 并调用其 pipeline 的第一个 valve \ No newline at end of file diff --git "a/Tomcat/Tomcat\347\232\204\347\272\277\347\250\213\346\250\241\345\236\213\350\256\276\350\256\241.md" "b/Tomcat/Tomcat\347\232\204\347\272\277\347\250\213\346\250\241\345\236\213\350\256\276\350\256\241.md" deleted file mode 100644 index c00bf8161d..0000000000 --- "a/Tomcat/Tomcat\347\232\204\347\272\277\347\250\213\346\250\241\345\236\213\350\256\276\350\256\241.md" +++ /dev/null @@ -1,211 +0,0 @@ -# UNIX系统的I/O模型 -同步阻塞I/O、同步非阻塞I/O、I/O多路复用、信号驱动I/O和异步I/O。 - -## 什么是 I/O -就是计算机内存与外部设备之间拷贝数据的过程。 - -## 为什么需要 I/O -CPU访问内存的速度远远高于外部设备,因此CPU是先把外部设备的数据读到内存里,然后再进行处理。 -当你的程序通过CPU向外部设备发出一个读指令,数据从外部设备拷贝到内存需要一段时间,这时CPU没事干,你的程序是: -- 主动把CPU让给别人 -- 还是让CPU不停查:数据到了吗?数据到了吗?... - -这就是I/O模型要解决的问题。 -# Java I/O模型 -对于一个网络I/O通信过程,比如网络数据读取,会涉及两个对象: -- 调用这个I/O操作的用户线程 -- 操作系统内核 - -一个进程的地址空间分为用户空间和内核空间,用户线程不能直接访问内核空间。 -当用户线程发起I/O操作后(Selector发出的select调用就是一个I/O操作),网络数据读取操作会经历两个步骤: -1. 用户线程等待内核将数据从网卡拷贝到内核空间 -2. 内核将数据从内核空间拷贝到用户空间 - -有人会好奇,内核数据从内核空间拷贝到用户空间,这样会不会有点浪费? -毕竟实际上只有一块内存,能否直接把内存地址指向用户空间可以读取? -Linux中有个叫mmap的系统调用,可以将磁盘文件映射到内存,省去了内核和用户空间的拷贝,但不支持网络通信场景! - - -各种I/O模型的区别就是这两个步骤的方式不一样。 - -## 同步阻塞I/O -用户线程发起read调用后就阻塞了,让出CPU。内核等待网卡数据到来,把数据从网卡拷贝到内核空间,接着把数据拷贝到用户空间,再把用户线程叫醒。 - -![](https://img-blog.csdnimg.cn/82492e5325a8474490b27099e2516073.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -## 同步非阻塞I/O -用户进程主动发起read调用,这是个系统调用,CPU由用户态切换到内核态,执行内核代码。 -内核发现该socket上的数据已到内核空间,将用户线程挂起,然后把数据从内核空间拷贝到用户空间,再唤醒用户线程,read调用返回。 - -用户线程不断发起read调用,数据没到内核空间时,每次都返回失败,直到数据到了内核空间,这次read调用后,在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的,等数据到了用户空间再把线程叫醒。 -![](https://img-blog.csdnimg.cn/99ce6ef4b03d4b629d5c69cb04650765.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -## I/O多路复用 -用户线程的读取操作分成两步: -- 线程先发起select调用,问内核:数据准备好了吗? -- 等内核把数据准备好了,用户线程再发起read调用 -在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的 - -为什么叫I/O多路复用? -因为一次select调用可以向内核查多个数据通道(Channel)的状态。 -![](https://img-blog.csdnimg.cn/1fdf320b193142c0be25d8ed73988961.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - ->NIO API可以不用Selector,就是同步非阻塞。使用了Selector就是IO多路复用。 - -## 异步I/O -用户线程发起read调用的同时注册一个回调函数,read立即返回,等内核将数据准备好后,再调用指定的回调函数完成处理。在这个过程中,用户线程一直没有阻塞。 -![](https://img-blog.csdnimg.cn/2a9157c42ae04ecfa35075059df25547.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -## 信号驱动I/O -可以把信号驱动I/O理解为“半异步”,非阻塞模式是应用不断发起read调用查询数据到了内核没有,而信号驱动把这个过程异步了,应用发起read调用时注册了一个信号处理函数,其实是个回调函数,数据到了内核后,内核触发这个回调函数,应用在回调函数里再发起一次read调用去读内核的数据。 -所以是半异步。 -# NioEndpoint组件 -Tomcat的NioEndpoint实现了I/O多路复用模型。 - -## 工作流程 -Java的多路复用器的使用: -1. 创建一个Selector,在其上注册感兴趣的事件,然后调用select方法,等待感兴趣的事情发生 -2. 感兴趣的事情发生了,比如可读了,就创建一个新的线程从Channel中读数据 - -NioEndpoint包含LimitLatch、Acceptor、Poller、SocketProcessor和Executor共5个组件。 -![](https://img-blog.csdnimg.cn/df667632760040c4a197f55936a8eec7.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -## LimitLatch -连接控制器,控制最大连接数,NIO模式下默认是8192。 -![](https://img-blog.csdnimg.cn/cbc41d109e944a14ad51650ec491da8f.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -当连接数到达最大时阻塞线程,直到后续组件处理完一个连接后将连接数减1。 -到达最大连接数后,os底层还是会接收客户端连接,但用户层已不再接收。 -核心代码: -```java -public class LimitLatch { - private class Sync extends AbstractQueuedSynchronizer { - - @Override - protected int tryAcquireShared() { - long newCount = count.incrementAndGet(); - if (newCount > limit) { - count.decrementAndGet(); - return -1; - } else { - return 1; - } - } - - @Override - protected boolean tryReleaseShared(int arg) { - count.decrementAndGet(); - return true; - } - } - - private final Sync sync; - private final AtomicLong count; - private volatile long limit; - - // 线程调用该方法,获得接收新连接的许可,线程可能被阻塞 - public void countUpOrAwait() throws InterruptedException { - sync.acquireSharedInterruptibly(1); - } - - // 调用这个方法来释放一个连接许可,则前面阻塞的线程可能被唤醒 - public long countDown() { - sync.releaseShared(0); - long result = getCount(); - return result; - } -} -``` -用户线程调用**LimitLatch#countUpOrAwait**拿到锁,若无法获取,则该线程会被阻塞在AQS队列。 -AQS又是怎么知道是阻塞还是不阻塞用户线程的呢? -由AQS的使用者决定,即内部类Sync决定,因为Sync类重写了**AQS#tryAcquireShared()**:若当前连接数count < limit,线程能获取锁,返回1,否则返回-1。 - -如何用户线程被阻塞到了AQS的队列,由Sync内部类决定什么时候唤醒,Sync重写AQS#tryReleaseShared(),当一个连接请求处理完了,又可以接收新连接,这样前面阻塞的线程将会被唤醒。 - -LimitLatch用来限制应用接收连接的数量,Acceptor用来限制系统层面的连接数量,首先是LimitLatch限制,应用层处理不过来了,连接才会堆积在操作系统的Queue,而Queue的大小由acceptCount控制。 -## Acceptor -Acceptor实现了Runnable接口,因此可以跑在单独线程里,在这个死循环里调用accept接收新连接。一旦有新连接请求到达,accept方法返回一个Channel对象,接着把Channel对象交给Poller去处理。 - -一个端口号只能对应一个ServerSocketChannel,因此这个ServerSocketChannel是在多个Acceptor线程之间共享的,它是Endpoint的属性,由Endpoint完成初始化和端口绑定。 -可以同时有过个Acceptor调用accept方法,accept是线程安全的。 - -### 初始化 -```java -protected void initServerSocket() throws Exception { - if (!getUseInheritedChannel()) { - serverSock = ServerSocketChannel.open(); - socketProperties.setProperties(serverSock.socket()); - InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset()); - - serverSock.socket().bind(addr,getAcceptCount()); - } else { - // Retrieve the channel provided by the OS - Channel ic = System.inheritedChannel(); - if (ic instanceof ServerSocketChannel) { - serverSock = (ServerSocketChannel) ic; - } - if (serverSock == null) { - throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited")); - } - } - // 阻塞模式 - serverSock.configureBlocking(true); //mimic APR behavior -} -``` -- bind方法的 getAcceptCount() 参数表示os的等待队列长度。当应用层的连接数到达最大值时,os可以继续接收连接,os能继续接收的最大连接数就是这个队列长度,可以通过acceptCount参数配置,默认是100 -![](https://img-blog.csdnimg.cn/0129d890067c4dafb5465f301990581b.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -ServerSocketChannel通过accept()接受新的连接,accept()方法返回获得SocketChannel对象,然后将SocketChannel对象封装在一个PollerEvent对象中,并将PollerEvent对象压入Poller的Queue里。 -这是个典型的“生产者-消费者”模式,Acceptor与Poller线程之间通过Queue通信。 - -## Poller -本质是一个Selector,也跑在单独线程里。 - -Poller在内部维护一个Channel数组,它在一个死循环里不断检测Channel的数据就绪状态,一旦有Channel可读,就生成一个SocketProcessor任务对象扔给Executor去处理。 - -内核空间的接收连接是对每个连接都产生一个channel,该channel就是Acceptor里accept方法得到的scoketChannel,后面的Poller在用selector#select监听内核是否准备就绪,才知道监听内核哪个channel。 - - -维护了一个 Queue: -![](https://img-blog.csdnimg.cn/8957657d0d404199b57624d13cdeab49.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -SynchronizedQueue的方法比如offer、poll、size和clear都使用synchronized修饰,即同一时刻只有一个Acceptor线程读写Queue。 -同时有多个Poller线程在运行,每个Poller线程都有自己的Queue。 -每个Poller线程可能同时被多个Acceptor线程调用来注册PollerEvent。 -Poller的个数可以通过pollers参数配置。 -### 职责 -- Poller不断的通过内部的Selector对象向内核查询Channel状态,一旦可读就生成任务类SocketProcessor交给Executor处理 -![](https://img-blog.csdnimg.cn/26a153742fe14f55b5919e86e03782cf.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -- Poller循环遍历检查自己所管理的SocketChannel是否已超时。若超时就关闭该SocketChannel - -## SocketProcessor -Poller会创建SocketProcessor任务类交给线程池处理,而SocketProcessor实现了Runnable接口,用来定义Executor中线程所执行的任务,主要就是调用Http11Processor组件处理请求:Http11Processor读取Channel的数据来生成ServletRequest对象。 - -Http11Processor并非直接读取Channel。因为Tomcat支持同步非阻塞I/O、异步I/O模型,在Java API中,对应Channel类不同,比如有AsynchronousSocketChannel和SocketChannel,为了对Http11Processor屏蔽这些差异,Tomcat设计了一个包装类叫作SocketWrapper,Http11Processor只调用SocketWrapper的方法去读写数据。 -## Executor -线程池,负责运行SocketProcessor任务类,SocketProcessor的run方法会调用Http11Processor来读取和解析请求数据。我们知道,Http11Processor是应用层协议的封装,它会调用容器获得响应,再把响应通过Channel写出。 - -Tomcat定制的线程池,它负责创建真正干活的工作线程。就是执行SocketProcessor#run,即解析请求并通过容器来处理请求,最终调用Servlet。 - -# Tomcat的高并发设计 -高并发就是能快速地处理大量请求,需合理设计线程模型让CPU忙起来,尽量不要让线程阻塞,因为一阻塞,CPU就闲了。 -有多少任务,就用相应规模线程数去处理。 -比如NioEndpoint要完成三件事情:接收连接、检测I/O事件和处理请求,关键就是把这三件事情分别定制线程数处理: -- 专门的线程组去跑Acceptor,并且Acceptor的个数可以配置 -- 专门的线程组去跑Poller,Poller的个数也可以配置 -- 具体任务的执行也由专门的线程池来处理,也可以配置线程池的大小 - -# 总结 -I/O模型是为了解决内存和外部设备速度差异。 -- 所谓阻塞或非阻塞是指应用程序在发起I/O操作时,是立即返回还是等待 -- 同步和异步,是指应用程序在与内核通信时,数据从内核空间到应用空间的拷贝,是由内核主动发起还是由应用程序来触发。 - -Tomcat#Endpoint组件的主要工作就是处理I/O,而NioEndpoint利用Java NIO API实现了多路复用I/O模型。 -读写数据的线程自己不会阻塞在I/O等待上,而是把这个工作交给Selector。 - -当客户端发起一个HTTP请求时,首先由Acceptor#run中的 -```java -socket = endpoint.serverSocketAccept(); -``` - -接收连接,然后传递给名称为Poller的线程去侦测I/O事件,Poller线程会一直select,选出内核将数据从网卡拷贝到内核空间的 channel(也就是内核已经准备好数据)然后交给名称为Catalina-exec的线程去处理,这个过程也包括内核将数据从内核空间拷贝到用户空间这么一个过程,所以对于exec线程是阻塞的,此时用户空间(也就是exec线程)就接收到了数据,可以解析然后做业务处理了。 - -> 参考 -> - https://blog.csdn.net/historyasamirror/article/details/5778378 \ No newline at end of file diff --git "a/Tomcat/Tomcat\350\277\233\347\250\213\345\215\240\347\224\250CPU\350\277\207\351\253\230\346\200\216\344\271\210\345\212\236\357\274\237.md" "b/Tomcat/Tomcat\350\277\233\347\250\213\345\215\240\347\224\250CPU\350\277\207\351\253\230\346\200\216\344\271\210\345\212\236\357\274\237.md" deleted file mode 100644 index 70905512f1..0000000000 --- "a/Tomcat/Tomcat\350\277\233\347\250\213\345\215\240\347\224\250CPU\350\277\207\351\253\230\346\200\216\344\271\210\345\212\236\357\274\237.md" +++ /dev/null @@ -1,77 +0,0 @@ -CPU经常会成为系统性能的瓶颈,可能: -- 内存泄露导致频繁GC,进而引起CPU使用率过高 -- 代码Bug创建了大量的线程,导致CPU频繁上下文切换 - -通常所说的CPU使用率过高,隐含着一个用来比较高与低的基准值,比如 -- JVM在峰值负载下的平均CPU利用率40% -- CPU使用率飙到80%就可认为不正常 - -JVM进程包含多个Java线程: -- 一些在等待工作 -- 另一些则正在执行任务 - -最重要的是找到哪些线程在消耗CPU,通过线程栈定位到问题代码 -如果没有找到个别线程的CPU使用率特别高,考虑是否线程上下文切换导致了CPU使用率过高。 - -# 案例 -程序模拟CPU使用率过高 - 在线程池中创建4096个线程 - -在Linux环境下启动程序: -java -Xss256k -jar demo-0.0.1-SNAPSHOT.jar -线程栈大小指定为256KB。对于测试程序来说,操作系统默认值8192KB过大,因为需要创建4096个线程。 - -使用top命令,我们看到Java进程的CPU使用率达到了961.6%,注意到进程ID是55790。 -![](https://img-blog.csdnimg.cn/ccc4bf66ef604b20a0875593d571c134.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -用更精细化的top命令查看这个Java进程中各线程使用CPU的情况: - -```java -#top -H -p 55790 -``` -![](https://img-blog.csdnimg.cn/513921500f344102b8857b0cf937b6f4.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -可见,有个叫“scheduling-1”的线程占用了较多的CPU,达到了42.5%。因此下一步我们要找出这个线程在做什么事情。 - -5. 为了找出线程在做什么,用jstack生成线程快照。 -jstack输出较大,一般将其写入文件: -```java -jstack 55790 > 55790.log -``` -打开55790.log,定位到第4步中找到的名为 **scheduling-1** 的线程,其线程栈: -![](https://img-blog.csdnimg.cn/fa069dc8e34e4c209d72b5b9a6fa8c40.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -看到AbstractExecutorService#submit这个函数调用,说明它是Spring Boot启动的周期性任务线程,向线程池中提交任务,该线程消耗了大量CPU。 - -# 上下文切换开销? -经历上述过程,往往已经可以定位到大量消耗CPU的线程及bug代码,比如死循环。但对于该案例:Java进程占用的CPU是961.6%, 而“scheduling-1”线程只占用了42.5%的CPU,那其它CPU被谁占用了? - -第4步用top -H -p pid命令看到的线程列表中还有许多名为“pool-1-thread-x”的线程,它们单个的CPU使用率不高,但是似乎数量比较多。你可能已经猜到,这些就是线程池中干活的线程。那剩下的CPU是不是被这些线程消耗了呢? - -还需要看jstack的输出结果,主要是看这些线程池中的线程是不是真的在干活,还是在“休息”呢? -![](https://img-blog.csdnimg.cn/4346e7a5f97943458a0878b66b1a1298.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70)发现这些“pool-1-thread-x”线程基本都处WAITING状态。 -![](https://img-blog.csdnimg.cn/d36a43b009fd4ec9beef152b8b980408.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- Blocking指的是一个线程因为等待临界区的锁(Lock或者synchronized关键字)而被阻塞的状态,请你注意的是处于这个状态的线程还没有拿到锁 -- Waiting指的是一个线程拿到了锁,但需等待其他线程执行某些操作。比如调用了Object.wait、Thread.join或LockSupport.park方法时,进入Waiting状态。前提是这个线程已经拿到锁了,并且在进入Waiting状态前,os层面会自动释放锁,当等待条件满足,外部调用了Object.notify或者LockSupport.unpark方法,线程会重新竞争锁,成功获得锁后才能进入到Runnable状态继续执行。 - -回到我们的“pool-1-thread-x”线程,这些线程都处在“Waiting”状态,从线程栈我们看到,这些线程“等待”在getTask方法调用上,线程尝试从线程池的队列中取任务,但是队列为空,所以通过LockSupport.park调用进到了“Waiting”状态。那“pool-1-thread-x”线程有多少个呢?通过下面这个命令来统计一下,结果是4096,正好跟线程池中的线程数相等。 -```bash -grep -o 'pool-2-thread' 55790.log | wc -l -``` -![](https://img-blog.csdnimg.cn/7085afda568d461d8dc52c2d6a92b602.png) - - -剩下CPU到底被谁消耗了? -应该怀疑CPU的上下文切换开销了,因为我们看到Java进程中的线程数比较多。 - -下面通过vmstat命令来查看一下操作系统层面的线程上下文切换活动: -![](https://img-blog.csdnimg.cn/a72a837da75e427a902b1c60c4210614.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -cs那一栏表示线程上下文切换次数,in表示CPU中断次数,我们发现这两个数字非常高,基本证实了我们的猜测,线程上下文切切换消耗了大量CPU。 -那具体是哪个进程导致的呢? - -停止Spring Boot程序,再次运行vmstat命令,会看到in和cs都大幅下降,这就证实引起线程上下文切换开销的Java进程正是55790。 -![](https://img-blog.csdnimg.cn/9200ce42e84543d6b2564f7f501d9b47.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -# 总结 -遇到CPU过高,首先定位哪个进程导致的,之后可以通过top -H -p pid命令定位到具体的线程。 -其次还要通jstack查看线程的状态,看看线程的个数或者线程的状态,如果线程数过多,可以怀疑是线程上下文切换的开销,我们可以通过vmstat和pidstat这两个工具进行确认。 \ No newline at end of file diff --git "a/Tomcat/Tomcat\350\277\236\346\216\245\345\231\250\350\256\276\350\256\241\346\236\266\346\236\204.md" "b/Tomcat/Tomcat\350\277\236\346\216\245\345\231\250\350\256\276\350\256\241\346\236\266\346\236\204.md" deleted file mode 100644 index 9642be5a67..0000000000 --- "a/Tomcat/Tomcat\350\277\236\346\216\245\345\231\250\350\256\276\350\256\241\346\236\266\346\236\204.md" +++ /dev/null @@ -1,118 +0,0 @@ -# 1 Tomcat 核心功能 -- 处理Socket连接,负责网络字节流与Request和Response对象的转化 -因此Tomcat设计了连接器(Connector),负责对外交流 -- 加载和管理Servlet,以及具体处理Request请求 -设计了容器(Container),负责内部处理 - -# 2 Tomcat支持的I/O模型 -- NIO -非阻塞I/O,采用Java NIO类库实现。 -- NIO.2 -异步I/O,采用JDK 7最新的NIO.2类库实现。 -- APR -采用Apache可移植运行库实现,是C/C++编写的本地库 - -![](https://img-blog.csdnimg.cn/d562828da21147818236f6e2d2bbc8d2.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -# 3 Tomcat支持的应用层协议 -- HTTP/1.1 -大部分Web应用采用的访问协议。 -- AJP -用于和Web服务器集成(如Apache)。 -- HTTP/2 -HTTP 2.0大幅度的提升了Web性能。 -# 4 Service -Tomcat为 **支持多种I/O模型和应用层协议**,一个容器可能对接多个连接器。 -但单独的连接器或容器都无法对外提供服务,需**组装**才能正常协作,而组装后的整体,就称为Service组件。所以,Service并不神奇,只是在连接器和容器外面多包了一层,把它们组装在一起。 - -Tomcat内可能有多个Service,在Tomcat中配置多个Service,可实现通过不同端口号访问同一台机器上部署的不同应用。 -![](https://img-blog.csdnimg.cn/94069010a40243b3b33fc96ae5f0f3d5.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -最顶层是Server(即一个Tomcat实例)。一个Server中有一或多个Service,一个Service中有多个连接器和一个容器。 - -连接器与容器之间通过标准的ServletRequest/ServletResponse通信。 -# 5 连接器架构 -连接器对Servlet容器屏蔽了 **协议及I/O模型的区别**,处理Socket通信和应用层协议的解析,得到Servlet请求。 -所以无论是HTTP、AJP,最终在容器中获取到的都是标准ServletRequest对象。 - -## 5.1 功能需求 -- 监听网络端口 -- 接受网络连接请求 -- 读取网络请求字节流 -- 根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的Tomcat Request对象 -- 将Tomcat Request对象转成标准的ServletRequest -- 调用Servlet容器,得到ServletResponse -- 将ServletResponse转成Tomcat Response对象 -- 将Tomcat Response转成网络字节流 -- 将响应字节流写回给浏览器。 - -那它应该有哪些子模块呢? -优秀的模块化设计应该考虑高内聚、低耦合。连接器需完成如下高内聚功能: -- 网络通信 -- 应用层协议解析 -- Tomcat Request/Response与ServletRequest/ServletResponse的转化 - -因此Tomcat设计3个组件实现这3功能:Endpoint、Processor和Adapter。 - -组件间通过抽象接口交互,以封装变化:将系统中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度。 - -不管网络通信I/O模型、应用层协议、浏览器端发送的请求信息如何变化,但整体处理逻辑不变: -- Endpoint -提供字节流给Processor -- Processor -提供Tomcat Request对象给Adapter -- Adapter -提供ServletRequest对象给容器 - -若要支持新的I/O方案、新的应用层协议,只需要实现相关具体子类,而上层通用处理逻辑不变。 - -由于I/O模型和应用层协议可自由组合,比如NIO + HTTP或者NIO.2 + AJP。Tomcat将网络通信和应用层协议解析放在一起考虑,设计了ProtocolHandler接口,封装这两种变化点。 -## 5.2 ProtocolHandler -各种协议和通信模型的组合有相应的具体实现类,如: -![](https://img-blog.csdnimg.cn/20210717231048532.png) -![](https://img-blog.csdnimg.cn/20210717231106745.png) -![](https://img-blog.csdnimg.cn/20210717231144614.png) -Tomcat设计了一系列抽象基类封装稳定部分,抽象基类AbstractProtocol实现了ProtocolHandler接口。 -每种应用层协议有自己的抽象基类,如AbstractAjpProtocol、AbstractHttp11Protocol,具体协议实现类扩展了协议层抽象基类。 -![](https://img-blog.csdnimg.cn/20210717232129435.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -如此设计,尽量地将稳定的部分放到抽象基类,同时每一种I/O模型和协议的组合都有相应的具体实现类,我们在使用时可以自由选择。 - -**Endpoint和Processor放在一起抽象成了ProtocolHandler组件**: -![](https://img-blog.csdnimg.cn/8005f739f3f442b28ed568395af2435e.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -连接器用ProtocolHandler处理网络连接、应用层协议,包含如下重要部件 -### 5.2.1 Endpoint -通信端点,即通信监听的接口,是具体的Socket接收和发送处理器,是对传输层的抽象,因此Endpoint用来实现TCP/IP协议。 - -Endpoint是一个接口,对应的抽象实现类是AbstractEndpoint,而AbstractEndpoint的具体子类,比如在NioEndpoint和Nio2Endpoint中,有两个重要的子组件:Acceptor和SocketProcessor。 -#### Acceptor -用于监听Socket连接请求。SocketProcessor用于处理接收到的Socket请求,它实现Runnable接口,在run方法里调用协议处理组件Processor进行处理。 - -为了提高处理能力,SocketProcessor被提交到线程池来执行。而这个线程池叫作执行器(Executor)。 -### 5.2.2 Processor -Processor用来实现应用层的HTTP协议,接收来自Endpoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理。 - -Processor是一个接口,定义了请求的处理等方法。它的抽象实现类AbstractProcessor对一些协议共有的属性进行封装,没有对方法进行实现。具体的实现有AjpProcessor、Http11Processor等,这些具体实现类实现了特定协议的解析方法和请求处理方式。 - -连接器的组件图: -![](https://img-blog.csdnimg.cn/8180c30045194e29b5c1d1ef500fa9a9.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -Endpoint接收到Socket连接后,生成一个SocketProcessor任务提交到线程池处理,SocketProcessor的run方法会调用Processor组件去解析应用层协议,Processor通过解析生成Request对象后,会调用Adapter的Service方法。 - -一个连接器对应一个监听端口,比如一扇门,一个web应用是一个业务部门,进了这个门后你可以到各个业务部门去办事。 -Tomcat配置的并发数是endpoint里那个线程池。 -### 5.2.3 Adapter -由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的Request类来“存放”这些请求信息。 -ProtocolHandler接口负责解析请求并生成Tomcat Request类,但这个Request对象不是标准ServletRequest,不能用Tomcat Request作为参数调用容器。 - -于是Tomcat引入CoyoteAdapter,连接器调用CoyoteAdapter的sevice方法,传入Tomcat Request对象,CoyoteAdapter负责将Tomcat Request转成ServletRequest,再调用容器的service方法。 - -连接器用ProtocolHandler接口来封装通信协议和I/O模型的差异,ProtocolHandler内部又分为Endpoint和Processor模块,Endpoint负责底层Socket通信,Processor负责应用层协议解析。连接器通过适配器Adapter调用容器。 - -**为什么要多一层adapter?** -在processor直接转换为容器的servletrequest和servletresponse是否更好,为何先转化为Tomcat的request和response,再用adapter做一层转换消耗性能? -若连接器直接创建ServletRequest、ServletResponse,就和Servlet协议耦合,连接器尽量保持独立性,它不一定要跟Servlet容器工作。 -对象转化的性能消耗还是比较少的,Tomcat对HTTP请求体采取了延迟解析策略,即TomcatRequest对象转化成ServletRequest时,请求体的内容都还没读取,直到容器处理这个请求的时候才读取。 - -Adapter一层使用的是适配器设计模式,好处是当容器版本升级只修改Adaper组件适配到新版本容器就可以了,protocal handler组件代码不需要改动。 -# 6 Tomcat V.S Netty -**为何Netty常用做底层通讯模块,而Tomcat作为web容器?** -可将Netty理解成Tomcat中的连接器,都负责网络通信、利用了NIO。但Netty素以高性能高并发著称,为何Tomcat不直接将连接器替换成Netty? -- Tomcat的连接器性能已经足够好了,同样是Java NIO编程,底层原理类似 -- Tomcat做为Web容器,需考虑Servlet规范,Servlet规范规定了对HTTP Body的读写是阻塞的,因此即使用到Netty,也不能充分发挥其优势。所以Netty一般用在非HTTP协议/Servlet场景。 \ No newline at end of file diff --git a/out/TODO/uml/ReplicaManager#appendRecordstxt/ReplicaManager#appendRecordstxt.png b/out/TODO/uml/ReplicaManager#appendRecordstxt/ReplicaManager#appendRecordstxt.png deleted file mode 100644 index f0f9ca8bf5..0000000000 Binary files a/out/TODO/uml/ReplicaManager#appendRecordstxt/ReplicaManager#appendRecordstxt.png and /dev/null differ diff --git a/out/TODO/uml/ReplicaManager#fetchMessages/ReplicaManager#fetchMessages.png b/out/TODO/uml/ReplicaManager#fetchMessages/ReplicaManager#fetchMessages.png deleted file mode 100644 index 0e78971422..0000000000 Binary files a/out/TODO/uml/ReplicaManager#fetchMessages/ReplicaManager#fetchMessages.png and /dev/null differ diff --git a/out/TODO/uml/appendRecords/appendRecords.png b/out/TODO/uml/appendRecords/appendRecords.png deleted file mode 100644 index 4834cb85f7..0000000000 Binary files a/out/TODO/uml/appendRecords/appendRecords.png and /dev/null differ diff --git a/out/TODO/uml/processFetchRequest/processFetchRequest.png b/out/TODO/uml/processFetchRequest/processFetchRequest.png deleted file mode 100644 index 546e4be796..0000000000 Binary files a/out/TODO/uml/processFetchRequest/processFetchRequest.png and /dev/null differ diff --git "a/\344\272\221\345\216\237\347\224\237/Docker/Docker\345\256\271\345\231\250\345\256\236\346\210\230(\345\205\253)-\350\260\210\350\260\210 Kubernetes \347\232\204\346\234\254\350\264\250.md" "b/\344\272\221\345\216\237\347\224\237/Docker/Docker\345\256\271\345\231\250\345\256\236\346\210\230(\345\205\253)-\350\260\210\350\260\210 Kubernetes \347\232\204\346\234\254\350\264\250.md" index a83cc67093..46173b34af 100644 --- "a/\344\272\221\345\216\237\347\224\237/Docker/Docker\345\256\271\345\231\250\345\256\236\346\210\230(\345\205\253)-\350\260\210\350\260\210 Kubernetes \347\232\204\346\234\254\350\264\250.md" +++ "b/\344\272\221\345\216\237\347\224\237/Docker/Docker\345\256\271\345\231\250\345\256\236\346\210\230(\345\205\253)-\350\260\210\350\260\210 Kubernetes \347\232\204\346\234\254\350\264\250.md" @@ -31,13 +31,14 @@ Borg系统,一直以来都被誉为Google公司内部最强大的“秘密武 相比于Spanner、BigTable等相对上层的项目,Borg要承担的责任,是承载Google整个基础设施的核心依赖。 在Google已经公开发表的基础设施体系论文中,Borg项目当仁不让地位居整个基础设施技术栈的最底层。 ![](https://img-blog.csdnimg.cn/2019101601190585.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -> 源于Google Omega论文的第一作者的博士毕业论文,描绘了当时Google已公开发表的整个基础设施栈。可发现MapReduce、BigTable等知名项目。Borg和其继任者Omega位于整个技术栈的最底层。 +> 这幅图,来自于Google Omega论文的第一作者的博士毕业论文。它描绘了当时Google已经公开发表的整个基础设施栈。在这个图里,你既可以找到MapReduce、BigTable等知名项目,也能看到Borg和它的继任者Omega位于整个技术栈的最底层。 -所以Borg可算是Google最不可能开源的一个项目。得益于Docker项目和容器技术发展,它却终于以Kubernetes身份开源。 +正是由于这样的定位,Borg可以说是Google最不可能开源的一个项目。 +得益于Docker项目和容器技术的风靡,它却终于得以以另一种方式与开源社区见面,就是Kubernetes项目。 相比于“小打小闹”的Docker公司、“旧瓶装新酒”的Mesos社区,**Kubernetes项目从一开始就比较幸运地站上了一个他人难以企及的高度:** 在它的成长阶段,这个项目每一个核心特性的提出,几乎都脱胎于Borg/Omega系统的设计与经验。 -这些特性在社区落地过程中,又在整个社区的合力之下得到了极大的改进,修复了大量Borg体系的历史缺陷。 +更重要的是,这些特性在开源社区落地的过程中,又在整个社区的合力之下得到了极大的改进,修复了很多当年遗留在Borg体系中的缺陷和问题。 尽管在发布之初被批“曲高和寡”,但在逐渐觉察到Docker技术栈的“稚嫩”和Mesos社区的“老迈”,社区很快就明白了:Kubernetes项目在Borg体系的指导下,体现出了一种独有的先进与完备性,这些才是一个基础设施领域开源项目的核心价值。 @@ -283,5 +284,6 @@ $ kubectl create -f nginx-deployment.yaml Kubernetes为用户提供的不仅限于一个工具。它真正的价值,还是在于提供了一套基于容器构建分布式系统的基础依赖 # 参考 + - 深入剖析Kubernetes - [Large-scale cluster management at Google with Borg](https://storage.googleapis.com/pub-tools-public-publication-data/pdf/43438.pdf) \ No newline at end of file diff --git "a/\345\244\247\346\225\260\346\215\256/Scala/Scala-\345\205\245\351\227\250.md" "b/\345\244\247\346\225\260\346\215\256/Scala/Scala-\345\205\245\351\227\250.md" index cb19270ca5..3cc41ffdcf 100644 --- "a/\345\244\247\346\225\260\346\215\256/Scala/Scala-\345\205\245\351\227\250.md" +++ "b/\345\244\247\346\225\260\346\215\256/Scala/Scala-\345\205\245\351\227\250.md" @@ -1,5 +1,6 @@ # 1 函数式编程思想 ## 1.1 介绍 +![](https://upload-images.jianshu.io/upload_images/4685968-2217fa78d2aae90a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ![](https://upload-images.jianshu.io/upload_images/4685968-339e97ac6c2f74e1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ![](https://upload-images.jianshu.io/upload_images/4685968-102c9902ed240289.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ![](https://upload-images.jianshu.io/upload_images/4685968-3c5db14174facab2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) diff --git "a/\345\244\247\346\225\260\346\215\256/\345\217\262\344\270\212\346\234\200\345\277\253!-10\345\260\217\346\227\266\345\244\247\346\225\260\346\215\256\345\205\245\351\227\250\345\256\236\346\210\230(\344\272\224)-\345\210\206\345\270\203\345\274\217\350\256\241\347\256\227\346\241\206\346\236\266MapReduce.md" "b/\345\244\247\346\225\260\346\215\256/\345\217\262\344\270\212\346\234\200\345\277\253!-10\345\260\217\346\227\266\345\244\247\346\225\260\346\215\256\345\205\245\351\227\250\345\256\236\346\210\230(\344\272\224)-\345\210\206\345\270\203\345\274\217\350\256\241\347\256\227\346\241\206\346\236\266MapReduce.md" new file mode 100644 index 0000000000..b552a051cb --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256/\345\217\262\344\270\212\346\234\200\345\277\253!-10\345\260\217\346\227\266\345\244\247\346\225\260\346\215\256\345\205\245\351\227\250\345\256\236\346\210\230(\344\272\224)-\345\210\206\345\270\203\345\274\217\350\256\241\347\256\227\346\241\206\346\236\266MapReduce.md" @@ -0,0 +1,48 @@ +![目录](https://upload-images.jianshu.io/upload_images/4685968-d1ad6dbf94d38f20.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 1 MapReduce概述 +![](https://upload-images.jianshu.io/upload_images/4685968-8a3f8a5a26992bb9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 2 MapReduce编程模型之通过wordcount词频统计分析案例入门 +![](https://upload-images.jianshu.io/upload_images/4685968-7563d21e44338bbd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# MapReduce执行流程 +![](https://upload-images.jianshu.io/upload_images/4685968-f92ef9dac3edcb2a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-49d9b19a457d7847.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +![](https://upload-images.jianshu.io/upload_images/4685968-e1f014833eae5eb8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- InputFormat +![](https://upload-images.jianshu.io/upload_images/4685968-1907b9114cc42568.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-3ac372af0238dc7b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-2f4a418b7d835e6b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-b3e573bdacdb712d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- OutputFormat +OutputFormt接口决定了在哪里以及怎样持久化作业结果。Hadoop为不同类型的格式提供了一系列的类和接口,实现自定义操作只要继承其中的某个类或接口即可。你可能已经熟悉了默认的OutputFormat,也就是TextOutputFormat,它是一种以行分隔,包含制表符界定的键值对的文本文件格式。尽管如此,对多数类型的数据而言,如再常见不过的数字,文本序列化会浪费一些空间,由此带来的结果是运行时间更长且资源消耗更多。为了避免文本文件的弊端,Hadoop提供了SequenceFileOutputformat,它将对象表示成二进制形式而不再是文本文件,并将结果进行压缩。 +# 3 MapReduce核心概念 +![](https://upload-images.jianshu.io/upload_images/4685968-28c6a0131a2dcc95.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-ae3ef868da0913d0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## 3.1 Split +![](https://upload-images.jianshu.io/upload_images/4685968-d6bf69ad9f81e9e4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## 3.2 InputFormat +# 4 MapReduce 1.x 架构 +![](https://upload-images.jianshu.io/upload_images/4685968-fb32aecae4a71f2f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +![](https://upload-images.jianshu.io/upload_images/4685968-decb115b06993cc7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-a282262365c87c60.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-b232e0cc860fd46d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-f4c54d1443a85677.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 5 MapReduce 2.x 架构 +![](https://upload-images.jianshu.io/upload_images/4685968-263326493524cfda.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 6 Java 实现 wordCount +![](https://upload-images.jianshu.io/upload_images/4685968-b2f88b1b8ad3d584.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![clean package](https://upload-images.jianshu.io/upload_images/4685968-d81f900ba386685c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![上传到Hadoop服务器](https://upload-images.jianshu.io/upload_images/4685968-7fb51bf009c4e2db.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![全路径没有问题](https://upload-images.jianshu.io/upload_images/4685968-4c23190abdf6b39e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-8f625f9805d6e160.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 7 重构 +![](https://upload-images.jianshu.io/upload_images/4685968-e23904523132b5f9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 8 Combiner编程 +![](https://upload-images.jianshu.io/upload_images/4685968-49b3ab702137d23e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 9 Partitoner +![](https://upload-images.jianshu.io/upload_images/4685968-bd17e7b57d287240.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-4572a64d4f8206b0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 10 JobHistoryServer + diff --git "a/\345\244\247\346\225\260\346\215\256/\345\244\247\346\225\260\346\215\256\345\205\245\351\227\250(\344\272\214) - \345\210\235\346\216\242Hadoop.md" "b/\345\244\247\346\225\260\346\215\256/\345\244\247\346\225\260\346\215\256\345\205\245\351\227\250(\344\272\214) - \345\210\235\346\216\242Hadoop.md" index 8ff98031ee..042705908f 100644 --- "a/\345\244\247\346\225\260\346\215\256/\345\244\247\346\225\260\346\215\256\345\205\245\351\227\250(\344\272\214) - \345\210\235\346\216\242Hadoop.md" +++ "b/\345\244\247\346\225\260\346\215\256/\345\244\247\346\225\260\346\215\256\345\205\245\351\227\250(\344\272\214) - \345\210\235\346\216\242Hadoop.md" @@ -2,7 +2,7 @@ ![](https://upload-images.jianshu.io/upload_images/4685968-4bbf5f7aae048c30.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 1 简介 -- 名字由来 +- 名字的由来 Hadoop这个名字不是一个缩写,而是一个虚构的名字。该项目的创建者,Doug Cutting解释Hadoop的得名 :“这个名字是我孩子给一个棕黄色的大象玩具命名的。我的命名标准就是简短,容易发音和拼写,没有太多的意义,并且不会被用于别处。小孩子恰恰是这方面的高手。” - 介绍 @@ -64,22 +64,22 @@ YARN是Yet Another Resource Negotiator的首字母缩写,意为 “另一种 从上图中可以看到,YARN可以统一调度多种不同的框架,这样不管对运维人员还是开发人员来说,都减轻了不少负担. ## 3.3 MapReduce -Hadoop核心组件之一,用于实现分布式并行计算。 +MapReduce是Hadoop核心组件之一,它用于实现分布式并行计算。Hadoop中的MapReduce源自于Google的MapReduce论文,论文发表于2004年12月。MapReduce其实就是Google MapReduce的克隆版,是Google MapReduce的开源实现。 -### 特点 +MapReduce特点: - 扩展性,如果机器的计算能力不够用,则可以以增加机器的方式来提升集群的计算能力 - 容错性,当某个计算节点的机器挂掉,会把任务分配给其他节点的机器完成 - 海量数据离线处理 -### 计算流程 - 统计词频 +下图简单展示了MapReduce的计算流程: ![](https://upload-images.jianshu.io/upload_images/4685968-2f0d9fcc6e84e6c7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -- Input环节,输入文本 -- Splitting环节,按空格分割每个单词 -- Mapping环节,相同单词都映射到同一节点 -- Shuffling环节,洗牌数据 -- Reducing环节,数据整合 -- 最终结果写入一个文件 +上图是一个统计词频的计算流程 +- 在Input环节中,我们把文本进行输入 +- 然后在Splitting环节按照空格分割每个单词 +- 接着在Mapping环节中把相同的单词都映射到同一个节点上 +- 到了Shuffling环节就会对数据进行洗牌 +- 最后到Reducing环节进行数据的整合 +- 并将最终的结果写入到一个文件中 # 4 Hadoop优势 ## 4.1 高可靠性 diff --git "a/\345\244\247\346\225\260\346\215\256/\345\244\247\346\225\260\346\215\256\345\205\245\351\227\250(\344\272\224)-\345\210\206\345\270\203\345\274\217\350\256\241\347\256\227\346\241\206\346\236\266MapReduce.md" "b/\345\244\247\346\225\260\346\215\256/\345\244\247\346\225\260\346\215\256\345\205\245\351\227\250(\344\272\224)-\345\210\206\345\270\203\345\274\217\350\256\241\347\256\227\346\241\206\346\236\266MapReduce.md" deleted file mode 100644 index 0282e5cbcc..0000000000 --- "a/\345\244\247\346\225\260\346\215\256/\345\244\247\346\225\260\346\215\256\345\205\245\351\227\250(\344\272\224)-\345\210\206\345\270\203\345\274\217\350\256\241\347\256\227\346\241\206\346\236\266MapReduce.md" +++ /dev/null @@ -1,66 +0,0 @@ -# 1 概述 -源自于Google的MapReduce论文,发表于2004年12月。 - -Hadoop MapReduce是Google MapReduce的克隆版 -## 优点 -海量数量离线处理 -易开发 -易运行 -## 缺点 -实时流式计算 - -# 2 MapReduce编程模型 -## wordcount词频统计 -![](https://img-blog.csdnimg.cn/img_convert/d30aef6db2174113c919b20226580eed.png) -# MapReduce执行流程 -- 将作业拆分成Map阶段和Reduce阶段 -- Map阶段: Map Tasks -- Reduce阶段、: Reduce Tasks -## MapReduce编程模型执行步骤 -- 准备map处理的输入数据 -- Mapper处理 -- Shuffle -- Reduce处理 -- 结果输出 -![](https://img-blog.csdnimg.cn/img_convert/b3514b6cf271ddcffe0b15c1c5a9521d.png) -- InputFormat -![](https://img-blog.csdnimg.cn/img_convert/fd11b71639ce1170689a5265dd1b525d.png) -![](https://img-blog.csdnimg.cn/img_convert/da4cd5f1a39b89744943351ea7897f71.png) -![](https://img-blog.csdnimg.cn/img_convert/1d604b7e3f0846ce6529d0f603c983d4.png) -![](https://img-blog.csdnimg.cn/img_convert/74a2e3d81bbb2a2a0781697e85a72f30.png) - -#### OutputFormat -OutputFormt接口决定了在哪里以及怎样持久化作业结果。Hadoop为不同类型的格式提供了一系列的类和接口,实现自定义操作只要继承其中的某个类或接口即可。你可能已经熟悉了默认的OutputFormat,也就是TextOutputFormat,它是一种以行分隔,包含制表符界定的键值对的文本文件格式。尽管如此,对多数类型的数据而言,如再常见不过的数字,文本序列化会浪费一些空间,由此带来的结果是运行时间更长且资源消耗更多。为了避免文本文件的弊端,Hadoop提供了SequenceFileOutputformat,它将对象表示成二进制形式而不再是文本文件,并将结果进行压缩。 -# 3 核心概念 -Split -InputFormat -OutputFormat -Combiner -Partitioner -![](https://img-blog.csdnimg.cn/img_convert/b23a2498ad2bb7feef2f9cf94f652d70.png) - -## 3.1 Split -![](https://img-blog.csdnimg.cn/img_convert/1f8b94642f966347e5fbc19579f3a8b5.png) -## 3.2 InputFormat -# 4 MapReduce 1.x 架构 -![](https://img-blog.csdnimg.cn/img_convert/978682a09b5d5e6e4c849aade6714cb3.png) - -![](https://img-blog.csdnimg.cn/img_convert/4d8cba7a246ba45e895e6b0aac5826c2.png) -![](https://img-blog.csdnimg.cn/img_convert/1358b4bbd10d5ffcf4cf4f3f28d63b64.png) -![](https://img-blog.csdnimg.cn/img_convert/040342a516484c19a2ddac1c5216ed98.png) -![](https://img-blog.csdnimg.cn/img_convert/c643cc847b1bca3a9616f6f9b7f605f5.png) -# 5 MapReduce 2.x 架构 -![](https://img-blog.csdnimg.cn/img_convert/b3760e5dff90acb65e3c7c0ae1c9f81e.png) -# 6 Java 实现 wordCount -![](https://img-blog.csdnimg.cn/img_convert/db8dbeb277b8a8b4ed7bf9dd02f6993e.png) -![clean package](https://img-blog.csdnimg.cn/img_convert/60c4b7482beb8c6d2db82e1298cda8b3.png) -![上传到Hadoop服务器](https://img-blog.csdnimg.cn/img_convert/d75de8ab33fec0b89631c4a242d9b5ea.png) -![全路径没有问题](https://img-blog.csdnimg.cn/img_convert/d7cf025e655cd15ffeac59f2b8df3d84.png) -![](https://img-blog.csdnimg.cn/img_convert/dd9815d16b7b059ee5370081c8593f6b.png) -# 7 重构 -![](https://img-blog.csdnimg.cn/img_convert/bc92c2418c746f4a52013bc7a10b4ec4.png) -# 8 Combiner编程 -![](https://img-blog.csdnimg.cn/img_convert/ed77f8d1c4a79eb349e0b816e9b4d7b3.png) -# 9 Partitoner -![](https://img-blog.csdnimg.cn/img_convert/95c49f4305150ef0987d6f6733b7ddaf.png) -![](https://img-blog.csdnimg.cn/img_convert/9cbb065c8bbba1da3972cbbe1eae4301.png) \ No newline at end of file diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/Linux/Linux\345\277\205\345\244\207\345\221\275\344\273\244\351\233\206\345\220\210.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/Linux/Linux\345\277\205\345\244\207\345\221\275\344\273\244\351\233\206\345\220\210.md" index 059eaeb8b2..ca5321a38c 100644 --- "a/\346\223\215\344\275\234\347\263\273\347\273\237/Linux/Linux\345\277\205\345\244\207\345\221\275\344\273\244\351\233\206\345\220\210.md" +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/Linux/Linux\345\277\205\345\244\207\345\221\275\344\273\244\351\233\206\345\220\210.md" @@ -814,7 +814,8 @@ lsof -i:端口号 查看服务器 8000 端口的占用情况: ## netstat -- 查看端口 +-npl +查看端口 ![](https://img-blog.csdnimg.cn/20210221125322342.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) # mv @@ -838,7 +839,7 @@ mv 目录名 文件名 :出错 再如将/usr/student下的所有文件和目录移到当前目录下,命令行为: `$ mv /usr/student/* . ` -# rm -r +#rm -r 递归删除, -f 表示 force >somefile @@ -847,10 +848,10 @@ mv 目录名 文件名 :出错 which java 查看 java 进程对应的目录 -# who +#who 显示当前用户 -# users +#users 显示当前会话 # zip -r filename.zip filesdir @@ -988,7 +989,7 @@ du ## 6.1 top 实时显示 process 的动态 -```bash +``` d : 改变显示的更新速度,或是在交谈式指令列( interactive command)按 s q : 没有任何延迟的显示速度,如果使用者是有 superuser 的权限,则 top 将会以最高的优先序执行 c : 切换显示模式,共有两种模式,一是只显示执行档的名称,另一种是显示完整的路径与名称S : 累积模式,会将己完成或消失的子行程 ( dead child process ) 的 CPU time 累积起来 @@ -1100,9 +1101,7 @@ Timer Unit:定时器 systemctl list-units命令可以查看当前系统的所有 Unit 。 -```bash netstat -nap | grep port -``` # 7 系统设置 ## 7.1 yum( Yellow dog Updater, Modified) @@ -1142,7 +1141,7 @@ yum clean, yum clean all (= yum clean packages; yum clean oldheaders) :清除缓 RPM 套件管理方式的出现,让 Linux 易于安装,升级,间接提升了 Linux 的适用度. ### 参数 -```bash +``` -a  查询所有套件。 -b<完成阶段><套件档>+或-t <完成阶段><套件档>+  设置包装套件的完成阶段,并指定套件档的文件名称。 -c  只列出组态配置文件,本参数需配合"-l"参数使用。 @@ -1376,36 +1375,17 @@ apt-get purge packagename 等同于 apt-get remove packagename --purge 配置文件只包括/etc目录中的软件服务使用的配置信息,不包括home目录中的 -## Curl -利用URL规则在命令行下工作的文件传输工具。支持文件的上传和下载,所以是综合传输工具,但按传统,习惯称curl为下载工具。 -curl支持包括HTTP、HTTPS、ftp等众多协议,还支持POST、cookies、认证、从指定偏移处下载部分文件、用户代理字符串、限速、文件大小、进度条等特征。 -适合网页处理流程和数据检索自动化。 -### 语法 -```clike -curl (选项) (参数) -``` -### 参数 -```clike --i/I 通过-I或-head可只打印HTTP头部信息,-i会带上网页内容 --X/--request 指定什么命令 -``` -![](https://img-blog.csdnimg.cn/7dc8964b172348378d4d059e8b0603e3.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/f11f36b1dd084268a3b707c3ab8c6c9d.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16)![](https://img-blog.csdnimg.cn/728954e03c5549bdbb9fec674a244c1b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - - -# 8 Linux常见问题解决方案 -## Repository list的更新日期久远 +# 8 Linux常见问题与解决方案 +## 1 Repository list的更新日期久远 - yum判断你上次更新repository list的时间太久远(2周以上),所以不让使用者操作yum,以避免安装到旧的软件包! ![](https://img-blog.csdnimg.cn/img_convert/986a2dea2586477e92e164ed1a568cbd.png) -### 解决方案 + +## 解决方案 - 清除yum repository缓存 ![sudo yum clean all](https://img-blog.csdnimg.cn/img_convert/7ad03eae3d6ee2db4e73d786c162244f.png) -## 有些工具安装之后,除了要修改root下的.bashfile(即加环境变量) ,还要修改etc/profile, 两个profile是干什么用的?有啥区别? -- /etc/profile -每个用户登录时都会运行的环境变量设置,属于系统级别的环境变量,其设置对所有用户适用 -- .bashfile -单用户登录时比如root会运行的,只适用于当前用户,且只有在你使用的也是bash作为shell时才行。 +## 问题:linux下有些工具安装之后,除了要修改root下的.bashfile(也就是添加个环境变量) ,还要修改etc/profile 下的环境变量 , 两个profile是干什么用的?区别? -rpm是red hat、fedora、centos这几个发行版使用的安装包,和其它tar.gz的区别是有个文件头,多了一些信息。rpm包多数是二进制文件,可以直接运行的,但tar.gz包很多是源代码,要编译后才能运行。 -二进制文件和windows下的exe文件一个意思,可以直接运行。 \ No newline at end of file +解答: +- /etc/profile :这个文件是每个用户登录时都会运行的环境变量设置,属于系统级别的环境变量,设置在里 面的东西对所有用户适用 +- .bashfile 是单用户登录时比如root会运行的,只对当前用户适用,而且只有在你使用的也是bash作为shell时才行. rpm是red hat,fedora,centos这几个发行版使用的安装包,和其它tar.gz的区别是有个文件头,多了一些信息。 rpm包多数是二进制文件,可以直接运行的,但tar.gz包很多是源代码,要编译后才能运行。 二进制文件和windows下的exe文件一个意思,可以直接运行。 \ No newline at end of file diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/Linux/\344\270\200\346\226\207\346\220\236\346\207\202select\343\200\201poll\345\222\214epoll\345\214\272\345\210\253.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/Linux/\344\270\200\346\226\207\346\220\236\346\207\202select\343\200\201poll\345\222\214epoll\345\214\272\345\210\253.md" deleted file mode 100644 index 743dcb131b..0000000000 --- "a/\346\223\215\344\275\234\347\263\273\347\273\237/Linux/\344\270\200\346\226\207\346\220\236\346\207\202select\343\200\201poll\345\222\214epoll\345\214\272\345\210\253.md" +++ /dev/null @@ -1,375 +0,0 @@ -# 1 select -select本质上是通过设置或检查存放fd标志位的数据结构进行下一步处理。 -这带来缺点: -- 单个进程可监视的fd数量被限制,即能监听端口的数量有限 -单个进程所能打开的最大连接数有`FD_SETSIZE`宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试 -一般该数和系统内存关系很大,具体数目可以`cat /proc/sys/fs/file-max`察看。32位机默认1024个,64位默认2048。 -![](https://img-blog.csdnimg.cn/20201103231954394.png#pic_center) - -- 对socket是线性扫描,即轮询,效率较低: -仅知道有I/O事件发生,却不知是哪几个流,只会无差异轮询所有流,找出能读数据或写数据的流进行操作。同时处理的流越多,无差别轮询时间越长 - O(n)。 - - -当socket较多时,每次select都要通过遍历`FD_SETSIZE`个socket,不管是否活跃,这会浪费很多CPU时间。如果能给 socket 注册某个回调函数,当他们活跃时,自动完成相关操作,即可避免轮询,这就是**epoll**与**kqueue**。 - -## 1.1 调用过程 -![](https://img-blog.csdnimg.cn/20201103234504233.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -```c -asmlinkage long sys_poll(struct pollfd * ufds, unsigned int nfds, long timeout) -{ - int i, j, fdcount, err; - struct pollfd **fds; - struct poll_wqueues table, *wait; - int nchunks, nleft; - - /* Do a sanity check on nfds ... */ - if (nfds > NR_OPEN) - return -EINVAL; - - if (timeout) { - /* Careful about overflow in the intermediate values */ - if ((unsigned long) timeout < MAX_SCHEDULE_TIMEOUT / HZ) - timeout = (unsigned long)(timeout*HZ+999)/1000+1; - else /* Negative or overflow */ - timeout = MAX_SCHEDULE_TIMEOUT; - } - // 2. 注册回调函数__pollwait - poll_initwait(&table); - wait = &table; - if (!timeout) - wait = NULL; - - err = -ENOMEM; - fds = NULL; - if (nfds != 0) { - fds = (struct pollfd **)kmalloc( - (1 + (nfds - 1) / POLLFD_PER_PAGE) * sizeof(struct pollfd *), - GFP_KERNEL); - if (fds == NULL) - goto out; - } - - nchunks = 0; - nleft = nfds; - while (nleft > POLLFD_PER_PAGE) { /* allocate complete PAGE_SIZE chunks */ - fds[nchunks] = (struct pollfd *)__get_free_page(GFP_KERNEL); - if (fds[nchunks] == NULL) - goto out_fds; - nchunks++; - nleft -= POLLFD_PER_PAGE; - } - if (nleft) { /* allocate last PAGE_SIZE chunk, only nleft elements used */ - fds[nchunks] = (struct pollfd *)__get_free_page(GFP_KERNEL); - if (fds[nchunks] == NULL) - goto out_fds; - } - - err = -EFAULT; - for (i=0; i < nchunks; i++) - // - if (copy_from_user(fds[i], ufds + i*POLLFD_PER_PAGE, PAGE_SIZE)) - goto out_fds1; - if (nleft) { - if (copy_from_user(fds[nchunks], ufds + nchunks*POLLFD_PER_PAGE, - nleft * sizeof(struct pollfd))) - goto out_fds1; - } - - fdcount = do_poll(nfds, nchunks, nleft, fds, wait, timeout); - - /* OK, now copy the revents fields back to user space. */ - for(i=0; i < nchunks; i++) - for (j=0; j < POLLFD_PER_PAGE; j++, ufds++) - __put_user((fds[i] + j)->revents, &ufds->revents); - if (nleft) - for (j=0; j < nleft; j++, ufds++) - __put_user((fds[nchunks] + j)->revents, &ufds->revents); - - err = fdcount; - if (!fdcount && signal_pending(current)) - err = -EINTR; - -out_fds1: - if (nleft) - free_page((unsigned long)(fds[nchunks])); -out_fds: - for (i=0; i < nchunks; i++) - free_page((unsigned long)(fds[i])); - if (nfds != 0) - kfree(fds); -out: - poll_freewait(&table); - return err; -} -``` -```c -static int do_poll(unsigned int nfds, unsigned int nchunks, unsigned int nleft, - struct pollfd *fds[], struct poll_wqueues *wait, long timeout) -{ - int count; - poll_table* pt = &wait->pt; - - for (;;) { - unsigned int i; - - set_current_state(TASK_INTERRUPTIBLE); - count = 0; - for (i=0; i < nchunks; i++) - do_pollfd(POLLFD_PER_PAGE, fds[i], &pt, &count); - if (nleft) - do_pollfd(nleft, fds[nchunks], &pt, &count); - pt = NULL; - if (count || !timeout || signal_pending(current)) - break; - count = wait->error; - if (count) - break; - timeout = schedule_timeout(timeout); - } - current->state = TASK_RUNNING; - return count; -} -``` - -1. 使用copy_from_user从用户空间拷贝fd_set到内核空间 -2. 注册回调函数`__pollwait` -![](https://img-blog.csdnimg.cn/908c942c09594f048dc2bebc05608ff9.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -3. 遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或datagram_poll) -4. 以tcp_poll为例,核心实现就是`__pollwait`,即上面注册的回调函数 -5. `__pollwait`,就是把current(当前进程)挂到设备的等待队列,不同设备有不同等待队列,如tcp_poll的等待队列是sk->sk_sleep(把进程挂到等待队列中并不代表进程已睡眠)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒。 -```c -void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *_p) -{ - struct poll_wqueues *p = container_of(_p, struct poll_wqueues, pt); - struct poll_table_page *table = p->table; - - if (!table || POLL_TABLE_FULL(table)) { - struct poll_table_page *new_table; - - new_table = (struct poll_table_page *) __get_free_page(GFP_KERNEL); - if (!new_table) { - p->error = -ENOMEM; - __set_current_state(TASK_RUNNING); - return; - } - new_table->entry = new_table->entries; - new_table->next = table; - p->table = new_table; - table = new_table; - } - - /* 添加新节点 */ - { - struct poll_table_entry * entry = table->entry; - table->entry = entry+1; - get_file(filp); - entry->filp = filp; - entry->wait_address = wait_address; - init_waitqueue_entry(&entry->wait, current); - add_wait_queue(wait_address,&entry->wait); - } -} -``` -```c -static void do_pollfd(unsigned int num, struct pollfd * fdpage, - poll_table ** pwait, int *count) -{ - int i; - - for (i = 0; i < num; i++) { - int fd; - unsigned int mask; - struct pollfd *fdp; - - mask = 0; - fdp = fdpage+i; - fd = fdp->fd; - if (fd >= 0) { - struct file * file = fget(fd); - mask = POLLNVAL; - if (file != NULL) { - mask = DEFAULT_POLLMASK; - if (file->f_op && file->f_op->poll) - mask = file->f_op->poll(file, *pwait); - mask &= fdp->events | POLLERR | POLLHUP; - fput(file); - } - if (mask) { - *pwait = NULL; - (*count)++; - } - } - fdp->revents = mask; - } -} -``` -6. poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值 -7. 若遍历完所有fd,还没返回一个可读写的mask掩码,则调schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。若超过一定超时时间(schedule_timeout指定),还没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有无就绪的fd -8. 把fd_set从内核空间拷贝到用户空间 -## 1.2 缺点 -内核需要将消息传递到用户空间,都需要内核拷贝动作。需要维护一个用来存放大量fd的数据结构,使得用户空间和内核空间在传递该结构时复制开销大。 - -- 每次调用select,都需把fd集合从用户态拷贝到内核态,fd很多时开销就很大 -- 同时每次调用select都需在内核遍历传递进来的所有fd,fd很多时开销就很大 -- select支持的文件描述符数量太小了,默认最大支持1024个 -- 主动轮询效率很低 -# 2 poll -和select类似,只是描述fd集合的方式不同,poll使用`pollfd`结构而非select的`fd_set`结构。 -管理多个描述符也是进行轮询,根据描述符的状态进行处理,但**poll没有最大文件描述符数量的限制**。 - -poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。 -- 它将用户传入的数组拷贝到内核空间 -- 然后查询每个fd对应的设备状态: - - 如果设备就绪 -在设备等待队列中加入一项继续遍历 - - 若遍历完所有fd后,都没发现就绪的设备 -挂起当前进程,直到设备就绪或主动超时,被唤醒后它又再次遍历fd。这个过程经历多次无意义的遍历。 - -没有最大连接数限制,因其基于链表存储,其缺点: -- 大量fd数组被整体复制于用户态和内核地址空间间,而不管是否有意义 -- 如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd - -所以又有了epoll模型。 -# 3 epoll -epoll模型修改主动轮询为被动通知,当有事件发生时,被动接收通知。所以epoll模型注册套接字后,主程序可做其他事情,当事件发生时,接收到通知后再去处理。 - -可理解为**event poll**,epoll会把哪个流发生哪种I/O事件通知我们。所以epoll是事件驱动(每个事件关联fd),此时我们对这些流的操作都是有意义的。复杂度也降到O(1)。 -```c -asmlinkage int sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) -{ - int error; - struct file *file, *tfile; - struct eventpoll *ep; - struct epitem *epi; - struct epoll_event epds; - - error = -EFAULT; - if (copy_from_user(&epds, event, sizeof(struct epoll_event))) - goto eexit_1; - - /* Get the "struct file *" for the eventpoll file */ - error = -EBADF; - file = fget(epfd); - if (!file) - goto eexit_1; - - /* Get the "struct file *" for the target file */ - tfile = fget(fd); - if (!tfile) - goto eexit_2; - - /* The target file descriptor must support poll */ - error = -EPERM; - if (!tfile->f_op || !tfile->f_op->poll) - goto eexit_3; - - /* - * We have to check that the file structure underneath the file descriptor - * the user passed to us _is_ an eventpoll file. And also we do not permit - * adding an epoll file descriptor inside itself. - */ - error = -EINVAL; - if (file == tfile || !IS_FILE_EPOLL(file)) - goto eexit_3; - - /* - * At this point it is safe to assume that the "private_data" contains - * our own data structure. - */ - ep = file->private_data; - - /* - * Try to lookup the file inside our hash table. When an item is found - * ep_find() increases the usage count of the item so that it won't - * desappear underneath us. The only thing that might happen, if someone - * tries very hard, is a double insertion of the same file descriptor. - * This does not rapresent a problem though and we don't really want - * to put an extra syncronization object to deal with this harmless condition. - */ - epi = ep_find(ep, tfile); - - error = -EINVAL; - switch (op) { - case EPOLL_CTL_ADD: - if (!epi) { - epds.events |= POLLERR | POLLHUP; - - error = ep_insert(ep, &epds, tfile); - } else - error = -EEXIST; - break; - case EPOLL_CTL_DEL: - if (epi) - error = ep_remove(ep, epi); - else - error = -ENOENT; - break; - case EPOLL_CTL_MOD: - if (epi) { - epds.events |= POLLERR | POLLHUP; - error = ep_modify(ep, epi, &epds); - } else - error = -ENOENT; - break; - } - - /* - * The function ep_find() increments the usage count of the structure - * so, if this is not NULL, we need to release it. - */ - if (epi) - ep_release_epitem(epi); - -eexit_3: - fput(tfile); -eexit_2: - fput(file); -eexit_1: - DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_ctl(%d, %d, %d, %u) = %d\n", - current, epfd, op, fd, event->events, error)); - - return error; -} - -``` - -## 3.1 触发模式 -**EPOLLLT**和**EPOLLET**两种: - -- LT,默认的模式(水平触发) -只要该fd还有数据可读,每次 `epoll_wait` 都会返回它的事件,提醒用户程序去操作, -- ET是“高速”模式(边缘触发) -![](https://img-blog.csdnimg.cn/20201103232957391.png#pic_center) -只会提示一次,直到下次再有数据流入之前都不会再提示,无论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读完,即读到read返回值小于请求值或遇到EAGAIN错误 - -epoll使用“事件”的就绪通知方式,通过`epoll_ctl`注册fd,一旦该fd就绪,内核就会采用类似回调机制激活该fd,`epoll_wait`便可收到通知。 -### EPOLLET触发模式的意义 -若用`EPOLLLT`,系统中一旦有大量无需读写的就绪文件描述符,它们每次调用`epoll_wait`都会返回,这大大降低处理程序检索自己关心的就绪文件描述符的效率。 -而采用`EPOLLET`,当被监控的文件描述符上有可读写事件发生时,`epoll_wait`会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用`epoll_wait`时,它不会通知你,即只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。这比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。 - -## 3.2 优点 -- 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口) -- 效率提升,不是轮询,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数 -即Epoll最大的优点就在于它只关心“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll -- 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。 -- epoll通过内核和用户空间共享一块内存来实现的 - -表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。 - -epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现。 - -select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。 -- 对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。 -- 对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。 -- 对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。 -# 4 总结 -select,poll,epoll都是IO多路复用机制,即可以监视多个描述符,一旦某个描述符就绪(读或写就绪),能够通知程序进行相应读写操作。 -但select,poll,epoll本质上都是**同步I/O**,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。 - -- select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。 - -- select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。 - -> 参考 -> - Linux下select/poll/epoll机制的比较 -> - https://www.cnblogs.com/anker/p/3265058.html \ No newline at end of file diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\346\214\207\344\273\244\345\222\214\350\277\220\347\256\227\346\230\257\345\246\202\344\275\225\345\215\217\344\275\234\346\236\204\346\210\220CPU\347\232\204\357\274\237.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\346\214\207\344\273\244\345\222\214\350\277\220\347\256\227\346\230\257\345\246\202\344\275\225\345\215\217\344\275\234\346\236\204\346\210\220CPU\347\232\204\357\274\237.md" deleted file mode 100644 index 5b4462873b..0000000000 --- "a/\346\223\215\344\275\234\347\263\273\347\273\237/\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\346\214\207\344\273\244\345\222\214\350\277\220\347\256\227\346\230\257\345\246\202\344\275\225\345\215\217\344\275\234\346\236\204\346\210\220CPU\347\232\204\357\274\237.md" +++ /dev/null @@ -1,85 +0,0 @@ -连通“指令”和“计算”这两大功能,才能构建完整的CPU。 -# 1 指令周期(Instruction Cycle) -计算机每执行一条指令的过程,可分解为如下步骤: -1. Fetch(取指令) -指令放在存储器,通过PC寄存器和指令寄存器取出指令的过程,由控制器(Control Unit)操作。 -从PC寄存器找到对应指令地址,据指令地址从内存把具体指令加载到指令寄存器,然后PC寄存器自增 -2. Decode(指令译码) -据指令寄存器里面的指令,解析成要进行何操作,是R、I、J中的哪一种指令,具体要操作哪些寄存器、数据或内存地址。该阶段也是由控制器执行。 -3. Execute(执行指令) -实际运行对应的R、I、J这些特定的指令,进行算术逻辑操作、数据传输或者直接的地址跳转。无论是算术操作、逻辑操作的R型指令,还是数据传输、条件分支的I型指令,都由算术逻辑单元(ALU)操作,即由运算器处理。 -如果是一个简单的无条件地址跳转,那可直接在控制器里完成,无需运算器。 -4. 重复1~3 - -这就是个永动机般的“FDE”循环,即指令周期。 -![](https://img-blog.csdnimg.cn/171cff94c628488b823513f4e38973eb.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -CPU还有两个Cycle: -## Machine Cycle,机器周期或者CPU周期 -CPU内部操作速度很快,但访问内存速度却慢很多。 -每条指令都需要从内存里面加载而来,所以一般把从内存里面读取一条指令的最短时间,称为CPU周期。 -## Clock Cycle,时钟周期及机器主频 -一个CPU周期,通常由几个时钟周期累积。一个CPU周期时间,就是这几个Clock Cycle总和。 - -对于一个指令周期,取出一条指令,然后执行它,至少需两个CPU周期: -- 取出指令,至少得一个CPU周期 -- 执行指令,至少也得一个CPU周期 -因为执行完的结果,还要写回内存 -## 三个周期(Cycle)之间的关系 -![](https://img-blog.csdnimg.cn/9d86b21db1494646a5fcde3bd86fb75d.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -一个指令周期,包含多个CPU周期,而一个CPU周期包含多个时钟周期。 -# 2 建立数据通路 -名字是什么其实并不重要,一般可以认为,数据通路就是我们的处理器单元,通常由两类原件组成: -- 操作元件,也叫组合逻辑元件(Combinational Element),就是ALU -在特定的输入下,根据下面的组合电路的逻辑,生成特定的输出。 -- 存储元件,也叫状态元件(State Element) -如在计算过程中要用到的寄存器,无论是通用寄存器还是状态寄存器,都是存储元件。 - -通过数据总线把它们连接起来,就可完成数据存储、处理和传输,即建立了数据通路。 -## 控制器 -可以把它看成只是机械地重复“Fetch - Decode - Execute“循环中的前两个步骤,然后把最后一个步骤,通过控制器产生的控制信号,交给ALU去处理。 -### 控制器将CPU指令解析成不同输出信号 -目前Intel CPU支持2000个以上指令。说明控制器输出的控制信号,至少有2000种不同组合。 - -运算器里的ALU和各种组合逻辑电路,可认为是一个**固定功能的电路**。 -控制器“翻译”出来的,就是不同控制信号,告诉ALU去做不同计算。正是控制器,才让我们能“编程”实现功能,才铸就了“存储程序型计算机”。 -- 指令译码器将输入的机器码,解析成不同操作码、操作数,然后传输给ALU计算 -![](https://img-blog.csdnimg.cn/cb0596de354c42baad326015b7a6f151.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -# 3 CPU对硬件电路的要求 -搭建CPU,还得在数字电路层面,实现如下功能。 -## ALU -就是个无状态,根据输入计算输出结果的第一个电路。 -## 支持状态读写的电路元件 - 寄存器 -要有电路能存储上次计算结果。 - -该计算结果不一定要立刻给下游电路使用,但可在需要时直接用。常见支持状态读写的电路: -- 锁存器(Latch) -- D触发器(Data/Delay Flip-flop)电路 -## 自动”电路,按固定周期实现PC寄存器自增 -自动执行Fetch - Decode - Execute。 - -我们的程序执行,并非靠人工拨动开关执行指令。得有个“自动”电路,无休止执行一条条指令。 - -看似复杂的各种函数调用、条件跳转,只是修改了PC寄存器保存的地址。PC寄存器里面的地址一修改,计算机即可加载一条指令新指令,往下运行。 -PC寄存器还叫程序计数器,随时间变化,不断计数。数字变大了,就去执行一条新指令。所以,我们需要的就是个自动计数的电路。 -## 译码电路 -无论是decode指令,还是对于拿到的内存地址去获取对应的数据或者指令,都要通过一个电路找到对应数据,就是“译码器”电路。 - -把这四类电路,通过各种方式组合在一起就能组成CPU。要实现这四种电路中的中间两种,我们还需要时钟电路的配合。下一节,我们一起来看一看,这些基础的电路功能是怎么实现的,以及怎么把这些电路组合起来变成一个CPU。 -# 总结 -至此,CPU运转所需的数据通路和控制器介绍完了,也找出完成这些功能,需要的4种基本电路: -- ALU这样的组合逻辑电路 -- 存储数据的锁存器和D触发器电路 -- 实现PC寄存器的计数器电路 -- 解码和寻址的译码器电路 - -> CPU 好像一个永不停歇的机器,一直在不停地读取下一条指令去运行。那为什么 CPU 还会有满载运行和 Idle 闲置的状态呢? - -操作系统内核有 idle 进程,优先级最低,仅当其他进程都阻塞时被调度器选中。idle 进程循环执行 HLT 指令,关闭 CPU 大部分功能以降低功耗,收到中断信号时 CPU 恢复正常状态。 CPU在空闲状态就会停止执行,即切断时钟信号,CPU主频会瞬间降低为0,功耗也会瞬间降为0。由于这个空闲状态是十分短暂的,所以你在任务管理器也只会看到CPU频率下降,不会看到降为0。 当CPU从空闲状态中恢复时,就会接通时钟信号,CPU频率就会上升。所以你会在任务管理器里面看到CPU的频率起伏变化。 - -uptime 命令查看平均负载 -![](https://img-blog.csdnimg.cn/ebdc106681da4dba974a91c1147092a5.png) -满载运行就是平均负载为1.0(一个一核心CPU),定义为特定时间间隔内运行队列中的平均线程数。 -load average 表示机器一段时间内的平均load,越低越好。过高可能导致机器无法处理其他请求及操作,甚至死机。 - -当CUP执行完当前系统分配的任务,为省电,系统将执行空闲任务(idle task),该任务循环执行HLT指令,CPU就会停止指令的执行,且让CPU处于HALT状态,CPU虽停止指令执行,且CPU部分功能模块将会被关闭(以低功耗),但CPU的LAPIC(Local Advanced Programmable Interrupt Controller)并不会停止工作,即CPU将会继续接收外部中断、异常等外部事件(事实上,CPU HALT状态的退出将由外部事件触发)。“Idle 闲置”是一种低功耗的状态,cpu在执行最低功耗的循环指令。实际上并非啥都没干,而是一直在干最最轻松的事儿。 -当CPU接收到这些外部事件,将会从HALT状态恢复,执行中断服务函数,且当中断服务函数执行完毕后,指令寄存器(CS:EIP)将会指向HLT指令的下一条指令,即CPU继续执行HLT指令之后的程序。 \ No newline at end of file diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206\345\255\246\344\271\240\350\267\257\347\272\277.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206\345\255\246\344\271\240\350\267\257\347\272\277.md" index 0e977b1fb7..e3882943c1 100644 --- "a/\346\223\215\344\275\234\347\263\273\347\273\237/\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206\345\255\246\344\271\240\350\267\257\347\272\277.md" +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206\345\255\246\344\271\240\350\267\257\347\272\277.md" @@ -1,8 +1,10 @@ -计算机组成原理,Computer Organization。Organization 意"组织机构"。 +计算机组成原理的英文叫Computer Organization + +Organization 意"组织机构"。 该组织机构能够进行各种计算、控制、读取输入,进行输出,达成各种强大的功能。 -计算机组成原理知识点大致可分为: +把整个计算机组成原理的知识点拆分成了四大部分 - 计算机的基本组成 - 计算机的指令和计算 diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\351\233\266\346\213\267\350\264\235\357\274\210Zero Copy\357\274\211\346\212\200\346\234\257\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210\357\274\237.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\351\233\266\346\213\267\350\264\235\357\274\210Zero Copy\357\274\211\346\212\200\346\234\257\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210\357\274\237.md" deleted file mode 100644 index ba67fb6257..0000000000 --- "a/\346\223\215\344\275\234\347\263\273\347\273\237/\351\233\266\346\213\267\350\264\235\357\274\210Zero Copy\357\274\211\346\212\200\346\234\257\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210\357\274\237.md" +++ /dev/null @@ -1,25 +0,0 @@ -rabbitmq 这么高吞吐量都是因为零拷贝技术,本文以 kafka 为例讲解。 - -消息从发送到落地保存,broker 维护的消息日志本身就是文件目录,每个文件都是二进制保存,生产者和消费者使用相同格式来处理。 - -Con获取消息时,服务器先从硬盘读取数据到内存,然后将内存中数据通过 socket 发给Con。 - -Linux的零拷贝技术:当数据在磁盘和网络之间传输时,避免昂贵的内核态数据拷贝,从而实现快速数据传输。 -Linux平台实现了这样的零拷贝机制,但Windows必须要到Java 8的60更新版本才能“享受”到。 -![](https://img-blog.csdnimg.cn/img_convert/74e0931f6470ebdcadd5ed9fbd8897f2.png) -- os将数据从磁盘读入到内核空间的页缓存 -- 应用程序将数据从内核空间读入到用户空间缓存 -- 应用程序将数据写回到内核空间到 socket 缓存 -- os将数据从 socket 缓冲区复制到网卡缓冲区,以便将数据经网络发出 - -该过程涉及: -- 4 次上下文切换 -- 4 次数据复制 -有两次复制操作是 CPU 完成的。但该过程中,数据完全无变化,仅是从磁盘复制到网卡缓冲区。 - -而通过“零拷贝”技术,能去掉这些没必要的数据复制操作, 也减少了上下文切换次数。 -现代的 unix 操作系统提供一个优化的代码路径,将数据从页缓存传输到 socket; -在 Linux 中,是通过 sendfile 系统调用完成的。Java 提供了访问这个系统调用的方法:**FileChannel.transferTo** API -![](https://img-blog.csdnimg.cn/img_convert/633eeab1746ea396f2d67bab8fb3fbc2.png) -使用 sendfile,只需一次拷贝,允许os将数据直接从页缓存发送到网络。 -所以在这个优化的路径中, 只有最后一步:将数据拷贝到网卡缓存中是必须的。 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(7)-\344\274\230\351\233\205\345\234\260\351\203\250\347\275\262 Kafka \351\233\206\347\276\244.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(7)-\344\274\230\351\233\205\345\234\260\351\203\250\347\275\262 Kafka \351\233\206\347\276\244.md" deleted file mode 100644 index e1f5682e02..0000000000 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(7)-\344\274\230\351\233\205\345\234\260\351\203\250\347\275\262 Kafka \351\233\206\347\276\244.md" +++ /dev/null @@ -1,109 +0,0 @@ -生产环境需考量各种因素,结合自身业务需求而制定。看一些考虑因素(以下顺序,可是分了顺序的哦) -# 1 OS -- Kafka不是JVM上的中间件吗?Java又是跨平台语言,把Kafka安装到不同的os有啥区别吗? -区别相当大! - -Kafka的确由Scala/Java编写,编译后源码就是“.class”文件。部署到啥OS应该一样,但毋庸置疑,部署在Linux上的生产环境是最多的,具体原因你能谈笑风生吗? -## 1.1 I/O模型 -I/O模型其实就是os执行I/O指令的方法,主流的I/O模型通常有5种类型: -1. 阻塞式I/O -e.g. Java中Socket的阻塞模式 -2. 非阻塞式I/O -e.g. Java中Socket的非阻塞模式 -3. I/O多路复用 -e.g. Linux中的系统调用`select`函数 -4. 信号驱动I/O -e.g. epoll系统调用则介于第三种和第四种模型之间 -5. 异步I/O -e.g. 很少有Linux支持,反而Windows系统提供了一个叫IOCP线程模型属于该类 - -I/O模型与Kafka的关系几何?Kafka Client 底层使用了Java的selector,而selector -- 在Linux上的实现机制是epoll -- 在Windows平台上的实现机制是select - -因为这点,Kafka部署在Linux上更有优势,能获得更高效的I/O性能。 -## 1.2 数据网络传输效率 -- Kafka生产和消费的消息都是通过网络传输,但消息保存在哪呢? -肯定是磁盘! - -故Kafka需在磁盘和网络间进行大量数据传输。在Linux部署Kafka能够享受到零拷贝技术带来的快速数据传输特性。 -## 1.3 社区生态 -社区对Windows平台上发现的Kafka Bug不做任何承诺。 -# 2 磁盘 -## 2.1 机械硬盘 or SSD -- 前者便宜且容量大,但易坏! -- 后者性能优势大,但是贵! - -建议是使用普通机械硬盘即可。 -- Kafka虽然大量使用磁盘,可多是顺序读写操作,一定程度规避了机械磁盘最大的劣势,即随机读写慢。从这一点上来说,使用SSD并没有太大性能优势,机械磁盘物美价廉 -- 而它因易损坏而造成的可靠性差等缺陷,又由Kafka在软件层面提供机制来保证 -## 2.2 是否应该使用磁盘阵列(RAID) -使用RAID的主要优势: -- 提供冗余的磁盘存储空间 -- 提供负载均衡 - -对于Kafka -- Kafka自己实现了冗余机制,提供高可靠性 -- 通过分区设计,也能在软件层面自行实现负载均衡 - -RAID优势也就没有那么明显了。虽然实际上依然有很多大厂确实是把Kafka底层的存储交由RAID,只是目前Kafka在存储这方面提供了越来越便捷的高可靠性方案,因此在线上环境使用RAID似乎变得不是那么重要了。 - -综上,追求性价比的公司可不搭建RAID,使用普通磁盘组成存储空间即可。使用机械磁盘完全能够胜任Kafka线上环境。 -## 2.3 磁盘容量 -集群到底需要多大? -Kafka需要将消息保存在磁盘上,这些消息默认会被保存一段时间然后自动被删除。 -虽然这段时间是可以配置的,但你应该如何结合自身业务场景和存储需求来规划Kafka集群的存储容量呢? - -假设有个业务 -- 每天需要向Kafka集群发送1亿条消息 -- 每条消息保存两份以防止数据丢失 -- 消息默认保存两周时间 - -现在假设消息的平均大小是1KB,那么你能说出你的Kafka集群需要为这个业务预留多少磁盘空间吗? - -计算: -- 每天1亿条1KB的消息,存两份 -`1亿 * 1KB * 2 / 1000 / 1000 = 200GB` - -- 一般Kafka集群除消息数据还存其他类型数据,比如索引数据 -再为其预留10%磁盘空间,因此总的存储容量就是220GB - -- 要存两周,那么整体容量即为 -220GB * 14,大约3TB -- Kafka支持数据的压缩,假设压缩比是0.75 -那么最后规划的存储空间就是0.75 * 3 = 2.25TB - -总之在规划磁盘容量时你需要考虑下面这几个元素: -- 新增消息数 -- 消息留存时间 -- 平均消息大小 -- 备份数 -- 是否启用压缩 -# 3 带宽 -对于Kafka这种通过网络进行大数据传输的框架,带宽易成为瓶颈。 - -普通以太网络,带宽主要有两种: -- 1Gbps的千兆网络 -- 10Gbps的万兆网络 - -以千兆网络为例,说明带宽资源规划。真正要规划的是所需的Kafka服务器的数量。假设机房环境是千兆网络,即1Gbps,现在有业务,其目标或SLA是在1小时内处理1TB的业务数据。 - -到底需要多少台Kafka服务器来完成这个业务呢? -### 计算 -带宽1Gbps,即每秒处理1Gb数据 -假设每台Kafka服务器都是安装在专属机器,即每台Kafka机器上没有混入其他服务 -通常情况下你只能假设Kafka会用到70%的带宽资源,因为总要为其他应用或进程留一些资源。超过70%的阈值就有网络丢包可能性,故70%的设定是一个比较合理的值,也就是说单台Kafka服务器最多也就能使用大约700Mb带宽。 - -这只是它能使用的最大带宽资源,你不能让Kafka服务器常规性使用这么多资源,故通常要再额外预留出2/3的资源,即 -`单台服务器使用带宽700Mb / 3 ≈ 240Mbps` -这里的2/3其实是相当保守的,可以结合机器使用情况酌情减少该值 - -有了240Mbps,可以计算1小时内处理1TB数据所需的服务器数量了。 -根据这个目标,每秒需要处理2336Mb的数据,除以240,约等于10台服务器。 -如果消息还需要额外复制两份,那么总的服务器台数还要乘以3,即30台。 -# 总结 -部署Kafka环境,一开始就要思考好实际场景下业务所需的集群环境,不能仅从单个维度上进行评估。 - -> 参考 -> - Linux内核模型架构 -> - Kafka核心技术与实战 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(\344\270\203) - \344\274\230\351\233\205\345\234\260\351\203\250\347\275\262 Kafka \351\233\206\347\276\244.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(\344\270\203) - \344\274\230\351\233\205\345\234\260\351\203\250\347\275\262 Kafka \351\233\206\347\276\244.md" new file mode 100644 index 0000000000..7d79f7f122 --- /dev/null +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(\344\270\203) - \344\274\230\351\233\205\345\234\260\351\203\250\347\275\262 Kafka \351\233\206\347\276\244.md" @@ -0,0 +1,127 @@ +既然是集群,必然有多个Kafka节点,只有单节点构成的Kafka伪集群只能用于日常测试,不可能满足线上生产需求。 +真正的线上环境需要考量各种因素,结合自身的业务需求而制定。看一些考虑因素(以下顺序,可是分了顺序的哦) + +# 1 操作系统 - OS +可能你会问Kafka不是JVM上的大数据框架吗?Java又是跨平台的语言,把Kafka安装到不同的操作系统上会有什么区别吗? +区别相当大! + +确实,Kafka由Scala/Java编写,编译后源码就是“.class”文件。 +本来部署到哪个OS应该一样,但是不同OS的差异还是给Kafka集群带来了相当大的影响。 +毋庸置疑,部署在Linux上的生产环境是最多的。 + +考虑操作系统与Kafka的适配性,Linux系统显然要比其他两个特别是Windows系统更加适合部署Kafka。可具体原因你能谈笑风生吗? +## 1.1 I/O模型 +I/O模型可以近似认为I/O模型就是OS执行I/O指令的方法。 +主流的I/O模型通常有5种类型: +1. 阻塞式I/O +e.g. Java中Socket的阻塞模式 +2. 非阻塞式I/O +e.g. Java中Socket的非阻塞模式 +3. I/O多路复用 +e.g. Linux中的系统调用`select`函数 +4. 信号驱动I/O +e.g. epoll系统调用则介于第三种和第四种模型之间 +5. 异步I/O +e.g. 很少有Linux支持,反而Windows系统提供了一个叫IOCP线程模型属于该类 + +我在这里不详细展开每一种模型的实现细节,因为那不是本文重点。 + +言归正传,I/O模型与Kafka的关系几何? +Kafka Client 底层使用了Java的selector,而selector +- 在Linux上的实现机制是epoll +- 在Windows平台上的实现机制是select + +因此在这一点上将Kafka部署在Linux上是有优势的,能够获得更高效的I/O性能。 +## 1.2 数据网络传输效率 +Kafka生产和消费的消息都是通过网络传输的,而消息保存在哪里呢? +肯定是磁盘! +故Kafka需要在磁盘和网络间进行大量数据传输。 +Linux有个零拷贝(Zero Copy)技术,就是当数据在磁盘和网络进行传输时避免昂贵内核态数据拷贝从而实现快速数据传输。Linux平台实现了这样的零拷贝机制,但有些令人遗憾的是在Windows平台上必须要等到Java 8的60更新版本才能“享受”到。 + +一句话,在Linux部署Kafka能够享受到零拷贝技术所带来的快速数据传输特性带来的极致快感。 + +## 1.3 社区生态 +社区目前对Windows平台上发现的Kafka Bug不做任何承诺。因此,Windows平台上部署Kafka只适合于个人测试或用于功能验证,千万不要应用于生产环境。 + +# 2 磁盘 +## 2.1 灵魂拷问:机械硬盘 or 固态硬盘 +- 前者便宜且容量大,但易坏! +- 后者性能优势大,但是贵! + + +建议是使用普通机械硬盘即可。 +- Kafka虽然大量使用磁盘,可多是顺序读写操作,一定程度上规避了机械磁盘最大的劣势,即随机读写慢。从这一点上来说,使用SSD并没有太大性能优势,机械磁盘物美价廉 +- 而它因易损坏而造成的可靠性差等缺陷,又由Kafka在软件层面提供机制来保证 + + +## 2.2 是否应该使用磁盘阵列(RAID) +使用RAID的两个主要优势在于: +- 提供冗余的磁盘存储空间 +- 提供负载均衡 + +不过就Kafka而言 +- Kafka自己实现了冗余机制提供高可靠性 +- 通过分区的设计,也能在软件层面自行实现负载均衡 + +如此说来RAID的优势也就没有那么明显了。虽然实际上依然有很多大厂确实是把Kafka底层的存储交由RAID的,只是目前Kafka在存储这方面提供了越来越便捷的高可靠性方案,因此在线上环境使用RAID似乎变得不是那么重要了。 +综上,追求性价比的公司可以不搭建RAID,使用普通磁盘组成存储空间即可。使用机械磁盘完全能够胜任Kafka线上环境。 + +## 2.3 磁盘容量 +集群到底需要多大? +Kafka需要将消息保存在磁盘上,这些消息默认会被保存一段时间然后自动被删除。 +虽然这段时间是可以配置的,但你应该如何结合自身业务场景和存储需求来规划Kafka集群的存储容量呢? + +假设有个业务 +- 每天需要向Kafka集群发送1亿条消息 +- 每条消息保存两份以防止数据丢失 +- 消息默认保存两周时间 + +现在假设消息的平均大小是1KB,那么你能说出你的Kafka集群需要为这个业务预留多少磁盘空间吗? + +计算: +- 每天1亿条1KB的消息,存两份 +`1亿 * 1KB * 2 / 1000 / 1000 = 200GB` + +- 一般Kafka集群除消息数据还存其他类型数据,比如索引数据 +再为其预留10%磁盘空间,因此总的存储容量就是220GB + +- 要存两周,那么整体容量即为 +220GB * 14,大约3TB +- Kafka支持数据的压缩,假设压缩比是0.75 +那么最后规划的存储空间就是0.75 * 3 = 2.25TB + +总之在规划磁盘容量时你需要考虑下面这几个元素: +- 新增消息数 +- 消息留存时间 +- 平均消息大小 +- 备份数 +- 是否启用压缩 + +# 3 带宽 +对于Kafka这种通过网络进行大数据传输的框架,带宽容易成为瓶颈。 +普通的以太网络,带宽主要有两种:1Gbps的千兆网络和10Gbps的万兆网络,特别是千兆网络应该是一般公司网络的标准配置了 +以千兆网络为例,说明带宽资源规划。 + +真正要规划的是所需的Kafka服务器的数量。 +假设机房环境是千兆网络,即1Gbps,现在有业务,其目标或SLA是在1小时内处理1TB的业务数据。 +那么问题来了,你到底需要多少台Kafka服务器来完成这个业务呢? + +### 计算 +带宽1Gbps,即每秒处理1Gb数据 +假设每台Kafka服务器都是安装在专属机器,即每台Kafka机器上没有混入其他服务 +通常情况下你只能假设Kafka会用到70%的带宽资源,因为总要为其他应用或进程留一些资源。超过70%的阈值就有网络丢包可能性,故70%的设定是一个比较合理的值,也就是说单台Kafka服务器最多也就能使用大约700Mb带宽。 + +这只是它能使用的最大带宽资源,你不能让Kafka服务器常规性使用这么多资源,故通常要再额外预留出2/3的资源,即 +`单台服务器使用带宽700Mb / 3 ≈ 240Mbps` +这里的2/3其实是相当保守的,可以结合机器使用情况酌情减少该值 + +有了240Mbps,可以计算1小时内处理1TB数据所需的服务器数量了。 +根据这个目标,每秒需要处理2336Mb的数据,除以240,约等于10台服务器。 +如果消息还需要额外复制两份,那么总的服务器台数还要乘以3,即30台。 + +# 总结 +与其盲目上马一套Kafka环境然后事后费力调整,不如在一开始就思考好实际场景下业务所需的集群环境。在考量部署方案时需要通盘考虑,不能仅从单个维度上进行评估。 + +# 参考 +- Linux内核模型架构 +- Kafka核心技术与实战 diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(4) -Kafka\351\227\250\346\264\276\347\237\245\345\244\232\345\260\221.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(\345\233\233) -Kafka\351\227\250\346\264\276\347\237\245\345\244\232\345\260\221.md" similarity index 100% rename from "\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(4) -Kafka\351\227\250\346\264\276\347\237\245\345\244\232\345\260\221.md" rename to "\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(\345\233\233) -Kafka\351\227\250\346\264\276\347\237\245\345\244\232\345\260\221.md" diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\351\253\230\346\200\247\350\203\275\345\216\237\347\220\206\345\210\206\346\236\220.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\351\253\230\346\200\247\350\203\275\345\216\237\347\220\206\345\210\206\346\236\220.md" index 2c90d94a66..65c0b5f134 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\351\253\230\346\200\247\350\203\275\345\216\237\347\220\206\345\210\206\346\236\220.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\351\253\230\346\200\247\350\203\275\345\216\237\347\220\206\345\210\206\346\236\220.md" @@ -214,5 +214,25 @@ Kafka 中存储的一般都是海量的消息数据,为了避免日志文件 ## 4.3 消息写入的性能 我们现在大部分企业仍然用的是机械结构的磁盘,如果把消息以随机的方式写入到磁盘,那么磁盘首先要做的就是寻址,也就是定位到数据所在的物理地址,在磁盘上就要找到对应的柱面、磁头以及对应的扇区;这个过程相对内 存来说会消耗大量时间,为了规避随机读写带来的时间消耗,kafka 采用顺序写的方式存储数据。 -即使是这样,但是频繁的 I/O 操作仍然会造成磁盘的性能瓶颈,所以 kafka 还有一个性能策略。 +即使是这样,但是频繁的 I/O 操作仍然会造成磁盘的性能瓶颈,所以 kafka 还有一个性能策略 + +## 4.4 零拷贝 +消息从发送到落地保存,broker 维护的消息日志本身就是文件目录,每个文件都是二进制保存,生产者和消费者使用相同的格式来处理。在消费者获取消息时,服务器先从硬盘读取数据到内存,然后把内存中的数据原封不动的通 过 socket 发送给消费者。 + +虽然这个操作描述起来很简单, 但实际上经历了很多步骤 +![](https://uploadfiles.nowcoder.com/files/20190425/5088755_1556181389694_16782311-d53f60c9f4f81137.png) + +▪ 操作系统将数据从磁盘读入到内核空间的页缓存 +▪ 应用程序将数据从内核空间读入到用户空间缓存中 +▪ 应用程序将数据写回到内核空间到 socket 缓存中 +▪ 操作系统将数据从 socket 缓冲区复制到网卡缓冲区,以便将数据经网络发出 + +这个过程涉及到 4 次上下文切换以及 4 次数据复制,并且有两次复制操作是由 CPU 完成。但是这个过程中,数据完全没有进行变化,仅仅是从磁盘复制到网卡缓冲区。 + +通过“零拷贝”技术,可以去掉这些没必要的数据复制操作, 同时也会减少上下文切换次数。现代的 unix 操作系统提供 一个优化的代码路径,用于将数据从页缓存传输到 socket; +在 Linux 中,是通过 sendfile 系统调用来完成的。 +Java 提供了访问这个系统调用的方法:FileChannel.transferTo API +![](https://uploadfiles.nowcoder.com/files/20190425/5088755_1556181389536_16782311-4bbdb21c5f83a6e9.png) + +使用 sendfile,只需要一次拷贝就行,允许操作系统将数据直接从页缓存发送到网络上。所以在这个优化的路径中, 只有最后一步将数据拷贝到网卡缓存中是需要的。 diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL count()\345\207\275\346\225\260.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL count()\345\207\275\346\225\260.md" deleted file mode 100644 index c446198286..0000000000 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL count()\345\207\275\346\225\260.md" +++ /dev/null @@ -1,73 +0,0 @@ -为统计记录数,由SELECT返回。假如有如下数据: -- 所有记录 -![](https://img-blog.csdnimg.cn/img_convert/bdc833650ffa00c374d040b6a3645fab.png) -- 统计行的总数 -![](https://img-blog.csdnimg.cn/img_convert/45f4593176cfd0f35a4214394690d6b7.png) -- 计算 Zara 的记录数 -![](https://img-blog.csdnimg.cn/img_convert/e04db05f173e28beac8991188a9ecff8.png) - -count(1)、count(*) 都是检索表中所有记录行的数目,不论其是否包含null值。 -count(1)比count(*)效率高。 - -count(字段)是检索表中的该字段的**非空**行数,不统计这个字段值为null的记录。 - -任何情况下最优选择 - -```sql -SELECT COUNT(1) FROM tablename -``` -尽量减少类似: -```java -SELECT COUNT(*) FROM tablename WHERE COL = 'value' -``` -杜绝: - -```java -SELECT COUNT(COL) FROM tablename WHERE COL2 = 'value' -``` - -如果表没有主键,那么count(1)比count(*)快 -如果有主键,那么count(主键,联合主键)比count(*)快 -如果表只有一个字段,count(*)最快 - -count(1)跟count(主键)一样,只扫描主键。 -count(*)跟count(非主键)一样,扫描整个表 -明显前者更快一些。 -# 执行效果 -## count(1) V.S count(*) -当表的数据量大些时,对表作分析之后,使用count(1)还要比使用count(*)用时多! - -从执行计划来看,count(1)和count(*)的效果是一样的。 但是在表做过分析之后,count(1)会比count(*)的用时少些(1w以内数据量),不过差不了多少。 - -如果count(1)是聚索引,id,那肯定是count(1)快。但是差的很小的。 -因为count(*) 会自动优化指定到那一个字段。所以没必要去count(1),用count(*),sql会帮你完成优化的 因此:count(1)和count(*)基本没有差别! - -## count(1) and count(字段) -- count(1) 会统计表中的所有的记录数,包含字段为null 的记录 -- count(字段) 会统计该字段在表中出现的次数,忽略字段为null 的情况。即不统计字段为null 的记录。 - -## count(*) 和 count(1)和count(列名)区别 - -执行效果上: -count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为NULL -count(1)包括了忽略所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULL -count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,即某个字段值为NULL时,不统计。 - -# 执行效率 -列名为主键,count(列名)会比count(1)快 -列名不为主键,count(1)会比count(列名)快 -如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*) -如果有主键,则 select count(主键)的执行效率是最优的 -如果表只有一个字段,则 select count(*)最优。 -# 实例 -![](https://img-blog.csdnimg.cn/bb6fd75177d9428abfaa837ef5879604.png) -![](https://img-blog.csdnimg.cn/fda1c0fe3b09444a80c138d1806bea59.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -```sql -select name, count(name), count(1), count(*), count(age), count(distinct(age)) -from counttest -group by name; -``` -![](https://img-blog.csdnimg.cn/9f7da59b31ad4ae78c41efcbfaf303c7.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -MyISAM有表元数据的缓存,例如行,即COUNT(*)值,对于MyISAM表的COUNT(*)无需消耗太多资源,但对于Innodb,就没有这种元数据,CONUT(*)执行较慢。 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL-count()\345\207\275\346\225\260\345\217\212\345\205\266\344\274\230\345\214\226.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL-count()\345\207\275\346\225\260\345\217\212\345\205\266\344\274\230\345\214\226.md" new file mode 100644 index 0000000000..61357e058b --- /dev/null +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL-count()\345\207\275\346\225\260\345\217\212\345\205\266\344\274\230\345\214\226.md" @@ -0,0 +1,108 @@ +很简单,就是为了统计记录数 +由SELECT返回 + +为了理解这个函数,让我们祭出 employee_tbl 表 +![所有记录](https://upload-images.jianshu.io/upload_images/4685968-c6f571a93a8d2480.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![统计行的总数](https://upload-images.jianshu.io/upload_images/4685968-1bcf595e704c1b10.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![计算 Zara 的记录数](https://upload-images.jianshu.io/upload_images/4685968-ab0b92ffe7be03ec.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +注意:由于 SQL 查询对大小写不敏感,所以在 WHERE 条件中,无论是写成 ZARA 还是 Zara,结果都是一样的 + + +# count(1),count(*),count(字段)区别 +## count(1)和count(*) +### 作用 +都是检索表中所有记录行的数目,不论其是否包含null值 +### 区别 +count(1)比count(*)效率高 + +二 . count(字段)与count(1)和count(*)的区别 + +count(字段)的作用是检索表中的这个字段的非空行数,不统计这个字段值为null的记录 + + +- 任何情况下SELECT COUNT(1) FROM tablename是最优选择 +- 尽量减少SELECT COUNT(*) FROM tablename WHERE COL = ‘value’ 这种 +- 杜绝SELECT COUNT(COL) FROM tablename WHERE COL2 = ‘value’ 的出现 + +- 如果表没有主键,那么count(1)比count(*)快 +- 如果有主键,那么count(主键,联合主键)比count(*)快 +- 如果表只有一个字段,count(*)最快 + +count(1)跟count(主键)一样,只扫描主键。 +count(*)跟count(非主键)一样,扫描整个表 +明显前者更快一些。 +执行效果: + + +1. count(1) and count(*) + +当表的数据量大些时,对表作分析之后,使用count(1)还要比使用count(*)用时多了! +从执行计划来看,count(1)和count(*)的效果是一样的。 但是在表做过分析之后,count(1)会比count(*)的用时少些(1w以内数据量),不过差不了多少。 + +如果count(1)是聚索引,id,那肯定是count(1)快。但是差的很小的。 +因为count(*),自动会优化指定到那一个字段。所以没必要去count(1),用count(*),sql会帮你完成优化的 因此:count(1)和count(*)基本没有差别! + +2. count(1) and count(字段) +两者的主要区别是 +(1) count(1) 会统计表中的所有的记录数,包含字段为null 的记录。 +(2) count(字段) 会统计该字段在表中出现的次数,忽略字段为null 的情况。即不统计字段为null 的记录。 + +count(*) 和 count(1)和count(列名)区别 + +执行效果上: +count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为NULL +count(1)包括了忽略所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULL +count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,即某个字段值为NULL时,不统计。 + +执行效率上: +列名为主键,count(列名)会比count(1)快 +列名不为主键,count(1)会比count(列名)快 +如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*) +如果有主键,则 select count(主键)的执行效率是最优的 +如果表只有一个字段,则 select count(*)最优。 +# 实例 +``` +mysql> create table counttest(name char(1), age char(2)); +Query OK, 0 rows affected (0.03 sec) + +mysql> insert into counttest values + -> ('a', '14'),('a', '15'), ('a', '15'), + -> ('b', NULL), ('b', '16'), + -> ('c', '17'), + -> ('d', null), + ->('e', ''); +Query OK, 8 rows affected (0.01 sec) +Records: 8 Duplicates: 0 Warnings: 0 + +mysql> select * from counttest; ++------+------+ +| name | age | ++------+------+ +| a | 14 | +| a | 15 | +| a | 15 | +| b | NULL | +| b | 16 | +| c | 17 | +| d | NULL | +| e | | ++------+------+ +8 rows in set (0.00 sec) + +mysql> select name, count(name), count(1), count(*), count(age), count(distinct(age)) + -> from counttest + -> group by name; ++------+-------------+----------+----------+------------+----------------------+ +| name | count(name) | count(1) | count(*) | count(age) | count(distinct(age)) | ++------+-------------+----------+----------+------------+----------------------+ +| a | 3 | 3 | 3 | 3 | 2 | +| b | 2 | 2 | 2 | 1 | 1 | +| c | 1 | 1 | 1 | 1 | 1 | +| d | 1 | 1 | 1 | 0 | 0 | +| e | 1 | 1 | 1 | 1 | 1 | ++------+-------------+----------+----------+------------+----------------------+ +5 rows in set (0.00 sec) +``` + +MyISAM有表元数据的缓存,例如行,即COUNT(*)值,对于MyISAM表的COUNT(*)无需消耗太多资源,但对于Innodb,就没有这种元数据,CONUT(*)执行较慢。 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\344\272\213\345\212\241\351\232\224\347\246\273\345\216\237\347\220\206\345\217\212undo log.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\344\272\213\345\212\241\351\232\224\347\246\273\345\216\237\347\220\206\345\217\212undo log.md" index 1ad047e23d..01bd45935d 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\344\272\213\345\212\241\351\232\224\347\246\273\345\216\237\347\220\206\345\217\212undo log.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\344\272\213\345\212\241\351\232\224\347\246\273\345\216\237\347\220\206\345\217\212undo log.md" @@ -78,26 +78,24 @@ V1、V2(事务在执行期间,即未提交前,看到的数据全程一致 事务启动时的视图可认为是静态的,不受其他事务更新影响。 -# 4 实现事务隔离 - undo log +# 4 实现事务隔离的关键 - undo log MySQL的每条记录在更新时都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可得到前一个状态的值。 ## 4.1 示例 -假设一个值从1被按顺改成2、3、4 +假设一个值从1被按顺序改成2、3、4 - undo log中的记录: ![](https://img-blog.csdnimg.cn/20200911032936782.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70#pic_center) -> 回滚段(rollback segment) - -当前值4,但在查询该记录时,在不同时刻启动事务有不同read-view。 +当前值4,但在查询这记录时,在不同时刻启动事务有不同的read-view。 在视图A、B、C,该记录的值分别是1、2、4,同一记录在系统中可存在多版本,即多版本并发控制(MVCC)。 对read-view A,要得到1,就必须将当前值依次执行图中所有的回滚操作得到。 即使现在有另外一个事务正在将4改成5,这个事务跟read-view A、B、C对应的事务不会冲突。 -## 何时删除undo log +- 何时删除undo log? 不需要时才删除。即系统会自己判断,当没有事务再用到这些undo log,undo log就会被删除。 -## 何时不需要undo log +- 何时不需要undo log? 当系统里没有比该undo log更早的read-view时。 ## 意义 @@ -111,19 +109,9 @@ MySQL的每条记录在更新时都会同时记录一条回滚操作。记录上 - system tablespace (MySQL 5.7默认) - undo tablespaces (MySQL 8.0默认) -# 多版本并发控制(MVCC) -使InnoDB支持一致性读 -- READ COMMITTED -- REPEATABLE READ - -让查询不被阻塞、无需等待被其他事务持有的锁,这种技术手段可以增加并发度。 - -InnoDB保留被修改行的旧版本。查询正在被其他事务更新的数据时,会读取更新之前的版本。每行数据都存在一个版本号,每次更新时都更新该版本。这种技术在数据库领域的使用并不普遍。某些数据库,以及某些MySQL存储引擎都不支持。 +回滚段(rollback segment) -聚簇索引的更新=替换更新 -二级索引的更新=删除+新建 - -# 避免长事务 +# 5 避免长事务 长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问db里的任何数据,所以该事务提交之前,db里它可能用到的回滚记录都必须保留,导致大量占存储。 在MySQL 5.5及以前,undo log是跟数据字典一起放在ibdata文件,即使长事务最终提交,回滚段被清理,文件也不会变小。 @@ -131,8 +119,10 @@ InnoDB保留被修改行的旧版本。查询正在被其他事务更新的数 除了对回滚段影响,长事务还占用锁资源,可能拖慢全库。 # 6 事务启动方式 +开发同学并不是有意长事务,通常误用。其实MySQL的事务启动方式有以下几种: + ## 6.1 显式启动事务 -begin 或 start transaction 开启事务: +begin 或 start transaction。配套的 - 提交语句 commit - 回滚语句 rollback diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\345\255\230\345\202\250\345\274\225\346\223\216\344\270\216\351\200\202\347\224\250\345\234\272\346\231\257.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\345\255\230\345\202\250\345\274\225\346\223\216\344\270\216\351\200\202\347\224\250\345\234\272\346\231\257.md" index 63aa67f843..6708e5b78b 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\345\255\230\345\202\250\345\274\225\346\223\216\344\270\216\351\200\202\347\224\250\345\234\272\346\231\257.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\345\255\230\345\202\250\345\274\225\346\223\216\344\270\216\351\200\202\347\224\250\345\234\272\346\231\257.md" @@ -9,7 +9,7 @@ MySQL服务器体系结构将应用程序开发者和DBA与存储级别的所有 ## MySQL架构 - 具有可插拔式存储引擎的MySQL体系结构 -![](https://img-blog.csdnimg.cn/20200828194011627.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) +![](https://img-blog.csdnimg.cn/20200828194011627.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) - Connectors 各种语言的客户端应用 - Connection Pool 为应用层,负责连接、验证等功能 @@ -160,7 +160,7 @@ MyISAM类型的表支持三种不同的存储结构:静态型、动态型、 # InnoDB - MySQL5.5后的默认存储引擎 -![](https://img-blog.csdnimg.cn/20200825042612761.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) +![](https://img-blog.csdnimg.cn/20200825042612761.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) 适用于更新密集型。 diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\346\237\245\350\257\242\344\274\230\345\214\226.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\346\237\245\350\257\242\344\274\230\345\214\226.md" index 36a8b99f2a..e681701dfe 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\346\237\245\350\257\242\344\274\230\345\214\226.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\346\237\245\350\257\242\344\274\230\345\214\226.md" @@ -23,7 +23,7 @@ show global status; 上面的参数是对所有存储引擎的表进行累计,下面参数是针对 ### InnoDB存储引擎 - Innodb_rows_read -![](https://img-blog.csdnimg.cn/2021061318083423.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/2021061318083423.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) SELECT查询返回的行数 - Innodb_rows_insered diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\347\232\204Order\346\216\222\345\272\217\345\256\236\347\216\260\345\216\237\347\220\206.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\347\232\204order by\345\256\236\347\216\260\345\216\237\347\220\206.md" similarity index 67% rename from "\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\347\232\204Order\346\216\222\345\272\217\345\256\236\347\216\260\345\216\237\347\220\206.md" rename to "\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\347\232\204order by\345\256\236\347\216\260\345\216\237\347\220\206.md" index 20b272b64c..3e52582541 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\347\232\204Order\346\216\222\345\272\217\345\256\236\347\216\260\345\216\237\347\220\206.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\347\232\204order by\345\256\236\347\216\260\345\216\237\347\220\206.md" @@ -1,20 +1,24 @@ -这天风和日丽,我还在工位上苦练摸鱼技术 +这天风和日丽,小a正在工位上苦练钓鱼技术, ![](https://img-blog.csdnimg.cn/20210413101116274.png) 突然接到产品的☎️,又来需求? ![](https://img-blog.csdnimg.cn/20210413101305688.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -只见猖狂的产品又开始口若黄河:我现在要查询到城市是“上海”的所有用户名,并且还要按用户名排序,返回前1000人的名字、年龄。 +只听到产品又开始口若黄河:我需要要查询到city是“上海”的所有人的name,并且还要按name排序返回前1000人的name、age。 -我急忙正襟危坐,从一堆库表中翻出相关责任表,找到其建表语句: -![](https://img-blog.csdnimg.cn/d7dcbdb63b7941e1a0dc4b32472be8f5.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -对照表结构,再看看产品需求 + 小a急忙正襟危坐,从一堆库表中翻出需要的表,抽出其建表语句: +![](https://img-blog.csdnimg.cn/20210413124053932.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +看看表结构,再看看产品的需求 ![](https://img-blog.csdnimg.cn/20210413134110379.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -太容易了!SQL随手一写: -![](https://img-blog.csdnimg.cn/cd6e5cb9ac87471aa4674d377e4a605b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -真理往往就是这么简单而朴实,一个需求好像就完美解决了。但作为一个性能优化giao手,考虑要尽量避免全表扫描,于是给 **city** 字段加个索引。 + + + +感觉很容易,随手SQL这么一写: +![](https://img-blog.csdnimg.cn/20210413105428402.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +诶,这语句看着简单而朴实,一个需求好像就完美解决了。但为了显示自己强大的性能优化水平,考虑到要避免全表扫描,于是又给 **city** 字段加索引。 建完索引,自然还需要使用explain验证一下: ```java -explain select city, name, age from citizen where city = '上海' order by name limit 1000; + explain select city, name, age from citizen where city = '上海' order by name limit 1000; +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+ @@ -22,78 +26,63 @@ explain select city, name, age from citizen where city = '上海' order by name +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+ 1 row in set, 1 warning (0.00 sec) ``` -Extra字段的 **Using filesort** 表示需要排序,**MySQL会给每个线程分配一块内存(sort_buffer)用于排序**。 -这时魔鬼产品突然凑过来问:给我看看你代码咋写的 +Extra字段的 **Using filesort** 表示需要排序,MySQL会给每个线程分配一块内存用于排序,称为**sort_buffer**。 -> 你这么写你真的懂MySQL底层怎么执行order by的吗? - -我惊醒了,还真没想过MySQL的底层实现。 - -> 产品经理冷笑道:你知道你的 city 索引长啥样吗? +这时魔鬼产品突然凑过来问:给我看看你代码咋写的,你这么写你真的懂MySQL 底层怎么执行order by的吗? +小a突然惊醒,还真没想过这些。 +产品经理冷笑道:你知道你的 city 索引长啥样吗? 我自己建立的,我咋可能不知道!随手直接画出 - city字段的索引示意图 -![](https://img-blog.csdnimg.cn/20210406195738724.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70)小产品,你可好好看了,这里 `id_x ~ id_(x+n)` 的数据都满足**city=上海**。 - -> 那你倒是说说这条SQL的执行流程? +![](https://img-blog.csdnimg.cn/20210406195738724.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/555519cdc0e046379d5e4ec3a6206371.png) +产品,你可好好看了,这里 `id_x ~ id_(x+n)` 的数据都满足city='上海’。 -不知道了吧,我来告诉你吧: -1. 初始化**sort_buffer**,放入`city, name, age`三字段 -2. 从索引`city`找到第一个满足`city=上海`的主键id, 即`id_x`; -3. 到id索引取出整行,取`city, name, age`三个字段的值,存入**sort_buffer** -4. 从索引`city`取下一个记录的主键id -5. 重复3、4,直到city值不满足查询条件,即主键`id_y` -6. 对**sort_buffer**数据按`name`做快排 -7. 取排序后结果的前1000行,返回给client +产品:那你倒是说说这条SQL的执行流程?不知道了吧,我来告诉你吧: +1. 初始化**sort_buffer**,确定放入`name、city、age`三字段 +2. 从索引`city`找到第一个满足`city='上海’`条件的主键id, 即`id_x`; +3. 到id主键索引取出整行,取`name、city、age`三个字段的值,存入**sort_buffer** +4. 从索引city取下一个记录的主键id +5. 重复3、4,直到city的值不满足查询条件,即主键`id_y` +6. 对**sort_buffer**中数据按`name`做快排 +7. 取排序后结果的前1000行返回给客户端 -这就是 -# 全字段排序 -执行流程示意图: +这就是**全字段排序**,执行流程如下: ![](https://img-blog.csdnimg.cn/20210412145927471.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -**按name排序** 这一操作可能: -- 在内存中完成 -- 或需要外部排序 - -这取决于: -- 排序所需的内存 -若`待排序数据量 < sort_buffer_size`,就在内存中排序。 +**按name排序** 这一操作可能在内存中完成,也可能需要外部排序,而这就取决于 +- 排序所需内存 - 参数**sort_buffer_size** -MySQL为排序开辟的内存(sort_buffer)的大小。若待排序数据量太大,内存放不下,则需利用磁盘临时文件辅助排序。 - -产品又开始炫技了,又问到: - -> order by语句何时会使用临时文件? +MySQL为排序开辟的内存(sort_buffer)的大小。若要排序的数据量小于**sort_buffer_size**,排序就在内存中完成。若排序数据量太大,内存放不下,则得利用磁盘临时文件辅助排序。 +产品又开始炫技了,又问到:你知道 `一条排序语句何时才会使用临时文件` 吗? 这?这还真又触及到我的知识盲区了! ![](https://img-blog.csdnimg.cn/20210413134237949.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -```sql -SET optimizer_trace='enabled=on'; +```java +mysql> SET optimizer_trace='enabled=on'; +Query OK, 0 rows affected (0.00 sec) /* 使用 @a 保存 Innodb_rows_read 的初始值 */ -select VARIABLE_VALUE into @a -from performance_schema.session_status -where variable_name = 'Innodb_rows_read'; +mysql> select VARIABLE_VALUE into @a from performance_schema.session_status where variable_name = 'Innodb_rows_read'; +Query OK, 1 row affected (0.00 sec) -select city, name,age -from citizen -where city='上海' -order by name -limit 1000; +mysql> select city, name,age from citizen where city='上海' order by name limit 1000; ++--------+------+-----+ +| city | name | age | ++--------+------+-----+ +| 上海 | java | 22 | +... /* 查看 OPTIMIZER_TRACE 输出 */ SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G /* 使用 @b 保存 Innodb_rows_read 的当前值 */ -select VARIABLE_VALUE into @b -from performance_schema.session_status -where variable_name = 'Innodb_rows_read'; +mysql> select VARIABLE_VALUE into @b from performance_schema.session_status where variable_name = 'Innodb_rows_read'; +Query OK, 1 row affected (0.00 sec) /* 计算 Innodb_rows_read 的差值 */ -select @b-@a; +mysql> select @b-@a; ``` 查看 **OPTIMIZER_TRACE** 结果中的 **number_of_tmp_files** 字段确认是否使用临时文件。 @@ -122,53 +111,45 @@ MySQL将需要排序的数据分成12份,每一份单独排序后存在这些 `select @b-@a` 的结果4000,即`整个执行过程只扫描了`4000行。 -> 为避免对结论造成干扰,我把**internal_tmp_disk_storage_engine**设置成MyISAM。否则,`select -> @b-@a`的结果会显示为4001。 -> 因为查询**OPTIMIZER_TRACE**表时,需要用到临时表,而**internal_tmp_disk_storage_engine**的默认值是InnoDB。若使用InnoDB,把数据从临时表取出时,会让**Innodb_rows_read**的值加1。 +注意,为了避免对结论造成干扰,我把**internal_tmp_disk_storage_engine**设置成MyISAM。否则,`select @b-@a`的结果会显示为4001。 +因为查询**OPTIMIZER_TRACE**表时,需要用到临时表,而**internal_tmp_disk_storage_engine**的默认值是InnoDB。若使用InnoDB,把数据从临时表取出时,会让**Innodb_rows_read**的值加1。 我惊奇地望着产品,像瞻仰伟人一般,不如你继承我的代码吧,让我来做产品? ![](https://img-blog.csdnimg.cn/20210413142731670.png) -# rowid排序 -上面的算法,只是读了一遍原表数据,剩下的操作都是在**sort_buffer**和临时文件中执行。 - -这就存在问题:若查询要返回的字段很多,则: -- **sort_buffer**要放的字段数就会很多=》 -- 内存能放下的行数就会变少=》 -- 就要分成很多临时文件=》 -- 排序性能就会很差。 - -**所以若单行很大,该算法的效率可不够行哦。** +`rowid排序` +上面的算法,只是对原表数据读了一遍,剩下的操作都是在**sort_buffer**和临时文件中执行。但这就存在问题:若查询要返回的字段很多,那么**sort_buffer**要放的字段数就会很多,内存里能够同时放下的行数就会变少,就要分成很多临时文件,排序性能就会很差。 +**所以若单行很大,该方法的效率可不够行哦。** ![](https://img-blog.csdnimg.cn/20210413143147457.png) - -产品老大又开始发难,那你知道若**MySQL认为排序的单行长度太大,它会咋样**? +产品大大又开始发难,那么你知道若**MySQL认为排序的单行长度太大,它又会干啥吗**? 现在修改个参数,让MySQL采用另外一种算法。 ```bash SET max_length_for_sort_data = 16; ``` - **max_length_for_sort_data** -MySQL用于控制**用于排序的行数据的长度**。若单行长度超过该值,MySQL就认为单行太大,要换个算法。 +MySQL用于控制**用于排序的行数据的长度**。若单行的长度超过该值,MySQL就认为单行太大,要换个算法。 -`city、name、age` 三字段的定义总长度36,那你看我把**max_length_for_sort_data**设为16会咋样? +`city、name、age` 三字段的定义总长度36,那你看我把**max_length_for_sort_data**设为16会咋样。 -新的算法放入**sort_buffer**的字段,只有待排序列(name字段)和主键id。 +新的算法放入**sort_buffer**的字段,只有要排序的列(即name字段)和主键id。 但这时,排序的结果就因少了`city`和`age`字段值,不能直接返回了,整个执行流程变成如下: -1. 初始化`sort_buffer`,确定放入两个字段,即`name`和`id` -2. 从`city`找到第一个满足 **city=上海** 的主键id,即`id_x` -3. 到`id`取出整行,取name、id这俩字段,存入`sort_buffer` +1. 初始化sort_buffer,确定放入两个字段,即`name`和`id` +2. 从`city`找到第一个满足city='上海’条件的主键id,也就是图中的id_x +3. 到id取出整行,取name、id这两个字段,存入sort_buffer 4. 从`city`取下一个记录的主键id -5. 重复3、4,直到不满足**city=上海**,即`id_y` -6. 对`sort_buffer`数据按name排序 -7. 遍历排序结果,取前1000行,并按照id的值回到原表中取出city、name和age三个字段返回给client +5. 重复步骤3、4直到不满足city='上海’,也就是图中的id_y +6. 对sort_buffer中的数据按照字段name进行排序 +7. 遍历排序结果,取前1000行,并按照id的值回到原表中取出city、name和age三个字段返回给客户端。 ![](https://img-blog.csdnimg.cn/20210413144202883.png) -听到这里,感觉明白了一些:产品你别急,你看我画下这个`rowid排序`执行过程的示意图,对不对? +听到这里,感觉明白了一些:产品你别急,你看我画下这个`rowid排序`执行过程的示意图,看看对不对? ![](https://img-blog.csdnimg.cn/20210412150358621.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -你看这个和你之前画的全字段排序示意图对比,其实就是多访问了一次表citizen的主键索引,即step7。 +你看这个和你之前画的全字段排序示意图,其实就是多访问了一次表citizen的主键索引,即step7。 -> resultSet是个逻辑概念,实际上MySQL服务端从排序后的**sort_buffer**中依次取出id,然后到原表查到city、name和age这三字段的结果,无需在服务端再耗费内存存储结果,而是直接返回给client。 +> 注意了,最后的resultSet是一个逻辑概念,实际上MySQL服务端从排序后的**sort_buffer**中依次取出id,然后到原表查到city、name和age这三字段的结果,不需要在服务端再耗费内存存储结果,而是直接返回给客户端。 + +这时查看rowid排序的**OPTIMIZER_TRACE**结果,看看和之前的不同之处在哪里 -这时查看rowid排序的**OPTIMIZER_TRACE**结果: ```sql "filesort_execution": [ ], @@ -197,6 +178,7 @@ MySQL用于控制**用于排序的行数据的长度**。若单行长度超过 对InnoDB,**rowid排序会要求回表,多造成了磁盘读,因此不会被优先选择**。 所以MySQL排序是个高成本操作。 - 是不是所有order by都需排序呢?若不排序就能得到正确的结果,那对系统的消耗会小很多,语句的执行时间也会变得更短。 + 并非所有order by都需排序操作。MySQL之所以需要生成临时表,并且在临时表上做排序,是因为原来的数据都是无序的。 - 如果能保证从city索引上取出来的行,天生就是按name递增排序,是不是就可以不用再排序了? @@ -220,6 +202,7 @@ alter table t add index citizen(city, name); ![](https://img-blog.csdnimg.cn/20210412151401991.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) 可见,该查询过程无需临时表,也无需排序。 + - 使用 explain 查看(city,name)联合索引,查询语句的执行计划 ```sql @@ -238,10 +221,12 @@ alter table t add index citizen(city, name); 索引上的信息足够满足查询请求,不需要再回到主键索引上去取数据。 按覆盖索引,可以再优化一下这个查询语句的执行流程。 -针对这个查询,可以创建一个city、name和age的联合索引,对应的SQL语句就是: +针对这个查询,我们可以创建一个city、name和age的联合索引,对应的SQL语句就是: + ```sql alter table t add index city_user_age(city, name, age); ``` + 这时,对于city字段的值相同的行来说,还是按照name字段的值递增排序的,此时的查询语句也就不再需要排序了。这样整个查询语句的执行流程就变成了: 1. 从索引(city,name,age)找到第一个满足city='上海’条件的记录,取出其中的city、name和age这三个字段的值,作为结果集的一部分直接返回 2. 从索引(city,name,age)取下一个记录,同样取出这三个字段的值,作为结果集的一部分直接返回 diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\347\232\204order by\346\211\247\350\241\214\345\216\237\347\220\206.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\347\232\204order by\346\211\247\350\241\214\345\216\237\347\220\206.md" new file mode 100644 index 0000000000..655afe843d --- /dev/null +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MySQL\347\232\204order by\346\211\247\350\241\214\345\216\237\347\220\206.md" @@ -0,0 +1,230 @@ +这天风和日丽,小a正在工位上苦练钓鱼技术, +![](https://img-blog.csdnimg.cn/20210413101116274.png) + +突然接到产品的☎️,又来需求? +![](https://img-blog.csdnimg.cn/20210413101305688.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +只听到产品又开始口若黄河:我需要要查询到city是“上海”的所有人的name,并且还要按name排序返回前1000人的name、age。 + + 小a急忙正襟危坐,从一堆库表中翻出需要的表,抽出其建表语句: +![](https://img-blog.csdnimg.cn/20210413124053932.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +看看表结构,再看看产品的需求,暗自窃喜,感觉很容易,随手SQL这么一写: +![](https://img-blog.csdnimg.cn/20210413105428402.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +诶,这语句看着简单而朴实,一个需求好像就完美解决了。但为了显示自己强大的性能优化水平,考虑到要避免全表扫描,于是又给 **city** 字段加索引。 +建完索引,自然还需要使用explain验证一下: +```java + explain select city, name, age from citizen where city = '上海' order by name limit 1000; ++----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+ +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | ++----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+ +| 1 | SIMPLE | citizen | NULL | ALL | city | NULL | NULL | NULL | 32 | 100.00 | Using where; Using filesort | ++----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+ +1 row in set, 1 warning (0.00 sec) +``` + +Extra字段的 **Using filesort** 表示需要排序,MySQL会给每个线程分配一块内存用于排序,称为**sort_buffer**。 + +这时魔鬼产品突然凑过来问:给我看看你代码咋写的,你这么写你真的懂MySQL 底层怎么执行order by的吗? +小a突然惊醒,还真没想过这些。 + +产品经理冷笑道:你知道你的 city 索引长啥样吗? +我自己建立的,我咋可能不知道!随手直接画出 +- city字段的索引示意图 +![](https://img-blog.csdnimg.cn/20210406195738724.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +产品,你可好好看了,这里 `id_x ~ id_(x+n)` 的数据都满足city='上海’。 + +产品:那你倒是说说这条SQL的执行流程?不知道了吧,我来告诉你吧: +1. 初始化**sort_buffer**,确定放入`name、city、age`三字段 +2. 从索引`city`找到第一个满足`city='上海’`条件的主键id, 即`id_x`; +3. 到id主键索引取出整行,取`name、city、age`三个字段的值,存入**sort_buffer** +4. 从索引city取下一个记录的主键id +5. 重复3、4,直到city的值不满足查询条件,即主键`id_y` +6. 对**sort_buffer**中数据按`name`做快排 +7. 取排序后结果的前1000行返回给客户端 + +这就是**全字段排序**,执行流程如下: +![](https://img-blog.csdnimg.cn/20210412145927471.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +**按name排序** 这一操作可能在内存中完成,也可能需要外部排序,而这就取决于 +- 排序所需内存 +- 参数**sort_buffer_size** +MySQL为排序开辟的内存(sort_buffer)的大小。若要排序的数据量小于**sort_buffer_size**,排序就在内存中完成。若排序数据量太大,内存放不下,则得利用磁盘临时文件辅助排序。 + +产品又开始炫技了,又问到:你知道 `一条排序语句何时才会使用临时文件` 吗? + +```java +/* 打开optimizer_trace,只对本线程有效 */ +mysql> SET optimizer_trace='enabled=on'; +Query OK, 0 rows affected (0.00 sec) + +/* @a保存Innodb_rows_read的初始值 */ +mysql> select VARIABLE_VALUE into @a from performance_schema.session_status where variable_name = 'Innodb_rows_read'; +Query OK, 1 row affected (0.00 sec) + +/* 执行语句 */ +mysql> select city, name,age from citizen where city='上海' order by name limit 1000; ++--------+------+-----+ +| city | name | age | ++--------+------+-----+ +| 上海 | java | 22 | +... + +/* 查看 OPTIMIZER_TRACE 输出 */ +SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G + +/* @b保存Innodb_rows_read的当前值 */ +mysql> select VARIABLE_VALUE into @b from performance_schema.session_status where variable_name = 'Innodb_rows_read'; +Query OK, 1 row affected (0.00 sec) + +/* 计算Innodb_rows_read差值 */ +mysql> select @b-@a; ++-------+ +| @b-@a | ++-------+ +| 33 | ++-------+ +1 row in set (0.00 sec) +``` +该方法是通过查看 **OPTIMIZER_TRACE** 的结果来确认的,你可以从 **number_of_tmp_files** 查看是否使用了临时文件。 +- 全排序的OPTIMIZER_TRACE部分结果 +![](https://img-blog.csdnimg.cn/20210407214308999.png) + +- **number_of_tmp_files**:排序过程中使用的临时文件数。为啥需要12个文件?内存放不下时,就需要使用外部排序,外部排序一般使用归并排序。 +MySQL将需要排序的数据分成12份,每一份单独排序后存在这些临时文件中。然后把这12个有序文件再合并成一个有序的大文件。 + +如果 **sort_buffer_size** 超过了需要排序的数据量的大小,**number_of_tmp_files** 就是0,表示排序可以直接在内存中完成。 + +否则就需要放在临时文件中排序。**sort_buffer_size**越小,需要分成的份数越多,**number_of_tmp_files**的值就越大。 +- **examined_rows** +测试表中有4000条满足city='上海’的记录,所以 examined_rows=4000:表示参与排序的行数是4000。 + +- **sort_mode** +里面的**packed_additional_fields**:排序过程对字符串做了“紧凑”处理。即使name字段的定义是varchar(16),在排序过程中还是要按实际长度分配空间。 + +查询语句`select @b-@a` 的返回结果是4000,表示整个执行过程只扫描了4000行。 +为了避免对结论造成干扰,我把**internal_tmp_disk_storage_engine**设置成MyISAM。否则,`select @b-@a`的结果会显示为4001。 +因为查询**OPTIMIZER_TRACE**表时,需要用到临时表,而**internal_tmp_disk_storage_engine**的默认值是InnoDB。如果使用的是InnoDB,把数据从临时表取出来的时候,会让**Innodb_rows_read**的值加1。 + +# rowid排序 +上面这个算法,只是对原表的数据读了一遍,剩下的操作都是在**sort_buffer**和临时文件中执行的。 +该算法有个问题:若查询要返回的字段很多,那么**sort_buffer**要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序性能就会很差。 + +所以若单行很大,该方法的效率可不够行哦。 + +那么,若**MySQL认为排序的单行长度太大,它会怎么做**? + +现在修改一个参数,让MySQL采用另外一种算法。 + +```bash +SET max_length_for_sort_data = 16; +``` +- **max_length_for_sort_data** +MySQL控制**用于排序的行数据的长度**的一个参数:若单行的长度超过该值,MySQL就认为单行太大,要换个算法。 + +city、name、age 三字段的定义总长度是36,我把**max_length_for_sort_data**设置为16,看看计算过程有什么改变。 + +新的算法放入**sort_buffer**的字段,只有要排序的列(即name字段)和主键id。 + +但这时,排序的结果就因为少了city和age字段的值,不能直接返回了,整个执行流程如下: +1. 初始化sort_buffer,确定放入两个字段,即name和id +2. 从索引city找到第一个满足city='上海’条件的主键id,也就是图中的ID_X +3. 到主键id索引取出整行,取name、id这两个字段,存入sort_buffer中 +4. 从索引city取下一个记录的主键id +5. 重复步骤3、4直到不满足city='上海’条件为止,也就是图中的ID_Y +6. 对sort_buffer中的数据按照字段name进行排序 +7. 遍历排序结果,取前1000行,并按照id的值回到原表中取出city、name和age三个字段返回给客户端。 + +### 执行流程示意图 +![](https://img-blog.csdnimg.cn/20210412150358621.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + + +对比全字段排序流程图,rowid排序多访问了一次表t的主键索引,即step7。 + +最后的“结果集”是一个逻辑概念,实际上MySQL服务端从排序后的**sort_buffer**中依次取出id,然后到原表查到city、name和age这三个字段的结果,不需要在服务端再耗费内存存储结果,是直接返回给客户端的。 + +这时执行 +```sql +select @b-@a +``` +结果会是多少呢?看看结果有什么不同。 + +examined_rows还是4000,即用于排序的数据是4000行。但是`select @b-@a`这个语句的值变成5000。 + +因为这时除了排序过程,在排序完成后,还要根据id去原表取值。由于语句是limit 1000,因此会多读1000行。 +- rowid排序的**OPTIMIZER_TRACE**部分输出 +![](https://img-blog.csdnimg.cn/20210411153650420.png) + +从**OPTIMIZER_TRACE**的结果中,看到另外两个信息也变了。 +- **sort_mode** 变成了 ****,表示参与排序的只有name和id字段 +- **number_of_tmp_files** 变成10了,是因为这时候参与排序的行数虽然仍然是4000行,但是每一行都变小了,因此需要排序的总数据量就变小了,需要的临时文件也相应地变少了。 + +# 全字段排序 VS rowid排序 +- 若MySQL认为排序内存太小,会影响排序效率,就会采用rowid排序 +这样排序过程中一次可以排序更多行,但需要再回到原表去取数据 +- 若MySQL认为内存够大,会优先选择全字段排序 +把需要的字段都放到sort_buffer中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。 + +这也体现了MySQL的一个设计思想:若内存够,就多利用内存,尽量减少磁盘访问。 + +对于InnoDB表,**rowid排序会要求回表多造成磁盘读,因此不会被优先选择**。 + +所以MySQL做排序是一个高成本的操作。那么是不是所有order by都需排序呢? +如果不排序就能得到正确的结果,那对系统的消耗会小很多,语句的执行时间也会变得更短。 + +并非所有order by都需排序操作。MySQL之所以需要生成临时表,并且在临时表上做排序,是因为原来的数据都是无序的。 + +- 如果能保证从city索引上取出来的行,天生就是按name递增排序,是不是就可以不用再排序了? +是的。 + +所以可以在市民表上创建一个city和name的联合索引,对应的SQL语句是: +```sql +alter table t add index citizen(city, name); +``` + +作为与city索引的对比,我们来看看这个索引的示意图。 +![](https://img-blog.csdnimg.cn/202104111826435.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +依然可以用树搜索的方式定位到第一个满足city='上海’的记录,并且额外确保了,接下来按顺序取“下一条记录”的遍历过程中,只要city的值是上海,name的值就一定是有序的。 +这样整个查询过程的流程就变成: +1. 从索引(city,name)找到第一个满足city='上海’条件的主键id +2. 到主键id索引取出整行,取name、city、age三个字段的值,作为结果集的一部分直接返回 +3. 从索引(city,name)取下一个记录主键id +4. 重复步骤2、3,直到查到第1000条记录,或者是不满足city='上海’条件时循环结束 + +- 引入(city,name)联合索引后,查询语句的执行计划 +![](https://img-blog.csdnimg.cn/20210412151401991.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +可见,该查询过程无需临时表,也无需排序。 + +- 使用 explain 查看(city,name)联合索引,查询语句的执行计划 +![](https://img-blog.csdnimg.cn/20210411190232615.png) +可见Extra字段中没有Using filesort了,也就是不需要排序了。而且由于(city,name)这个联合索引本身有序,所以该查询也不用把4000行全都读一遍,只要找到满足条件的前1000条记录即可退出。在这个例子里,只需扫描1000次。 + +该语句的执行流程有没有可能进一步简化呢? +- 覆盖索引 +索引上的信息足够满足查询请求,不需要再回到主键索引上去取数据。 + +按覆盖索引,可以再优化一下这个查询语句的执行流程。 +针对这个查询,我们可以创建一个city、name和age的联合索引,对应的SQL语句就是: + +```sql +alter table t add index city_user_age(city, name, age); +``` + +这时,对于city字段的值相同的行来说,还是按照name字段的值递增排序的,此时的查询语句也就不再需要排序了。这样整个查询语句的执行流程就变成了: +1. 从索引(city,name,age)找到第一个满足city='上海’条件的记录,取出其中的city、name和age这三个字段的值,作为结果集的一部分直接返回 +2. 从索引(city,name,age)取下一个记录,同样取出这三个字段的值,作为结果集的一部分直接返回 +3. 重复执行步骤2,直到查到第1000条记录,或者是不满足city='上海’条件时循环结束 + +引入 `(city,name,age)` 联合索引后,查询语句的执行流程 +![](https://img-blog.csdnimg.cn/20210412151620798.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + + +- explain查看(city,name,age)联合索引后,查询语句的执行计划 +![](https://img-blog.csdnimg.cn/20210411212531910.png) +Extra字段里面多了“Using index”,表示的就是使用了覆盖索引,性能上会快很多。 +这并不是说要为了每个查询能用上覆盖索引,就要把语句中涉及的字段都建上联合索引,毕竟索引还是有维护代价的。这是一个需要折中考虑的。 + +参考 +- “order by”是怎么工作的? \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/\346\210\221\346\230\257\345\246\202\344\275\225\344\270\200\346\255\245\346\255\245\350\256\251\345\205\254\345\217\270\347\232\204MySQL\346\224\257\346\222\221\344\272\277\347\272\247\346\265\201\351\207\217\347\232\204.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/\346\210\221\346\230\257\345\246\202\344\275\225\344\270\200\346\255\245\346\255\245\350\256\251\345\205\254\345\217\270\347\232\204MySQL\346\224\257\346\222\221\344\272\277\347\272\247\346\265\201\351\207\217\347\232\204.md" deleted file mode 100644 index 7fd3d540bd..0000000000 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/\346\210\221\346\230\257\345\246\202\344\275\225\344\270\200\346\255\245\346\255\245\350\256\251\345\205\254\345\217\270\347\232\204MySQL\346\224\257\346\222\221\344\272\277\347\272\247\346\265\201\351\207\217\347\232\204.md" +++ /dev/null @@ -1,131 +0,0 @@ -# 1 主从读写分离 -大部分互联网业务都是读多写少,因此优先考虑DB如何支撑更高查询数,首先就需要区分读、写流量,这才方便针对读流量单独扩展,即主从读写分离。 - -若前端流量突增导致从库负载过高,DBA会优先做个从库扩容上去,这样对DB的读流量就会落到多个从库,每个从库的负载就降了下来,然后开发再尽力将流量挡在DB层之上。 - -> Cache V.S MySQL读写分离 -> 由于从开发和维护的难度考虑,引入缓存会引入复杂度,要考虑缓存数据一致性,穿透,防雪崩等问题,并且也多维护一类组件。所以推荐优先采用读写分离,扛不住了再使用Cache。 - -## 1.1 core -主从读写分离一般将一个DB的数据拷贝为一或多份,并且写入到其它的DB服务器中: -- 原始DB为主库,负责数据写入 -- 拷贝目标DB为从库,负责数据查询 - -所以主从读写分离的关键: -- 数据的拷贝 -即主从复制 -- 屏蔽主从分离带来的访问DB方式的变化 -让开发人员使用感觉依旧在使用单一DB - -# 2 主从复制 -MySQL的主从复制依赖于binlog,即记录MySQL上的所有变化并以二进制形式保存在磁盘上二进制日志文件。 - -主从复制就是将binlog中的数据从主库传输到从库,一般异步:主库操作不会等待binlog同步完成。 -## 2.1 主从复制的过程 -- 从库在连接到主节点时会创建一个I/O线程,以请求主库更新的binlog,并把接收到的binlog写入relay log文件,主库也会创建一个log dump线程发送binlog给从库 -- 从库还会创建一个SQL线程,读relay log,并在从库中做回放,最终实现主从的一致性 - -使用独立的log dump线程是异步,避免影响主库的主体更新流程,而从库在接收到信息后并不是写入从库的存储,是写入一个relay log,这是为避免写入从库实际存储会比较耗时,最终造成从库和主库延迟变长。 - -- 主从异步复制的过程![](https://img-blog.csdnimg.cn/d03b032126af44019d8785523c3e8203.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -基于性能考虑,主库写入流程并没有等待主从同步完成就返回结果,极端情况下,比如主库上binlog还没来得及落盘,就发生磁盘损坏或机器掉电,导致binlog丢失,主从数据不一致。不过概率很低,可容忍。 - -> 主库宕机后,binlog丢失导致的主从数据不一致也只能手动恢复。 - -主从复制后,即可: -- 在写入时只写主库 -- 在读数据时只读从库 - -这样即使写请求会锁表或锁记录,也不会影响读请求执行。高并发下,可部署多个从库共同承担读流量,即一主多从支撑高并发读。 - -从库也能当成个备库,以避免主库故障导致数据丢失。 - -那无限制地增加从库就能支撑更高并发吗? -NO!从库越多,从库连接上来的I/O线程越多,主库也要创建同样多log dump线程处理复制的请求,对于主库资源消耗较高,同时受限于主库的网络带宽,所以一般一个主库最多挂3~5个从库。 -## 2.2 主从复制的副作用 -比如发朋友圈这一操作,就存在数据的: -- 同步操作 -如更新DB -- 异步操作 -如将朋友圈内容同步给审核系统 - -所以更新完主库后,会将朋友圈ID写入MQ,由Consumer依据ID在从库获取朋友圈信息再发给审核系统。 -**此时若主从DB存在延迟,会导致在从库取不到朋友圈信息,出现异常!** - -- 主从延迟对业务的影响示意图 -![](https://img-blog.csdnimg.cn/80fea7d207da46f3a7f82592e16a4f6d.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -## 2.3 避免主从复制的延迟 -这咋办呢?其实解决方案有很多,核心思想都是 **尽量不去从库查询数据**。因此针对上述案例,就有如下方案: -### 2.3.1 数据冗余 -可在发MQ时,不止发送朋友圈ID,而是发给Consumer需要的所有朋友圈信息,避免从DB重新查询数据。 - -> 推荐该方案,因为足够简单,不过可能造成单条消息较大,从而增加消息发送的带宽和时间。 -### 2.3.2 使用Cache -在同步写DB的同时,把朋友圈数据写Cache,这样Consumer在获取朋友圈信息时,优先查询Cache,这也能保证数据一致性。 - -该方案适合新增数据的场景。若是在更新数据场景下,先更新Cache可能导致数据不一致。比如两个线程同时更新数据: -> - 线程A把Cache数据更新为1 -> - 另一个线程B把Cache数据更新为2 -> - 然后线程B又更新DB数据为2 -> - 线程A再更新DB数据为1 - -最终DB值(1)和Cache值(2)不一致! -### 2.3.3 查询主库 -可以在Consumer中不查询从库,而改为查询主库。 - -使用要慎重,要明确查询的量级不会很大,是在主库的可承受范围之内,否则会对主库造成较大压力。 - -若非万不得已,不要使用该方案。因为要提供一个查询主库的接口,很难保证其他人不滥用该方法。 - -主从同步延迟也是排查问题时容易忽略。 -有时会遇到从DB获取不到信息的诡异问题,会纠结代码中是否有一些逻辑把之前写入内容删除了,但发现过段时间再去查询时又能读到数据,这基本就是主从延迟问题。 -所以,一般把从库落后的时间作为一个重点DB指标,做监控和报警,正常时间在ms级,达到s级就要告警。 - -> 主从的延迟时间预警,那如何通过哪个数据库中的哪个指标来判别? 在从从库中,通过监控show slave -> status\G命令输出的Seconds_Behind_Master参数的值判断,是否有发生主从延时。 -> 这个参数值是通过比较sql_thread执行的event的timestamp和io_thread复制好的 -> event的timestamp(简写为ts)进行比较,而得到的这么一个差值。 -> 但如果复制同步主库bin_log日志的io_thread线程负载过高,则Seconds_Behind_Master一直为0,即无法预警,通过Seconds_Behind_Master这个值来判断延迟是不够准确。其实还可以通过比对master和slave的binlog位置。 - -# 3 如何访问DB -使用主从复制将数据复制到多个节点,也实现了DB的读写分离,这时,对DB的使用也发生了变化: -- 以前只需使用一个DB地址 -- 现在需使用一个主库地址,多个从库地址,且需区分写入操作和查询操作,再结合“分库分表”,复杂度大大提升。 - -为降低实现的复杂度,业界涌现了很多DB中间件解决DB的访问问题,大致分为: -## 3.1 应用程序内部 -如TDDL( Taobao Distributed Data Layer),以代码形式内嵌运行在应用程序内部。可看成是一种数据源代理,它的配置管理多个数据源,每个数据源对应一个DB,可能是主库或从库。 -当有一个DB请求时,中间件将SQL语句发给某个指定数据源,然后返回处理结果。 -#### 优点 -简单易用,部署成本低,因为植入应用程序内部,与程序一同运行,适合运维较弱的小团队。 -#### 缺点 -缺乏多语言支持,都是Java语言开发的,无法支持其他的语言。版本升级也依赖使用方的更新。 -## 3.2 独立部署的代理层方案 -如Mycat、Atlas、DBProxy。 - -这类中间件部署在独立服务器,业务代码如同在使用单一DB,实际上它内部管理着很多的数据源,当有DB请求时,它会对SQL语句做必要的改写,然后发往指定数据源。 -#### 优点 -- 一般使用标准MySQL通信协议,所以可支持多种语言 -- 独立部署,所以方便维护升级,适合有运维能力的大中型团队 -#### 缺点 -所有的SQL语句都需要跨两次网络:从应用到代理层和从代理层到数据源,所以在性能上会有一些损耗。 -# 4 总结 -可以把主从复制引申为存储节点之间互相复制存储数据的技术,可以实现数据冗余,以达到备份和提升横向扩展能力。 - -使用主从复制时,需考虑: - - 主从的一致性和写入性能的权衡 -若保证所有从节点都写入成功,则写性能一定受影响;若只写主节点就返回成功,则从节点就可能出现数据同步失败,导致主从不一致。互联网项目,一般优先考虑性能而非数据的强一致性 -- 主从的延迟 -会导致很多诡异的读取不到数据的问题 - -很多实际案例: -- Redis通过主从复制实现读写分离 -- Elasticsearch中存储的索引分片也可被复制到多个节点 -- 写入到HDFS中,文件也会被复制到多个DataNode中 - -不同组件对于复制的一致性、延迟要求不同,采用的方案也不同,但设计思想是相通的。 -# FAQ -> 若大量订单,通过userId hash到不同库,对前台用户订单查询有利,但后台系统页面需查看全部订单且排序,SQL执行就很慢。这该怎么办呢? - -由于后台系统不能直接查询分库分表的数据,可考虑将数据同步至一个单独的后台库或同步至ES。 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\344\270\273\344\273\216\345\244\215\345\210\266\345\216\237\347\220\206\345\217\212\350\277\207\346\234\237key\345\244\204\347\220\206.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\344\270\273\344\273\216\345\244\215\345\210\266\345\216\237\347\220\206\345\217\212\350\277\207\346\234\237key\345\244\204\347\220\206.md" deleted file mode 100644 index 54ebe3ee6d..0000000000 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\344\270\273\344\273\216\345\244\215\345\210\266\345\216\237\347\220\206\345\217\212\350\277\207\346\234\237key\345\244\204\347\220\206.md" +++ /dev/null @@ -1,215 +0,0 @@ -在Redis复制的基础上(不包括Redis Cluster或Redis Sentinel作为附加层提供的高可用功能),使用和配置主从复制非常简单,能使得 【Redis从服务器】(下文称R)能精确得复制 【Redis主服务器】(下文称M)的内容。 -每当 R 和 M 之间的连接断开时, R 会自动重连到 M,并且无论这期间 M 发生了什么, R 都将尝试让自身成为 M 的精确副本。 -# 1 依赖机制 -该系统的运行依靠如下重要的机制: -## 1.1 更新 R -当一个 M 和一个 R 连接正常时, M 会发送一连串命令流保持对 R 的更新,以便将自身数据集的改变复制给 R,这包括客户端的写入、key 的过期或被逐出等 -## 1.2 部分重同步 -当 M 和 R 断连后,因为网络问题、或者是主从意识到连接超时, R 重新连接上 M 并会尝试进行部分重同步:它会尝试只获取在断开连接期间内丢失的命令流。 -## 1.3 全量重同步 -当无法进行部分重同步时, R 会请求全量重同步。 -这涉及到一个更复杂过程,比如M需要创建所有数据的快照,将之发送给 R ,之后在数据集更改时持续发送命令流到 R。 - -Redis使用默认的异步复制,低延迟且高性能,适用于大多数 Redis 场景。但是,R会异步确认其从M周期接收到的数据量。 - -客户端可使用 WAIT 命令来请求同步复制某些特定的数据。但WAIT命令只能确保在其他 Redis 实例中有指定数量的已确认的副本:在故障转移期间,由于不同原因的故障转移或是由于 Redis 持久性的实际配置,故障转移期间确认的写入操作可能仍然会丢失。 -# 2 Redis 复制特点 -- Redis 使用异步复制,R 和 M 之间异步地确认处理的数据量 -- 一个 M 可有多个 R -- R 可接受其他 R 的连接 -除了多个 R 可以连接到同一 M,R 间也可以像层级连接其它 R。Redis 4.0起,所有 sub-R 将会从 M 收到完全一样的复制流 -- Redis 复制在 M 侧是非阻塞的 -M 在一或多 R 进行初次同步或者是部分重同步时,可以继续处理查询请求 -- 复制在 R 侧大部分也是非阻塞 -当 R 进行初次同步时,它可以使用旧数据集处理查询请求,假设在 redis.conf 中配置了让 Redis 这样做。否则,你可以配置如果复制流断开, Redis R 会返回一个 error 给客户端。但在初次同步后,旧数据集必须被删除,同时加载新的数据集。 R 在这个短暂的时间窗口内(如果数据集很大,会持续较长时间),会阻塞到来的连接请求。自 Redis 4.0 开始,可以配置 Redis 使删除旧数据集的操作在另一个不同的线程中进行,但是,加载新数据集的操作依然需要在主线程中进行并且会阻塞 R -- 复制可被用在可伸缩性,以便只读查询可以有多个 R 进行(例如 O(N) 复杂度的慢操作可以被下放到 R ),或者仅用于数据安全和高可用 -- 可使用复制来避免 M 将全部数据集写入磁盘造成的开销:一种典型的技术是配置你的 M 的 `redis.conf`以避免对磁盘进行持久化,然后连接一个 R ,配置为不定期保存或是启用 AOF。但是,这个设置必须小心处理,因为重启的 M 将从一个空数据集开始:如果一个 R 试图与它同步,那么这个 R 也会被清空! - -# 1 单机“危机” -- 容量瓶颈 -- 机器故障 -- QPS瓶颈 - -![](https://img-blog.csdnimg.cn/20200904132333485.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) - -- 一主多从 -![](https://img-blog.csdnimg.cn/20200904150126617.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) - -## 主从复制作用 -- 数据副本 -- 扩展读性能 - -1. 一个M可以有多个R -2. 一个R只能有一个M -3. 数据流向是单向的,M => R - -# 2 实现复制的操作 -## 2.1 命令:Rof -- 异步执行,很耗时间 -![](https://img-blog.csdnimg.cn/20200904150903762.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -无需重启,但是不便于配置管理。 -## 2.2 配置 -```shell -Rof ip port -R-read-only yes -``` -虽然可统一配置,但需重启。 -# 3 全量复制 -1. M执行`bgsave`,在本地生成一份RDB -![](https://img-blog.csdnimg.cn/7c7eda61e919428d9090b56a4c61505e.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -2. M将RDB发给salve,若RDB复制时间>60s(repl-timeout) -![](https://img-blog.csdnimg.cn/6cfb55cdc5ca441c9de0cc67d060590e.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -则replica就会认为复制失败,可适当调大该参数(对于千兆网卡的机器,一般每秒传输100MB,6G文件,很可能超过60s) -![](https://img-blog.csdnimg.cn/5a4ce67217eb4f41a622d630d9268ccf.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -3. M在生成RDB时,会将所有新的写命令缓存在内存中,在salve保存了rdb之后,再将新的写命令复制给salve -4. 若在复制期间,内存缓冲区持续消耗超过64MB,或者一次性超过256MB,则停止复制,复制失败 -5. R node接收到RDB之后,清空自己的旧数据,然后重新加载RDB到自己的内存中,同时**基于旧的数据版本**对外提供服务 -6. 如果R开启了AOF,那么会立即执行BGREWRITEAOF,重写AOF - -![](https://img-blog.csdnimg.cn/20210401150137560.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -RDB生成、RDB通过网络拷贝、R旧数据的清理、R aof rewrite,很耗费时间。 -如果复制的数据量在4G~6G之间,那么很可能全量复制时间消耗到1分半到2分钟。 -## 3.1 全量复制开销 -1. bgsave时间 -2. RDB文件网络传输时间 -3. 从节点清空数据时间 -4. 从节点加载RDB的时间 -5. 可能的AOF重写时间 -## 3.2 全量同步细节 -M 开启一个后台save进程,以便生成一个 RDB 文件。同时它开始缓冲所有从客户端接收到的新的写入命令。当后台save完成RDB文件时, M 将该RDB数据集文件发给 R, R会先将其写入磁盘,然后再从磁盘加载到内存。再然后 M 会发送所有缓存的写命令发给 R。这个过程以指令流的形式完成并且和 Redis 协议本身的格式相同。 - -当主从之间的连接因为一些原因崩溃之后, R 能够自动重连。如果 M 收到了多个 R 要求同步的请求,它会执行一个单独的后台保存,以便于为多个 R 服务。 -### 加速复制 -默认情况下,M接收SYNC命令后执行BGSAVE,将数据先保存到磁盘,若磁盘性能差,则**写入磁盘会消耗大量性能**。 -因此在Redis 2.8.18时进行改进,可以设置无需写入磁盘直接发生RDB快照给R,加快复制速度。 - -复制SYNC策略:磁盘或套接字。仅仅接受差异就无法继续复制过程的新副本和重新连接副本需要进行所谓的“完全同步”。 RDB文件从主数据库传输到副本数据库。传输可以通过两种不同的方式进行:1)支持磁盘:Redis主服务器创建一个新过程,将RDB文件写入磁盘。后来,该文件由父进程逐步传输到副本。 2)无盘:Redis主服务器创建一个新进程,该进程将RDB文件直接写入副本套接字,而完全不接触磁盘。使用磁盘支持的复制,在生成RDB文件的同时,只要生成RDB文件的当前子级完成工作,就可以将更多副本排入队列并与RDB文件一起使用。如果使用无盘复制,则一旦传输开始,新的副本将排队,并且当当前副本终止时将开始新的传输。当使用无盘复制时,主服务器在开始传输之前等待一段可配置的时间(以秒为单位),以希望多个副本可以到达并且传输可以并行化。使用慢速磁盘和快速(大带宽)网络时,无盘复制效果更好。 -修改配置: -```java -repl-diskless-sync yes (默认no) -``` -# 4 增量复制 -1. 如果全量复制过程中,M-R网络连接中断,那么salve重连M时,会触发增量复制 -2. M直接从自己的backlog中获取部分丢失的数据,发送给R node -3. msater就是根据R发送的psync中的offset来从backlog中获取数据的 -![](https://img-blog.csdnimg.cn/20200905001841252.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -# 5 M关闭持久化时的复制安全性 -在使用 Redis 复制功能时的设置中,推荐在 M 和 R 中启用持久化。 -当不可能启用时,例如由于非常慢的磁盘性能而导致的延迟问题,应该配置实例来避免重启后自动重新开始复制。 - -关闭持久化并配置了自动重启的 M 是危险的: -1. 设置节点 A 为 M 并关闭它的持久化设置,节点 B 和 C 从 节点 A 复制数据 -2. 节点 A 宕机,但它有一些自动重启系统可重启进程。但由于持久化被关闭了,节点重启后其数据集是空的! -3. 这时B、C 会从A复制数据,但A数据集空,因此复制结果是它们会销毁自身之前的数据副本! - -当 Redis Sentinel 被用于高可用并且 M 关闭持久化,这时如果允许自动重启进程也是很危险的。例如, M 可以重启的足够快以致于 Sentinel 没有探测到故障,因此上述的故障模式也会发生。 -任何时候数据安全性都是很重要的,所以如果 M 使用复制功能的同时未配置持久化,那么自动重启进程这项就该被禁用。 - -> 用Redis主从同步,写入Redis的数据量太大,没加频次控制,导致每秒几十万写入,主从延迟过大,运维频频报警,在主库不挂掉的情况下,这样大量写入会不会造成数据丢失? -> 若主从延迟很大,数据会堆积到redis主库的发送缓冲区,会导致主库OOM。 - -# 6 复制工作原理 -- 每个 M 都有一个 replication ID :一个较大的伪随机字符串,标记了一个给定的数据集。 -![](https://img-blog.csdnimg.cn/20200905221152322.png#pic_center) -每个 M 也持有一个偏移量,M 将自己产生的复制流发送给 R 时,发送多少个字节的数据,自身的偏移量就会增加多少,目的是当有新的操作修改自己的数据集时,它可据此更新 R 的状态。 -![](https://img-blog.csdnimg.cn/20200905221515483.png#pic_center) - -复制偏移量即使在没有一个 R 连接到 M 时,也会自增,所以基本上每一对给定的 -`Replication ID, offset` -都会标识一个 M 数据集的确切版本。 - -## psync -R使用`psync`从M复制,psync runid offset -![](https://img-blog.csdnimg.cn/20200905233312575.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) - -M会根据自身情况返回响应信息: -- 可能是FULLRESYNC runid offset触发全量复制 -- 可能是CONTINUE触发增量复制 - -R 连接到 M 时,它们使用 PSYNC 命令来发送它们记录的旧的 M replication ID 和它们至今为止处理的偏移量。通过这种方式, M 能够仅发送 R 所需的增量部分。 -但若 M 的缓冲区中没有足够的命令积压缓冲记录,或者如果 R 引用了不再知道的历史记录(replication ID),则会转而进行一个全量重同步:在这种情况下, R 会得到一个完整的数据集副本,从头开始。即: -- 若R重连M,那么M仅会复制给R缺少的部分数据 -- 若第一次连接M,那么会触发全量复制 - -Redis使用复制保证数据同步,以2.8版本为界: -## 2.8前性能较差的复制和命令传播 -首先是从服务器发生同步操作sync,主服务器执行bgsave生成一个全量RDB文件,然后传输给从服务器。 -同时主服务器会把这一过程中执行的写命令写入缓存区。从服务器会把RDB文件进行一次全量加载。 -加载完毕后,主服务器会把缓存区中的写命令传给从服务器。从服务器执行命令后,主从服务器的数据就一致了。 -这种方式每次如果网络出现故障,故障重连后都要进行全量数据的复制。对主服务器的压力太大,也会增加主从网络传输的资源消耗。 -## 2.8后的优化 -增加部分重同步功能,就是同步故障后的一部分数据,而非全量数据。这种优化在量级非常大的情况下效率提升很明显。 - -## 4.0的PSYNC2 -# 7 复制的完整流程 -![](https://img-blog.csdnimg.cn/20190705083122154.png) -> R如果跟M有网络故障,断开连接会自动重连。 -> M如果发现有多个R都重新连接,仅会启动一个rdb save操作,用一份数据服务所有R。 - -1. R启动,仅保存M的信息,包括M的`host`和`ip`,但复制流程尚未开始M host和ip配置在 `redis.conf` 中的 Rof -2. R内部有个定时任务,每s检查是否有新的M要连接和复制,若发现,就跟M建立socket网络连接。 -3. R发送ping命令给M -4. 口令认证 - 若M设置了requirepass,那么salve必须同时发送Mauth的口令认证 -5. M **第一次执行全量复制**,将所有数据发给R -6. M后续持续将写命令,异步复制给R - -## heartbeat -主从节点互相都会发送heartbeat信息。 -M默认每隔10秒发送一次heartbeat,salve node每隔1秒发送一个heartbeat。 - -# 8 断点续传 -Redis 2.8开始支持主从复制的断点续传 -![](https://img-blog.csdnimg.cn/2019070508465819.png) - -主从复制过程,若网络连接中断,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。 - -## M和R都会维护一个offset -- M在自身基础上累加offset,R亦是 -- R每秒都会上报自己的offset给M,同时M保存每个R的offset - -M和R都要知道各自数据的offset,才能知晓互相之间的数据不一致情况。 - -## backlog -M会在内存中维护一个backlog,默认1MB。M给R复制数据时,也会将数据在backlog中同步写一份。 - -`backlog主要是用做全量复制中断时候的增量复制`。 - -M和R都会保存一个replica offset还有一个M id,offset就是保存在backlog中的。若M和R网络连接中断,R会让M从上次replica offset开始继续复制。但若没有找到对应offset,就会执行resynchronization。 - -## M run id -- info server,可见M run id -![](https://img-blog.csdnimg.cn/20200905232843801.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) - -根据host+ip定位M node,是不靠谱的,如果M node重启或者数据出现了变化,那么R node应该根据不同的run id区分,run id不同就做全量复制。 -如果需要不更改run id重启redis,可使用: -```shell -redis-cli debug reload -``` -# 9 无磁盘化复制 -M在内存中直接创建RDB,然后发送给R,不会在自己本地持久化。 -只需要在配置文件中开启` repl-diskless-sync yes `即可. -```shell -等待 5s 再开始复制,因为要等更多 R 重连 -repl-diskless-sync-delay 5 -``` -# 10 处理过期key -Redis 的过期机制可以限制 key 的生存时间。此功能取决于 Redis 实例计算时间的能力,但是,即使使用 Lua 脚本更改了这些 key,Redis Rs 也能正确地复制具有过期时间的 key。 - -为实现功能,Redis 不能依靠主从使用同步时钟,因为这是一个无法解决的问题并且会导致 race condition 和数据不一致,所以 Redis 使用三种主要的技术使过期的 key 的复制能够正确工作: -1. R 不会让 key 过期,而是等待 M 让 key 过期。当一个 M 让一个 key 到期(或由于 LRU 删除)时,它会合成一个 DEL 命令并传输到所有 R -2. 但由于这是 M 驱动的 key 过期行为,M 无法及时提供 DEL 命令,所以有时 R 的内存中仍可能存在逻辑上已过期的 key 。为处理该问题,R 使用它的逻辑时钟以报告只有在不违反数据集的一致性的读取操作(从主机的新命令到达)中才存在 key。用这种方法,R 避免报告逻辑过期的 key 仍然存在。在实际应用中,使用 R 程序进行缩放的 HTML 碎片缓存,将避免返回已经比期望的时间更早的数据项 -3. 在Lua脚本执行期间,不执行任何 key 过期操作 -当一个Lua脚本运行时,概念上讲,M 中的时间是被冻结的,这样脚本运行的时候,一个给定的键要么存在or不存在。这可以防止 key 在脚本中间过期,保证将相同的脚本发送到 R ,从而在二者的数据集中产生相同的效果。 - -一旦 R 被提升 M ,它将开始独立过期 key,而不需要任何旧 M 帮助。 - -# 11 重新启动和故障转移后的部分重同步 -Redis 4.0 开始,当一个实例在故障转移后被提升为 M 时,它仍然能够与旧 M 的 R 进行部分重同步。为此,R 会记住旧 M 的旧 replication ID 和复制偏移量,因此即使询问旧的 replication ID,也可以将部分复制缓冲提供给连接的 R 。 - -但是,升级的 R 的新 replication ID 将不同,因为它构成了数据集的不同历史记录。例如,M 可以返回可用,并且可以在一段时间内继续接受写入命令,因此在被提升的 R 中使用相同的 replication ID 将违反一对复制标识和偏移对只能标识单一数据集的规则。 - -另外,R 在关机并重新启动后,能够在 RDB 文件中存储所需信息,以便与 M 进行重同步。这在升级的情况下很有用。当需要时,最好使用 SHUTDOWN 命令来执行 R 的保存和退出操作。 - -> 参考 -> - https://raw.githubusercontent.com/antirez/redis/2.8/00-RELEASENOTES -> - https://redis.io/topics/replication \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\210\206\347\211\207.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\210\206\347\211\207.md" index 5843913023..8585cdc213 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\210\206\347\211\207.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\210\206\347\211\207.md" @@ -2,175 +2,57 @@ > 本文已收录在github,欢迎 star/fork: > https://github.com/Wasabi1234/Java-Interview-Tutorial -分片,Redis 数据的分布方式,分片就是将数据拆分到多个 Redis 实例,这样每个实例将只是所有键的一个子集。 - -# 1 为什么要分区? -随着请求量和数据量的增加,一台机器已无法满足需求,就需要把数据和请求分散到多台机器,这就需要引入分布式存储。 - -## 1.1 分布式存储的特性 -- 增强可用性 -如果数据库的某个节点出现故障,在其他节点的数据仍然可用 -- 维护方便 -如果数据库的某个节点出现故障,需要修复数据,只需修复该节点 -- 均衡I/O -可以把不同的请求映射到各节点以平衡 I/O,改善整个系统性能 -- 改善查询性能 -对分区对象的查询可以仅搜索自己关心的节点,提高检索速度 - -分布式存储首先要解决把整个数据集按分区规则映射到多个节点的问题,即把数据集划分到多个节点,每个节点负责整体数据的一个子集: -1. 分片可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。 -2. 分片使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。 +分片就是将数据拆分到多个 Redis 实例,这样每个实例将只是所有键的一个子集。 -# 有哪些分片方案? -假设: -- 有 4 个 Redis 实例 R0,R1,R2,R3 -- 很多表示用户的键,像 user:1,user:2 +# 1 分片有什么作用? +1. 分片可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。 +2. 分片使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。 +# 2 分片方案 +假想我们有 4 个 Redis 实例 R0,R1,R2,R3; +很多表示用户的键,像 user:1,user:2等。 有如下方案可映射键到指定 Redis 节点。 -## 范围分区(range partitioning) -也叫顺序分区,最简单的分区方式。通过映射对象的范围到指定的 Redis 实例来完成分片。 - -- 假设用户从 ID 1 ~ 33 进入实例 R0,34 ~ 66 进入R1 - -![](https://img-blog.csdnimg.cn/20210430155203966.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -### 优点 -- 键值业务相关 -- 可顺序访问 -同一范围内的范围查询不需要跨节点,提升查询速度 -- 支持批量操作 - -### 缺点 -- 数据分散度易倾斜 -- 需要一个映射范围到实例的表格。该表需要管理,不同类型的对象都需要一个表,所以范围分片在 Redis 中常常并不可取,因这要比其他分片可选方案低效得多。 - -### 产品 -- BigTable -- HBase -- MySQL -- Oracle - -## 2.2 哈希分区(hash partitioning) -传统分布式算法,适于任何键,不必是 `object_name:` 形式: -1. 使用一个哈希函数(例如crc32) ,将key转为一个数字,比如93024922 -2. 对该数据进行取模,将其转换为一个 0 到 3 之间数字,该数字即可映射到4个 节点之一。93024922 模 4 等于 2,所以键 foobar 存储到 R2 -![](https://img-blog.csdnimg.cn/20210430155222501.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -### 2.2.1 分类 -#### 2.2.1.1 节点取余分区 -##### 4redis节点 -![](https://img-blog.csdnimg.cn/20210505141558125.png) -20 个数据 -![](https://img-blog.csdnimg.cn/20210505141612173.png) -数据分布 -![](https://img-blog.csdnimg.cn/20210505141633953.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -##### 5redis节点 -数据分布 -![](https://img-blog.csdnimg.cn/20210505141711115.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -> 蓝色表与4个节点时是相同的槽。 - -可见,redis0只有20命中、redis1只有1命中、redis2只有2命中、redis3只有3命中。最终命中率是: 4/20=20% - -- hash(key) % nodes -![](https://img-blog.csdnimg.cn/20210430170928276.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -#### 数据迁移 -当添加一个节点时 -![](https://img-blog.csdnimg.cn/20210430171024286.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 多倍扩容 -![](https://img-blog.csdnimg.cn/20210430171121829.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -客户端分片:哈希+取余。 -节点伸缩:数据节点关系变化,导致数据迁移。迁移数量和添加节点数量有关:建议翻倍扩容。 -优点:实现简单 -缺点:当扩容或收缩节点时,需要迁移的数据量大(虽然翻倍扩容可以相对减少迁移量) - -#### 2.2.1.2 一致性哈希分区(Consistent hashing) -##### 原理 -- 环形 hash 空间 -按常用 hash 算法,将对应的 key hash到一个具有 `2^32` 个桶的空间,即(0 ~ `2^32` - 1)的数字空间中。 - -将这些数字头尾相连,想象成一个闭合环形: -- 把数据通过一定的 hash 算法映射到环上 -- 将机器通过一定的 hash 算法映射到环上 -- 节点按顺时针转动,遇到的第一个机器,就把数据放在该机器 - -- 把对象映射到hash空间 -![](https://img-blog.csdnimg.cn/20210511100911394.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -#### 把cache映射到hash空间 -基本思想就是将对象和cache都映射到同一个hash数值空间中, 并且使用相同的hash算法 - -```bash -hash(cache A) = key A; -hash(cache C) = key C; -``` -在移除 or 添加一个 cache 时,能够尽可能小的改变已存在的 key 映射关系 -#### Consistent hashing 一致性算法 -![](https://img-blog.csdnimg.cn/c9800de2da2844eb93882a508076573c.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -##### 移除 Cache -- 删除CacheB后,橙色区为被影响范围 -![](https://img-blog.csdnimg.cn/d1ef70c6454e4346bcdbe69c4c8a919e.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -##### 添加Cache![](https://img-blog.csdnimg.cn/dfe4a2f89dae48e4abf7017814330f7c.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 理想的分布式 -![](https://img-blog.csdnimg.cn/20210505145541903.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -现实却很拥挤-即倾斜性: -![](https://img-blog.csdnimg.cn/20210505145603981.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -###### Hash倾斜性 -![](https://img-blog.csdnimg.cn/bf37282868b044848234ac52d86b748f.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -为解决该问题,引入虚拟节点 -![](https://img-blog.csdnimg.cn/503943a48f194bf385efecd88bfc37f1.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -- 虚拟节点 -![](https://img-blog.csdnimg.cn/20210505145715687.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -命中率计算公式:服务器台数n,新增服务器数m -```java -(1 - n/(n + m) ) * 100% -``` - -![](https://img-blog.csdnimg.cn/20210430171238577.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 一致性哈希-扩容 -![](https://img-blog.csdnimg.cn/20210430171258523.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -客户端分片:哈希+顺时针(优化取余) -节点伸缩:只影响邻近节点,但还是有数据迁移 -翻倍伸缩:保证最小迁移数据和负载均衡 -#### 2.2.1.3 虚拟槽哈希分区(Redis Cluster采用) -- 虚拟槽分配 -![](https://img-blog.csdnimg.cn/img_convert/9c34e9351e74c1aacf6954ce66dba007.png) -- 预设虚拟槽 -每个槽映射一个数据子集, 一般比节点数大 -- 良好的哈希函数 -例如CRC16 -- 服务端管理节点、槽、数据 - -### 特点 -- 数据分散度高 -- 键值分布业务无关 -- 无法顺序访问 -- 支持批量操作 - -### 产品 -- 一致性哈希Memcache -- Redis Cluster -- ... +## 2.1 范围分片(range partitioning) +最简单的分片方式。通过映射对象的范围到指定的 Redis 实例来完成分片。 +例如,可假设用户从 ID 0 ~ 10000 进入实例 R0,10001 ~ 20000 进入实例 R1。 + +这套办法行得通,并且事实上在实践中被很多人采用。 + +### 2.1.1 缺点 +需要一个映射范围到实例的表格。该表需要管理,不同类型的对象都需要一个表,所以范围分片在 Redis 中常常并不可取,因这要比其他分片可选方案低效得多。 + +## 2.2 `哈希分片(hash partitioning)` +该模式适于任何键,不必是 `object_name:` 形式,就像这样简单: +1. 使用一个哈希函数(例如crc32) ,将键名转为一个数字,比如93024922 +2. 对该数据进行取模,将其转换为一个 0 到 3 之间数字,该数字即可映射到4个 节点之一。93024922 模 4 等于 2,所以键 foobar 应当存储到 R2 哈希分片的一种高端形式称为一致性哈希(consistent hashing),被一些 **Redis 客户端和代理实现**。 # 3 分片的各种实现 -可由软件栈中的不同部分来承担。 +分片可由软件栈中的不同部分来承担。 + ## 3.1 客户端分片 客户端直接选择正确节点来写入和读取指定键,许多 Redis 客户端实现了客户端分片。 + ## 3.2 代理协助分片 客户端发送请求到一个可以理解 Redis 协议的代理上,而不是直接发送到 Redis 实例。代理会根据配置好的分片模式,来保证转发我们的请求到正确的 Redis 实例,并返回响应给客户端。 -Redis 和 Memcached 的代理 Twemproxy 都实现了代理协助的分片。 +Redis 和 Memcached 的代理 Twemproxy 都实现了代理协助的分片. + ## 3.3 查询路由 + 可发送你的查询到一个随机实例,该实例会保证转发你的查询到正确节点。 Redis 集群在客户端的帮助下,实现了查询路由的一种混合形式,请求不是直接从 Redis 实例转发到另一个,而是客户端收到重定向到正确的节点。 # 4 分片的缺点 Redis 的一些特性与分片在一起时玩的不是很好: + - 涉及多个键的操作通常不支持。例如,无法直接对映射在两个不同 Redis 实例上的键执行交集 - 涉及多个键的事务不能使用 - 分片的粒度是键,所以不能使用一个很大的键来分片数据集,例如一个很大的sorted set - 当使用了分片,数据处理变得更复杂。例如,你需要处理多个 RDB/AOF 文件,备份数据时需要聚合多个实例和主机的持久化文件 - 添加和删除容量也很复杂。例如,Redis 集群具有运行时动态添加和删除节点的能力来支持透明地再均衡数据,但是其他方式,像客户端分片和代理都不支持这个特性。但有一种称为预分片(Presharding)的技术在这一点上能帮上忙。 + # 5 数据存储or缓存? 尽管无论是将 Redis 作为数据存储还是缓存,Redis 分片概念上都是一样的。 - 但作为数据存储时有个重要局限:当 Redis 作为数据存储时,一个给定的键总是映射到相同 Redis 实例。 @@ -181,6 +63,7 @@ Redis 的一些特性与分片在一起时玩的不是很好: 主要概念: - 如果 Redis 用作缓存,使用一致性哈希来实现伸缩扩展很容易 - 如果 Redis 用作存储,使用固定的键到节点的映射,所以节点的数量必须固定不能改变。否则,当增删节点时,就需要一个支持再平衡节点间键的系统,当前只有 Redis 集群可以做到这点。 + # 6 预分片 分片存在一个问题,除非我们使用 Redis 作为缓存,否则增加和删除节点都是件麻烦事,而使用固定的键和实例映射要简单得多。 @@ -191,6 +74,7 @@ Redis 的一些特性与分片在一起时玩的不是很好: 这样,当数据存储增长,需要更多 Redis 服务器,你要做的就是简单地将实例从一台服务器移动到另外一台。当你新添加了第一台服务器,你就需要把一半的 Redis 实例从第一台服务器搬到第二台,以此类推。 使用 Redis 复制,就可以在很小或者根本不需要停机的时间内完成移动数据: + 1. 在新服务器上启动一个空实例 2. 移动数据,配置新实例为源实例的从服务 3. 停止客户端 @@ -200,6 +84,8 @@ Redis 的一些特性与分片在一起时玩的不是很好: 7. 最后关闭掉旧服务器上不再使用的实例 # 7 Redis分片实现 +探讨完 Redis 分片理论,如何实践呢?又应该使用什么系统呢? + ## 7.1 Redis 集群 Redis 集群是自动分片和高可用的首选方式。一旦 Redis 集群以及支持 Redis 集群的客户端可用,Redis 集群将会成为 Redis 分片的事实标准。 @@ -216,8 +102,9 @@ Twemproxy 支持在多个 Redis 实例间自动分片,若节点不可用,还 从根本上说,Twemproxy 是介于客户端和 Redis 实例之间的中间层,这就可以在最下的额外复杂性下可靠地处理我们的分片。这是当前建议的处理 Redis 分片的方式。 ## 7.3 支持一致性哈希的客户端 + Twemproxy 之外的可选方案,是使用实现了客户端分片的客户端,通过一致性哈希或者别的类似算法。有多个支持一致性哈希的 Redis 客户端,例如 Redis-rb 和 Predis。 -查看完整的 Redis 客户端列表,看看是不是有支持你的编程语言的,并实现了一致性哈希的成熟客户端即可。 +查看完整的 Redis 客户端列表,看看是不是有支持你的编程语言的,并实现了一致性哈希的成熟客户端即可~ -> 参考 -> - https://redis.io/topics/partitioning \ No newline at end of file +参考 +- https://redis.io/topics/partitioning \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\221\275\344\273\244.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\221\275\344\273\244.md" index e8d1ca0c3c..1704ae2675 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\221\275\344\273\244.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\221\275\344\273\244.md" @@ -315,7 +315,7 @@ Redis 4.0.0 起,HSET 是万数值,允许多个字段/值对。 如果 key 指定的哈希集不存在,会创建一个新的哈希集并与 key 关联 如果字段已存在,该操作无效果 -# 4 list +# 4 list 结构 双向列表,适用于最新列表,关注列表 ## 1. lpush @@ -364,9 +364,9 @@ start 和 end 偏移量都是基于0的下标,即list的第一个元素下标 移除并且返回 key 对应的 list 的第一个元素 ### 返回值 bulk-string-reply返回第一个元素的值,或者当 key 不存在时返回 nil。 -## 7 rpop +##7. rpop 移除并返回存于 key 的 list 的最后一个元素。 -### 返回值 +###返回值 bulk-string-reply最后一个元素的值,或者当 key 不存在的时候返回 nil ## 8 bl-pop key [key ...] timeout 阻塞列表的弹出 diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\244\215\345\210\266.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\244\215\345\210\266.md" new file mode 100644 index 0000000000..db08ef6c1f --- /dev/null +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\244\215\345\210\266.md" @@ -0,0 +1,208 @@ +> 全是干货的技术号: +> 本文已收录在github,欢迎 star/fork: +> https://github.com/Wasabi1234/Java-Interview-Tutorial + +在Redis复制的基础上(不包括Redis Cluster或Redis Sentinel作为附加层提供的高可用功能),使用和配置主从复制非常简单,能使得从 Redis 服务器(下文称 slave)能精确得复制主 Redis 服务器(下文称 master)的内容。每次当 slave 和 master 之间的连接断开时, slave 会自动重连到 master 上,并且无论这期间 master 发生了什么, slave 都将尝试让自身成为 master 的精确副本。 + +该系统的运行依靠三个重要机制: +1. 当一个 master 实例和一个 slave 实例连接正常时, master 会发送一连串命令流保持对 slave 的更新,以便将自身数据集的改变复制给 slave,这包括客户端的写入、key 的过期或被逐出等等 +2. 当 master 和 slave 断连后,因为网络问题、或者是主从意识到连接超时, slave 重新连接上 master 并会尝试进行部分重同步:这意味着它会尝试只获取在断开连接期间内丢失的命令流 +3. 当无法进行部分重新同步时, slave 会请求全量重同步。这涉及到一个更复杂过程,比如master 需要创建所有数据的快照,将之发送给 slave ,之后在数据集更改时持续发送命令流到 slave + + +Redis使用默认的异步复制,低延迟且高性能,适用于大多数 Redis 场景。但是,slave会异步确认其从master周期接收到的数据量。 + +客户端可使用 WAIT 命令来请求同步复制某些特定的数据。但是,WAIT 命令只能确保在其他 Redis 实例中有指定数量的已确认的副本:在故障转移期间,由于不同原因的故障转移或是由于 Redis 持久性的实际配置,故障转移期间确认的写入操作可能仍然会丢失。 + +# Redis 复制特点 +- Redis 使用异步复制,slave 和 master 之间异步地确认处理的数据量 +- 一个 master 可以拥有多个 slave +- slave 可以接受其他 slave 的连接。除了多个 slave 可以连接到同一 master , slave 之间也可以像层级连接其它 slave。Redis 4.0 起,所有的 sub-slave 将会从 master 收到完全一样的复制流 +- Redis 复制在 master 侧是非阻塞的,即master 在一或多 slave 进行初次同步或者是部分重同步时,可以继续处理查询请求 +- 复制在 slave 侧大部分也是非阻塞的。当 slave 进行初次同步时,它可以使用旧数据集处理查询请求,假设在 redis.conf 中配置了让 Redis 这样做的话。否则,你可以配置如果复制流断开, Redis slave 会返回一个 error 给客户端。但是,在初次同步之后,旧数据集必须被删除,同时加载新的数据集。 slave 在这个短暂的时间窗口内(如果数据集很大,会持续较长时间),会阻塞到来的连接请求。自 Redis 4.0 开始,可以配置 Redis 使删除旧数据集的操作在另一个不同的线程中进行,但是,加载新数据集的操作依然需要在主线程中进行并且会阻塞 slave +- 复制可被用在可伸缩性,以便只读查询可以有多个 slave 进行(例如 O(N) 复杂度的慢操作可以被下放到 slave ),或者仅用于数据安全和高可用 +- 可使用复制来避免 master 将全部数据集写入磁盘造成的开销:一种典型的技术是配置你的 master 的 `redis.conf`以避免对磁盘进行持久化,然后连接一个 slave ,配置为不定期保存或是启用 AOF。但是,这个设置必须小心处理,因为重启的 master 将从一个空数据集开始:如果一个 slave 试图与它同步,那么这个 slave 也会被清空! + +# 1 单机“危机” +- 容量瓶颈 +- 机器故障 +- QPS瓶颈 + + + +![](https://img-blog.csdnimg.cn/20200904132333485.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70#pic_center) + +- 一主多从 +![](https://img-blog.csdnimg.cn/20200904150126617.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70#pic_center) + +## 主从复制作用 +- 数据副本 +- 扩展读性能 + +## 总结 +1. 一个master可以有多个slave +2. 一个slave只能有一个master +3. 数据流向是单向的,master => slave + +# 2 实现复制的操作 +如下两种实现方式: + +### slaveof 命令 +- 异步执行,很耗时间 +![](https://img-blog.csdnimg.cn/20200904150903762.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70#pic_center) + +无需重启,但是不便于配置的管理。 + +### 配置 +```shell +slaveof ip port +slave-read-only yes +``` +虽然可统一配置,但是需要重启。 + +# 3 全量复制 +1. master执行`bgsave`,在本地生成一份RDB快照client-output-buffer-limit slave 256MB 64MB 60 +2. master node将RDB快照发送给salve node,若RDB复制时间超过60秒(repl-timeout),那么slave node就会认为复制失败,可适当调大该参数(对于千兆网卡的机器,一般每秒传输100MB,6G文件,很可能超过60s) +3. master node在生成RDB时,会将所有新的写命令缓存在内存中,在salve node保存了rdb之后,再将新的写命令复制给salve node +4. 若在复制期间,内存缓冲区持续消耗超过64MB,或者一次性超过256MB,那么停止复制,复制失败 +5. slave node接收到RDB之后,清空自己的旧数据,然后重新加载RDB到自己的内存中,同时**基于旧的数据版本**对外提供服务 +6. 如果slave node开启了AOF,那么会立即执行BGREWRITEAOF,重写AOF + +RDB生成、RDB通过网络拷贝、slave旧数据的清理、slave aof rewrite,很耗费时间 + +如果复制的数据量在4G~6G之间,那么很可能全量复制时间消耗到1分半到2分钟 +## 3.1 全量复制开销 +1. bgsave时间 +2. RDB文件网络传输时间 +3. 从节点清空数据时间 +4. 从节点加载RDB的时间 +5. 可能的AOF重写时间 + +## 3.2 全量同步细节 +master 开启一个后台save进程,以便生成一个 RDB 文件。同时它开始缓冲所有从客户端接收到的新的写入命令。当后台save完成RDB文件时, master 将该RDB数据集文件发给 slave, slave会先将其写入磁盘,然后再从磁盘加载到内存。再然后 master 会发送所有缓存的写命令发给 slave。这个过程以指令流的形式完成并且和 Redis 协议本身的格式相同。 + +当主从之间的连接因为一些原因崩溃之后, slave 能够自动重连。如果 master 收到了多个 slave 要求同步的请求,它会执行一个单独的后台保存,以便于为多个 slave 服务。 + +# 4 增量复制 + +1. 如果全量复制过程中,master-slave网络连接中断,那么salve重连master时,会触发增量复制 +2. master直接从自己的backlog中获取部分丢失的数据,发送给slave node +3. msater就是根据slave发送的psync中的offset来从backlog中获取数据的 +![](https://img-blog.csdnimg.cn/20200905001841252.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70#pic_center) + +# 5 master关闭持久化时的复制安全性 +在使用 Redis 复制功能时的设置中,推荐在 master 和 slave 中启用持久化。 +当不可能启用时,例如由于非常慢的磁盘性能而导致的延迟问题,应该配置实例来避免重启后自动重新开始复制。 + +关闭持久化并配置了自动重启的 master 是危险的: +1. 设置节点 A 为 master 并关闭它的持久化设置,节点 B 和 C 从 节点 A 复制数据 +2. 节点 A 宕机,但它有一些自动重启系统可重启进程。但由于持久化被关闭了,节点重启后其数据集是空的! +3. 这时B、C 会从A复制数据,但A数据集空,因此复制结果是它们会销毁自身之前的数据副本! + +当 Redis Sentinel 被用于高可用并且 master 关闭持久化,这时如果允许自动重启进程也是很危险的。例如, master 可以重启的足够快以致于 Sentinel 没有探测到故障,因此上述的故障模式也会发生。 +任何时候数据安全性都是很重要的,所以如果 master 使用复制功能的同时未配置持久化,那么自动重启进程这项就该被禁用。 + +# 6 复制工作原理 +- 每个 master 都有一个 replication ID :一个较大的伪随机字符串,标记了一个给定的数据集。 +![](https://img-blog.csdnimg.cn/20200905221152322.png#pic_center) + +- 每个 master 也持有一个偏移量,master 将自己产生的复制流发送给 slave 时,发送多少个字节的数据,自身的偏移量就会增加多少,目的是当有新的操作修改自己的数据集时,它可据此更新 slave 的状态。 +![](https://img-blog.csdnimg.cn/20200905221515483.png#pic_center) + +复制偏移量即使在没有一个 slave 连接到 master 时,也会自增,所以基本上每一对给定的 +`Replication ID, offset` +都会标识一个 master 数据集的确切版本。 + +## psync +slave使用`psync`从master复制,psync runid offset +![](https://img-blog.csdnimg.cn/20200905233312575.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70#pic_center) + +master会根据自身情况返回响应信息: +- 可能是FULLRESYNC runid offset触发全量复制 +- 可能是CONTINUE触发增量复制 + +slave 连接到 master 时,它们使用 PSYNC 命令来发送它们记录的旧的 master replication ID 和它们至今为止处理的偏移量。通过这种方式, master 能够仅发送 slave 所需的增量部分。 +但若 master 的缓冲区中没有足够的命令积压缓冲记录,或者如果 slave 引用了不再知道的历史记录(replication ID),则会转而进行一个全量重同步:在这种情况下, slave 会得到一个完整的数据集副本,从头开始。即: +- 若slave重连master,那么master仅会复制给slave缺少的部分数据 +- 若第一次连接master,那么会触发全量复制 + + +# 7 复制的完整流程 +![](https://img-blog.csdnimg.cn/20190705083122154.png) + +> slave如果跟master有网络故障,断开连接会自动重连。 +> master如果发现有多个slave都重新连接,仅会启动一个rdb save操作,用一份数据服务所有slave。 + + +1. slave启动,仅保存master的信息,包括master的`host`和`ip`,但复制流程尚未开始master host和ip配置在 `redis.conf` 中的 slaveof +2. slave内部有个定时任务,每s检查是否有新的master要连接和复制,若发现,就跟master建立socket网络连接。 +3. slave发送ping命令给master +4. 口令认证 - 若master设置了requirepass,那么salve必须同时发送masterauth的口令认证 +5. master **第一次执行全量复制**,将所有数据发给slave +6. master后续持续将写命令,异步复制给slave + +## heartbeat +主从节点互相都会发送heartbeat信息。 +master默认每隔10秒发送一次heartbeat,salve node每隔1秒发送一个heartbeat。 + +# 8 断点续传 +Redis 2.8开始支持主从复制的断点续传 +![](https://img-blog.csdnimg.cn/2019070508465819.png) + +主从复制过程,若网络连接中断,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。 + +## master和slave都会维护一个offset +- master在自身基础上累加offset,slave亦是 +- slave每秒都会上报自己的offset给master,同时master保存每个slave的offset + +master和slave都要知道各自数据的offset,才能知晓互相之间的数据不一致情况。 + +## backlog +master会在内存中维护一个backlog,默认1MB。master给slave复制数据时,也会将数据在backlog中同步写一份。 + +`backlog主要是用做全量复制中断时候的增量复制`。 + +master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的。若master和slave网络连接中断,slave会让master从上次replica offset开始继续复制。但若没有找到对应offset,就会执行resynchronization。 + +## master run id +- info server,可见master run id +![](https://img-blog.csdnimg.cn/20200905232843801.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70#pic_center) + +根据host+ip定位master node,是不靠谱的,如果master node重启或者数据出现了变化,那么slave node应该根据不同的run id区分,run id不同就做全量复制。 +如果需要不更改run id重启redis,可使用: +```shell +redis-cli debug reload +``` + +# 9 无磁盘化复制 +master在内存中直接创建RDB,然后发送给slave,不会在自己本地持久化。 +只需要在配置文件中开启` repl-diskless-sync yes `即可. + +```shell +等待 5s 再开始复制,因为要等更多 slave 重连 +repl-diskless-sync-delay 5 +``` + +# 10 处理过期key +Redis 的过期机制可以限制 key 的生存时间。此功能取决于 Redis 实例计算时间的能力,但是,即使使用 Lua 脚本更改了这些 key,Redis slaves 也能正确地复制具有过期时间的 key。 + +为实现这功能,Redis 不能依靠主从使用同步时钟,因为这是一个无法解决的问题并且会导致 race condition 和数据不一致,所以 Redis 使用三种主要的技术使过期的 key 的复制能够正确工作: +1. slave 不会让 key 过期,而是等待 master 让 key 过期。当一个 master 让一个 key 到期(或由于 LRU 算法删除)时,它会合成一个 DEL 命令并传输到所有 slave +2. 但由于这是 master 驱动的 key 过期行为,master 无法及时提供 DEL 命令,所以有时 slave 的内存中仍然可能存在逻辑上已过期的 key 。为了处理这问题,slave 使用它的逻辑时钟以报告只有在不违反数据集的一致性的读取操作(从主机的新命令到达)中才存在 key。用这种方法,slave 避免报告逻辑过期的 key 仍然存在。在实际应用中,使用 slave 程序进行缩放的 HTML 碎片缓存,将避免返回已经比期望的时间更早的数据项 +3. 在Lua脚本执行期间,不执行任何 key 过期操作。当一个Lua脚本运行时,从概念上讲,master 中的时间是被冻结的,这样脚本运行的时候,一个给定的键要么存在要么不存在。这可以防止 key 在脚本中间过期,保证将相同的脚本发送到 slave ,从而在二者的数据集中产生相同的效果。 + +一旦 slave 被提升 master ,它将开始独立过期 key,而不需要任何旧 master 帮助。 + +# 11 重新启动和故障转移后的部分重同步 +Redis 4.0 开始,当一个实例在故障转移后被提升为 master 时,它仍然能够与旧 master 的 slave 进行部分重同步。为此,slave 会记住旧 master 的旧 replication ID 和复制偏移量,因此即使询问旧的 replication ID,也可以将部分复制缓冲提供给连接的 slave 。 + +但是,升级的 slave 的新 replication ID 将不同,因为它构成了数据集的不同历史记录。例如,master 可以返回可用,并且可以在一段时间内继续接受写入命令,因此在被提升的 slave 中使用相同的 replication ID 将违反一对复制标识和偏移对只能标识单一数据集的规则。 + +另外,slave 在关机并重新启动后,能够在 RDB 文件中存储所需信息,以便与 master 进行重同步。这在升级的情况下很有用。当需要时,最好使用 SHUTDOWN 命令来执行 slave 的保存和退出操作。 + +# 参考 +- https://raw.githubusercontent.com/antirez/redis/2.8/00-RELEASENOTES +- https://redis.io/topics/replication + + +![](https://img-blog.csdnimg.cn/20200825235213822.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\346\225\260\346\215\256\347\273\223\346\236\204\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\346\225\260\346\215\256\347\273\223\346\236\204\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" deleted file mode 100644 index 8cf19f5f5b..0000000000 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\346\225\260\346\215\256\347\273\223\346\236\204\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" +++ /dev/null @@ -1,552 +0,0 @@ -# 1 概述 -## 数据结构和内部编码 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWEyYzI5MzFmYzlkMGQ1OGEucG5n?x-oss-process=image/format,png) -## 无传统关系型数据库的 Table 模型 -schema 所对应的db仅以编号区分。同一 db 内,key 作为顶层模型,它的值是扁平化的。即 db 就是key的命名空间。 -key的定义通常以 `:` 分隔,如:`Article:Count:1` -常用的Redis数据类型有:string、list、set、map、sorted-set -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWEwNzBkZWExMTNlYWY4ZDEucG5n?x-oss-process=image/format,png) -## redisObject通用结构 -Redis中的所有value 都是以object 的形式存在的,其通用结构如下 -![](https://img-blog.csdnimg.cn/2021061610354559.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- type 数据类型 -指 string、list 等类型 -- encoding 编码方式 -指的是这些结构化类型具体的实现方式,同一个类型可以有多种实现。e.g. string 可以用int 来实现,也可以使用char[] 来实现;list 可以用ziplist 或者链表来实现 -- lru -本对象的空转时长,用于有限内存下长时间不访问的对象清理 -- refcount -对象引用计数,用于GC -- ptr 数据指针 -指向以 encoding 方式实现这个对象实际实现者的地址。如:string 对象对应的SDS地址(string的数据结构/简单动态字符串) - -## 单线程 -![](https://img-blog.csdnimg.cn/20210205224515421.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -### 单线程为何这么快? -- 纯内存 -- 非阻塞I/O -- 避免线程切换和竞态消耗 -![](https://img-blog.csdnimg.cn/20210205224529469.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 一次只运行一条命令 -- 拒绝长(慢)命令 -keys, flushall, flushdb, slow lua script, mutil/exec, operate big value(collection) -- 其实不是单线程 -fysnc file descriptor -close file descriptor - -# 2 string -Redis中的 string 可表示很多语义 -- 字节串(bits) -- 整数 -- 浮点数 - -redis会根据具体的场景完成自动转换,并根据需要选取底层的实现方式。 -例如整数可以由32-bit/64-bit、有符号/无符号承载,以适应不同场景对值域的要求。 - -- 字符串键值结构,也能是 JSON 串或 XML 结构 -![](https://img-blog.csdnimg.cn/20210205224713852.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -## 内存结构 -在Redis内部,string的内部以 int、SDS(简单动态字符串 simple dynamic string)作为存储结构 -- int 用来存放整型 -- SDS 用来存放字节/字符和浮点型SDS结构 -### SDS -```c -typedef struct sdshdr { - // buf中已经占用的字符长度 - unsigned int len; - // buf中剩余可用的字符长度 - unsigned int free; - // 数据空间 - char buf[]; -} -``` -- 结构图![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQ3OWZlMGFhYzZmYmU0NGQucG5n?x-oss-process=image/format,png) -存储的内容为“Redis”,Redis采用类似C语言的存储方法,使用'\0'结尾(仅是定界符)。 -SDS的free 空间大小为0,当free > 0时,buf中的free 区域的引入提升了SDS对字符串的处理性能,可以减少处理过程中的内存申请和释放次数。 -### buf 的扩容与缩容 -当对SDS 进行操作时,如果超出了容量。SDS会对其进行扩容,触发条件如下: -- 字节串初始化时,buf的大小 = len + 1,即加上定界符'\0'刚好用完所有空间 -- 当对串的操作后小于1M时,扩容后的buf 大小 = 业务串预期长度 * 2 + 1,也就是扩大2倍。 -- 对于大小 > 1M的长串,buf总是留出 1M的 free空间,即2倍扩容,但是free最大为 1M。 -### 字节串与字符串 -SDS中存储的内容可以是ASCII 字符串,也可以是字节串。由于SDS通过len 字段来确定业务串的长度,因此业务串可以存储非文本内容。对于字符串的场景,buf[len] 作为业务串结尾的'\0' 又可以复用C的已有字符串函数。 -### SDS编码的优化 -value 在内存中有2个部分:redisObject和ptr指向的字节串部分。 -在创建时,通常要分别为2个部分申请内存,但是对于小字节串,可以一次性申请。 - -incr userid:pageview (单线程:无竞争)。缓存视频的基本信息(数据源在MySQL) -![](https://img-blog.csdnimg.cn/20210205230250128.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -```java -public VideoInfo get(Long id) { - String redisKey = redisPrefix + id; - VideoInfo videoInfo e redis.get(redisKey); - if (videoInfo == null) { - videoInfo = mysql.get(id); - if (videoInfo != null) { - // 序列化 - redis.set(redisKey serialize(videoInfo)): - } - } -} -``` - - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWUwMjdmOWUwYmY4NWQ2MzgucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LThhYmU4MTk1OTZkNzE1ZmYucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQyYzYzMTk4ZDMzZWU2Y2QucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTZlMDM2MWI2NGRhODI1MzMucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTIxZWVjZDkwNzI2MmJjN2QucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTYxYTUwYWI3Y2QzN2FhYjkucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTRhOTg4NmI4ZGEyNGZlZDIucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWJiODU2MDBiM2I1Mjg4ODgucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTI3NjAzOTRkNTk5M2FkODgucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQwNzZjOWM1ZGQwZjUwNzMucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWMwODNlYzM5MzQ0ZmEyM2EucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQxYzRmZGNlN2Q3ZDA2YTQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTJlN2RiOGNiNTU2Mzg3ZWQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTBhODMxZmE4NjQwNmJjMjkucG5n?x-oss-process=image/format,png) -![String类型的value基本操作](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTdjZGRlZTM3NGU4M2I3ZDYucG5n?x-oss-process=image/format,png) -除此之外,string 类型的value还有一些CAS的原子操作,如:get、set、set value nx(如果不存在就设置)、set value xx(如果存在就设置)。 - -String 类型是二进制安全的,也就是说在Redis中String类型可以包含各种数据,比如一张JPEG图片或者是一个序列化的Ruby对象。一个String类型的值最大长度可以是512M。 - -在Redis中String有很多有趣的用法 -* 把String当做原子计数器,这可以使用INCR家族中的命令来实现:[INCR](https://github.com/antirez/redis-doc/blob/master/commands/incr), [DECR](https://github.com/antirez/redis-doc/blob/master/commands/decr), [INCRBY](https://github.com/antirez/redis-doc/blob/master/commands/incrby)。 -* 使用[APPEND](https://github.com/antirez/redis-doc/blob/master/commands/append)命令来给一个String追加内容。 -* 把String当做一个随机访问的向量(Vector),这可以使用[GETRANGE](https://github.com/antirez/redis-doc/blob/master/commands/getrange)和 [SETRANGE](https://github.com/antirez/redis-doc/blob/master/commands/setrange)命令来实现 -* 使用[GETBIT](https://github.com/antirez/redis-doc/blob/master/commands/getbit) 和[SETBIT](https://github.com/antirez/redis-doc/blob/master/commands/setbit)方法,在一个很小的空间中编码大量的数据,或者创建一个基于Redis的Bloom Filter 算法。 -# List -可从头部(左侧)加入元素,也可以从尾部(右侧)加入元素。有序列表。 - -像微博粉丝,即可以list存储做缓存。 -```bash -key = 某大v - -value = [zhangsan, lisi, wangwu] -``` -所以可存储一些list型的数据结构,如: -- 粉丝列表 -- 文章的评论列表 - -可通过lrange命令,即从某元素开始读取多少元素,可基于list实现分页查询,这就是基于redis实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。 - -搞个简单的消息队列,从list头推进去,从list尾拉出来。 - -List类型中存储一系列String值,这些String按照插入顺序排序。 - -## 5.1 内存数据结构 -List 类型的 value对象,由 linkedlist 或 ziplist 实现。 -当 List `元素个数少并且元素内容长度不大`采用ziplist 实现,否则使用linkedlist - -### 5.1.1 linkedlist实现 -链表的代码结构 -```c -typedef struct list { - // 头结点 - listNode *head; - // 尾节点 - listNode *tail; - // 节点值复制函数 - void *(*dup)(void * ptr); - // 节点值释放函数 - void *(*free)(void *ptr); - // 节点值对比函数 - int (*match)(void *ptr, void *key); - // 链表长度 - unsigned long len; -} list; - -// Node节点结构 -typedef struct listNode { - struct listNode *prev; - struct listNode *next; - void *value; -} listNode; -``` -linkedlist 结构图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTg1NzAyZDg2NzYxZDFiZjMucG5n?x-oss-process=image/format,png) -### 5.1.2 ziplist实现 -存储在连续内存 -![](https://img-blog.csdnimg.cn/ac58e5b93d294a958940f681d4b165e5.png) -- zlbytes -ziplist 的总长度 -- zltail -指向最末元素。 -- zllen -元素的个数。 -- entry -元素内容。 -- zlend -恒为0xFF,作为ziplist的定界符 - -linkedlist和ziplist的rpush、rpop、llen的时间复杂度都是O(1): -- ziplist的lpush、lpop都会牵扯到所有数据的移动,时间复杂度为O(N) -由于List的元素少,体积小,这种情况还是可控的。 - -ziplist的Entry结构: -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTJkNWQ1YWI3NGRhYmZhOTkucG5n?x-oss-process=image/format,png) -记录前一个相邻的Entry的长度,便于双向遍历,类似linkedlist的prev指针。 -ziplist是连续存储,指针由偏移量来承载。 -Redis中实现了2种方式实现: -- 当前邻 Entry的长度小于254 时,使用1字节实现 -- 否则使用5个字节 - -> 当前一个Entry长度变化时,可能导致后续的所有空间移动,虽然这种情况发生可能性较小。 - -Entry内容本身是自描述的,意味着第二部分(Entry内容)包含了几个信息:Entry内容类型、长度和内容本身。而内容本身包含:类型长度部分和内容本身部分。类型和长度同样采用变长编码: -- 00xxxxxx :string类型;长度小于64,0~63可由6位bit 表示,即xxxxxx表示长度 -- 01xxxxxx|yyyyyyyy : string类型;长度范围是[64, 16383],可由14位 bit 表示,即xxxxxxyyyyyyyy这14位表示长度。 -- 10xxxxxx|yy..y(32个y) : string类型,长度大于16383. -- 1111xxxx :integer类型,integer本身内容存储在xxxx 中,只能是1~13之间取值。也就是说内容类型已经包含了内容本身。 -- 11xxxxxx :其余的情况,Redis用1个字节的类型长度表示了integer的其他几种情况,如:int_32、int_24等。 -由此可见,ziplist 的元素结构采用的是可变长的压缩方法,针对于较小的整数/字符串的压缩效果较好 - -- LPUSH命令 -在头部加入一个新元素 -- RPUSH命令 -在尾部加入一个新元素 - -当在一个空的K执行这些操作时,会创建一个新列表。当一个操作清空了一个list时,该list对应的key会被删除。若使用一个不存在的K,就会使用一个空list。 -```bash -LPUSH mylist a   # 现在list是 "a" -LPUSH mylist b   # 现在list是"b","a" -RPUSH mylist c   # 现在list是 "b","a","c" (注意这次使用的是 RPUSH) -``` -list的最大长度是`2^32 – 1`个元素(4294967295,一个list中可以有多达40多亿个元素)。 - -从时间复杂度的角度来看,Redis list类型的最大特性是:即使是在list的头端或者尾端做百万次的插入和删除操作,也能保持稳定的很少的时间消耗。在list的两端访问元素是非常快的,但是如果要访问一个很大的list中的中间部分的元素就会比较慢了,时间复杂度是O(N) -## 适用场景 -- 社交中使用List进行时间表建模,使用 LPUSH 在用户时间线中加入新元素,然后使用 LRANGE 获得最近加入元素 -- 可以把[LPUSH] 和[LTRIM] 命令结合使用来实现定长的列表,列表中只保存最近的N个元素 -- 做MQ,依赖BLPOP这种阻塞命令 -# Set -类似List,但无序且其元素不重复。 - -向集合中添加多次相同的元素,集合中只存在一个该元素。在实际应用中,这意味着在添加一个元素前不需要先检查元素是否存在。 - -支持多个服务器端命令来从现有集合开始计算集合,所以执行集合的交集,并集,差集都很快。 - -set的最大长度是`2^32 – 1`个元素(一个set中可多达40多亿个元素)。 -## 内存数据结构 -Set在Redis中以intset 或 hashtable存储: -- 对于Set,HashTable的value永远为NULL -- 当Set中只包含整型数据时,采用intset作为实现 - -### intset -核心元素是一个字节数组,从小到大有序的存放元素 -![](https://img-blog.csdnimg.cn/20200911231000505.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -结构图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTczN2MzY2NiZDAxMzcxZjEucG5n?x-oss-process=image/format,png) -因为元素有序排列,所以SET的获取操作采用二分查找,复杂度为O(log(N))。 - -进行插入操作时: -- 首先通过二分查找到要插入位置 -- 再对元素进行扩容 -- 然后将插入位置之后的所有元素向后移动一个位置 -- 最后插入元素 - -时间复杂度为O(N)。为使二分查找的速度足够快,存储在content 中的元素是定长的。 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTkwODQ2NzBlNzk2MTJlMGIucG5n?x-oss-process=image/format,png) -当插入2018 时,所有的元素向后移动,并且不会发生覆盖。 -当Set 中存放的整型元素集中在小整数范围[-128, 127]内时,可大大的节省内存空间。 -IntSet支持升级,但是不支持降级。 - -- Set 基本操作 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LThkYmNkMTJiMDRlZmJjZmQucG5n?x-oss-process=image/format,png) -## 适用场景 -无序集合,自动去重,数据太多时不太推荐使用。 -直接基于set将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于JVM内存里的HashSet进行去重,但若你的某个系统部署在多台机器呢?就需要基于redis进行全局的set去重。 - -可基于set玩交集、并集、差集操作,比如交集: -- 把两个人的粉丝列表整一个交集,看看俩人的共同好友 -- 把两个大v的粉丝都放在两个set中,对两个set做交集 - -全局这种计算开销也大。 - -- 记录唯一的事物 -比如想知道访问某个博客的IP地址,不要重复的IP,这种情况只需要在每次处理一个请求时简单的使用SADD命令就可以了,可确保不会插入重复IP - -- 表示关系 -你可以使用Redis创建一个标签系统,每个标签使用一个Set表示。然后你可以使用SADD命令把具有特定标签的所有对象的所有ID放在表示这个标签的Set中 -如果你想要知道同时拥有三个不同标签的对象,那么使用SINTER - -- 可使用[SPOP](https://github.com/antirez/redis-doc/blob/master/commands/spop) 或 [SRANDMEMBER](https://github.com/antirez/redis-doc/blob/master/commands/srandmember) 命令从集合中随机的提取元素。 - -# Hash/Map -一般可将结构化的数据,比如一个对象(前提是这个对象未嵌套其他的对象)给缓存在redis里,然后每次读写缓存的时候,即可直接操作hash里的某个字段。 -```json -key=150 - -value={ - “id”: 150, - “name”: “zhangsan”, - “age”: 20 -} -``` -hash类的数据结构,主要存放一些对象,把一些简单的对象给缓存起来,后续操作的时候,可直接仅修改该对象中的某字段的值。 -```c -value={ - “id”: 150, - “name”: “zhangsan”, - “age”: 21 -} -``` -因为Redis本身是一个K.V存储结构,Hash结构可理解为subkey - subvalue -这里面的subkey - subvalue只能是 -- 整型 -- 浮点型 -- 字符串 - -因为Map的 value 可表示整型和浮点型,因此Map也可以使用` hincrby` 对某个field的value值做自增操作。 - -## 内存数据结构 -hash有HashTable 和 ziplist 两种实现。对于数据量较小的hash,使用ziplist 实现。 -### HashTable 实现 -HashTable在Redis 中分为3 层,自底向上分别是: -- dictEntry:管理一个field - value 对,保留同一桶中相邻元素的指针,以此维护Hash 桶中的内部链 -- dictht:维护Hash表的所有桶链 -- dict:当dictht需要扩容/缩容时,用户管理dictht的迁移 - -dict是Hash表存储的顶层结构 -```c -// 哈希表(字典)数据结构,Redis 的所有键值对都会存储在这里。其中包含两个哈希表。 -typedef struct dict { - // 哈希表的类型,包括哈希函数,比较函数,键值的内存释放函数 - dictType *type; - // 存储一些额外的数据 - void *privdata; - // 两个哈希表 - dictht ht[2]; - // 哈希表重置下标,指定的是哈希数组的数组下标 - int rehashidx; /* rehashing not in progress if rehashidx == -1 */ - // 绑定到哈希表的迭代器个数 - int iterators; /* number of iterators currently running */ -} dict; -``` -Hash表的核心结构是dictht,它的table 字段维护着 Hash 桶,桶(bucket)是一个数组,数组的元素指向桶中的第一个元素(dictEntry)。 - -```c -typedef struct dictht { - //槽位数组 - dictEntry **table; - //槽位数组长度 - unsigned long size; - //用于计算索引的掩码 - unsigned long sizemask; - //真正存储的键值对数量 - unsigned long used; -} dictht; -``` -结构图![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTEwYzRhMWFkMGY3MDExMTcucG5n?x-oss-process=image/format,png) -Hash表使用【链地址法】解决Hash冲突。当一个 bucket 中的 Entry 很多时,Hash表的插入性能会下降,此时就需要增加bucket的个数来减少Hash冲突。 -#### Hash表扩容 -和大多数Hash表实现一样,Redis引入负载因子判定是否需要增加bucket个数: -```bash -负载因子 = Hash表中已有元素 / bucket数量 -``` -扩容后,bucket 数量是原先2倍。目前有2 个阀值: -- 小于1 时一定不扩容 -- 大于5 时一定扩容 - -- 在1 ~ 5 之间时,Redis 如果没有进行`bgsave/bdrewrite` 操作时则会扩容 -- 当key - value 对减少时,低于0.1时会进行缩容。缩容之后,bucket的个数是原先的0.5倍 -### ziplist 实现 -这里的 ziplist 和List#ziplist的实现类似,都是通过Entry 存放元素。 -不同的是,Map#ziplist的Entry个数总是2的整数倍: -- 第奇数个Entry存放key -- 下个相邻Entry存放value - -ziplist承载时,Map的大多数操作不再是O(1)了,而是由Hash表遍历,变成了链表的遍历,复杂度变为O(N) -由于Map相对较小时采用ziplist,采用Hash表时计算hash值的开销较大,因此综合起来ziplist的性能相对好一些 - -哈希键值结构 -![](https://img-blog.csdnimg.cn/919d84762fc44637b12de9e9ebf11a94.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTE1MTAyZWVkODNjMzA4ZWMucG5n?x-oss-process=image/format,png) -特点: -- Map的map -- Small redis -- field不能相同,value可相同 - -```bash -hget key field O(1) -# 获取 hash key 对应的 field 的 value - -hset key field value O(1) -# 设置 hash key 对应的 field 的 value - -hdel key field O(1) -# 删除 hash key 对应的 field 的 value -``` -## 实操 -```bash -127.0.0.1:6379> hset user:1:info age 23 -(integer) 1 -127.0.0.1:6379> hget user:1:info age -"23" -127.0.0.1:6379> hset user:1:info name JavaEdge -(integer) 1 -127.0.0.1:6379> hgetall user:1:info -1) "age" -2) "23" -3) "name" -4) "JavaEdge" -127.0.0.1:6379> hdel user:1:info age -(integer) 1 -127.0.0.1:6379> hgetall user:1:info -1) "name" -2) "JavaEdge" -``` - -```bash -hexists key field O(1) -# 判断hash key是否有field -hlen key O(1) -# 获取hash key field的数量 -``` - -```bash -127.0.0.1:6379> hgetall user:1:info -1) "name" -2) "JavaEdge" -127.0.0.1:6379> HEXISTS user:1:info name -(integer) 1 -127.0.0.1:6379> HLEN user:1:info -(integer) 1 -``` - -```bash -hmget key field1 field2... fieldN O(N) -# 批量获取 hash key 的一批 field 对应的值 -hmset key field1 value1 field2 value2...fieldN valueN O(N) -# 批量设置 hash key的一批field value - -``` -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWU3NjM0MmRkMDlkNjAwNzYucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTc3M2FjMWI5NGVhZDA3MTAucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTFmZDkzNTYyMWNjYzg5NTUucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTJhODk3MjllYTA0ODg2YmQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTM3Y2ZiOTcxY2JlMDhiOTQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTFmMzJmMGJkMjk0MDFhZmEucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTE3OTRmMjk0NTMzZjQ2ZGMucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWVhMTVmZDBkNjg2YjlkNmQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTFmNzRhMjEwOTE5YjJhN2UucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LThhMWE1MjNhNWE4NDJiOTAucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTI1OThiNTVlN2FlNzA1ZGMucG5n?x-oss-process=image/format,png) -![方便单条更新,但是信息非整体,不便管理](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LThiODAyYmFhODgyNzBlZTgucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWE2ZDZhOGZmMmVmYTM4NmIucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTIwMjg4MjgzZDE1ODUyYWQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWI1YTNmZGQwODdjMTIzM2MucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTJjNzdkNWQzYTc1OTc5NzMucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTQ0NGYzNWU3MzM5MzRjNTkucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTA5OTc2M2M1YzM0YjM4YzUucG5n?x-oss-process=image/format,png) -Redis Hashes 保存String域和String值之间的映射,所以它们是用来表示对象的绝佳数据类型(比如一个有着用户名,密码等属性的User对象) -``` -| `1` | `@cli` | - -| `2` | `HMSET user:1000 username antirez password P1pp0 age 34` | - -| `3` | `HGETALL user:1000` | - -| `4` | `HSET user:1000 password 12345` | - -| `5` | `HGETALL user:1000` | -``` -一个有着少量数据域(这里的少量大概100上下)的hash,其存储方式占用很小的空间,所以在一个小的Redis实例中就可以存储上百万的这种对象 - -Hash的最大长度是2^32 – 1个域值对(4294967295,一个Hash中可以有多达40多亿个域值对) -# Sorted sets(zset) -有序集合,去重但可排序,写进去时候给个分数,可以自定义排序规则。比如想根据时间排序,则写时可以使用时间戳作为分数。 - -排行榜:将每个用户以及其对应的什么分数写进去。 -```bash -127.0.0.1:6379> zadd board 1.0 JavaEdge -(integer) 1 -``` - -获取排名前100的用户: -```bash -127.0.0.1:6379> zrevrange board 0 99 -1) "JavaEdge" -``` -用户在排行榜里的排名: -```bash -127.0.0.1:6379> zrank board JavaEdge -(integer) 0 -``` - -```bash -127.0.0.1:6379> zadd board 85 zhangsan -(integer) 1 -127.0.0.1:6379> zadd board 72 wangwu -(integer) 1 -127.0.0.1:6379> zadd board 96 lisi -(integer) 1 -127.0.0.1:6379> zadd board 62 zhaoliu -(integer) 1 - -# 获取排名前3的用户 -127.0.0.1:6379> zrevrange board 0 3 -1) "lisi" -2) "zhangsan" -3) "wangwu" -4) "zhaoliu" - -127.0.0.1:6379> zrank board zhaoliu -(integer) 1 -``` -类似于Map的key-value对,但有序 -- key :key-value对中的键,在一个Sorted-Set中不重复 -- value : 浮点数,称为 score -- 有序 :内部按照score 从小到大的顺序排列 -## 基本操作 -由于Sorted-Set 本身包含排序信息,在普通Set 的基础上,Sorted-Set 新增了一系列和排序相关的操作: -- Sorted-Set的基本操作 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTI2OTNlZDUxNGU0Njc5MTgucG5n?x-oss-process=image/format,png) -## 内部数据结构 -Sorted-Set类型的valueObject 内部结构有两种: -1. ziplist -![](https://img-blog.csdnimg.cn/20200911183043109.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -实现方式和Map类似,由于Sorted-Set包含了Score的排序信息,ziplist内部的key-value元素对的排序方式也是按照Score递增排序的,意味着每次插入数据都要移动之后的数据,因此ziplist适于元素个数不多,元素内容不大的场景。 -2. skiplist+hashtable -![](https://img-blog.csdnimg.cn/20200911183355830.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -更通用的场景,Sorted-Set使用sliplist来实现。 -### 8.2.1 zskiplist -和通用的跳表不同的是,Redis为每个level 对象增加了span 字段,表示该level 指向的forward节点和当前节点的距离,使得getByRank类的操作效率提升 -- 数据结构 -![](https://img-blog.csdnimg.cn/20200911184359226.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -- 结构示意图![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTk5OGJhNjJjZTQ1MTE1YTcucG5n?x-oss-process=image/format,png) - -每次向skiplist 中新增或者删除一个节点时,需要同时修改图标中红色的箭头,修改其forward和span的值。 - -需要修改的箭头和对skip进行查找操作遍历并废弃过的路径是吻合的。span修改仅是+1或-1。 -zskiplist 的查找平均时间复杂度 O(Log(N)),因此add / remove的复杂度也是O(Log(N))。因此Redis中新增的span 提升了获取rank(排序)操作的性能,仅需对遍历路径相加即可(矢量相加)。 - -还有一点需要注意的是,每个skiplist的节点level 大小都是随机生成的(1-32之间)。 -- zskiplistNode源码 -![](https://img-blog.csdnimg.cn/20200911185457885.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -### 8.2.2 hashtable -zskiplist 是zset 实现顺序相关操作比较高效的数据结构,但是对于简单的zscore操作效率并不高。Redis在实现时,同时使用了Hashtable和skiplist,代码结构如下: -![](https://img-blog.csdnimg.cn/20200911190122653.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -Hash表的存在使得Sorted-Set中的Map相关操作复杂度由O(N)变为O(1)。 - -Redis有序集合类型与Redis的集合类型类似,是非重复的String元素的集合。不同之处在于,有序集合中的每个成员都关联一个Score,Score是在排序时候使用的,按照Score的值从小到大进行排序。集合中每个元素是唯一的,但Score有可能重复。 - -使用有序集合可以很高效的进行,添加,移除,更新元素的操作(时间消耗与元素个数的对数成比例)。由于元素在集合中的位置是有序的,使用get ranges by score或者by rank(位置)来顺序获取或者随机读取效率都很高。(本句不确定,未完全理解原文意思,是根据自己对Redis的浅显理解进行的翻译)访问有序集合中间部分的元素也非常快,所以可以把有序集合当做一个不允许重复元素的智能列表,你可以快速访问需要的一切:获取有序元素,快速存在测试,快速访问中间的元素等等。 - -简短来说,使用有序集合可以实现很多高性能的工作,这一点在其他数据库是很难实现的。 - -## 应用 -* 在大型在线游戏中创建一个排行榜,每次有新的成绩提交,使用[ZADD]命令加入到有序集合中。可以使用[ZRANGE]命令轻松获得成绩名列前茅的玩家,你也可以使用[ZRANK]根据一个用户名获得该用户的分数排名。把ZRANK 和 ZRANGE结合使用你可以获得与某个指定用户分数接近的其他用户。这些操作都很高效。 - -* 有序集合经常被用来索引存储在Redis中的数据。比如,如果你有很多用户,用Hash来表示,可以使用有序集合来为这些用户创建索引,使用年龄作为Score,使用用户的ID作为Value,这样的话使用[ZRANGEBYSCORE]命令可以轻松和快速的获得某一年龄段的用户。zset有个ZSCORE的操作,用于返回单个集合member的分数,它的操作复杂度是O(1),这就是收益于你这看到的hash table。这个hash table保存了集合元素和相应的分数,所以做ZSCORE操作时,直接查这个表就可以,复杂度就降为O(1)了。 - -而跳表主要服务范围操作,提供O(logN)的复杂度。 -# Bitmaps -位图类型,String类型上的一组面向bit操作的集合。由于 strings是二进制安全的blob,并且它们的最大长度是512m,所以bitmaps能最大设置 2^32个不同的bit。 -# HyperLogLogs -pfadd/pfcount/pfmerge。 -在redis的实现中,使用标准错误小于1%的估计度量结束。这个算法的神奇在于不再需要与需要统计的项相对应的内存,取而代之,使用的内存一直恒定不变。最坏的情况下只需要12k,就可以计算接近2^64个不同元素的基数。 -# GEO -geoadd/geohash/geopos/geodist/georadius/georadiusbymember -Redis的GEO特性在 Redis3.2版本中推出,这个功能可以将用户给定的地理位置(经、纬度)信息储存起来,并对这些信息进行操作。 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/\344\270\272\345\225\245Redis Cluster\350\256\276\350\256\241\346\210\22016384\344\270\252\346\247\275\357\274\237.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/\344\270\272\345\225\245Redis Cluster\350\256\276\350\256\241\346\210\22016384\344\270\252\346\247\275\357\274\237.md" deleted file mode 100644 index cee7f47012..0000000000 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/\344\270\272\345\225\245Redis Cluster\350\256\276\350\256\241\346\210\22016384\344\270\252\346\247\275\357\274\237.md" +++ /dev/null @@ -1,32 +0,0 @@ -# 消息大小考虑 -crc16()一共可以有: -```bash -2^16 -1=65535 -``` -不同的余数,代表bitmap 有 65535 bit。所以bitmap的大小可以计算为 - -```bash -65535 / 8 (8bit/byte)/1024(1k)=7.99 Kbytes -``` - -尽管crc16能得到65535个值,但redis选择16384个slot,是因为16384的消息只占用了2k,而65535则需要8k。 - -正常的心跳包携带节点的完整配置,可以以幂等方式替换旧配置以更新旧配置。这意味着它们包含原始形式的节点的插槽配置,该节点使用2K的空间和16384个slot,但使用65535的插槽会使用令人望而却步的 8K 的空间。 - -```bash -65k = 8 * 8 (8 bit/byte) * 1024(1k) = 8K bitmap size -``` -## 为什么要传全量的slot状态? -因为分布式场景,基于状态的设计更合理,状态的传播具有幂等性。 -# 集群规模设计考虑 -同时,由于其他设计权衡,Redis Cluster 不太可能扩展到超过 1000 个主节点。集群设计最多支持1000个分片,16384是相对比较好的选择,需要保证在最大集群规模下,slot均匀分布场景下,每个分片平均分到的slot不至于太小。 -所以16384是在正确的范围内,以确保每个 master 有足够的插槽,最多 1000 个 maters,但这个数量足够小,可以轻松地将插槽配置作为原始位图传播。 - -在小集群中,位图将难以压缩,因为当 N 小时,位图将设置的槽位/N 位占很大比例的位。 -## 为什么不考虑压缩? -集群规模较小的场景下,每个分片负责大量的slot,很难压缩。 - -简而言之,它是消息大小和主机持有的平均slot数之间权衡的结果。 - -参考 -- https://github.com/redis/redis/issues/2576 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/\345\246\202\344\275\225\350\247\243\345\206\263Redis\347\232\204\345\271\266\345\217\221\347\253\236\344\272\211\351\227\256\351\242\230.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/\345\246\202\344\275\225\350\247\243\345\206\263Redis\347\232\204\345\271\266\345\217\221\347\253\236\344\272\211\351\227\256\351\242\230.md" deleted file mode 100644 index 07b6285907..0000000000 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/\345\246\202\344\275\225\350\247\243\345\206\263Redis\347\232\204\345\271\266\345\217\221\347\253\236\344\272\211\351\227\256\351\242\230.md" +++ /dev/null @@ -1,6 +0,0 @@ -# Redis的并发竞争问题 -- 多客户端同时并发写一个key,可能本来应该先到的数据后到了,导致数据版本出错 -- 多客户端同时获取一个key,修改值之后再写回去,只要顺序错了,数据也错了 -# 解决方案 -其实Redis本身就有解决这个问题的CAS类的乐观锁方案。 -![](https://img-blog.csdnimg.cn/20190509175418361.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/Java\351\253\230\346\200\247\350\203\275\347\263\273\347\273\237\347\274\223\345\255\230\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/Java\351\253\230\346\200\247\350\203\275\347\263\273\347\273\237\347\274\223\345\255\230\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" index f22b2aedaf..6ba0533ac0 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/Java\351\253\230\346\200\247\350\203\275\347\263\273\347\273\237\347\274\223\345\255\230\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/Java\351\253\230\346\200\247\350\203\275\347\263\273\347\273\237\347\274\223\345\255\230\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" @@ -1,3 +1,5 @@ +![](https://img-blog.csdnimg.cn/2020081311184130.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) + # 1 屈服于现实的磁盘 MQ都使用磁盘来存储消息。这样服务器下电也不会丢数据。绝大多数用于生产系统的服务器,都会使用多块磁盘组成磁盘阵列,这样即使其中的一块异常,也可把数据从其他磁盘中恢复。 diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RabbitMQ/RabbitMQ-\346\225\264\345\220\210Spring\345\274\200\345\217\221.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RabbitMQ/RabbitMQ-\346\225\264\345\220\210Spring\345\274\200\345\217\221.md" index c380f814a0..9236b301f2 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RabbitMQ/RabbitMQ-\346\225\264\345\220\210Spring\345\274\200\345\217\221.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RabbitMQ/RabbitMQ-\346\225\264\345\220\210Spring\345\274\200\345\217\221.md" @@ -1,4 +1,13 @@ -# 1 [相关源码](https://github.com/Wasabi1234/RabbitMQ_Tutorial) +> 全是干货的技术号: +> 本文已收录在github,欢迎 star/fork: +> https://github.com/Wasabi1234/Java-Interview-Tutorial + +[相关源码](https://github.com/Wasabi1234/RabbitMQ_Tutorial) +# 1 你将学到 +- RabbitMQ 整合 Spring AMQP实战 +- RabbitMQ 整合 Spring Boot实战 +- RabbitMQ 整合 Spring Cloud实战 + # 2 SpringAMQP用户管理组件 - RabbitAdmin RabbitAdmin 类可以很好的操作 rabbitMQ,在 Spring 中直接进行注入即可 @@ -9,16 +18,25 @@ RabbitAdmin 的底层实现 - 从 Spring 容器中获取 Exchange、Bingding、Routingkey 以及Queue 的 @Bean 声明 - 然后使用 rabbitTemplate 的 execute 方法进行执行对应的声明、修改、删除等一系列 RabbitMQ 基础功能操作。例如添加交换机、删除一个绑定、清空一个队列里的消息等等 - 依赖结构 -![](https://img-blog.csdnimg.cn/20190701130301812.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190701130301812.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +RabbitAdmin实现了4个Interface: AmqpAdmin, ApplicationContextAware, ApplicationEventPublisherAware,InitializingBean。 -RabbitAdmin实现了4个Interface: -- AmqpAdmin -- ApplicationContextAware -- ApplicationEventPublisherAware -- InitializingBean ### AmqpAdmin 为AMQP指定一组基本的便携式AMQP管理操作 -![](https://img-blog.csdnimg.cn/20190701131344162.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190701131344162.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +### ApplicationEventPublisherAware +实现该接口的类,通过函数setApplicationEventPublisher()获得它执行所在的ApplicationEventPublisher。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190701155733978.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +### ApplicationContextAware +实现该接口的类,通过函数setApplicationContext()获得它执行所在的ApplicationContext。一般用来初始化object +![](https://img-blog.csdnimg.cn/20190702041941650.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +### InitializingBean +![](https://img-blog.csdnimg.cn/20190702042042485.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +若class中实现该接口,在Spring Container中的bean生成之后,自动调用函数afterPropertiesSet()。 +因其实现了InitializingBean接口,其中只有一个方法,且在Bean加载后就执行 +该功能可以被用来检查是否所有的mandatory properties都设置好 + - 以上Interfaces的执行顺序 ApplicationEventPublisherAware -> ApplicationContextAware -> InitializingBean. @@ -30,20 +48,20 @@ RabbitAdmin借助于 ApplicationContextAware 和 InitializingBean来获取我们 下面是RabbitAdmin中afterPropertiesSet()函数的代码片段。这里在创建connection的时候调用函数initialize()。 于是以此为突破口进行源码分析 - RabbitAdmin#afterPropertiesSet -![](https://img-blog.csdnimg.cn/20190702050143931.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190702050143931.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) 这里 最后分别调用函数declareExchanges(),declareQueues(),declareBindings()来声明RabbitMQ Entity - 先定义了三个集合,利用applicationContext.getBeansOfType来获得container中的Exchange,Queue,Binding声明放入集合中 -![](https://img-blog.csdnimg.cn/20190702050619423.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190702050619423.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 然后调用filterDeclarables()来过滤不能declareable的bean![](https://img-blog.csdnimg.cn/20190702051006589.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +- 然后调用filterDeclarables()来过滤不能declareable的bean![](https://img-blog.csdnimg.cn/20190702051006589.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 按照RabbitMQ的方式拼接 -![](https://img-blog.csdnimg.cn/20190702055307309.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190702055307309.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 使用rabbitTemplate执行交互 -![](https://img-blog.csdnimg.cn/20190702055350722.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190702055350722.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) ## 2.2 实操 回顾一下消费者配置 @@ -63,24 +81,24 @@ RabbitAdmin借助于 ApplicationContextAware 和 InitializingBean来获取我们 TopicExchange : 多关键字匹配 ``` -![](https://img-blog.csdnimg.cn/20190702065621582.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190702065621582.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 测试代码![](https://img-blog.csdnimg.cn/20190702065336309.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 查看管控台![](https://img-blog.csdnimg.cn/20190702065439712.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190702065520705.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +- 测试代码![](https://img-blog.csdnimg.cn/20190702065336309.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +- 查看管控台![](https://img-blog.csdnimg.cn/20190702065439712.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190702065520705.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) # 3 SpringAMQP - RabbitMQ声明式配置使用 SpringAMQP 声明即在 rabbit 基础 API 里面声明一个 exchange、Bingding、queue。使用SpringAMQP 去声明,就需要使用 @Bean 的声明方式 -![](https://img-blog.csdnimg.cn/20190702082752208.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 查看管控台![](https://img-blog.csdnimg.cn/20190702090044875.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190702092111226.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190702082752208.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +- 查看管控台![](https://img-blog.csdnimg.cn/20190702090044875.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190702092111226.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) # 3 消息模板 - RabbitTemplate 上节中最后提到,这是与与 SpringAMQP 整合发送消息的关键类,它提供了丰富的发送消息方法 包括可靠性投递消息方法、回调监听消息接口 `ConfirmCallback`、返回值确认接口 `ReturnCallback `等. 同样我们需要注入到 Spring 容器中,然后直接使用. RabbitTemplate 在 Spring 整合时需要实例化,但是在 Springboot 整合时,在配置文件里添加配置即可 - 先声明bean -![](https://img-blog.csdnimg.cn/20190702094556582.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 测试![](https://img-blog.csdnimg.cn/20190702095800431.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190702094556582.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +- 测试![](https://img-blog.csdnimg.cn/20190702095800431.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) # 4 SpringAMQP消息容器-SimpleMessageListenerContainer 这个类非常的强大,我们可以对他进行很多的设置,用对于消费者的配置项,这个类都可以满足。它有监听单个或多个队列、自动启动、自动声明功能。 - 设置事务特性、事务管理器、事务属性、事务并发、是否开启事务、回滚消息等。但是我们在实际生产中,很少使用事务,基本都是采用补偿机制 @@ -91,11 +109,11 @@ RabbitTemplate 在 Spring 整合时需要实例化,但是在 Springboot 整合 > SimpleMessageListenerContainer 可以进行动态设置,比如在运行中的应用可以动态的修改其消费者数量的大小、接收消息的模式等。 - 很多基于 RabbitMQ 的自制定化后端管控台在进行设置的时候,也是根据这一去实现的 -![](https://img-blog.csdnimg.cn/20190702103609861.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190702103830482.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190702103941759.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190702104108857.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190702104212629.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190702103609861.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190702103830482.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190702103941759.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190702104108857.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190702104212629.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) # 5 SpringAMQP消息适配器-MessageListenerAdapter 消息监听适配器,通过反射将消息处理委托给目标监听器的处理方法,并进行灵活的消息类型转换. 允许监听器方法对消息内容类型进行操作,完全独立于RabbitMQ API @@ -109,47 +127,47 @@ RabbitTemplate 在 Spring 整合时需要实例化,但是在 Springboot 整合 注意:发送响应消息仅在使用ChannelAwareMessageListener入口点(通常通过Spring消息监听器容器)时可用。 用作MessageListener不支持生成响应消息。 ## 源码分析 -![](https://img-blog.csdnimg.cn/20190703112518590.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703112518590.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) 继承自`AbstractAdaptableMessageListener`类,实现了`MessageListener`和`ChannelAwareMessageListener`接口 而`MessageListener`和`ChannelAwareMessageListener`接口的`onMessage`方法就是具体容器监听队列处理队列消息的方法 -![](https://img-blog.csdnimg.cn/20190703112741904.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703112741904.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) ## 实操 - 委托类MessageDelegate,类中定义的方法也就是目标监听器的处理方法 -![](https://img-blog.csdnimg.cn/20190703110706732.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 配置类代码![](https://img-blog.csdnimg.cn/20190703110826994.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703110706732.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +- 配置类代码![](https://img-blog.csdnimg.cn/20190703110826994.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 运行测试代码 -![](https://img-blog.csdnimg.cn/20190703110927685.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703110927685.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 结果 -![](https://img-blog.csdnimg.cn/2019070311101753.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/2019070311101753.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) 从源码分析小节中的成员变量,我们可以看出使用MessageListenerAdapter处理器进行消息队列监听处理 - 如果容器没有设置setDefaultListenerMethod 则处理器中默认的处理方法名是`handleMessage` - 如果设置了setDefaultListenerMethod -![](https://img-blog.csdnimg.cn/20190703113640114.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703113640114.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) 则处理器中处理消息的方法名就是setDefaultListenerMethod方法参数设置的值 -![](https://img-blog.csdnimg.cn/20190703113929481.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703113929481.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) 也可以通过setQueueOrTagToMethodName方法为不同的队列设置不同的消息处理方法。 `MessageListenerAdapter`的`onMessage`方法 - 如果将参数改为String运行会出错!应当是字节数组,这时就需要使用转换器才能保证正常运行 -![](https://img-blog.csdnimg.cn/20190703114102112.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703114102112.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 使用转换器 -![](https://img-blog.csdnimg.cn/20190703114713677.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190703114734657.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190703115029275.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703114713677.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703114734657.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703115029275.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) 测试代码运行成功! -![](https://img-blog.csdnimg.cn/20190703120217719.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190703120434742.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190703120525213.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703120217719.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703120434742.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703120525213.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) # 6 消息转换器 - MessageConverter 我们在进行发送消息的时候,正常情况下消息体为二进制的数据方式进行传输,如果希望内部帮我们进行转换,或者指定自定义的转换器,就需要用到 `MessageConverter `了 - 我们自定义常用转换器,都需要实现这个接口,然后重写其中的两个方法 -![](https://img-blog.csdnimg.cn/20190703124244469.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703124244469.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) ## 常见的转换器 - Json 转换器 - jackson2JsonMessageConverter Java 对象的转换功能 @@ -160,29 +178,29 @@ Java对象的映射关系 比如图片类型、PDF、PPT、流媒体 ## 实操 - Order类 -![](https://img-blog.csdnimg.cn/20190703124732639.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190703124859608.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 配置JSON转换器![](https://img-blog.csdnimg.cn/20190703125153881.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703124732639.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703124859608.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +- 配置JSON转换器![](https://img-blog.csdnimg.cn/20190703125153881.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 测试代码 -![](https://img-blog.csdnimg.cn/2019070312561814.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190703125706999.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/2019070312561814.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703125706999.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 配置Java对象转换器 -![](https://img-blog.csdnimg.cn/20190703130109286.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703130109286.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 测试代码及结果 -![](https://img-blog.csdnimg.cn/20190703130309588.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 多个Java对象映射转换![](https://img-blog.csdnimg.cn/20190703130554776.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703130309588.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +- 多个Java对象映射转换![](https://img-blog.csdnimg.cn/20190703130554776.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 测试代码及结果 -![](https://img-blog.csdnimg.cn/2019070313131050.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190703131346801.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/2019070313131050.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703131346801.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 全局转换器 -![](https://img-blog.csdnimg.cn/20190703182004723.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703182004723.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 图片转换器实现 -![](https://img-blog.csdnimg.cn/20190703182523521.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703182523521.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - PDF转换器实现 -![](https://img-blog.csdnimg.cn/20190703182650464.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190703185719599.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703182650464.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703185719599.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 测试代码及结果 -![](https://img-blog.csdnimg.cn/20190703185752598.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70)![](https://img-blog.csdnimg.cn/20190703185834535.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703185752598.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70)![](https://img-blog.csdnimg.cn/20190703185834535.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) # 7 RabbitMQ与SpringBoot2.x整合实战 ## 7.1 配置详解 - publisher-confirms @@ -194,41 +212,41 @@ Java对象的映射关系 > 在生产端还可以配置其他属性,比如发送重试、超时时间、次数、间隔等 ## Pro - 配置文件 -![](https://img-blog.csdnimg.cn/20190703191738927.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703191738927.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 主配置 -![](https://img-blog.csdnimg.cn/20190703190758641.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703190758641.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 添加一个自定义的交换机 -![](https://img-blog.csdnimg.cn/20190703225705132.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703225705132.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 添加一个Q -![](https://img-blog.csdnimg.cn/20190703230430261.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703230430261.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 建立绑定关系 -![](https://img-blog.csdnimg.cn/20190703230941421.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190703231011535.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190703233404929.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703230941421.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703231011535.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703233404929.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 测试及结果 -![](https://img-blog.csdnimg.cn/20190703235354725.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190703235354725.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) ## Con配置 消费端的 RabbitListener 是一个组合注解,里面可以注解配置 。 @QueueBinding @Queue @Exchange 直接通过这个组合注解一次性搞定消费端交换机、队列、绑定、路由、并且配置监听功能等。 - 将Pro中的绑定全部删除,再启动Con的sb服务 -![](https://img-blog.csdnimg.cn/20190704000336742.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190704000401500.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704000336742.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704000401500.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) ## 发送一个 Java 实体对象 - 在Con声明队列、交换机、routingKey基本配置 -![](https://img-blog.csdnimg.cn/20190704092634274.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704092634274.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - Con -![](https://img-blog.csdnimg.cn/20190704092733133.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704092733133.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) > Payload 注解中的路径要跟Pro的实体路径完全一致,要不然会找到不到该类,这里为了简便就不写一个 common.jar 了,在实际开发里面,这个 Java Bean 应该放在 common.jar中 - 注意实体要实现 Serializable 序列化接口,要不然发送消息会失败 -![](https://img-blog.csdnimg.cn/20190704093852589.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704093852589.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - Pro 照样跟着写一个发消息的方法 -![](https://img-blog.csdnimg.cn/20190704094016584.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704094016584.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 测试代码及结果 -![](https://img-blog.csdnimg.cn/20190704094212455.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190704094310427.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704094212455.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704094310427.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) # 8 RabbitMQ & Spring Cloud Stream整合实战 Spring Cloud全家桶在整个中小型互联网公司异常的火爆,Spring Cloud Stream也就渐渐的被大家所熟知,本小节主要来绍RabbitMQ与Spring Cloud Stream如何集成 ## 8.1 编程模型 @@ -239,11 +257,11 @@ Spring Cloud全家桶在整个中小型互联网公司异常的火爆,Spring Clo 外部消息传递系统和应用程序之间的桥接提供的生产者和消费者消息(由目标绑定器创建) - 消息 生产者和消费者用于与目标绑定器(以及通过外部消息传递系统的其他应用程序)通信的规范数据结构 -![](https://img-blog.csdnimg.cn/20190704100414674.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704100414674.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) ## 8.2 应用模型 Spring Cloud Stream应用程序由中间件中立核心组成。该应用程序通过Spring Cloud Stream注入其中的输入和输出通道与外界通信。通过中间件特定的Binder实现,通道连接到外部代理。 -![](https://img-blog.csdnimg.cn/20190704100544237.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -## 8.3 RabbitMQ绑定概述![](https://img-blog.csdnimg.cn/20190704101501891.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704100544237.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +## 8.3 RabbitMQ绑定概述![](https://img-blog.csdnimg.cn/20190704101501891.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) 默认情况下,RabbitMQ Binder实现将每个目标映射到TopicExchange。对于每个使用者组,Queue绑定到该TopicExchange。每个使用者实例都为其组的Queue具有相应的RabbitMQ Consumer实例。对于分区生成器和使用者,队列以分区索引为后缀,并使用分区索引作为路由键。对于匿名使用者(没有组属性的用户),使用自动删除队列(具有随机的唯一名称)。 Barista接口: Barista接口是定义来作为后面类的参数,这一接口定义来通道类型和通道名称,通道名称是作为配置用,通道类型则决定了app会使用这一 通道进行发送消息还是从中接收消息 @@ -260,12 +278,12 @@ Barista接口: Barista接口是定义来作为后面类的参数,这一接口 ## 8.5 实操 ### Pro - pom核心文件 -![](https://img-blog.csdnimg.cn/20190704105257283.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704105257283.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - Sender -![](https://img-blog.csdnimg.cn/20190704110913136.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704110913136.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) 注解`@EnableBinding`声明了这个应用程序绑定了2个通道:INPUT和OUTPUT。这2个通道是在接口`Barista`中定义的(Spring Cloud Stream默认设置)。所有通道都是配置在一个具体的消息中间件或绑定器中 - Barista接口 -![](https://img-blog.csdnimg.cn/20190704112934707.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704112934707.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - @Input 声明了它是一个输入类型的通道,名字是Barista.INPUT_CHANNEL,也就是position3的input_channel。这一名字与上述配置app2的配置文件中position1应该一致,表明注入了一个名字叫做input_channel的通道,它的类型是input,订阅的主题是position2处声明的mydest这个主题 @@ -282,36 +300,35 @@ Barista接口: Barista接口是定义来作为后面类的参数,这一接口 ### Con - Pom核心文件 -![](https://img-blog.csdnimg.cn/20190704113317198.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704113317198.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 应用启动类 -![](https://img-blog.csdnimg.cn/20190704113434327.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704113434327.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - Barista接口 -![](https://img-blog.csdnimg.cn/20190704113713148.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704113713148.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 配置文件 -![](https://img-blog.csdnimg.cn/20190704114526499.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704114526499.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 接收 -![](https://img-blog.csdnimg.cn/20190704114755779.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704114755779.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 启动Con服务,查看管控台 -![](https://img-blog.csdnimg.cn/2019070411503383.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190704115119708.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/201907041151564.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/2019070411503383.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190704115119708.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/201907041151564.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 运行Pro测试代码及结果 -![](https://img-blog.csdnimg.cn/2019070411543336.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/2019070411543336.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) ![](https://img-blog.csdnimg.cn/20190704115517511.png) ![](https://img-blog.csdnimg.cn/2019070411555535.png) # 9 总结 本文我们学习了Spring AMQP的相关知识,通过实战对RabbitMQ集成Spring有了直观的认识,这样为 我们后续的学习、工作使用都打下了坚实的基础,最后我们整合了SpringBoot与Spring Cloud Stream,更方便更高效的集成到我们的应用服务中去! - -> 参考 -> - [SpringAMQP 用户管理组件 RabbitAdmin 以及声明式配置](https://juejin.im/post/5c541218e51d450134320378) [Spring Boot -> - RabbitMQ源码分析](https://zhuanlan.zhihu.com/p/54450318) [SpringAMQP 之 RabbitTemplate](https://juejin.im/post/5c5c29efe51d457fff4102f1) -> [SpringAMQP 消息容器 - -> SimpleMessageListenerContainer](https://juejin.im/user/5c3dfed2e51d4552232fc9cd) -> [MessageListenerAdapter详解](https://www.jianshu.com/p/d21bafe3b9fd) -> [SpringAMQP 消息转换器 - -> MessageConverter](https://juejin.im/post/5c5d925de51d457fc75f7a0c) -> [RabbitMQ 与 SpringBoot2.X -> 整合](https://juejin.im/post/5c64e3fd6fb9a049d132a557) [Spring Cloud -> Stream](https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/2.2.0.RELEASE/spring-cloud-stream.html#spring-cloud-stream-reference) \ No newline at end of file +# 参考 +[SpringAMQP 用户管理组件 RabbitAdmin 以及声明式配置](https://juejin.im/post/5c541218e51d450134320378) +[Spring Boot - RabbitMQ源码分析](https://zhuanlan.zhihu.com/p/54450318) +[SpringAMQP 之 RabbitTemplate](https://juejin.im/post/5c5c29efe51d457fff4102f1) +[SpringAMQP 消息容器 - SimpleMessageListenerContainer](https://juejin.im/user/5c3dfed2e51d4552232fc9cd) +[MessageListenerAdapter详解](https://www.jianshu.com/p/d21bafe3b9fd) +[SpringAMQP 消息转换器 - MessageConverter](https://juejin.im/post/5c5d925de51d457fc75f7a0c) +[RabbitMQ 与 SpringBoot2.X 整合](https://juejin.im/post/5c64e3fd6fb9a049d132a557) +[Spring Cloud Stream](https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/2.2.0.RELEASE/spring-cloud-stream.html#spring-cloud-stream-reference) + +![](https://img-blog.csdnimg.cn/20200825235213822.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RabbitMQ/RabbitMQ\351\253\230\347\272\247\347\211\271\346\200\247-\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\350\247\243\345\206\263\346\226\271\346\241\210.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RabbitMQ/RabbitMQ\351\253\230\347\272\247\347\211\271\346\200\247-\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\350\247\243\345\206\263\346\226\271\346\241\210.md" index 32a00e81ac..e5b3a72d38 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RabbitMQ/RabbitMQ\351\253\230\347\272\247\347\211\271\346\200\247-\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\350\247\243\345\206\263\346\226\271\346\241\210.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RabbitMQ/RabbitMQ\351\253\230\347\272\247\347\211\271\346\200\247-\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\350\247\243\345\206\263\346\226\271\346\241\210.md" @@ -1,115 +1,164 @@ -# 1 RabbitMQ -一款分布式消息中间件,基于erlang开发, 具备语言级别的高并发处理能力。和Spring框架是同一家公司。支持持久化、高可用。 -## 核心概念 +> 全是干货的技术号: +> 本文已收录在 +> +> github: +> +> https://github.com/Wasabi1234/Java-Interview-Tutorial +> +> 码云: +> +>https://gitee.com/JavaEdge/Java-Interview-Tutorial +> +> 欢迎 star/fork + +# 1 极速了解MQ +> 介绍Rabbitmg用于解决分布式事务必须掌握的5个核心概念 + +一款分布式消息中间件,基于erlang语言开发, 具备语言级别的高并发处理能力。和Spring框架是同一家公司。 +支持持久化、高可用 + +## 核心5个概念: 1. Queue: 真正存储数据的地方 2. Exchange: 接收请求,转存数据 3. Bind: 收到请求后存储到哪里 4. 消息生产者:发送数据的应用 5. 消息消费者: 取出数据处理的应用 -![](https://img-blog.csdnimg.cn/2019110903371374.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -# 2 分布式事务问题 -> 分布式事务是一个业务问题,不能脱离具体场景。 +![](https://img-blog.csdnimg.cn/2019110903371374.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) + + + +# 2、分布式事务问题 +> 分布式事务是一个业务问题,不能脱离具体的场景。 + +## 2.1 分布式事务的几种解决方案 +● 基于数据库XA/ JTA协议的方式 +需要数据库厂商支持; JAVA组件有atomikos等 +● 异步校对数据的方式 +支付宝、微信支付主动查询支付状态、对账单的形式; +● 基于可靠消息(MQ)的解决方案 +异步场景;通用性较强;拓展性较高 +● TCC编程式解决方案 +严选、阿里、蚂蚁金服自己封装的DTX + +本文目标:针对所有人群,学会基于可靠消息来解决分布式事务问题。 +分布式事务的解决方案,业务针对性很强,重要的是思路,而不是照搬 - 美团点评系统架构 -![](https://img-blog.csdnimg.cn/20191109111135363.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20191109111135363.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) -## 多系统间的分布式事务问题 -![](https://img-blog.csdnimg.cn/20191109111326448.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +## 2.2 多系统间的分布式事务问题 +![](https://img-blog.csdnimg.cn/20191109111326448.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - 用户下单生成订单 -![](https://img-blog.csdnimg.cn/20191109111524174.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 需要传递订单数据,由此产生两个事务一致性问题 -![](https://img-blog.csdnimg.cn/20191109111712542.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -## 错误案例 -![](https://img-blog.csdnimg.cn/20191109112532574.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -### 当接口调用失败时 -订单系统事务回滚,提示用户操作失败。 -自以为这样的接口调用写法,就不会有分布式事务问题。 - -### 接口调用成功或失败 -都会产生分布式事务问题: -- 接口调用成功 -订单系统数据库事务提交失败,运单系统没有回滚,产生数据 -- 接口调用超时 -订单系统数据库事务回滚,运单系统接口继续执行,产生数据 - -所以都会导致数据不一致问题。 -# 3 正确实现分布式事务(五步法) -- 之前都是订单系统直接HTTP请求运单系统的接口,出问题了!![](https://img-blog.csdnimg.cn/2019110911365199.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 因此考虑发消息给MQ,异步暂存 -![](https://img-blog.csdnimg.cn/20191109113742318.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20191109111524174.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) +- 需要传递订单数据,由此产生两个事务一致性问题 +![](https://img-blog.csdnimg.cn/20191109111712542.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) + +## 错误的案例 +![](https://img-blog.csdnimg.cn/20191109112532574.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) + +### 当接口调用失败时,订单系统事务回滚,提示用户操作失败 +`误以为这样的接口调用写法,就不会有分布式事务问题` + +### 接口调用成功或者失败,都会产生分布式事务问题: +1. 接口调用成功,订单系统数据库事务提交失败,运单系统没有回滚,产生数据 +2. 接口调用超时,订单系统数据库事务回滚,运单系统接口继续执行,产生数据 + +上述两种情况,都会导致数据不一致的问题 + +# 3、实现分布式事务 - 五步法 +> 通过MQ解决分布式事务的5个步骤, 以及分布式事务处理中要注意的地方 + +- 之前都是订单系统发送HTTP请求运单系统的接口,出问题了!![](https://img-blog.csdnimg.cn/2019110911365199.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) +- 因此我们考虑发消息给MQ, 异步暂存! +![](https://img-blog.csdnimg.cn/20191109113742318.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) ## 3.1 整体设计思路 -![](https://img-blog.csdnimg.cn/2019110911390025.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -外卖下订单后,慢慢等待运单中心数据生成,并非强制要求同时。但要保证: -- 可靠生产 -保证消息一定要发送到Rabitmq服务 -- 可靠消费 -保证消息取出来一定正确消费掉 +![](https://img-blog.csdnimg.cn/2019110911390025.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) +外卖下订单后,可以慢慢等待运单中心数据生成,并非强制要求同时性 + + +1. 可靠生产:保证消息一定要发送到Rabitmq服务 +2. 可靠消费:保证消息取出来一定正确消费掉 最终使多方数据达到一致。 -## 实现步骤 -### 步骤1 - 可靠的消息生产记录消息发送 -![](https://img-blog.csdnimg.cn/20191110002502550.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -#### 隐患 -- 可能消息发送失败: -![](https://img-blog.csdnimg.cn/20191110002547799.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -为确保数据一定成功发送到MQ。在同一事务中,增加一个记录表的操作, 记录`每一条发往MQ的数据以及它的发送状态`。 -- 于是在订单系统中增加一个本地信息表 -![](https://img-blog.csdnimg.cn/20191110002746532.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -不再通过HTTP请求直接调用运单系统接口,而是使用MQ: -生成订单时,也保存本地信息表 -![](https://img-blog.csdnimg.cn/20191110003415706.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20191110003248457.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20191110004913354.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -### 步骤2-可靠消息生产(修改消息发送状态) -- 利用RabbitMQ的事务发布确认机制(confirm):开启后,MQ准确受理消息会返回回执 -![](https://img-blog.csdnimg.cn/20191110003908628.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 然后就能知道如何更新本地信息表 + +## 3.2 步骤1 - 可靠的消息生产记录消息发送 +![](https://img-blog.csdnimg.cn/20191110002502550.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) +- 存在隐患 - 可能消息发送失败呀! +![](https://img-blog.csdnimg.cn/20191110002547799.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) + +➢ 为了确保数据一定成功发送到MQ。 +➢ 在同一事务中,增加一个记录表的操作, 记录`每一条发往MQ的数据以及它的发送状态` +于是我们在订单系统中增加一个本地信息表![](https://img-blog.csdnimg.cn/20191110002746532.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) + +于是在代码实践中,不再通过HTTP接口调用运单系统接口,而是使用MQ + +生成订单时,也保存本地信息表 +![](https://img-blog.csdnimg.cn/20191110003248457.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20191110003415706.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20191110004913354.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) +## 3.3 步骤2 - 可靠消息生产(修改消息发送状态) +- 利用RabbitMQ事务发布确认机制(confirm) +开启后,MQ准确受理消息会返回回执 +![](https://img-blog.csdnimg.cn/20191110003908628.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) +- 然后就能知道如何更新本地信息表了 ![](https://img-blog.csdnimg.cn/2019111000394665.png) -- 确保在SpringBoot项目中开启Confirm机制 -![](https://img-blog.csdnimg.cn/20191110004341395.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -#### 代码实现 -![](https://img-blog.csdnimg.cn/201911100049513.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20191110005151992.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +-确保在SB中开启Confirm机制 +![](https://img-blog.csdnimg.cn/20191110004341395.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) + +![](https://img-blog.csdnimg.cn/201911100049513.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20191110005151992.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) -- 若出现回执没收到、消息状态修改失败等特殊情况 -兜底方案:定时检查消息表,超时没发送成功,再次重发。 +- 如果出现回执没收到、消息状态修改失败等特殊情况 +`兜底方案:定时检查消息表,超时没发送成功,再次重发` -### 步骤3 - 可靠消息处理(正常处理) -- 运单系统收到消息数据后,突然宕机或访问运单DB时,DB突然宕机,消息数据不就丢了? -![](https://img-blog.csdnimg.cn/20191110010205178.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +## 3.4 步骤3 - 可靠消息处理(正常处理) +- 运单系统收到消息数据后,突然宕机,或者访问运单DB时,DB突然宕机,消息数据不就丢了吗!!! +![](https://img-blog.csdnimg.cn/20191110010205178.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) +于是需要以下特性: -于是还需要如下处理: ➢ 幂等性 防止重复消息数据的处理,一次用户操作,只对应一次数据处理 -![](https://img-blog.csdnimg.cn/20191110010346674.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20191110010346674.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) ➢ 开启`手动ACK模式` 由消费者控制消息的重发/清除/丢弃 -![](https://img-blog.csdnimg.cn/20191110010518296.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -### 步骤4 - 可靠消息处理(消息重发) -![](https://img-blog.csdnimg.cn/2019111001144842.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -消费者处理失败,需要MQ重发给消费者。出现异常一般会重试几次,由消费者自身记录重试次数,并进行次数控制。 - -### 步骤五 - 可靠消息处理(消息丢弃) -消费者处理失败,直接丢弃或者转移到死信队列(DLQ)。`重试次数过多、消息内容格式错误等情况,通过线上预警机制通知运维`。 -![](https://img-blog.csdnimg.cn/20191110012037386.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -# 4 总结 -## MQ实现分布式事务分析 -### 优点 +![](https://img-blog.csdnimg.cn/20191110010518296.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) +### 3.5 步骤4 - 可靠消息处理(消息重发) +![](https://img-blog.csdnimg.cn/2019111001144842.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) +消费者处理失败,需要MQ再次重发给消费者。 +出现异常一般会重试几次,由消费者自身记录重试次数,并进行次数控制(不会永远重试!) + +## 3.6 步骤五 - 可靠消息处理(消息丢弃) +消费者处理失败,直接丢弃或者转移到死信队列(DLQ) +`重试次数过多、消息内容格式错误等情况,通过线上预警机制通知运维人员` +![](https://img-blog.csdnimg.cn/20191110012037386.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) +# 4 总结及扩展 +## 4.1 MQ方案的优点和缺点 +口优点 1. 通用性强 2. 拓展性强 3. 方案成熟 -### 缺点 -- 基于消息中间件,只适合异步场景 -- 消息处理会有延迟,需要业务上能够容忍 +口缺点 +4. 基于消息中间件,只适合异步场景 +5. 消息处理会有延迟,需要业务上能够容忍 + +尽量避免分布式事务; +尽量将非核心事务做成异步; + +## 4.2 拓展 +### 分布式事务解决方案的理论依据 +CAP理论 +BASE理论 +2PC协议 +3PC协议 +Paxos算法. +Raft一致性协议 -尽量避免分布式事务,尽量将非核心事务做成异步。 + +# 参考 +[美团配送系统架构演进实践](https://tech.meituan.com/2018/07/26/peisong-sys-arch-evolution.html) -> 参考 -> - [美团配送系统架构演进实践](https://tech.meituan.com/2018/07/26/peisong-sys-arch-evolution.html) \ No newline at end of file +![](https://img-blog.csdnimg.cn/20200825235213822.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RabbitMQ/RabbitMQ\351\253\230\347\272\247\347\211\271\346\200\247\344\271\213-\344\274\230\345\205\210\347\272\247\351\230\237\345\210\227\357\274\210Priority Queue\357\274\211.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RabbitMQ/RabbitMQ\351\253\230\347\272\247\347\211\271\346\200\247\344\271\213-\344\274\230\345\205\210\347\272\247\351\230\237\345\210\227\357\274\210Priority Queue\357\274\211.md" index 57eabe0ba4..81d8a24d87 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RabbitMQ/RabbitMQ\351\253\230\347\272\247\347\211\271\346\200\247\344\271\213-\344\274\230\345\205\210\347\272\247\351\230\237\345\210\227\357\274\210Priority Queue\357\274\211.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RabbitMQ/RabbitMQ\351\253\230\347\272\247\347\211\271\346\200\247\344\271\213-\344\274\230\345\205\210\347\272\247\351\230\237\345\210\227\357\274\210Priority Queue\357\274\211.md" @@ -50,4 +50,6 @@ ch.queueDeclare("my-priority-queue", true, false, false, args); 参考 - https://www.rabbitmq.com/priority.html -- https://www.rabbitmq.com/queues.html#optional-arguments \ No newline at end of file +- https://www.rabbitmq.com/queues.html#optional-arguments + +![](https://img-blog.csdnimg.cn/20200825235213822.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/RocketMQ\345\256\236\346\210\230(1)-\347\256\200\344\273\213.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/ RocketMQ\345\256\236\346\210\230(\344\270\200) - \347\256\200\344\273\213.md" similarity index 100% rename from "\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/RocketMQ\345\256\236\346\210\230(1)-\347\256\200\344\273\213.md" rename to "\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/ RocketMQ\345\256\236\346\210\230(\344\270\200) - \347\256\200\344\273\213.md" diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/RocketMQ\345\256\242\346\210\267\347\253\257\346\230\257\345\246\202\344\275\225\346\204\237\347\237\245Broker\350\212\202\347\202\271\347\232\204\357\274\237.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/RocketMQ Client\345\246\202\344\275\225\345\234\250\351\233\206\347\276\244\346\236\266\346\236\204\344\270\255\345\256\232\344\275\215Broker\350\212\202\347\202\271\357\274\237.md" similarity index 100% rename from "\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/RocketMQ\345\256\242\346\210\267\347\253\257\346\230\257\345\246\202\344\275\225\346\204\237\347\237\245Broker\350\212\202\347\202\271\347\232\204\357\274\237.md" rename to "\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/RocketMQ Client\345\246\202\344\275\225\345\234\250\351\233\206\347\276\244\346\236\266\346\236\204\344\270\255\345\256\232\344\275\215Broker\350\212\202\347\202\271\357\274\237.md" diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/RocketMQ\345\256\236\346\210\230(3)-\346\266\210\346\201\257\347\232\204\346\234\211\345\272\217\346\200\247.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/RocketMQ \351\253\230\347\272\247\347\211\271\346\200\247-\346\266\210\346\201\257\347\232\204\346\234\211\345\272\217\346\200\247.md" similarity index 81% rename from "\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/RocketMQ\345\256\236\346\210\230(3)-\346\266\210\346\201\257\347\232\204\346\234\211\345\272\217\346\200\247.md" rename to "\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/RocketMQ \351\253\230\347\272\247\347\211\271\346\200\247-\346\266\210\346\201\257\347\232\204\346\234\211\345\272\217\346\200\247.md" index 512c017712..8c58b83f98 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/RocketMQ\345\256\236\346\210\230(3)-\346\266\210\346\201\257\347\232\204\346\234\211\345\272\217\346\200\247.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/RocketMQ \351\253\230\347\272\247\347\211\271\346\200\247-\346\266\210\346\201\257\347\232\204\346\234\211\345\272\217\346\200\247.md" @@ -1,48 +1,56 @@ # 1 为什么需要消息有序 -996一辈子了,准备去银行存取款,对应两个异步短信消息,要保证先存后取: -- M1 存钱 -- M2 取钱 +996 一年终于攒了十万存在银行卡里准备存取款,对应两个异步的短信消息,要保证先存后取: +- M1 - 存钱 +- M2 - 取钱 ![](https://img-blog.csdnimg.cn/20191110204432867.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_1,color_FFFFFF,t_70) -而MQ默认发消息到不同Q显然是行不通的,会乱序。 -因此,需发往同一Q,依赖队列的先进先出机制。 +而MQ默认发消息到不同queue显然是行不通的,会乱序。因此,需要发往同一queue,依靠其先进先出机制。 + # 2 基本概念 -有序消息,又叫顺序消息(FIFO消息),指消息的消费顺序和产生顺序相同。 +有序消息又叫顺序消息(FIFO 消息),指消息的消费顺序和产生顺序相同。 -如订单的生成、付款、发货,这串消息必须按序处理。顺序消息又可分为: +比如订单的生成、付款、发货,这串消息必须按顺序处理。 +顺序消息又分为如下: ## 2.1 全局顺序 -一个Topic内所有的消息都发布到同一Q,按FIFO顺序进行发布和消费: +一个Topic内所有的消息都发布到同一个queue,按照FIFO顺序进行发布和消费: ![](https://img-blog.csdnimg.cn/20191110205132371.png) + ### 适用场景 性能要求不高,所有消息严格按照FIFO进行消息发布和消费的场景。 + ## 2.2 分区顺序 -对于指定的一个Topic,所有消息按`sharding key`进行区块(queue)分区,同一Q内的消息严格按FIFO发布和消费。 +对于指定的一个Topic,所有消息按`sharding key`进行区块(queue)分区,同一queue内的消息严格按FIFO发布和消费。 - Sharding key是顺序消息中用来区分不同分区的关键字段,和普通消息的Key完全不同。 ![](https://img-blog.csdnimg.cn/20191110205442842.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) + ### 适用场景 性能要求高,根据消息中的sharding key去决定消息发送到哪个queue。 + ## 2.3 对比 - 发送方式对比 ![](https://img-blog.csdnimg.cn/20191110210418100.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) + # 3 如何保证消息顺序? 在MQ模型中,顺序需由3个阶段去保障 1. 消息被发送时保持顺序 2. 消息被存储时保持和发送的顺序一致 3. 消息被消费时保持和存储的顺序一致 + ![](https://img-blog.csdnimg.cn/20191110210708395.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) + # 4 RocketMQ 有序消息实现原理 ![](https://img-blog.csdnimg.cn/20191110210921254.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_1,color_FFFFFF,t_70) RocketMQ消费端有两种类型: - MQPullConsumer - MQPushConsumer -![](https://img-blog.csdnimg.cn/20201114141638859.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) +![](https://img-blog.csdnimg.cn/20201114141638859.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) 底层都是通过pull机制实现,pushConsumer是一种API封装而已。 - `MQPullConsumer` 由用户控制线程,主动从服务端获取消息,每次获取到的是一个`MessageQueue`中的消息。 - `PullResult`中的 `List msgFoundList` -![](https://img-blog.csdnimg.cn/20201114142006113.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) +![](https://img-blog.csdnimg.cn/20201114142006113.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) - `MQPushConsumer`由用户注册`MessageListener`来消费消息,在客户端中需要保证调用`MessageListener`时消息的顺序性 ![](https://img-blog.csdnimg.cn/20191110212543478.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_1,color_FFFFFF,t_70) @@ -56,6 +64,7 @@ RocketMQ消费端有两种类型: ![](https://img-blog.csdnimg.cn/20191110213124288.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - 判断是并发的还是有序的,对应不同服务实现类 ![](https://img-blog.csdnimg.cn/20191110213603167.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_1,color_FFFFFF,t_70) + # 5 有序消息的缺陷 发送顺序消息无法利用集群的Failover特性,因为不能更换MessageQueue进行重试。 diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/RocketMQ\345\256\236\346\210\230(2)-\346\236\266\346\236\204.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/RocketMQ\345\256\236\346\210\230(2)-\346\236\266\346\236\204.md" deleted file mode 100644 index 020e893f26..0000000000 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/RocketMQ/RocketMQ\345\256\236\346\210\230(2)-\346\236\266\346\236\204.md" +++ /dev/null @@ -1,45 +0,0 @@ -# 1 角色 -RocketMQ由四个角色组成: -- Producer -消息生产者 -- Consumer -消费者 -- Broker -MQ服务,负责接收、分发消息 -- NameServer -负责MQ服务之间的协调 -# 2 架构设计 -![](https://img-blog.csdnimg.cn/20191025001711704.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -## NameServer-MQ服务注册发现中心 -提供轻量级服务发现和路由。 -每个名称服务器记录完整的路由信息,提供相应的读写服务,并支持快速存储扩展。 - -> NameServer 充当路由信息提供者。生产者/消费者客户查找主题以查找相应的broker列表。 -# 3 搭建 -## 配置 -- runserver.sh 设置小点 -![](https://img-blog.csdnimg.cn/20191103145158719.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -- runbroker.sh 设置小点 -![](https://img-blog.csdnimg.cn/20191103145746578.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -## 启动 -```bash -nohup sh bin/mqnamesrv > logs/namesrv.log 2>&1 & -``` -```bash -nohup sh bin/mqbroker -n localhost:9876 > -~/logs/rocketmqlogs/broker.log 2>&1 & -``` -- 启动报错 -![](https://img-blog.csdnimg.cn/20191103151015192.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -- 查看日志![](https://img-blog.csdnimg.cn/20191103150942509.png) -- 改启动文件,添加JAVA_HOME变量![](https://img-blog.csdnimg.cn/20191103154507585.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 启动成功 -![](https://img-blog.csdnimg.cn/20191103154618638.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -启动broker -```bash -nohup sh bin/mqbroker -c conf/broker.conf -n localhost:9876 > logs/broker.log 2>&1 & -``` -![](https://img-blog.csdnimg.cn/20191103155747201.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -# remoting模块架构 -![](https://img-blog.csdnimg.cn/20201008002847207.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225\350\247\243\346\236\220\347\263\273\345\210\227(1)-\346\266\210\346\201\257\351\230\237\345\210\227\347\232\204\346\204\217\344\271\211.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225\350\247\243\346\236\220\347\263\273\345\210\227\357\274\210\344\270\200\357\274\211- \346\266\210\346\201\257\351\230\237\345\210\227\347\232\204\346\204\217\344\271\211.md" similarity index 100% rename from "\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225\350\247\243\346\236\220\347\263\273\345\210\227(1)-\346\266\210\346\201\257\351\230\237\345\210\227\347\232\204\346\204\217\344\271\211.md" rename to "\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225\350\247\243\346\236\220\347\263\273\345\210\227\357\274\210\344\270\200\357\274\211- \346\266\210\346\201\257\351\230\237\345\210\227\347\232\204\346\204\217\344\271\211.md" diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225\350\247\243\346\236\220\347\263\273\345\210\227(2)- MQ\351\200\211\345\236\213.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225\350\247\243\346\236\220\347\263\273\345\210\227\357\274\210\344\272\214\357\274\211- MQ\351\200\211\345\236\213.md" similarity index 72% rename from "\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225\350\247\243\346\236\220\347\263\273\345\210\227(2)- MQ\351\200\211\345\236\213.md" rename to "\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225\350\247\243\346\236\220\347\263\273\345\210\227\357\274\210\344\272\214\357\274\211- MQ\351\200\211\345\236\213.md" index 39bb631981..6b9b135afb 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225\350\247\243\346\236\220\347\263\273\345\210\227(2)- MQ\351\200\211\345\236\213.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225\350\247\243\346\236\220\347\263\273\345\210\227\357\274\210\344\272\214\357\274\211- MQ\351\200\211\345\236\213.md" @@ -1,4 +1,4 @@ -# 1 选型标准 +# 1 MQ选型的标准 ## 1.1 开源(白嫖) 方便可以修改源代码,而非一味地等待软件提供商猴年马月发布的下个版本解决。在知识产权下,使用开源的才可商用。 @@ -13,14 +13,13 @@ 具备足够好的性能,能满足绝大多数场景的性能要求。 看完标准,于是市面上主要就如下可供选择: + # 2 RabbitMQ ## 2.1 优点 Erlang语言编写,最早是为电信行业系统可靠通信设计,是支持AMQP协议的消息队列之一。相当轻量级的消息队列,非常容易部署和使用。号称世上使用最广泛的开源消息队列。 ### 支持非常灵活的路由配置 -和其他消息队列不同,它在生产者(Producer)和队列(Queue)之间增加了一个Exchange模块。 -作用和交换机相似,根据配置的路由规则将生产者发出的消息分发到不同队列。 -路由规则也非常灵活,甚至你可以自己来实现路由规则。如果你正好需要该功能,RabbitMQ是个不错选择。 +和其他消息队列不同,它在生产者(Producer)和队列(Queue)之间增加了一个Exchange模块。作用和交换机也非常相似,根据配置的路由规则将生产者发出的消息分发到不同的队列中。路由的规则也非常灵活,甚至你可以自己来实现路由规则。如果你正好需要该功能,RabbitMQ是个不错选择。 ### 支持的客户端语言 所有消息队列中最多的。 @@ -30,7 +29,7 @@ Erlang语言编写,最早是为电信行业系统可靠通信设计,是支 当大量消息积压的时候,RabbitMQ的性能急剧下降。 ### 性能 -介绍的这几个消息队列中最差的,根据官方给出的测试数据综合我们日常使用的经验,依据硬件配置的不同,它大概可以处理几w~十几w条/s。也足够支撑绝大多数应用场景了,不过,如果你的应用对消息队列的性能要求非常高,那不要选RabbitMQ。 +介绍的这几个消息队列中最差的,根据官方给出的测试数据综合我们日常使用的经验,依据硬件配置的不同,它大概每秒钟可以处理几万到十几万条消息。其实,这个性能也足够支撑绝大多数的应用场景了,不过,如果你的应用对消息队列的性能要求非常高,那不要选RabbitMQ。 ### Erlang 小众语言,学习曲线非常陡峭。如果想做扩展和二次开发,慎重考虑维护问题。 @@ -45,30 +44,30 @@ RocketMQ有着不错的性能,稳定性和可靠性,具备一个现代的消 大多数问题你都可以找到中文的答案,也许会成为你选择它的一个原因。 - RocketMQ使用Java语言开发 -贡献者大多数都是中国人,源代码相对也比较容易读懂,很容易对RocketMQ进行扩展或者二次开发。 +贡献者大多数都是中国人,源代码相对也比较容易读懂,你很容易对RocketMQ进行扩展或者二次开发。 - RocketMQ对在线业务的响应时延做了很多的优化 -大多数情况下可以做到ms级响应,如果你的应用场景很在意响应时延,那应该选择使用RocketMQ。 +大多数情况下可以做到毫秒级的响应,如果你的应用场景很在意响应时延,那应该选择使用RocketMQ。 -> RocketMQ是怎么做到低延时的? +> rocketMQ是怎么做到低延时的? 主要是设计上的选择问题,Kafka中到处都是“批量和异步”设计,它更关注的是整体的吞吐量,而RocketMQ的设计选择更多的是尽量及时处理请求。 比如发消息,同样是用户调用了send()方法,RockMQ它会直接把这个消息发出去,而Kafka会把这个消息放到本地缓存里面,然后择机异步批量发送。 所以,RocketMQ它的时延更小一些,而Kafka的吞吐量更高。 - RocketMQ的性能比RabbitMQ要高一个数量级 -大概处理几十w条/s。 +每秒钟大概能处理几十万条消息。 ## 3.2 缺点 -作为国产的消息队列,相比国外的比较流行的同类产品,在国际上还没有那么流行,与周边生态系统的集成和兼容程度要略逊一筹。 +RocketMQ的一个劣势是,作为国产的消息队列,相比国外的比较流行的同类产品,在国际上还没有那么流行,与周边生态系统的集成和兼容程度要略逊一筹。 # 4 Kafka 最早由LinkedIn开发,目前也是Apache顶级项目。最初的设计目的是用于处理海量的日志。 -在早期的版本中,为了获得极致性能,在设计方面做了很多的牺牲,比如: +在早期的版本中,为了获得极致的性能,在设计方面做了很多的牺牲,比如: - 不保证消息的可靠性 - 可能会丢失消息 - 不支持集群 -- 功能上较简陋 +- 功能上也比较简陋 这些牺牲对于处理海量日志这个特定的场景都是可以接受的。这个时期的Kafka甚至不能称之为一个合格的消息队列。 @@ -87,7 +86,6 @@ RocketMQ有着不错的性能,稳定性和可靠性,具备一个现代的消 但Kafka这种异步批量的设计带来的问题是,它的同步收发消息的响应时延比较高,因为当客户端发送一条消息的时候,Kafka并不会立即发送出去,而是要等一会儿攒一批再发送,在它的Broker中,很多地方都会使用这种“先攒一波再一起处理”的设计。 当你的业务场景中,每秒钟消息数量没有那么多的时候,Kafka的时延反而会比较高。所以,Kafka不太适合在线业务场景。 -# 其它MQ - ActiveMQ 最老牌的开源消息队列,十年前唯一可供选择的开源消息队列,目前已进入老年期,社区不活跃。无论是功能还是性能方面,ActiveMQ都与现代的消息队列存在明显的差距,它存在的意义仅限于兼容那些还在用的爷辈儿系统。 - ZeroMQ @@ -97,11 +95,8 @@ Pulsar是一个新兴的开源消息队列产品,最早是由Yahoo开发,目 目前已经在使用Pulsar的公司已经不少了,国内的话有下面几家: 涂鸦智能、腾讯计费系统、智联招聘、甜橙金融、EMQX。 -# kafka、activemq、rabbitmq、rocketmq对比 -![](https://img-blog.csdnimg.cn/20190516180246505.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - # 5 选型总结 -- 最早大家都用ActiveMQ,但是现在用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,算了吧,不推荐 -- 后来大家开始用RabbitMQ,但erlang语言阻止了大量的java工程师去深入研究和掌控他,几乎处于不可控,但是开源的,比较稳定支持,活跃度也高。如果消息队列并不是你将要构建系统的主角之一,你对消息队列功能和性能都没有很高的要求,只需要一个开箱即用易于维护的产品,建议RabbitMQ。 -- 如果你的系统使用消息队列主要场景是处理在线业务,比如在交易系统中用消息队列传递订单,那RocketMQ的低延迟和金融级的稳定性是你需要的。 -- 如果需要处理海量的消息,像收集日志、监控信息或是前端的埋点这类数据,或是你的应用场景大量使用了大数据、流计算相关的开源产品,那Kafka是最适合。 \ No newline at end of file +如果消息队列并不是你将要构建系统的主角之一,你对消息队列功能和性能都没有很高的要求,只需要一个开箱即用易于维护的产品,建议RabbitMQ。 +如果你的系统使用消息队列主要场景是处理在线业务,比如在交易系统中用消息队列传递订单,那RocketMQ的低延迟和金融级的稳定性是你需要的。 + +如果需要处理海量的消息,像收集日志、监控信息或是前端的埋点这类数据,或是你的应用场景大量使用了大数据、流计算相关的开源产品,那Kafka是最适合。 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/\347\274\223\345\255\230/Guava Cache\347\274\223\345\255\230\350\256\276\350\256\241\345\216\237\347\220\206.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/\347\274\223\345\255\230/Guava Cache\347\274\223\345\255\230\350\256\276\350\256\241\345\216\237\347\220\206.md" index d82329c0ea..23d1195590 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/\347\274\223\345\255\230/Guava Cache\347\274\223\345\255\230\350\256\276\350\256\241\345\216\237\347\220\206.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/\347\274\223\345\255\230/Guava Cache\347\274\223\345\255\230\350\256\276\350\256\241\345\216\237\347\220\206.md" @@ -1,104 +1,51 @@ Google开源的Java重用工具集库Guava里的一款缓存工具,实现的缓存功能: - 自动将entry节点加载进缓存结构 -- 当缓存的数据超过设置的最大值时,使用LRU移除 +- 当缓存的数据超过设置的最大值时,使用LRU算法移除 - 具备根据entry节点上次被访问或者写入时间计算它的过期机制 - 缓存的key被封装在WeakReference引用内 - 缓存的Value被封装在WeakReference或SoftReference引用内 - 统计缓存使用过程中命中率、异常率、未命中率等统计数据 -Guava Cache架构设计源于ConcurrentHashMap,简单场景下可自行编码通过HashMap做少量数据的缓存。 -但若结果可能随时间改变或希望存储的数据空间可控,最好自己实现这种数据结构。 +Guava Cache的架构设计源于ConcurrentHashMap。 +简单场景下可自行编码通过HashMap做少量数据的缓存。但如果结果可能随时间改变或希望存储的数据空间可控,最好自己实现这种数据结构。 -使用多个segments方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求。 -Cache类似Map,是存储K.V对的集合,不过它还需处理evict、expire、dynamic load等算法逻辑,需要一些额外信息实现这些操作,需做方法与数据的关联封装。 -## Guava Cache数据结构 -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtMjVhY2JjYzk3NGJlYjk0MS5wbmc?x-oss-process=image/format,png) -Cache由多个Segment组成,而每个Segment包含一个ReferenceEntry数组: +使用多个segments方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求。Cache类似Map,是存储K.V对的集合,不过它还需处理evict、expire、dynamic load等算法逻辑,需要一些额外信息来实现这些操作。 +根据OOP思想,需做方法与数据的关联封装。 -```java -volatile @MonotonicNonNull AtomicReferenceArray> table; -``` -每个ReferenceEntry数组项都是一条ReferenceEntry链。 -![](https://img-blog.csdnimg.cn/8088871d635349d88e7bd52e5f795d08.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_Q1NETiBASmF2YUVkZ2Uu,size_16,color_FFFFFF,t_70,g_se,x_16) +如cache的内存数据模型,使用`ReferenceEntry`接口来封装一个K.V对 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTUzNWQ1MTFkOTFmYjg1ZjAucG5n?x-oss-process=image/format,png) +而用ValueReference来封装Value值 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTMwMmUzOWRmMjMyMTRjNjUucG5n?x-oss-process=image/format,png) +之所以用Reference命令,是因为Cache要支持WeakReference Key和SoftReference、WeakReference value。 -一个ReferenceEntry包含key、hash、value Reference、next字段,除了在ReferenceEntry数组项中组成的链。 +- Guava Cache数据结构图 +![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtMjVhY2JjYzk3NGJlYjk0MS5wbmc?x-oss-process=image/format,png) +**ReferenceEntry**是对一个键值对节点的抽象,它包含了key和值的ValueReference抽象类。 +Cache由多个Segment组成,而每个Segment包含一个ReferenceEntry数组 +每个ReferenceEntry数组项都是一条ReferenceEntry链。 +一个ReferenceEntry包含key、hash、valueReference、next字段 +除了在ReferenceEntry数组项中组成的链,在一个Segment中,所有ReferenceEntry还组成access链(accessQueue)和write链(writeQueue) ReferenceEntry可以是强引用类型的key,也可以WeakReference类型的key,为了减少内存使用量,还可以根据是否配置了expireAfterWrite、expireAfterAccess、maximumSize来决定是否需要write链和access链确定要创建的具体Reference:StrongEntry、StrongWriteEntry、StrongAccessEntry、StrongWriteAccessEntry等。 -### ReferenceEntry接口 -![](https://img-blog.csdnimg.cn/a16a7a0d4a2a49b9ab33ca85c09e666d.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_Q1NETiBASmF2YUVkZ2Uu,size_16,color_FFFFFF,t_70,g_se,x_16) - -引用 map 中的节点。 -map 中的节点可处于如下状态: -有效的: -- Live:设置了有效的键/值 -- Loading:加载中 - -无效的: -- Expired:时间已过(键/值仍可设置) -- Collected:键/值已部分收集,但尚未清理 -- Unset:标记为未设置,等待清理或重用 - -### ValueReference -Value的引用 -![](https://img-blog.csdnimg.cn/96d7ae6da2c34356848e23e801ccd85f.png) - -之所以用Reference命令,是因为Cache要支持: -- WeakReference K -- SoftReference、WeakReference V - **ValueReference** 因为Cache支持强引用的Value、SoftReference Value以及WeakReference Value,因而它对应三个实现类:StrongValueReference、SoftValueReference、WeakValueReference。 为了支持动态加载机制,它还有一个LoadingValueReference,在需要动态加载一个key的值时,先把该值封装在LoadingValueReference中,以表达该key对应的值已经在加载了,如果其他线程也要查询该key对应的值,就能得到该引用,并且等待改值加载完成,从而保证该值只被加载一次,在该值加载完成后,将LoadingValueReference替换成其他ValueReference类型。ValueReference对象中会保留对ReferenceEntry的引用,这是因为在Value因为WeakReference、SoftReference被回收时,需要使用其key将对应的项从Segment的table中移除。 -### Queue -在一个Segment中,所有ReferenceEntry还组成: -- access链(accessQueue) -- write链(writeQueue) - -为了实现LRU,Guava Cache在Segment中添加的这两条链,都是双向链表,通过ReferenceEntry中的如下链接而成: -- `previousInWriteQueue` -- `nextInWriteQueue` -- `previousInAccessQueue` -- `nextInAccessQueue` -#### WriteQueue -![](https://img-blog.csdnimg.cn/e8080405487b4cab8a7898dce01d9486.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_Q1NETiBASmF2YUVkZ2Uu,size_13,color_FFFFFF,t_70,g_se,x_16) -#### AccessQueue -![](https://img-blog.csdnimg.cn/c8127cb7a26348da8490fab748749bf4.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_Q1NETiBASmF2YUVkZ2Uu,size_13,color_FFFFFF,t_70,g_se,x_16) - -WriteQueue和AccessQueue都自定义了offer、peek、remove、poll等操作: -![](https://img-blog.csdnimg.cn/1cec317938ad4ca0ad54ee1e825eb391.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_Q1NETiBASmF2YUVkZ2Uu,size_10,color_FFFFFF,t_70,g_se,x_16) -offer操作,若是: -- 新增节点 -直接加到该链的结尾 -- 已存在的节点 -将该节点链接的链尾 - -remove操作: -直接从该链中移除该节点 - -poll操作: -将头节点的下一个节点移除,并返回。 - -## 缓存相关操作 -## Segment的evict清除策略 -在每次调用操作的开始和结束时触发清理工作,这样比一般的缓存另起线程监控清理相比,可减少开销。 -但若长时间没有调用方法,会导致不能及时清理释放内存空间。 - -evict主要处理四个Queue: -- keyReferenceQueue -- valueReferenceQueue -- writeQueue -- accessQueue - -前两个queue是因为WeakReference、SoftReference被垃圾回收时加入的,清理时只需遍历整个queue,将对应项从LocalCache移除即可: -- keyReferenceQueue存放ReferenceEntry -- valueReferenceQueue存放的是ValueReference - -要从Cache中移除需要有key,因而ValueReference需要有对ReferenceEntry的引用。 - -后两个Queue,只需检查是否配置了相应的expire时间,然后从头开始查找已经expire的Entry,将它们移除。 - -Segment中的put操作:put操作相对比较简单,首先它需要获得锁,然后尝试做一些清理工作,接下来的逻辑类似ConcurrentHashMap中的rehash,查找位置并注入数据。需要说明的是当找到一个已存在的Entry时,需要先判断当前的ValueRefernece中的值事实上已经被回收了,因为它们可以是WeakReference、SoftReference类型,如果已经被回收了,则将新值写入。并且在每次更新时注册当前操作引起的移除事件,指定相应的原因:COLLECTED、REPLACED等,这些注册的事件在退出的时候统一调用Cache注册的RemovalListener,由于事件处理可能会有很长时间,因而这里将事件处理的逻辑在退出锁以后才做。最后,在更新已存在的Entry结束后都尝试着将那些已经expire的Entry移除。另外put操作中还需要更新writeQueue和accessQueue的语义正确性。 +### WriteQueue和AccessQueue +为了实现最近最少使用算法,Guava Cache在Segment中添加了两条链:write链(writeQueue)和access链(accessQueue),这两条链都是`双向链表`,通过ReferenceEntry中的`previousInWriteQueue`、`nextInWriteQueue`和`previousInAccessQueue`、`nextInAccessQueue`链接而成 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTYwMjQ5MmJkZDdhYjFhZDEucG5n?x-oss-process=image/format,png) +但是以Queue的形式表达 +WriteQueue和AccessQueue都是自定义了offer、add(直接调用offer)、remove、poll等操作的逻辑 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTgzMjRkNDA2NjkxYTI1ZTkucG5n?x-oss-process=image/format,png) +对offer(add)操作,如果是新加的节点,则直接加入到该链的结尾,如果是已存在的节点,则将该节点链接的链尾;对remove操作,直接从该链中移除该节点;对poll操作,将头节点的下一个节点移除,并返回。 + +#### **了解了cache的整体结构后,再来看下针对缓存的相关操作就简单多了:** +* Segment中的evict清除策略操作,是在`每一次调用操作的开始和结束时触发清理工作` +这样比一般的缓存另起线程监控清理相比,可以`减少开销`,但如果长时间没有调用方法的话,会导致不能及时的清理释放内存空间的问题 +evict主要处理四个Queue:1\. keyReferenceQueue;2\. valueReferenceQueue;3\. writeQueue;4\. accessQueue +前两个queue是因为WeakReference、SoftReference被垃圾回收时加入的,清理时只需要遍历整个queue,将对应的项从LocalCache中移除即可,这里keyReferenceQueue存放ReferenceEntry,而valueReferenceQueue存放的是ValueReference,要从Cache中移除需要有key,因而ValueReference需要有对ReferenceEntry的引用,这个前面也提到过了 +而对后面两个Queue,只需要检查是否配置了相应的expire时间,然后从头开始查找已经expire的Entry,将它们移除即可 +* Segment中的put操作:put操作相对比较简单,首先它需要获得锁,然后尝试做一些清理工作,接下来的逻辑类似ConcurrentHashMap中的rehash,查找位置并注入数据。需要说明的是当找到一个已存在的Entry时,需要先判断当前的ValueRefernece中的值事实上已经被回收了,因为它们可以是WeakReference、SoftReference类型,如果已经被回收了,则将新值写入。并且在每次更新时注册当前操作引起的移除事件,指定相应的原因:COLLECTED、REPLACED等,这些注册的事件在退出的时候统一调用Cache注册的RemovalListener,由于事件处理可能会有很长时间,因而这里将事件处理的逻辑在退出锁以后才做。最后,在更新已存在的Entry结束后都尝试着将那些已经expire的Entry移除。另外put操作中还需要更新writeQueue和accessQueue的语义正确性。 Segment带CacheLoader的get操作 ```java @@ -169,60 +116,63 @@ System.out.println(cache.getIfPresent("word")); build生成器的两种方式都实现了一种逻辑: `从缓存中取key的值,如果该值已经缓存过了则返回缓存中的值,如果没有缓存过可以通过某个方法来获取这个值`。 -### CacheLoader -针对整个cache定义的,可认为是统一的根据K值load V的方法: +### cacheloader +定义比较宽泛,是针对整个cache定义的,可认为是统一的根据key值load value的方法 ```java -/** - * CacheLoader - */ -public void loadingCache() { - LoadingCache graphs = CacheBuilder.newBuilder() - .maximumSize(1000) - .build(new CacheLoader() { - @Override - public String load(String key) { - if ("key".equals(key)) { - return "key return result"; - } else { - return "get-if-absent-compute"; - } - } - }); - String resultVal = null; - try { - resultVal = graphs.get("key"); - } catch (ExecutionException e) { - e.printStackTrace(); - } -} + /** + * CacheLoader + */ + public void loadingCache() + { + LoadingCache graphs =CacheBuilder.newBuilder() + .maximumSize(1000).build(new CacheLoader() + { + @Override + public String load(String key) throws Exception + { + System.out.println("key:"+key); + if("key".equals(key)){ + return "key return result"; + }else{ + return "get-if-absent-compute"; + } + } + }); + String resultVal = null; + try { + resultVal = graphs.get("key"); + } catch (ExecutionException e) { + e.printStackTrace(); + } + + System.out.println(resultVal); + } ``` ### callable 较为灵活,允许你在get的时候指定load方法 ```java -/** - * - * Callable -*/ -public void callablex() throws ExecutionException - { - Cache cache = CacheBuilder.newBuilder() - .maximumSize(1000).build(); - String result = cache.get("key", new Callable() + /** + * + * Callable + */ + public void callablex() throws ExecutionException { - public String call() - { - return "result"; - } - }); - System.out.println(result); - } + Cache cache = CacheBuilder.newBuilder() + .maximumSize(1000).build(); + String result = cache.get("key", new Callable() + { + public String call() + { + return "result"; + } + }); + System.out.println(result); + } + ``` # 总结 -Guava Cache基于ConcurrentHashMap的设计,在高并发场景支持和线程安全上都有相应改进策略,使用Reference引用命令,提升高并发下的数据访问速度并保持了GC的可回收,有效节省空间。 - +Guava Cache基于ConcurrentHashMap的优秀设计借鉴,在高并发场景支持和线程安全上都有相应的改进策略,使用Reference引用命令,提升高并发下的数据……访问速度并保持了GC的可回收,有效节省空间。 write链和access链的设计,能更灵活、高效的实现多种类型的缓存清理策略,包括基于容量的清理、基于时间的清理、基于引用的清理等。 - 编程式的build生成器管理,让使用者有更多的自由度,能够根据不同场景设置合适的模式。 - -还可以显式清除、统计信息、移除事件的监听器、自动加载等功能。 \ No newline at end of file +此外,还可以显式清除、统计信息、移除事件的监听器、自动加载等功能。 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/\351\224\256\345\200\274\346\225\260\346\215\256\345\272\223\347\232\204\345\237\272\346\234\254\346\236\266\346\236\204.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/\347\274\223\345\255\230/\344\270\232\345\212\241\347\263\273\347\273\237\347\274\223\345\255\230\346\250\241\345\274\217.md" similarity index 100% rename from "\346\225\260\346\215\256\345\255\230\345\202\250/Redis/\351\224\256\345\200\274\346\225\260\346\215\256\345\272\223\347\232\204\345\237\272\346\234\254\346\236\266\346\236\204.md" rename to "\346\225\260\346\215\256\345\255\230\345\202\250/\347\274\223\345\255\230/\344\270\232\345\212\241\347\263\273\347\273\237\347\274\223\345\255\230\346\250\241\345\274\217.md" diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/\347\274\223\345\255\230/\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/\347\274\223\345\255\230/\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230\350\256\276\350\256\241\346\250\241\345\274\217.md" deleted file mode 100644 index 5aca6258d0..0000000000 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/\347\274\223\345\255\230/\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ /dev/null @@ -1,70 +0,0 @@ -很多同学更新缓存时,**先删除缓存,再更新数据库**,而后续操作会把数据再装载到缓存。 - -**这个逻辑是错误的**。最简单的两个并发操作:更新&查询。 -更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存,然后更新操作更新了数据库。于是,缓存中的数据还是老数据,导致缓存中的数据是脏的,而且还一直这样脏下去。所以特此总结一下几个缓存更新的Design Pattern。 - -先不讨论更新缓存和更新数据这两个事是一个事务的事,或是会有失败的可能,先假设更新数据库和更新缓存都可以成功的情况。 - -# 1 Cache Aside(旁路缓存) -最常用的模式: -- 失效 -应用先从cache取数据,没有得到,则从DB取数据,成功后,放入cache -- 命中 -应用程序从cache中取数据,取到后返回 -- 更新 -先把数据存到DB,成功后,再让缓存失效 -![Cache-Aside-Design-Pattern-Flow-Diagram](https://img-blog.csdnimg.cn/img_convert/a583a805ba22360ab0e5dd257ef37b1c.png) -![Updating Data using the Cache-Aside Pattern - Flow Diagram](https://img-blog.csdnimg.cn/img_convert/897b76d9ef2bec14e800c75753653d3a.png) - -注意,是先更新DB,成功后,让缓存失效。 - -一个查询操作,一个更新操作的并发 -首先,没有了删除cache数据的操作,而是先更新数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新的数据,但是,更新操作马上让缓存的失效了,后续的查询操作再把数据从数据库中拉出来。而不会像文章开头的那个逻辑产生的问题,后续的查询操作一直都在取老的数据。 - -这是标准的design pattern,包括Facebook的论文《[Scaling Memcache at Facebook](https://www.usenix.org/system/files/conference/nsdi13/nsdi13-final170_update.pdf)》也使用了这个策略。为什么不是写DB后更新缓存?可以看一下Quora上的这个问答《[Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?](https://www.quora.com/Why-does-Facebook-use-delete-to-remove-the-key-value-pair-in-Memcached-instead-of-updating-the-Memcached-during-write-request-to-the-backend)》,主要是怕两个并发的写操作导致脏数据。 - -那Cache Aside有并发问题吗? -有。比如,一个是`读操作`,但是没有命中缓存,然后就到数据库中取数据,此时来了一个`写操作`,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。 - -这个情形理论上会出现,不过,实际上出现的概率可能非常低,因为需要发生在读缓存时缓存失效,而且并发着有一个写操作。 -而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大 - -这也就是Quora上的那个答案里说的,要么通过2PC或是Paxos协议保证一致性,要么就是拼命的降低并发时脏数据的概率,而Facebook使用了这个降低概率的玩法,因为2PC太慢,而Paxos太复杂。当然,最好还是为缓存设置上过期时间。 -# 2 Read/Write Through Pattern -上面的Cache Aside,应用代码需要维护两个数据存储,一个是缓存,一个是数据库,应用程序比较啰嗦。 -而`Read/Write Through`是把更新数据库的操作由缓存自己代理,所以,对于应用层来说,就简单很多。 -可理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的Cache。 -## 2.1 Read Through -Read Through 就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出) -- Cache Aside是由`调用方负责`把数据加载入缓存 -- Read Through则用`缓存服务`自己来加载,从而对应用方是透明的 -## 2.2 Write Through -和Read Through相仿,不过是在更新数据时发生 -当有数据更新时 -- 如果没有命中缓存,直接更新数据库,然后返回 -- 如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作) - -下图中的Memory可以理解为就是我们例子里的数据库 -![A write-through cache with no-write allocation](https://img-blog.csdnimg.cn/img_convert/642013cf5ea3b5ec2859a4c0feabccd6.png) -# 3 Write Behind(异步写回) -又叫 Write Back。在更新数据时,只更新缓存,不更新DB,而我们的缓存会异步批量更新DB - -## 优点 -- 让数据的I/O操作飞快无比(因为直接操作内存嘛 ) -- 因为异步,write back还可以合并对同一个数据的多次操作,所以性能的提高是相当可观 - -## 缺点 -数据不是强一致性的,而且可能会丢失(我们知道Unix/Linux非正常关机会导致数据丢失,就是因为这个事)。 - -另外,Write Back实现逻辑比较复杂,因为他需要track哪些数据是被更新的,需要刷到持久层。 -os的write back会在仅当这个cache需要失效时,才会被真正持久化,比如,内存不够了,或是进程退出了等情况,这又叫lazy write。 - -比如在向磁盘中写数据时采用的也是这种策略。无论是: -- os层面的 Page Cache -- 日志的异步刷盘 -- 消息队列中消息的异步写入磁盘 - -大多采用了这种策略。因为这个策略在性能优势明显,直接写内存,避免了直接写磁盘造成的随机写。 - -- A write-back cache with write allocation -![](https://img-blog.csdnimg.cn/img_convert/3210481d0a1230f6d75eb45058776f43.png) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/\347\274\223\345\255\230/\347\274\223\345\255\230\344\270\216\346\225\260\346\215\256\345\272\223\344\270\200\350\207\264\346\200\247\350\247\243\345\206\263\346\226\271\346\241\210.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/\347\274\223\345\255\230/\347\274\223\345\255\230\344\270\216\346\225\260\346\215\256\345\272\223\344\270\200\350\207\264\346\200\247\350\247\243\345\206\263\346\226\271\346\241\210.md" deleted file mode 100644 index ce4d8e563a..0000000000 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/\347\274\223\345\255\230/\347\274\223\345\255\230\344\270\216\346\225\260\346\215\256\345\272\223\344\270\200\350\207\264\346\200\247\350\247\243\345\206\263\346\226\271\346\241\210.md" +++ /dev/null @@ -1,245 +0,0 @@ -只要使用Redis做缓存,就必然存在缓存和DB数据一致性问题。若数据不一致,则业务应用从缓存读取的数据就不是最新数据,可能导致严重错误。比如将商品的库存缓存在Redis,若库存数量不对,则下单时就可能出错,这是不能接受的。 -# 1 什么是缓存和DB的数据一致性 -`一致性`包含如下情况: -- 缓存有数据 -缓存的数据值需和DB相同 -- 缓存无数据 -DB必须是最新值 - -不符合这两种情况的,都属于**缓存和DB数据不一致**。 - -# 2 缓存的读写模式 -根据是否接收写请求,可将缓存分成读写缓存和只读缓存。 -## 2.1 读写缓存 -若要对数据进行**增删改,需要在Cache进行**。 -同时根据采取的写回策略,决定是否同步写回DB: -### 2.1.1 同步直写 -写缓存时,也同步写数据库,缓存和数据库中的数据一致。 -### 2.1.2 异步写回 -写缓存时不同步写DB,等到数据从缓存中淘汰时,再写回DB。使用这种策略时,若数据还没有写回DB,缓存就发生故障,则此时,DB就没有最新数据了。 - -所以,对于读写缓存,要想保证缓存和DB数据一致,就要采用`同步直写`。若采用这种策略,就需同时更新缓存和DB。所以,要在业务代码中使用事务,保证缓存和DB更新的原子性,即两者: -- 要么一起更新 -- 要么都不更新,返回错误信息,进行重试 - -否则,我们无法实现同步直写。 - -有些场景下,我们对数据一致性要求不高,比如缓存的是电商商品的非关键属性或短视频的创建或修改时间等,则可以使用`异步写回`。 -## 2.2 只读缓存 -- 新增数据 -直接写DB -- 删改数据 -删改DB,删除只读缓存中的数据 - -这样应用后续再访问这些增删改的数据时,由于Cache无数据 =》缓存缺失。 -此时,再从DB把数据读入Cache,这样后续再访问数据时,直接读Cache。 - -下面我们针对只读缓存,看看具体会遇到哪些问题,又该如何解决。 - -# 3 新增数据 -数据直接写到DB,不操作Cache。此时,Cache本身无新增数据,而DB是最新值,所以,此时缓存和DB数据一致。 - -# 4 删改数据 -此时应用既要更新DB,也要删除Cache。这俩操作若无法保证原子性,就可能出现数据不一致。 - -## 4.1 先删Cache,再更新DB -![](https://img-blog.csdnimg.cn/20210708153307119.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -## 4.2 先更新DB,再删除Cache -![](https://img-blog.csdnimg.cn/20210708153236452.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - -综上,在更新DB和删除Cache时,无论这俩操作谁先执行,只要有一个操作失败了,就会导致客户端读到旧值。 - -那怎么办?好像怎么都会导致数据不一致? -# 5 数据不一致的解决方案 -## 5.1 无并发 -### 重试 -将: -- 要删除的Cache值 -- 或要更新的DB值 - -暂存到MQ。 - -当应用删除Cache或更新DB: -- 成功 -把这些值从MQ去除,避免重复操作,这时即可保证DB、Cache数据一致性。 -- 失败 -重试。从MQ重新读取这些值,然后再次进行删除或更新。若重试超过一定次数,还没成功,就向业务层发送报错信息。 - -在更新数据库和删除缓存值的过程中,其中一个操作失败了: -#### 先更新DB,再删除缓存 -- 若删除缓存失败,再次重试后删除成功 -![](https://img-blog.csdnimg.cn/20210708155139659.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -其它情况不再赘述。 - -即使这两个操作第一次执行时都没有失败,当有大量并发请求时,应用还是有可能读到不一致的数据。 -按不同的删除和更新顺序,分成两种情况来看 - -## 5.2 高并发 -### 5.2.1 先删除Cache,再更新DB -假设现有时刻t1< t2 < t3,线程 T1、T2: -||T1|T2|问题| -|--|--|--|--| -|t1|删除缓存 X 的缓存值|| -|t2|| 1. 读取数据,缓存缺失,
    从 DB 读 X,读到旧值
    2.把数据 X 写入缓存 | 1.T1 尚未更新 DB,导致 T2 读到旧值
    2.T2 把旧值写入缓存,导致其它线程可能读到旧值 -|t3|更新DB 中的 X | |缓存中是旧值,DB 是新值,二者不一致 - -此时,该怎么办呢? -#### 解决方案 -T1更新完DB后,让它sleep一段时间,再删除缓存。 - -> 为什么要sleep一段时间呢? - -为了让T2能够先从DB读数据,再把缺失数据写入缓存,然后,T1再进行删除。 -所以,T1 sleep的时间,就需要大于T2读取数据再写入缓存的时间。 - -> 这个时间怎么确定? - -在业务程序运行时,统计下线程读数据和写缓存的操作时间,以此为基础来进行估算。 - -这样,当其它线程读数据时,会发现缓存缺失,所以会从DB读最新值。因为这个方案会在第一次删除缓存值后,延迟一段时间再次进行删除,所以称为“延迟双删”。 -```bash -redis.delKey(X) -db.update(X) -Thread.sleep(N) -redis.delKey(X) -``` -### 5.2.2 先更新DB,再删除Cache -||T1|T2|问题| -|--|--|--|--| -|t1|删除 DB 的数据 X|| -|t2|| 读数据X,Cache命中,
    从Cache读X,读到旧值 |T1 尚未删除 Cache
    导致 T2 读到 Cache 旧值 -|t3| 删除 Cache的数据 X | | - -这种情况下,如果其他线程并发读缓存的请求不多,那么,就不会有很多请求读取到旧值。 -而且,线程A一般也会很快删除缓存值,这样一来,其他线程再次读取时,就会发生缓存缺失,进而从数据库中读取最新值。所以,这种情况对业务的影响较小。 - -至此,Cache和DB数据不一致的原因也都有了对应解决方案。 -- 删除Cache或更新DB失败而导致数据不一致 -重试,确保删除或更新成功 -- 在删除Cache、更新DB这两步操作中,有其他线程的并发读操作,导致其他线程读取到旧值 -延迟双删 - -绝大多数场景都会将Redis作为只读缓存: -- 既可以先删除缓存值再更新数据库 -- 也可以先更新数据库再删除缓存 - -推荐优先使用先更新数据库再删除缓存: -- 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力 -- 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置 - -不过,当使用先更新数据库再删除缓存时,也有个地方需要注意,如果业务层要求必须读取一致的数据,那么,我们就需要在更新数据库时,先在Redis缓存客户端暂存并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性。 - -# 6 直接更新 Cache -在只读缓存中进行数据的删改操作时,需要在缓存中删除相应的缓存值。若此过程不是删除缓存,而是直接更新缓存,效果如何? - -这种情况相当于把Redis当做**读写缓存**使用,删改操作同时操作DB、Cache。 - -## 6.1 无并发 -### 先更新数据库,再更新缓存 -若更新DB成功,但Cache更新失败,此时DB最新值,但缓存旧值,后续读请求会直接命中缓存,得到旧值。 - -### 先更新缓存,再更新数据库 -如果更新缓存成功,但数据库更新失败: -- 缓存中是最新值 -- 数据库中是旧值 - -后续读请求会直接命中缓存,但得到的是最新值,短期对业务影响不大。但一旦缓存过期或满容后被淘汰,读请求就会从数据库中重新加载旧值到缓存中,之后的读请求会从缓存中得到旧值,对业务产生影响。 - -针对这种其中一个操作可能失败的情况,类似只读缓存方案,也可使用重试。把第二步操作放入到MQ中,消费者从MQ取出消息,再更新缓存或数据库,成功后把消息从消息队列删除,否则进行重试,以此达到数据库和缓存的最终一致。 - -## 6.2 并发读写 -也会产生不一致,分为以下4种双写场景。 - -> 双写模式下,更新DB有返回值,更新Redis的操作可放到更新DB返回后进行,通过数据库的行锁机制,可以避免更新DB是线程A,B,但更新Redis是线程B,A的情况。 - -### 先更新数据库,再更新缓存 -写+读并发。 -线程A先更新数据库,之后线程B读取数据,此时线程B会命中缓存,读取到旧值,之后线程A更新缓存成功,后续的读请求会命中缓存得到最新值。 - -这时,线程A未更新完缓存之前,在这期间的读请求会短暂读到旧值,对业务短暂影响。 - -### 先更新缓存,再更新数据库 -写+读并发。 -线程A先更新缓存成功,之后线程B读取数据,此时线程B命中缓存,读取到最新值后返回,之后线程A更新数据库成功。这种场景下,虽然线程A还未更新完数据库,数据库会与缓存存在短暂不一致,但在这之前进来的读请求都能直接命中缓存,获取到最新值,所以对业务没影响。 - -### 先更新数据库,再更新缓存 -写+写并发。 -线程A和线程B同时更新同一条数据,更新数据库的顺序是先A后B,但更新缓存时顺序是先B后A,这会导致数据库和缓存的不一致。 - -### 先更新缓存,再更新数据库 -写+写并发。 -与场景3类似,线程A和线程B同时更新同一条数据,更新缓存的顺序是先A后B,但是更新数据库的顺序是先B后A,这也会导致数据库和缓存的不一致。 - -场景1和2对业务影响较小,场景3和4会造成数据库和缓存不一致,影响较大。即读写缓存下,写+读并发对业务的影响较小,而写+写并发时,会造成数据库和缓存的不一致。 - -针对场景3、4解决方案:对于写请求,配合分布式锁。写请求进来时,针对同一资源的修改操作,先加分布式锁,这样同一时间只允许一个线程去更新DB和Cache,没有拿到锁的线程把操作放入到MQ,延时处理。 -这样保证多个线程操作同一资源的顺序性,以此保证一致性。 - -综上,使用读写缓存同时操作数据库和缓存时,因为其中一个操作失败导致不一致的问题,同样可以通过MQ重试解决。 -而在并发的场景下,读+写并发对业务没有影响或者影响较小,而写+写并发时需要配合分布式锁的使用,才能保证缓存和数据库的一致性。 - -另外,读写缓存模式由于会同时更新数据库和缓存: -- 优点 -缓存一直会有数据。若更新后立即访问,可直接命中缓存,能降低读请求对DB的压力(没有只读缓存的删除缓存导致缓存缺失和再加载的过程) -- 缺点 -若更新后的数据,之后很少再被访问到,会导致缓存中保留的不是最热数据,缓存利用率不高(只读缓存中保留的都是热数据) - -所以读写缓存比较适合用于读写相当的业务场景。 - - - - -# 总结 -![](https://img-blog.csdnimg.cn/20210708164600442.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -# 延时双删策略 -写DB前后都执行`redis.del(key)`,并设定合理超时时间。 - -## 执行流程 -1. 先删除缓存 -2. 再写数据库 -3. 休眠xx毫秒(根据具体业务时间) -4. 再次删除缓存 - -xx毫秒怎么确定? - -需要评估项目读数据业务逻辑耗时,以确保读请求结束,写请求可删除读请求造成的缓存脏数据。 - -该策略还要考虑 redis 和数据库主从同步的耗时。最后的写数据的休眠时间:则在读数据业务逻辑的耗时的基础上,加上几百ms即可。比如:休眠1秒。 - -# 设置缓存过期时间 -理论上,设置缓存过期时间,是保证最终一致性的解决方案。 -所有的写操作以DB为准,只要到达缓存过期时间,则后面的读请求自然会从DB读取新值,然后回填缓存。 - -结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加写请求耗时。 - -# 写完数据库后,再次删除缓存成功保证 - -上述的方案有一个缺点,那就是操作完数据库后,由于种种原因删除缓存失败,这时,可能就会出现数据不一致的情况。 -需提供保障重试方案。 - -## 方案一 -### 具体流程 - -1. 更新数据库数据 -2. 缓存因为种种问题删除失败 -3. 将需要删除的key发送至消息队列 -4. 自己消费消息,获得需要删除的key -5. 继续重试删除操作,直到成功 - -然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二。 -在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。 - -## 方案二 -### 具体流程 - -1. 更新数据库数据 -2. 数据库会将操作信息写入binlog日志当中 -3. 订阅程序提取出所需要的数据以及key -4. 另起一段非业务代码,获得该信息 -5. 尝试删除缓存操作,发现删除失败 -6. 将这些信息发送至消息队列 -7. 重新从消息队列中获得该数据,重试操作。 - -以上方案都是在业务中经常会碰到的场景,可以依据业务场景的复杂和对数据一致性的要求来选择具体的方案。 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\345\233\236\346\272\257\347\256\227\346\263\225.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\345\233\236\346\272\257\347\256\227\346\263\225.md" deleted file mode 100644 index c001787db6..0000000000 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\345\233\236\346\272\257\347\256\227\346\263\225.md" +++ /dev/null @@ -1,68 +0,0 @@ -DFS利用的就是是回溯算法思想,但其实回溯还可用在如正则表达式匹配、编译原理中的语法分析等。 - -数学问题也可,如数独、八皇后、0-1背包、图的着色、旅行商问题、全排列等。 -# 理解“回溯算法” -> 若人生可重来,如何才能在岔路口做出最正确选择,让自己的人生“最优”? - -贪心算法,在每次面对岔路口的时候,都做出看起来最优的选择,期望这一组选择可以使得我们的人生达到“最优”。但不一定能得到的是最优解。 - -> 如何确保得到最优解? - -回溯算法很多时候都应用在“搜索”问题:在一组可能解中,搜索期望解。 -处理思想,类似枚举搜索:枚举所有解,找到满足期望的解。 - -为规律枚举所有可能解,避免遗漏、重复,将问题求解过程分为多个阶段。 -每个阶段,都要面对一个岔路口,先随意选一条路走,当发现这条路走不通(不符合期望的解),就回退到上一个岔路口,另选一种走法继续走。 -# 八皇后 -8x8的棋盘,往里放8个棋子(皇后),每个棋子所在的行、列、对角线都不能有另一个棋子。 -![](https://img-blog.csdnimg.cn/0532b7d902884ce29a148695e4872ca0.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_11,color_FFFFFF,t_70,g_se,x_16) - -把这个问题划分成8个阶段,依次将8个棋子放到第一行、第二行、第三行……第八行。 -放置过程中,不停地检查当前方法,是否满足要求 -- 满足 -跳到下一行继续放置棋子 -- 不满足 -换种方法尝试 - -适合递归实现: -![](https://img-blog.csdnimg.cn/4e7b69d48d3a4d04a3faafba4e91f90b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -# 0-1背包 -经典解法是动态规划,但还有简单但没那么高效的回溯解法。 - -- 有一背包,背包总承载重量Wkg -- 有n个物品,每个物品重量不等且不可分割 -- 期望选择几件物品,装载到背包中。在不超过背包所能装载重量的前提下,求背包中物品的总重量max? - -这个背包问题,物品不可分割,要么装要么不装,所以叫0-1背包,就无法通过贪心解决了。 - -- 对于每个物品来说,都有两种选择,装 or 不装 -- 对于n个物品,就有 $2^n$ 种装法,去掉总重量超过Wkg的,从剩下的装法中选择总重量最接近Wkg的 -- 但如何才能不重复地穷举出这 $2^n$ 种装法? - -这就能回溯,把物品依次排列,整个问题分解为n个阶段,每个阶段对应一个物品怎么选择; -- 先对第一个物品进行处理,选择装进去 or 不装进去 -- 再递归处理剩下物品 - -搜索剪枝的技巧:当发现 已选择物品重量 > Wkg,就停止探测剩下物品。 -![](https://img-blog.csdnimg.cn/e47c8d0cd7ae4203a3c8b109b62e4f01.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -## 正则表达式 -假设正表达式中只包含`*`、`?`通配符且现在规定: -- `*` 匹配任意多个(大于等于0个)任意字符 -- `?` 匹配0或1个任意字符 - -如何用回溯算法,判断某给定文本,是否匹配给定的正则表达式? -依次考察正则表达式中的每个字符,当是非通配符时,就直接跟文本的字符进行匹配: -- 相同 -继续往下处理 -- 不同 -回溯 - -遇到特殊字符时,就有多种处理方式,如`*`有多种匹配方案,可匹配任意个文本串中的字符,先随意选择一种匹配方案,然后继续考察剩下字符: -- 若中途发现无法继续匹配,就回到岔路口,重新选择一种匹配方案,再继续匹配剩下字符。 -![](https://img-blog.csdnimg.cn/31c5e631f10743328322e91c5387fa70.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -# 总结 -回溯算法思想很简单,大部分都是用来解决广义搜索问题:从一组可能解中,选出一个满足要求的解。 - -回溯非常适合用递归实现,剪枝是提高回溯效率的一种技巧,无需穷举搜索所有情况。 - -回溯算法可解决很多问题,如我们开头提到的深度优先搜索、八皇后、0-1背包问题、图的着色、旅行商问题、数独、全排列、正则表达式匹配等。 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204/\344\270\272\344\275\225Redis\344\275\277\347\224\250\350\267\263\350\241\250\350\200\214\351\235\236\347\272\242\351\273\221\346\240\221\345\256\236\347\216\260SortedSet\357\274\237.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204/\344\270\272\344\275\225Redis\344\275\277\347\224\250\350\267\263\350\241\250\350\200\214\351\235\236\347\272\242\351\273\221\346\240\221\345\256\236\347\216\260SortedSet\357\274\237.md" deleted file mode 100644 index c812467965..0000000000 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204/\344\270\272\344\275\225Redis\344\275\277\347\224\250\350\267\263\350\241\250\350\200\214\351\235\236\347\272\242\351\273\221\346\240\221\345\256\236\347\216\260SortedSet\357\274\237.md" +++ /dev/null @@ -1,391 +0,0 @@ -知道跳表(Skip List)是在看关于Redis的书的时候,Redis中的有序集合使用了跳表数据结构。接着就查了一些博客,来学习一下跳表。后面会使用Java代码来简单实现跳表。 -# 什么是跳表 -跳表由William Pugh发明,他在论文《Skip lists: a probabilistic alternative to balanced trees》中详细介绍了跳表的数据结构和插入删除等操作,论文是这么介绍跳表的: - -> Skip lists are a data structure that can be used in place of balanced trees.Skip lists use probabilistic balancing rather than strictly enforced balancing and as a result the algorithms for insertion and deletion in skip lists are much simpler and significantly faster than equivalent algorithms for balanced trees. - -也就是说,跳表可以用来替代红黑树,使用概率均衡技术,使得插入、删除操作更简单、更快。先来看论文里的一张图: -![这里写图片描述](https://img-blog.csdnimg.cn/img_convert/4671dd3346d9d2d1cef44fe4ea9e1f93.png) -观察上图 - - - a:已排好序的链表,查找一个结点最多需要比较N个结点。 - - b:每隔2个结点增加一个指针,指向该结点间距为2的后续结点,那么查找一个结点最多需要比较ceil(N/2)+1个结点。 - - c,每隔4个结点增加一个指针,指向该结点间距为4的后续结点,那么查找一个结点最多需要比较ceil(N/4)+1个结点。 - - 若每第`2^i` 个结点都有一个指向间距为 `2^i`的后续结点的指针,这样不断增加指针,比较次数会降为log(N)。这样的话,搜索会很快,但插入和删除会很困难。 - -一个拥有k个指针的结点称为一个k层结点(level k node)。按照上面的逻辑,50%的结点为1层,25%的结点为2层,12.5%的结点为3层...如果每个结点的层数随机选取,但仍服从这样的分布呢(上图e,对比上图d)? - -使一个**k层结点的第i个指针**指向**第i层的下一个结点**,而不是它后面的第2^(i-1)个结点,那么结点的插入和删除只需要原地修改操作;一个结点的层数,是在它被插入的时候随机选取的,并且永不改变。因为这样的数据结构是基于链表的,并且额外的指针会跳过中间结点,所以作者称之为跳表(Skip Lists)。 - -二分查找底层依赖数组随机访问的特性,所以只能用数组实现。若数据存储在链表,就没法用二分搜索了? - -其实只需稍微改造下链表,就能支持类似“二分”的搜索算法,即跳表(Skip list),支持快速的新增、删除、搜索操作。 - -Redis中的有序集合(Sorted Set)就是用跳表实现的。我们知道红黑树也能实现快速的插入、删除和查找操作。那Redis 为何不选择红黑树来实现呢? -![](https://img-blog.csdnimg.cn/c41cd7c5d6c34ea7bd75bc83d2f932c1.png) -# 跳表的意义究竟在于何处? -单链表即使存储的数据有序,若搜索某数据,也只能从头到尾遍历,搜索效率很低,平均时间复杂度是O(n)。 - -追求极致的程序员就开始想了,那这该如何提高链表结构的搜索效率呢? -若如下图,对链表建立一级“索引”,每两个结点提取一个结点到上一级,把抽出来的那级叫作索引或索引层。图中的down表示down指针,指向下一级结点。 -![](https://img-blog.csdnimg.cn/c8c2a8196d4f4346933b9f717a548182.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -比如要搜索16: -- 先遍历索引层,当遍历到索引层的13时,发现下一个结点是17,说明目标结点位于这俩结点中间 -- 然后通过down指针,下降到原始链表层,继续遍历 -此时只需再遍历2个结点,即可找到16! - -原先单链表结构需遍历10个结点,现在只需遍历7个结点即可。可见,加一层索引,所需遍历的结点个数就减少了,搜索效率提升。 -![](https://img-blog.csdnimg.cn/862cdc1cf3234e0f937a2e65a52a585a.png) - -若再加层索引,搜索效率是不是更高?于是每两个结点再抽出一个结点到第二级索引。现在搜索16,只需遍历6个结点了! -![](https://img-blog.csdnimg.cn/b9eb35ea32904818b53747416d2b18f7.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -这里数据量不大,可能你也没感觉到搜索效率ROI高吗。 - -那数据量就变大一点,现有一64结点链表,给它建立五级的索引。 -![](https://img-blog.csdnimg.cn/a7fd9946b27642fca7bbfddeda2546fb.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -原来没有索引时,单链表搜索62需遍历62个结点! -现在呢?只需遍历11个!所以你现在能体会到了,当链表长度n很大时,建立索引后,搜索性能显著提升。 - -这种有多级索引的,可以提高查询效率的链表就是最近火遍面试圈的跳表。 -作为严谨的程序员,我们又开始好奇了 -# 跳表的搜索时间复杂度 -我们都知道单链表搜索时间复杂度O(n),那如此快的跳表呢? - -若链表有n个结点,会有多少级索引呢?假设每两个结点抽出一个结点作为上级索引,则: -- 第一级索引结点个数是n/2 -- 第二级n/4 -- 第三级n/8 -- ... -- 第k级就是`n/(2^k)` - -假设索引有h级,最高级索引有2个结点,可得: - - -```markup -n/(2h) = 2 -``` -所以: - -```markup -h = log2n-1 -``` -若包含原始链表这一层,整个跳表的高度就是log2 n。我们在跳表中查询某个数据的时候,如果每一层都要遍历m个结点,那在跳表中查询一个数据的时间复杂度就是O(m*logn)。 - -那这个m的值是多少呢?按照前面这种索引结构,我们每一级索引都最多只需要遍历3个结点,也就是说m=3,为什么是3呢?我来解释一下。 - -假设我们要查找的数据是x,在第k级索引中,我们遍历到y结点之后,发现x大于y,小于后面的结点z,所以我们通过y的down指针,从第k级索引下降到第k-1级索引。在第k-1级索引中,y和z之间只有3个结点(包含y和z),所以,我们在K-1级索引中最多只需要遍历3个结点,依次类推,每一级索引都最多只需要遍历3个结点。 - - - -通过上面的分析,我们得到m=3,所以在跳表中查询任意数据的时间复杂度就是O(logn)。这个查找的时间复杂度跟二分查找是一样的。换句话说,我们其实是基于单链表实现了二分查找,是不是很神奇?不过,天下没有免费的午餐,这种查询效率的提升,前提是建立了很多级索引,也就是我们在第6节讲过的空间换时间的设计思路。 - -# 跳表是不是很费内存? -由于跳表要存储多级索引,势必比单链表消耗更多存储空间。那到底是多少呢? -若原始链表大小为n: -- 第一级索引大约有n/2个结点 -- 第二级索引大约有n/4个结点 -- ... -- 最后一级2个结点 - -多级结点数的总和就是: -```java -n/2+n/4+n/8…+8+4+2=n-2 -``` -所以空间复杂度是O(n)。这个量还是挺大的,能否再稍微降低索引占用的内存空间呢? -若每三五个结点才抽取一个到上级索引呢? -![](https://img-blog.csdnimg.cn/048531dae1394c048529edf6279dcfeb.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -- 第一级索引需要大约n/3个结点 -- 第二级索引需要大约n/9个结点 -- 每往上一级,索引结点个数都除以3 - -假设最高级索引结点个数为1,总索引结点数: - -```java -n/3+n/9+n/27+…+9+3+1=n/2 -``` -尽管空间复杂度还是O(n),但比上面的每两个结点抽一个结点的索引构建方法,要减少了一半的索引结点存储空间。 - -我们大可不必过分在意索引占用的额外空间,实际开发中,原始链表中存储的有可能是很大的对象,而索引结点只需存储关键值和几个指针,无需存储对象,所以当对象比索引结点大很多时,那索引占用的额外空间可忽略。 - -# 插入和删除的时间复杂度 -## 插入 -在跳表中插入一个数据,只需O(logn)时间复杂度。 -单链表中,一旦定位好要插入的位置,插入的时间复杂度是O(1)。但这里为了保证原始链表中数据的有序性,要先找到插入位置,所以这个过程中的查找操作比较耗时。 - -单纯的单链表,需遍历每个结点以找到插入的位置。但跳表搜索某结点的的时间复杂度是O(logn),所以搜索某数据应插入的位置的时间复杂度也是O(logn)。 -![](https://img-blog.csdnimg.cn/f6f355ffaa644532b72f6f7e1f5e4400.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -## 删除 -如果这个结点在索引中也有出现,除了要删除原始链表的结点,还要删除索引中的。 -因为单链表删除操作需拿到要删除结点的前驱结点,然后通过指针完成删除。所以查找要删除结点时,一定要获取前驱结点。若是双向链表,就没这个问题了。 - -# 跳表索引动态更新 -当不停往跳表插入数据时,若不更新索引,就可能出现某2个索引结点之间数据非常多。极端情况下,跳表还会退化成单链表。 -![](https://img-blog.csdnimg.cn/c95c956e246c4e05889a2903b8490c15.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -作为一种动态数据结构,我们需要某种手段来维护索引与原始链表大小之间的平衡,也就是说,如果链表中结点多了,索引结点就相应地增加一些,避免复杂度退化,以及查找、插入、删除操作性能下降。 - -像红黑树、AVL树这样的平衡二叉树通过左右旋保持左右子树的大小平衡,而跳表是通过随机函数维护前面提到的“平衡性”。 - -往跳表插入数据时,可以选择同时将这个数据插入到部分索引层中。 - -> 那如何选择加入哪些索引层呢? - -通过一个随机函数决定将这个结点插入到哪几级索引中,比如随机函数生成了值K,那就把这个结点添加到第一级到第K级这K级索引中。 -![](https://img-blog.csdnimg.cn/58a68ae89e384c71b0d9c9852efce0a8.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -> 为何Redis要用跳表来实现有序集合,而不是红黑树? - -Redis中的有序集合支持的核心操作主要支持: -- 插入一个数据 -- 删除一个数据 -- 查找一个数据 -- 迭代输出有序序列 -以上操作,红黑树也能完成,时间复杂度跟跳表一样。 -- **按照区间查找数据** -红黑树的效率低于跳表。跳表可以做到`O(logn)`定位区间的起点,然后在原始链表顺序往后遍历即可。 - -除了性能,还有其它原因: -- 代码实现比红黑树好懂、好写多了,因为简单就代表可读性好,不易出错 -- 跳表更灵活,可通过改变索引构建策略,有效平衡执行效率和内存消耗 - -因为红黑树比跳表诞生更早,很多编程语言中的Map类型(比如JDK 的 HashMap)都是通过红黑树实现的。业务开发时,直接从JDK拿来用,但跳表没有一个现成的实现,只能自己实现。 - -# 跳表的代码实现(Java 版) -## 数据结构定义 -表中的元素使用结点来表示,结点的层数在它被插入时随机计算决定(与表中已有结点数目无关)。 -一个i层的结点有i个前向指针(java中使用结点对象数组forward来表示),索引为从1到i。用MaxLevel来记录跳表的最大层数。 -跳表的层数为当前所有结点中的最大层数(如果list为空,则层数为1)。 -列表头header拥有从1到MaxLevel的前向指针: -```java -public class SkipList { - - // 最高层数 - private final int MAX_LEVEL; - // 当前层数 - private int listLevel; - // 表头 - private SkipListNode listHead; - // 表尾 - private SkipListNode NIL; - // 生成randomLevel用到的概率值 - private final double P; - // 论文里给出的最佳概率值 - private static final double OPTIMAL_P = 0.25; - - public SkipList() { - // 0.25, 15 - this(OPTIMAL_P, (int)Math.ceil(Math.log(Integer.MAX_VALUE) / Math.log(1 / OPTIMAL_P)) - 1); - } - - public SkipList(double probability, int maxLevel) { - P = probability; - MAX_LEVEL = maxLevel; - - listLevel = 1; - listHead = new SkipListNode(Integer.MIN_VALUE, null, maxLevel); - NIL = new SkipListNode(Integer.MAX_VALUE, null, maxLevel); - for (int i = listHead.forward.length - 1; i >= 0; i--) { - listHead.forward[i] = NIL; - } - } - - // 内部类 - class SkipListNode { - int key; - T value; - SkipListNode[] forward; - - public SkipListNode(int key, T value, int level) { - this.key = key; - this.value = value; - this.forward = new SkipListNode[level]; - } - } -} -``` -## 搜索算法 -按key搜索,找到返回该key对应的value,未找到则返回null。 - -通过遍历forward数组来需找特定的searchKey。假设skip list的key按照从小到大的顺序排列,那么从跳表的当前最高层listLevel开始寻找searchKey。在某一层找到一个非小于searchKey的结点后,跳到下一层继续找,直到最底层为止。那么根据最后搜索停止位置的下一个结点,就可以判断searchKey在不在跳表中。 -- 在跳表中找8的过程: -![这里写图片描述](https://img-blog.csdnimg.cn/img_convert/b7897fff74c4bf4e65f6c7ececdfe3ad.png) - -## 插入和删除算法 -都是通过查找与连接(search and splice): -![这里写图片描述](https://img-blog.csdnimg.cn/img_convert/29c8004c9ff02ce97b7987474eae5522.png) -维护一个update数组,在搜索结束之后,update[i]保存的是待插入/删除结点在第i层的左侧结点。 - -### 插入 - -若key不存在,则插入该key与对应的value;若key存在,则更新value。 - -如果待插入的结点的层数高于跳表的当前层数listLevel,则更新listLevel。 - -选择待插入结点的层数randomLevel: - -randomLevel只依赖于跳表的最高层数和概率值p。 - -另一种实现方法为,如果生成的randomLevel大于当前跳表的层数listLevel,那么将randomLevel设置为listLevel+1,这样方便以后的查找,在工程上是可以接受的,但同时也破坏了算法的随机性。 -### 删除 -删除特定的key与对应的value。如果待删除的结点为跳表中层数最高的结点,那么删除之后,要更新listLevel。 -```java -public class SkipList { - - // 最高层数 - private final int MAX_LEVEL; - // 当前层数 - private int listLevel; - // 表头 - private SkipListNode listHead; - // 表尾 - private SkipListNode NIL; - // 生成randomLevel用到的概率值 - private final double P; - // 论文里给出的最佳概率值 - private static final double OPTIMAL_P = 0.25; - - public SkipList() { - // 0.25, 15 - this(OPTIMAL_P, (int)Math.ceil(Math.log(Integer.MAX_VALUE) / Math.log(1 / OPTIMAL_P)) - 1); - } - - public SkipList(double probability, int maxLevel) { - P = probability; - MAX_LEVEL = maxLevel; - - listLevel = 1; - listHead = new SkipListNode(Integer.MIN_VALUE, null, maxLevel); - NIL = new SkipListNode(Integer.MAX_VALUE, null, maxLevel); - for (int i = listHead.forward.length - 1; i >= 0; i--) { - listHead.forward[i] = NIL; - } - } - - // 内部类 - class SkipListNode { - int key; - T value; - SkipListNode[] forward; - - public SkipListNode(int key, T value, int level) { - this.key = key; - this.value = value; - this.forward = new SkipListNode[level]; - } - } - - public T search(int searchKey) { - SkipListNode curNode = listHead; - - for (int i = listLevel; i > 0; i--) { - while (curNode.forward[i].key < searchKey) { - curNode = curNode.forward[i]; - } - } - - if (curNode.key == searchKey) { - return curNode.value; - } else { - return null; - } - } - - public void insert(int searchKey, T newValue) { - SkipListNode[] update = new SkipListNode[MAX_LEVEL]; - SkipListNode curNode = listHead; - - for (int i = listLevel - 1; i >= 0; i--) { - while (curNode.forward[i].key < searchKey) { - curNode = curNode.forward[i]; - } - // curNode.key < searchKey <= curNode.forward[i].key - update[i] = curNode; - } - - curNode = curNode.forward[0]; - - if (curNode.key == searchKey) { - curNode.value = newValue; - } else { - int lvl = randomLevel(); - - if (listLevel < lvl) { - for (int i = listLevel; i < lvl; i++) { - update[i] = listHead; - } - listLevel = lvl; - } - - SkipListNode newNode = new SkipListNode(searchKey, newValue, lvl); - - for (int i = 0; i < lvl; i++) { - newNode.forward[i] = update[i].forward[i]; - update[i].forward[i] = newNode; - } - } - } - - public void delete(int searchKey) { - SkipListNode[] update = new SkipListNode[MAX_LEVEL]; - SkipListNode curNode = listHead; - - for (int i = listLevel - 1; i >= 0; i--) { - while (curNode.forward[i].key < searchKey) { - curNode = curNode.forward[i]; - } - // curNode.key < searchKey <= curNode.forward[i].key - update[i] = curNode; - } - - curNode = curNode.forward[0]; - - if (curNode.key == searchKey) { - for (int i = 0; i < listLevel; i++) { - if (update[i].forward[i] != curNode) { - break; - } - update[i].forward[i] = curNode.forward[i]; - } - - while (listLevel > 0 && listHead.forward[listLevel - 1] == NIL) { - listLevel--; - } - } - } - - private int randomLevel() { - int lvl = 1; - while (lvl < MAX_LEVEL && Math.random() < P) { - lvl++; - } - return lvl; - } - - public void print() { - for (int i = listLevel - 1; i >= 0; i--) { - SkipListNode curNode = listHead.forward[i]; - while (curNode != NIL) { - System.out.print(curNode.key + "->"); - curNode = curNode.forward[i]; - } - System.out.println("NIL"); - } - } - - public static void main(String[] args) { - SkipList sl = new SkipList(); - sl.insert(20, 20); - sl.insert(5, 5); - sl.insert(10, 10); - sl.insert(1, 1); - sl.insert(100, 100); - sl.insert(80, 80); - sl.insert(60, 60); - sl.insert(30, 30); - sl.print(); - System.out.println("---"); - sl.delete(20); - sl.delete(100); - sl.print(); - } -} -``` \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204/\345\246\202\344\275\225\350\256\276\350\256\241\345\255\230\345\202\250\347\244\276\344\272\244\345\271\263\345\217\260\345\245\275\345\217\213\345\205\263\347\263\273\347\232\204\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204/\345\246\202\344\275\225\350\256\276\350\256\241\345\255\230\345\202\250\347\244\276\344\272\244\345\271\263\345\217\260\345\245\275\345\217\213\345\205\263\347\263\273\347\232\204\346\225\260\346\215\256\347\273\223\346\236\204.md" deleted file mode 100644 index 7546256101..0000000000 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204/\345\246\202\344\275\225\350\256\276\350\256\241\345\255\230\345\202\250\347\244\276\344\272\244\345\271\263\345\217\260\345\245\275\345\217\213\345\205\263\347\263\273\347\232\204\346\225\260\346\215\256\347\273\223\346\236\204.md" +++ /dev/null @@ -1,118 +0,0 @@ -x博中,两个人可以互相关注,互加好友,那如何存储这些社交网络的好友关系呢? - -这就要用到:图。 -# 什么是“图”?(Graph) -和树比起来,这是一种更加复杂的非线性表结构。 - -树的元素称为节点,图中元素叫作顶点(vertex)。图中的一个顶点可以与任意其他顶点建立连接关系,这种建立的关系叫作边(edge)。 -![](https://img-blog.csdnimg.cn/9e8d606b57b24f239a46a36734d4249c.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -社交网络就是典型的图结构。 - -把每个用户看作一个顶点。如果两个用户之间互加好友,就在两者之间建立一条边。 -所以,整个微信的好友关系就可用一张图表示。 -每个用户有多少个好友,对应到图中就叫作顶点的度(degree),即跟顶点相连接的边的条数。 - -不过微博的社交关系跟微信还有点不同,更复杂一点。微博允许单向关注,即用户A关注用户B,但B可不关注A。 - -> 如何用图表示这种单向社交关系呢? - -这就引入边的“方向”。 - -A关注B,就在图中画一条从A到B的带箭头的边,表示边的方向。A、B互关,就画一条从A指向B的边,再画一条从B指向A的边,这种边有方向的图叫作“有向图”。边没有方向的图也就叫“无向图”。 -![](https://img-blog.csdnimg.cn/f4ef3aa2589649838a55c13b5d559459.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -无向图中有“度”:一个顶点有多少条边。 -有向图中,把度分为: -- 入度(In-degree) -有多少条边指向这个顶点,即有多少粉丝 -- 出度(Out-degree) -有多少条边是以这个顶点为起点指向其他顶点。对应到微博的例子,即关注了多少人 - -QQ社交关系更复杂,不仅记录用户之间的好友关系,还记录了两个用户之间的亲密度,如何在图中记录这种好友关系亲密度呢? -这就要用到带权图(weighted graph),每条边都有个权重(weight),可以通过这个权重来表示QQ好友间的亲密度。 -![](https://img-blog.csdnimg.cn/3807928549ef4d99af2744c1350d8630.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -# 存储 -## 邻接矩阵存储方法 -最直观的一种存储方法,邻接矩阵(Adjacency Matrix)。 - -依赖一个二维数组: -- 无向图 -如果顶点i与顶点j之间有边,就将A[i][j]和A[j][i]标记为1 -- 有向图 -如果顶点i到顶点j之间,有一条箭头从顶点i指向顶点j的边,那我们就将A[i][j]标记为1 -如果有一条箭头从顶点j指向顶点i的边,我们就将A[j][i]标记为1 -- 带权图,数组中就存储相应的权重 -![](https://img-blog.csdnimg.cn/43973239a1424f979308c26e8c1781ba.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -> 简单、直观,但比较浪费存储空间! - -无向图,若A[i][j]==1,则A[j][i]==1。实际上,只需存储一个即可。即无向图的二维数组,如果将其用对角线划分为上下两部分,则只需利用上或下面这样一半空间就够了,另外一半其实完全浪费。 -如果存储的是稀疏图(Sparse Matrix),即顶点很多,但每个顶点的边并不多,则更浪费空间。 -如微信有好几亿用户,对应到图就是好几亿顶点。但每个用户好友并不很多,一般也就三五百个而已。如果我们用邻接矩阵来存储,那绝大部分的存储空间都被浪费了。 - -但这也并不是说,邻接矩阵的存储方法就完全没有优点。首先,邻接矩阵的存储方式简单、直接,因为基于数组,所以在获取两个顶点的关系时,就非常高效。其次,用邻接矩阵存储图的另外一个好处是方便计算。这是因为,用邻接矩阵的方式存储图,可以将很多图的运算转换成矩阵之间的运算。比如求解最短路径问题时会提到一个Floyd-Warshall算法,就是利用矩阵循环相乘若干次得到结果。 -## 邻接表存储方法 -针对上面邻接矩阵比较浪费内存空间,另外一种图存储,邻接表(Adjacency List)。 - -有点像散列表?每个顶点对应一条链表,链表中存储的是与这个顶点相连接的其他顶点。图中画的是一个有向图的邻接表存储方式,每个顶点对应的链表里面,存储的是指向的顶点。对于无向图来说,也是类似的,不过,每个顶点的链表中存储的,是跟这个顶点有边相连的顶点,你可以自己画下。 -![](https://img-blog.csdnimg.cn/ee3d7c7dc6f146c6adb34bcafe3b19d5.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -- 邻接矩阵存储较浪费空间,但更省时 -- 邻接表较节省存储空间,但较耗时 - -如上图示例,若要确定是否存在一条从顶点2到顶点4的边,就要遍历顶点2的链表,看其中是否存在顶点4,而链表存储对缓存不友好。所以邻接表查询两个顶点之间的关系较为低效。 - -基于链表法解决冲突的散列表中,若链过长,为提高查找效率,可将链表换成其他更高效数据结构,如平衡二叉查找树。 -邻接表长得很像散列。所以,也可将邻接表同散列表一样进行“优化”。 - -可将邻接表中的链表改成平衡二叉查找树。实际可选用红黑树。即可更快速查找两个顶点之间是否存在边。 -这里的二叉查找树也可换成其他动态数据结构,如跳表、散列表。 -还可将链表改成有序动态数组,通过二分查找快速定位两个顶点之间是否存在边。 -# 如何存储微博、微信等社交网络中的好友关系? -虽然微博有向图,微信是无向图,但对该问题,二者思路类似,以微博为例。 - -数据结构服务于算法,选择哪种存储方法和需支持的操作有关。 -对于微博用户关系,需支持如下操作: -- 判断用户A是否关注了用户B -- 判断用户A是否是用户B的粉丝 -- 用户A关注用户B -- 用户A取消关注用户B -- 根据用户名称的首字母排序,分页获取用户的粉丝列表 -- 根据用户名称的首字母排序,分页获取用户的关注列表 - -因为社交网络是一张稀疏图,使用邻接矩阵存储比较浪费存储空间。所以,这里采用邻接表。 - -但一个邻接表存储这种有向图也是不够的。查找某用户关注了哪些用户很容易,但若想知道某用户都被哪些用户关注了,即粉丝列表就没法了。 - -因此,还需一个逆邻接表,存储用户的被关注关系: -- 邻接表,每个顶点的链表中,存储的就是该顶点指向的顶点 -查找某个用户关注了哪些用户 -- 逆邻接表,每个顶点的链表中,存储的是指向该顶点的顶点 -查找某个用户被哪些用户关注 -![](https://img-blog.csdnimg.cn/69ba70ef961d441c9753c31258af84a8.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -基础的邻接表不适合快速判断两个用户是否为关注与被关注关系,所以进行优化,将邻接表的链表改为支持快速查找的动态数据结构。 - -> 那是红黑树、跳表、有序动态数组还是散列表呢? - -因需按照用户名称首字母排序,分页获取用户的粉丝列表或关注列表,跳表最合适:插入、删除、查找都非常高效,时间复杂度$O(logn)$,空间复杂度稍高,是$O(n)$。 -跳表存储数据先天有序,分页获取粉丝列表或关注列表,非常高效。 - -对小规模数据,如社交网络中只有几万、几十万个用户,可将整个社交关系存储在内存,该解决方案没问题。 - -> 但像微博上亿用户,数据量太大,无法全部存储在内存,何解? - -可通过哈希算法等数据分片方案,将邻接表存储在不同机器: -- 机器1存储顶点1,2,3的邻接表 -- 机器2存储顶点4,5的邻接表 -逆邻接表的处理方式同理。 - -当要查询顶点与顶点关系时,利用同样的哈希算法,先定位顶点所在机器,然后再在相应机器上查找。 -![](https://img-blog.csdnimg.cn/295354cd7c904fa793e2784269c58241.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -还能借助外部存储(比如硬盘),因为外部存储的存储空间比内存多很多: -如用下表存储这样一个图。为高效支持前面定义的操作,可建多个索引,比如第一列、第二列,给这两列都建立索引。 -![](https://img-blog.csdnimg.cn/b95b76c1fcce4f4a9df754bd80616f04.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -> 参考 -> - https://chowdera.com/2021/03/20210326155939001z.html -> - https://www.zhihu.com/question/20216864 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\350\216\267\345\217\226Top 10\345\276\256\345\215\232\347\203\255\346\220\234\345\205\263\351\224\256\350\257\215\357\274\237.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\350\216\267\345\217\226Top 10\345\276\256\345\215\232\347\203\255\346\220\234\345\205\263\351\224\256\350\257\215\357\274\237.md" deleted file mode 100644 index 0c21f06f0a..0000000000 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\350\216\267\345\217\226Top 10\345\276\256\345\215\232\347\203\255\346\220\234\345\205\263\351\224\256\350\257\215\357\274\237.md" +++ /dev/null @@ -1,178 +0,0 @@ -搜索引擎每天会接收大量的用户搜索请求,它会把这些用户输入的搜索关键词记录下来,然后再离线统计分析,得到最热门TopN搜索关键词。 - -现在有一包含10亿个搜索关键词的日志文件,如何能快速获取到热门榜Top 10搜索关键词? -可用堆解决,堆的几个应用:优先级队列、求Top K和求中位数。 -# 优先级队列 -首先应该是一个队列。队列最大的特性FIFO。 但优先级队列中,数据出队顺序是按优先级来,优先级最高的,最先出队。 - -方法很多,但堆实现最直接、高效。因为堆和优先级队列很相似。一个堆即可看作一个优先级队列。很多时候,它们只是概念上的区分。 -- 往优先级队列中插入一个元素,就相当于往堆中插入一个元素 -- 从优先级队列中取出优先级最高的元素,就相当于取出堆顶元素 - -优先级队列应用场景非常多:赫夫曼编码、图的最短路径、最小生成树算法等,Java的PriorityQueue。 -## 合并有序小文件 -- 有100个小文件 -- 每个文件100M -- 每个文件存储有序字符串 - -将这100个小文件合并成一个有序大文件,就用到优先级队列。 -像归排的合并函数。从这100个文件中,各取第一个字符串,放入数组,然后比较大小,把最小的那个字符串放入合并后的大文件中,并从数组中删除。 - -假设,这最小字符串来自13.txt这个小文件,就再从该小文件取下一个字符串并放入数组,重新比较大小,并且选择最小的放入合并后的大文件,并且将它从数组中删除。依次类推,直到所有的文件中的数据都放入到大文件为止。 - -用数组存储从小文件中取出的字符串。每次从数组取最小字符串,都需循环遍历整个数组,不高效,如何更高效呢? -就要用到优先级队列,即堆:将从小文件中取出的字符串放入小顶堆,则堆顶元素就是优先级队列队首,即最小字符串。 -将这个字符串放入大文件,并将其从堆中删除。 -再从小文件中取出下一个字符串,放入到堆 -循环该 过程,即可将100个小文件中的数据依次放入大文件。 - -删除堆顶数据、往堆插数据时间复杂度都是$O(logn)$,该案例$n=100$。 -这不比原来数组存储高效多了? -# 2 高性能定时器 -有一定时器,维护了很多定时任务,每个任务都设定了一个执行时间点。 -定时器每过一个单位时间(如1s),就扫描一遍任务,看是否有任务到达设定执行时间。若到达,则执行。 -![](https://img-blog.csdnimg.cn/a8005a51f4ed46ada66d75c0f198c5f0.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -显然这样每过1s就扫描一遍任务列表很低效: -- 任务约定执行时间离当前时间可能还很久,这样很多次扫描其实都无意义 -- 每次都要扫描整个任务列表,若任务列表很大,就很耗时 - -这时就该优先级队列上场了。按任务设定的执行时间,将这些任务存储在优先级队列,队首(即小顶堆的堆顶)存储最先执行的任务。 - -这样,定时器就无需每隔1s就扫描一遍任务列表了。 - -> $队首任务执行时间点 - 当前时间点相减 = 时间间隔T$ - -T就是,从当前时间开始,需等待多久,才会有第一个任务要被执行。 -定时器就能设定在T秒后,再来执行任务。 -当前时间点 ~ $(T-1)s$ 时间段,定时器无需做任何事情。 - -当Ts时间过去后,定时器取优先级队列中队首任务执行 -再计算新的队首任务执行时间点与当前时间点差值,将该值作为定时器执行下一个任务需等待时间。 - -如此设计,定时器既不用间隔1s就轮询一次,也无需遍历整个任务列表,性能大大提高。 -# 利用堆求Top K -求Top K的问题抽象成两类: -## 静态数据集合 -数据集合事先确定,不会再变。 - -可维护一个大小为K的小顶堆,顺序遍历数组,从数组中取数据与堆顶元素比较: -- >堆顶 -删除堆顶,并将该元素插入堆 -- <堆顶 -do nothing,继续遍历数组 - -等数组中的数据都遍历完,堆中数据就是Top K。 - -遍历数组需要$O(n)$时间复杂度 -一次堆化操作需$O(logK)$时间复杂度 -所以最坏情况下,n个元素都入堆一次,所以时间复杂度就是$O(nlogK)$ -## 动态数据集合 -数据集合事先并不确定,有数据动态地加入到集合中,也就是求实时Top K。 -一个数据集合中有两个操作: -- 添加数据 -- 询问当前TopK数据 - -若每次询问Top K大数据,都基于当前数据重新计算,则时间复杂度$O(nlogK)$,n表示当前数据的大小。 -其实可一直都维护一个K大小的小顶堆,当有数据被添加到集合,就拿它与堆顶元素对比: -- >堆顶 -就把堆顶元素删除,并且将这个元素插入到堆中 -- <堆顶 -do nothing。无论何时需查询当前的前K大数据,都可以里立刻返回给他 -# 利用堆求中位数 -求**动态数据**集合中的中位数: -- 数据个数奇数 -把数据从小到大排列,第$\frac{n}{2}+1$个数据就是中位数 -- 数据个数是偶数 -处于中间位置的数据有两个,第$\frac{n}{2}$个、第$\frac{n}{2}+1$个数据,可随意取一个作为中位数,比如取两个数中靠前的那个,即第$\frac{n}{2}$个数据 - -一组静态数据的中位数是固定的,可先排序,第$\frac{n}{2}$个数据就是中位数。 -每次询问中位数,直接返回该固定值。所以,尽管排序的代价比较大,但是边际成本会很小。但是,如果我们面对的是动态数据集合,中位数在不停地变动,如果再用先排序的方法,每次询问中位数的时候,都要先进行排序,那效率就不高了。 - -借助堆,不用排序,即可高效地实现求中位数操作: -需维护两个堆: -- 大顶堆 -存储前半部分数据 -- 小顶堆 -存储后半部分数据 && 小顶堆数据都 > 大顶堆数据 - -即若有n(偶数)个数据,从小到大排序,则: -- 前 $\frac{n}{2}$ 个数据存储在大顶堆 -- 后$\frac{n}{2}$个数据存储在小顶堆 - -大顶堆中的堆顶元素就是我们要找的中位数。 - -n是奇数也类似: -- 大顶堆存储$\frac{n}{2}+1$个数据 -- 小顶堆中就存储$\frac{n}{2}$个数据 - -数据动态变化,当新增一个数据时,如何调整两个堆,让大顶堆堆顶继续是中位数, 若: -- 新加入的数据 ≤ 大顶堆堆顶,则将该新数据插到大顶堆 -- 新加入的数据大于等于小顶堆的堆顶元素,我们就将这个新数据插入到小顶堆。 - -这时可能出现,两个堆中的数据个数不符合前面约定的情况,若: -- n是偶数,两个堆中的数据个数都是 $\frac{n}{2}$ -- n是奇数,大顶堆有 $\frac{n}{2}+1$ 个数据,小顶堆有 $\frac{n}{2}$ 个数据 - -即可从一个堆不停将堆顶数据移到另一个堆,以使得两个堆中的数据满足上面约定。 - -插入数据涉及堆化,所以时间复杂度$O(logn)$,但求中位数只需返回大顶堆堆顶,所以时间复杂度$O(1)$。 - -利用两个堆还可快速求其他百分位的数据,原理类似。 -“如何快速求接口的99%响应时间? - -中位数≥前50%数据,类比中位数,若将一组数据从小到大排列,这个99百分位数就是大于前面99%数据的那个数据。 - -假设有100个数据:1,2,3,……,100,则99百分位数就是99,因为≤99的数占总个数99%。 - -> 那99%响应时间是啥呢? - -若有100个接口访问请求,每个接口请求的响应时间都不同,如55ms、100ms、23ms等,把这100个接口的响应时间按照从小到大排列,排在第99的那个数据就是99%响应时间,即99百分位响应时间。 - -即若有n个数据,将数据从小到大排列后,99百分位数大约就是第n*99%个数据。 -维护两个堆,一个大顶堆,一个小顶堆。假设当前总数据的个数是n,大顶堆中保存n*99%个数据,小顶堆中保存n*1%个数据。大顶堆堆顶的数据就是我们要找的99%响应时间。 - -每插入一个数据时,要判断该数据跟大顶堆、小顶堆堆顶的大小关系,以决定插入哪个堆: -- 新插入数据 < 大顶堆的堆顶,插入大顶堆 -- 新插入的数据 > 小顶堆的堆顶,插入小顶堆 - -但为保持大顶堆中的数据占99%,小顶堆中的数据占1%,每次新插入数据后,都要重新计算,这时大顶堆和小顶堆中的数据个数,是否还符合99:1: -- 不符合,则将一个堆中的数据移动到另一个堆,直到满足比例 -移动的方法类似前面求中位数的方法 - -如此,每次插入数据,可能涉及几个数据的堆化操作,所以时间复杂度$O(logn)$。 -每次求99%响应时间时,直接返回大顶堆中的堆顶即可,时间复杂度$O(1)$。 -# 含10亿个搜索关键词的日志文件,快速获取Top 10 -很多人肯定说使用MapReduce,但若将场景限定为单机,可使用内存为1GB,你咋办? - -用户搜索的关键词很多是重复的,所以首先要统计每个搜索关键词出现的频率。 -可通过散列表、平衡二叉查找树或其他一些支持快速查找、插入的数据结构,记录关键词及其出现次数。 - -假设散列表。 -顺序扫描这10亿个搜索关键词。当扫描到某关键词,去散列表中查询: -- 存在,对应次数加一 -- 不存在,插入散列表,并记录次数1 - -等遍历完这10亿个搜索关键词后,散列表就存储了不重复的搜索关键词及出现次数。 - -再根据堆求Top K方案,建立一个大小为10小顶堆,遍历散列表,依次取出每个搜索关键词及对应出现次数,然后与堆顶搜索关键词对比: -- 出现次数 > 堆顶搜索关键词的次数 -删除堆顶关键词,将该出现次数更多的关键词入堆。 - -以此类推,当遍历完整个散列表中的搜索关键词之后,堆中的搜索关键词就是出现次数最多的Top 10搜索关键词了。 - -但其实有问题。10亿的关键词还是很多的。 -假设10亿条搜索关键词中不重复的有1亿条,如果每个搜索关键词的平均长度是50个字节,那存储1亿个关键词起码需要5G内存,而散列表因为要避免频繁冲突,不会选择太大的装载因子,所以消耗的内存空间就更多了。 -而机器只有1G可用内存,无法一次性将所有的搜索关键词加入内存。 - -> 何解? - -因为相同数据经哈希算法后的哈希值相同,可将10亿条搜索关键词先通过哈希算法分片到10个文件: -- 创建10个空文件:00~09 -- 遍历这10亿个关键词,并通过某哈希算法求哈希值 -- 哈希值同10取模,结果就是该搜索关键词应被分到的文件编号 - -10亿关键词分片后,每个文件都只有1亿关键词,去掉重复的,可能就只剩1000万,每个关键词平均50个字节,总大小500M,1G内存足矣。 - -针对每个包含1亿条搜索关键词的文件: -- 利用散列表和堆,分别求Top 10 -- 10个Top 10放一起,取这100个关键词中,出现次数Top 10关键词,即得10亿数据的Top 10热搜关键词 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MyBatis or JPA.md" "b/\346\227\245\345\270\270\344\270\232\345\212\241\351\227\256\351\242\230\350\247\243\345\206\263\346\226\271\346\241\210/MyBatis or JPA.md" similarity index 100% rename from "\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/MyBatis or JPA.md" rename to "\346\227\245\345\270\270\344\270\232\345\212\241\351\227\256\351\242\230\350\247\243\345\206\263\346\226\271\346\241\210/MyBatis or JPA.md" diff --git "a/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/\345\210\206\345\270\203\345\274\217\346\234\215\345\212\241\346\216\245\345\217\243\347\232\204\345\271\202\347\255\211\346\200\247.md" "b/\346\227\245\345\270\270\344\270\232\345\212\241\351\227\256\351\242\230\350\247\243\345\206\263\346\226\271\346\241\210/\345\210\206\345\270\203\345\274\217\346\234\215\345\212\241\346\216\245\345\217\243\347\232\204\345\271\202\347\255\211\346\200\247.md" similarity index 100% rename from "\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/\345\210\206\345\270\203\345\274\217\346\234\215\345\212\241\346\216\245\345\217\243\347\232\204\345\271\202\347\255\211\346\200\247.md" rename to "\346\227\245\345\270\270\344\270\232\345\212\241\351\227\256\351\242\230\350\247\243\345\206\263\346\226\271\346\241\210/\345\210\206\345\270\203\345\274\217\346\234\215\345\212\241\346\216\245\345\217\243\347\232\204\345\271\202\347\255\211\346\200\247.md" diff --git "a/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/\345\210\206\345\272\223\345\210\206\350\241\250\345\220\216\345\205\250\345\261\200id\347\224\237\346\210\220\346\226\271\346\241\210.md" "b/\346\227\245\345\270\270\344\270\232\345\212\241\351\227\256\351\242\230\350\247\243\345\206\263\346\226\271\346\241\210/\345\210\206\345\272\223\345\210\206\350\241\250\345\220\216\345\205\250\345\261\200id\347\224\237\346\210\220\346\226\271\346\241\210.md" similarity index 100% rename from "\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/\345\210\206\345\272\223\345\210\206\350\241\250\345\220\216\345\205\250\345\261\200id\347\224\237\346\210\220\346\226\271\346\241\210.md" rename to "\346\227\245\345\270\270\344\270\232\345\212\241\351\227\256\351\242\230\350\247\243\345\206\263\346\226\271\346\241\210/\345\210\206\345\272\223\345\210\206\350\241\250\345\220\216\345\205\250\345\261\200id\347\224\237\346\210\220\346\226\271\346\241\210.md" diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/\346\225\260\346\215\256\345\272\223\350\241\250\350\256\276\350\256\241\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/\346\227\245\345\270\270\344\270\232\345\212\241\351\227\256\351\242\230\350\247\243\345\206\263\346\226\271\346\241\210/\346\225\260\346\215\256\345\272\223\350\241\250\350\256\276\350\256\241\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" similarity index 100% rename from "\346\225\260\346\215\256\345\255\230\345\202\250/MySQL/\346\225\260\346\215\256\345\272\223\350\241\250\350\256\276\350\256\241\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" rename to "\346\227\245\345\270\270\344\270\232\345\212\241\351\227\256\351\242\230\350\247\243\345\206\263\346\226\271\346\241\210/\346\225\260\346\215\256\345\272\223\350\241\250\350\256\276\350\256\241\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" diff --git "a/\346\235\203\351\231\220\347\256\241\347\220\206/Shiro\345\256\236\346\210\230(\345\205\255)---\346\235\203\351\231\220\347\274\223\345\255\230.md" "b/\346\235\203\351\231\220\347\256\241\347\220\206/Shiro\345\256\236\346\210\230(\345\205\255)---\346\235\203\351\231\220\347\274\223\345\255\230.md" index 63e0c81b51..b7fb45f91c 100644 --- "a/\346\235\203\351\231\220\347\256\241\347\220\206/Shiro\345\256\236\346\210\230(\345\205\255)---\346\235\203\351\231\220\347\274\223\345\255\230.md" +++ "b/\346\235\203\351\231\220\347\256\241\347\220\206/Shiro\345\256\236\346\210\230(\345\205\255)---\346\235\203\351\231\220\347\274\223\345\255\230.md" @@ -70,7 +70,7 @@ this.manager = net.sf.ehcache.CacheManager.create(getCacheManagerConfigFileInput ``` - 测试用例 -```java +``` @Test public void testClearCachedAuthenticationInfo() { login(u1.getUsername(), password); diff --git "a/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\346\210\230(1)-\345\255\220\345\237\237\343\200\201\346\240\270\345\277\203\345\237\237\343\200\201\351\200\232\347\224\250\345\237\237\345\222\214\346\224\257\346\222\221\345\237\237\347\255\211\345\237\272\346\234\254\346\246\202\345\277\265.md" "b/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\346\210\230(1)-\345\255\220\345\237\237\343\200\201\346\240\270\345\277\203\345\237\237\343\200\201\351\200\232\347\224\250\345\237\237\345\222\214\346\224\257\346\222\221\345\237\237\347\255\211\345\237\272\346\234\254\346\246\202\345\277\265.md" index 928d29571f..d070235378 100644 --- "a/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\346\210\230(1)-\345\255\220\345\237\237\343\200\201\346\240\270\345\277\203\345\237\237\343\200\201\351\200\232\347\224\250\345\237\237\345\222\214\346\224\257\346\222\221\345\237\237\347\255\211\345\237\272\346\234\254\346\246\202\345\277\265.md" +++ "b/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\346\210\230(1)-\345\255\220\345\237\237\343\200\201\346\240\270\345\277\203\345\237\237\343\200\201\351\200\232\347\224\250\345\237\237\345\222\214\346\224\257\346\222\221\345\237\237\347\255\211\345\237\272\346\234\254\346\246\202\345\277\265.md" @@ -1,9 +1,8 @@ # 1 领域 用以确定边界。 - DDD按规则细分业务领域,细分到一定程度,DDD会将问题范围限定在特定边界,在该边界内建立领域模型,进而用代码实现该领域模型,解决相应业务问题。 -领域就是该边界内要解决的**业务问题域**。其越大,则业务范围越广。 +领域就是该边界内要解决的业务问题域。其越大,则业务范围越广。 ## 领域模型的特点 对业务领域建模: @@ -12,14 +11,15 @@ DDD按规则细分业务领域,细分到一定程度,DDD会将问题范围 - 需要经验 简单的领域模型: -- 几乎和DB中的表一一对应 +- 几乎和数据库中的表一一对应 - 复杂领域模型 - 使用了继承,组合,设计模式等各种手段 # 2 子域 -领域可再划分为多个子领域,即子域。 -每个子域对应一个更小的问题域或业务范围。 +领域可再划分为多个子领域,即子域。每个子域对应一个更小的问题域或业务范围。 + +DDD是处理复杂领域的设计思想,它试图分离技术实现的复杂度。 -DDD是处理复杂领域的设计思想,它试图分离技术实现的复杂度。每个细分的领域都有一个知识体系,即DDD的领域模型。在所有子域研究完后,就建立了领域模型。 +每个细分的领域都有一个知识体系,即DDD的领域模型。在所有子域研究完后,就建立了领域模型。 比如酒店行业,一开始的酒店核心系统是单体架构,后来业务发展,开始转型中台,引入微服务。微服务架构就需划分业务领域边界,建立领域模型,并实现微服务落地。 可根据业务关联度及流程边界将酒店领域细分为:预订,入住,退房,客房服务,点餐等领域事件。 diff --git "a/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\346\210\230(2)-\351\231\220\347\225\214\344\270\212\344\270\213\346\226\207(bounded context).md" "b/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\346\210\230(2)-\351\231\220\347\225\214\344\270\212\344\270\213\346\226\207(bounded context).md" index d79cc79eab..72e8b956d5 100644 --- "a/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\346\210\230(2)-\351\231\220\347\225\214\344\270\212\344\270\213\346\226\207(bounded context).md" +++ "b/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\346\210\230(2)-\351\231\220\347\225\214\344\270\212\344\270\213\346\226\207(bounded context).md" @@ -1,11 +1,10 @@ 限界上下文定义领域边界,以确保每个上下文含义在它特定的边界内都具有唯一含义,领域模型则存于该边界内。 # 通用语言 -## 定义 事件风暴过程中,通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言。限界上下文中的通用语言提供了设计领域模型的概念术语。 通用语言是必须通过与领域专家详细讨论后才得到的统一语言,不管你在团队承担什么角色,在同一领域的软件生命周期里都使用统一语言交流。通用语言定义上下文含义,可解决交流障碍,使领域专家和开发人员能够协作,从而确保业务需求的正确表达。 -## 组成 + 通用语言包含术语和用例场景,能够直接反映在代码。通用语言中的 - 名词 给领域对象等概念命名,如商品、订单,对应**实体**对象 @@ -14,8 +13,8 @@ - 动词 表示可完成的操作,比如一个动作或事件,如商品已下单、订单已付款,对应**领域事件**或**命令** -通用语言贯穿DDD。基于它,代码可读性更好,最后能将业务需求准确转化为代码设计。 -![](https://img-blog.csdnimg.cn/2020092919365579.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) +通用语言贯穿DDD。基于它,代码可读性更好,最后将业务需求准确转化为代码设计。 +![](https://img-blog.csdnimg.cn/2020092919365579.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70#pic_center) 1. 事件风暴时,领域专家会和设计、开发人员一起建立领域模型,在领域建模过程中形成通用的业务术语和用户故事,这也是一个项目团队统一语言的过程 2. 通过用户故事分析会形成一个个领域对象,这些领域对象对应领域模型的业务对象,每一个业务对象和领域对象都有通用的名词术语,并且一一映射 @@ -35,15 +34,10 @@ DDD分析和设计过程中的每一个环节都需要保证限界上下文内 ## 案例 业务的通用语言也有它的业务边界。限界上下文就是用来细分领域,从而定义通用语言所在的边界。 - -如电商领域的商品,商品在不同阶段有不同术语: -- 销售阶段是商品 -- 运输阶段则变成货物 - -同一个东西,由于业务领域不同,赋予了这些术语不同涵义和职责边界,这个边界就可能会成为未来微服务设计的边界。领域边界就是通过限界上下文来定义的。 +如电商领域的商品,商品在不同阶段有不同术语,在销售阶段是商品,而在运输阶段则变成货物。同一个东西,由于业务领域不同,赋予了这些术语不同涵义和职责边界,这个边界就可能会成为未来微服务设计的边界。领域边界就是通过限界上下文来定义的。 # 限界上下文和微服务 -子域还可根据需要进一步拆分为子子域。如支付子域,继续拆为收款、付款子子域。拆到一定程度后,有些子子域的领域边界可能变成限界上下文的边界了。 +子域还可根据需要进一步拆分为子子域:比如,支付子域继续拆分为收款和付款子子域。拆到一定程度后,有些子子域的领域边界可能变成限界上下文的边界了。 子域可能会包含多个限界上下文。 如理赔子域就包括报案、查勘和定损等多个限界上下文(限界上下文与理赔的子子域领域边界重合)。也有可能子域本身的边界就是限界上下文边界,如投保子域。 diff --git "a/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\347\232\204\346\210\230\347\225\245\350\256\276\350\256\241.md" "b/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\347\232\204\346\210\230\347\225\245\350\256\276\350\256\241.md" deleted file mode 100644 index 22d890033e..0000000000 --- "a/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\347\232\204\346\210\230\347\225\245\350\256\276\350\256\241.md" +++ /dev/null @@ -1,78 +0,0 @@ -模型设计,DDD 分两阶段,战略设计和战术设计。 -# 战略设计 -战略设计的子域、限界上下文和上下文映射图等概念大致分为: -- 业务划分 -以区分不同业务,即划分识别出来的业务概念 -- 落地成解决方案 -将划分出来的业务落地 - -# 业务概念的划分 -首先需要明确: -## 问题是什么 -我们要解决的问题就是领域问题,相关概念如,子域、核心域、支撑域、通用域等。其实都是:如何分解问题。 -## 如何解决 -领域(Domain)是一号位,对应待解决的问题。解决问题通常思路是分而治之,DDD就把一个大领域分解成若干小领域(子域(Subdomain))。 - -DDD首先要建立起一套通用语言,拥有一致的业务词汇表,它们对应模型。 -接着,要分类词汇,即把它们划分到不同子域。**关键就是分离关注点。** - -比如,做个项目管理软件,就要有用户、项目、团队,不同人还要扮演不同角色。 -第一步,至少先分开身份管理、项目管理,因为关注点不同: -- 身份管理,关注用户身份信息,如用户名、密码 -- 项目管理,关注项目和团队等 - -这就有了俩子域:身份管理,项目管理。 - -若直接给结果,你可能会觉得很好理解。但划分出不同子域还是容易出问题的,因为有些概念不易区分。 -比如,用户应该怎么划分?放在身份管理是合适的,但项目管理也要用到呀! - -根据单一职责原则,它给了我们一个重要思考维度,**变化从何而来**? -不同角色的人关注不同变化,所以,虽然我们用的词都是“用户”,但想表达的含义其实不同,最好分开这些不同的含义,即分开不同角色: -- 身份管理中,它是“用户” -- 项目管理中,就成了“项目成员” -# 业务概念的落地 -DDD问题层面的概念已经阐述完毕。接下来,就是解决方案层面。 - -切分出的子域,怎样落实到代码? -首先要解决的就是这些子域如何组织? -- 写一个程序把所有子域都放里面 -- 每个子域单独做个应用 -- 有一些在一起,有一些分开 - -这就引出DDD的 -# 限界上下文(Bounded Context) -限定了通用语言自由使用的边界,一旦出界,含义便无法保证。 -比如,同样是“订单”,不加限制,很难区分它在哪种场景。一旦定义了限界上下文,那交易上下文的“订单”和物流上下文的“订单”肯定不同。就是因为订单这个说法,在不同边界内,含义不同。 - -注意,子域和限界上下文不一定是一一对应的,可能在一个限界上下文中包含了多个子域,也可能在一个子域横跨了多个限界上下文。 - -限界上下文是在解决方案层面,所以,就可以把限界上下文看作一个独立系统。限界上下文与微服务的理念契合,每个限界上下文都可成为一个独立服务。 - -限界上下文是完全独立的,不会为了完成一个业务需求要跑到其他服务中去做很多事,这恰是很多微服务出问题的点,比如一个业务功能要调用很多其他系统功能。 - -有限界上下文,就可以把整个业务分解到不同的限界上下文中,但是,尽管我们拆分了系统,它们终究还是一个系统,免不了交互。 -比如: -- 一个用户下了订单,这是在订单上下文中完成的 -- 用户要去支付,这是在支付上下文中完成的 - -我们要通过某种途径让订单上下文的一些信息发送到支付上下文。所以,要有一种描述方式,描述不同限界上下文之间交互的方式-上下文映射图(Context Map)。 -DDD 给我们提供了一些描述这种交互的方式,比如: -- 合作关系(Partnership) -- 共享内核(Shared Kernel) -- 客户-供应商(Customer-Supplier) -- 跟随者(Conformist) -- 防腐层(Anticorruption Layer) -防腐层是最具防御性的一种关系,就是在外部模型和内部模型之间建立起一个翻译层,将外部模型转化为内部模型。但凡有可能,就要建立防腐层,将外部模型完全隔离开。 -- 开放主机服务(Open Host Service) -- 发布语言(Published Language) -- 各行其道(Separate Ways) -- 大泥球(Big Ball of Mud) -要规避 - -这么多交互方式,主要是为让你在头脑中仔细辨认,看看限界上下文之间到底在以怎样的方式交互。 - -知道不同限界上下文之间交互方式后,不同交互方式就可落地为不同协议。 -常用协议如:REST API、RPC 或是 MQ, 按需选型即可。 - -在我们定义好不同的限界上下文,将它们之间的交互呈现出来之后,就得到了一张上下文映射图。 -上下文映射图是可以帮助我们理解系统的各个部分之间,是怎样进行交互的,建立全局性认知。 \ No newline at end of file diff --git "a/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/\345\205\205\350\241\200\346\250\241\345\236\213\343\200\201\350\264\253\350\241\200\351\242\206\345\237\237\346\250\241\345\236\213.md" "b/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/\345\205\205\350\241\200\346\250\241\345\236\213\343\200\201\350\264\253\350\241\200\351\242\206\345\237\237\346\250\241\345\236\213.md" index 351028cad7..4bda8731aa 100644 --- "a/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/\345\205\205\350\241\200\346\250\241\345\236\213\343\200\201\350\264\253\350\241\200\351\242\206\345\237\237\346\250\241\345\236\213.md" +++ "b/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/\345\205\205\350\241\200\346\250\241\345\236\213\343\200\201\350\264\253\350\241\200\351\242\206\345\237\237\346\250\241\345\236\213.md" @@ -10,16 +10,15 @@ - 只有状态的对象就是所谓的“贫血对象”(常称为VO——Value Object) - 只有行为的对象就是我们常见的N层结构中的Logic/Service/Manager层(对应到EJB2中的Stateless Session Bean)。(曾经Spring的作者Rod Johnson也承认,Spring不过是在沿袭EJB2时代的“事务脚本”,也就是面向过程编程) -贫血领域模型是一个存在已久的反模式,它不是个好东西。 +贫血领域模型是一个存在已久的反模式,目前仍有许多拥趸者。Martin Fowler曾经和Eric Evans聊天谈到它时,都觉得这个模型似乎越来越流行了。作为领域模型的推广者,他们觉得这不是一件好事。 -- 贫血领域模型的基本特征 -它第一眼看起来还真像这么回事儿。项目中有许多对象,它们的命名都是根据领域来的。对象之间有着丰富的连接方式,和真正的领域模型非常相似。但当你检视这些对象的行为时,会发现它们基本上没有任何行为,仅仅是一堆getter/setter。 +贫血领域模型的基本特征是:它第一眼看起来还真像这么回事儿。项目中有许多对象,它们的命名都是根据领域来的。对象之间有着丰富的连接方式,和真正的领域模型非常相似。但当你检视这些对象的行为时,会发现它们基本上没有任何行为,仅仅是一堆getter/setter。 其实这些对象在设计之初就被定义为只能包含数据,不能加入领域逻辑。这些逻辑要全部写入一组叫Service的对象中。这些Service构建在领域模型之上,使用这些模型来传递数据。 -## 反模式的恐怖 -它完全和面向对象设计背道而驰。面向对象设计主张将数据和行为绑定在一起,而贫血领域模型则更像是一种面向过程设计。 +这种反模式的恐怖之处在于,它完全和面向对象设计背道而驰。 +面向对象设计主张将数据和行为绑定在一起,而贫血领域模型则更像是一种面向过程设计,Martin Fowler和Eric在Smalltalk时就极力反对这种做法。更糟糕的时,很多人认为这些贫血领域对象是真正的对象,从而彻底误解了面向对象设计的涵义。 -贫血领域模型的根本问题在于,它**引入了领域模型设计的所有成本,却没有带来任何好处**。 +如今,面向对象的概念已经传播得很广泛了,而要反对这种贫血领域模型的做法,还需要更多论据。贫血领域模型的根本问题在于,它**引入了领域模型设计的所有成本,却没有带来任何好处**。 最主要的成本是将对象映射到数据库中,从而产生了一个O/R(对象关系)映射层。只有当你充分使用了面向对象设计来组织复杂的业务逻辑后,这一成本才能够被抵消。如果将所有行为都写入到Service对象,那最终你会得到一组事务处理脚本,从而错过了领域模型带来的好处。正如martin在企业应用架构模式一书中说到的,领域模型并不一定是最好的工具。 将行为放入领域模型,这点和分层设计(领域层、持久化层、展现层等)并不冲突。因为领域模型中放入的是和领域相关的逻辑——验证、计算、业务规则等。如果你要讨论能否将数据源或展现逻辑放入到领域模型中,这就不在本文论述范围之内了。 @@ -36,7 +35,7 @@ Eric Evans的Domain Driven Design一书中提到: 服务层很薄——所有重要的业务逻辑都写在领域层。他在服务模式中复述了这一观点: 如今人们常犯的错误是不愿花时间将业务逻辑放到合适的领域模型中,从而逐渐形成面向过程的程序设计。 -为什么这种反模式会那么常见呢。我怀疑是因为大多数人并没有使用过一个设计良好的领域模型,特别是那些以数据为中心的开发人员。此外,有些技术也会推动这种反模式,比如J2EE的Entity Bean,这会让我更倾向于使用POJO领域模型。 +我不清楚为什么这种反模式会那么常见。我怀疑是因为大多数人并没有使用过一个设计良好的领域模型,特别是那些以数据为中心的开发人员。此外,有些技术也会推动这种反模式,比如J2EE的Entity Bean,这会让我更倾向于使用POJO领域模型。 总之,如果你将大部分行为都放置在服务层,那么你就会失去领域模型带来的好处。如果你将所有行为都放在服务层,那你就无可救药了。 @@ -69,7 +68,7 @@ Eric Evans的Domain Driven Design一书中提到: 如果一个对象包含其他对象,那就将职责继续委托下去,由具体的 POJO 执行业务逻辑,将策略模式更加细粒度,而不是写 ifelse。 -> 参考 -> - 《领域驱动设计》 -> - https://www.zhihu.com/question/20360521/answer/14891150 -> - https://www.martinfowler.com/bliki/AnemicDomainModel.html \ No newline at end of file +参考 +- 《领域驱动设计》 +- https://www.zhihu.com/question/20360521/answer/14891150 +- https://www.martinfowler.com/bliki/AnemicDomainModel.html \ No newline at end of file diff --git "a/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Dubbo/Dubbo\347\232\204SPI\346\234\272\345\210\266.md" "b/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Dubbo/Dubbo\345\222\214JDK\347\232\204SPI\346\234\272\345\210\266\345\257\271\346\257\224\350\257\246\350\247\243.md" similarity index 56% rename from "\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Dubbo/Dubbo\347\232\204SPI\346\234\272\345\210\266.md" rename to "\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Dubbo/Dubbo\345\222\214JDK\347\232\204SPI\346\234\272\345\210\266\345\257\271\346\257\224\350\257\246\350\247\243.md" index 15237562fb..8c1547fbe1 100644 --- "a/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Dubbo/Dubbo\347\232\204SPI\346\234\272\345\210\266.md" +++ "b/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Dubbo/Dubbo\345\222\214JDK\347\232\204SPI\346\234\272\345\210\266\345\257\271\346\257\224\350\257\246\350\247\243.md" @@ -1,34 +1,66 @@ -Dubbo 没使用 Java SPI,而重新实现了一套功能更强的 SPI。 +# 1 SPI简介 +SPI,Service Provider Interface,一种服务发现机制。 -Dubbo SPI 逻辑封装在 ExtensionLoader 类,通过 ExtensionLoader,可加载指定实现类。Dubbo SPI 所需配置文件需放置在 `META-INF/dubbo` 路径:![](https://img-blog.csdnimg.cn/20201220143821997.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) +比如一接口有3个实现类,那么在系统运行时,这个接口到底该选择哪个实现类? +这就需要SPI,**根据指定或默认的配置,找到对应的实现类,加载进来,然后使用该实现类实例**。 +![](https://img-blog.csdnimg.cn/20201220141747102.png) -配置内容如下: +在系统实际运行的时候,会加载你的配置,用实现A2实例化一个对象来提供服务。 + +比如你要通过jar包给某个接口提供实现,然后你就在自己jar包的`META-INF/services/`目录下放一个接口同名文件,指定接口的实现是自己这个jar包里的某个类。 +![](https://img-blog.csdnimg.cn/20201220142131599.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +别人用这个接口,然后用你的jar包,就会在运行时通过你的jar包的那个文件找到这个接口该用哪个实现类。这是JDK提供的功能。 + +比如你有个工程A,有个接口A,接口A在工程A没有实现类,系统运行时怎么给接口A选个实现类呢? +可以自己搞个jar包,`META-INF/services/`,放上一个文件,文件名即接口名,接口A,接口A的实现类=`com.javaedge.service.实现类A2`。 + +让工程A来依赖你的jar包,然后在系统运行时,工程A跑起来,对于接口A,就会扫描依赖的jar包,看看有没有`META-INF/services`文件夹。 +如果有,再看看有没有名为接口A的文件,如果有,在里面找一下指定的接口A的实现是你的jar包里的哪个类! + +# 2 适用场景 +插件扩展的场景,比如你开发了一个开源框架,若你想让别人自己写个插件,安排到你的开源框架里中,扩展功能 + +## 2.1 Java中的SPI +经典的思想体现,其实大家平时都在用,比如JDBC。Java定义了一套JDBC的接口,但并未提供其实现类。 + +但实际上项目运行时,要使用JDBC接口的哪些实现类呢? +一般要根据自己使用的数据库引入: +- MySQL,`mysql-jdbc-connector.jar` +![](https://img-blog.csdnimg.cn/20201220151405844.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +系统运行时碰到你使用JDBC的接口,就会在底层使用你引入的那个jar中提供的实现类。 + +## 2.2 Dubbo中的SPI +Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类,通过 ExtensionLoader,可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下 +![](https://img-blog.csdnimg.cn/20201220143821997.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +配置内容如下。 ```java -Protocol protocol = ExtensionLoader - .getExtensionLoader(Protocol.class) - .getAdaptiveExtension(); +Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); ``` -Dubbo要判断系统运行时,应该选用该Protocol接口的哪个实现类。 -它会去找一个你配置的Protocol,将你配置的Protocol实现类,加载进JVM,将其实例化。 +Dubbo要判断一下,在系统运行时,应该选用这个Protocol接口的哪个实现类。 +它会去找一个你配置的Protocol,将你配置的Protocol实现类,加载进JVM,将其实例化。 微内核,可插拔,大量的组件,Protocol负责RPC调用的东西,你可以实现自己的RPC调用组件,实现Protocol接口,给自己的一个实现类即可。 -这行代码就是Dubbo里大量使用的,对很多组件都保留一个接口和多个实现,然后在系统运行的时候动态根据配置去找到对应实现类。 -若你没配置,那就走默认实现。 -# 实例 -![](https://img-blog.csdnimg.cn/2020122014531547.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) +这行代码就是Dubbo里大量使用的,就是对很多组件,都是保留一个接口和多个实现,然后在系统运行的时候动态根据配置去找到对应的实现类。如果你没配置,那就走默认的实现。 + +### 2.2.1 实例 +![](https://img-blog.csdnimg.cn/2020122014531547.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + 在Dubbo自己的jar里 在`/META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol`文件中: -![](https://img-blog.csdnimg.cn/20201220145724211.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20201220150004508.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20201220150358794.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20201220145724211.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20201220150004508.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20201220150358794.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) 即可看到Dubbo的SPI机制默认流程,就是Protocol接口 -- **@SPI("dubbo")** +- @SPI("dubbo") 通过SPI机制提供实现类,实现类是通过将`dubbo`作为默认key去配置文件里找到的,配置文件名称为接口全限定名,通过`dubbo`作为key可以找到默认的实现类`org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol` > Dubbo的默认网络通信协议,就是dubbo协议,用的DubboProtocol > 在 Java 的 SPI 配置文件里每一行只有一个实现类的全限定名,在 Dubbo的 SPI配置文件中是 key=value 的形式,我们只需要对应的 key 就能加载对应的实现。 -# 源码 -```java + +### 实现源码 + +```csharp /** * 返回指定名字的扩展。如果指定名字的扩展不存在,则抛异常 {@link IllegalStateException}. */ @@ -91,7 +123,7 @@ private T createExtension(String name) { 比如这个Protocol接口搞了俩`@Adaptive`注解了方法,在运行时会针对Protocol生成代理类,该代理类的那俩方法中会有代理代码,代理代码会在运行时动态根据url中的protocol来获取key(默认是dubbo),也可以自己指定,如果指定了别的key,那么就会获取别的实现类的实例。通过这个url中的参数不同,就可以控制动态使用不同的组件实现类 -# 扩展Dubbo组件 +# 3 扩展Dubbo组件 自己写个工程,可以打成jar包的那种哦 - 里面的`src/main/resources`目录下 - 搞一个`META-INF/services` @@ -106,5 +138,10 @@ private T createExtension(String name) { 这个时候provider启动的时候,就会加载到我们jar包里的`my=com.javaedge.MyProtocol`这行配置,接着会根据你的配置使用你定义好的MyProtocol了,这个就是简单说明一下,你通过上述方式,可以替换掉大量的dubbo内部的组件,就是扔个你自己的jar包,然后配置一下即可~ - Dubbo的SPI原理图 ![](https://img-blog.csdnimg.cn/20190709133144886.png) -Dubbo中提供了大量的类似上面的扩展点。 -你要扩展一个东西,只需自己写个jar,让你的consumer或者是provider工程,依赖它,在你的jar里指定目录下配置好接口名称对应的文件,里面通过`key=实现类`然后对对应的组件,用类似``用你的哪个key对应的实现类来实现某个接口,你可以自己去扩展dubbo的各种功能,提供你自己的实现! \ No newline at end of file + +Dubbo中提供了大量的类似上面的扩展点. +你要扩展一个东西,只需自己写个jar,让你的consumer或者是provider工程,依赖它,在你的jar里指定目录下配置好接口名称对应的文件,里面通过`key=实现类`然后对对应的组件,用类似``用你的哪个key对应的实现类来实现某个接口,你可以自己去扩展dubbo的各种功能,提供你自己的实现! + +参考 +- 《Java工程师面试突击第1季》 +- https://dubbo.apache.org/zh-cn/docs/source_code_guide \ No newline at end of file diff --git "a/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Dubbo/Dubbo\346\224\257\346\214\201\347\232\204\351\200\232\344\277\241\343\200\201\345\272\217\345\210\227\345\214\226\345\215\217\350\256\256\350\257\246\350\247\243.md" "b/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Dubbo/Dubbo\346\224\257\346\214\201\347\232\204\351\200\232\344\277\241\343\200\201\345\272\217\345\210\227\345\214\226\345\215\217\350\256\256\350\257\246\350\247\243.md" index acfcef624e..92dcc7a58a 100644 --- "a/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Dubbo/Dubbo\346\224\257\346\214\201\347\232\204\351\200\232\344\277\241\343\200\201\345\272\217\345\210\227\345\214\226\345\215\217\350\256\256\350\257\246\350\247\243.md" +++ "b/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Dubbo/Dubbo\346\224\257\346\214\201\347\232\204\351\200\232\344\277\241\343\200\201\345\272\217\345\210\227\345\214\226\345\215\217\350\256\256\350\257\246\350\247\243.md" @@ -1,5 +1,5 @@ # dubbo支持不同的通信协议 -![](https://img-blog.csdnimg.cn/20190516115342103.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20190516115342103.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) ## dubbo协议 diff --git "a/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/RESTful\346\236\266\346\236\204.md" "b/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/RESTful\346\236\266\346\236\204.md" deleted file mode 100644 index 441aeb87ac..0000000000 --- "a/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/RESTful\346\236\266\346\236\204.md" +++ /dev/null @@ -1,151 +0,0 @@ -# 1 导读 -## 1.1 来源 -REST这个词,是Roy Thomas Fielding在他2000年的博士论文中提出的。 - -他参与设计了HTTP协议,也是Apache Web Server项目(可惜现在已经是 Nginx 的天下)的co-founder。 -论文地址:[Architectural Styles and the Design of Network-based Software Architectures](http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm) -REST章节:[Fielding Dissertation: CHAPTER 5: Representational State Transfer (REST)](http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) - -## 1.2 名称 -Fielding将他对互联网软件的架构原则,定名为REST ,即Representational State Transfer的缩写,资源在网络中以某种表现形式进行状态转移。 - -如果一个架构符合REST原则,就称它为RESTful架构。 - -Resource `Re`presentational `S`tate `T`ransfer,通过URI+动作来操作一个资源。 -## 1.3 拆文解字 -### Resource -资源,即数据(网络的核心)。将网络上的信息实体看作是资源,比如可以是: -- 文本 -- 图片 -- 服务 -- 音频 -... - - -#### 标识 -资源用URI统一标识,URI只使用名词来指定资源,原则上不使用动词,因为它们是资源的标识。 -最佳实践: -URL root: -```bash -[https://example.org/api/v1/](https://example.org/api/v1/) -[https://api.example.com/v1/](https://api.example.com/v1/) -``` -API versioning: -可以放在URL里,也可以用HTTP的header: -```bash -/api/v1/ -``` - -URI使用名词而非动词,推荐复数: -BAD -* /getProducts -* /listOrders -* /retrieveClientByOrder?orderId=1 - -GOOD -* GET /products -return the list of all products -* POST /products -add a product to the collection -* GET /products/4 -retrieve product -* PATCH/PUT /products/4 -update product - -### Representational 表现层 -Representational:某种表现形式,比如用JSON,XML,JPEG。 - -- 文本 -txt、html、 xml、json、二进制 -- 图片 -jpg、png -- http协议的 content-type 和 accept - -Case: -book是一个资源,获取不同的格式 - -### State Transfer -State Transfer:状态转化。通过HTTP动词实现,用以操作这些资源。 - -- 幂等性 -每次HTTP请求相同的参数,相同的URI,产生的结果是相同的 -- GET-获取资源 -http://www.book.com/book/001 -- POST-创建资源,不具有幂等性 -http://www.book.com/book/ -- PUT-创建(更新)资源 -http://www.book.com/book/001 -- DELETE-删除资源 -http://www.book.com/book/001 - -REST描述的是在网络中client和server的一种交互形式。REST本身不实用,实用的是如何设计 RESTful API(REST风格的网络接口)。 - -Server提供的RESTful API中,URL中只使用名词来指定资源,原则上不使用动词。 - -“资源”是REST架构或者说整个网络处理的核心。比如: - -```bash -[http://api/v1/newsfeed](http://api.qc.com/v1/newsfeed) -获取某人的新鲜事 -[http://api/v1/friends](http://api.qc.com/v1/friends) -获取某人的好友列表 -[http://api/v1/profile](http://api.qc.com/v1/profile) -获取某人的详细信息 -``` - -用HTTP协议里的动词来实现资源的添加,修改,删除等操作,即通过HTTP动词实现资源的状态扭转: - -```bash -DELETE [http://api.qc.com/v1/](http://api.qc.com/v1/friends)friends -删除某人的好友 (在http parameter指定好友id) -POST [http://api.qc.com/v1/](http://api.qc.com/v1/friends)friends -添加好友 -UPDATE [http://api.qc.com/v1/profile](http://api.qc.com/v1/profile) -更新个人资料 -``` -禁止使用: - -```bash -GET [http://api.qc.com/v1/deleteFriend](http://api.qc.com/v1/deleteFriend) -``` - -Server和Client之间传递某资源的一个表现形式,比如用JSON,XML传输文本,或者用JPG,WebP传输图片等。当然还可以压缩HTTP传输时的数据(on-wire data compression)。 - -用 HTTP Status Code传递Server的状态信息。比如最常用的 200 表示成功,500 表示Server内部错误等。 - -Web端不再用之前典型的JSP架构,而是改为前段渲染和附带处理简单的商务逻辑(比如AngularJS或者BackBone的一些样例)。Web端和Server只使用上述定义的API来传递数据和改变数据状态。格式一般是JSON。iOS和Android同理可得。由此可见,Web,iOS,Android和第三方开发者变为平等的角色通过一套API来共同消费Server提供的服务。 - -# 为什么要用RESTful架构 -以前网页是前端后端融在一起的,比如JSP等。在桌面时代问题不大,但近年移动互联网的发展,各种类型的Client层出不穷,RESTful可以通过一套统一的接口为 Web,iOS和Android提供服务。另外对于广大平台来说,比如Facebook platform,微博开放平台,微信公共平台等,它们不需要有显式的前端,只需要一套提供服务的接口,于是RESTful更是它们最好的选择。 - -RESTful架构下: -![](https://img-blog.csdnimg.cn/img_convert/c92c2a8400cac46ca1693e0cdbaedb2e.png) -### Server API的RESTful设计原则 -保证 HEAD 和 GET 方法是安全的,不会对资源状态有所改变(污染)。比如严格杜绝如下情况: - -```bash -GET /deleteProduct?id=1 -``` - -资源的地址推荐用嵌套结构。比如: - -```bash -GET /friends/10375923/profile -UPDATE /profile/primaryAddress/city -``` - -警惕返回结果的大小。如果过大,及时进行分页(pagination)或者加入限制(limit)。HTTP协议支持分页(Pagination)操作,在Header中使用 Link 即可。 - -使用正确的HTTP Status Code表示访问状态:[HTTP/1.1: Status Code Definitions](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html) - -在返回结果用明确易懂的文本(注意返回的错误是要给人看的,避免用 1001 这种错误信息),而且适当加入注释。 - -关于安全: -自己的接口就用https,加上一个key做一次hash放在最后即可。考虑到国情,HTTPS在无线网络里不稳定,可以使用应用层的加密手段把整个HTTP的payload加密。 - -RESTful 是无状态的。 - -> 参考 -> - [Getting Started · Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) -> - [授权机制说明](http://open.weibo.com/wiki/%E6%8E%88%E6%9D%83%E6%9C%BA%E5%88%B6%E8%AF%B4%E6%98%8E) -> https://www.zhihu.com/question/28557115/answer/48094438 \ No newline at end of file diff --git "a/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Spring Cloud Netflix(old)/Hystrix\347\272\277\347\250\213\346\261\240\346\234\272\345\210\266\347\232\204\350\265\204\346\272\220\351\232\224\347\246\273\345\234\250\344\270\232\345\212\241\344\270\255\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Spring Cloud Netflix(old)/Hystrix\347\272\277\347\250\213\346\261\240\346\234\272\345\210\266\347\232\204\350\265\204\346\272\220\351\232\224\347\246\273\345\234\250\344\270\232\345\212\241\344\270\255\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" deleted file mode 100644 index 4b2f61221d..0000000000 --- "a/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Spring Cloud Netflix(old)/Hystrix\347\272\277\347\250\213\346\261\240\346\234\272\345\210\266\347\232\204\350\265\204\346\272\220\351\232\224\347\246\273\345\234\250\344\270\232\345\212\241\344\270\255\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" +++ /dev/null @@ -1,139 +0,0 @@ -# 1 pom.xml -```xml - - com.netflix.hystrix - hystrix-core - 1.5.12 - -``` -# 2 将商品服务接口调用的逻辑进行封装 -hystrix资源隔离,其实是提供了一个抽象,叫做command。若把对某个依赖服务的所有调用请求,全部隔离在同一份资源池内。 - -- 资源隔离 -对这个依赖服务的所有调用请求,全部走这个资源池内的资源,不会去用其他的资源。 - -hystrix最基本的资源隔离的技术 --- 线程池隔离技术 - -对某个依赖服务,商品服务所有的调用请求,全部隔离到一个线程池内,对商品服务的每次调用请求都封装在一个command。 - -每个command(服务调用请求)都是使用线程池内的一个线程去执行。 -即使商品服务接口故障了,最多只有10个线程会hang死在调用商品服务接口的路上。缓存服务的tomcat内其他的线程还是可以用来调用其他的服务,做其他的事情 -```java -public class CommandHelloWorld extends HystrixCommand { - - private final String name; - - public CommandHelloWorld(String name) { - super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")); - this.name = name; - } - - @Override - protected String run() { - return "Hello " + name + "!"; - } - -} -``` -不让超出这个量的请求去执行了,保护说,不要因为某一个依赖服务的故障,导致耗尽了缓存服务中的所有的线程资源去执行。 - -# 3 开发一个支持批量商品变更的接口 -- HystrixCommand -获取一条数据 -- HystrixObservableCommand -获取多条数据 - -```java -public class ObservableCommandHelloWorld extends HystrixObservableCommand { - - private final String name; - - public ObservableCommandHelloWorld(String name) { - super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")); - this.name = name; - } - - @Override - protected Observable construct() { - return Observable.create(new Observable.OnSubscribe() { - @Override - public void call(Subscriber observer) { - try { - if (!observer.isUnsubscribed()) { - observer.onNext("Hello " + name + "!"); - observer.onNext("Hi " + name + "!"); - observer.onCompleted(); - } - } catch (Exception e) { - observer.onError(e); - } - } - } ).subscribeOn(Schedulers.io()); - } -} -``` - -# 4 command的调用方式 -## 4.1 同步 -```java -new CommandHelloWorld("World").execute(), -new ObservableCommandHelloWorld("World").toBlocking().toFuture().get() -``` -如果你认为observable command只会返回一条数据,那么可以调用上面的模式,去同步执行,返回一条数据 - -## 4.2 异步 -```java -new CommandHelloWorld("World").queue(), -new ObservableCommandHelloWorld("World").toBlocking().toFuture() -``` -对command调用queue(),仅仅将command放入线程池的一个等待队列,就立即返回,拿到一个Future对象,后面可以做一些其他的事情,然后过一段时间对future调用get()方法获取数据 -```java -// observe():hot,已经执行过了 -// toObservable(): cold,还没执行过 - -Observable fWorld = new CommandHelloWorld("World").observe(); - -assertEquals("Hello World!", fWorld.toBlocking().single()); - -fWorld.subscribe(new Observer() { - - @Override - public void onCompleted() { - - } - - @Override - public void onError(Throwable e) { - e.printStackTrace(); - } - - @Override - public void onNext(String v) { - System.out.println("onNext: " + v); - } - -}); - -Observable fWorld = new ObservableCommandHelloWorld("World").toObservable(); - -assertEquals("Hello World!", fWorld.toBlocking().single()); - -fWorld.subscribe(new Observer() { - - @Override - public void onCompleted() { - - } - - @Override - public void onError(Throwable e) { - e.printStackTrace(); - } - - @Override - public void onNext(String v) { - System.out.println("onNext: " + v); - } - -}); -``` \ No newline at end of file diff --git "a/\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/1.md" "b/\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/1.md" similarity index 100% rename from "\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/1.md" rename to "\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/1.md" diff --git "a/\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/13_\345\274\200\345\217\221\345\223\201\347\211\214\345\220\215\347\247\260\350\216\267\345\217\226\346\216\245\345\217\243\347\232\204\345\237\272\344\272\216\346\234\254\345\234\260\347\274\223\345\255\230\347\232\204fallback\351\231\215\347\272\247\346\234\272\345\210\266.md" "b/\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/13_\345\274\200\345\217\221\345\223\201\347\211\214\345\220\215\347\247\260\350\216\267\345\217\226\346\216\245\345\217\243\347\232\204\345\237\272\344\272\216\346\234\254\345\234\260\347\274\223\345\255\230\347\232\204fallback\351\231\215\347\272\247\346\234\272\345\210\266.md" similarity index 100% rename from "\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/13_\345\274\200\345\217\221\345\223\201\347\211\214\345\220\215\347\247\260\350\216\267\345\217\226\346\216\245\345\217\243\347\232\204\345\237\272\344\272\216\346\234\254\345\234\260\347\274\223\345\255\230\347\232\204fallback\351\231\215\347\272\247\346\234\272\345\210\266.md" rename to "\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/13_\345\274\200\345\217\221\345\223\201\347\211\214\345\220\215\347\247\260\350\216\267\345\217\226\346\216\245\345\217\243\347\232\204\345\237\272\344\272\216\346\234\254\345\234\260\347\274\223\345\255\230\347\232\204fallback\351\231\215\347\272\247\346\234\272\345\210\266.md" diff --git "a/\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\347\252\201\347\240\264Java\351\235\242\350\257\225-hystrix\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237\345\217\257\347\224\250\346\200\247\345\217\212\350\256\276\350\256\241\345\216\237\345\210\231.md" "b/\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\347\252\201\347\240\264Java\351\235\242\350\257\225-hystrix\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237\345\217\257\347\224\250\346\200\247\345\217\212\350\256\276\350\256\241\345\216\237\345\210\231.md" similarity index 100% rename from "\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\347\252\201\347\240\264Java\351\235\242\350\257\225-hystrix\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237\345\217\257\347\224\250\346\200\247\345\217\212\350\256\276\350\256\241\345\216\237\345\210\231.md" rename to "\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\347\252\201\347\240\264Java\351\235\242\350\257\225-hystrix\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237\345\217\257\347\224\250\346\200\247\345\217\212\350\256\276\350\256\241\345\216\237\345\210\231.md" diff --git "a/\346\236\266\346\236\204/\350\275\257\344\273\266\346\236\266\346\236\204\345\210\206\345\261\202\346\226\271\346\263\225\350\256\272.md" "b/\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\350\275\257\344\273\266\346\236\266\346\236\204\345\210\206\345\261\202\346\226\271\346\263\225\350\256\272.md" similarity index 100% rename from "\346\236\266\346\236\204/\350\275\257\344\273\266\346\236\266\346\236\204\345\210\206\345\261\202\346\226\271\346\263\225\350\256\272.md" rename to "\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\350\275\257\344\273\266\346\236\266\346\236\204\345\210\206\345\261\202\346\226\271\346\263\225\350\256\272.md" diff --git "a/\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221/\351\253\230\345\217\257\346\211\251\345\261\225\346\200\247\347\263\273\347\273\237\347\232\204\350\256\276\350\256\241.md" "b/\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\346\211\251\345\261\225\346\200\247\347\263\273\347\273\237\347\232\204\350\256\276\350\256\241.md" similarity index 100% rename from "\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221/\351\253\230\345\217\257\346\211\251\345\261\225\346\200\247\347\263\273\347\273\237\347\232\204\350\256\276\350\256\241.md" rename to "\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\346\211\251\345\261\225\346\200\247\347\263\273\347\273\237\347\232\204\350\256\276\350\256\241.md" diff --git "a/\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(10)-Hystrix\347\232\204\347\272\277\347\250\213\346\261\240+\346\234\215\345\212\241+\346\216\245\345\217\243\345\210\222\345\210\206\344\273\245\345\217\212\350\265\204\346\272\220\346\261\240\347\232\204\345\256\271\351\207\217\345\244\247\345\260\217\346\216\247\345\210\266.md" "b/\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(10)-Hystrix\347\232\204\347\272\277\347\250\213\346\261\240+\346\234\215\345\212\241+\346\216\245\345\217\243\345\210\222\345\210\206\344\273\245\345\217\212\350\265\204\346\272\220\346\261\240\347\232\204\345\256\271\351\207\217\345\244\247\345\260\217\346\216\247\345\210\266.md" similarity index 100% rename from "\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(10)-Hystrix\347\232\204\347\272\277\347\250\213\346\261\240+\346\234\215\345\212\241+\346\216\245\345\217\243\345\210\222\345\210\206\344\273\245\345\217\212\350\265\204\346\272\220\346\261\240\347\232\204\345\256\271\351\207\217\345\244\247\345\260\217\346\216\247\345\210\266.md" rename to "\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(10)-Hystrix\347\232\204\347\272\277\347\250\213\346\261\240+\346\234\215\345\212\241+\346\216\245\345\217\243\345\210\222\345\210\206\344\273\245\345\217\212\350\265\204\346\272\220\346\261\240\347\232\204\345\256\271\351\207\217\345\244\247\345\260\217\346\216\247\345\210\266.md" diff --git "a/\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(12) - \345\237\272\344\272\216request cache\350\257\267\346\261\202\347\274\223\345\255\230\346\212\200\346\234\257\344\274\230\345\214\226\346\211\271\351\207\217\345\225\206\345\223\201\346\225\260\346\215\256\346\237\245\350\257\242\346\216\245\345\217\243.md" "b/\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(12) - \345\237\272\344\272\216request cache\350\257\267\346\261\202\347\274\223\345\255\230\346\212\200\346\234\257\344\274\230\345\214\226\346\211\271\351\207\217\345\225\206\345\223\201\346\225\260\346\215\256\346\237\245\350\257\242\346\216\245\345\217\243.md" similarity index 100% rename from "\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(12) - \345\237\272\344\272\216request cache\350\257\267\346\261\202\347\274\223\345\255\230\346\212\200\346\234\257\344\274\230\345\214\226\346\211\271\351\207\217\345\225\206\345\223\201\346\225\260\346\215\256\346\237\245\350\257\242\346\216\245\345\217\243.md" rename to "\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(12) - \345\237\272\344\272\216request cache\350\257\267\346\261\202\347\274\223\345\255\230\346\212\200\346\234\257\344\274\230\345\214\226\346\211\271\351\207\217\345\225\206\345\223\201\346\225\260\346\215\256\346\237\245\350\257\242\346\216\245\345\217\243.md" diff --git "a/\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(16) - \345\237\272\344\272\216timeout\346\234\272\345\210\266\346\235\245\344\270\272\345\225\206\345\223\201\346\234\215\345\212\241\346\216\245\345\217\243\347\232\204\350\260\203\347\224\250\350\266\205\346\227\266\346\217\220\344\276\233\345\256\211\345\205\250\344\277\235\346\212\244.md" "b/\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(16) - \345\237\272\344\272\216timeout\346\234\272\345\210\266\346\235\245\344\270\272\345\225\206\345\223\201\346\234\215\345\212\241\346\216\245\345\217\243\347\232\204\350\260\203\347\224\250\350\266\205\346\227\266\346\217\220\344\276\233\345\256\211\345\205\250\344\277\235\346\212\244.md" similarity index 100% rename from "\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(16) - \345\237\272\344\272\216timeout\346\234\272\345\210\266\346\235\245\344\270\272\345\225\206\345\223\201\346\234\215\345\212\241\346\216\245\345\217\243\347\232\204\350\260\203\347\224\250\350\266\205\346\227\266\346\217\220\344\276\233\345\256\211\345\205\250\344\277\235\346\212\244.md" rename to "\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(16) - \345\237\272\344\272\216timeout\346\234\272\345\210\266\346\235\245\344\270\272\345\225\206\345\223\201\346\234\215\345\212\241\346\216\245\345\217\243\347\232\204\350\260\203\347\224\250\350\266\205\346\227\266\346\217\220\344\276\233\345\256\211\345\205\250\344\277\235\346\212\244.md" diff --git "a/\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(17) - \345\237\272\344\272\216Hystrix\347\232\204\351\253\230\345\217\257\347\224\250\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241\347\232\204\346\200\273\347\273\223.md" "b/\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(17) - \345\237\272\344\272\216Hystrix\347\232\204\351\253\230\345\217\257\347\224\250\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241\347\232\204\346\200\273\347\273\223.md" similarity index 100% rename from "\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(17) - \345\237\272\344\272\216Hystrix\347\232\204\351\253\230\345\217\257\347\224\250\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241\347\232\204\346\200\273\347\273\223.md" rename to "\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(17) - \345\237\272\344\272\216Hystrix\347\232\204\351\253\230\345\217\257\347\224\250\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241\347\232\204\346\200\273\347\273\223.md" diff --git "a/\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(9) - \345\237\272\344\272\216Hystrix\347\232\204\344\277\241\345\217\267\351\207\217\346\212\200\346\234\257\345\257\271\345\234\260\347\220\206\344\275\215\347\275\256\350\216\267\345\217\226\351\200\273\350\276\221\350\277\233\350\241\214\350\265\204\346\272\220\351\232\224\347\246\273\344\270\216\351\231\220\346\265\201.md" "b/\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(9) - \345\237\272\344\272\216Hystrix\347\232\204\344\277\241\345\217\267\351\207\217\346\212\200\346\234\257\345\257\271\345\234\260\347\220\206\344\275\215\347\275\256\350\216\267\345\217\226\351\200\273\350\276\221\350\277\233\350\241\214\350\265\204\346\272\220\351\232\224\347\246\273\344\270\216\351\231\220\346\265\201.md" similarity index 100% rename from "\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(9) - \345\237\272\344\272\216Hystrix\347\232\204\344\277\241\345\217\267\351\207\217\346\212\200\346\234\257\345\257\271\345\234\260\347\220\206\344\275\215\347\275\256\350\216\267\345\217\226\351\200\273\350\276\221\350\277\233\350\241\214\350\265\204\346\272\220\351\232\224\347\246\273\344\270\216\351\231\220\346\265\201.md" rename to "\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\347\224\250\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241(9) - \345\237\272\344\272\216Hystrix\347\232\204\344\277\241\345\217\267\351\207\217\346\212\200\346\234\257\345\257\271\345\234\260\347\220\206\344\275\215\347\275\256\350\216\267\345\217\226\351\200\273\350\276\221\350\277\233\350\241\214\350\265\204\346\272\220\351\232\224\347\246\273\344\270\216\351\231\220\346\265\201.md" diff --git "a/\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\346\236\266\346\236\204\350\256\276\350\256\241(2) - Hystrix.md" "b/\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\347\224\250\346\236\266\346\236\204\350\256\276\350\256\241(2) - hystrix\350\246\201\350\247\243\345\206\263\347\232\204\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237\345\217\257\347\224\250\346\200\247\351\227\256\351\242\230\344\273\245\345\217\212\345\205\266\350\256\276\350\256\241\345\216\237\345\210\231.md" similarity index 64% rename from "\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\346\236\266\346\236\204\350\256\276\350\256\241(2) - Hystrix.md" rename to "\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\347\224\250\346\236\266\346\236\204\350\256\276\350\256\241(2) - hystrix\350\246\201\350\247\243\345\206\263\347\232\204\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237\345\217\257\347\224\250\346\200\247\351\227\256\351\242\230\344\273\245\345\217\212\345\205\266\350\256\276\350\256\241\345\216\237\345\210\231.md" index 9b76e32fe0..6222b4b47e 100644 --- "a/\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\346\236\266\346\236\204\350\256\276\350\256\241(2) - Hystrix.md" +++ "b/\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\347\224\250\346\236\266\346\236\204\350\256\276\350\256\241(2) - hystrix\350\246\201\350\247\243\345\206\263\347\232\204\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237\345\217\257\347\224\250\346\200\247\351\227\256\351\242\230\344\273\245\345\217\212\345\205\266\350\256\276\350\256\241\345\216\237\345\210\231.md" @@ -1,4 +1,17 @@ -# Hystrix是什么 +# 1 导读 + +高可用性这个topic,然后咱们会用几讲的时间来讲解一下如何用hystrix,来构建高可用的服务的架构 + +咱们会用一个真实的项目背景,作为业务场景,来带出来在这个特定的业务场景下,可能会产生哪些各种各样的可用性的一些问题 + +针对这些问题,我们用hystrix的解决方案和原理是什么 + +带着大家,纯手工将所有的服务的高可用架构的代码,全部纯手工自己敲出来 + +形成高可用服务架构的项目实战的一个教程 + +# 2 Hystrix是什么 + 在分布式系统中,每个服务都可能会调用很多其他服务,被调用的那些服务就是依赖服务,有的时候某些依赖服务出现故障也是很常见的。 Hystrix可以让我们在分布式系统中对服务间的调用进行控制,加入一些调用延迟或者依赖故障的容错机制。 @@ -6,14 +19,26 @@ Hystrix可以让我们在分布式系统中对服务间的调用进行控制, Hystrix通过将依赖服务进行**资源隔离**,进而避免某个依赖服务出现故障的时候,在整个系统所有的依赖服务调用中蔓延,同时Hystrix还提供故障时的fallback降级机制 总而言之,Hystrix通过这些方法帮助我们提升分布式系统的可用性和稳定性 -# Hystrix的历史 -hystrix,一种高可用保障的框架,Netflix API团队从2011年开始做一些提升系统可用性和稳定性的工作,Hystrix就是从那时候开始发展出来的。 + +- 什么是分布式系统以及其中的故障和hystrix +![](https://ask.qcloudimg.com/http-save/1752328/d1razflqky.png) + +# 3 Hystrix的历史 + +hystrix,一种高可用保障的框架,类似于spring(ioc,mvc),mybatis,activiti,lucene,框架,预先封装好的为了解决某个特定领域的特定问题的一套代码库 + +框架,用了框架之后,来解决这个领域的特定的问题,就可以大大减少我们的工作量,提升我们的工作质量和工作效率,框架,hystrix,就是高可用性保障的一个框架 + +Netflix(可以认为是国外的优酷或者爱奇艺之类的视频网站),API团队从2011年开始做一些提升系统可用性和稳定性的工作,Hystrix就是从那时候开始发展出来的。 在2012年的时候,Hystrix就变得比较成熟和稳定了,Netflix中,除了API团队以外,很多其他的团队都开始使用Hystrix。 +时至今日,Netflix中每天都有数十亿次的服务间调用,通过Hystrix框架在进行,而Hystrix也帮助Netflix网站提升了整体的可用性和稳定性 + 2018 年 11 月,Hystrix 在其 [Github 主页](https://github.com/Netflix/Hystrix/blob/master/README.md#hystrix-status)宣布,不再开放新功能,推荐开发者使用其他仍然活跃的开源项目。维护模式的转变绝不意味着 Hystrix 不再有价值。相反,Hystrix 激发了很多伟大的想法和项目,其思想仍值得我们深入学习! -# Hystrix的设计原则 +# 4 Hystrix的设计原则 + - 对依赖服务调用时出现的调用延迟和调用失败进行控制和容错保护 - 在复杂的分布式系统中,阻止某一个依赖服务的故障在整个系统中蔓延,服务A->服务B->服务C,服务C故障了,服务B也故障了,服务A故障了,整套分布式系统全部故障,整体宕机 - 提供fail-fast(快速失败)和快速恢复的支持 @@ -21,18 +46,27 @@ hystrix,一种高可用保障的框架,Netflix API团队从2011年开始做 - 支持近实时的监控、报警以及运维操作5 Hystrix要解决的问题在复杂的分布式系统架构中,每个服务都有很多的依赖服务,而每个依赖服务都可能会故障 如果服务没有和自己的依赖服务进行隔离,那么可能某一个依赖服务的故障就会拖垮当前这个服务 -# 举例 +举例来说 + 某个服务有30个依赖服务,每个依赖服务的可用性非常高,已经达到了99.99%的高可用性 那么该服务的可用性就是99.99%的30次方,也就是99.7%的可用性 -99.7%的可用性就意味着3%的请求可能会失败,因为3%的时间内系统可能出现了故障不可用了。 +99.7%的可用性就意味着3%的请求可能会失败,因为3%的时间内系统可能出现了故障不可用了 + +对于1亿次访问来说,3%的请求失败,也就意味着300万次请求会失败,也意味着每个月有2个小时的时间系统是不可用的 + +在真实生产环境中,可能更加糟糕 + +上面也就是说,即使你每个依赖服务都是99.99%高可用性,但是一旦你有几十个依赖服务,还是会导致你每个月都有几个小时是不可用的 + +画图分析说,当某一个依赖服务出现了调用延迟或者调用失败时,为什么会拖垮当前这个服务?以及在分布式系统中,故障是如何快速蔓延的? -对于1亿次访问来说,3%的请求失败,也就意味着300万次请求会失败,也意味着每个月有2个小时的时间系统是不可用的。在真实生产环境中,可能更加糟糕。 +- 依赖服务的故障导致服务被拖垮以及故障的蔓延示意图 +![](https://ask.qcloudimg.com/http-save/1752328/twu9s9m6xx.png) + +# 6 Hystrix的更加细节的设计原则 -上面也就是说,即使你每个依赖服务都是99.99%高可用性,但是一旦你有几十个依赖服务,还是会导致你每个月都有几个小时是不可用的。 -![](https://img-blog.csdnimg.cn/83a446c51c3e4b27beb3e42560458ca0.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -# Hystrix的更加细节的设计原则 - 阻止任何一个依赖服务耗尽所有的资源,比如tomcat中的所有线程资源 - 避免请求排队和积压,采用限流和fail fast来控制故障 - 提供fallback降级机制来应对故障 @@ -43,7 +77,8 @@ hystrix,一种高可用保障的框架,Netflix API团队从2011年开始做 调用这个依赖服务的时候,client调用包有bug,阻塞,等等,依赖服务的各种各样的调用的故障,都可以处理 -# Hystrix如何实现它的目标 +# 7 Hystrix如何实现它的目标 + - 通过HystrixCommand或者HystrixObservableCommand来封装对外部依赖的访问请求,这个访问请求一般会运行在独立的线程中,资源隔离 - 对于超出我们设定阈值的服务调用,直接进行超时,不允许其耗费过长时间阻塞住。这个超时时间默认是99.5%的访问时间,但是一般我们可以自己设置一下 - 为每一个依赖服务维护一个独立的线程池,或者是semaphore,当线程池已满时,直接拒绝对这个服务的调用 @@ -54,5 +89,15 @@ hystrix,一种高可用保障的框架,Netflix API团队从2011年开始做 画图分析,对依赖进行资源隔离后,如何避免依赖服务调用延迟或失败导致当前服务的故障 -- 资源隔离如何保护依赖服务的故障不要拖垮整个系统 -![](https://img-blog.csdnimg.cn/b7b99687fdbb4352974f7006f5f5c989.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) +- 资源隔离如何保护依赖服务的故障不要拖垮整个系统![](https://ask.qcloudimg.com/http-save/1752328/0tx22cj2bs.png) + +# 参考 + +- 《Java工程师面试突击第1季-中华石杉老师》 + +# X 交流学习 +![](https://img-blog.csdnimg.cn/20190504005601174.jpg) +## [Java交流群](https://jq.qq.com/?_wv=1027&k=5UB4P1T) +## [博客](https://blog.csdn.net/qq_33589510) + +## [Github](https://github.com/Wasabi1234) diff --git "a/\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\347\232\204\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241-\350\265\204\346\272\220\351\232\224\347\246\273\343\200\201\351\231\220\346\265\201\343\200\201\347\206\224\346\226\255\343\200\201\351\231\215\347\272\247\343\200\201\347\233\221\346\216\247.md" "b/\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\347\224\250\347\232\204\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241-\350\265\204\346\272\220\351\232\224\347\246\273\343\200\201\351\231\220\346\265\201\343\200\201\347\206\224\346\226\255\343\200\201\351\231\215\347\272\247\343\200\201\347\233\221\346\216\247.md" similarity index 100% rename from "\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\347\232\204\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241-\350\265\204\346\272\220\351\232\224\347\246\273\343\200\201\351\231\220\346\265\201\343\200\201\347\206\224\346\226\255\343\200\201\351\231\215\347\272\247\343\200\201\347\233\221\346\216\247.md" rename to "\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\217\257\347\224\250\347\232\204\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241-\350\265\204\346\272\220\351\232\224\347\246\273\343\200\201\351\231\220\346\265\201\343\200\201\347\206\224\346\226\255\343\200\201\351\231\215\347\272\247\343\200\201\347\233\221\346\216\247.md" diff --git "a/\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241\344\271\213\351\201\223\357\274\210\344\270\200\357\274\211- \346\226\271\346\263\225\350\256\272.md" "b/\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241\344\271\213\351\201\223\357\274\210\344\270\200\357\274\211- \346\226\271\346\263\225\350\256\272.md" similarity index 100% rename from "\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241\344\271\213\351\201\223\357\274\210\344\270\200\357\274\211- \346\226\271\346\263\225\350\256\272.md" rename to "\346\236\266\346\236\204/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241\344\271\213\351\201\223\357\274\210\344\270\200\357\274\211- \346\226\271\346\263\225\350\256\272.md" diff --git "a/\351\207\215\346\236\204/\345\276\256\346\234\215\345\212\241\351\241\271\347\233\256\345\246\202\344\275\225\346\240\241\351\252\214\345\217\202\346\225\260.md" "b/\347\274\226\347\250\213\350\247\204\350\214\203/EffectiveJava/\345\276\256\346\234\215\345\212\241\351\241\271\347\233\256\345\246\202\344\275\225\346\240\241\351\252\214\345\217\202\346\225\260.md" similarity index 100% rename from "\351\207\215\346\236\204/\345\276\256\346\234\215\345\212\241\351\241\271\347\233\256\345\246\202\344\275\225\346\240\241\351\252\214\345\217\202\346\225\260.md" rename to "\347\274\226\347\250\213\350\247\204\350\214\203/EffectiveJava/\345\276\256\346\234\215\345\212\241\351\241\271\347\233\256\345\246\202\344\275\225\346\240\241\351\252\214\345\217\202\346\225\260.md" diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\345\274\202\345\270\270\346\234\272\345\210\266\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/\347\274\226\347\250\213\350\247\204\350\214\203/\347\274\226\347\250\213\346\200\235\346\203\263/Java\345\274\202\345\270\270\346\234\272\345\210\266\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" similarity index 100% rename from "JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/Java\345\274\202\345\270\270\346\234\272\345\210\266\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" rename to "\347\274\226\347\250\213\350\247\204\350\214\203/\347\274\226\347\250\213\346\200\235\346\203\263/Java\345\274\202\345\270\270\346\234\272\345\210\266\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" diff --git "a/\351\207\215\346\236\204/Java\351\200\211\346\211\213\344\270\272\344\275\225\350\246\201\346\214\201\347\273\255\346\200\247\345\255\246\344\271\240.md" "b/\347\274\226\347\250\213\350\247\204\350\214\203/\351\230\277\351\207\214 Java \345\274\200\345\217\221\346\211\213\345\206\214\350\257\246\350\247\243/Java\346\211\213\345\206\214\347\232\204\346\204\217\344\271\211.md" similarity index 95% rename from "\351\207\215\346\236\204/Java\351\200\211\346\211\213\344\270\272\344\275\225\350\246\201\346\214\201\347\273\255\346\200\247\345\255\246\344\271\240.md" rename to "\347\274\226\347\250\213\350\247\204\350\214\203/\351\230\277\351\207\214 Java \345\274\200\345\217\221\346\211\213\345\206\214\350\257\246\350\247\243/Java\346\211\213\345\206\214\347\232\204\346\204\217\344\271\211.md" index c6ae78de13..e6dea56903 100644 --- "a/\351\207\215\346\236\204/Java\351\200\211\346\211\213\344\270\272\344\275\225\350\246\201\346\214\201\347\273\255\346\200\247\345\255\246\344\271\240.md" +++ "b/\347\274\226\347\250\213\350\247\204\350\214\203/\351\230\277\351\207\214 Java \345\274\200\345\217\221\346\211\213\345\206\214\350\257\246\350\247\243/Java\346\211\213\345\206\214\347\232\204\346\204\217\344\271\211.md" @@ -1,5 +1,5 @@ -# 痛点 -- 这辈子读了很多书和理论,但写代码时就忘了 +# 现实的尴尬 +- 这辈子读了很多书和理论,但真正写代码都忘了 - 很多知识点,知其然而不知其所以然,浮于表面,止于面试 - 以为读源码就能提升,殊不知嵌套太多,只是迷失自我 - 不重视业务,开发过程才知道设计和业务需求的差距,延期说来就来 diff --git "a/\351\207\215\346\236\204/\347\250\213\345\272\217\345\274\202\345\270\270\345\244\204\347\220\206\345\256\236\350\267\265.md" "b/\347\274\226\347\250\213\350\247\204\350\214\203/\351\230\277\351\207\214 Java \345\274\200\345\217\221\346\211\213\345\206\214\350\257\246\350\247\243/\347\250\213\345\272\217\345\274\202\345\270\270\345\244\204\347\220\206\345\256\236\350\267\265.md" similarity index 100% rename from "\351\207\215\346\236\204/\347\250\213\345\272\217\345\274\202\345\270\270\345\244\204\347\220\206\345\256\236\350\267\265.md" rename to "\347\274\226\347\250\213\350\247\204\350\214\203/\351\230\277\351\207\214 Java \345\274\200\345\217\221\346\211\213\345\206\214\350\257\246\350\247\243/\347\250\213\345\272\217\345\274\202\345\270\270\345\244\204\347\220\206\345\256\236\350\267\265.md" diff --git "a/\351\207\215\346\236\204/\347\251\272\346\214\207\351\222\210\351\227\256\351\242\230\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/\347\274\226\347\250\213\350\247\204\350\214\203/\351\230\277\351\207\214 Java \345\274\200\345\217\221\346\211\213\345\206\214\350\257\246\350\247\243/\347\251\272\346\214\207\351\222\210\351\227\256\351\242\230\346\234\200\344\275\263\345\256\236\350\267\265.md" similarity index 97% rename from "\351\207\215\346\236\204/\347\251\272\346\214\207\351\222\210\351\227\256\351\242\230\346\234\200\344\275\263\345\256\236\350\267\265.md" rename to "\347\274\226\347\250\213\350\247\204\350\214\203/\351\230\277\351\207\214 Java \345\274\200\345\217\221\346\211\213\345\206\214\350\257\246\350\247\243/\347\251\272\346\214\207\351\222\210\351\227\256\351\242\230\346\234\200\344\275\263\345\256\236\350\267\265.md" index ae0cf56122..6a6c21e973 100644 --- "a/\351\207\215\346\236\204/\347\251\272\346\214\207\351\222\210\351\227\256\351\242\230\346\234\200\344\275\263\345\256\236\350\267\265.md" +++ "b/\347\274\226\347\250\213\350\247\204\350\214\203/\351\230\277\351\207\214 Java \345\274\200\345\217\221\346\211\213\345\206\214\350\257\246\350\247\243/\347\251\272\346\214\207\351\222\210\351\227\256\351\242\230\346\234\200\344\275\263\345\256\236\350\267\265.md" @@ -251,7 +251,11 @@ public void doSomeOperation(Operation operation) { } } ``` -可构造一个 NullXXX 类拓展自某个接口,这样该接口需要为 null 时,直接返回该对象即可: + +《设计模式之禅》(第二版)554 页在拓展篇讲述了 “空对象模式”。 + +可以构造一个 NullXXX 类拓展自某个接口, 这样这个接口需要为 null 时,直接返回该对象即可: + ```java public class NullOperation implements Operation { @@ -450,6 +454,10 @@ public static void doSomething(@NotNull String param) { proccess(param); } ``` + +# 5. 总结 +本节主要讲述空指针的含义,空指针常见的中枪姿势,以及如何避免空指针异常。下一节将为你揭秘 当 switch 遇到空指针,又会发生什么奇妙的事情。 + # 参考 - 《 阿里巴巴Java 开发手册 1.5.0:华山版》 - 《Java Language Specification: Java SE 8 Edition》 diff --git "a/\350\201\214\344\270\232\345\217\221\345\261\225/Java\347\263\273\347\273\237\347\272\277\344\270\212\347\224\237\344\272\247\351\227\256\351\242\230\346\216\222\346\237\245\344\270\200\346\212\212\346\242\255.md" "b/\350\201\214\344\270\232\345\217\221\345\261\225/Java\347\263\273\347\273\237\347\272\277\344\270\212\347\224\237\344\272\247\351\227\256\351\242\230\346\216\222\346\237\245\344\270\200\346\212\212\346\242\255.md" deleted file mode 100644 index 601e23ba5b..0000000000 --- "a/\350\201\214\344\270\232\345\217\221\345\261\225/Java\347\263\273\347\273\237\347\272\277\344\270\212\347\224\237\344\272\247\351\227\256\351\242\230\346\216\222\346\237\245\344\270\200\346\212\212\346\242\255.md" +++ /dev/null @@ -1,178 +0,0 @@ -# 1 环境 -## 1.1 Dev -可以随意使用任何熟悉的工具排查。只要问题能重现,排查就不会太难,最多就是把程序调试到各种框架源码,所以这也是为何面试都会问源码,不求都看过,但要有思路知道如何去看能解决问题。 - -## 1.2 Test -比开发环境少了debug,不过也可使用jvisualvm或Arthas,附加到远程JVM进程。 - -还有测试环境是允许造数据来模拟我们需要的场景的哦,因此这时遇到问题记得主动沟通测试人员造数据让bug更容易复现。 - -## 1.3 Prd -该环境下开发人员的权限最低,所以排查问题时障碍很大: -- 无法使用调试工具从远程附加进程 -- 快速恢复为先,即使在结婚,也得赶紧修复线上问题。而且生产环境流量大、网络权限严格、调用链路复杂,因此更容易出问题,也是出问题最多的环境。 - -# 2 监控 -生产环境出现问题时,因为要尽快恢复应用,就不可能保留完整现场用于排查和测试。因此,是否有充足的信息(日志、监控和快照)可以了解历史、还原bug 场景。 -最常用的就是 ELK 的日志了,注意: -- 确保错误、异常信息可被完整记录到文件日志 -- 确保生产上程序的日志级别是INFO以上 -记录日志要使用合理的日志优先级,DEBUG用于开发调试、INFO用于重要流程信息、WARN用于需要关注的问题、ERROR用于阻断流程的错误 - -生产环境需开发配合运维才能做好完备监控: -## 主机维度 -对CPU、内存、磁盘、网络等资源做监控。如果应用部署在虚拟机或k8s集群,那么除了对物理机做基础资源监控外,同样还要对虚拟机或Pod监控。监控层数取决于应用的部署方案,有一层OS就要做一层监控。 -## 网络维度 -监控专线带宽、交换机基本情况、网络延迟 - -## 所有的中间件和存储都要做好监控 -不仅仅是监控进程对CPU、内存、磁盘IO、网络使用的基本指标,更重要的是监控组件内部的一些重要指标。比如最常用的Prometheus,就提供了大量exporter对接各种中间件和存储系统 -## 应用层面 -需监控JVM进程的类加载、内存、GC、线程等常见指标(比如使用Micrometer来做应用监控),此外还要确保能够收集、保存应用日志、GC日志 - -我们再来看看快照。这里的“快照”是指,应用进程在某一时刻的快照。通常情况下,我们会为生产环境的Java应用设置-XX:+HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath=…这2个JVM参数,用于在出现OOM时保留堆快照。这个课程中,我们也多次使用MAT工具来分析堆快照。 - -# 分析定位问题的最佳实践 -定位问题,首先要定位问题出在哪个层次:Java应用程序自身问题还是外部因素导致。 -- 可以先查看程序是否有异常,异常信息一般比较具体,可以马上定位到大概的问题方向 -- 如果是一些资源消耗型的问题可能不会有异常,我们可以通过指标监控配合显性问题点来定位。 - -一般问题原因可归类如下: -## 程序发布后 Bug -回滚,再慢慢通过版本差异分析根因。 -## 外部因素 -比如主机、中间件或DB问题。 -这种按主机层面问题、中间件或存储(统称组件)的问题分为: -### 主机层 -可使用工具排查: -#### CPU相关 -使用top、vmstat、pidstat、ps -#### 内存相关 -使用free、top、ps、vmstat、cachestat、sar -#### IO相关 -使用lsof、iostat、pidstat、sar、iotop、df、du -#### 网络相关 -使用ifconfig、ip、nslookup、dig、ping、tcpdump、iptables - -#### 组件 -从如下方面排查: -- 组件所在主机是否有问题 -- 组件进程基本情况,观察各种监控指标 -- 组件的日志输出,特别是错误日志 -- 进入组件控制台,使用一些命令查看其运作情况。 - -## 系统资源不够造成系统假死 -> 通常先通过重启和扩容解决问题,之后再分析,最好能留个快照。 - -系统资源不够,一般可能: -### CPU使用高 -若现场还在,具体分析流程: -- 在服务器执行`top -Hp pid` -查看进程中哪个线程CPU使用高 -- 输入大写的P将线程按照 CPU 使用率排序,并把明显占用CPU的线程ID转换为16进制 -- 在jstack命令输出的线程栈中搜索这个线程ID,定位出问题的线程当时的调用栈 - -若无法直接在服务器执行top,可采样定位:间隔固定时间运行一次jstack,采样几次后,对比采样得出哪些线程始终处于运行状态,找出问题线程。 - -若现场没了,可排除法分析。CPU使用高,一般是由下面的因素引起的: -- 突发压力 -可通过应用之前的负载均衡的流量或日志量确认,诸如Nginx等反向代理都会记录URL,可依靠代理的Access Log进行细化定位,也可通过监控观察JVM线程数的情况。压力问题导致CPU使用高的情况下,如果程序的各资源使用没有明显不正常,之后可以通过压测+Profiler(jvisualvm就有这个功能)进一步定位热点方法;如果资源使用不正常,比如产生了几千个线程,就需要考虑调参 - -- GC -可通过JVM监控GC相关指标、GC Log确认。如果确认是GC压力,那么内存使用也很可能会不正常,需要按照内存问题分析流程做进步分析。 -- 死循环或不正常处理流程 -可以结合应用日志分析。一般情况下,应用执行过程中都会产生一些日志,可以重点关注日志量异常部分。 -### 内存泄露或OOM -最简单的就是堆转储后使用MAT分析。堆转储,包含了堆现场全貌和线程栈信息,一般观察支配树图、直方图就可以马上看到占用大量内存的对象,可以快速定位到内存相关问题 -Java进程对内存的使用不仅仅是堆区,还包括线程使用的内存(线程个数*每一个线程的线程栈)和元数据区。每一个内存区都可能产生OOM,可以结合监控观察线程数、已加载类数量等指标分析 -注意看JVM参数的设置是否有明显不合理的,限制了资源。 - -### IO问题 -除非是代码问题引起的资源不释放等问题,否则通常都不是由Java进程内部因素引发的。 - -### 网络 -一般也是由外部因素引起。对于连通性问题,结合异常信息通常比较容易定位;对于性能或瞬断问题,可以先尝试使用ping等工具简单判断,如果不行再使用tcpdump或Wireshark。 - -# 迷茫时的最佳实践 -偶尔可能分析和定位难题,会迷失自我。如果你也这样,可参考如下经验 -## cause or result? -比如业务执行的很慢,而且线程数增多,那就可能是: -- 代码逻辑有问题、依赖的外部服务慢 -使得自己的业务逻辑执行缓慢,在访问量不变情况下,就需要更多线程处理。比如,10 TPS的并发原先一次请求1s即可完成,10个线程可支撑;现在执行完成需要10s,就需100个线程 -- 请求量增大 -使得线程数增多,应用本身CPU不足,上下文切换问题导致处理变慢 - -这时就需要多结合监控指标和各服务的入口流量,分析慢是cause or result。 - -## 探求规律 -如果没头绪,那就试试总结规律吧! -比如 -- 有一堆服务器做负载均衡,出问题时可分析监控和日志看请求是否是均匀分布的,可能问题都集中在某个机器节点上 -- 应用日志一般会记录线程名称,出问题时可分析日志是否集中在某类线程 -- 若发现应用开启大量TCP连接,通过netstat可分析出主要集中连接到哪个服务 - -探求到了规律,就很容易突破了。 - -## 调用拓扑 -比如看到Nginx返回502,一般可认为是下游服务的问题导致网关无法完成请求转发。 -对于下游服务,不能想当然就认为是我们的Java程序,比如在拓扑上可能Nginx代理的是Kubernetes的Traefik Ingress,链路是Nginx->Traefik->应用,如果一味排查Java程序的健康,则始终找不到根因。 - -有时虽然使用了Feign进行服务调用,出现连接超时也不一定就是服务端问题,有可能是客户端通过URL调用服务端,并非通过Eureka的服务发现实现的客户端负载均衡。即客户端连接的是Nginx代理而非直接连接应用,客户端连接服务出现的超时,其实是Nginx代理宕机所致。 - -## 资源限制 -观察各种监控指标,如果发现曲线慢慢上升然后稳定在一个水平线,一般就是资源达到瓶颈。 - -观察网络带宽曲线时,如果带宽上升到120MB左右不动了,很可能就是打满了1GB的网卡或传输带宽 -观察到数据库活跃连接数上升到10个不动了,很可能是连接池打满了 - -观察监控一旦看到任何这样曲线,都要引起重视。 - -## 连锁反应 -CPU、内存、IO和网络相辅相成,一个资源出现瓶颈,很可能同时引起其他资源连锁反应。 - -内存泄露后对象无法回收会造成大量Full GC,CPU会大量消耗在GC从而引起CPU使用增加 - -经常会把数据缓存在内存队列进行异步IO,网络或磁盘出现问题时,就很可能会引起内存暴涨。 - -所以出问题时,要综合考虑避免误判 -## 客户端or服务端or传输问题? -比如MySQL访问慢了,可能: -- 客户端原因,连接池不够导致连接获取慢、GC停顿、CPU占满 -- 传输过程问题 -包括光纤可能被挖断了呀、防火墙、路由表等设置有问题 -- 真的服务端背锅了 - -这都需要逐一排查区分。 - -服务端慢一般可以看到MySQL出慢日志,传输慢一般可以通过ping来简单定位,排除了这两个可能,并且仅仅是部分客户端出现访问慢的情况,就需要怀疑是客户端本身的问题。对于第三方系统、服务或存储访问出现慢的情况,不能完全假设是服务端的问题。 - -第七,快照类工具和趋势类工具需要结合使用。比如,jstat、top、各种监控曲线是趋势类工具,可以让我们观察各个指标的变化情况,定位大概的问题点;而jstack和分析堆快照的MAT是快照类工具,用于详细分析某一时刻应用程序某一个点的细节。 - -一般情况下,我们会先使用趋势类工具来总结规律,再使用快照类工具来分析问题。如果反过来可能就会误判,因为快照类工具反映的只是一个瞬间程序的情况,不能仅仅通过分析单一快照得出结论,如果缺少趋势类工具的帮助,那至少也要提取多个快照来对比。 - -第八,不要轻易怀疑监控。我曾看过一个空难事故的分析,飞行员在空中发现仪表显示飞机所有油箱都处于缺油的状态,他第一时间的怀疑是油表出现故障了,始终不愿意相信是真的缺油,结果飞行不久后引擎就断油熄火了。同样地,在应用出现问题时,我们会查看各种监控系统,但有些时候我们宁愿相信自己的经验,也不相信监控图表的显示。这可能会导致我们完全朝着错误的方向来排查问题。 - -如果你真的怀疑是监控系统有问题,可以看一下这套监控系统对于不出问题的应用显示是否正常,如果正常那就应该相信监控而不是自己的经验。 - -第九,如果因为监控缺失等原因无法定位到根因的话,相同问题就有再出现的风险,需要做好三项工作: - -做好日志、监控和快照补漏工作,下次遇到问题时可以定位根因; -针对问题的症状做好实时报警,确保出现问题后可以第一时间发现; -考虑做一套热备的方案,出现问题后可以第一时间切换到热备系统快速解决问题,同时又可以保留老系统的现场。 - -# 总结 -## 分析问题必须讲理 -靠猜是猜不出来的,需要提前做好基础监控的建设。监控的话,需要在基础运维层、应用层、业务层等多个层次进行。定位问题的时候,我们同样需要参照多个监控层的指标表现综合分析。 - -## 定位问题要先对原因进行大致分类 -比如是内部问题还是外部问题、CPU相关问题还是内存相关问题、仅仅是A接口的问题还是整个应用的问题,然后再去进一步细化探索,一定是从大到小来思考问题;在追查问题遇到瓶颈的时候,我们可以先退出细节,再从大的方面捋一下涉及的点,再重新来看问题。 - -## 经验很重要 -遇到重大问题的时候,往往也需要根据直觉来第一时间找到最有可能的点,这里甚至有运气成分。我还和你分享了我的九条经验,建议你在平时解决问题的时候多思考、多总结,提炼出更多自己分析问题的套路和拿手工具。 - -定位到问题原因后,要做好复盘回溯。每次故障的解决都是宝贵经验,复盘不止是记录问题,更是为了架构优化。 -复盘可重点关注如下: -- 记录完整的时间线、处理措施、上报流程等信息 -- 分析问题的根本原因 -- 给出短、中、长期改进方案,包括但不限于代码改动、SOP、流程,并记录跟踪每一个方案进行闭环 -- 定期组织团队回顾过去的故障 \ No newline at end of file diff --git "a/\350\201\214\344\270\232\345\217\221\345\261\225/\345\277\205\350\257\273\344\271\246\347\261\215.md" "b/\350\201\214\344\270\232\345\217\221\345\261\225/\345\277\205\350\257\273\344\271\246\347\261\215.md" deleted file mode 100644 index efc142ed09..0000000000 --- "a/\350\201\214\344\270\232\345\217\221\345\261\225/\345\277\205\350\257\273\344\271\246\347\261\215.md" +++ /dev/null @@ -1,357 +0,0 @@ -以下皆出自本人亲自翻阅过的书籍,体验良好,豆瓣大众也以为然,遂列举,以供后浪规划学习。 - -* * * - -1 JavaSE -======== - -1.1 基础 ------- - -### 《Java 核心技术:卷1 》 - -适合转行及大一的CS专业新生们 - -![](https://img-blog.csdnimg.cn/20200124010655430.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - - - -1.2 进阶 ------- - -### Java 编程思想 中文第四版 - -即使是最晦涩的概念,在Bruce Eckel的文字亲和力和小而直接的编程示例面前也会化解于无形。从Java的基础语法到最高级特性(深入的面向对象概念、多线程、自动项目构建、单元测试和调试等),本书都能逐步指导你轻松掌握。 - -作者拥有多年教学经验,对C、C++以及Java语言都有独到、深入的见解,以通俗易懂及小而直接的示例解释了一个个晦涩抽象的概念。包含了Java语言基础语法以及高级特性,适合各个层次的Java程序员阅读。 - -![](https://img-blog.csdnimg.cn/2020041022402693.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - - -###  On Java 8 (Java 编程思想 英文第五版) - -![](https://img-blog.csdnimg.cn/20200410224536738.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - - -### Effective Java中文版(第3版) - -90个条目,每个条目讨论Java程序设计中的一条规则。这些规则反映了最有经验的优秀程序员在实践中常用的一些有益的做法。 - -每一章都涉及软件设计的一个主要方面,并不一定需要按部就班地从头读到尾,每个条目都有一定程度的独立性。相互之间经常交叉引用,因此可以很容易地在书中找到自己需要的内容。 - -本书的目标是帮助读者更加有效地使用Java编程语言及其基本类库:java.lang、java.util和java.io,以及子包,如java.util.concurrent和java.util.function。 - -![](https://img-blog.csdnimg.cn/20200124010747616.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - -并发 --- - -### 《Java并发编程实战》 - -并发领域圣经,适合进阶选手的阅读,由 JDK 并发包作者亲自执笔,科学权威地讲解了并发的设计原理。 - -![](https://img-blog.csdnimg.cn/20200124012004430.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - -2 JVM -===== - -2.1 基础 ------- - -### 深入理解Java虚拟机(第3版) - -这是一部从工作原理和工程实践两个维度深入剖析JVM的著作,是计算机领域公认的经典,繁体版在台湾也颇受欢迎。 - -第3版在第2版的基础上做了重大修订,内容更丰富、实战性更强:根据新版JDK对内容进行了全方位的修订和升级,围绕新技术和生产实践新增逾10万字,包含近50%的全新内容,并对第2版中含糊、瑕疵和错误内容进行了修正。 - -![](https://img-blog.csdnimg.cn/20200124010454852.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - -Inside the Java 2 Virtual Machine -================================= - -3 操作系统(Linux) -============= - - 3.1 基础 -------- - -鳥哥的Linux私房菜(第四版) ----------------- - -![](https://img-blog.csdnimg.cn/20200124011312222.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - -深入理解计算机系统(原书第3版) ----------------- - -和第2版相比,本版内容上最大变化是,从以IA32和x86-64为基础转变为完全以x86-64为基础。主要更新如下: - -基于x86-64,大量地重写代码,首次介绍对处理浮点数据的程序的机器级支持。 - -处理器体系结构修改为支持64位字和操作的设计。 - -引入更多的功能单元和更复杂的控制逻辑,使基于程序数据流表示的程序性能模型预测更加可靠。 - -扩充关于用GOT和PLT创建与位置无关代码的讨论,描述了更加强大的链接技术(比如库打桩)。 - -增加了对信号处理程序更细致的描述,包括异步信号安全的函数等。 - -采用新函数,更新了与协议无关和线程安全的网络编程。 - -![](https://img-blog.csdnimg.cn/8af0446b4d08458991f91cb69286303d.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - - - - - -《**UNIX环境高级编程**》第三版 -------------------- - -被誉为UNIX编程“圣经”的Advanced Programming in the UNIX Environment一书的第3版。在本书第2版出版后的8年中,UNIX行业发生了巨大的变化,特别是影响UNIX编程接口的有关标准变化很大。本书在保持前一版风格的基础上,根据最新的标准对内容进行了修订和增补,反映了最新的技术发展。书中除了介绍UNIX文件和目录、标准I/O库、系统数据文件和信息、进程环境、进程控制、进程关系、信号、线程、线程控制、守护进程、各种I/O、进程间通信、网络IPC、伪终端等方面的内容,还在此基础上介绍了众多应用实例,包括如何创建数据库函数库以及如何与网络打印机通信等 - -![](https://img-blog.csdnimg.cn/20200124012046460.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - -4 Spring 框架 -=========== - -4.1 基础 ------- - -### Spring实战(第4版) - -入门经典书籍。第5版最新但是设计不适合初学者,所以推荐四版。适合刚开始学习Spring 框架的Java 开发人员快速上手。 - -![](https://img-blog.csdnimg.cn/20200926042752695.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -=================================================================================================================================================================================================== - -5 数据库(MySQL) -============ - -5.1 基础 ------- - -### 《SQL 必知必会》 - -本书是深受世界各地读者欢迎的SQL经典畅销书,内容丰富,文字简洁明快,针对Oracle、SQL Server、MySQL、DB2、PostgreSQL、SQLite等各种主流数据库提供了大量简明的实例。与其他同类图书不同,它没有过多阐述数据库基础理论,而是专门针对一线软件开发人员,直接从SQL SELECT开始,讲述实际工作环境中最常用和最必需的SQL知识,实用性极强。通过本书,读者能够从没有多少SQL经验的新手,迅速编写出世界级的SQL! - -![](https://img-blog.csdnimg.cn/20200124011613817.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - -### 《高性能 MySQL》第三版 - -![](https://img-blog.csdnimg.cn/20200124011712860.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - - - -6 Redis -======= - -6.1 基础 ------- - -### Redis设计与实现 - -![](https://img-blog.csdnimg.cn/20200124011831993.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - -### Redis开发与运维 - -本书全面讲解Redis基本功能及其应用,并结合线上开发与运维监控中的实际使用案例,深入分析并总结了实际开发运维中遇到的“陷阱”,以及背后的原因, 包含大规模集群开发与管理的场景、应用案例与开发技巧,为高效开发运维提供了大量实际经验和建议。本书不要求读者有任何Redis使用经验,对入门与进阶DevOps的开发者提供有价值的帮助。主要内容包括:Redis的安装配置、API、各种高效功能、客户端、持久化、复制、高可用、内存、哨兵、集群、缓存设计等,Redis高可用集群解决方案,Redis设计和使用中的问题,最后提供了一个开源工具:Redis监控运维云平台CacheCloud。 - -![](https://img-blog.csdnimg.cn/20200630094214256.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### Redis 深度历险:核心原理与应用实践 - -《Redis 深度历险:核心原理与应用实践》分为基础和应用篇、原理篇、集群篇、拓展篇、源码篇共 5 大块内容。基础和应用篇讲解对读者来说最有价值的内容,可以直接应用到实际工作中;原理篇、集群篇让开发者透过简单的技术表面看到精致的底层世界;拓展篇帮助读者拓展技术视野和夯实基础,便于进阶学习;源码篇让高阶的读者能够读懂源码,掌握核心技术实力。 - -适合人群:有 Redis 基础,渴望深度掌握 Redis 技术原理的中高级后端开发者;渴望成功进入大型互联网企业研发部的中高级后端开发者;需要支撑公司 Redis 中间件运维工作的初中级运维工程师;对 Redis 中间件技术好奇的中高级前端技术研究者。 - -![](https://img-blog.csdnimg.cn/20200630094508786.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -6.2 进阶 ------- - -### Redis5设计与源码分析 - -本书系统讲解Redis 5设计、数据结构、底层命令实现,以及持久化、主从复制、集群的实现。 - -![](https://img-blog.csdnimg.cn/20200630094640802.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -7 软件设计 -====== - - 7.1 基础 -------- - -### 《Head First设计模式》 - -![](https://img-blog.csdnimg.cn/20200630094740534.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 大话设计模式 - -![](https://img-blog.csdnimg.cn/20200630094820433.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 设计模式之禅(第2版) - -本书是设计模式领域公认的3本经典著作之一,“极具趣味,容易理解,但讲解又极为严谨和透彻”是本书的写作风格和方法的最大特点。深刻解读6大设计原则和28种设计模式的准确定义、应用方法和最佳实践,全方位比较各种同类模式之间的异同,详细讲解将不同的模式组合使用的方法。 - -![](https://img-blog.csdnimg.cn/20201011051617541.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 代码整洁之道 - -本书提出:代码质量与其整洁度成正比。干净的代码,既在质量上较为可靠,也为后期维护、升级奠定了良好基础。本书给出一系列行之有效的整洁代码操作实践,并辅以来自现实项目的正、反两面的范例。 - -遵循这些规则,就能编写出干净的代码,有效提升代码质量。涵盖从命名到重构的多个编程方面。 - -![](https://img-blog.csdnimg.cn/20211006015429607.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -### UNIX编程艺术 - -本书主要介绍了Unix系统领域中的设计和开发哲学、思想文化体系、原则与经验,由公认的Unix编程大师、开源运动领袖人物之一Eric S. Raymond倾力多年写作而成。包括Unix设计者在内的多位领域专家也为本书贡献了宝贵的内容。本书内容涉及社群文化、软件开发设计与实现,覆盖面广、内容深邃,完全展现了作者极其深厚的经验积累和领域智慧。 - -![](https://img-blog.csdnimg.cn/20211010013840628.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - - - -8 架构 -==== - -8.1 基础 ------- - -### 《大型网站技术架构:核心原理与案例分析》\- 面试架构知识点核心书籍 - -通过梳理大型网站技术发展历程,剖析大型网站技术架构模式,深入讲述大型互联网架构设计的核心原理,并通过一组典型网站技术架构设计案例,为读者呈现一幅包括技术选型、架构设计、性能优化、Web 安全、系统发布、运维监控等在内的大型网站开发全景视图。了解大型网站的解决方案和开发理念。 - -![](https://img-blog.csdnimg.cn/20200926043702445.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -8.2 进阶 ------- - -### 亿级流量网站架构核心技术 - -京东架构师经验之谈,总结并梳理了亿级流量网站高可用和高并发原则,通过实例详细介绍了如何落地这些原则。 - -分为四部分:概述、高可用原则、高并发原则、案例实战。从负载均衡、限流、降级、隔离、超时与重试、回滚机制、压测与预案、缓存、池化、异步化、扩容、队列等多方面详细介绍了亿级流量网站的架构核心技术,让读者看后能快速运用到实践项目中。  - -![](https://img-blog.csdnimg.cn/20201130145340753.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 企业应用架构模式 - -本书作者是当今面向对象软件开发的权威,他在一组专家级合作者的帮助下,将40多种经常出现的解决方案转化成模式,最终写成这本能够应用于任何一种企业应用平台的、关于解决方案的、不可或缺的手册。本书获得了2003年度美国软件开发杂志图书类的生产效率奖和读者选择奖。本书分为两大部分。第一部分是关于如何开发企业应用的简单介绍。第二部分是本书的主体,是关于模式的详细参考手册,每个模式都给出使用方法和实现信息 - -![](https://img-blog.csdnimg.cn/20201002040637862.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 架构整洁之道 - -创造“Clean神话”的Bob大叔在架构领域的登峰之作,围绕“架构整洁”这一重要导向,系统地剖析其缘起、内涵及应用场景,涵盖软件研发完整过程及所有核心架构模式。 - -![](https://img-blog.csdnimg.cn/20211005030603809.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -========================================================================================================================================================================================= - -### 数据密集型应用系统设计 - -全书分为三大部分: - -第一部分,主要讨论有关增强数据密集型应用系统所需的若干基本原则。首先开篇第1章即瞄准目标:可靠性、可扩展性与可维护性,如何认识这些问题以及如何达成目标。第2章我们比较了多种不同的数据模型和查询语言,讨论各自的适用场景。接下来第3章主要针对存储引擎,即数据库是如何安排磁盘结构从而提高检索效率。第4章转向数据编码(序列化)方面,包括常见模式的演化历程。 - -第二部分,我们将从单机的数据存储转向跨机器的分布式系统,这是扩展性的重要一步,但随之而来的是各种挑战。所以将依次讨论数据远程复制(第5章)、数据分区(第6章)以及事务(第7章)。接下来的第8章包括分布式系统的更多细节,以及分布式环境如何达成一致性与共识(第9章)。 - -第三部分,主要针对产生派生数据的系统,所谓派生数据主要指在异构系统中,如果无法用一个数据源来解决所有问题,那么一种自然的方式就是集成多个不同的数据库、缓存模块以及索引模块等。首先第10章以批处理开始来处理派生数据,紧接着第11章采用流式处理。第12章总结之前介绍的多种技术,并分析讨论未来构建可靠、可扩展和可维护应用系统可能的新方向或方法。 - -![](https://img-blog.csdnimg.cn/20211006191349733.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - - - -9 DDD -===== - - 9.1 基础 -------- - -### 领域驱动设计模式、原理与实践 - -![](https://img-blog.csdnimg.cn/20200926052219965.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 实现领域驱动设计 - -领域驱动设计(DDD)教我们如何做好软件的,同时也是教我们如何更好地使用面向对象技术的。它为我们提供了设计软件的全新视角,同时也给开发者留下了一大难题:如何将领域驱动设计付诸实践?Vaughn Vernon 的这本《实现领域驱动设计》为我们给出了全面的解答。 - -《实现领域驱动设计》分别从战略和战术层面详尽地讨论了如何实现DDD,其中包含了大量的最佳实践、设计准则和对一些问题的折中性讨论。《实现领域驱动设计》共分为14 章,在DDD 战略部分,《实现领域驱动设计》向我们讲解了领域、限界上下文、上下文映射图和架构等内容,战术部分包括实体、值对象、领域服务、领域事件、聚合和资源库等内容。一个虚构的案例研究贯穿全书,这对于实例讲解DDD 实现来说非常有用。 - -《实现领域驱动设计》在DDD 的思想和实现之间建立起了一座桥梁,架构师和程序员均可阅读,同时也可以作为一本DDD 参考书。 - -![](https://img-blog.csdnimg.cn/20200926044228475.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -9.2 进阶 ------- - -### 领域驱动设计 - -领域驱动设计方面的经典之作。全书围绕设计和开发实践,结合项目案例,向读者阐述如何在真实的软件开发中应用领域驱动设计。给出了领域驱动设计的系统化方法,并将人们普遍接受的一些实践综合到一起,融入了作者的见解和经验,展现了一些可扩展的设计新实践、已验证过的技术以及便于应对复杂领域的软件项目开发的基本原则。 - -![](https://img-blog.csdnimg.cn/20200926044555766.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - - -10 计算机网络 -======== - -10.1 基础 -------- - -### 《图解HTTP》 - -本书对HTTP协议进行全面系统介绍。作者由HTTP协议的发展历史娓娓道来,严谨细致地剖析了HTTP协议的结构,列举诸多常见通信场景及实战案例,最后延伸到Web安全、最新技术动向等方面。本书的特色为在讲解的同时,辅以大量生动形象的通信图例,更好地帮助读者深刻理解HTTP通信过程中客户端与服务器之间的交互情况。读者可通过本书快速了解并掌握HTTP协议的基础,前端工程师分析抓包数据,后端工程师实现REST API、实现自己的HTTP服务器等过程中所需的HTTP相关知识点本书均有介绍。 - -![](https://img-blog.csdnimg.cn/202009260447331.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -11 数据结构与算法 -========== - -11.1 基础 -------- - -### 算法(第4版) - -Sedgewick畅销著作的最新版,反映了经过几十年演化而成的算法核心知识体系,全面论述排序、搜索、图处理和字符串处理的算法和数据结构,涵盖每位程序员应知应会的50种算法,全新的Java实现代码,采用模块化的编程风格,所有代码均可供读者使用。 - -![](https://img-blog.csdnimg.cn/20200703165148393.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 大话数据结构 - -本书为超级畅销书《大话设计模式》作者程杰潜心三年推出的扛鼎之作!以一个计算机教师教学为场景,讲解数据结构和相关算法的知识。通篇以一种趣味方式来叙述,大量引用了各种各样的生活知识来类比,并充分运用图形语言来体现抽象内容,对数据结构所涉及到的一些经典算法做到逐行分析、多算法比较。与市场上的同类数据结构图书相比,本书内容趣味易读,算法讲解细致深刻,是一本非常适合自学的读物。 - -本书以一个计算机教师教学为场景,讲解数据结构和相关算法的知识。通篇以一种趣味方式来叙述,大量引用了各种各样的生活知识来类比,并充分运用图形语言来体现抽象内容,对数据结构所涉及到的一些经典算法做到逐行分析、多算法比较。与市场上的同类数据结构图书相比,本书内容趣味易读,算法讲解细致深刻,是一本非常适合自学的读物。 - -![](https://img-blog.csdnimg.cn/20200703165428511.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - - -11.2 进阶 -------- - -### 程序员代码面试指南(第2版) - -程序员代码面试"神书”!书中对IT名企代码面试各类题目的最优解进行了总结,并提供了相关代码实现。针对当前程序员面试缺乏权威题目汇总这一痛点,本书选取将近300道真实出现过的经典代码面试题,帮助广大程序员的面试准备做到接近万无一失。"刷”完本书后,你就是"题王”!《程序员代码面试指南:IT名企算法与数据结构题目最优解(第2版)》采用题目解答的方式组织内容,并把面试题类型相近或者解法相近的题目尽量放在一起,读者在学习本书时很容易看出面试题解法之间的联系,使知识的学习避免碎片化。本书所收录的所有面试题都给出了最优解讲解和代码实现,并且提供了一些普通解法和最优解法的运行时间对比,让读者真切地感受到最优解的魅力!书中收录了大量新题和最优解分析,这些内容源自笔者多年来"死磕自己”的深入思考。提升算法和数据结构等方面能力。 - -![](https://img-blog.csdnimg.cn/2020070316532482.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -12 网络编程 -======= - -12.1 基础 -------- - -### Netty 实战 - -本书中文版基于Netty4.1.9做了修订。Netty之父”Trustin Lee作序推荐。无论是构建高性能的Web、游戏服务器、推送系统、RPC框架、消息中间件还是分布式大数据处理引擎,都离不开Netty,在整个行业中,Netty广泛而成功的应用,使其成为了Java高性能网络编程的卓绝框架。无论是想要学习Spring 5 、Spark、Cassandra等这样的系统,还是通过学习Netty来构建自己的基于Java的高性能网络框架,或者是更加具体的高性能Web或者游戏服务器等,本书都将是你的超强拍档。 - -![](https://img-blog.csdnimg.cn/20201030002004281.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -13 消息队列 -======= - -RabbitMQ 实战 ------------ - -![](https://img-blog.csdnimg.cn/20210703182055114.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/Cookie\343\200\201Session\346\234\272\345\210\266.md" "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/Cookie\343\200\201Session\346\234\272\345\210\266.md" deleted file mode 100644 index 6a3d4592cb..0000000000 --- "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/Cookie\343\200\201Session\346\234\272\345\210\266.md" +++ /dev/null @@ -1,324 +0,0 @@ -会话(Session)跟踪是Web程序中常用的技术,用来**跟踪用户的整个会话** -常用的会话跟踪技术是Cookie与Session。 -**Cookie通过在客户端记录信息确定用户身份** -**Session通过在服务器端记录信息确定用户身份** - -本文将系统地讲述Cookie与Session机制,并比较说明什么时候不能用Cookie,什么时候不能用Session。 - -# **1 Cookie机制** -理论上,**一个用户的所有请求操作都应该属于同一个会话**,而另一个用户的所有请求操作则应该属于另一个会话,二者不能混淆 -例如,用户A在超市购买的任何商品都应该放在A的购物车内,不论是用户A什么时间购买的,这都是属于同一个会话的,不能放入用户B或用户C的购物车内,这不属于同一个会话。 - -而Web应用程序是使用HTTP协议传输数据的 -**HTTP协议是无状态的协议。一旦数据交换完毕,客户端与服务器端的连接就会关闭,再次交换数据需要建立新的连接。这就意味着服务器无法从连接上跟踪会话** -即用户A购买了一件商品放入购物车内,当再次购买商品时服务器已经无法判断该购买行为是属于用户A的会话还是用户B的会话了。`要跟踪该会话,必须引入一种机制。` - -Cookie就是这样的一种机制。它可以弥补HTTP协议无状态的不足。在Session出现之前,基本上所有的网站都采用Cookie来跟踪会话。 - -## **1.1. 什么是Cookie** -由于HTTP是一种无状态的协议,服务器单从网络连接上无从知道客户身份。怎么办呢? -**给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理** - -Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。 - - -查看某个网站颁发的Cookie很简单。在浏览器地址栏输入`javascript:alert (document. cookie)`就可以了(需要有网才能查看)。JavaScript脚本会弹出一个对话框显示本网站颁发的所有Cookie的内容 -![](https://img-blog.csdnimg.cn/img_convert/c9bdbef1b99c7e7e41354fbfc9aa3e86.png) -其中第一行`BAIDUID`记录的就是笔者的身份,只是Baidu使用特殊的方法将Cookie信息加密了 - -如果浏览器不支持Cookie(如大部分手机中的浏览器)或者把Cookie禁用了,Cookie功能就会失效。 - -不同的浏览器采用不同的方式保存Cookie -IE浏览器会在“C:\Documents and Settings\你的用户名\Cookies”文件夹下以文本文件形式保存,一个文本文件保存一个Cookie -## **1.2 记录用户访问次数** -Java中把Cookie封装成了`javax.servlet.http.Cookie`类 -![](https://img-blog.csdnimg.cn/img_convert/8c2990a324dd323f20348303c1c4d8c2.png) -每个Cookie都是该Cookie类的对象。服务器通过操作Cookie类对象对客户端Cookie进行操作。 -通过`request.getCookies()` -获取客户端提交的所有Cookie(以Cookie[]数组形式返回) -![](https://img-blog.csdnimg.cn/img_convert/52acee6c638b06fd564b482f7a625460.png) -通过`response.addCookie(Cookiecookie)` -向客户端设置Cookie - -Cookie对象使用key-value属性对的形式保存用户状态,一个Cookie对象保存一个属性对,一个request/response同时使用多个Cookie。因为Cookie类位于包javax.servlet.http.*下面,所以JSP中不需要import该类。 - -### 1.1.3 Cookie的不可跨域名性 -- 很多网站都会使用Cookie,Google会向客户端颁发Cookie,Baidu也会向客户端颁发Cookie。那浏览器访问Google会不会也带上Baidu颁发的Cookie? -不会! - -**Cookie具有不可跨域名性**。根据Cookie规范,浏览器访问Google只会携带Google的Cookie,而不会携带Baidu的Cookie。Google也只能操作Google的Cookie,而不能操作Baidu的Cookie。 - -Cookie在客户端由浏览器管理。浏览器会保证Google只会操作Google的Cookie而不会操作Baidu的Cookie,从而保证用户隐私安全。浏览器判断一个网站是否能操作另一个网站Cookie的依据是域名。Google与Baidu的域名不一样,因此Google不能操作Baidu的Cookie。 - -虽然网站images.google.com与网站www.google.com同属Google,但域名不同,二者同样不能互相操作彼此Cookie。 - -用户登录网站www.google.com之后会发现访问images.google.com时登录信息仍然有效,而普通的Cookie是做不到的。这是因为Google做了特殊处理! - -**1.1.4 Unicode编码:保存中文** - -中文与英文字符不同,**中文属于Unicode字符,在内存中占4个字符,而英文属于ASCII字符,内存中只占2个字节**。Cookie中使用Unicode字符时需要对Unicode字符进行编码,否则会乱码。 - -提示:Cookie中保存中文只能编码。一般使用UTF-8编码即可。不推荐使用GBK等中文编码,因为浏览器不一定支持,而且JavaScript也不支持GBK编码。 - -**1.1.5 BASE64编码:保存二进制图片** - -Cookie不仅可以使用ASCII字符与Unicode字符,还可以使用二进制数据。例如在Cookie中使用数字证书,提供安全度。使用二进制数据时也需要进行编码。 - -注意:由于浏览器每次请求服务器都会携带Cookie,因此Cookie内容不宜过多,否则影响速度。Cookie的内容应该少而精。 - -**1.1.6 设置Cookie的所有属性** - -除了name与value之外,Cookie还具有其他几个常用的属性。每个属性对应一个getter方法与一个setter方法。Cookie类的所有属性如表1.1所示。 - -![image.png](https://img-blog.csdnimg.cn/img_convert/bb36276019a1961db27dfbf4b49f9c32.png) - - -**1.1.7 Cookie的有效期** -Cookie的maxAge决定着Cookie的有效期,单位为秒(Second)。Cookie中通过getMaxAge()方法与setMaxAge(int maxAge)方法来读写maxAge属性。 - -如果maxAge属性为正数,则表示该Cookie会在maxAge秒之后自动失效。浏览器会将maxAge为正数的Cookie持久化,即写到对应的Cookie文件中。无论客户关闭了浏览器还是电脑,只要还在maxAge秒之前,登录网站时该Cookie仍然有效。下面代码中的Cookie信息将永远有效。 -```java -Cookie cookie = new Cookie("username","helloweenvsfei"); // 新建Cookie - -cookie.setMaxAge(Integer.MAX_VALUE); // 设置生命周期为MAX_VALUE - -response.addCookie(cookie); // 输出到客户端 -``` -如果maxAge为负数,则表示该Cookie仅在本浏览器窗口以及本窗口打开的子窗口内有效,关闭窗口后该Cookie即失效。 -maxAge为负数的Cookie,为临时性Cookie,不会被持久化,不会被写到Cookie文件中。Cookie信息保存在浏览器内存中,因此关闭浏览器该Cookie就消失了。Cookie默认的maxAge值为–1。 - -如果maxAge为0,则表示删除该Cookie。Cookie机制没有提供删除Cookie的方法,因此通过设置该Cookie即时失效实现删除Cookie的效果。失效的Cookie会被浏览器从Cookie文件或者内存中删除, - -例如: - -```java -Cookie cookie = new Cookie("username","helloweenvsfei"); // 新建Cookie - -cookie.setMaxAge(0); // 设置生命周期为0,不能为负数 - -response.addCookie(cookie); // 必须执行这一句 -``` - -response对象提供的Cookie操作方法只有一个添加操作add(Cookie cookie)。 - -要想修改Cookie只能使用一个同名的Cookie来覆盖原来的Cookie,达到修改的目的。删除时只需要把maxAge修改为0即可。 - -注意:从客户端读取Cookie时,包括maxAge在内的其他属性都是不可读的,也不会被提交。浏览器提交Cookie时只会提交name与value属性。maxAge属性只被浏览器用来判断Cookie是否过期。 - -**1.1.8 Cookie的修改、删除** -- Cookie并不提供修改、删除操作。如果要修改某个Cookie,只需要新建一个同名的Cookie,添加到response中覆盖原来的Cookie。 -- 如果要删除某个Cookie,只需要新建一个同名的Cookie,并将maxAge设置为0,并添加到response中覆盖原来的Cookie。注意是0而不是负数。负数代表其他的意义。读者可以通过上例的程序进行验证,设置不同的属性。 -- 注意:修改、删除Cookie时,新建的Cookie除value、maxAge之外的所有属性,例如name、path、domain等,都要与原Cookie完全一样。否则,浏览器将视为两个不同的Cookie不予覆盖,导致修改、删除失败。 - -### 1.1.9 Cookie的域名 -Cookie是不可跨域名的。域名www.google.com颁发的Cookie不会被提交到域名www.baidu.com去。这是由Cookie的隐私安全机制(能够禁止网站非法获取其他网站的Cookie)决定的。 - -正常情况下,同一个一级域名下的两个二级域名如: -- www.java.com -- images.java.com - -也不能交互使用Cookie,因为二者的域名并不严格相同。 - -跨域z则是我们访问两个不同的域名或路径时,希望带上同一个cookie,跨域的具体实现方式有很多。如果想所有java.com名下的二级域名都可以使用该Cookie,可以设置Cookie的domain参数,表示浏览器访问这个域名时才带上这个cookie。 -```java -Cookie cookie = new Cookie("time","20080808"); -cookie.setDomain(".java.com"); -cookie.setPath("/"); -cookie.setMaxAge(Integer.MAX_VALUE); -response.addCookie(cookie); -``` -读者可以修改本机hosts文件配置多个临时域名,然后使用程序设置跨域名Cookie验证domain属性。 - -domain参数必须以点(".")开始。另外,name相同但domain不同的两个Cookie是两个不同的Cookie。如果想要两个域名完全不同的网站共有Cookie,可以生成两个Cookie,domain属性分别为两个域名,输出到客户端。 - -### 1.1.10 Cookie的路径 -- domain属性 -决定运行访问Cookie的域名 -- path属性 -表示访问的URL是这个path或者子路径时才带上这个cookie,决定允许访问Cookie的路径(ContextPath) - -例如,如果只允许/session/下的程序使用Cookie,可以这么写: -```java -Cookie cookie = new Cookie("time","20080808"); -cookie.setPath("/session/"); -response.addCookie(cookie); -``` -设置为“/”时允许所有路径使用Cookie。path属性需要使用符号“/”结尾。name相同但domain同同的两个Cookie也是不同的 - -注意:页面只能获取它属于的Path的Cookie。例如/session/test/a.jsp不能获取到路径为/session/abc/的Cookie。使用时一定要注意。 - -**1.1.11 Cookie的安全属性** -HTTP协议不仅是无状态的,而且是不安全的。使用HTTP协议的数据不经过任何加密就直接在网络上传播,有被截获的可能。使用HTTP协议传输很机密的内容是一种隐患。 -如果不希望Cookie在HTTP等非安全协议中传输,可以设置Cookie的secure属性为true。浏览器只会在HTTPS和SSL等安全协议中传输此类Cookie。下面的代码设置secure属性为true: -```java -Cookie cookie = new Cookie("time", "20080808"); -cookie.setSecure(true); -response.addCookie(cookie); -``` -提示:secure属性并不能对Cookie内容加密,不能保证绝对的安全性。 -如果需要高安全性,**需要在程序中对Cookie内容加密 - -**1.1.12 JavaScript操作Cookie** -Cookie是保存在浏览器端的,因此浏览器具有操作Cookie的先决条件。 -浏览器可以使用脚本程序如JS或者VBScript等操作Cookie。 -这里以JavaScript为例介绍常用的Cookie操作。 -例如下面的代码会输出本页面所有的Cookie。 -`` -由于JavaScript能够任意地读写Cookie,有些好事者便想使用JavaScript程序去窥探用户在其他网站的Cookie。不过这是徒劳的,W3C组织早就意识到JavaScript对Cookie的读写所带来的安全隐患并加以防备了,W3C标准的浏览器会阻止JavaScript读写任何不属于自己网站的Cookie。换句话说,A网站的JavaScript程序读写B网站的Cookie不会有任何结果。 - -**1.1.13 案例:永久登录** -如果用户是在自己家的电脑上网,登录时就可以记住他的登录信息,下次访问时不需要再次登录,直接访问即可。 -实现方法是**把登录信息如账号、密码等保存在Cookie中,并控制Cookie的有效期,下次访问时再验证Cookie中的登录信息即可。** - -保存登录信息有多种方案 -- 最直接的是把用户名与密码都保持到Cookie中,下次访问时检查Cookie中的用户名与密码,与数据库比较。这是**一种比较危险的选择,一般不把密码等重要信息保存到Cookie中**。 - -- 还有**一种方案是把密码加密后保存到Cookie中,下次访问时解密并与数据库比较**。这种方案略微安全一些。如果不希望保存密码,还可以把登录的时间戳保存到Cookie与数据库中,到时只验证用户名与登录时间戳就可以了。 -这几种方案验证账号时都要查询数据库。 - -- 本例将采用另一种方案,只在登录时查询一次数据库,以后访问验证登录信息时不再查询数据库。实现方式是 -**把账号按照一定的规则加密后,连同账号一块保存到Cookie中。下次访问时只需要判断账号的加密规则是否正确即可**。 -本例把账号保存到名为account的Cookie中,把账号连同密钥用MD5算法加密后保存到名为ssid的Cookie中。验证时验证Cookie中的账号与密钥加密后是否与Cookie中的ssid相等。 - -登录时可以选择登录信息的有效期:关闭浏览器即失效、30天内有效与永久有效。通过设置Cookie的age属性来实现,注意观察代码 - -提示:该加密机制中最重要的部分为算法与密钥。由于MD5算法的不可逆性,即使用户知道了账号与加密后的字符串,也不可能解密得到密钥。因此,只要保管好密钥与算法,该机制就是安全的。 - -**1.2 Session机制** -Web应用程序中还经常使用Session来记录客户端状态。**Session是服务器端使用的一种记录客户端状态的机制**,使用上比Cookie简单一些,相应的也**增加了服务器的存储压力**。 - -**1.2.1 什么是Session** -Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。 -客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。 - -如果说**Cookie机制是通过检查客户身上的“通行证”来确定客户身份的话,那么Session机制就是通过检查服务器上的“客户明细表”来确认客户身份。Session相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了** - -**1.2.2 实现用户登录** -Session对应的类为javax.servlet.http.HttpSession类。 -每个来访者对应一个Session对象,所有该客户的状态信息都保存在这个Session对象里。 -**Session对象是在客户端第一次请求服务器的时候创建的** -Session也是一种key-value的属性对,通过getAttribute(Stringkey)和setAttribute(String key,Objectvalue)方法读写客户状态信息 -Servlet里通过request.getSession()方法获取该客户的Session - -request还可以使用getSession(boolean create)来获取Session。区别是如果该客户的Session不存在,request.getSession()方法会返回null,而getSession(true)会先创建Session再将Session返回。 - -Servlet中必须使用request来编程式获取HttpSession对象,而JSP中内置了Session隐藏对象,可以直接使用。如果使用声明了<%@page session="false" %>,则Session隐藏对象不可用。 - - -当多个客户端执行程序时,服务器会保存多个客户端的Session。获取Session的时候也不需要声明获取谁的Session。**Session机制决定了当前客户只会获取到自己的Session,而不会获取到别人的Session。各客户的Session也彼此独立,互不可见**。 - -提示**:Session的使用比Cookie方便,但是过多的Session存储在服务器内存中,会对服务器造成压力。** - -**1.2.3 Session的生命周期** -Session保存在服务器端。**为了获得更高的存取速度,服务器一般把Session放在内存里。每个用户都会有一个独立的Session。如果Session内容过于复杂,当大量客户访问服务器时可能会导致内存溢出。因此,Session里的信息应该尽量精简** - -**Session在用户第一次访问服务器的时候自动创建**。需要注意只有访问JSP、Servlet等程序时才会创建Session,只访问HTML、IMAGE等静态资源并不会创建Session。如果尚未生成Session,也可以使用request.getSession(true)强制生成Session。 - -**Session生成后,只要用户继续访问,服务器就会更新Session的最后访问时间,并维护该Session**。用户每访问服务器一次,无论是否读写Session,服务器都认为该用户的Session“活跃(active)”了一次。 - -**1.2.4 Session的有效期** -由于会有越来越多的用户访问服务器,因此Session也会越来越多。**为防止内存溢出,服务器会把长时间内没有活跃的Session从内存删除。这个时间就是Session的超时时间。如果超过了超时时间没访问过服务器,Session就自动失效了。** - -Session的超时时间为maxInactiveInterval属性,可以通过对应的getMaxInactiveInterval()获取,通过setMaxInactiveInterval(longinterval)修改。 - -Session的超时时间也可以在web.xml中修改。另外,通过调用Session的invalidate()方法可以使Session失效。 - -**1.2.5 Session的常用方法** - -Session中包括各种方法,使用起来要比Cookie方便得多。Session的常用方法如表1.2所示。 - -![image.png](https://img-blog.csdnimg.cn/img_convert/b85d5e07803eade931b06a5b40d54f32.png) - -Tomcat中Session的默认超时时间为20分钟。通过setMaxInactiveInterval(int seconds)修改超时时间。可以修改web.xml改变Session的默认超时时间。例如修改为60分钟: - - - - 60 - - - - - -注意:参数的单位为分钟,而setMaxInactiveInterval(int s)单位为秒。 -**1.2.6 Session对浏览器的要求** - -虽然Session保存在服务器,对客户端是透明的,它的正常运行仍然需要客户端浏览器的支持。这是因为Session需要使用Cookie作为识别标志。HTTP协议是无状态的,Session不能依据HTTP连接来判断是否为同一客户,因此服务器向客户端浏览器发送一个名为JSESSIONID的Cookie,它的值为该Session的id(也就是HttpSession.getId()的返回值)。Session依据该Cookie来识别是否为同一用户。 - -该Cookie为服务器自动生成的,它的maxAge属性一般为–1,表示仅当前浏览器内有效,并且各浏览器窗口间不共享,关闭浏览器就会失效。 - -因此同一机器的两个浏览器窗口访问服务器时,会生成两个不同的Session。但是由浏览器窗口内的链接、脚本等打开的新窗口(也就是说不是双击桌面浏览器图标等打开的窗口)除外。这类子窗口会共享父窗口的Cookie,因此会共享一个Session。 - -注意:新开的浏览器窗口会生成新的Session,但子窗口除外。子窗口会共用父窗口的Session。例如,在链接上右击,在弹出的快捷菜单中选择“在新窗口中打开”时,子窗口便可以访问父窗口的Session。 - -如果客户端浏览器将Cookie功能禁用,或者不支持Cookie怎么办?例如,绝大多数的手机浏览器都不支持Cookie。Java Web提供了另一种解决方案:URL地址重写。 - -**1.2.7 URL地址重写** -URL地址重写是对客户端不支持Cookie的解决方案。 -URL地址重写的原理是将该用户Session的id信息重写到URL地址中。 -服务器能够解析重写后的URL获取Session的id。这样即使客户端不支持Cookie,也可以使用Session来记录用户状态。HttpServletResponse类提供了encodeURL(Stringurl)实现URL地址重写,例如: -```java - - "> - Homepage - -``` -该方法会自动判断客户端是否支持Cookie。如果客户端支持Cookie,会将URL原封不动地输出来。如果客户端不支持Cookie,则会将用户Session的id重写到URL中。重写后的输出可能是这样的: -``` - - Homepage - -``` -即在文件名的后面,在URL参数的前面添加了字符串“;jsessionid=XXX”。其中XXX为Session的id。 -分析一下可以知道,增添的jsessionid字符串既不会影响请求的文件名,也不会影响提交的地址栏参数。用户单击这个链接的时候会把Session的id通过URL提交到服务器上,服务器通过解析URL地址获得Session的id。 - -如果是页面重定向(Redirection),URL地址重写可以这样写: -```jsp -<% - if(“administrator”.equals(userName)) - - { - - response.sendRedirect(response.encodeRedirectURL(“administrator.jsp”)); - - return; - - } -%> -``` -效果跟response.encodeURL(String url)是一样的:如果客户端支持Cookie,生成原URL地址,如果不支持Cookie,传回重写后的带有jsessionid字符串的地址。 - -对于WAP程序,由于大部分的手机浏览器都不支持Cookie,WAP程序都会采用URL地址重写来跟踪用户会话。比如用友集团的移动商街等。 - -注意:TOMCAT判断客户端浏览器是否支持Cookie的依据是请求中是否含有Cookie。尽管客户端可能会支持Cookie,但是由于第一次请求时不会携带任何Cookie(因为并无任何Cookie可以携带),URL地址重写后的地址中仍然会带有jsessionid。当第二次访问时服务器已经在浏览器中写入Cookie了,因此URL地址重写后的地址中就不会带有jsessionid了。 - -**1.2.8 Session中禁止使用Cookie** -Java Web规范支持通过配置的方式禁用Cookie。下面举例说一下怎样通过配置禁止使用Cookie。 - -打开项目sessionWeb的WebRoot目录下的META-INF文件夹(跟WEB-INF文件夹同级,如果没有则创建),打开context.xml(如果没有则创建),编辑内容如下: - -代码1.11 /META-INF/context.xml - -```xml - - - - - -``` - -或者修改Tomcat全局的conf/context.xml,修改内容如下: -context.xml -```xml - - - - - - - -``` - -部署后TOMCAT便不会自动生成名JSESSIONID的Cookie,Session也不会以Cookie为识别标志,而仅仅以重写后的URL地址为识别标志了。 - -注意:该配置只是禁止Session使用Cookie作为识别标志,并不能阻止其他的Cookie读写。也就是说服务器不会自动维护名为JSESSIONID的Cookie了,但是程序中仍然可以读写其他的Cookie。 \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/HTTP/HTTP\345\215\217\350\256\256\347\232\204\345\211\215\344\270\226\344\273\212\347\224\237.md" "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/HTTP/HTTP\345\215\217\350\256\256\347\232\204\345\211\215\344\270\226\344\273\212\347\224\237.md" deleted file mode 100644 index 200e7ca617..0000000000 --- "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/HTTP/HTTP\345\215\217\350\256\256\347\232\204\345\211\215\344\270\226\344\273\212\347\224\237.md" +++ /dev/null @@ -1,173 +0,0 @@ -Tomcat本身是一个“HTTP服务器 + Servlet容器”,想深入理解Tomcat工作原理,理解HTTP协议的工作原理是基础。 - -# HTTP的本质 -HTTP协议是浏览器与服务器之间的数据传送协议。作为应用层协议,HTTP是基于TCP/IP协议来传递数据的(HTML文件、图片、查询结果等),HTTP协议不涉及数据包(Packet)传输,主要规定了客户端和服务器之间的通信格式。 - -假如浏览器需要从远程HTTP服务器获取一个HTML文本,在这个过程中,浏览器实际上要做两件事情。 -- 与服务器建立Socket连接 -浏览器从地址栏获取用户输入的网址和端口,去连接远端的服务器,这样就能通信了。 -- 生成**请求数据**并通过Socket发送出去 -这个请求数据长什么样?请求什么内容?浏览器需告诉服务端什么? - -首先,你要让服务端知道你是想获取内容 or 提交内容 -然后,你需要告诉服务端你想要哪个内容。 - -要把这些信息以何格式放入请求?这就是HTTP协议要解决的问题。 -即**HTTP协议本质是一种浏览器与服务器之间约定好的通信格式**。 -浏览器与服务器之间具体是怎么工作的? - -# HTTP工作原理 -- 一次HTTP请求过程 -![](https://img-blog.csdnimg.cn/20210714225935750.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -1. 用户通过浏览器操作,比如输入www.google.cn,并回车,于是浏览器获取该事件 -2. 浏览器向服务端发出TCP连接请求 -3. 服务程序接受浏览器的连接请求,并经过TCP三次握手建立连接 -4. 浏览器将请求数据打包成一个HTTP协议格式的数据包 -5. 浏览器将该数据包推入网络,数据包经过网络传输,最终达到端服务程序 -6. 服务端程序拿到这个数据包后,同样以HTTP协议格式解包,获取到客户端的意图 -7. 得知客户端意图后进行处理,比如提供静态文件或者调用服务端程序获得动态结果 -8. 服务器将响应结果(可能是HTML或者图片等)按照HTTP协议格式打包 -9. 服务器将响应数据包推入网络,数据包经过网络传输最终达到到浏览器 -10. 浏览器拿到数据包后,以HTTP协议的格式解包,然后解析数据,假设这里的数据是HTML -11. 浏览器将HTML文件展示在页面上 - -Tomcat作为一个HTTP服务器,在这个过程中主要 -- 接受连接 -- 解析请求数据 -- 处理请求 -- 发送响应 - -# HTTP格式 -## 请求数据 -你有没有注意到,在浏览器和HTTP服务器之间通信的过程中,首先要将数据打包成HTTP协议的格式,那HTTP协议的数据包具体长什么样呢? -### 组成 -HTTP请求数据由三部分组成,分别是请求行、请求报头、请求正文。 -比如一个简单的登录请求,浏览器会发如下HTTP请求: -#### 请求行 -```bash -GET /login?callBack=xxx HTTP/1.1 -``` - -#### 请求报头 -```bash -Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 -Accept-Encoding: gzip, deflate, br -Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7 -Cache-Control: max-age=0 -Connection: keep-alive -Host: www.nowcoder.com -User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) -``` -#### 请求正文 -```bash -{"country":86, "password":"22"} -``` - -当这个HTTP请求数据到达Tomcat后,Tomcat会把HTTP请求数据字节流解析成一个Request对象,这个Request对象封装了HTTP所有的请求信息。 -接着Tomcat把这个Request对象交给Web应用去处理,处理完后得到一个Response对象,Tomcat会把这个Response对象转成HTTP格式的响应数据并发送给浏览器。 - -## HTTP响应 -HTTP的响应也是由三部分组成:状态行、响应报头、报文主体。 -#### 状态行 -```bash -HTTP/1.1 200 OK -``` -#### 响应报头 -```bash -Connection: keep-alive -Content-Length: 0 -Content-Type: text/html; charset=UTF-8 -Date: Thu, 15 Jul 2021 02:37:13 GMT -Server: openresty -X-Backend-Response: 0.002 -``` -#### 报文主体 -```bash -``` - -# Cookie和Session -## 无状态 -HTTP协议本身是无状态的,不同请求间协议内容无相关性,即本次请求与上次请求没有内容的依赖关系,本次响应也只针对本次请求的数据,至于服务器应用程序为用户保存的状态是属于应用层,与HTTP协议本身无关! - -无状态,就是为完成某一操作,请求里包含了所有信息,服务端无需保存请求的状态,即无需保存session,无session的好处是带来了服务端良好的可伸缩性,方便failover,请求被LB转到不同server实例也没有区别。这样看,正是REST架构风格,才有了HTTP的无状态特性。REST和HTTP1.1其实都出自同一人之手。 - -http最初设计成无状态的是因为只是用来浏览静态文件的,无状态协议已经足够,也没什么负担。 -但随着web发展,它需要变得有状态,但是不是就要修改HTTP协议使之有状态? -不需要: -- 因为我们经常长时间逗留在某一个网页,然后才进入到另一个网页,如果在这两个页面之间维持状态,代价很高 -- 其次,历史让http无状态,但是现在对http提出了新的要求,按照软件领域的通常做法是,保留历史经验,在http协议上再加上一层实现我们的目的。所以引入cookie、session等机制来实现这种有状态的连接。 - -在分布式场景下,为减少Session不一致,一般将Session存储到Redis,而非直接存储在server实例。 - -现在的Web容器都支持将session存储在第三方中间件(如Redis)中,为什么大多都绕过容器,直接在应用中将会话数据存入中间件中? -因为用Web容器的Session方案需要侵入特定的Web容器,用Spring Session可能比较简单,它使得程序员甚至感觉不到Servlet容器的存在,可以专心开发Web应用。它是通过Servlet规范中的Filter机制拦截了所有Servlet请求,偷梁换柱,将标准Servlet请求对象包装,换成自己的Request包装类对象,当程序员通过包装后的Request对象的getSession方法拿Session时,是通过Spring拿Session,和Web容器无关。 - - -有了 session 就够了吗?这还有一个问题:Web应用不知道你是谁。 -比如你登录淘宝,在购物车添加了三件商品,刷新一下网页,这时系统提示你仍然处未登录状态,购物车也空了! -因此HTTP协议需要一种技术让请求与请求之间建立联系,服务器需要知道这个请求来自谁,于是出现了Cookie。 - -## Cookie -Cookie是HTTP报文的一个请求头,Web应用可以将用户的标识信息或者其他一些信息(用户名等)存储在Cookie。用户经过验证后,每次HTTP请求报文中都包含Cookie,这样服务器读取这个Cookie请求头就知道用户是谁了。 -Cookie本质上就是一份存储在用户本地的文件,里面包含了每次请求中都需要传递的信息。 - -## Session -由于Cookie以明文的方式存储在本地,而Cookie中往往带有用户信息,这样就造成了非常大的安全隐患,于是产生了Session。 - -### 如何理解Session -可理解为服务器端开辟的存储空间,里面保存了用户的状态,用户信息以Session的形式存储在服务端。当用户请求到来时,服务端可以把用户的请求和用户的Session对应起来。 - -### Session如何对应请求 -通过Cookie,浏览器在Cookie中填个了类似sessionid的字段标识请求。 - -### 工作过程 -- 服务端创建Session同时,为该Session生成唯一的sessionid -- 通过set-cookie放在HTTP的响应头 -- 浏览器将sessionid写到cookie里 -- 当浏览器再次发送请求时,自动携带该sessionid -- 服务器接受到请求后,根据sessionid找到相应Session -- 找到Session后,即可在Session中获取或添加内容 -- 这些内容只会保存在服务器,发到客户端的只有sessionid,这样相对安全,也节省网络流量,无需在Cookie中存储大量用户信息 - -### Session创建与存储 -在服务器端程序运行的过程中创建的,不同语言实现的应用程序有不同的创建Session的方法。 - -在Java中,是Web应用程序在调用HttpServletRequest的getSession方法时,由Web容器(比如Tomcat)创建的。 - -Tomcat的Session管理器提供了多种持久化方案来存储Session,通常会采用高性能的存储方式,比如Redis,并且通过集群部署的方式,防止单点故障,从而提升高可用。同时,Session有过期时间,因此Tomcat会开启后台线程定期的轮询,如果Session过期了就将Session失效。 - -引入session是因为cookie存在客户端,有安全隐患;但是session id也是通过cookie由客户端发送到服务端,虽然敏感的用户信息没有在网络上传输了,但是攻击者拿到sessionid也可以冒充受害者发送请求,这就是为什么我们需要https,加密后攻击者就拿不到sessionid了,另外CSRF也是一种防止session劫持的方式。 -token比如jwt token,本质也就是个加密的cookie。 - -- Cookie本质上就是一份存储在用户本地的文件,包含每次请求中都需要传递的信息 - -# HTTP长连接 -HTTP协议和其他应用层协议一样,本质上是一种通信格式。类比现实生活: -- HTTP是通信的方式 -HTTP是信封 -- HTML是通信的目的 -信封里面的信(HTML)才是内容,但没有信封,信也没办法寄出去。 - -像Cookie这些信息就像在信封表面的那些发信人相关个人信息。 -TCP连接就是送信员,负责真正的传输数据信息。无状态表示每次寄信都是新的信封。 -在服务端看来这些信之间是没有关系的,并且服务端通过阅读这封信就得到它要的全部信息,无需从其它地方(比如Session)获取这封信的更多上下文信息,服务端就知道怎么处理和回信。 - -HTTP协议就是浏览器与服务器之间的沟通语言,具体交互过程是请求、处理和响应。 - -在HTTP/1.0时期,每次HTTP请求都会创建一个新的TCP连接,请求完成后之后这个TCP连接就会被关闭。 -这种通信模式的效率不高,所以在HTTP/1.1中,引入了HTTP长连接的概念。 - -使用长连接的HTTP协议,会在响应头加入`Connection:keep-alive`。 -这样当浏览器完成一次请求后,浏览器和服务器之间的TCP连接不会关闭,再次访问这个服务器上的网页时,浏览器会继续使用这一条已经建立的连接,也就是说两个请求可能共用一个TCP连接。 - -**keep-alive**表示TCP的连接可复用,指的是利用已有传输通道进行HTTP协议内容的传输,省去创建、关闭连接的开销以达到提升性能效果(类似线程池的复用线程)。 -Connection:keep-alive只是建立TCP层的状态,省去了下一次的TCP三次握手,而HTTP本身还是继续保持无状态。 -应用程序其实一般不关心这次HTTP请求的TCP传输细节,只关心HTTP协议的内容,因此只要复用TCP连接时做好必要的数据重置,是不算有状态的。 - -HTTP的无状态性与共用TCP连接发送多个请求之间没有冲突,这些请求之间相对独立,唯一的关系可能只有发送的先后顺序关系。 -HTTP/1.1中的长连接没有解决 head of line blocking,请求是按顺序排队处理的,前面的HTTP请求处理会阻塞后面的HTTP请求,虽然HTTP pipelining对连接请求做了改善,但是复杂度太大,并没有普及,以至于后面的连接必须等待前面的返回了才能够发送。这个问题直到HTTP/2.0采取二进制分帧编码方式才彻底解决。 - -- HTTP 1.0 -买一个信封只能传送一个来回的信。 -- HTPP 1.1 **keep–alive** -买一个信封可重复使用,但前提是得等到服务端把这个信封里的信处理完,并送回来! \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/HTTP\350\266\205\346\227\266\345\244\204\347\220\206.md" "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/HTTP\350\266\205\346\227\266\345\244\204\347\220\206.md" deleted file mode 100644 index a944497e0d..0000000000 --- "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/HTTP\350\266\205\346\227\266\345\244\204\347\220\206.md" +++ /dev/null @@ -1,190 +0,0 @@ -# 1 超时,网络终究是靠不住的 -HTTP调用既然是网络请求,就可能超时,超时错误分两种,connect timeout和read timeout,前者可能是网络问题,或者服务端连接池不够用了。后者是连接已经建立了,但是服务端太忙了,不能及时处理完你的请求。 - -因此在实际开发中都需要注意考虑这些超时的处理措施。 - -框架设置的默认超时时间是否合理? -- 过短,请求还未处理完成,你有些急不可耐了呀! -- 过长 -请求早已超出正常响应时间而挂了! - -网络不稳定性,超时后可以通过定时任务请求重试 -这时,就要注意考虑服务端接口幂等性设计,即是否允许重试? - -框架是否会像浏览器那样限制并发连接数,以免在高并发下,HTTP调用的并发数成为瓶颈! - -## 1.1 HTTP调用框架技术选型 -- Spring Cloud全家桶或Dubbo -使用Feign进行声明式服务调用或 Dubbo自己的一套服务调用流程。 -- 只使用Spring Boot -HTTP客户端,如Apache HttpClient - -## 1.2 连接超时配置 && 读取超时参数 -虽然应用层是HTTP协议,但网络层始终是TCP/IP协议。TCP/IP是面向连接的协议,在传输数据之前需先建立连接。 -网络框架都会提供如下超时相关参数: -![](https://img-blog.csdnimg.cn/20201111193236911.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -- 连接超时参数ConnectTimeout -可自定义配置的建立连接最长等待时间 -- 读取超时参数ReadTimeout -控制从Socket上读取数据的最长等待时间。 - -## 1.3 常见的写 bug 姿势 -### 连接超时配置过长 -比如60s。TCP三次握手正常建立连接所需时间很短,在ms级最多到s级,不可能需要十几、几十秒,多半是网络或防火墙配置问题。这时如果几秒还连不上,那么可能永远也连不上。所以设置特别长的连接超时无意义,1~5秒即可。 -如果是纯内网调用,还可以设更短,在下游服务无法连接时,快速失败 -### 无脑排查连接超时问题 -服务一般会有多个节点,若别的客户端通过负载均衡连接服务端,那么客户端和服务端会直接建立连接,此时出现连接超时大概率是服务端问题 -而若服务端通过Nginx反向代理来负载均衡,客户端连接的其实是Nginx,而非服务端,此时出现连接超时应排查Nginx - - -### 读取超时参数和读取超时“坑点” - -#### 只要读取超时,服务端程序的正常执行就一定中断了? -##### 案例 -client接口内部通过`HttpClient`调用服务端接口server,客户端读取超时2秒,服务端接口执行耗时5秒。 -![](https://img-blog.csdnimg.cn/20201111204103629.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -调用client接口后,查看日志: -- 客户端2s后出现`SocketTimeoutException`,即读取超时![](https://img-blog.csdnimg.cn/2020111120432842.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -- 服务端却泰然地在3s后执行完成![](https://img-blog.csdnimg.cn/2020111120451912.png#pic_center) - -Tomcat Web服务器是把服务端请求提交到线程池处理,只要服务端收到请求,网络层面的超时和断开便不会影响服务端的执行。因此,出现读取超时不能随意假设服务端的处理情况,需要根据业务状态考虑如何进行后续处理。 - -#### 读取超时只是Socket网络层面概念,是数据传输的最长耗时,故将其配置很短 -比如100ms。 - -发生读取超时,网络层面无法区分如下原因: -- 服务端没有把数据返回给客户端 -- 数据在网络上耗时较久或丢包 - -但TCP是连接建立完成后才传输数据,对于网络情况不是特差的服务调用,可认为: -- **连接超时** -网络问题或服务不在线 -- **读取超时** -服务处理超时。读取超时意味着向Socket写入数据后,我们等到Socket返回数据的超时时间,其中包含的时间或者说绝大部分时间,是服务端处理业务逻辑的时间 - -#### 超时时间越长,任务接口成功率越高,便将读取超时参数配置过长 -HTTP请求一般需要获得结果,属**同步调用**。 -若超时时间很长,在等待 Server 返回数据同时,Client 线程(通常为 Tomcat 线程)也在等待,当下游服务出现大量超时,程序可能也会受到拖累创建大量线程,最终崩溃。 -- 对**定时任务**或**异步任务**,读取超时配置较长问题不大 -- 但面向用户响应的请求或是微服务平台的同步接口调用,并发量一般较大,应该设置一个较短的读取超时时间,以防止被下游服务拖慢,通常不会设置读取超时超过30s。 - -评论可能会有人问了,若把读取超时设为2s,而服务端接口需3s,不就永远拿不到执行结果? -的确,因此**设置读取超时要结合实际情况**: -- 过长可能会让下游抖动影响到自己 -- 过短又可能影响成功率。甚至,有些时候我们还要根据下游服务的SLA,为不同的服务端接口设置不同的客户端读取超时。 - -## 1.4 最佳实践 -连接超时代表建立TCP连接的时间,读取超时代表了等待远端返回数据的时间,也包括远端程序处理的时间。在解决连接超时问题时,我们要搞清楚连的是谁;在遇到读取超时问题的时候,我们要综合考虑下游服务的服务标准和自己的服务标准,设置合适的读取超时时间。此外,在使用诸如Spring Cloud Feign等框架时务必确认,连接和读取超时参数的配置是否正确生效。 -# 2 Feign&&Ribbon -## 2.1 如何配置超时 -为Feign配置超时参数的难点在于,Feign自身有两个超时参数,它使用的负载均衡组件Ribbon本身还有相关配置。这些配置的优先级是啥呢? - -## 2.2 案例 -- 测试服务端超时,假设服务端接口,只休眠10min -![](https://img-blog.csdnimg.cn/20201111211514444.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -- Feign调用该接口: -![](https://img-blog.csdnimg.cn/2020111121200699.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) - -- 通过Feign Client进行接口调用 -![](https://img-blog.csdnimg.cn/20201111213831460.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) - -在配置文件仅指定服务端地址的情况下: - -```bash -clientsdk.ribbon.listOfServers=localhost:45678 -``` - -得到如下输出: - -```bash -[21:46:24.222] [http-nio-45678-exec-4] [WARN ] [o.g.t.c.h.f.FeignAndRibbonController:26 ] - - 执行耗时:222ms 错误:Connect to localhost:45679 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] - failed: Connection refused (Connection refused) executing - POST http://clientsdk/feignandribbon/server -``` -Feign默认读取超时是1秒,如此短的读取超时算是“坑”。 -- 分析源码 -![](https://img-blog.csdnimg.cn/20201111215719405.png#pic_center)![](https://img-blog.csdnimg.cn/20201111215731406.png#pic_center)![](https://img-blog.csdnimg.cn/20201111215630631.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) - -## 自定义配置Feign客户端的两个全局超时时间 -可以设置如下参数: -```bash -feign.client.config.default.readTimeout=3000 -feign.client.config.default.connectTimeout=3000 -``` -修改配置后重试,得到如下日志: - -```bash -[http-nio-45678-exec-3] [WARN ] [o.g.t.c.h.f.FeignAndRibbonController :26 ] - 执行耗时:3006ms 错误:Read timed out executing POST http://clientsdk/feignandribbon/server -``` -3秒读取超时生效。 -注意:这里有一个大坑,如果希望只修改读取超时,可能会只配置这么一行: - -```bash -feign.client.config.default.readTimeout=3000 -``` -测试会发现,这样配置无法生效。 - -## 要配置Feign读取超时,必须同时配置连接超时 -查看`FeignClientFactoryBean`源码 -- 只有同时设置`ConnectTimeout`、`ReadTimeout`,Request.Options才会被覆盖 -![](https://img-blog.csdnimg.cn/20201111221444938.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) - -想针对单独的Feign Client设置超时时间,可以把default替换为Client的name: -```bash -feign.client.config.default.readTimeout=3000 -feign.client.config.default.connectTimeout=3000 -feign.client.config.clientsdk.readTimeout=2000 -feign.client.config.clientsdk.connectTimeout=2000 -``` - -## 单独的超时可覆盖全局超时 -```bash - [http-nio-45678-exec-3] [WARN ] [o.g.t.c.h.f.FeignAndRibbonController :26 ] - - 执行耗时:2006ms 错误:Read timed out executing - POST http://clientsdk/feignandribbon/server -``` - -## 除了可以配置Feign,也可配置Ribbon组件的参数以修改两个超时时间 -参数首字母要大写,和Feign的配置不同。 -```bash -ribbon.ReadTimeout=4000 -ribbon.ConnectTimeout=4000 -``` - -可以通过日志证明参数生效: - -```bash -[http-nio-45678-exec-3] [WARN ] [o.g.t.c.h.f.FeignAndRibbonController :26 ] - -执行耗时:4003ms 错误:Read timed out executing -POST http://clientsdk/feignandribbon/server -``` - -## 同时配置Feign和Ribbon的参数 -谁会生效? -```bash -clientsdk.ribbon.listOfServers=localhost:45678 -feign.client.config.default.readTimeout=3000 -feign.client.config.default.connectTimeout=3000 -ribbon.ReadTimeout=4000 -ribbon.ConnectTimeout=4000 -``` -最终生效的是Feign的超时: - -```bash -[http-nio-45678-exec-3] [WARN ] [o.g.t.c.h.f.FeignAndRibbonController :26 ] - -执行耗时:3006ms 错误:Read timed out executing -POST http://clientsdk/feignandribbon/server -``` - -## 同时配置Feign和Ribbon的超时,以Feign为准 -在`LoadBalancerFeignClient`源码 -如果`Request.Options`不是默认值,就会创建一个`FeignOptionsClientConfig`代替原来Ribbon的`DefaultClientConfigImpl`,导致**Ribbon的配置被Feign覆盖**: -![](https://img-blog.csdnimg.cn/20201111222305978.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -但若这么配置,最终生效的还是Ribbon的超时(4秒),难点Ribbon又反覆盖了Feign?不,这还是因为坑点二,**单独配置Feign的读取超时无法生效**: -```bash -clientsdk.ribbon.listOfServers=localhost:45678 -feign.client.config.default.readTimeout=3000 -feign.client.config.clientsdk.readTimeout=2000 -ribbon.ReadTimeout=4000 -``` \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/HTTP\351\207\215\345\244\215\350\257\267\346\261\202.md" "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/HTTP\351\207\215\345\244\215\350\257\267\346\261\202.md" deleted file mode 100644 index c1356ccb48..0000000000 --- "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/HTTP\351\207\215\345\244\215\350\257\267\346\261\202.md" +++ /dev/null @@ -1,99 +0,0 @@ -# Ribbon自动重试请求 -一些HTTP客户端往往会内置一些重试策略,其初衷是好的,毕竟因为网络问题导致丢包虽然频繁但持续时间短,往往重试就能成功, -但要留心这是否符合我们期望。 - -## 案例 -短信重复发送的问题,但短信服务的调用方用户服务,反复确认代码里没有重试逻辑。 -那问题究竟出在哪里? - -- Get请求的发送短信接口,休眠2s以模拟耗时: -![](https://img-blog.csdnimg.cn/20201111225415763.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) - -配置一个Feign供客户端调用: -![](https://img-blog.csdnimg.cn/20201111225647955.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) - -Feign内部有一个Ribbon组件负责客户端负载均衡,通过配置文件设置其调用的服务端为两个节点: - -```bash -SmsClient.ribbon.listOfServers=localhost:45679,localhost:45678 -``` - -- 客户端接口,通过Feign调用服务端 -![](https://img-blog.csdnimg.cn/20201111230748895.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -在45678和45679两个端口上分别启动服务端,然后访问45678的客户端接口进行测试。因为客户端和服务端控制器在一个应用中,所以45678同时扮演了客户端和服务端的角色。 - -在45678日志中可以看到,29秒时客户端收到请求开始调用服务端接口发短信,同时服务端收到了请求,2秒后(注意对比第一条日志和第三条日志)客户端输出了读取超时的错误信息: - -```bash -[http-nio-45678-exec-4] [INFO ] [c.d.RibbonRetryIssueClientController:23 ] - client is called -[http-nio-45678-exec-5] [INFO ] [c.d.RibbonRetryIssueServerController:16 ] - http://localhost:45678/ribbonretryissueserver/sms is called, 13600000000=>a2aa1b32-a044-40e9-8950-7f0189582418 -[http-nio-45678-exec-4] [ERROR] [c.d.RibbonRetryIssueClientController:27 ] - send sms failed : Read timed out executing GET http://SmsClient/ribbonretryissueserver/sms?mobile=13600000000&message=a2aa1b32-a044-40e9-8950-7f0189582418 -``` -而在另一个服务端45679的日志中还可以看到一条请求,客户端接口调用后的1秒: -```bash -[http-nio-45679-exec-2] [INFO ] [c.d.RibbonRetryIssueServerController:16 ] - http://localhost:45679/ribbonretryissueserver/sms is called, 13600000000=>a2aa1b32-a044-40e9-8950-7f0189582418 -``` -客户端接口被调用的日志只输出了一次,而服务端的日志输出了两次。虽然Feign的默认读取超时时间是1秒,但客户端2秒后才出现超时错误。 -说明**客户端自作主张进行了一次重试**,导致短信重复发送。 -## 源码揭秘 -查看Ribbon源码,MaxAutoRetriesNextServer参数默认为1,也就是Get请求在某个服务端节点出现问题(比如读取超时)时,Ribbon会自动重试一次: -![](https://img-blog.csdnimg.cn/20201111232018867.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) - -![](https://img-blog.csdnimg.cn/20201111232254555.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) - -### 解决方案 -1. 把发短信接口从Get改为Post -API设计规范:有状态的API接口不应定义为Get。根据HTTP协议规范,Get请求适用于数据查询,Post才是把数据提交到服务端用于修改或新增。选择Get还是Post的依据,应该是API行为,而非参数大小。 -> 常见误区:Get请求的参数包含在Url QueryString中,会受浏览器长度限制,所以一些开发会选择使用JSON以Post提交大参数,使用Get提交小参数。 - -2. 将`MaxAutoRetriesNextServer`参数配为0,禁用服务调用失败后在下一个服务端节点的自动重试。在配置文件中添加一行即可: - -```bash -ribbon.MaxAutoRetriesNextServer=0 -``` -### 问责 -至此,问题出在用户服务还是短信服务? -也许双方都有问题吧。 -- Get请求应该是无状态或者幂等的,短信接口可以设计为支持幂等调用 -- 用户服务的开发同学,如果对Ribbon的重试机制有所了解的话,或许就能在排查问题上少走弯路 - -## 最佳实践 -对于重试,因为HTTP协议认为Get请求是数据查询操作,是无状态的,又考虑到网络出现丢包是比较常见的事情,有些HTTP客户端或代理服务器会自动重试Get/Head请求。如果你的接口设计不支持幂等,需要关闭自动重试。但,更好的解决方案是,遵从HTTP协议的建议来使用合适的HTTP方法。 - -# 并发限制爬虫抓取 -HTTP请求调用还有一个常见的问题:并发数的限制,导致程序处理性能无法提升。 -## 案例 -某爬虫项目,整体爬取数据效率很低,增加线程池数量也无谓,只能堆机器。 -现在模拟该场景,探究问题本质。 - -假设要爬取的服务端是这样的一个简单实现,休眠1s返回数字1: -![](https://img-blog.csdnimg.cn/20201111233015454.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) - -爬虫需多次调用该接口抓取数据,为确保线程池不是并发瓶颈,使用了一个无线程上限的`newCachedThreadPool`,然后使用`HttpClient`执行HTTP请求,把请求任务循环提交到线程池处理,最后等待所有任务执行完成后输出执行耗时: -![](https://img-blog.csdnimg.cn/20201111233347439.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) - -- 使用默认的`PoolingHttpClientConnectionManager`构造的`CloseableHttpClient`,测试一下爬取10次的耗时: -![](https://img-blog.csdnimg.cn/20201111233512569.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -虽然一个请求需要1s执行完成,但线程池可扩张使用任意数量线程。 -按道理,10个请求并发处理的时间基本相当于1个请求的处理时间,即1s,但日志中显示实际耗时5秒: -![](https://img-blog.csdnimg.cn/20201111233832252.png#pic_center) - -## 源码解析 -`PoolingHttpClientConnectionManager`源码有两个重要参数: -![](https://img-blog.csdnimg.cn/20201111234051864.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -- defaultMaxPerRoute=2,即同一主机/域名的最大并发请求数为2。我们的爬虫需要10个并发,显然是**默认值太小限制了爬虫的效率**。 -- maxTotal=20,即所有主机整体最大并发为20,这也是HttpClient整体的并发度。**我们请求数是10最大并发是10,20不会成为瓶颈**。举一个例子,使用同一个HttpClient访问10个域名,defaultMaxPerRoute设置为10,为确保每一个域名都能达到10并发,需要把maxTotal设置为100。 - -`HttpClient`是常用的HTTP客户端,那为什么默认值限制得这么小? -很多早期的浏览器也限制了同一个域名两个并发请求。对于同一个域名并发连接的限制,其实是HTTP 1.1协议要求的,这里有这么一段话: - -> Clients that use persistent connections SHOULD limit the number of simultaneous connections that they maintain to a given server. A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy. A proxy SHOULD use up to 2*N connections to another server or proxy, where N is the number of simultaneously active users. These guidelines are intended to improve HTTP response times and avoid congestion. -HTTP 1.1协议是20年前制定的,现在HTTP服务器的能力强很多了,所以有些新的浏览器没有完全遵从2并发这个限制,放开并发数到了8甚至更大。 -如果需要通过HTTP客户端发起大量并发请求,不管使用什么客户端,请务必确认客户端的实现默认的并发度是否满足需求。 - -尝试声明一个新的HttpClient放开相关限制,设置maxPerRoute为50、maxTotal为100,然后修改一下刚才的wrong方法,使用新的客户端进行测试: -![](https://img-blog.csdnimg.cn/20201111234405368.png#pic_center) -输出如下,10次请求在1秒左右执行完成。可以看到,因为放开了一个Host 2个并发的默认限制,爬虫效率得到了大幅提升: -![](https://img-blog.csdnimg.cn/20201111234518322.png#pic_center) -## 最佳实践 -若你的客户端有比较大的请求调用并发,比如做爬虫,或是扮演类似代理的角色,又或者是程序本身并发较高,如此小的默认值很容易成为吞吐量的瓶颈,需要及时调整。 \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/Unix\347\232\204IO\346\250\241\345\236\213\350\247\243\346\236\220.md" "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/Unix\347\232\204IO\346\250\241\345\236\213\350\247\243\346\236\220.md" deleted file mode 100644 index e7f4cd5e0e..0000000000 --- "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/Unix\347\232\204IO\346\250\241\345\236\213\350\247\243\346\236\220.md" +++ /dev/null @@ -1,200 +0,0 @@ -IO 是主存和外部设备 ( 硬盘、各种移动终端及网络等 ) 拷贝数据的过程。IO 是操作系统的底层功能,通过 I/O 指令完成。网络编程领域的IO专指网络IO。 - -# JDK 的 NIO -NIO,即NEW IO,引入了多路选择器、Channel 和 Bytebuffer。 -os为了保护自身稳定,会将内存空间划分为内核、用户空间。当需通过 TCP 发送数据时,在应用程序中实际上执行了将数据从用户空间拷贝至内核空间,再由内核进行实际的发送动作;而从 TCP 读取数据时则反过来,等待内核将数据准备好,再从内核空间拷贝至用户空间,应用数据才能处理。针对在两个阶段上不同的操作,Unix 定义了 5 种 IO 模型 -# 1 阻塞式IO(Blocking IO) -最流行的 IO 模型,在客户端上特别常见,因为其编写难度最低,也最好理解。 - -在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样: -1. 通常涉及等待数据从网络中到达。当所有等待数据到达时,它被复制到内核中的某个缓冲区 -2. 把数据从内核缓冲区复制到应用程序缓冲区 -![](https://img-blog.csdnimg.cn/20201102000306356.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) - -用户进程调用`recvfrom(系统调用)`,kernel开始IO的第一个阶段:准备数据。 -对network io,很多时候数据在一开始还没有到达(比如,还没收到一个完整的UDP包),这时kernel就要等待足够数据。 -而用户进程整个被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除 block状态,重新运行。 - -所以,blocking IO的特点就是在IO执行的两个阶段都被阻塞。调用返回成功或发生错误前,应用程序都在阻塞在方法的调用上。当方法调用成功返回后,应用程序才能开始处理数据。 - -JDK1.4前,Java只支持BIO。 -示例代码: -```java -public static void main(String[] args) throws IOException { - // 创建一个客户端socket实例 - Socket socket = new Socket(); - // 尝试连接一个远端的服务器地址 - socket.connect(InetSocketAddress.createUnresolved("localhost", 4591)); - // 在连接成功后则获取输入流 - InputStream inputStream = socket.getInputStream(); - byte[] content = new byte[128]; - // 并且尝试读取数据 - int bytesOfRead = inputStream.read(content); -} -``` - -在输入流上的read调用会阻塞,直到有数据被读取成功或连接发生异常。 -read的调用就会经历上述将程序阻塞,然后内核等待数据准备后,将数据从内核空间复制到用户空间,即入参传递进来的二进制数组中。 -实际读取的字节数可能小于数组的长度,方法的返回值正是实际读取的字节数。 - -# 非阻塞式IO -允许将一个套接字设置为非阻塞。当设置为非阻塞时,是在通知内核:如果一个操作需要将当前的调用线程阻塞住才能完成时,不采用阻塞的方式,而是返回一个错误信息。其模型如下 - - - -可以看到,在内核没有数据时,尝试对数据的读取不会导致线程阻塞,而是快速的返回一个错误。直到内核中收到数据时,尝试读取,就会将数据从内核复制到用户空间,进行操作。 - -可以看到,在非阻塞模式下,要感知是否有数据可以读取,需要不断的轮训,这么做往往会耗费大量的 CPU。所以这种模式不是很常见。 - -JDK1.4提供新的IO包 - NIO,其中的SocketChannel提供了对非阻塞 IO 的支持。 -```java -public static void main(String[] args) throws IOException - { - SocketChannel socketChannel = SocketChannel.open(); - socketChannel.configureBlocking(false); - socketChannel.connect(InetSocketAddress.createUnresolved("192.168.31.80", 4591)); - ByteBuffer buffer = ByteBuffer.allocate(128); - while (socketChannel.read(buffer) == 0) - { - ; - } - } -``` - -一个SocketChannel实例就类似从前的一个Socket对象。 - -首先是通过`SocketChannel.open()`调用新建了一个SocketChannel实例,默认情况下,新建的socket实例都是阻塞模式,通过`java.nio.channels.spi.AbstractSelectableChannel#configureBlocking`调用将其设置为非阻塞模式,然后连接远程服务端。 - -`java.nio.channels.SocketChannel`使用`java.nio.ByteBuffer`作为数据读写的容器,可简单将`ByteBuffer`看成是一个内部持有二进制数据的包装类。 - -调用方法java.nio.channels.SocketChannel#read(java.nio.ByteBuffer)时会将内核中已经准备好的数据复制到ByteBuffer中。但是如果内核中此时并没有数据(或者说socket的读取缓冲区没有数据),则方法会立刻返回,并不会阻塞住。这也就对应了上图中,在内核等待数据的阶段(socket的读取缓冲区没有数据),读取调用时会立刻返回错误的。只不过在Java中,返回的错误在上层处理为返回一个读取为0的结果。 - -# IO复用 -IO复用指的应用程序阻塞在系统提供的两个调用select或poll上。当应用程序关注的套接字存在可读情况(也就是内核收到数据了),select或poll的调用被返回。此时应用程序可以通过recvfrom调用完成数据从内核空间到用户空间的复制,进而进行处理。具体的模型如下 - - - -可以看到,和 阻塞式IO 相比,都需要等待,并不存在优势。而且由于需要2次系统调用,其实还稍有劣势。但是IO复用的优点在于,其select调用,可以同时关注多个套接字,在规模上提升了处理能力。 - -IO复用的模型支持一样也是在JDK1.4中的 NIO 包提供了支持。可以参看如下示例代码: - -```java -public static void main(String[] args) throws IOException - { - /**创建2个Socket通道**/ - SocketChannel socketChannel = SocketChannel.open(); - socketChannel.configureBlocking(false); - socketChannel.connect(InetSocketAddress.createUnresolved("192.168.31.80", 4591)); - SocketChannel socketChannel2 = SocketChannel.open(); - socketChannel2.configureBlocking(false); - socketChannel2.connect(InetSocketAddress.createUnresolved("192.168.31.80", 4591)); - /**创建2个Socket通道**/ - /**创建一个选择器,并且两个通道在这个选择器上注册了读取关注**/ - Selector selector = Selector.open(); - socketChannel.register(selector, SelectionKey.OP_READ); - socketChannel2.register(selector, SelectionKey.OP_READ); - /**创建一个选择器,并且两个通道在这个选择器上注册了读取关注**/ - ByteBuffer buffer = ByteBuffer.wrap(new byte[128]); - //选择器可以同时检查所有在其上注册的通道,一旦哪个通道有关注事件发生,select调用就会返回,否则一直阻塞 - selector.select(); - Set selectionKeys = selector.selectedKeys(); - Iterator iterator = selectionKeys.iterator(); - while (iterator.hasNext()) - { - SelectionKey selectionKey = iterator.next(); - SocketChannel channel = (SocketChannel) selectionKey.channel(); - channel.read(buffer); - iterator.remove(); - } - } -``` - -代码一开始,首先是新建了2个客户端通道,连接到服务端上。接着创建了一个选择器Selector。选择器就是 Java 中实现 IO 复用的关键。选择器允许通道将自身的关注事件注册到选择器上。完成注册后,应用程序调用java.nio.channels.Selector#select()方法,程序进入阻塞等待直到注册在选择器上的通道中发生其关注的事件,则select调用会即可返回。然后就可以从选择器中获取刚才被选中的键。从键中可以获取对应的通道对象,然后就可以在通道对象上执行读取动作了。 - -结合IO复用模型,可以看到,select调用的阻塞阶段,就是内核在等待数据的阶段。一旦有了数据,内核等待结束,select调用也就返回了。 - -# 信号驱动IO -![](https://img-blog.csdnimg.cn/20201102011116492.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) - -与非阻塞IO类似,其在数据等待阶段并不阻塞,但是原理不同。信号驱动IO是在套接字上注册了一个信号调用方法。这个注册动作会将内核发出一个请求,在套接字的收到数据时内核会给进程发出一个sigio信号。该注册调用很快返回,因此应用程序可以转去处理别的任务。当内核准备好数据后,就给进程发出了信号。进程就可以通过recvfrom调用来读取数据。其模型如下 - - - -这种模型的优点就是在数据包到达之前,进程不会被阻塞。而且采用通知的方式也避免了轮训带来的损耗。 - -这种模型在Java中并没有对应的实现。 - -# 异步IO -异步IO的实现一般是通过系统调用,向内核注册了一个套接字的读取动作。这个调用一般包含了:缓存区指针,缓存区大小,偏移量、操作完成时的通知方式。该注册动作是即刻返回的,并且在整个IO的等待期间,进程都不会被阻塞。当内核收到数据,并且将数据从内核空间复制到用户空间完成后,依据注册时提供的通知方式去通知进程。其模型如下: - - - -与信号驱动 IO 相比,最大的不同在于信号驱动 IO 是内核通知应用程序可以读取数据了;而 异步IO 是内核通知应用程序数据已经读取完毕了。 - -Java 在 1.7 版本引入对 异步IO 的支持,可以看如下的例子: - -```java -public class MainDemo -{ - public static void main(String[] args) throws IOException, ExecutionException, InterruptedException - { - final AsynchronousSocketChannel asynchronousSocketChannel = AsynchronousSocketChannel.open(); - Future connect = asynchronousSocketChannel.connect(InetSocketAddress.createUnresolved("192.168.31.80", 3456)); - connect.get(); - ByteBuffer buffer = ByteBuffer.wrap(new byte[128]); - asynchronousSocketChannel.read(buffer, buffer, new CompletionHandler() - { - @Override - public void completed(Integer result, ByteBuffer buffer) - { - //当读取到数据,流中止,或者读取超时到达时均会触发回调 - if (result > 0) - { - //result代表着本次读取的数据,代码执行到这里意味着数据已经被放入buffer了 - processWithBuffer(buffer); - } - else if (result == -1) - { - //流中止,没有其他操作 - } - else{ - asynchronousSocketChannel.read(buffer, buffer, this); - } - } - - private void processWithBuffer(ByteBuffer buffer) - { - } - - @Override - public void failed(Throwable exc, ByteBuffer attachment) - { - } - }); - } -} -``` - -代码看上去和IO复用时更简单了。 - -首先是创建一个异步的 Socket 通道,注意,这里和 NIO 最大的区别就在于创建的是异步Socket通道,而 NIO 创建的属于同步通道。 - -执行connect方法尝试连接远程,此时方法会返回一个future,这意味着该接口是非阻塞的。实际上connect动作也是可以传入回调方法,将连接结果在回调方法中进行传递的。这里为了简化例子,就直接使用future了。 - -连接成功后开始在通道上进行读取动作。这里就是和 NIO 中最大的不同。读取的时候需要传入一个回调方法。当数据读取成功时回调方法会被调用,并且当回调方法被调用时读取的数据已经被填入了ByteBuffer。 - -主线程在调用读取方法完成后不会被阻塞,可以去执行别的任务。可以看到在整个过程都不需要用户线程参与,内核完成了所有的工作。 - -# 同步 V.S 异步 -根据 POSIX 的定义: -- 同步:同步操作导致进程阻塞,直到 IO 操作完成 -- 异步:异步操作不导致进程阻塞 - -来看下五种 IO 模型的对比,如下 - - - -可以看到,根据定义,前 4 种模型,在数据的读取阶段,全部都是阻塞的,因此是同步IO。而异步IO模型在整个IO过程中都不阻塞,因此是异步IO。 - -参考 - - http://www.tianshouzhi.com/api/tutorials/netty/221 \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/OSI\345\210\206\345\261\202\346\250\241\345\236\213/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\345\272\224\347\224\250\345\261\202.md" "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\345\272\224\347\224\250\345\261\202.md" similarity index 100% rename from "\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/OSI\345\210\206\345\261\202\346\250\241\345\236\213/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\345\272\224\347\224\250\345\261\202.md" rename to "\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\345\272\224\347\224\250\345\261\202.md" diff --git "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/OSI\345\210\206\345\261\202\346\250\241\345\236\213/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.md" "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.md" similarity index 100% rename from "\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/OSI\345\210\206\345\261\202\346\250\241\345\236\213/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.md" rename to "\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.md" diff --git "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/OSI\345\210\206\345\261\202\346\250\241\345\236\213/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\347\211\251\347\220\206\345\261\202.md" "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\347\211\251\347\220\206\345\261\202.md" similarity index 100% rename from "\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/OSI\345\210\206\345\261\202\346\250\241\345\236\213/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\347\211\251\347\220\206\345\261\202.md" rename to "\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\347\211\251\347\220\206\345\261\202.md" diff --git "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/OSI\345\210\206\345\261\202\346\250\241\345\236\213/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\347\275\221\347\273\234\345\261\202.md" "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\347\275\221\347\273\234\345\261\202.md" similarity index 98% rename from "\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/OSI\345\210\206\345\261\202\346\250\241\345\236\213/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\347\275\221\347\273\234\345\261\202.md" rename to "\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\347\275\221\347\273\234\345\261\202.md" index 4b636b21ee..a7e60a01d2 100644 --- "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/OSI\345\210\206\345\261\202\346\250\241\345\236\213/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\347\275\221\347\273\234\345\261\202.md" +++ "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\347\275\221\347\273\234\345\261\202.md" @@ -53,8 +53,8 @@ IP 地址 ::= { <网络号>, <主机号>} - 最大传送单元 MTU ![](https://img-blog.csdnimg.cn/img_convert/21d4d66f03d8fc4b09ed0859dcdd306a.png) -![](https://img-blog.csdnimg.cn/20201210230136250.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20201210230221432.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20201210230136250.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20201210230221432.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - IP 数据报分片![](https://img-blog.csdnimg.cn/img_convert/fc39ceaa459bf4a81a903c5f92649c3d.png) - 生存时间(8 位)记为 TTL (Time To Live)数据报在网络中可通过的路由器数的最大值 diff --git "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/OSI\345\210\206\345\261\202\346\250\241\345\236\213/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\350\277\220\350\276\223\345\261\202.md" "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\350\277\220\350\276\223\345\261\202.md" similarity index 90% rename from "\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/OSI\345\210\206\345\261\202\346\250\241\345\236\213/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\350\277\220\350\276\223\345\261\202.md" rename to "\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\350\277\220\350\276\223\345\261\202.md" index 942a83ad86..8e2253b69f 100644 --- "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/OSI\345\210\206\345\261\202\346\250\241\345\236\213/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\350\277\220\350\276\223\345\261\202.md" +++ "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\344\271\213\350\277\220\350\276\223\345\261\202.md" @@ -27,7 +27,7 @@ ## 2.1 TCP 与 UDP - 两个对等运输实体在通信时传送的数据单位叫作运输协议数据单元 TPDU (Transport Protocol Data Unit)。 - TCP 传送的数据单位协议是 TCP 报文段(segment) -- UDP(User Datagram Protocol) 传送的数据单位协议是 UDP 报文或用户数据报 +- UDP 传送的数据单位协议是 UDP 报文或用户数据报 - TCP/IP 体系中的运输层协议 ![](https://img-blog.csdnimg.cn/img_convert/bc69dec73738a8f0119c650387316f4b.png) @@ -54,30 +54,25 @@ - 熟知端口,0~1023 - 登记端口号,1024~49151,为没有熟知端口号的应用程序使用的。使用这个范围的端口号必须在 IANA 登记,以防止重复 - 客户端口号或短暂端口号,49152~65535,留给客户进程选择暂时使用。当服务器进程收到客户进程的报文时,就知道了客户进程所使用的动态端口号。通信结束后,这个端口号可供其他客户进程以后使用 -# 4 TCP(Transmission Control Protocol) -TCP是**传输控制协议**,一种**面向连接**的、**可靠的**、**基于字节流**的传输层通信协议,由IETF的RFC 793定义。 +# 4 TCP +## 4.1 TCP 最主要的特点 +- 面向连接的运输层协议 +面向连接意味着两个使用 TCP的应用在交换数据前必须先建立一个 TCP 连接,在一个 TCP 连接中,仅有两方进行彼此通信,广播和多播不能用于 TCP +- 每一条 TCP 连接只能有两个端点(endpoint) +每一条 TCP 连接只能是点对点的(一对一) +- 提供可靠交付的服务 +- 提供全双工通信 +- 面向字节流 +字节流服务:两个应用程序通过 TCP 连接,TCP 不在字节中插入记录标识符 +TCP对字节流的内容不做任何解释,不知道传输的字节流数据是二进制数据还是 ASCII 字符或其他类型数据,对字节流的解释由TCP连接双方的应用层 -## 4.1 特点 -### 面向连接 -连接需要先创建再使用,创建连接的三次握手有一定开销。两个使用 TCP的应用在交换数据前必须先建立一个 TCP 连接,在一个 TCP 连接中,仅有两方进行彼此通信,广播和多播不能用于 TCP -### 每条 TCP 连接只能有两个端点(endpoint) -每条 TCP 连接只能是点对点(一对一) -### 可靠交付 - -### 全双工通信 - -### 面向字节流 -两个应用程序通过 TCP 连接,TCP 不在字节中插入记录标识符。TCP对字节流内容不做任何编解码解释,不知道传输的字节流数据是二进制数据还是 ASCII 字符或其它类型数据,对字节流的编解码由TCP连接双方的应用层协商。 -字节是发送数据的最小单元,TCP协议本身无法区分哪几个字节是完整的消息体,也无法感知是否有多个客户端在使用同一个TCP连接,TCP只是一个读写数据的管道。 - TCP 面向流的概念 ![](https://img-blog.csdnimg.cn/img_convert/d6261fff949753fd3854eec394121778.png) -![](https://img-blog.csdnimg.cn/20210710223506585.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -## 注意 - - TCP 连接是一条虚连接而不是一条真正的物理连接 - - TCP 对应用进程一次把多长的报文发送到TCP 的缓存中是不关心的 - - TCP 根据对方给出的窗口值和当前网络拥塞的程度来决定一个报文段应包含多少个字节(而UDP 发送的报文长度是应用进程给出的) - - TCP 可把太长的数据块划分短一些再传送 +### 应当注意 + - TCP 连接是一条虚连接而不是一条真正的物理连接 + - TCP 对应用进程一次把多长的报文发送到TCP 的缓存中是不关心的 + - TCP 根据对方给出的窗口值和当前网络拥塞的程度来决定一个报文段应包含多少个字节(而UDP 发送的报文长度是应用进程给出的) + - TCP 可把太长的数据块划分短一些再传送 TCP 也可等待积累有足够多的字节后再构成报文段发送出去 ## 4.2 TCP 的连接 TCP 把连接作为最基本的抽象,每一条 TCP 连接有两个端点。TCP 连接的端点不是主机、主机的IP 地址、应用进程、运输层的协议端口,TCP 连接的端点叫做套接字(socket)!端口号拼接到(contatenated with) IP 地址即构成了套接字。 @@ -126,11 +121,6 @@ TCP 连接 ::= {socket1, socket2} - B关闭与A的连接,发FIN给A - A发ACK报文确认,并将确认序号设置为收到序号加1 ![](https://img-blog.csdnimg.cn/img_convert/2f85b4f054b6c614e1a9a77464ad1c2a.png) - -如下图中上框就是三次挥手,下框内即是四次挥手 -![](https://img-blog.csdnimg.cn/20210322181905895.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - - ### 4.2.4 为什么建立连接是三次握手,而关闭连接却四次握手 服务端的`listen`状态下的socket当收到syn报文的连接请求后,它可把ACK和SYN(ACK起应答作用,SYN起同步作用)放在同一个报文发送。 @@ -174,12 +164,6 @@ TCP 的可靠传输机制用字节的序号进行控制:所有的确认都是基 TCP 两端的四个窗口经常处于动态变化中 TCP 连接的往返时间 **RTT** 也不是固定不变的:需要使用特定的算法估算较为合理的重传时间 - -## 应用场景 -聊天消息传输、推送,单人语音、视频聊天等。几乎UDP能做的都能做,但需要考虑复杂性、性能问题。 - -## 限制 -无法进行广播、多播等操作 # 5 TCP 报文段的首部格式 ![TCP 报文段结构](https://img-blog.csdnimg.cn/img_convert/089aee8bee7d09961d61151719407881.png) - 源/目的端口——各占 2 字节 @@ -298,6 +282,7 @@ TCP窗口的单位是字节 - 发送方每收到一个对新报文段的确认(重传的不算在内)就使 cwnd 加 1 ![](https://img-blog.csdnimg.cn/20210221135936183.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + ### 传输轮次(transmission round) - 使用慢开始算法后,每经过一个传输轮次,拥塞窗口 cwnd 就加倍 - 一个传输轮次所经历的时间其实就是往返时间 RTT diff --git "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234(3)-DHCP & IP\347\232\204\"\345\255\275\347\274\230\".md" "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234(\344\270\211) - DHCP & IP\347\232\204\"\345\255\275\347\274\230\".md" similarity index 78% rename from "\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234(3)-DHCP & IP\347\232\204\"\345\255\275\347\274\230\".md" rename to "\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234(\344\270\211) - DHCP & IP\347\232\204\"\345\255\275\347\274\230\".md" index f65cce8f8a..b846004c1a 100644 --- "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234(3)-DHCP & IP\347\232\204\"\345\255\275\347\274\230\".md" +++ "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234(\344\270\211) - DHCP & IP\347\232\204\"\345\255\275\347\274\230\".md" @@ -1,155 +1,211 @@ -和其他机器通讯,就需要一个通讯地址,要给网卡配置这样一个地址。 +![](https://ask.qcloudimg.com/http-save/1752328/c8ix1ovhb3.png)和其他机器通讯,就需要一个通讯地址,要给网卡配置这么一个地址。 # 1 配置IP地址 -可使用: -- ifconfig -- ip addr -设置后,用这俩命令,将网卡up一下,即可开始工作。 +可以使用ifconfig,也可以使用ip addr。设置好了以后,用这两个命令,将网卡up一下,就可以开始工作了。 ## 1.1 net-tools -```bash + +``` $ sudo ifconfig eth1 10.0.0.1/24 $ sudo ifconfig eth1 up ``` + ## 1.2 iproute2 -```bash + +``` $ sudo ip addr add 10.0.0.1/24 dev eth1 $ sudo ip link set up eth1 ``` -若配置的是一个和谁都无关的地址呢? -例如,旁边机器都是192.168.1.x,我就配置个16.158.23.6,会咋样? -包发不出去! - -这是为啥? -***192.168.1.6*** 就在你这台机器的旁边,甚至在同一交换机,而你把机器地址设为 ***16.158.23.6***。 -在这台机器,企图去ping ***192.168.1.6***,觉得只要将包发出去,同一交换机的另一台机器应该马上就能收到,是嘛? -Linux不是这样的,它并不智能,你眼睛看到那台机器就在旁边,Linux则是根据自己的逻辑处理的: -**`只要是在网络上跑的包,都是完整的,可以有下层没上层,绝对不可能有上层没下层`**。 -所以它: -- 有自己的源IP地址 ***16.158.23.6*** -- 也有目标IP地址 ***192.168.1.6*** - -但包发不出去,是因为MAC层还没填。自己的MAC地址自己肯定知道,但目标MAC填啥? -是不是该填 ***192.168.1.6*** 机器的MAC地址? -不是! + +如果配置的是一个和谁都不搭嘎的地址呢? + +例如,旁边的机器都是192.168.1.x,我非得配置一个16.158.23.6,会出现什么现象呢? + +不会出现任何现象,就是包发不出去呗 + +为什么发不出去呢? + +_**192.168.1.6**_ 就在你这台机器的旁边,甚至在同一交换机,而你把机器的地址设为 _**16.158.23.6**_ + +在这台机器上,你企图去ping _**192.168.1.6**_,觉得只要将包发出去,同一个交换机的另一台机器马上就能收到,是嘛? + +可Linux不是这样的,没你想得那么智能 + +你用肉眼看到那台机器就在旁边,它则需要根据自己的逻辑处理 + +**`只要是在网络上跑的包,都是完整的,可以有下层没上层,绝对不可能有上层没下层`** + +所以,它有自己的源IP地址 _**16.158.23.6**_,也有目标IP地址 _**192.168.1.6**_,但包发不出去,这是因为MAC层还没填 + +自己的MAC地址自己知道,但目标MAC填啥呢? + +是不是填 _**192.168.1.6**_ 机器的MAC地址呢? + +当然不是! + Linux会判断要去的这个地址和我是一个网段吗,或者和我的一个网卡是同一网段吗? + 只有是一个网段的,它才会发送ARP请求,获取MAC地址 + 如果发现不是呢? -Linux默认的逻辑,如果这是一个跨网段的调用,它不会直接将包发送到网络上,而是将包发送到网关。 +**`Linux默认的逻辑,如果这是一个跨网段的调用,它不会直接将包发送到网络上,而是将包发送到网关`** 如果配置了网关,Linux会获取网关的MAC地址,然后将包发出去 -对于 ***192.168.1.6*** 机器,虽然路过家门的这个包,目标IP是它,但是无奈MAC地址不是它的,所以它的网卡是不会把包收进去的 + +对于 _**192.168.1.6**_ 机器,虽然路过家门的这个包,目标IP是它,但是无奈MAC地址不是它的,所以它的网卡是不会把包收进去的 **如果没有配置网关呢**? + 那包压根就发不出去 -如果将网关配置为 ***192.168.1.6*** 呢? +如果将网关配置为 _**192.168.1.6**_ 呢? + 不可能,Linux不会让你配置成功 + 因为 **`网关要和当前的网络至少一个网卡是同一个网段`** + 怎能允你16.158.23.6的网关是192.168.1.6呢? 所以,当你需要手动配置一台机器的网络IP时,一定要好好问问你的网管 + 如果在机房里面,要去网管那申请,让他给你分配一段正确的IP地址 + 当然,真正配置的时候,一定不是直接用命令配置的,而是放在一个配置文件里面 + **不同系统的配置文件格式不同,但是无非就是CIDR、子网掩码、广播地址和网关地址** # 2 DHCP - 动态主机配置协议 + 配置IP后一般不能变,配置一个服务端的机器还可以,但如果是客户端的机器呢? + 我抱着一台笔记本电脑在公司里走来走去,或者白天来晚上走,每次使用都要配置IP地址,那可怎么办?还有人事、行政等非技术人员,如果公司所有的电脑都需要IT人员配置,肯定忙不过来啊。 需要有一个自动配置的协议,即**动态主机配置协议(Dynamic Host Configuration Protocol),简称DHCP** 那网管就轻松多了。只需要配置一段共享的IP地址 + 每一台新接入的机器都通过DHCP协议,来这个共享的IP地址里申请,然后自动配置好就可 + 等用完了,还回去,这样其他的机器也能用。 -**数据中心里面的服务器,IP一旦配置好,基本不会变 -这就相当于买房自己装修。DHCP的方式就相当于租房。你不用装修,都是帮你配置好的。你暂时用一下,用完退租就可以了。** +\*\*数据中心里面的服务器,IP一旦配置好,基本不会变 + +这就相当于买房自己装修。DHCP的方式就相当于租房。你不用装修,都是帮你配置好的。你暂时用一下,用完退租就可以了。\*\* # 3 DHCP的工作方式 + 当一台机器新加入一个网络的时候,肯定啥情况都不知道,只知道自己的MAC地址 + 怎么办?先吼一句,我来啦,有人吗?这时候的沟通基本靠“吼” + 这一步,我们称为DHCP Discover。 -新来的机器使用IP地址 ***0.0.0.0*** 发送了一个广播包,目的IP地址为 ***255.255.255.255*** +新来的机器使用IP地址 _**0.0.0.0**_ 发送了一个广播包,目的IP地址为 _**255.255.255.255**_ + 广播包封装了UDP,UDP封装了BOOTP + 其实DHCP是BOOTP的增强版,但是如果去抓包的话,很可能看到的名称还是BOOTP协议 在广播包里,新人大喊:我是新来的(Boot request),我的MAC地址是这个,我还没有IP,谁能给租给我个IP地址! - 格式就像 -![](https://img-blog.csdnimg.cn/20190820013104636.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) +![](https://ask.qcloudimg.com/http-save/1752328/7aajibaa43.png) 如果一个网管在网络里面配置了**DHCP Server**,他就相当于这些IP的管理员 + 立刻能知道来了一个“新人” + 这个时候,我们可以体会MAC地址唯一的重要性了 + 当一台机器带着自己的MAC地址加入一个网络的时候,MAC是它唯一的身份,如果连这个都重复了,就没办法配置了。 只有MAC唯一,IP管理员才能知道这是一个新人,需要租给它一个IP地址,这个过程我们称为**DHCP Offer** + 同时,DHCP Server为此客户保留为它提供的IP地址,从而不会为其他DHCP客户分配此IP地址。 DHCP Offer的格式就像这样,里面有给新人分配的地址。 -![](https://img-blog.csdnimg.cn/20190820013258466.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) +![](https://ask.qcloudimg.com/http-save/1752328/3btvsn4ffv.png) DHCP Server 仍然使用广播地址作为目的地址 + 因为,此时请求分配IP的新人还没有自己的IP + DHCP Server回复说,我分配一个可用的IP给你,你看如何? + 除此之外,服务器还发送了子网掩码、网关和IP地址租用期等信息。 新来的机器很开心,它的“吼”得到了回复,并且有人愿意租给它一个IP地址了,这意味着它可以在网络上立足了 + 当然更令人开心的是,如果有多个DHCP Server,这台新机器会收到多个IP地址,简直受宠若惊!!! 它会选择其中一个DHCP Offer,一般是最先到达的那个,并且会向网络发送一个DHCP Request广播数据包,包中包含客户端的MAC地址、接受的租约中的IP地址、提供此租约的DHCP服务器地址等,并告诉所有DHCP Server它将接受哪一台服务器提供的IP地址,告诉其他DHCP服务器,谢谢你们的接纳,并请求撤销它们提供的IP地址,以便提供给下一个IP租用请求者 -![](https://img-blog.csdnimg.cn/201908202336176.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) + +![](https://ask.qcloudimg.com/http-save/1752328/qsvfbm3y9b.png) 还没得到DHCP Server的最后确认,客户端仍然使用0.0.0.0为源IP地址、255.255.255.255为目标地址进行广播 + 在BOOTP里面,接受某个DHCP Server的分配的IP。 当DHCP Server接收到客户机的DHCP request之后,会广播返回给客户机一个DHCP ACK消息包,表明已经接受客户机的选择,并将这一IP地址的合法租用信息和其他的配置信息都放入该广播包,发给客户机,欢迎它加入网络大家庭 -![](https://img-blog.csdnimg.cn/20190820233637478.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) + +![](https://ask.qcloudimg.com/http-save/1752328/2fgu26qeru.png) 最终租约达成的时候,还是需要广播一下,让大家都知道。 # IP地址的收回和续租 + 既然是租房子,就是有租期的。租期到了,管理员就要将IP收回。 + 如果不用的话,收回就收回了。就像你租房子一样,如果还要续租的话,不能到了时间再续租,而是要提前一段时间给房东说 + **DHCP也是这样** 客户机会在租期过去50%的时候,直接向为其提供IP地址的DHCP Server发送DHCP request消息包 + 客户机接收到该服务器回应的DHCP ACK消息包,会根据包中所提供的新的租期以及其他已经更新的TCP/IP参数,更新自己的配置 + 这样,IP租用更新就完成了。 网络管理员不仅能自动分配IP地址,还能帮你自动安装操作系统! # 预启动执行环境(PXE) + 数据中心里面的管理员可能一下子就拿到几百台空的机器,一个个安装操作系统,会累死的。 所以管理员希望的不仅仅是自动分配IP地址,还要自动安装系统。装好系统之后自动分配IP地址,直接启动就能用了 安装操作系统,应该有个光盘吧。数据中心里不能用光盘吧,想了一个办法就是,可以将光盘里面要安装的操作系统放在一个服务器上,让客户端去下载 + 但是客户端放在哪里呢?它怎么知道去哪个服务器上下载呢?客户端总得安装在一个操作系统上呀,可是这个客户端本来就是用来安装操作系统的呀? 其实,这个过程和操作系统启动的过程有点儿像。 + - 首先,启动BIOS,读取硬盘的MBR启动扇区,将GRUB启动起来 - 然后将权力交给GRUB,GRUB加载内核、加载作为根文件系统的initramfs文件 - 再将权力交给内核 - 最后内核启动,初始化整个操作系统。 安装操作系统的过程,只能插在BIOS启动之后了 + 因为没安装系统之前,连启动扇区都没有。因而这个过程叫做预启动执行环境 **(Pre-boot Execution Environment),PXE** PXE协议分为客户端和服务器端,由于还没有操作系统,只能先把客户端放在BIOS里面 + 当计算机启动时,BIOS把PXE客户端调入内存里面,就可以连接到服务端做一些操作了。 首先,PXE客户端自己也需要有个IP地址 + 因为PXE的客户端启动起来,就可以发送一个DHCP的请求,让DHCP Server给它分配一个地址。PXE客户端有了自己的地址,那它怎么知道PXE服务器在哪里呢?对于其他的协议,都好办,要么人告诉他 + 例如,告诉浏览器要访问的IP地址,或者在配置中告诉它;例如,微服务之间的相互调用。 但是PXE客户端启动的时候,啥都没有 + 好在DHCP Server除了分配IP地址以外,还可以做一些其他的事情。这里有一个DHCP Server的一个样例配置: + ``` ddns-update-style interim; ignore client-updates; @@ -167,31 +223,49 @@ subnet 192.168.1.0 netmask 255.255.255.0 next-server 192.168.1.180; } ``` + 按照上面的原理,默认的DHCP Server是需要配置的,无非是我们配置IP的时候所需要的IP地址段、子网掩码、网关地址、租期等 + 如果想使用PXE,则需要配置next-server,指向PXE服务器的地址,另外要配置初始启动文件filename 这样PXE客户端启动之后,发送DHCP请求之后,除了能得到一个IP地址,还可以知道PXE服务器在哪里,也可以知道如何从PXE服务器上下载某个文件,去初始化操作系统。 # 解析PXE的工作过程 + ## 启动PXE客户端 + 通过DHCP协议告诉DHCP Server,我,穷b,打钱! + DHCP Server便租给它一个IP地址,同时也给它PXE服务器的地址、启动文件pxelinux.0 ## 初始化机器 + PXE客户端知道要去PXE服务器下载这个文件后,就可以初始化机器 + 便开始下载(TFTP协议),还需要有一个TFTP服务器 + PXE客户端向TFTP服务器请求下载这个文件,TFTP服务器说好啊,于是就将这个文件传给它 ## 执行文件 + 然后,PXE客户端收到这个文件后,就开始执行这个文件 + 这个文件会指示PXE客户端,向TFTP服务器请求计算机的配置信息pxelinux.cfg + TFTP服务器会给PXE客户端一个配置文件,里面会说内核在哪里、initramfs在哪里。PXE客户端会请求这些文件。 ## 启动Linux内核 + 一旦启动了操作系统,以后就啥都好办了 -![](https://img-blog.csdnimg.cn/20190821003023985.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) + +![](https://ask.qcloudimg.com/http-save/1752328/cay61t0gaw.png) # 总结 + DHCP协议主要是用来给客户租用IP地址,和房产中介很像,要商谈、签约、续租,广播还不能“抢单”; -DHCP协议能给客户推荐“装修队”PXE,能够安装操作系统,这个在云计算领域大有用处。 \ No newline at end of file +DHCP协议能给客户推荐“装修队”PXE,能够安装操作系统,这个在云计算领域大有用处。 + +# 参考 + +- 趣谈网络协议 \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234(2)-\346\233\276\350\256\260\345\220\246,\346\237\245IP\345\234\260\345\235\200.md" "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234(\344\272\214) - \346\233\276\350\256\260\345\220\246,\346\237\245IP\345\234\260\345\235\200.md" similarity index 56% rename from "\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234(2)-\346\233\276\350\256\260\345\220\246,\346\237\245IP\345\234\260\345\235\200.md" rename to "\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234(\344\272\214) - \346\233\276\350\256\260\345\220\246,\346\237\245IP\345\234\260\345\235\200.md" index a049f0b2ca..1cce1a4a7b 100644 --- "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234(2)-\346\233\276\350\256\260\345\220\246,\346\237\245IP\345\234\260\345\235\200.md" +++ "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234(\344\272\214) - \346\233\276\350\256\260\345\220\246,\346\237\245IP\345\234\260\345\235\200.md" @@ -1,167 +1,219 @@ -- 1.1.1.1 不是测试用的,原来一直没分配,现在被用来做一个DNS了,宣传是比谷歌等公司的DNA服务更保护用户隐私 -- 255.255.255.255,代表有限广播,它的目标是网络中的所有主机 -- 0.0.0.0,通常代表未知的源主机。当主机采用DHCP动态获取IP地址而无法获得合法IP地址时,会用IP地址0.0.0.0来表示源主机IP地址未知 -- NID不能以数字127开头。NID 127被保留给内部回送函数,作为本机循环测试使用 -例如,使用命令ping 127.0.0.1测试TCP/IP协议栈是否正确安装。在路由器中,同样支持循环测试地址的使用。 -# 1 ip -## 1.1 ifconfig V.S ip addr +![](https://ask.qcloudimg.com/http-save/1752328/cyt6u5rhid.png) + +先献上几个梗 + +- 1.1.1.1 不是测试用的,原来一直没分配,现在被用来做一个DNS了,宣传是比谷歌等公司的dns服务 +更保护用户隐私。 +- IP地址255.255.255.255,代表有限广播,它的目标是网络中的所有主机。 +- IP地址0.0.0.0,通常代表未知的源主机。当主机采用DHCP动态获取IP地址而无法获得合法IP地址时,会用IP地址0.0.0.0来表示源主机IP地址未知。 +- NID不能以数字127开头。NID 127被保留给内部回送函数,作为本机循环测试使用。 +例如,使用命令ping 127.0.0.1测试TCP/IP协议栈是否正确安装。在路由器中,同样支持循环测试地址的使用。1 查ip - Windows - ipconfig - Linux上- ifconfig 还有--- **ip addr** ->net-tools起源于BSD,自2001年起,Linux社区已经对其停止维护,而iproute2旨在取代net-tools,并提供了一些新功能。一些Linux发行版已经停止支持net-tools,只支持iproute2。 -net-tools通过procfs(/proc)和ioctl系统调用去访问和改变内核网络配置,而iproute2则通过netlink套接字接口与内核通讯。net-tools中工具的名字比较杂乱,而iproute2则相对整齐和直观,基本是ip命令加后面的子命令。 +ifconfig & ip addr的区别 + +> net-tools起源于BSD,自2001年起,Linux社区已经对其停止维护,而iproute2旨在取代net-tools,并提供了一些新功能。一些Linux发行版已经停止支持net-tools,只支持iproute2。 + +net-tools通过procfs(/proc)和ioctl系统调用去访问和改变内核网络配置,而iproute2则通过netlink套接字接口与内核通讯。 + +net-tools中工具的名字比较杂乱,而iproute2则相对整齐和直观,基本是ip命令加后面的子命令。 + 虽然取代意图很明显,但是这么多年过去了,net-tool依然还在被广泛使用,最好还是两套命令都掌握吧。 -- ip addr -![](https://img-blog.csdnimg.cn/2019081823483347.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -显示这台机器上所有的网卡,大部分的网卡都有一个IP地址。 +想象你登录进入一个非常小的Linux系统,发现既没有_ifconfig_命令,也没有_ip addr_命令, + +是不是感觉这个系统没法用? + +可以自行安装net-tools和iproute2这两个工具 + +- 运行ip addr。不出意外,应该会输出下面的内容 +![](https://ask.qcloudimg.com/http-save/1752328/82jzge91jg.png) + +该命令显示这台机器上所有的网卡 + +大部分的网卡都有一个IP地址 + +**IP地址是一个网卡在网络世界的通讯地址,相当于我们现实世界的门牌号码** + +既然是门牌号,不能大家都一样,不然就会冲突,快递就找不到地方了 + +所以,有时候咱们的电脑弹出网络地址冲突,出现上不去网的情况,多半是IP地址冲突 + +如上输出的结果,192.168.10.208就是一个IP地址 + +地址被点分隔为四个部分,每个部分8bit,总共32位 + +这样产生的IP地址的数量很快就不够用了 + +于是就有了IPv6,也就是上面输出结果里面inet6 fe80::... -**IP地址是一个网卡在网络世界的通讯地址,相当于门牌号码** -既然是门牌号,不能大家都一样,不然就会冲突。所以,有时候咱们的电脑弹出网络地址冲突,出现上不去网的情况,多半是IP地址冲突。 +这个有128位,现在看来是够够的 -ip地址被点分隔为四个部分,每个部分8bit,总共32位。这样产生的IP地址的数量很快就不够用了。于是就有了IPv6,即inet6 fe80::... -这个有128位,现在看来是够的。 -## 1.2 分类 -32位IP地址被分为5类: -![](https://img-blog.csdnimg.cn/20190818235334304.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70)![](https://img-blog.csdnimg.cn/2021071018153487.png) -## 构成 -![](https://img-blog.csdnimg.cn/20210710183717791.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -A、B、 C类主要分为两部分: -- 前部分 - 网络号 -- 后部分 - 主机号 +本来32位的IP地址就不够,还被分成了5类。现在想想,当时分配地址的时候,真是太奢侈了 -可以理解成大家都是1单元1号,但我是Java小区的,而你是PHP小区的。 +![](https://ask.qcloudimg.com/http-save/1752328/ahm1fa9bqx.png) -C网广播地址一般为: 192.XXX.XXX.255 ( 比如:192.168.1.255 ) +在网络地址中,至少在当时设计的时候,对于A、B、 C类主要分两部分 -A、B、C三类地址所能包含的主机的数量: -![](https://img-blog.csdnimg.cn/2019081823542586.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- C类地址能包含的最大主机数量实在太少了,只有254个。当年设计恐怕也没想到,现在估计一个网咖都不够用。 -- B类地址能包含的最大主机数量又太多。6万多台机器放在一个网络下面,一般的企业基本达不到这个规模,闲着的地址就是浪费。 +- 前面一部分是网络号 +- 后面一部分是主机号 + +这很好理解,大家都是六单元1001号 + +我是小区A的六单元1001号 + +而你是小区B的六单元1001号。 + +- 下面这个表格,详细地展示了A、B、C三类地址所能包含的主机的数量 +![](https://ask.qcloudimg.com/http-save/1752328/iu04jhmf47.png) +这里面有个尴尬的事情,就是C类地址能包含的最大主机数量实在太少了,只有254个 +当时设计的时候恐怕没想到,现在估计一个网吧都不够用吧 +而B类地址能包含的最大主机数量又太多了。6万多台机器放在一个网络下面,一般的企业基本达不到这个规模,闲着的地址就是浪费。 # 2 无类型域间选路(CIDR) -这打破了原来设计的几类地址的做法,将32位的IP地址分为:网络号+主机号。 -10.100.122.2/24,这个IP地址中有一个斜杠,斜杠后面有个数字24,这种地址表示形式,就是**CIDR**。 -24表示32位中前24位是网络号,后8位是主机号。 +这打破了原来设计的几类地址的做法,将32位的IP地址一分为二 + +- 前面是网络号 +- 后面是主机号 + +10.100.122.2/24,这个IP地址中有一个斜杠,斜杠后面有个数字24 + +这种地址表示形式,就是**CIDR** + +后面24的意32位中前24是网络号,后8位是主机号 伴随着CIDR存在的 -- **广播地址** -10.100.122.255 若发送这个地址,则所有10.100.122网络里面的机器都可以收到 -- **子网掩码** + +- 一个是**广播地址** +10.100.122.255 如果发送这个地址,所有10.100.122网络里面的机器都可以收到 +- 另一个是**子网掩码** 255.255.255.0 -将子网掩码和IP地址进行AND计算,得到**网络号** +将子网掩码和IP地址进行AND计算,就可得到**网络号** + - 前面三个255,转成二进制都是1 -1和任何数值取AND,都是原来数值,前三个数不变:10.100.122 +1和任何数值取AND,都是原来数值,因而前三个数不变,为10.100.122 - 后面一个0,转换成二进制是0 0和任何数值取AND,都是0,因而最后一个数变为0,合起来就是10.100.122.0 # 3 公/私有IP地址 + 日常工作,几乎不用划分A类、B类或者C类,很多人就忘记了这个分类,只记得CIDR + 但是有一点还是要注意的,就是**公有IP地址和私有IP地址** -![](https://img-blog.csdnimg.cn/20190819233834172.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +![](https://ask.qcloudimg.com/http-save/1752328/rftycekj7l.png) + 上面的表格。表格最右列是私有IP地址段 + 平时看到的数据中心里,办公室/家/学校的IP地址,一般都是私有IP地址段 + 因为这些地址允许组织内部的IT人员自己管理、分配,而且可重复 + 因此,你学校的某个私有IP地址段和我学校的可以是一样的。 > 这就像每个小区有自己的楼编号和门牌号,你们小区可以叫6栋,我们小区也叫6栋,没有任何问题 -但是一旦出了小区,就需要使用公有IP地址。就像人民路888号,是国家统一分配的,不能两个小区都叫人民路888号。 +> 但是一旦出了小区,就需要使用公有IP地址。就像人民路888号,是国家统一分配的,不能两个小区都叫人民路888号。 **公有IP地址有个组织统一分配,你需要去买** + 如果你搭建一个网站,给你学校的人使用,让你们学校的IT人员给你一个IP地址就行 + 但是假如你要做一个类似网易163这样的网站,就需要有公有IP地址,这样全世界的人才能访问。 表格中的**192.168.0.x**是最常用的私有IP地址 + 你家里有Wi-Fi,对应就会有一个IP地址。一般你家里地上网设备不会超过256个,所以/24基本就够了 + 有时候我们也能见到/16的CIDR,这两种是最常见的,也是最容易理解的。 不需要将十进制转换为二进制32位,就能明显看出192.168.0是网络号,后面是主机号 + 而整个网络里面的第一个地址192.168.0.1,往往就是你这个私有网络的出口地址 + 例如,你家里的电脑连接Wi-Fi,Wi-Fi路由器的地址就是192.168.0.1 -![](https://img-blog.csdnimg.cn/20190819235121180.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70)而192.168.0.255就是广播地址。一旦发送这个地址,整个192.168.0网络里面的所有机器都能收到。 + +![](https://ask.qcloudimg.com/http-save/1752328/n3r1rqg895.png)而192.168.0.255就是广播地址。一旦发送这个地址,整个192.168.0网络里面的所有机器都能收到。 但是也不总都是这样的情况。因此,其他情况往往就会很难理解,还容易出错。 -# 4 CIDR可不好算 -## 16.158.165.91/22 -这个CIDR,求该网络的第一个地址、子网掩码和广播地址。 -首先声明16.158.165.1是错的! +# 4 一个容易“犯错”的CIDR -/22不是8的整数倍,只能先变成二进制。 -- 16.158的部分不会动,占了前16位 -- 中间的165,二进制为‭10100101‬。除了前面的16位,还剩6位。所以,这8位中前6位是网络号,16.158.<101001>,而<01>.91是机器号。 +我们来看16.158.165.91/22这个CIDR -第一个地址是16.158.<101001><00>.1,即16.158.164.1 -子网掩码是255.255.<111111><00>.0,即255.255.252.0 -广播地址为16.158.<101001><11>.255,即16.158.167.255。 +求一下这个网络的第一个地址、子网掩码和广播地址 -## IP : 192.168.124.7 -子网掩码:255.255.255.192 -网络地址:192.168.124.0 -广播地址:192.168.124.63 +你要是上来就写16.158.165.1,那就大错特错!!! -```bash -255.255.255.192=》11111111.11111111.11111111.11000000 -所以可划分网段:2^2=4个: -0~63 192.168.124.7属于该网段 -64~127 -128~191 -192~255 -``` -所以广播地址为:192.168.124.63。所以广播地址最后一位不一定是 255。 +/22不是8的整数倍,不好办,只能先变成二进制来看 -## 广播通信问题 -主机一:192.168.124.7 子网掩码 : 255.255.255.192 -主机二: 192.168.124.100 子网掩码: 255.255.255.192 +- 16.158的部分不会动,它占了前16位 +- 中间的165,变为二进制为‭10100101‬。除了前面的16位,还剩6位。所以,这8位中前6位是网络号,16.158.<101001>,而<01>.91是机器号。 -主机一广播地址: 192.168.124.63 -主机二广播地址: 192.168.124.127 -所以无法互相通信。 +第一个地址是16.158.<101001><00>.1,即16.158.164.1 +子网掩码是255.255.<111111><00>.0,即255.255.252.0 + +广播地址为16.158.<101001><11>.255,即16.158.167.255。 D类是**组播地址** + 使用这一类地址,属于某个组的机器都能收到 + 这有点类似在公司里面大家都加入了一个邮件组。发送邮件,加入这个组的都能收到 在IP地址的后面有个scope + - 对于eth0这张网卡来讲,是global,说明这张网卡是可以对外的,可以接收来自各个地方的包 - 对于lo来讲,是host,说明仅可以供本机相互通信。 lo全称是loopback,又称**环回接口**,往往会被分配到127.0.0.1这个地址 这个地址用于本机通信,经过内核处理后直接返回,不会在任何网络中出现。 # 5 MAC地址 + 在IP地址的上一行是link/ether fa:16:3e:c7:79:75 brd ff:ff:ff:ff:ff:ff + 这个被称为**MAC地址** + 是一个网卡的物理地址,用十六进制,6个byte表示 MAC地址号称全局唯一,不会有两个网卡有相同的MAC地址,而且网卡自生产出来,就带着这个地址 + 很多人看到这里就会想,既然这样,整个互联网的通信,全部用MAC地址好了,只要知道了对方的MAC地址,就可以把信息传过去。 这样当然是不行的 + **`一个网络包要从一个地方传到另一个地方,除了要有确定的地址,还需要有定位功能`** + 而有门牌号码属性的`IP地址,才是有远程定位功能` > 例如,你去XX市XX路XX号X楼X层找XX,你在路上问路,可能被问的人不知道X楼是哪个,但是可以给你指网商路怎么去 > 但是如果你问一个人,你知道这个身份证号的人在哪里吗?可想而知,没有人知道。 `MAC地址更像是身份证,是一个唯一的标识` + 它的唯一性设计是为了组网的时候,不同的网卡放在一个网络里面的时候,可以不用担心冲突 + 从硬件角度,保证不同的网卡有不同的标识。 MAC地址是有一定定位功能的,只不过范围非常有限 + > 你可以根据IP地址,找到XX市XX路XX号X楼X层,但是依然找不到我,你就可以靠吼了,大声喊身份证XXXX的是哪位?我听到了,我就会站起来说,是我啊 -但是如果你在上海,到处喊身份证XXXX的是哪位,我不在现场,当然不会回答,因为我在杭州不在上海。 +> 但是如果你在上海,到处喊身份证XXXX的是哪位,我不在现场,当然不会回答,因为我在杭州不在上海。 所以,MAC地址的通信范围比较小,局限在一个子网里面 + 例如,从192.168.0.2/24访问192.168.0.3/24是可以用MAC地址的 + 一旦跨子网,即从192.168.0.2/24到192.168.1.2/24,MAC地址就不行了,需要IP地址起作用了 # 6 网络设备的状态标识 -***``*** 叫**net_device flags,网络设备的状态标识** + +_**``**_ 叫**net\_device flags,网络设备的状态标识** - UP 网卡处于启动的状态 @@ -169,26 +221,36 @@ MAC地址是有一定定位功能的,只不过范围非常有限 网卡有广播地址,可以发送广播包 - MULTICAST 网卡可以发送多播包 -- LOWER_UP +- LOWER\_UP L1是启动的,也即网线插着呢 - MTU1500 最大传输单元MTU为1500,这是以太网的默认值。 网络包是层层封装的 + MTU是二层MAC层的概念。MAC层有MAC的头,以太网规定连MAC头带正文合起来,不允许超过1500个字节。正文里面有IP的头、TCP的头、HTTP的头。如果放不下,就需要分片来传输。 -- qdisc pfifo_fast (queueing discipline,排队规则) +- qdisc pfifo\_fast (queueing discipline,排队规则) 内核如果需要通过某个网络接口发送数据包,它都需要按照为这个接口配置的qdisc(排队规则)把数据包加入队列。 最简单的qdisc是pfifo,它不对进入的数据包做任何的处理,数据包采用先入先出的方式通过队列 -pfifo_fast稍微复杂一些,它的队列包括三个波段(band)。在每个波段里面,使用先进先出规则。 + +pfifo\_fast稍微复杂一些,它的队列包括三个波段(band)。在每个波段里面,使用先进先出规则。 三个波段(band)的优先级也不相同。band 0的优先级最高,band 2的最低。如果band 0里面有数据包,系统就不会处理band 1里面的数据包,band 1和band 2之间也是一样。 数据包是按照**服务类型(Type of Service,TOS)** 被分配到三个波段(band)里面的 + TOS是IP头里面的一个字段,代表了当前的包是高优先级的,还是低优先级的。 +队列是个好东西,后面我们讲云计算中的网络的时候,会有很多用户共享一个网络出口的情况,这个时候如何排队,每个队列有多粗,队列处理速度应该怎么提升,我都会详细为你讲解。 + # 7 总结 + - IP是地址,有定位功能;MAC是身份证,无定位功能 - CIDR可以用来判断是不是本地人 -- IP分公有IP、私有IP \ No newline at end of file +- IP分公有的IP和私有的IP + +# 参考 + +- 趣谈网络协议 \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\343\200\220\345\233\276\350\247\243\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\343\200\221\347\211\251\347\220\206\345\261\202\345\222\214MAC\345\261\202\347\232\204\344\272\262\345\257\206\345\205\263\347\263\273.md" "b/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\343\200\220\345\233\276\350\247\243\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\343\200\221\347\211\251\347\220\206\345\261\202\345\222\214MAC\345\261\202\347\232\204\344\272\262\345\257\206\345\205\263\347\263\273.md" deleted file mode 100644 index aa0a5252be..0000000000 --- "a/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\351\207\215\345\255\246\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\343\200\220\345\233\276\350\247\243\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\343\200\221\347\211\251\347\220\206\345\261\202\345\222\214MAC\345\261\202\347\232\204\344\272\262\345\257\206\345\205\263\347\263\273.md" +++ /dev/null @@ -1,63 +0,0 @@ -机器有了IP,就能在网络和其他机器通信。 -# 物理层 -电脑连电脑时,需要配置这俩电脑的IP地址、子网掩码和默认网关。要想两台电脑能够通信,这三项必须配置成为一个网络,可以一个是192.168.0.1/24,另一个是192.168.0.2/24,否则不通。 - -两台电脑间的网络包,包含MAC层。IP层要封装了MAC层才能将包放入物理层。 - -至此两台电脑构成最小的局域网 - LAN。 - -怎么把三台电脑连在一起? -以前有Hub - 集线器。这种设备有多个口,可以连接多台电脑。不同于交换机,集线器没有大脑,完全工作在物理层。它会将自己收到的每一个字节,都复制到其他端口。这是物理层联通的方案。 - -# 数据链路层 -Hub采取广播模式,若每一台电脑发出的包,局域网内每个电脑都能收到,那就麻烦了。必须解决如下问题(MAC层要解决的): -## 包发给谁?谁接收? -这里用到一个物理地址 - **链路层地址**。但因该层主要解决媒体接入控制,所以常被称为**MAC地址**。 - -解决这个问题牵扯该层的网络包格式。 -比如以太网,该层的最开始,就是目标MAC地址、源MAC地址。 -![](https://img-blog.csdnimg.cn/20210622182256653.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -大部分的类型是IP数据包,然后IP里面包含TCP、UDP,以及HTTP等。 - -有了目标MAC地址,数据包在链路上广播,MAC的网卡才能发现,这个包是给它的。MAC的网卡接收这个包,然后打开IP包,发现IP地址也是自己的,再打开TCP包,发现端口是自己,也就是80,而nginx就是监听80。 - -于是将请求提交给nginx,nginx返回一个网页。然后将网页需要发回请求的机器。然后层层封装,最后到MAC层。因为来时有源MAC地址,返回时,源MAC就变成目标MAC,返给请求的机器。 -## 有无发送顺序? -MAC,Medium Access Control,媒体访问控制。就是控制在往媒体上发数据时,谁先发、谁后发。这个规则称为**多路访问**。比如如下方案: -- 多车道 -每个车一个车道,你走你的,我走我的。这叫信道划分 -- 今天单号出行,明天双号出行 -这叫轮流协议 -- 不管啥事,有事儿先出门,发现特堵,就回去。错过高峰再出 -这叫随机接入协议,以太网就是这种。 - -这就解决了媒体接入控制的问题。MAC层就是用来解决多路访问的堵车问题的。 -## 发送时出错,咋办? -对于以太网,该层的最后是CRC,计算整个包是否在发送过程出错。 - -当源机器知道目标机器,可将目标地址放入包,若不知道呢? -一个广播的网络里面接入了N台机器,如何知道每个MAC地址是谁?即已知IP地址,求MAC地址的协议。 -![](https://img-blog.csdnimg.cn/20210622232702973.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -即:在一个局域网里,当知道了IP地址,不知道MAC咋办。 -![](https://img-blog.csdnimg.cn/20210622232924976.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -发送一个广播包,谁是这个IP谁来回答。具体询问和回答的报文就像下面这样: -![](https://img-blog.csdnimg.cn/202106222330175.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -为避免每次都用ARP请求,机器本地也会进行ARP缓存。机器会不断上线下线,IP也可能会变,所以ARP的MAC地址缓存过一段时间会过期。 - -# 交换机 -Hub是广播的,不管某个接口是否需要,所有Bit都会被发出去,然后让主机自行判断是否需要。 -这种方式,当路上车少时没问题,但车一多,产生冲突概率就高了。把不需要的包转发过去,也属于浪费。看来Hub这种一股脑转发的设备是不行的,需要更智能的。因为每个口都只连接一台电脑,这台电脑又不怎么变更IP和MAC地址,只需记住这台电脑的MAC地址,若目标MAC地址不是这台电脑,这口就不用转发了。 - -所以需要知道目标MAC地址是否就是连接某个口的电脑的MAC地址。这就要一个能把MAC头拿下来,检查目标MAC地址,然后根据策略转发的设备 - 交换机。 - -#### 交换机怎么知道每个口的电脑的MAC地址? -交换机会学习。 - -MAC1电脑将一个包发送给MAC2电脑,当这个包到达交换机,一开始交换机也不知道MAC2电脑在哪个口,它只能将包转发给除了来的那个口之外的其他所有的口。 -但这时,交换机会记住,MAC1是来自一个明确的口。以后有包的目的地址是MAC1的,直接发送到这个口。 - -当交换机作为一个关卡一样,过了一段时间之后,就有了整个网络结构。这时,基本不用广播了,全部可准确转发。 -每个机器的IP地址会变,所在口也会变,所以交换机的学习结果,称为转发表,有过期时间。 \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/JavaEE/\346\234\215\345\212\241\345\256\232\344\275\215\345\231\250\346\250\241\345\274\217\357\274\210Service Locator Pattern\357\274\211.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/JavaEE/\346\234\215\345\212\241\345\256\232\344\275\215\345\231\250\346\250\241\345\274\217\357\274\210Service Locator Pattern\357\274\211.md" similarity index 100% rename from "\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/JavaEE/\346\234\215\345\212\241\345\256\232\344\275\215\345\231\250\346\250\241\345\274\217\357\274\210Service Locator Pattern\357\274\211.md" rename to "\350\256\276\350\256\241\346\250\241\345\274\217/JavaEE/\346\234\215\345\212\241\345\256\232\344\275\215\345\231\250\346\250\241\345\274\217\357\274\210Service Locator Pattern\357\274\211.md" diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\344\270\255\344\273\213\350\200\205\346\250\241\345\274\217.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\344\270\255\344\273\213\350\200\205\346\250\241\345\274\217.md" similarity index 100% rename from "\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\344\270\255\344\273\213\350\200\205\346\250\241\345\274\217.md" rename to "\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\344\270\255\344\273\213\350\200\205\346\250\241\345\274\217.md" diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\216\237\345\236\213\346\250\241\345\274\217.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\216\237\345\236\213\346\250\241\345\274\217.md" similarity index 100% rename from "\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\216\237\345\236\213\346\250\241\345\274\217.md" rename to "\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\216\237\345\236\213\346\250\241\345\274\217.md" diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\221\275\344\273\244\346\250\241\345\274\217.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\221\275\344\273\244\346\250\241\345\274\217.md" similarity index 100% rename from "\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\221\275\344\273\244\346\250\241\345\274\217.md" rename to "\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\221\275\344\273\244\346\250\241\345\274\217.md" diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\244\207\345\277\230\345\275\225\346\250\241\345\274\217.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\244\207\345\277\230\345\275\225\346\250\241\345\274\217.md" similarity index 100% rename from "\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\244\207\345\277\230\345\275\225\346\250\241\345\274\217.md" rename to "\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\244\207\345\277\230\345\275\225\346\250\241\345\274\217.md" diff --git "a/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\267\245\345\216\202\346\226\271\346\263\225\346\250\241\345\274\217.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\267\245\345\216\202\346\226\271\346\263\225\346\250\241\345\274\217.md" new file mode 100644 index 0000000000..295400962c --- /dev/null +++ "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\267\245\345\216\202\346\226\271\346\263\225\346\250\241\345\274\217.md" @@ -0,0 +1,236 @@ +- 所牵涉源代码地址 +https://github.com/Wasabi1234/design-patterns +# 0 简单工厂案例 +![](https://upload-images.jianshu.io/upload_images/4685968-7a2b52fa68ffee5e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-1814ce7ea3bed285.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-3353ca443c842e0c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-67b48971f4020fe8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-c9424893b63f6f45.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-da57e6568f523cd3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## JDK 应用实例 +### 日历类 +![](https://upload-images.jianshu.io/upload_images/4685968-6d0189ad2ca154b0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-50f2491fe7a2ac75.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-1cf0e5cd50176c08.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +### 迭代器 +Collection 接口就相当于 VideoFactory +![](https://upload-images.jianshu.io/upload_images/4685968-7f01c2df10f7b309.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +相当于各种具体的工厂,如 JavaVideoFactory +![父类接口,子类实现](https://upload-images.jianshu.io/upload_images/4685968-c1969f53a9e8f9d2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +Itr 就是具体产品 JavaVideo +![](https://upload-images.jianshu.io/upload_images/4685968-a737909bb84e4167.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +### 工厂应用 +#### 为解决 url 协议扩展使用 +![](https://upload-images.jianshu.io/upload_images/4685968-b924b06429f554ed.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![Launcher#Factory静态类](https://upload-images.jianshu.io/upload_images/4685968-4e9beb0c810025a8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +#### logback 应用 +![LoggerFactory#getLogger(String name)](https://upload-images.jianshu.io/upload_images/4685968-4033df2ab32a341d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-a5dfaa56b86b1b32.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-685e3cfb144801e9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## JDBC实例 +![直接注册 MySQL 驱动](https://upload-images.jianshu.io/upload_images/4685968-4a5d57ae6416a5af.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-d311bc6304e17ef5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-def56f6f534fa472.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-d07bf98d6b57c227.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +返回值是一个抽象类,必有一子类实现其,看一下 +![](https://upload-images.jianshu.io/upload_images/4685968-b3580b7d4d7180b5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-e36d91e68ff08352.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![通过间接继承此处理器](https://upload-images.jianshu.io/upload_images/4685968-4a27bb9c3b3ed64b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-d2a104c4bc566b56.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-3ec72ed07f9f2d4e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +这其中URLStreamHandler就相当于各种抽象产品,而其实现类即各种具体的产品 +URLStreamHandlerFactory就相当于 VideoFactory +而如下 Factory 就相当于如 JavaVideoFactory/PythonVideoFactory +![](https://upload-images.jianshu.io/upload_images/4685968-e0d2ec52f3edb023.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## Logback实例 +![](https://upload-images.jianshu.io/upload_images/4685968-056b7e3e6eebb1c8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-888e10bb2d071322.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-fb5fcd3912e4f649.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +# 1 工厂方法模式案例 +## 1.1 简单工厂的升级 +![](https://upload-images.jianshu.io/upload_images/4685968-1e3130ff50160485.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +![](https://upload-images.jianshu.io/upload_images/4685968-2edda92bd092d600.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-0751b26d4d587b5d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-2aa397d86f68fe93.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-aa45708a5fd3aa38.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +对造人过程进行分析,该过程涉及三个对象:女娲、八卦炉、三种不同肤色的人 +- 女娲可以使用场景类`Client`表示 +- 八卦炉类似于一个工厂,负责制造生产产品(即人类) +- 三种不同肤色的人,他们都是同一个接口下的不同实现类,对于八卦炉来说都是它生产出的产品 +![女娲造人类图](https://upload-images.jianshu.io/upload_images/4685968-23b729aba2523fee.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 接口Human是对人类的总称,每个人种都至少具有两个方法 +![](https://upload-images.jianshu.io/upload_images/4685968-d1b24b0cee720eb7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 黑色人种 +![](https://upload-images.jianshu.io/upload_images/4685968-5124fe0aaebb211f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 黄色人种 +![](https://upload-images.jianshu.io/upload_images/4685968-5dc7946d579ffb56.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 白色人种 +![](https://upload-images.jianshu.io/upload_images/4685968-8b2d955ebefc1471.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +所有人种定义完毕,下一步就是定义一个八卦炉,然后烧制人类 + +最可能给八卦炉下达什么样的生产命令呢? +应该是`给我生产出一个黄色人种(YellowHuman类)` +而不会是`给我生产一个会走、会跑、会说话、皮肤是黄色的人种` +因为这样的命令增加了交流的成本,作为一个生产的管理者,只要知道生产什么就可以了,而不需要事物的具体信息 +![](https://upload-images.jianshu.io/upload_images/4685968-bb006308572d3439.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +在这里采用了泛型,通过定义泛型对createHuman的输入参数产生两层限制 +● 必须是Class类型 +● 必须是Human的实现类 +其中的`T`表示,只要实现了Human接口的类都可以作为参数 + +只有一个八卦炉,其实现生产人类的方法 +![](https://upload-images.jianshu.io/upload_images/4685968-eb7a7b7669772452.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +人种有了,八卦炉也有了,剩下的工作就是女娲采集黄土,然后命令八卦炉开始生产 +![](https://upload-images.jianshu.io/upload_images/4685968-1756e8409b6f5598.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +人种有了,八卦炉有了,负责生产的女娲也有了 +运行一下,结果如下所示 +![](https://upload-images.jianshu.io/upload_images/4685968-0e911383ba8416d9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +以上就是工厂方法模式 +# 2 定义 +![](https://upload-images.jianshu.io/upload_images/4685968-728bf0b4fe33b89c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 官方定义 +Define an interface for creating an object,but let subclasses decide which class to instantiate.Factory Method lets a class defer instantiation to subclasses +定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类 +![](https://upload-images.jianshu.io/upload_images/4685968-132c8ab8cb8d1302.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +![工厂方法模式的通用类图](http://upload-images.jianshu.io/upload_images/4685968-8e20457c9b86f71b.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +在工厂方法模式中,抽象产品类Product负责定义产品的共性,实现对事物最抽象的定义;Creator为抽象创建类,也就是抽象工厂,具体如何创建产品类是由具体的实现工厂ConcreteCreator完成的。工厂方法模式的变种较多,我们来看一个比较实用的通用源码。 +- 抽象产品类![](https://upload-images.jianshu.io/upload_images/4685968-e19c2599d115daaa.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +具体的产品类可以有多个,都继承于抽象产品类 +- 具体产品类 +![](https://upload-images.jianshu.io/upload_images/4685968-be2036255104404a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-44979f2d21bfa26a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 抽象工厂类 +负责定义产品对象的产生 +![](https://upload-images.jianshu.io/upload_images/4685968-9b6549b0faae91c4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 具体工厂类 +具体如何产生一个产品的对象,是由具体的工厂类实现的 +![](https://upload-images.jianshu.io/upload_images/4685968-b60b98650513095a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 场景类 +![](https://upload-images.jianshu.io/upload_images/4685968-b479b632eeeb1c02.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-607a2c562ed1a2d0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +该通用代码是一个比较实用、易扩展的框架,读者可以根据实际项目需要进行扩展 +# 3 应用 +## 3.1 优点 +![](https://upload-images.jianshu.io/upload_images/4685968-83c08c2a4f6d6724.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-279640d183d64bcd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 良好的封装性,代码结构清晰 +一个对象创建是有条件约束的,如一个调用者需要一个具体的产品对象,只要知道这个产品的类名(或约束字符串)就可以了,不用知道创建对象的艰辛过程,降低模块间的耦合 +- 工厂方法模式的扩展性非常优秀 +在增加产品类的情况下,只要适当地修改具体的工厂类或扩展一个工厂类,就可以完成“拥抱变化” +例如在我们的例子中,需要增加一个棕色人种,则只需要增加一个BrownHuman类,工厂类不用任何修改就可完成系统扩展。 +- 屏蔽产品类 +这一特点非常重要,产品类的实现如何变化,调用者都不需要关心,它只需要关心产品的接口,只要接口保持不变,系统中的上层模块就不要发生变化 +因为产品类的实例化工作是由工厂类负责的,`一个产品对象具体由哪一个产品生成是由工厂类决定的` +在数据库开发中,大家应该能够深刻体会到工厂方法模式的好处:如果使用JDBC连接数据库,数据库从MySQL切换到Oracle,需要改动的地方就是切换一下驱动名称(前提条件是SQL语句是标准语句),其他的都不需要修改,这是工厂方法模式灵活性的一个直接案例。 +- 典型的解耦框架 +高层模块值需要知道产品的抽象类,其他的实现类都不用关心 +符合迪米特法则,我不需要的就不要去交流 +也符合依赖倒置原则,只依赖产品类的抽象 +当然也符合里氏替换原则,使用产品子类替换产品父类,没问题! +## 3.2 缺点 +![](https://upload-images.jianshu.io/upload_images/4685968-3562169226a921ab.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-6c486bc9daa9ab9c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## 3.3 适用场景 +![](https://upload-images.jianshu.io/upload_images/4685968-2892cbc52a9be8ca.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-1201cd113bfaeb44.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +### 工厂方法模式是new一个对象的替代品 +在所有需要生成对象的地方都可以使用,但是需要慎重地考虑是否要增加一个工厂类进行管理,增加代码的复杂度 +### 需要灵活的、可扩展的框架时 +万物皆对象,那万物也就皆产品类 +例如需要设计一个连接邮件服务器的框架,有三种网络协议可供选择:POP3、IMAP、HTTP +我们就可以把这三种连接方法作为产品类,定义一个接口如`IConnectMail` +然后定义对邮件的操作方法 +用不同的方法实现三个具体的产品类(也就是连接方式) +再定义一个工厂方法,按照不同的传入条件,选择不同的连接方式 +如此设计,可以做到完美的扩展,如某些邮件服务器提供了WebService接口,很好,我们只要增加一个产品类就可以了 +### 异构项目 +例如通过WebService与一个非Java的项目交互,虽然WebService号称是可以做到异构系统的同构化,但是在实际的开发中,还是会碰到很多问题,如类型问题、WSDL文件的支持问题,等等。从WSDL中产生的对象都认为是一个产品,然后由一个具体的工厂类进行管理,减少与外围系统的耦合。 +### 使用在测试驱动开发的框架下 +例如,测试一个类A,就需要把与类A有关联关系的类B也同时产生出来,我们可以使用工厂方法模式把类B虚拟出来,避免类A与类B的耦合。目前由于JMock和EasyMock的诞生,该使用场景已经弱化了,读者可以在遇到此种情况时直接考虑使用JMock或EasyMock +# 4 扩展 +工厂方法模式有很多扩展,而且与其他模式结合使用威力更大,下面将介绍4种扩展。 +## 4.1 缩小为简单工厂模式 +我们这样考虑一个问题:一个模块仅需要一个工厂类,没有必要把它产生出来,使用静态的方法就可以了,根据这一要求,我们把上例中的`AbstarctHumanFactory`修改一下 +![简单工厂模式类图](http://upload-images.jianshu.io/upload_images/4685968-72f86b5e992c5237.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +我们在类图中去掉了`AbstractHumanFactory`抽象类,同时把`createHuman`方法设置为静态类型,简化了类的创建过程,变更的源码仅仅是HumanFactory和NvWa类 + +- 简单工厂模式中的工厂类 +![待考证](https://upload-images.jianshu.io/upload_images/4685968-4c9740ec9baec87a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +HumanFactory类仅有两个地方发生变化 +- 去掉继承抽象类 +- 在`createHuman`前增加static关键字 + +工厂类发生变化,也同时引起了调用者NvWa的变化 + ![简单工厂模式中的场景类](https://upload-images.jianshu.io/upload_images/4685968-5765ddc36a0bdc84.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +运行结果没有发生变化,但是我们的类图变简单了,而且调用者也比较简单,该模式是工厂方法模式的弱化,因为简单,所以称为`简单工厂模式`(Simple Factory Pattern),也叫做`静态工厂模式` +在实际项目中,采用该方法的案例还是比较多的 +- 其缺点 +工厂类的扩展比较困难,不符合开闭原则,但它仍然是一个非常实用的设计模式。 +## 4.2 升级为多个工厂类 +当我们在做一个比较复杂的项目时,经常会遇到初始化一个对象很耗费精力的情况,所有的产品类都放到一个工厂方法中进行初始化会使代码结构不清晰 +例如,一个产品类有5个具体实现,每个实现类的初始化(不仅仅是new,初始化包括new一个对象,并对对象设置一定的初始值)方法都不相同,如果写在一个工厂方法中,势必会导致该方法巨大无比,那该怎么办? + +考虑到需要结构清晰,我们就为每个产品定义一个创造者,然后由调用者自己去选择与哪个工厂方法关联 +我们还是以女娲造人为例,每个人种都有一个固定的八卦炉,分别造出黑色人种、白色人种、黄色人种 +![多个工厂类的类图](http://upload-images.jianshu.io/upload_images/4685968-79af47234aac2999.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 每个人种(具体的产品类)都对应了一个创建者,每个创建者都独立负责创建对应的产品对象,非常符合单一职责原则,看看代码变化 +![多工厂模式的抽象工厂类](https://upload-images.jianshu.io/upload_images/4685968-03006ceca2c8f242.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +抽象方法中已经不再需要传递相关参数了,因为每一个具体的工厂都已经非常明确自己的职责:创建自己负责的产品类对象。 + +- 黑色人种的创建工厂实现 +![](https://upload-images.jianshu.io/upload_images/4685968-504d24acb4e0b24d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 黄色人种的创建类 +![](https://upload-images.jianshu.io/upload_images/4685968-e523b54e2cbae3a4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 白色人种的创建类 +![](https://upload-images.jianshu.io/upload_images/4685968-ef6f65ec05c16cf7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +三个具体的创建工厂都非常简单,但是,如果一个系统比较复杂时工厂类也会相应地变复杂。 +- 场景类NvWa修改后的代码 +![](https://upload-images.jianshu.io/upload_images/4685968-ad52150fd79c52c0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +运行结果还是相同 +每一个产品类都对应了一个创建类,好处就是创建类的职责清晰,而且结构简单,但是给可扩展性和可维护性带来了一定的影响。为什么这么说呢?如果要扩展一个产品类,就需要建立一个相应的工厂类,这样就增加了扩展的难度。因为工厂类和产品类的数量相同,维护时需要考虑两个对象之间的关系。 + +当然,在复杂的应用中一般采用多工厂的方法,然后再增加一个协调类,避免调用者与各个子工厂交流,协调类的作用是封装子工厂类,对高层模块提供统一的访问接口。 +## 4.3 替代单例模式 +单例模式的核心要求就是`在内存中只有一个对象`,通过工厂方法模式也可以只在内存中生产一个对象 +![工厂方法模式替代单例模式类图](http://upload-images.jianshu.io/upload_images/4685968-6ee750bc612b6d6f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +Singleton定义了一个private的无参构造函数,目的是不允许通过new的方式创建一个对象 +![单例类](https://upload-images.jianshu.io/upload_images/4685968-fcdbcb35b5e7805e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +Singleton保证不能通过正常的渠道建立一个对象 + +- 那SingletonFactory如何建立一个单例对象呢? +反射~ +![负责生成单例的工厂类](https://upload-images.jianshu.io/upload_images/4685968-2a0728c6b43e3cdf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +通过获得类构造器,然后设置private访问权限,生成一个对象,然后提供外部访问,保证内存中的对象唯一 + +以上通过工厂方法模式创建了一个单例对象,该框架可以继续扩展,在一个项目中可以产生一个单例构造器,所有需要产生单例的类都遵循一定的规则(构造方法是private),然后通过扩展该框架,只要输入一个类型就可以获得唯一的一个实例。 +## 3.4 延迟初始化(Lazy initialization) +一个对象被消费完毕后,并不立刻释放,工厂类保持其初始状态,等待再次被使用 +延迟初始化是工厂方法模式的一个扩展应用 +![延迟初始化的通用类图 +](http://upload-images.jianshu.io/upload_images/4685968-dde2820312878278.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +ProductFactory负责产品类对象的创建工作,并且通过prMap变量产生一个缓存,对需要再次被重用的对象保留 +![延迟加载的工厂类](https://upload-images.jianshu.io/upload_images/4685968-faeaf3df679a26c5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +通过定义一个Map容器,容纳所有产生的对象,如果在Map容器中已经有的对象,则直接取出返回;如果没有,则根据需要的类型产生一个对象并放入到Map容器中,以方便下次调用。 + +延迟加载框架是可以扩展的,例如限制某一个产品类的最大实例化数量,可以通过判断Map中已有的对象数量来实现,这样的处理是非常有意义的,例如JDBC连接数据库,都会要求设置一个MaxConnections最大连接数量,该数量就是内存中最大实例化的数量。 + +延迟加载还可以用在对象初始化比较复杂的情况下,例如硬件访问,涉及多方面的交互,则可以通过延迟加载降低对象的产生和销毁带来的复杂性。 +# 4 最佳实践 +工厂方法模式在项目中使用得非常频繁,以至于很多代码中都包含工厂方法模式 +该模式几乎尽人皆知,但不是每个人都能用得好。熟能生巧,熟练掌握该模式,多思考工厂方法如何应用,而且工厂方法模式还可以与其他模式混合使用(例如模板方法模式、单例模式、原型模式等),变化出无穷的优秀设计,这也正是软件设计和开发的乐趣所在。 diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\273\272\351\200\240\350\200\205\346\250\241\345\274\217.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\273\272\351\200\240\350\200\205\346\250\241\345\274\217.md" similarity index 100% rename from "\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\273\272\351\200\240\350\200\205\346\250\241\345\274\217.md" rename to "\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\345\273\272\351\200\240\350\200\205\346\250\241\345\274\217.md" diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\346\212\275\350\261\241\345\267\245\345\216\202\346\250\241\345\274\217\357\274\210Abstract-Factory-Pattern\357\274\211.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\346\212\275\350\261\241\345\267\245\345\216\202\346\250\241\345\274\217\357\274\210Abstract-Factory-Pattern\357\274\211.md" similarity index 100% rename from "\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\346\212\275\350\261\241\345\267\245\345\216\202\346\250\241\345\274\217\357\274\210Abstract-Factory-Pattern\357\274\211.md" rename to "\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\346\212\275\350\261\241\345\267\245\345\216\202\346\250\241\345\274\217\357\274\210Abstract-Factory-Pattern\357\274\211.md" diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\347\255\226\347\225\245\346\250\241\345\274\217\357\274\210Strategy-Pattern\357\274\211.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\347\255\226\347\225\245\346\250\241\345\274\217\357\274\210Strategy-Pattern\357\274\211.md" similarity index 100% rename from "\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\347\255\226\347\225\245\346\250\241\345\274\217\357\274\210Strategy-Pattern\357\274\211.md" rename to "\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\347\255\226\347\225\245\346\250\241\345\274\217\357\274\210Strategy-Pattern\357\274\211.md" diff --git "a/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" new file mode 100644 index 0000000000..88fae975df --- /dev/null +++ "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" @@ -0,0 +1,31 @@ +# 定义与类型 +![](https://upload-images.jianshu.io/upload_images/4685968-5a9576189c8e5e1a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 适用场景 +![](https://upload-images.jianshu.io/upload_images/4685968-223bb37e8742148d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 优点 +![](https://upload-images.jianshu.io/upload_images/4685968-26e681be14b48d5a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 缺点 +![](https://upload-images.jianshu.io/upload_images/4685968-4b77b933afb1074f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# coding +![](https://upload-images.jianshu.io/upload_images/4685968-ed888f5d321b034a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-4b1046fb905f15e0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-fb6d0ff24fd80c73.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-765ddd9d7be6e8f1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-2378eaba412ee8c5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-ae51d4db6d247db7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-87dd65c810ed8420.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-ea740980ef1ab583.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-793412716d22062c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-e31e3ccc7e5e86a7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +接下来,来到观察者- `Teacher`的代码区中 +![](https://upload-images.jianshu.io/upload_images/4685968-2705098e1cadd417.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 源码应用 +![JDK应用](https://upload-images.jianshu.io/upload_images/4685968-3b2ca4ec326660c4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## Guava +![](https://upload-images.jianshu.io/upload_images/4685968-99bc1215f23ac110.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-95cdf8cad0660f10.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![注册,即添加观察者](https://upload-images.jianshu.io/upload_images/4685968-03e5008cacf284ab.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-a2a768f37c0748cd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-c41574e7e3b2144d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![移除观察者](https://upload-images.jianshu.io/upload_images/4685968-235da3b8d7cef292.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-d95b863446e24048.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\350\247\243\351\207\212\345\231\250\346\250\241\345\274\217\357\274\210Interpreter-Pattern\357\274\211.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\350\247\243\351\207\212\345\231\250\346\250\241\345\274\217\357\274\210Interpreter-Pattern\357\274\211.md" similarity index 100% rename from "\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\350\247\243\351\207\212\345\231\250\346\250\241\345\274\217\357\274\210Interpreter-Pattern\357\274\211.md" rename to "\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\350\247\243\351\207\212\345\231\250\346\250\241\345\274\217\357\274\210Interpreter-Pattern\357\274\211.md" diff --git "a/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md" new file mode 100644 index 0000000000..ea724ca639 --- /dev/null +++ "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230---\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md" @@ -0,0 +1,57 @@ +# 1 定义与类型 +## 1.1 定义 +- 维基 +它包含了一些命令对象和一系列的处理对象; +每一个处理对象决定它能处理哪些命令对象,它也知道如何将它不能处理的命令对象传递给该链中的下一个处理对象. +该模式还描述了往该处理链的末尾添加新的处理对象的方法. + +- 精简定义 +为请求创建一个接收此次请求对象的链. + +## 1.2 类型 +行为型 + +# 2 适用场景 +一个请求的处理需要多个对象当中的一个或几个协作处理 +当然也包括需要全部的情况 + +# 3 优点 +请求的发送者和接收者(请求的处理)解耦 +责任链可以动态组合 + + +# 4 缺点 +- 责任链太长或者处理时间过长,影响性能 +- 责任链有可能过多 + +5 # 相关设计模式 +~和状态模式 +- 各个对象并不指定下一个所要处理的对象者是谁,只有在客户端类设置链顺序及元素,知道被某个责任链处理或者整条链结束. +- 每个状态知道自己下一个所要处理的对象者是谁,即在编译时确定 + +# 6 实战 +- 相关类 +![](https://upload-images.jianshu.io/upload_images/4685968-5f61ab5ed1db8c31.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-b4ac47bf8102362c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-3885c81df1ebbae4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-a34d00e17af71a0a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-b950249ca12d3c7e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- UML图 +![](https://upload-images.jianshu.io/upload_images/4685968-ab22fc917646f57a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 测试类 +![](https://upload-images.jianshu.io/upload_images/4685968-1a5b383f7f2bf188.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-535359f026338231.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 将博客注释掉 +![](https://upload-images.jianshu.io/upload_images/4685968-5fb4c20bf2895c6e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-536d32e80b910927.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 调试过程 +![](https://upload-images.jianshu.io/upload_images/4685968-fe1fce0b021fd204.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-f5f7e9ee7d87c866.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +开始发布 +![](https://upload-images.jianshu.io/upload_images/4685968-8cbf04eb02013d8e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +# 框架源码应用 +![](https://upload-images.jianshu.io/upload_images/4685968-0891c4661445243c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- doFilter相当于 deploy 方法 +![](https://upload-images.jianshu.io/upload_images/4685968-dbb26c1e36a5cb1f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-6506748536cee1ad.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) \ No newline at end of file diff --git "a/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" new file mode 100644 index 0000000000..eb4bcc9fd9 --- /dev/null +++ "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" @@ -0,0 +1,436 @@ +# 1 定义与类型 +- 模板方法模式(Template Method Pattern) +Define the skeleton of an algorithm in an operation,deferring some steps to subclasses.Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.(定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。) +![定义](https://upload-images.jianshu.io/upload_images/4685968-e39f281f77196ca1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![定义补充](https://upload-images.jianshu.io/upload_images/4685968-c9664ee9d4a0e136.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![类型](https://upload-images.jianshu.io/upload_images/4685968-45cd5433a82e2df6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![模板方法模式的通用类图](https://upload-images.jianshu.io/upload_images/4685968-39e33a07bfb7e900.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +模板方法模式确实非常简单,仅仅使用了Java的继承机制,但它是一个应用非常广泛的模式。其中,AbstractClass叫做抽象模板,它的方法分为两类: +● 基本方法 +基本操作,是由子类实现的方法,并且在模板方法被调用 +● 模板方法 +可以有一个或几个,一般是一个具体方法,也就是一个框架,实现对基本方法的调度,完成固定的逻辑 +为了防止恶意的操作,一般模板方法都加上final关键字,不允许被覆写。 + +在类图中还有一个角色:具体模板。ConcreteClass1和ConcreteClass2属于具体模板,实现父类所定义的一个或多个抽象方法,也就是父类定义的基本方法在子类中得以实现。 + +我们来看其通用代码,AbstractClass抽象模板类 +``` +public abstract class AbstractClass { + //基本方法 + protected abstract void doSomething(); + //基本方法 + protected abstract void doAnything(); + //模板方法 + public void templateMethod(){ + /* + * 调用基本方法,完成相关的逻辑 + */ + this.doAnything(); + this.doSomething(); + } +} +``` +- 具体模板类 +``` +public class ConcreteClass1 extends AbstractClass { + //实现基本方法 + protected void doAnything() { + //业务逻辑处理 + } + protected void doSomething() { + //业务逻辑处理 + } +} +public class ConcreteClass2 extends AbstractClass { + //实现基本方法 + protected void doAnything() { + //业务逻辑处理 + } + protected void doSomething() { + //业务逻辑处理 + } +} +``` +- 场景类 +``` +public class Client { + public static void main(String[] args) { + AbstractClass class1 = new ConcreteClass1(); + AbstractClass class2 = new ConcreteClass2(); + //调用模板方法 + class1.templateMethod(); + class2.templateMethod(); + } +} +``` +抽象模板中的基本方法尽量设计为protected类型,符合迪米特法则,不需要暴露的属性或方法尽量不要设置为protected类型。实现类若非必要,尽量不要扩大父类中的访问权限。 +# 2 适用场景 +![场景](https://upload-images.jianshu.io/upload_images/4685968-2e2b6f6b2d30b1a6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +● 多个子类有公有的方法,并且逻辑基本相同时。 +● 重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现。 +● 重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通过钩子函数(见“模板方法模式的扩展”)约束其行为。 +# 3 优点 +![](https://upload-images.jianshu.io/upload_images/4685968-1cd27db040651115.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +● 封装不变部分,扩展可变部分 +把认为是不变部分的算法封装到父类实现,而可变部分的则可以通过继承来继续扩展 +● 提取公共部分代码,便于维护 +如果我们不抽取到父类中,任由这种散乱的代码发生,想想后果是什么样子?维护人员为了修正一个缺陷,需要到处查找类似的代码 +● 行为由父类控制,子类实现 +基本方法是由子类实现的,因此子类可以通过扩展的方式增加相应的功能,符合开闭原则 +# 4 缺点 +![](https://upload-images.jianshu.io/upload_images/4685968-255b37b0b71729b7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +抽象类负责声明最抽象、最一般的事物属性和方法,实现类完成具体的事物属性和方法 +但是模板方法模式却颠倒了,抽象类定义了部分抽象方法,由子类实现,子类执行的结果影响了父类的结果,也就是子类对父类产生了影响,这在复杂的项目中,会带来代码阅读的难度,而且也会让新手产生不适感。 + +# 相关设计模式 +![](https://upload-images.jianshu.io/upload_images/4685968-fc9260cd849805ed.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 示例 - 辉煌工程——制造悍马 +先不考虑扩展性,那好办,先按照最一般的经验设计类图 +![悍马车模型最一般的类图](https://upload-images.jianshu.io/upload_images/4685968-66f076c6cb35e384.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 非常简单的实现,悍马车有两个型号,H1和H2 +按照需求,只需要悍马模型,那好我就给你悍马模型,先写个抽象类,然后两个不同型号的模型实现类,通过简单的继承就可以实现业务要求 + +我们先从抽象类开始编写 + +抽象悍马模型 +``` +public abstract class HummerModel { +     /* +      * 首先,这个模型要能够被发动起来,别管是手摇发动,还是电力发动,反正 +      * 是要能够发动起来,那这个实现要在实现类里了 +      */ +     public abstract void start(); +     //能发动,还要能停下来,那才是真本事 +     public abstract void stop();        +     //喇叭会出声音,是滴滴叫,还是哔哔叫 +     public abstract void alarm(); +     //引擎会轰隆隆地响,不响那是假的 +     public abstract void engineBoom(); +     //那模型应该会跑吧,别管是人推的,还是电力驱动的,总之要会跑 +     public abstract void run(); +} +``` +定义了悍马模型都必须具有的特质:能够发动、停止,喇叭会响,引擎可以轰鸣,而且还可以停止 +但是每个型号的悍马实现是不同的 + +- H1型号的悍马 +``` +public class HummerH1Model extends HummerModel { +     //H1型号的悍马车鸣笛 +     public void alarm() { +             System.out.println("悍马H1鸣笛..."); +     } +     //引擎轰鸣声 +     public void engineBoom() { +             System.out.println("悍马H1引擎声音是这样的..."); +     } +     //汽车发动 +     public void start() { +             System.out.println("悍马H1发动..."); +     }   +     //停车 +     public void stop() { +             System.out.println("悍马H1停车..."); +     } +     //开动起来 +     public void run(){ +             //先发动汽车 +             this.start(); +             //引擎开始轰鸣 +             this.engineBoom(); +             //然后就开始跑了,跑的过程中遇到一条狗挡路,就按喇叭 +             this.alarm(); +             //到达目的地就停车 +             this.stop(); +     } +} +``` +注意run()方法,这是一个汇总的方法,一个模型生产成功,要拿给客户检测吧,怎么检测? +“是骡子是马,拉出去溜溜”,这就是一种检验方法,让它跑起来! +通过run()这样的方法,把模型的所有功能都测试到了。 + +- H2型号悍马 +``` +public class HummerH2Model extends HummerModel { +     //H2型号的悍马车鸣笛 +     public void alarm() { +             System.out.println("悍马H2鸣笛..."); +     } +     //引擎轰鸣声 +     public void engineBoom() { +             System.out.println("悍马H2引擎声音是这样在..."); +     } +     //汽车发动 +     public void start() { +             System.out.println("悍马H2发动..."); +     }   +     //停车 +     public void stop() { +             System.out.println("悍马H2停车..."); +     }   +     //开动起来 +     public void run(){ +             //先发动汽车 +             this.start();               +             //引擎开始轰鸣 +             this.engineBoom();          +             //然后就开始跑了,跑的过程中遇到一条狗挡路,就按喇叭 +             this.alarm();               +             //到达目的地就停车 +             this.stop(); +     } +} +``` + +程序编写到这里,已经发现问题`两个实现类的run()方法完全相同` +那这个run()方法的实现应该出现在抽象类,不应该在实现类上,抽象是所有子类的共性封装 + +问题发现,马上更改 +![ 修改后的悍马车模类图](http://upload-images.jianshu.io/upload_images/4685968-a69b29d5469b7e06.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +抽象类`HummerModel#run()`,由抽象方法变更为实现方法 +``` +public abstract class HummerModel { +     /* +      * 首先,这个模型要能发动起来,别管是手摇发动,还是电力发动,反正 +      * 是要能够发动起来,那这个实现要在实现类里了 +      */ +     public abstract void start();       +     //能发动,还要能停下来,那才是真本事 +     public abstract void stop();        +     //喇叭会出声音,是滴滴叫,还是哔哔叫 +     public abstract void alarm();       +     //引擎会轰隆隆地响,不响那是假的 +     public abstract void engineBoom();  +     //那模型应该会跑吧,别管是人推的,还是电力驱动,总之要会跑 +     public void run(){ +             //先发动汽车 +             this.start();               +             //引擎开始轰鸣 +             this.engineBoom();          +             //然后就开始跑了,跑的过程中遇到一条狗挡路,就按喇叭 +             this.alarm();               +             //到达目的地就停车 +             this.stop(); +     } +} +``` +在抽象的悍马模型上已经定义了run()方法的执行规则,先启动,然后引擎立刻轰鸣,中间还要按一下喇叭,然后停车 +它的两个具体实现类就不需要实现run()方法了,只要把相应代码上的run()方法删除即可 + +场景类实现的任务就是把生产出的模型展现给客户 +``` +public class Client { +     public static void main(String[] args) { +             //XX公司要H1型号的悍马 +             HummerModel h1 = new HummerH1Model();               +             //H1模型演示 +             h1.run(); +     } +} +``` +运行结果 +``` +悍马H1发动... + +悍马H1引擎声音是这样的... + +悍马H1鸣笛... + +悍马H1停车... +``` +目前客户只要看H1型号的悍马车,没问题,生产出来,同时可以运行起来给他看看。非常简单,那如果我告诉你这就是模板方法模式你会不会很不屑呢?就这模式,太简单了,我一直在使用呀!是的,你经常在使用,但你不知道这是模板方法模式,那些所谓的高手就可以很牛地说:“用模板方法模式就可以实现”,你还要很崇拜地看着,哇,牛人,模板方法模式是什么呀?这就是模板方法模式。 +# 扩展 + ![](https://upload-images.jianshu.io/upload_images/4685968-1bfcdc28387a7178.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +到目前为止,这两个模型都稳定地运行,突然有一天,老大急匆匆地找到了我: + +“看你怎么设计的,车子一启动,喇叭就狂响,吵死人了!客户提出H1型号的悍马喇叭想让它响就响,H2型号的喇叭不要有声音,赶快修改一下。” + +自己惹的祸,就要想办法解决它,稍稍思考一下,解决办法有了,先画出类图 +![扩展悍马车模类图](http://upload-images.jianshu.io/upload_images/4685968-45658d3f5688a489.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +类图改动似乎很小,在抽象类`HummerModel`中增加了一个实现方法`isAlarm`,确定各个型号的悍马是否需要声音,由各个实现类覆写该方法,同时其他的基本方法由于不需要对外提供访问,因此也设计为protected类型 +- 扩展后的抽象模板类 +``` +public abstract class HummerModel { +     /* +      * 首先,这个模型要能够被发动起来,别管是手摇发动,还是电力发动,反正 +      * 是要能够发动起来,那这个实现要在实现类里了 +      */ +     protected abstract void start();    +     //能发动,还要能停下来,那才是真本事 +     protected abstract void stop();     +     //喇叭会出声音,是滴滴叫,还是哔哔叫 +     protected abstract void alarm();    +     //引擎会轰隆隆的响,不响那是假的 +     protected abstract void engineBoom();       +     //那模型应该会跑吧,别管是人推的,还是电力驱动,总之要会跑 +     final public void run() {           +             //先发动汽车 +             this.start();               +             //引擎开始轰鸣 +             this.engineBoom();          +             //要让它叫的就是就叫,喇嘛不想让它响就不响 +             if(this.isAlarm()){ +                      this.alarm(); +             } +             //到达目的地就停车 +             this.stop(); +     }   +     //钩子方法,默认喇叭是会响的 +     protected  boolean isAlarm(){ +             return true; +     } +} +``` +- 在抽象类中,`isAlarm`是一个实现方法 +模板方法根据其返回值决定是否要响喇叭,子类可以覆写该返回值,由于H1型号的喇叭是想让它响就响,不想让它响就不响,由人控制 + +- 扩展后的H1悍马 +``` +public class HummerH1Model extends HummerModel { +     private boolean alarmFlag = true;  //要响喇叭 +     protected void alarm() { +             System.out.println("悍马H1鸣笛..."); +     } +     protected void engineBoom() { +             System.out.println("悍马H1引擎声音是这样的..."); +     } +     protected void start() { +             System.out.println("悍马H1发动..."); +     } +     protected void stop() { +             System.out.println("悍马H1停车..."); +     } +     protected boolean isAlarm() { +             return this.alarmFlag; +     } +     //要不要响喇叭,是由客户来决定的 +     public void setAlarm(boolean isAlarm){ +             this.alarmFlag = isAlarm; +     } +} +``` +只要调用H1型号的悍马,默认是有喇叭响的,当然你可以不让喇叭响,通过isAlarm(false)就可以实现。H2型号的悍马是没有喇叭声响的 + +- 扩展后的H2悍马 +``` +public class HummerH2Model extends HummerModel { +     protected void alarm() { +             System.out.println("悍马H2鸣笛..."); +     } +     protected void engineBoom() { +             System.out.println("悍马H2引擎声音是这样的..."); +     } +     protected void start() { +             System.out.println("悍马H2发动..."); +     } +     protected void stop() { +             System.out.println("悍马H2停车..."); +     } +     //默认没有喇叭的 +     protected boolean isAlarm() { +             return false; +     } +} +``` +H2型号的悍马设置isAlarm()的返回值为false,也就是关闭了喇叭功能 + +- 扩展后的场景类 +``` +public class Client { +     public static void main(String[] args) throws IOException { +             System.out.println("-------H1型号悍马--------"); +             System.out.println("H1型号的悍马是否需要喇叭声响?0-不需要   1-需要"); +             String type=(new BufferedReader(new InputStreamReader([System.in](http://system.in/)))).readLine(); +             HummerH1Model h1 = new HummerH1Model(); +             if(type.equals("0")){   +                     h1.setAlarm(false); +             } +             h1.run(); +             System.out.println("\n-------H2型号悍马--------"); +             HummerH2Model h2 = new HummerH2Model(); +             h2.run(); +     } +} +``` +运行是需要交互的,首先,要求输入H1型号的悍马是否有声音,如下所示: +``` +-------H1型号悍马-------- + +H1型号的悍马是否需要喇叭声响?0-不需要 1-需要 + +输入“0”后的运行结果如下所示: + +-------H1型号悍马-------- + +H1型号的悍马是否需要喇叭声响?0-不需要 1-需要 + +0 + +悍马H1发动... + +悍马H1引擎声音是这样的... + +悍马H1停车... + +-------H2型号悍马-------- + +悍马H2发动... + +悍马H2引擎声音是这样的... + +悍马H2停车... + +输入“1”后的运行结果如下所示: + +-------H1型号悍马-------- + +H1型号的悍马是否需要喇叭声响?0-不需要 1-需要 + +1 + +悍马H1发动... + +悍马H1引擎声音是这样的... + +悍马H1鸣笛... + +悍马H1停车... + +-------H2型号悍马-------- + +悍马H2发动... + +悍马H2引擎声音是这样的... + +悍马H2停车... +``` +看到没,H1型号的悍马是由客户自己控制是否要响喇叭,也就是说外界条件改变,影响到模板方法的执行。 +在我们的抽象类中isAlarm的返回值就是影响了模板方法的执行结果,该方法就叫做钩子方法(Hook Method)。有了钩子方法模板方法模式才算完美,大家可以想想,由子类的一个方法返回值决定公共部分的执行结果,是不是很有吸引力呀! + +模板方法模式就是在模板方法中按照一定的规则和顺序调用基本方法,具体到前面那个例子,就是run()方法按照规定的顺序(先调用start(),然后再调用engineBoom(),再调用alarm(),最后调用stop())调用本类的其他方法,并且由isAlarm()方法的返回值确定run()中的执行顺序变更。 + + + +# Coding!!! +![](https://upload-images.jianshu.io/upload_images/4685968-753ff691e6ee5b5c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-4a1e2f336d316ab9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-ce273adc1d8907dd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-d1d4ebd9bd2452d5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-b0bec7541a33cc5c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +# 最佳实践 +初级程序员在写程序的时候经常会问高手“父类怎么调用子类的方法”。这个问题很有普遍性,反正我是被问过好几回,那么父类是否可以调用子类的方法呢?我的回答是能,但强烈地、极度地不建议这么做,那该怎么做呢? + +● 把子类传递到父类的有参构造中,然后调用 +● 使用反射的方式调用 +● 父类调用子类的静态方法 + +这三种都是父类直接调用子类的方法,好用不?好用!解决问题了吗?解决了!项目中允许使用不?不允许! +我就一直没有搞懂为什么要用父类调用子类的方法。如果一定要调用子类,那为什么要继承它呢?搞不懂。其实这个问题可以换个角度去理解,父类建立框架,子类在重写了父类部分的方法后,再调用从父类继承的方法,产生不同的结果(而这正是模板方法模式)。这是不是也可以理解为父类调用了子类的方法呢?你修改了子类,影响了父类行为的结果,曲线救国的方式实现了父类依赖子类的场景,模板方法模式就是这种效果。 + +模板方法在一些开源框架中应用非常多,它提供了一个抽象类,然后开源框架写了一堆子类。在《××× In Action》中就说明了,如果你需要扩展功能,可以继承这个抽象类,然后覆写protected方法,再然后就是调用一个类似execute方法,就完成你的扩展开发,非常容易扩展的一种模式。 +# 应用 +## AbstractList diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\212\266\346\200\201\346\250\241\345\274\217.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\212\266\346\200\201\346\250\241\345\274\217.md" similarity index 71% rename from "\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\212\266\346\200\201\346\250\241\345\274\217.md" rename to "\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\212\266\346\200\201\346\250\241\345\274\217.md" index 59e6f8fc6c..16195426c6 100644 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\212\266\346\200\201\346\250\241\345\274\217.md" +++ "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\212\266\346\200\201\346\250\241\345\274\217.md" @@ -7,17 +7,21 @@ 该模式中,类的行为基于其状态改变。即允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。 # 3 架构 -## Context-环境类 -拥有状态的对象,环境类有时可以充当状态管理器(State Manager),在环境类中对状态进行切换操作。 -## State-抽象状态类 -可以是抽象类,也可是接口,不同状态类就是继承这个父类的不同子类,状态类的产生是由于环境类存在多个状态,同时还满足:这些状态经常需要切换,在不同状态下对象行为不同。 -## ConcreteState-具体状态类 +- Context:环境类 +- State:抽象状态类 +- ConcreteState:具体状态类 + + # 4 意义 解决:对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。 状态模式的关键是引入了一个抽象类来专门表示对象的状态 - 抽象状态类。而对象的每种具体状态类都继承该类,并在不同具体状态类中实现不同状态的行为,包括各种状态之间的转换。 -可以将不同对象下的行为单独提取出来封装在**具体的状态类**,使得**环境类**对象在其内部状态改变时可以改变它的行为,对象看起来似乎修改了它的类,而实际上是由于切换到不同的具体状态类实现的。 -由于**环境类**可以设置为任一具体状态类,因此它针对抽象状态类进行编程,在程序运行时可以将任一具体状态类的对象设置到环境类中,从而使得环境类可以改变内部状态,并且改变行为。 +在状态模式结构中需要理解环境类与抽象状态类的作用: +- 环境类实际上就是拥有状态的对象,环境类有时候可以充当状态管理器(State Manager),可在环境类中对状态进行切换操作。 +- 抽象状态类可以是抽象类,也可是接口,不同状态类就是继承这个父类的不同子类,状态类的产生是由于环境类存在多个状态,同时还满足:这些状态经常需要切换,在不同状态下对象行为不同。 +因此可以将不同对象下的行为单独提取出来封装在具体的状态类中,使得环境类对象在其内部状态改变时可以改变它的行为,对象看起来似乎修改了它的类,而实际上是由于切换到不同的具体状态类实现的。由于环境类可以设置为任一具体状态类,因此它针对抽象状态类进行编程,在程序运行时可以将任一具体状态类的对象设置到环境类中,从而使得环境类可以改变内部状态,并且改变行为。 + + # 5 优点 1. 封装了转换规则 2. 枚举可能的状态,在枚举状态之前需要确定状态种类 @@ -55,6 +59,7 @@ ![](https://img-blog.csdnimg.cn/20201004191242755.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_25,color_FFFF00,t_70#pic_center) 1. State 接口 +![](https://img-blog.csdnimg.cn/20201004022025441.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_25,color_FFFF00,t_70#pic_center) 2. 实现 State 接口的实体状态类 ![](https://img-blog.csdnimg.cn/20201004184658967.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_25,color_FFFF00,t_70#pic_center) 3. Context:带有某状态的类 @@ -66,23 +71,17 @@ - 共享状态 在有些情况下多个环境对象需共享同一状态,若期望在系统中实现多个环境对象实例共享一个或多个状态对象,那么需要将这些状态对象定义为环境的静态成员对象。 -## 简单状态模式 -状态都相互独立,状态之间无须进行转换的状态模式,这是最简单的一种状态模式。 -每个状态类都封装与状态相关的操作,无需关心状态切换,可在客户端直接实例化状态类,然后将状态对象设置到环境类。 - -遵循“开闭原则”,在客户端可以针对抽象状态类进行编程,而将具体状态类写到配置文件中,同时增加新的状态类对原有系统也不造成任何影响。 -## 可切换状态的状态模式 +## 简单状态模式与可切换状态的状态模式 +- 简单状态模式 +状态都相互独立,状态之间无须进行转换的状态模式,这是最简单的一种状态模式。这种状态模式,每个状态类都封装与状态相关的操作,无需关心状态切换,可在客户端直接实例化状态类,然后将状态对象设置到环境类。它遵循“开闭原则”,在客户端可以针对抽象状态类进行编程,而将具体状态类写到配置文件中,同时增加新的状态类对原有系统也不造成任何影响。 +- 可切换状态的状态模式 大多数的状态模式都是可切换状态的状态模式,在实现状态切换时,在具体状态类内部需要调用环境类Context的setState()方法进行状态的转换操作,在具体状态类中可以调用到环境类的方法,因此状态类与环境类之间通常还存在关联关系或者依赖关系。通过在状态类中引用环境类的对象来回调环境类的setState()方法实现状态的切换。在这种可以切换状态的状态模式中,增加新的状态类可能需要修改其他某些状态类甚至环境类的源代码,否则系统无法切换到新增状态。 # 11 总结 状态模式允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象,状态模式是一种对象行为型模式。 - 状态模式包含三个角色:环境类又称为上下文类,它是拥有状态的对象,在环境类中维护一个抽象状态类State的实例,这个实例定义当前状态,在具体实现时,它是一个State子类的对象,可以定义初始状态;抽象状态类用于定义一个接口以封装与环境类的一个特定状态相关的行为;具体状态类是抽象状态类的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。 - 状态模式描述了对象状态的变化以及对象如何在每一种状态下表现出不同的行为。 +状态模式的主要优点在于封装了转换规则,并枚举可能的状态,它将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为,还可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数;其缺点在于使用状态模式会增加系统类和对象的个数,且状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,对于可以切换状态的状态模式不满足“开闭原则”的要求。 +状态模式适用情况包括:对象的行为依赖于它的状态(属性)并且可以根据它的状态改变而改变它的相关行为;代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,使客户类与类库之间的耦合增强。 -状态模式的主要优点在于封装了转换规则,并枚举可能状态,它将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为,还可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数;其缺点在于使用状态模式会增加系统类和对象的个数,且状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,对于可以切换状态的状态模式不满足“开闭原则”的要求。 - -## 适用情况 -- 对象的行为依赖于它的状态(属性)并且可以根据它的状态改变而改变它的相关行为 -- 代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,使客户类与类库之间的耦合增强。 \ No newline at end of file + \ No newline at end of file diff --git "a/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\255\226\347\225\245\346\250\241\345\274\217(Strategy-Pattern).md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\255\226\347\225\245\346\250\241\345\274\217(Strategy-Pattern).md" new file mode 100644 index 0000000000..4e0c46e216 --- /dev/null +++ "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\255\226\347\225\245\346\250\241\345\274\217(Strategy-Pattern).md" @@ -0,0 +1,101 @@ +# 0.0 相关源码链接 +https://github.com/Wasabi1234/design-patterns + +# 1 定义 +![](https://upload-images.jianshu.io/upload_images/4685968-f3e6ce1684ece913.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +也叫做政策模式(Policy Pattern) +- 维基百科 +对象有某个行为,但是在不同的场景中,该行为有不同的实现算法. +比如每个人都要“交个人所得税”,但是“在美国交个人所得税”和“在中国交个人所得税”就有不同的算税方法. +- 定义 +Define a family of algorithms,encapsulate each one,and make them interchangeable. +定义一组算法,将每个算法都封装起来,并且使它们之间可以互换. + + +在`运行时`(非编译时)改变软件的算法行为 +- 主要思想 +定义一个通用的问题,使用不同的算法来实现,然后将这些算法都封装在一个统一接口的背后. + +![策略模式的通用类图](https://upload-images.jianshu.io/upload_images/4685968-ad1caf184324decf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +策略模式使用的就是面向对象的继承和多态机制 + +策略模式的三个角色 +● Context 封装角色 +也叫做上下文角色,起承上启下封装作用; +屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化. + +● Strategy抽象策略角色 +策略、算法家族的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性 + +● ConcreteStrategy具体策略角色 +实现抽象策略中的操作,含有具体的算法 + +### 通用源码 +- 抽象策略角色,它是一个非常普通的接口,在我们的项目中就是一个普通得不能再普通的接口了,定义一个或多个具体的算法 + + +# 2 适用场景 +针对一个对象,其行为有些是固定的不变的,有些是容易变化的,针对不同情况有不同的表现形式。那么对于这些容易变化的行为,我们不希望将其实现绑定在对象中,而是希望以动态的形式,针对不同情况产生不同的应对策略。那么这个时候就要用到策略模式了。简言之,策略模式就是为了应对对象中复杂多变的行为而产生的。 + +- 系统有很多类,而他们的区别仅仅在于他们的行为不同 +- 一个系统需要动态地在几种算法中选择一种 + +# 3 优点 +- 符合开闭原则 +- 避免使用多重条件转移语句 +比如省去大量的 if/else 和 switch 语句,降低代码的耦合 +- 提高算法的保密性和安全性 +只需知道策略的作用,而不关心内部实现 + +# 4 缺点 +- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类 +- 产生很多策略类 + +# 5 相关设计模式的差异 +## 策略模式和工厂模式 +- 行为型 +接收已经创建好的对象,从而实现不同的行为 +- 创造型 +接收指令,创建出符合要求的具体对象 + +## 策略模式和状态模式 +- 若系统中某个类的某个行为存在多种实现方式,客户端需要知道到底使用哪个策略 +- 若系统中某个对象存在多种状态,不同状态下的行为又具有差异性,状态之间会自动转换,客户端不需要关心具体状态 + +# 6 实战 +![](https://upload-images.jianshu.io/upload_images/4685968-0ebc08f41e07cdca.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +![](https://upload-images.jianshu.io/upload_images/4685968-98e2b70fe0d9a3f0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-ecbce7b0043a7490.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-5dab16664b2d6639.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-57e3f0490d67cfb0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-8a75a258378f8a69.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![image.png](https://upload-images.jianshu.io/upload_images/4685968-844075f01a9e349b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +改造后的测试类 +![](https://upload-images.jianshu.io/upload_images/4685968-4991d2eaad9357c1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +可见 if/else 语句过多,采取策略+工厂模式结合 +- 策略工厂 +![](https://upload-images.jianshu.io/upload_images/4685968-230088ca260db256.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 最新测试类 +![](https://upload-images.jianshu.io/upload_images/4685968-acf80da4fa5ea954.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 输出结果 +![](https://upload-images.jianshu.io/upload_images/4685968-7d26033a1b39bd6a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +# 7 源码应用解析 +## JDK中的比较器接口 +- 策略比较器 +![](https://upload-images.jianshu.io/upload_images/4685968-307666896c3d1800.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![具体策略](https://upload-images.jianshu.io/upload_images/4685968-d928dd16bea44a60.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +比如Arrays类中的 sort 方法通过传入不同比较接口器的实现达到不同排序策略 +![](https://upload-images.jianshu.io/upload_images/4685968-f92073712e30ce66.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## JDK中的TreeMap +类似于促销活动中有促销策略对象,在T reeMap 中也有比较器对象 +![](https://upload-images.jianshu.io/upload_images/4685968-424f787da17d4876.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +compare 方法进步加工 +![](https://upload-images.jianshu.io/upload_images/4685968-32e02456542c1e48.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## Spring 中的Resource +不同访问策略 +![](https://upload-images.jianshu.io/upload_images/4685968-66d6191177faaf2a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## Spring 中bean 的初始化ceInstantiationStrategy +- 两种 bean 的初始化策略 +![](https://upload-images.jianshu.io/upload_images/4685968-8fa5e44e491aafdc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) diff --git "a/\351\207\215\346\236\204/\344\275\240\347\232\204\345\233\242\351\230\237Code Review\344\270\200\345\244\251\345\207\240\346\254\241\357\274\237.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\243\205\351\245\260\345\231\250\346\250\241\345\274\217(Decorator Pattern).md" similarity index 100% rename from "\351\207\215\346\236\204/\344\275\240\347\232\204\345\233\242\351\230\237Code Review\344\270\200\345\244\251\345\207\240\346\254\241\357\274\237.md" rename to "\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\243\205\351\245\260\345\231\250\346\250\241\345\274\217(Decorator Pattern).md" diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\277\255\344\273\243\345\231\250\346\250\241\345\274\217.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\277\255\344\273\243\345\231\250\346\250\241\345\274\217.md" similarity index 100% rename from "\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\277\255\344\273\243\345\231\250\346\250\241\345\274\217.md" rename to "\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\277\255\344\273\243\345\231\250\346\250\241\345\274\217.md" diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\351\200\202\351\205\215\345\231\250\346\250\241\345\274\217.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\351\200\202\351\205\215\345\231\250\346\250\241\345\274\217.md" similarity index 51% rename from "\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\351\200\202\351\205\215\345\231\250\346\250\241\345\274\217.md" rename to "\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\351\200\202\351\205\215\345\231\250\346\250\241\345\274\217.md" index 3a49860571..8b116eeab5 100644 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\351\200\202\351\205\215\345\231\250\346\250\241\345\274\217.md" +++ "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\351\200\202\351\205\215\345\231\250\346\250\241\345\274\217.md" @@ -1,21 +1,19 @@ -# 1 导读 -一般客户端通过目标类的接口访问它所提供的服务。 -有时,现有类可以满足客户端类的需要,但所提供接口不一定是客户端所期望的,可能因为现有类中方法名与目标类中定义的方法名不一致。 +# 1 动机 +类似于电源适配器的设计和编码技巧。一般客户端通过目标类的接口访问它所提供的服务。 +有时,现有类可以满足客户类需要,但所提供接口不一定是客户类所期望的,可能因为现有类中方法名与目标类中定义的方法名不一致 -这时,**现有接口需要转化为客户端的期望接口,保证复用现有类**。若不进行这样转化,客户端就不能利用现有类所提供功能,适配器模式就可以完成这样的转化。 +这时,**现有接口需要转化为客户类期望的接口,保证复用现有类** +如果不进行这样的转化,客户类就不能利用现有类所提供的功能,适配器模式可以完成这样的转化。 在适配器模式中可以定义一个包装类,包装不兼容接口的对象 -- 包装类 -适配器(Adapter) -- 所包装的对象 -适配者(Adaptee),即被适配的类 +- 包装类指的就是适配器(Adapter) +- 所包装的对象就是适配者(Adaptee),即被适配的类 -适配器提供客户类需要的接口。 -适配器的实现就是把客户端的请求转化为对适配者的相应接口的调用。即当客户类调用适配器方法时,在适配器类的内部将调用适配者类的方法,而该过程对客户类透明,客户类并不直接访问适配者类。 -因此,适配器可以使由于接口不兼容而不能交互的类可以一起协作。 +适配器提供客户类需要的接口 +适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。即当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。因此,适配器可以使由于接口不兼容而不能交互的类可以一起工作 # 2 定义 -将一个接口转换成客户端希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器。 +将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器 既可以作为类结构型模式,也可以作为对象结构型模式。 # 3 结构 @@ -25,13 +23,14 @@ * Client:客户类 适配器模式有对象适配器和类适配器两种实现: + ## 3.1 对象适配器 ![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtZGU0NWZjYzgyNjQwMjY4My5qcGc?x-oss-process=image/format,png) ## 3.2 类适配器 ![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtMzFiNzdkZmVlZjcyNzdmNC5qcGc?x-oss-process=image/format,png) -# 4 时序图 +#4 时序图 ![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtZDNkY2JhZDQ1YjBkMjE3Yi5qcGc?x-oss-process=image/format,png) # 5 代码分析 @@ -53,24 +52,148 @@ *AudioPlayer* 使用适配器类 *MediaAdapter* 传递所需的音频类型,不需要知道能播放所需格式音频的实际类。*AdapterPatternDemo*,我们的演示类使用 *AudioPlayer* 类来播放各种格式。 ![适配器模式的 UML 图](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtZjJmNzQ1ZWNmNzcyNmViYy5qcGc?x-oss-process=image/format,png) + ### 步骤 1 -创建接口。 -![](https://img-blog.csdnimg.cn/20210718163101535.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70)![](https://img-blog.csdnimg.cn/20210718163326636.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +为媒体播放器和更高级的媒体播放器创建接口。 + +*MediaPlayer.java* +``` +public interface MediaPlayer { + public void play(String audioType, String fileName); +} +``` + +*AdvancedMediaPlayer.java* +``` +public interface AdvancedMediaPlayer { + public void playVlc(String fileName); + public void playMp4(String fileName); +} +``` ### 步骤 2 -![](https://img-blog.csdnimg.cn/20210718163812845.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20210718163847504.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +创建实现了 *AdvancedMediaPlayer* 接口的实体类 + +*VlcPlayer.java* + +``` +public class VlcPlayer implements AdvancedMediaPlayer{ + @Override + public void playVlc(String fileName) { + System.out.println("Playing vlc file. Name: "+ fileName); + } + + @Override + public void playMp4(String fileName) { + //什么也不做 + } +} +``` + +*Mp4Player.java* +``` +public class Mp4Player implements AdvancedMediaPlayer{ + + @Override + public void playVlc(String fileName) { + //什么也不做 + } + + @Override + public void playMp4(String fileName) { + System.out.println("Playing mp4 file. Name: "+ fileName); + } +} +``` + ### 步骤 3 -![](https://img-blog.csdnimg.cn/20210718163721309.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) + +创建实现了 *MediaPlayer* 接口的适配器类。 + +*MediaAdapter.java* + +``` +public class MediaAdapter implements MediaPlayer { + + AdvancedMediaPlayer advancedMusicPlayer; + + public MediaAdapter(String audioType){ + if(audioType.equalsIgnoreCase("vlc") ){ + advancedMusicPlayer = new VlcPlayer(); + } else if (audioType.equalsIgnoreCase("mp4")){ + advancedMusicPlayer = new Mp4Player(); + } + } + + @Override + public void play(String audioType, String fileName) { + if(audioType.equalsIgnoreCase("vlc")){ + advancedMusicPlayer.playVlc(fileName); + }else if(audioType.equalsIgnoreCase("mp4")){ + advancedMusicPlayer.playMp4(fileName); + } + } +} +``` ### 步骤 4 -![](https://img-blog.csdnimg.cn/20210718164931518.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +创建实现了 *MediaPlayer* 接口的实体类。 + +*AudioPlayer.java* +``` +public class AudioPlayer implements MediaPlayer { + MediaAdapter mediaAdapter; + + @Override + public void play(String audioType, String fileName) { + + //播放 mp3 音乐文件的内置支持 + if(audioType.equalsIgnoreCase("mp3")){ + System.out.println("Playing mp3 file. Name: "+ fileName); + } + //mediaAdapter 提供了播放其他文件格式的支持 + else if(audioType.equalsIgnoreCase("vlc") + || audioType.equalsIgnoreCase("mp4")){ + mediaAdapter = new MediaAdapter(audioType); + mediaAdapter.play(audioType, fileName); + } + else{ + System.out.println("Invalid media. "+ + audioType + " format not supported"); + } + } +} +``` ### 步骤 5 -![](https://img-blog.csdnimg.cn/20210718165039929.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) + +使用 AudioPlayer 来播放不同类型的音频格式。 + +*AdapterPatternDemo.java* + +``` +public class AdapterPatternDemo { + public static void main(String[] args) { + AudioPlayer audioPlayer = new AudioPlayer(); + + audioPlayer.play("mp3", "beyond the horizon.mp3"); + audioPlayer.play("mp4", "alone.mp4"); + audioPlayer.play("vlc", "far far away.vlc"); + audioPlayer.play("avi", "mind me.avi"); + } +} +``` + ### 步骤 6 -测试类输出: -![](https://img-blog.csdnimg.cn/20210718180057697.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) + +验证输出。 + +``` +Playing mp3 file. Name: beyond the horizon.mp3 +Playing mp4 file. Name: alone.mp4 +Playing vlc file. Name: far far away.vlc +Invalid media. avi format not supported +``` # 案例 ## 实现 `Iterable` 的 `Fibnoacci` 生成器 @@ -78,32 +201,37 @@ - 不过你并不是总拥有源代码的控制权 - 并且,除非必须这么做,否则,我们也不愿意重写一个类 -因此另一种选择,创建一个 适配器(Adapter) 来实现所需接口。有多种适配器的实现,例如继承: -![](https://img-blog.csdnimg.cn/20200627210435744.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +因此另一种选择,创建一个 *适配器* (Adapter) 来实现所需的接口 + +有多种适配器的实现。例如继承: +![](https://img-blog.csdnimg.cn/20200627210435744.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) 在 *for-in* 语句中使用 `IterableFibonacci`,必须在构造函数中提供一个边界值,这样 `hasNext()` 才知道何时返回 **false**,结束循环。 # 8 优点 -- 解耦目标类和适配者类,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码 -- 增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性 -- 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则” + +* 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。 +* 增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。 +* 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。 类适配器模式还具有如下优点: 由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。 - 对象适配器模式还具有如下优点: 一个对象适配器可以把多个不同的适配者适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口。 # 9 缺点 ## 类适配器模式 -对于Java不支持多继承,一次最多只能适配一个适配者类,而且目标抽象类只能为抽象类,不能为具体类,其使用有一定的局限性,不能将一个适配者类和它的子类都适配到目标接口。 +对于Java、C#等不支持多重继承的语言,一次最多只能适配一个适配者类,而且目标抽象类只能为抽象类,不能为具体类,其使用有一定的局限性,不能将一个适配者类和它的子类都适配到目标接口。 ## 对象适配器模式 与类适配器模式相比,要想置换适配者类的方法就不容易。如果一定要置换掉适配者类的一个或多个方法,就只好先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。 + # 10 适用环境 在以下情况下可以使用适配器模式: -- 系统需要使用现有的类,而这些类的接口不符合系统的需要 -- 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作 + +* 系统需要使用现有的类,而这些类的接口不符合系统的需要。 +* 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。 + # 11 模式应用 Sun公司在1996年公开了Java语言的数据库连接工具JDBC,JDBC使得Java语言程序能够与数据库连接,并使用SQL语言来查询和操作数据。JDBC给出一个客户端通用的抽象接口,每一个具体数据库引擎(如SQL Server、Oracle、MySQL等)的JDBC驱动软件都是一个介于JDBC接口和数据库引擎接口之间的适配器软件。抽象的JDBC接口和各个数据库引擎API之间都需要相应的适配器软件,这就是为各个不同数据库引擎准备的驱动程序。 @@ -117,4 +245,4 @@ Sun公司在1996年公开了Java语言的数据库连接工具JDBC,JDBC使得J * 适配器模式包含四个角色:目标抽象类定义客户要用的特定领域的接口;适配器类可以调用另一个接口,作为一个转换器,对适配者和抽象目标类进行适配,它是适配器模式的核心;适配者类是被适配的角色,它定义了一个已经存在的接口,这个接口需要适配;在客户类中针对目标抽象类进行编程,调用在目标抽象类中定义的业务方法。 * 在类适配器模式中,适配器类实现了目标抽象类接口并继承了适配者类,并在目标抽象类的实现方法中调用所继承的适配者类的方法;在对象适配器模式中,适配器类继承了目标抽象类并定义了一个适配者类的对象实例,在所继承的目标抽象类方法中调用适配者类的相应业务方法。 * 适配器模式的主要优点是将目标类和适配者类解耦,增加了类的透明性和复用性,同时系统的灵活性和扩展性都非常好,更换适配器或者增加新的适配器都非常方便,符合“开闭原则”;类适配器模式的缺点是适配器类在很多编程语言中不能同时适配多个适配者类,对象适配器模式的缺点是很难置换适配者类的方法。 -* 适配器模式适用情况包括:系统需要使用现有的类,而这些类的接口不符合系统的需要;想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类一起工作。 \ No newline at end of file +* 适配器模式适用情况包括:系统需要使用现有的类,而这些类的接口不符合系统的需要;想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类一起工作。 diff --git "a/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(1)-\345\215\225\344\270\200\350\201\214\350\264\243\345\216\237\345\210\231.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(1)-\345\215\225\344\270\200\350\201\214\350\264\243\345\216\237\345\210\231.md" new file mode 100644 index 0000000000..38068c5c56 --- /dev/null +++ "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(1)-\345\215\225\344\270\200\350\201\214\350\264\243\345\216\237\345\210\231.md" @@ -0,0 +1,167 @@ +> `文章收录在我的 GitHub 仓库,欢迎Star/fork:` +> [Java-Interview-Tutorial](https://github.com/Wasabi1234/Java-Interview-Tutorial) +> https://github.com/Wasabi1234/Java-Interview-Tutorial + +- 相关源码 + +https://github.com/Wasabi1234/design-patterns + +# 1 简介 +## 1.1 定义 +不要存在多于一个导致类变更的原因。该原则备受争议,争议之处在于对职责的定义,什么是类的职责?怎么划分类的职责? + + +## 1.2 特点 +一个类/接口/方法只负责一项职责。 + +## 1.3 优点 +降低类的复杂度、提高类的可读性,提高系统的可维护性、降低变更引起的风险。 + +# 2 代码实战 +## 2.1 鸟类案例 +- 最开始的 Bird 类 +![](https://img-blog.csdnimg.cn/20201011052517957.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) +- 测试类 +![简单测试类](https://img-blog.csdnimg.cn/img_convert/7ea1da00d7a9e0615db6170063d5d468.png) + +显然鸵鸟用翅膀飞是错误的! +- 修改类实现 +![](https://img-blog.csdnimg.cn/2020101105314531.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) + +- 以上的设计依旧很差,总不能一味堆砌 ifelse 添加鸟类,结合该业务逻辑,考虑分别实现类职责,即根据单一原则创建两种鸟类即可。 +![](https://img-blog.csdnimg.cn/20201011053445284.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) +![](https://img-blog.csdnimg.cn/20201011053528342.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) +![](https://img-blog.csdnimg.cn/20201011053706470.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) + + +## 2.2 课程案例 +- 最初的课程接口有两个职责,耦合过大 +![](https://img-blog.csdnimg.cn/20201011054054803.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) + +- 按职责拆分 +![](https://img-blog.csdnimg.cn/20201011063546976.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) +![](https://img-blog.csdnimg.cn/20201011063629925.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) + +![](https://img-blog.csdnimg.cn/img_convert/82c766b21a88c40cdc3a1946c5cf51c4.png) + +# 3 终极实例 + +## 3.1 单一!!! +只要做过项目,肯定要接触到用户、机构、角色管理这些模块,基本上使用的都是RBAC模型(Role-Based Access Control,基于角色的访问控制,通过分配和取消角色来完成用户权限的授予和取消,使动作主体(用户)与资源的行为(权限)分离),确实是一个很好的解决办法。 +我们这里要讲的是用户管理、修改用户的信息、增加机构(一个人属于多个机构)、增加角色等,用户有这么多的信息和行为要维护,我们就把这些写到一个接口中,都是用户管理类 + +- 用户信息维护类图 +![](https://img-blog.csdnimg.cn/img_convert/377b177c0732a18271498795b9720c91.png) + +得有问题,用户的属性和用户的行为没有分开,这是一个严重的错误! +这个接口确实设计得一团糟 +- 应该把用户的信息抽取成一个BO(Business Object,业务对象) +- 把行为抽取成一个Biz(Business Logic,业务逻辑) + +- 职责划分后的类图 +![](https://img-blog.csdnimg.cn/img_convert/bcfd4544da6e11c680c9533c3eb54900.png) + +重新拆封成两个接口 +- IUserBO负责用户的属性,简单地说,IUserBO的职责就是收集和反馈用户的属性信息 +- IUserBiz负责用户的行为,完成用户信息的维护和变更 + +看一看分拆成两个接口怎么使用。我们现在是面向接口编程,所以产生了这个UserInfo对象之后,当然可以把它当IUserBO接口使用。也可以当IUserBiz接口使用,这要看你在什么地方使用了。 +- 要获得用户信息,就当是IUserBO的实现类 +- 要是希望维护用户的信息,就把它当作IUserBiz的实现类就成 +```java +IUserInfo userInfo = new UserInfo(); +//我要赋值了,我就认为它是一个纯粹的BO +IUserBO userBO = (IUserBO)userInfo; +userBO.setPassword("abc"); +//我要执行动作了,我就认为是一个业务逻辑类 +IUserBiz userBiz = (IUserBiz)userInfo; +userBiz.deleteUser(); +``` +确实可以如此,问题也解决了,但是我们来分析一下刚才的动作,为什么要把一个接口拆分成两个呢? +其实,在实际的使用中,我们更倾向于使用两个不同的类或接口:一个是IUserBO,一个是IUserBiz +![项目中经常采用的SRP类图](https://img-blog.csdnimg.cn/img_convert/ddf683f0892e2a6a08726c7642b9dd25.png) +以上我们把一个接口拆分成两个接口的动作,就是依赖了单一职责原则,那什么是单一职责原则呢?单一职责原则的定义是:应该有且仅有一个原因引起类的变更。 + +## 3.2 打破你的传统思维 +- SRP +There should never be more than one reason for a class to change. + +再来看看下面这个例子是否好理解。电话通话的时候有4个过程发生:拨号、通话、回应、挂机 +那我们写一个接口 +![电话类图](https://img-blog.csdnimg.cn/img_convert/a7fab40bd3a15f919374e59ecb03b59a.png) +- 电话过程 +```java +public interface IPhone { + //拨通电话 + public void dial(String phoneNumber); + //通话 + public void chat(Object o); + //通话完毕,挂电话 + public void hangup(); +} +``` +实现类也比较简单,就不再写了,大家看看这个接口有没有问题?我相信大部分的读者都会说这个没有问题呀,以前我就是这么做的呀,某某书上也是这么写的呀,还有什么什么的源码也是这么写的!是的,这个接口接近于完美,看清楚了,是“接近”! +单一职责原则要求一个接口或类只有一个原因引起变化,也就是一个接口或类只有一个职责,它就负责一件事情,看看上面的接口只负责一件事情吗?是只有一个原因引起变化吗?好像不是! + +IPhone这个接口可不是只有一个职责,它包含了两个职责: +- 协议管理 +dial()和hangup()两个方法实现的是协议管理,分别负责拨号接通和挂机 +- 数据传送 +chat()实现的是数据的传送,把我们说的话转换成模拟信号或数字信号传递到对方,然后再把对方传递过来的信号还原成我们听得懂的语言。 + +协议接通的变化会引起这个接口或实现类的变化吗?会的!那数据传送(想想看,电话不仅仅可以通话,还可以上网)的变化会引起这个接口或实现类的变化吗?会的!那就很简单了,这里有两个原因都引起了类的变化。这两个职责会相互影响吗?电话拨号,我只要能接通就成,甭管是电信的还是网通的协议;电话连接后还关心传递的是什么数据吗? +通过这样的分析,我们发现类图上的IPhone接口包含了两个职责,而且这两个职责的变化不相互影响,那就考虑拆分成两个接口 +![职责分明的电话类图](https://img-blog.csdnimg.cn/img_convert/a32b12f5a750fccb83178a41a30131d5.png) + + +这个类图看上去有点复杂了,完全满足了单一职责原则的要求,每个接口职责分明,结构清晰,但是我相信你在设计的时候肯定不会采用这种方式,一个手机类要把ConnectionManager和DataTransfer组合在一块才能使用。 +组合是一种强耦合关系,你和我都有共同的生命期,这样的强耦合关系还不如使用接口实现的方式呢,而且还增加了类的复杂性,多了两个类。 +经过这样的思考后,我们再修改一下类图 +![简洁清晰、职责分明的电话类图](https://img-blog.csdnimg.cn/img_convert/6440c34103fafd869b635156ddff80cf.png) +这样的设计才是完美的,一个类实现了两个接口,把两个职责融合在一个类中。你会觉得这个Phone有两个原因引起变化了呀,是的,但是别忘记了我们是面向接口编程,我们对外公布的是接口而不是实现类。而且,如果真要实现类的单一职责,这个就必须使用上面的组合模式了,这会引起类间耦合过重、类的数量增加等问题,人为地增加了设计的复杂性。 + +## 好处 +● 类的复杂性降低,实现什么职责都有清晰明确的定义 +● 可读性提高,复杂性降低,那当然可读性提高了 +● 可维护性提高,可读性提高,那当然更容易维护了 +● 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。 + + +其实单一职责原则最难划分的就是职责。 +一个职责一个接口,但问题是“职责”没有一个量化的标准,一个类到底要负责那些职责?这些职责该怎么细化?细化后是否都要有一个接口或类? +这些都需要从实际的项目去考虑,从功能上来说,定义一个IPhone接口也没有错,实现了电话的功能,而且设计还很简单,仅仅一个接口一个实现类,实际的项目我想大家都会这么设计。项目要考虑可变因素和不可变因素,以及相关的收益成本比率,因此设计一个IPhone接口也可能是没有错的。 +但是,如果纯从“学究”理论上分析就有问题了,有两个可以变化的原因放到了一个接口中,这就为以后的变化带来了风险。如果以后模拟电话升级到数字电话,我们提供的接口IPhone是不是要修改了?接口修改对其他的Invoker类是不是有很大影响? + +单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。 + +# 4 我单纯,所以我快乐 +对于接口,设计时一定要单一,但是对于实现类就需要多方面考虑。 +生搬硬套单一职责原则会引起类的剧增,给维护带来非常多的麻烦,而且过分细分类的职责也会人为地增加系统的复杂性。本来一个类可以实现的行为硬要拆成两个类,然后再使用聚合或组合的方式耦合在一起,人为制造了系统的复杂性。所以原则是死的,人是活的,这句话很有道理。 + +单一职责原则很难在项目中得到体现,非常难,为什么? +在国内,技术人员的地位和话语权都比较低,因此在项目中需要考虑环境,考虑工作量,考虑人员的技术水平,考虑硬件的资源情况,等等,最终妥协的结果是经常违背单一职责原则。而且,我们中华文明就有很多属于混合型的产物,比如筷子,我们可以把筷子当做刀来使用,分割食物;还可以当叉使用,把食物从盘子中移动到口中。而在西方的文化中,刀就是刀,叉就是叉,你去吃西餐的时候这两样肯定都是有的,刀就是切割食物,叉就是固定食物或者移动食物,分工很明晰。这种文化的差异很难一步改造过来,但是我相信随着技术的深入,单一职责原则必然会深入到项目的设计中,而且这个原则是那么的简单,简单得不需要我们更加深入地思考,单从字面上大家都应该知道是什么意思,单一职责嘛! + +单一职责适用于接口、类,同时也适用于方法。一个方法尽可能做一件事情,比如一个方法修改用户密码,不要把这个方法放到“修改用户信息”方法中,这个方法的颗粒度很粗 + +- 一个方法承担多个职责 +![](https://img-blog.csdnimg.cn/img_convert/5cebbf619d200508262e3ddd37e4251e.png) + +在IUserManager中定义了一个方法changeUser,根据传递的类型不同,把可变长度参数changeOptions修改到userBO这个对象上,并调用持久层的方法保存到数据库中。 +在我的项目组中,如果有人写了这样一个方法,我不管他写了多少程序,花了多少工夫,一律重写! +原因很简单:方法职责不清晰,不单一,不要让别人猜测这个方法可能是用来处理什么逻辑的。比较好的设计如下 +- 一个方法承担一个职责 +![](https://img-blog.csdnimg.cn/img_convert/21d3889d0f1a23a242e5a24cc464dea5.png) + +通过类图可知,如果要修改用户名称,就调用changeUserName方法 +要修改家庭地址,就调用changeHomeAddress方法 +要修改单位电话,就调用changeOfficeTel方法 +每个方法的职责非常清晰明确,不仅开发简单,而且日后的维护也非常容易,大家可以逐渐养成这样的习惯。 + + + +# 5 最佳实践 +是的,类的单一职责确实受非常多因素的制约,纯理论地来讲,这个原则是非常优秀的,但是现实有现实的难处,你必须去考虑项目工期、成本、人员技术水平、硬件情况、网络情况甚至有时候还要考虑政府政策、垄断协议等因素。 +对于单一职责原则,建议是接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。 + +参考 +- 《设计模式之蝉》 \ No newline at end of file diff --git "a/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(2)-\345\274\200\351\227\255\345\216\237\345\210\231.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(2)-\345\274\200\351\227\255\345\216\237\345\210\231.md" new file mode 100644 index 0000000000..ad01b893bf --- /dev/null +++ "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(2)-\345\274\200\351\227\255\345\216\237\345\210\231.md" @@ -0,0 +1,100 @@ +# 1 看得懂定义吗? +`Software entities like classes,modules and functions should be open for extension but closed for modifications` +一个软件实体如类、模块和方法应该对扩展开放,对修改关闭。 + +官方定义就是这么让人魔怔。对扩展开放?开放什么?对修改关闭,怎么关闭? +![](https://img-blog.csdnimg.cn/20210531162533878.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +- 用抽象构建框架,用实现扩展细节 +一个软件实体应该通过扩展来实现变化,而不是通过修改已有代码来实现变化。它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。 + +# 2 代码示例 +- 书籍接口 +![](https://img-blog.csdnimg.cn/20210531170731356.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +- Java书籍实现类 +![](https://img-blog.csdnimg.cn/20210531170902658.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +- 测试类 +![](https://img-blog.csdnimg.cn/20210531170942849.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +现在想添加一个折扣优惠方法:若直接修改原接口,则每个实现类都得重新添加方法实现。 + +但接口应该是稳定的,不应频繁修改! + +- Java 书籍折扣类 +![](https://img-blog.csdnimg.cn/20210531171331822.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +- 现在的 UML 图 +![](https://img-blog.csdnimg.cn/20210531171412248.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +`接口应该是稳定且可靠的,不应该经常发生变化`,否则接口作为契约的作用就失去了效能。 + +- 修改实现类 +直接在getPrice()中实现打折处理,好办法,我相信大家在项目中经常使用的就是这样的办法,通过class文件替换的方式可以完成部分业务变化(或是缺陷修复)。 +该方法在项目有明确的章程(团队内约束)或优良的架构设计时,是一个非常优秀的方法,但是该方法还是有缺陷的。 +例如采购书籍人员也是要看价格的,由于该方法已经实现了打折处理价格,因此采购人员看到的也是打折后的价格,会`因信息不对称而出现决策失误`的情况。因此,该方案也不是一个最优的方案。 + +- 通过扩展实现变化 +`增加一个子类`OffNovelBook,覆写getPrice方法,高层次的模块(static静态模块区)通过OffNovelBook类产生新的对象,完成业务变化对系统的最小化开发。好办法,修改也少,风险也小。 + +开闭原则对扩展开放,对修改关闭,但并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。 + +变化可归纳为如下类型: +- 逻辑变化 +只变化一个逻辑,不涉及其它模块。比如原有的一个算法是`a*b+c`,现在需要修改为`a*b*c`,可以通过修改原有类中的方法完成,前提条件是所有依赖或关联类都按照相同的逻辑处理。 +- 子模块变化 +一个模块变化,会对其他的模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化时,高层次的模块修改是必然的。 +- 可见视图变化 +可见视图是提供给客户使用的界面,如Swing。若仅是按钮、文字的重新排布倒是简单,最司空见惯的是业务耦合变化,什么意思呢?一个展示数据的列表,按照原有的需求是6列,突然有一天要增加1列,而且这一列要跨N张表,处理M个逻辑才能展现出来,这样的变化是比较恐怖的,但还是可以通过扩展来完成变化。 + +所以放弃修改历史的想法吧,一个项目的基本路径应该这样:项目开发、重构、测试、投产、运维,其中的重构可以对原有的设计和代码进行修改,运维尽量减少对原有代码的修改,保持历史代码的纯洁性,提高系统的稳定性。 +# 为什么选择开闭原则 +## 开闭原则对测试的影响 +有变化提出时,我们就需要考虑一下,原有的健壮代码是否可以不修改,仅仅通过扩展实现变化呢? +否则,就需要把原有的测试过程回笼一遍,需要进行单元测试、功能测试、集成测试甚至是验收测试。 + +以上面提到的书店售书为例,IBook接口写完了,实现类NovelBook也写好了,我们需要写一个测试类进行测试,测试类如代码 +```java +public class NovelBookTest extends TestCase { + private String name = "平凡的世界"; + private int price = 6000; + private String author = "路遥"; + private IBook novelBook = new NovelBook(name,price,author); + //测试getPrice方法 + public void testGetPrice() { + //原价销售,根据输入和输出的值是否相等进行断言 + super.assertEquals(this.price, this.novelBook.getPrice()); + } +} +``` +若加个打折销售需求,直接修改getPrice,那就要修改单元测试类。而且在实际项目中,一个类一般只有一个测试类,其中可以有很多的测试方法,在一堆本来就很复杂的断言中进行大量修改,难免出现测试遗漏。 + +所以,需要通过扩展实现业务逻辑变化,而非修改。可通过增加一个子类OffNovelBook完成业务需求变化,这对测试有什么好处呢? +重新生成一个测试文件OffNovelBookTest,然后对getPrice进行测试,单元测试是孤立测试,只要保证我提供的方法正确就成,其他的不管。 +```java +public class OffNovelBookTest extends TestCase { + private IBook below40NovelBook = new OffNovelBook("平凡的世界",3000,"路遥"); + private IBook above40NovelBook = new OffNovelBook("平凡的世界",6000,"路遥"); + // 测试低于40元的数据是否是打8折 + public void testGetPriceBelow40() { + super.assertEquals(2400, this.below40NovelBook.getPrice()); + } + // 测试大于40的书籍是否是打9折 + public void testGetPriceAbove40(){ + super.assertEquals(5400, this.above40NovelBook.getPrice()); + } +} +``` +新增加的类,新增加的测试方法,只要保证新增加类是正确的就可以了。 +## 提高复用性 +OOP中,所有逻辑都是从原子逻辑组合而来,而非在一个类中独立实现一个业务逻辑。只有这样代码才可复用,粒度越小,被复用可能性越大。 +- 为什么要复用? +减少代码量,避免相同逻辑分散,避免后来的维护人员为修改一个小bug或加个新功能而在整个项目中到处查找相关代码,然后发出对开发人员“极度失望”的感慨。 +- 如何才能提高复用率? +缩小逻辑粒度,直到一个逻辑不可再拆分为止。 +## 提高可维护性 +一款软件投产后,维护人员的工作不仅仅是对数据进行维护,还可能要对程序进行扩展,维护人员最乐意做的事情就是扩展一个类,而非修改一个类,甭管原有代码写得好坏,让维护人员读懂原有代码,然后再修改,是炼狱!不要让他在原有代码海洋里瞎游完毕后再修改,那是对维护人员的摧残。 + +## OOP +万物皆对象,我们需要把所有的事物都抽象成对象,然后针对对象进行操作,但运动是一定的,有运动就有变化,有变化就要有策略去应对,怎么快速应对呢?这就需要在设计之初考虑到所有可能变化的因素,然后留下接口,等待“可能”转为“现实”。 + +- 优点 +提高软件系统的可复用性及可维护性 \ No newline at end of file diff --git "a/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(3)-\344\276\235\350\265\226\345\200\222\347\275\256\345\216\237\345\210\231.md" "b/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(3)-\344\276\235\350\265\226\345\200\222\347\275\256\345\216\237\345\210\231.md" new file mode 100644 index 0000000000..e69de29bb2 diff --git "a/\351\207\215\346\236\204/OOP \344\270\211\345\244\247\347\211\271\345\276\201\344\271\213\345\244\232\346\200\201\357\274\210Polymorphism\357\274\211.md" "b/\351\207\215\346\236\204/OOP \344\270\211\345\244\247\347\211\271\345\276\201\344\271\213\345\244\232\346\200\201\357\274\210Polymorphism\357\274\211.md" deleted file mode 100644 index f0ece83792..0000000000 --- "a/\351\207\215\346\236\204/OOP \344\270\211\345\244\247\347\211\271\345\276\201\344\271\213\345\244\232\346\200\201\357\274\210Polymorphism\357\274\211.md" +++ /dev/null @@ -1,147 +0,0 @@ -OOP三大特性最重要的:多态。 - -很多程序员虽然在用支持OOP的语言,但却从未用过多态。 - -- 只使用封装、继承的编程方式,称为基于对象(Object Based)编程 -- 只有加入多态,才能称为OOP -没写过多态,就是没写过OO代码。 - -正是有了多态,软件设计才有更大弹性,更好拥抱变化。 -# 如何理解多态? -多态,即一个接口,多种形态。 - -一个draw方法,以正方形调用,则画正方形;以圆形调用,则画圆形: -![](https://img-blog.csdnimg.cn/26163eccb42440899cbf633925961e08.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -继承的两种方式之一的实现继承,请尽可能用组合替代。而接口继承,主要是给多态用的。 - -因为重点在于继承体系的使用者,主要考虑父类,而非子类。 -如下代码段,不必考虑具体形状是啥,仅需调用它的draw方法 -![](https://img-blog.csdnimg.cn/986e35f6e6134ec897c497265a6b23f5.png) -优势在于,一旦有新变化,比如将正方形换成圆,除了变量初始化,其它代码不需要动。 - -> 既然多态这么好,为什么很多人感觉无法在项目中自如地多态? - -多态需构建抽象。 - -# 构建抽象 -找出不同事物的共同点,这是最具挑战的。令人懵逼的也往往是眼中的不同之处。在很多人眼里,鸡就是鸡,鸭就是鸭。 - -寻找共同点,根基还是**分离关注点**。 -当你能看出鸡、鸭都有羽毛,都养在家里,你才可能识别“家禽”。 - -> 构建出的抽象会以接口(此处接口不一定是个语法,而是一个类型的约束)体现。所以,本文讨论的多态范畴内,接口、抽象类、父类等概念等价,统一称为接口。 - -## 接口的意义 -### 接口隔离了变化部分、不变部分 -- 不变部分 -接口的约定 -- 变化部分 -子类各自的实现 - -最影响程序的就是各种变化。有时需求来了,你的代码就得跟着改,一个可能的原因就是各种代码混在了一起。 -比如,一个通信协议的调整,你要改业务逻辑,这明显不合理。 -所以识别出变化与不变,是区分程序员水平的一大标准。 - -### 接口是边界 -清晰界定系统内不同模块的职责很关键,而模块间彼此通信最重要的就是通信协议,对应到代码中的接口。 - -很多程序员在接口中添加方法很随意,因为他们眼里,不存在实现者和使用者的角色差异,导致没有清晰边界,后果就是模块定义随意,彼此之间互相耦合,最终玩死自己。 - -所以,理解多态在于理解接口,理解接口在于谨慎选择接口中的方法。 - -面向接口编程的价值就源于多态。 - -这些原则你可能都听说过,但写代码时,就会忽略细节。 -比如: -![](https://img-blog.csdnimg.cn/491f7d59c37849fb8790c5a3e42edf3d.png) -这显然没有面向接口编程,推荐写法: -![](https://img-blog.csdnimg.cn/855dad01a2bb4dc08f4a2cb9b7d6474b.png) -差别就在于变量类型,是面向一个接口,还是面向一个具体实现类。 - -多态对程序员的要求更高,需要你能感知未来变化! - -# 实现多态 -**OOP会限制使用函数指针,它是对程序控制权的间接转移施加了约束。** -理解这句话,就要理解多态如何实现的。 - -Linux文件系统用C实现了OOP,就是用了函数指针: -![](https://img-blog.csdnimg.cn/140c9d5229104bd2b859041322e29283.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -即可这样赋值: -![](https://img-blog.csdnimg.cn/d58e3218f6934aea98b1958a7abc2094.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -给该结构体赋不同值,就能实现不同文件系统。 -但这样非常不安全。既然是个结构体字段,就可能改写它: -![](https://img-blog.csdnimg.cn/28209b781f58425a80d0dc49cd5e391b.png) -本该在hellofs_read运行的代码,跑进了sillyfs_read,程序崩溃。对于C这种灵活语言,你无法禁止这种操作,只能靠人为规定和代码检查。 - -到了OOP 语言,这种做法由一种编程结构变成一种语法。给函数指针赋值的操作下沉到了运行时去实现。运行时的实现,就是个查表过程: -![](https://img-blog.csdnimg.cn/deb97483fcb544aa94513061a2f97ff7.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -一个类在编译时,会给其中的函数在虚拟函数表中找个位置,把函数指针地址写进去,不同子类对应不同虚拟表。 -当用接口去调用对应函数时,实际上完成的就是在对应虚拟函数表的一个偏移,不管现在面对哪个子类,都可找到相应实现函数。 - -C++这种注重运行时消耗的语言: -- 只有virtual函数会出现在虚拟函数表 -- 普通函数就是直接的函数调用,以此减少消耗 - -对于Java程序员,可通过给无需改写的方法添加final帮助运行时优化。 - -当多态成为语法,就限制了函数指针的使用,犯错率大大降低! - -# 没有继承的多态 -封装,多态。至于继承,却不是必然选项。只要能够遵循相同接口,即可表现出多态,所以,多态并不一定要依赖继承。 - -动态语言中一个常见说法 - Duck Typing,若走起来像鸭子,叫起来像鸭子,那它就是鸭子。 -两个类可不在同一继承体系下,但只要有相同接口,就是一种多态。 - -如下代码段:Duck和FakeDuck不在一棵继承树上,但make_quack调用时,它们俩都可传进去。 -![](https://img-blog.csdnimg.cn/5ead17c395794be4a5123eba5095ff47.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_10,color_FFFFFF,t_70,g_se,x_16) - -很多软件都有插件能力,而插件结构本身就是多态。 -比如,著名的开源图形处理软件GIMP,它自身是用C开发的,为它编写插件就需要按照它规定的结构去编写代码: - -```c -struct GimpPlugInInfo -{ - /* GIMP 应用初始启动时调用 */ - GimpInitProc init_proc; - - /* GIMP 应用退出时调用 */ - GimpQuitProc quit_proc; - - /* GIMP 查询插件能力时调用 */ - GimpQueryProc query_proc; - - /* 插件安装之后,开始运行时调用*/ - GimpRunProc run_proc; -}; -``` - -我们所需做的就是按照这个结构声明出PLUG_IN_INFO,这是隐藏的名字,将插件的能力注册给GIMP这个应用: - -```c -GimpPlugInInfo PLUG_IN_INFO = { - init, - quit, - query, - run -}; -``` -这里用的C语言,但依然能表现多态。 - -多态依赖于继承,这只是某些程序设计语言自身的特点。在面向对象本身的体系中,封装和多态才是重中之重,而继承则很尴尬。 - -**一定要跳出单一语言的局限,这样,才能对各种编程思想有更本质的认识。** - -OOP三大特点的地位: -- 封装是面向对象的根基,软件就是靠各种封装好的对象逐步组合出来的 -- 继承给了继承体系内的所有对象一个约束,让它们有了统一的行为 -- 多态让整个体系能够更好地应对未来的变化。 - -# FAQ -某系统需要对普通用户增删改查,后来加了超级管理员用户也需要增删改查。把用户的操作抽象成接口方法,让普通用户和管理员用户实现接口方法…… 那么问题来了,这些接口方法的出入参没法完全共用,比如查询用户信息接口,普通用户和超级管理员用户的返回体信息字段不同。所以没法抽象,请问一下老师这种应不应该抽象呢?如果应该做成抽象需要怎么分离变的部分呢 - -应该分,因为管理员和普通用户的关注点是不同的。管理员和普通用户可以分别提供接口,分别提供相应的内容。 -如果说非要二者共用,可以考虑在服务层共用,在接口层面分开,在接口层去适配不同的接口。 -# 总结 -多态是基于对象和面向对象的分水岭。多态就是接口一样,实现不同。 -**建立起恰当抽象,面向接口编程。** \ No newline at end of file diff --git "a/\351\207\215\346\236\204/OOP\344\270\211\345\244\247\347\211\271\346\200\247\344\271\213\345\260\201\350\243\205.md" "b/\351\207\215\346\236\204/OOP\344\270\211\345\244\247\347\211\271\346\200\247\344\271\213\345\260\201\350\243\205.md" deleted file mode 100644 index 881de22e73..0000000000 --- "a/\351\207\215\346\236\204/OOP\344\270\211\345\244\247\347\211\271\346\200\247\344\271\213\345\260\201\350\243\205.md" +++ /dev/null @@ -1,118 +0,0 @@ -像C语言这种结构化编程帮助我们解决了很多问题,但随现代应用系统的代码量剧增,其局限也越发明显:各模块依赖关系太强,不能有效隔离变化。 - -于是,OOP诞生。但对于大部分初学就是C语言的开发人员,习惯了结构化编程思维,认为: -```java -OO=数据+函数 -``` -不能说是错的,但层次太低。结构化编程思维就如管中窥豹,只能看到局部。想要用好OOP,则需更宏观的视野。 - -# 如何理解封装 -OO是解决更大规模应用开发的一种尝试,它提升了程序员管理程序的尺度。 - -封装,是OO的根基: -- 它把紧密相关的信息放在一起,形成一个单元 -- 若该单元稳定,即可将该单元和其它单元继续组合,构成更大单元 -- 同理,继续构建更大单元,层层封装变大 -# OO的初心 -OO由Alan Kay提出,图灵奖获得者。其最初构想,对象就是细胞。将细胞组织起来,组成身体各器官,再组织起来,就构成人体。而当你去观察人时,就不用再去考虑每个细胞如何。所以,OO提供了更宏观思维。 - -但这一切的前提:每个对象要构建好,即封装要做好。就像每个细胞都有细胞壁将其与外界隔离,形成一个完整个体。 - -Kay强调**对象之间只能通过消息通信**。 -按如今程序设计语言通常做法,发消息就是方法调用,对象之间就是靠方法调用通信。 - -> 但这方法调用并非简单地把对象内部的数据通过方法暴露。Kay的构想甚至想把数据去掉。 - -因为封装的重点在于**对象提供了哪些行为,而非有哪些数据**。 -即便我们把对象理解成数据+函数,数据、函数也不是对等的: -- 函数是接口 -接口是稳定的 -- 数据是内部的实现 -实现是易变的,应该隐藏 - -很多人的开发习惯:写一个类,写其一堆字段,然后生成一堆getter、setter,暴露这些字段的访问。 -这种做法是错误的,它把数据当成设计核心,这一堆getter、setter,就等于暴露实现细节。 - -**正确做法**: -1. 设计一个类,先考虑对象应提供哪些**行为** -2. 然后,根据这些行为提供对应方法 -3. 最后考虑实现这些方法要有哪些**字段** - -所以连接二者的是方法,其命名就是个大学问了,应体现你的意图,而非具体怎么做的。所以,getXXX和setXXX绝不是个好命名。 -比如,设计:用户修改密码。 - -一些人上手就来: -![](https://img-blog.csdnimg.cn/5ab9f7f30f824b32b24444885ef1c1ee.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_19,color_FFFFFF,t_70,g_se,x_16) - -但推荐写法是表达你的意图: -![](https://img-blog.csdnimg.cn/3d818b23e99d43b7bdbabf50b007ddd3.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -两段代码只是修改密码的方法名不同,但更重要的差异是: -- 一个在说做什么 -- 一个在说怎么做 - -**将意图与实现分离**,优秀设计须考虑的问题。 - -实际项目中,有时确实需要暴露一些数据。 -所以,当确实需暴露时,再写getter也不迟,你一定要问自己为何要加getter? -关于setter: -- 大概率是你用错名字,应该用一个表达意图的名字 -- setter通常意味着修改,这是不推荐的 - -可变的对象会带来很多的问题,后续再深入讨论。所以,设计中更好的做法是设计不变类。 - -Lombok很好,少写很多代码,但必须限制它的使用,像Data和Setter都不该用。Java Bean本来也不是应该用在所有情况下的技术,导致很多人误用。 -# 减少接口的暴露 -之所以需要封装,就是要构建一个内聚单元。所以,要减少该单元对外的暴露: -- 减少内部实现细节的暴露 -- 减少对外暴露的接口 - -OOP语言都支持public、private,日常开发经常会轻率地给一个方法加public,不经意间暴露了一些本是内部实现的部分。 - -比如,一个服务要停下来时,你可能要把一些任务都停下来: -![](https://img-blog.csdnimg.cn/498578fc8ea2413dadd84961d0067381.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_14,color_FFFFFF,t_70,g_se,x_16) - -别人可能这样调用时: -![](https://img-blog.csdnimg.cn/bd93854e9f3a4acb8ecf56f37b3a7122.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_13,color_FFFFFF,t_70,g_se,x_16) - -突然某天,你发现停止轮询任务必须在停止定时器任务之前,你就不得不要求别人改代码。而这一切就是因为我们很草率地给那两个方法加上public,让别人有机会看到这俩方法。 - -设计角度,必须谨慎自省:这个方法有必要暴露吗? -其实可仅暴露一个方法: -![](https://img-blog.csdnimg.cn/18eaf9df12fe4f56b6a32cf226053673.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_14,color_FFFFFF,t_70,g_se,x_16)外部的调用代码也会简化: -![](https://img-blog.csdnimg.cn/85533a09a5cf4e8180b361b02bed0b65.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_11,color_FFFFFF,t_70,g_se,x_16) -尽可能减少接口暴露,该原则适于类的设计、系统设计。 -很多人都特别随意在系统里添加接口,让一个看似不复杂的系统,随便就有成百上千个接口。 - -后续,当你想改造系统,去掉一些接口时,很可能造成线上事故,因为你根本不知道哪个团队在何时用到了它。 -所以,软件设计中,谨慎暴露接口! - -可总结出:最小化接口暴露,即每增加一个接口,都要找到一个充分的理由! -# 总结 -封装,除了要减少内部实现细节的暴露,还要减少对外接口的暴露。每暴露一个公共API就增加一份职责,所以在每次暴露API时就要问自己,这个职责是自己必要的,还是有可能会增加不必要的负担。 -一个原则是最小化接口暴露。 - -注意区分: -- OO和 Java 语言 -- 传输数据和业务对象 - -Java语言特点就是一切皆对象,Java中对象的概念跟OO中对象的概念不同: -- 前者是语言特性 -- 后者是一种编程范式 - -在具体编码中,哪些属于对象,哪些不属于对象,应该是程序员掌控。 -比如: -- DDD中的领域实体,就是对象,需仔细设计其行为接口 -- 一些POJO,可看成数据载体,可直接加getter、setter的(没有这些默认getter、setter,很多第三方数据转化都很不方便,比如JSON,SQL)。使用时,不归结为对象即可 - -**基于行为进行封装,不要暴露实现细节,最小化接口暴露。** - -Demeter 不是一个人,而是一个项目,项目主页 http://www.ccs.neu.edu/research/demeter/。最早提到迪米特法则的论文出版于 1989 年,Assuring good style for object-oriented programs。还有一本书,1996 年出版,Adaptive Object-Oriented Software: The Demeter Method with Propagation Patterns。没有看过。 - -Demeter 是希腊神话中的大地和丰收女神,也叫做德墨忒尔。 - -迪米特法则简单的说,分为两个部分:不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。其实如果用另一个名字“最小知识原则”可能更容易理解一些,这个也算是程序员的“黑话”吧。 - -虽然接触OOP已经很久了,不过写程序时,大多数人还是习惯“一个对象一张表”,也没有太多考虑封装。整个类里都是 getter、setter 的事情也做过,这就像是用“面向对象的语言写面向过程的代码”。 - -> 参考 -> - https://www2.ccs.neu.edu/research/demeter/ \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\344\273\200\344\271\210\346\230\257\345\210\206\347\246\273\345\205\263\346\263\250\347\202\271\357\274\237.md" "b/\351\207\215\346\236\204/\344\273\200\344\271\210\346\230\257\345\210\206\347\246\273\345\205\263\346\263\250\347\202\271\357\274\237.md" deleted file mode 100644 index 084836a16a..0000000000 --- "a/\351\207\215\346\236\204/\344\273\200\344\271\210\346\230\257\345\210\206\347\246\273\345\205\263\346\263\250\347\202\271\357\274\237.md" +++ /dev/null @@ -1,182 +0,0 @@ -软件开发就是在解决问题。 -- 那问题一般如何解决? -最常见的解决思路是分而治之。但如何分解、组合,就是软件设计中考虑的问题。 - -然而,软件设计环节的大部分人都聚焦如何组合,忽略了第一步:分解。 -“分解?不就是把一个大系统拆成子系统,再把子系统拆成模块,一层层拆下去呗。” -这种程度分解远远不够,因为粒度太大了,就会导致不同东西混淆一起。听不懂?那就看个案例吧。 -# 技术和业务耦合 -某清结算系统,一开始觉得是个业务规则比较多的系统,偶尔出点故障,也情有可原。 -但分析系统故障报告后,发现这个系统设计得极其复杂。 -有一处:上游系统以推送方式向这个系统发消息。在原本的实现中,开发人员发现这个过程可能会丢消息,于是设计了一个补偿机制: -因为推送过来的数据是之前由这个系统发出去的,它本身有这些数据的初始信息,于是,开发人员就在DB增加一个状态,记录消息返回的情况。 -一旦发现丢消息,该系统就会访问上游接口,将丢失的数据请求回来。 - -就是这补偿机制设计,带来了后续问题。比如,当系统业务量增加时,DB访问压力本身很大,但在这时,丢数据概率也增加了,用于补偿的线程也会频繁访问DB,因为它要找出丢失数据,还要把请求回来的数据写回DB。 - -即一旦业务量上升,本来就吃力的系统,负担更重,系统卡顿在所难免。 - -补偿机制的设计有问题,在于上游系统向下游推消息,应该是个通信层面问题。而在原有的设计中,因为那个状态的添加,这个问题被带到业务层面。 -典型的分解没有做好,分解粒度太大。开发只考虑业务功能,忽视其他维度。技术和业务混在一起。 - -> 那到底怎么设计呢? - -既然是否丢消息是通信层面的事,争取在通信层解决。 -当时的解决方案是,选择吞吐量更大的MQ。在未来可见的业务量下,消息都不会丢。通信层面的问题在通信层面解决了,业务层面也就不会受到影响了。改造后,系统的稳定性果然得到大幅提升。 - -- 这样上游系统的补偿接口,现在也不需要了,上游系统得到简化 -- 这个系统里那个表示状态的字段,其实还被用在了业务处理中,也引发过其他问题,现在它只用在业务处理中,角色单一了,相关问题也就少了 - -有人见怪了,觉得补偿机制还是要的吧,就算换吞吐量大的消息队列,丢失消息还是有可能出现的,只是几率小很多。只是之前的补偿机制设计得不合理? -若分析是不是丢消息,就要看它何时会丢消息。之前的业务丢消息是因为MQ处理不过来,而换了吞吐更好的MQ就不存在该问题了。 -其实,真正需要的是可靠的信息传送通道,至于是不是MQ不重要。若怕丢消息,可在生产端重试,在消费者端做幂等。补偿是一个能把场景弄复杂的做法,不推荐。 - -常见的还有要区分技术异常和业务异常的。技术层面的异常信息不应该暴露给上层的业务人员。典型的例子就是大型网站的错误页面,而不是直接把后台的npe堆栈信息抛给用户。 - -技术与业务的分割线太模糊。代码的重构优化会点,但是分离关注点就涉及到具体的业务了,具体业务的划分与分离就又迷茫了。 - -简单区分: -- 业务人员能理解的就是业务 -比如,订单 -- 业务人员不理解的就是技术 -比如,多线程 - -软件设计都期望将粒度分解越小越好,但又嫌分解太小过于麻烦。就像很多人希望别人写好文档,自己却不写。 - -业务代码和技术实现往往被混写在一起,都是因为分离不够! -# 分离关注点 -看来分解粒度太大是不太好哦。那到底该如何考虑分解? - -传统上,我们习惯的分解问题的方式是树型。 -比如,按功能分解,可分为:功能1、2、3等,然后,每个功能再分成功能1.1、功能1.2、功能2.1、功能3.1等。 - -只从业务上看,似乎没问题。但要实现一个真实的系统,不仅要考虑功能性需求,还要考虑非功能性需求。 -比如数据不能丢失、有的系统还要求处理速度快。 - -这与业务不是同维度,设计时要能发现这些非功能性需求。 -分解问题时候,会有很多维度,每个都代表着一个关注点,这就是设计中一个常见的说法,“分离关注点(Separation of concerns)”。 - -可以分离的关注点很多,最常见的就是把业务处理和技术实现两个关注点混杂。 - -> 如果现在业务的处理性能跟不上,你有什么办法解决吗? - -多线程!的确是一种解决方案。但若不限制地去修改这段代码成多线程,则会引入多线程相关问题,比如,各种资源竞争、数据同步,稍有不慎,更多 bug。 - -**写好业务规则**和**正确地处理多线程**,这是两个不同关注点,应该分离业务代码和多线程代码。 - -业务程序员基本都不该写多线程程序,应由专门程序员把并发处理封装成框架,提供给大家使用,写业务代码即可。 - -> Kent Beck 曾曰:我不准备在这本书里讲高并发问题,我的做法是把高并发问题从我的程序里移出去 - -把业务处理和技术实现混在一起的问题还有很多。 -比如经常问怎么处理分布式事务,怎么做分库分表等。更该问的是,业务需要分布式事务吗?我是不是业务划分不清楚,才造成DB压力? - -程序员最常犯的错误就是认为所有问题都是技术问题,总试图用技术解决所有问题。任何试图用技术去解决其他关注点的问题,只是越挣扎,陷得越深。 - -另外容易产生混淆的关注点是 -# 不同的数据变动方向 -做数据库访问用Spring Data JPA好,还是MyBatis好。Spring Data JPA简化了数据库访问,自动生成对应的SQL语句,而MyBatis则要自己手写SQL。 - -普通的增删改查用Spring Data JPA非常省事,但对于一些复杂场景,他会担心自动生成SQL的性能有问题,还是手写SQL优化来得直接。是不是挺纠结的? - -为什么需要复杂查询? -你会说有一些统计报表需要。 - -那你发现了其中混淆关注点的地方?普通的增删改查需要经常改动数据库,而复杂查询的使用频率其实很低。 - -之所以出现工具选择的困难,是因为把两种数据使用频率不同的场景混在一起。如果将前台访问(处理增删改查)和后台访问(统计报表)分开,纠结也就不复存在。 - -不同的数据变动方向还有很多,比如: -- 动静分离,就是把变和不变的内容分开 -- 读写分离,就是把读和写分开 -- 前面提到的高频和低频,也可以分解开; -…… - -不同的数据变动方向,就是一个潜在的、可以分离的关注点。 - -分离关注点,不只适用于宏观的层面。 -在微观的代码层面,你用同样的思维方式,也可以帮助你识别出一些混在一起的代码。比如,很多程序员很喜欢写setter,但你真的有那么多要改变的东西吗?实际上可能就是封装没做好而已。 - -分离关注点之所以重要,有两方面原因: -- 不同的关注点混在一起会带来一系列的问题,正如前面提到的各种问题 -- 当分解得足够细小,你就会发现不同模块的共性,才有机会把同样的信息聚合在一起。这会为软件设计的后续过程,也就是组合,做好准备。 - -# CQRS(Command Query Responsibility Segregation) -命令与查询职责分离,其分离了增删改与查询这两个关注点。 -- 静态上,拆分了这两块代码。使各自可以采用不同技术栈,做针对性的调优。动态上,切分了流量,能够更灵活的做资源分配。 - -- 查询服务的实现 -可以走从库,这有利于降低主库压力,也可以做到水平扩展。但需要注意数据延迟问题。在异步同步和同步多写上要做好权衡。 -也可都走主库,这时候查询服务最好增加缓存层,降低主库压力,而增删改服务要做好缓存的级联操作,以保证缓存时效 -当然也可以走非关系型数据库,搜索引擎类的es,solr,分布式存储的tidb等等,按需选择。 - -通常增删改会涉及到很多domain knowledge. 平时更多的操作其实是查询,不需要通过从持久化生成domain model到内存中再返回。 -# FAQ -## 订单系统 -- 先下单写到DB -- 然后发送消息给MQ - -这两步没法放到一个事务。 如果用本地消息表: -- order写DB -- 然后在写本地消息表 - -这两步就能放到一个事务了,保证肯定成功。 -然后再有线程读取本地消息表,MQ发消息,如果成功,更改本地消息表状态 。 -### 分析 -下单入库和发消息给下游确实是两个动作,但这两个动作的顺序一定是这样?一定要在一个线程完成吗?可不可以先发消息呢? -比如,把消息发给下游后,有个下游接收到消息后,再把消息入库。 -如果这样,发消息,由MQ保证消息不丢,下游入库,又可保证订单持久化。这种设计下,其实并不需要事务,也就不必为事务纠结了。 - -发现大家在工作中往往不做分离,分析需求的时候把方案揉在一起。可以怎样去练习做分离呢? -有一种从小事练起的方法,就是写代码时,把自己写的函数行数限定在一定的规模之下,比如,10行。超过10行的代码,你就要去仔细想想是否是有东西混在了一起。 -这种方法锻炼的就是找出不同关注点的思维习惯,一旦你具备了这种思维习惯,再去看大的设计,自然也会发现不同的关注点。 - -## 用户购买会员 -目前设计了两张表: -- 存储用户购买会员的所有记录 -- 存当前的会员信息 (主要是开始、结束时间,但没有会员等级之类) -设计这张表是为了SQL关联查询方便,不用再判断是否过期 - -但有个问题:我要用定时器一直扫这表,等会员过期了,就得删除对应记录。 -这么做的问题在哪? 更好的解决方式应该是什么?如果做到更细维度的拆分? - -首先,没有把业务和实现分清。 -业务是实现一个会员系统,涉及会员购买,主要是会员时间要延长,还会涉及会员资格的判断,即当前用户是否是会员。 - -基于这些内容判断,可以有不同实现。根据当前实现,可以这样: -购买会员; -- 若会员信息不存在,则添加会员信息 -- 会员信息存在,则修改会员结束时间 - -会员资格判别,根据用户 ID 和当前时间是否在时间范围内查询: -- 记录存在,则是会员 -- 否则不是 - -因此可考虑: -- 购买会员时,可产生会员购买记录,此记录仅供后续查询用 -- 只有当会员信息表过大时,才考虑是否需要删除 - -在这个实现中: -- 把 购买 和 会员信息 分开 -- 把 会员信息是否生效 与 记录是否删除 分开 -# 总结 -软件设计第一步:分解。 - -大多数系统的设计做得不够好,问题常常出现在分解这步就没做好。常见的分解问题就是分解的粒度太大,把各种维度混淆在一起。在设计中,将一个模块的不同维度分开,有一个专门的说法,叫分离关注点。 - -分离关注点很重要,一方面,不同的关注点混在一起会带来许多问题;另一方面,分离关注点有助于我们发现不同模块的共性,更好地进行设计。分离关注点,是我们在做设计的时候,需要时时绷起的一根弦。 - -- 分离关注点是一种意识,在设计中要意识到需要分离关注点。一旦自己在做设计时,出现纠结或者是觉得设计有些复杂,首先需要想想,是不是因为把不同的关注点混在了一起。这种意识是需要训练的,让自己从无意识中摆脱出来。 -一种可以考虑的做法是,把它与一些实践结合起来,比如,在设计评审的DoD中,增加一条“是否考虑了分离关注点”。 -- 分离关注点也要从小事开始练习 -比如,可以从编写小函数开始。给自己设定了一个目标,函数代码小于10行。每次写完代码,就可以对代码进行调整。超过10行代码的函数,问问自己,是不是有混在一起的内容? - - 如果有循环,循环里面的部分就是对单个元素的处理,可以提取到一个函数里 - - 如果有if...else,每种情况都可以单独放到一个函数里 - - 如果有多个 if...else,要问问自己是不是缺少了一些模型,是不是可以用多态解决 -... -如果经过练习,函数都能写短,那就可以开始做类的练习。把每个类写小,以此类推,逐步训练。《重构》里《代码的坏味道》可以成为我们改进的参考点。 - -只有在日常开发的过程中不断练习,才能成为我们的下意识反应。 - -**分离关注点,发现的关注点越多越好,粒度越小越好。** -![](https://img-blog.csdnimg.cn/99612de7ae6e46e2a8d0f1646a9d513a.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\344\275\240\346\200\216\344\271\210\346\200\273\346\230\257\350\203\275\345\206\231\345\207\272\344\270\244\344\270\211\345\215\203\350\241\214\347\232\204controller\347\261\273\357\274\237.md" "b/\351\207\215\346\236\204/\344\275\240\346\200\216\344\271\210\346\200\273\346\230\257\350\203\275\345\206\231\345\207\272\344\270\244\344\270\211\345\215\203\350\241\214\347\232\204controller\347\261\273\357\274\237.md" deleted file mode 100644 index 8fe5608175..0000000000 --- "a/\351\207\215\346\236\204/\344\275\240\346\200\216\344\271\210\346\200\273\346\230\257\350\203\275\345\206\231\345\207\272\344\270\244\344\270\211\345\215\203\350\241\214\347\232\204controller\347\261\273\357\274\237.md" +++ /dev/null @@ -1,126 +0,0 @@ -你一定经常见到一个两三千行的 controller 类,类之所以发展成如此庞大,有如下原因: -- 长函数太多 -- 类里面有特别多的字段和函数 -量变引起质变,可能每个函数都很短小,但数量太多 -# 1 程序的modularity -你思考过为什么你不会把all code写到一个文件?因为你的潜意识里明白: -- 相同的功能模块无法复用 -- 复杂度远超出个人理解极限 - -一个人理解的东西是有限的,在国内互联网敏捷开发环境下,更没有人能熟悉所有代码细节。 - -解决复杂的最有效方案就是分而治之。所以,各种程序设计语言都有自己的模块划分(**modularity**)方案: -- 从最初的按文件划分 -- 到后来使用OO按类划分 - -开发者面对的不再是细节,而是模块,模块数量显然远比细节数量少,理解成本大大降低,开发效率也提高了,再也不用 996, 每天都能和妹纸多聊几句了。 - -modularity,本质就是分解问题,其背后原因,就是个人理解能力有限。 - -> 说这么多我都懂,那到底怎么把大类拆成小类? - -# 2 大类是怎么来的? -## 2.1 职责不单一 -**最容易产生大类的原因**。 - -CR一段代码: -![](https://img-blog.csdnimg.cn/6544c51823ed4e45a1182220fae18d03.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -该类持有大类的典型特征,包含一坨字段:这些字段都缺一不可吗? -- userId、name、nickname等应该是一个用户的基本信息 -- email、phoneNumber 也算是和用户相关联 -很多应用都提供使用邮箱或手机号登录方式,所以,这些信息放在这里,也能理解 -- authorType,作者类型,表示作者是签约作者还是普通作者,签约作者可设置作品的付费信息,但普通作者无此权限 -- authorReviewStatus,作者审核状态,作者成为签约作者,需要有一个申请审核的过程,该状态字段就是审核状态 -- editorType,编辑类型,编辑可以是主编,也可以是小编,权限不同 - -这还不是 User 类的全部。但只看这些内容就能看出问题: -- 普通用户既不是作者,也不是编辑 -作者和编辑这些相关字段,对普通用户无意义 -- 对那些成为作者的用户,编辑的信息意义不大 -因为作者不能成为编辑。编辑也不会成为作者,作者信息对成为编辑的用户无意义 - -总有一些信息对一部分人毫无意义,但对另一部分人又必需。出现该问题的症结在于只有“一个”用户类。 - -普通用户、作者、编辑,三种不同角色,来自不同业务方,关心的是不同内容。仅因为它们都是同一系统的用户,就把它们都放到一个用户类,导致任何业务方的需求变动,都会反复修改该类,**严重违反单一职责原则**。 -所以破题的关键就是职责拆分。 - -虽然这是一个类,但它把不同角色关心的东西都放在一起,就愈发得臃肿了。 - -只需将不同信息拆分即可: -```java -public class User { - private long userId; - private String name; - private String nickname; - private String email; - private String phoneNumber; - ... -} - -public class Author { - private long userId; - private AuthorType authorType; - private ReviewStatus authorReviewStatus; - ... -} - -public class Editor { - private long userId; - private EditorType editorType; - ... -} -``` -拆出 Author、Editor 两个类,将和作者、编辑相关的字段分别移至这两个类里。 -这俩类分别有个 userId 字段,用于关联该角色和具体用户。 -## 2.2 字段未分组 -有时觉得有些字段确实都属于某个类,结果就是,这个类还是很大。 - -之前拆分后的新 User 类: -```java -public class User { - private long userId; - private String name; - private String nickname; - private String email; - private String phoneNumber; - ... -} -``` -这些字段应该都算用户信息的一部分。但依然也不算是个小类,因为该类里的字段并不属于同一种类型的信息。 -如,userId、name、nickname算是用户的基本信息,而 email、phoneNumber 则属于用户的联系方式。 - -需求角度看,基本信息是那种一旦确定一般就不变的内容,而联系方式则会根据实际情况调整,如绑定各种社交账号。把这些信息都放到一个类里面,类稳定程度就差点。 - -据此,可将 User 类的字段分组: -```java -public class User { - private long userId; - private String name; - private String nickname; - private Contact contact; - ... -} - -public class Contact { - private String email; - private String phoneNumber; - ... -} -``` -引入一个 Contact 类(联系方式),把 email 和 phoneNumber 放了进去,后面再有任何关于联系方式的调整就都可以放在这个类里面。 -此次调整,把不同信息重新组合,但每个类都比原来要小。 - -前后两次拆分到底有何不同? -- 前面是根据职责,拆分出不同实体 -- 后面是将字段做了分组,用类把不同的信息分别封装 - -大类拆解成小类,本质上是个设计工作,依据单一职责设计原则。 - -若把大类都拆成小类,类的数量就会增多,那人们理解的成本是不是也会增加呢? -**这也是很多人不拆分大类的借口。** - -各种程序设计语言中,本就有如包、命名空间等机制,将各种类组合在一起。在你不需要展开细节时,面对的是一个类的集合。 -再进一步,还有各种程序库把这些打包出来的东西再进一步打包,让我们只要面对简单的接口,而不必关心各种细节。 - -软件正这样层层封装构建出来的。 \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\344\275\240\347\234\237\347\232\204\344\274\232\345\243\260\346\230\216\345\217\230\351\207\217\345\220\227\357\274\237.md" "b/\351\207\215\346\236\204/\344\275\240\347\234\237\347\232\204\344\274\232\345\243\260\346\230\216\345\217\230\351\207\217\345\220\227\357\274\237.md" deleted file mode 100644 index 7ea637469d..0000000000 --- "a/\351\207\215\346\236\204/\344\275\240\347\234\237\347\232\204\344\274\232\345\243\260\346\230\216\345\217\230\351\207\217\345\220\227\357\274\237.md" +++ /dev/null @@ -1,180 +0,0 @@ -一个变量声明怎么还有坏味道?难道变量声明都不让用? - -变量声明是写程序不可或缺的一部分,我并不打算让你戒掉变量声明,严格地说,我们是要把变量初始化这件事做好。 - -# 变量的初始化 -```java -EpubStatus status = null; -CreateEpubResponse response = createEpub(request); -if (response.getCode() == 201) { - status = EpubStatus.CREATED; -} else { - status = EpubStatus.TO_CREATE; -} -``` -向另外一个服务发请求创建 EPUB,若创建: -- 成功 -返回 HTTP 201,表示创建成功,然后就把状态置为 **CREATED** -- 失败 -则把状态置为 **TO_CREATE**。后面对 **TO_CREATE** 作品,还要再尝试创建 - -> 暂且是否要写 else 放下,毕竟这也是一个坏味道。 - -重点在 status 变量,虽然 status 在声明时,就赋个null,但实际上,这值没任何用,因为status值,其实是在经过后续处理之后,才有了真正值。 -即语义上,第一行的变量初始化没用,是假的初始化。 - -一个变量的初始化分为: -- 声明 -- 赋值 - -两个部分。变量初始化最好一次性完成。这段代码里的变量赋值是在声明很久后才完成,即变量初始化没有一次性完成。 - -这种代码真正的问题就是**不清晰,变量初始化与业务处理混在在一起**。 -通常这种代码后面紧接着就是一大堆复杂业务。当代码混在一起,必须从一堆业务逻辑里抽丝剥茧,才能理顺逻辑,知道变量到底怎么初始化。 - -这种代码在实际的代码库中出现的频率非常高,各种变形形态。 -有的变量在很远地方才做真正赋值,完成初始化,进一步增加了理解复杂度。 - -所以请变量一次性完成初始化。 - -重构代码: -```java -final CreateEpubResponse response = createEpub(request); -final EpubStatus status = toEpubStatus(response); - - -private EpubStatus toEpubStatus(final CreateEpubResponse response) { - if (response.getCode() == 201) { - return EpubStatus.CREATED; - } - return EpubStatus.TO_CREATE; -} -``` -提取出了一个函数,将 response 转成对应的内部的 EPUB状态。 - -很多人之所以这样写代码,一个重要的原因是很多人的编程习惯是从 C 语言来的。C 语言在早期的版本中,一个函数用到的变量必须在整个函数的一开始就声明出来。 -在 C 语言诞生的年代,当时计算机能力有限内存小,编译器技术也处于刚刚起步的阶段,把变量放在前面声明出来,有助于减小编译器编写的难度。到了 C++年代,这个限制就逐步放开了,所以,C++程序是支持变量随用随声明的。对于今天的大多数程序设计语言来说,这个限制早就不存在了,但很多人的编程习惯却留在了那个古老的年代。 - -在新的变量声明中,我加上了 final,在 Java 的语义中,一个变量加上了 final,也就意味着这个变量不能再次赋值。对,我们需要的正是这样的限制。 - -**尽可能编写不变的代码**。这里其实是这个话题的延伸,尽可能使用不变的量。 - -如果我们能够按照使用场景做一个区分,把变量初始化与业务处理分开,你会发现,在很多情况下,变量只在初始化完成之后赋值,就足以满足我们的需求了,在一段代码中,要使用可变量的场景不多。 - -**推广一下,在能使用 final 的地方尽量使用 final,限制变量的赋值!** -“能使用”,不仅包括普通的变量声明,还包含参数声明,还有类字段的声明,甚至类和方法的声明。 -尝试着调整自己现有代码,给变量声明都加上 final,你会发现许多值得重构的代码。 - -还有异常处理的场景,强迫你把变量的声明与初始化分开,就像下面这段代码: -```java -InputStream is = null; - - -try { - is = new FileInputStream(...); - ... -} catch (IOException e) { - ... -} finally { - if (is != null) { - is.close(); - } -} -``` -之所以要把 InputStream 变量 is 单独声明,是为了能够在 finanlly 块里面访问到。 -写成这样的原因是 Java 早期版本只能这样,而如果采用 Java 7 后版本,采用 `try-with-resource` 的写法,代码就可以更简洁了: -```java -try (InputStream is = new FileInputStream(...)) { - ... -} -``` -这样一来,InputStream 变量的初始化就一次性完成了,符合该原则! - -# 集合初始化 -```java -List permissions = new ArrayList<>(); -permissions.add(Permission.BOOK_READ); -permissions.add(Permission.BOOK_WRITE); -check.grantTo(Role.AUTHOR, permissions); -``` -给作者赋予作品读写权限。 -注意 permissions 集合,先给 permission 初始化成了一个 ArrayList,这时,permissions 虽然存在,但不会把它传给 grantTo,还不能直接使用,因为它还缺少必要信息。 -然后,将 BOOK_READ 和 BOOK_WRITE 两个枚举对象添加,这时的 permissions 对象才是需要的对象。 - -声明一个集合,然后,调用一堆添加的方法,将所需的对象添加进去,是不是就是你写代码的习性? - -其实 permissions 对象一开始的变量声明,并没有完成这个集合真正的初始化,只有当集合所需的对象添加完毕后,才是它应有的样子。 -即只有添加了元素的集合才是需要的。 - -是不是就发现和前面说的变量先声明后赋值,其实一回事,从一个变量的声明到初始化成一个可用状态,中间隔了太远。 - -之所以很多人习惯这么写,一个原因就是在早期的 Java 版本中,没有提供很好的集合初始化方法。这种代码,也是很多动态语言的支持者调侃 Java 啰嗦的靶子。 - -现如今,Java在这方面早已经改进了许多,各种程序库已经提供了一步到位的写法,我们先来看看 Java 9 之后的写法: -```java -List permissions = List.of( - Permission.BOOK_READ, - Permission.BOOK_WRITE -); -check.grantTo(Role.AUTHOR, permissions); -``` -如果你的项目还没有升级 Java 9 之后的版本,使用 Guava也可以: - -```java -List permissions = ImmutableList.of( - Permission.BOOK_READ, - Permission.BOOK_WRITE -); -check.grantTo(Role.AUTHOR, permissions); -``` -代码是不是清爽多了! - -用的一个 ImmutableList,即不可变 List,实际上,你查看第一段代码的实现就会发现,它也是一个不变 List。这是什么意思呢? -这个 List 一旦创建好了,就不能修改了,对应的实现就是各种添加、删除之类的方法全部都禁用了。 - -初看起来,这是限制了我们的能力,但我们对比一下代码就不难发现,很多时候,我们对于一个集合的使用,除了声明时添加元素之外,后续就只是把它当作一个只读集合。 -所以很多情况下,一个不变集合够用。 - -再复杂一些的,集合的声明和添加元素之间隔了很远,不注意的话,甚至不觉得它们是在完成一次初始化。 - -```java -private static Map CODE_MAPPING = new HashMap<>(); -... - - -static { - CODE_MAPPING.put(LOCALE.ENGLISH, "EN"); - CODE_MAPPING.put(LOCALE.CHINESE, "CH"); -} -``` -传输时的映射方案,将不同的语言版本映射为不同的代码。这里 **CODE_MAPPING** 是一个类的 static 变量,而这个类的声明里还有其它一些变量。所以,隔了很远之后,才有一个 static 块向这个集合添加元素。 - -如果我们能够用一次性声明的方式,这个单独的 static 块就是不需要的: - -```java -private static Map CODE_MAPPING = ImmutableMap.of( - LOCALE.ENGLISH, "EN", - LOCALE.CHINESE, "CH" -); -``` - -对比我们改造前后的代码,二者之间还有一个更关键的区别:前面的代码是命令式代码,而后面的代码是声明式。 - -命令式的代码,就是告诉你“怎么做”的代码,就像改造前的代码,声明一个集合,然后添加一个元素,再添加一个元素。而声明式的代码,是告诉你“做什么”的代码,改造后就是,我要一个包含了这两个元素的集合。 - -声明式的代码体现的意图,是更高层面的抽象,把意图和实现分开,从某种意义上来说,也是一种分离关注点。 - -所以,用声明式的标准来看代码,是一个发现代码坏味道的重要参考。 - -无论是变量的声明与赋值分离,还是初始化一个集合的分步骤,其实反映的都是不同时代编程风格的烙印。变量的声明是 C 早期的编程风格,异常处理是 Java 早期的风格,而集合声明也体现出不同版本 Java 的影子。 - -编程不仅仅是要学习实现功能,风格也要与时俱进。 - -# 总结 -变量的初始化包含变量的声明和赋值两个部分,一个编程的原则是“变量要一次性完成初始化”。 - -这就有个坏味道:变量的声明和赋值是分离的。二者分离带来的问题就是,把赋值的过程与业务处理混杂在一起。发现变量声明与赋值分离一个做法就是在声明前面加上 final,用“不变性”约束代码。 - -传统的集合初始化方式是命令式的,而今天我们完全可以用声明式的方式进行集合的初始化,让初始化的过程一次性完成。再进一步,以声明式的标准来看代码,会帮助我们发现许多的坏味道。 - -**请一次性完成变量的初始化!** \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\344\275\240\347\234\237\347\232\204\344\274\232\347\273\231\345\217\230\351\207\217\345\221\275\345\220\215\345\220\227\357\274\237.md" "b/\351\207\215\346\236\204/\344\275\240\347\234\237\347\232\204\344\274\232\347\273\231\345\217\230\351\207\217\345\221\275\345\220\215\345\220\227\357\274\237.md" deleted file mode 100644 index 0aaa2ef0d3..0000000000 --- "a/\351\207\215\346\236\204/\344\275\240\347\234\237\347\232\204\344\274\232\347\273\231\345\217\230\351\207\217\345\221\275\345\220\215\345\220\227\357\274\237.md" +++ /dev/null @@ -1,246 +0,0 @@ -有读者看到标题就开始敲键盘了,我知道,命名不就是不能用 abc、123 命名,名字要有意义嘛,这有什么好讲的? -然而,即便懂得了名字要有意义,很多程序员依然无法逃离命名沼泽。 - -# 不精准的命名 -什么叫精准? -废话不多说,CR 一段代码: -```java -public void processChapter(long chapterId) { - Chapter chapter = this.repository.findByChapterId(chapterId); - if (chapter == null) { - throw new IllegalArgumentException("Unknown chapter [" + chapterId + "]"); -t - } - - chapter.setTranslationState(TranslationState.TRANSLATING); - this.repository.save(chapter); -} -``` -看上去挺正常。 -但我问你,这段代码在干嘛?你就需要调动全部注意力,去认真阅读这段代码,找出其中逻辑。经过阅读发现,这段代码做的就是把一个章节的翻译状态改成翻译中。 - -> 为什么你需要阅读这段代码细节,才知道这段代码在干嘛? - -问题就在函数名,processChapter,这个函数确实是在处理章节,但这个名字太宽泛。如果说“将章节的翻译状态改成翻译中”叫做处理章节,那么: -- “将章节的翻译状态改成翻译完” -- “修改章节内容” - -是不是也能叫处理章节? -所以,如果各种场景都能叫处理章节,那么处理章节就是个宽泛名,没有错,但**不精准**! - -表面看,这个名字是有含义,但实际上,并不能有效反映这段代码含义。 -如果我在做的是一个信息处理系统,你根本无法判断,是一个电商平台,还是一个图书管理系统,从沟通的角度看,这就不是一个有效的沟通。要想理解它,你需要消耗大量认知成本,无论是时间,还是精力。 - -命名过于宽泛,不能精准描述,这是很多代码在命名上存在的严重问题,也是代码难以理解的根源所在。 - -或许这么说你的印象还是不深刻,看看下面这些词是不是经常出现在你的代码里:data、info、flag、process、handle、build、maintain、manage、modify 等等。这些名字都属于典型的过宽泛名字,当这些名字出现在你的代码里,多半是写代码的人当时没有想好用什么名字,就开始写代码了。 - -回到前面那段代码上,如果它不叫“处理章节”,那应该叫什么? -- 命名要能够描述出这段代码在做的事情 -这段代码在做的事情就是“将章节修改为翻译中”。那是不是它就应该叫 changeChapterToTranlsating呢? -相比于“处理章节”,changeChapterToTranlsating这个名字已经进了一步,然而,它也不算是一个好名字,因为它更多的是在描述这段代码在做的细节。 -之所以要将一段代码封装起来,是我们不想知道那么多细节。如果把细节平铺开来,那本质上和直接阅读代码细节差别不大。 -- 一个好的名字应该描述意图,而非细节 -就这段代码而言, 我们为什么要把翻译状态修改成翻译中,这一定是有**意图**。我们把翻译状态修改成翻译中,是因为我们在这里开启了一个翻译的过程。所以,这段函数应该命名 startTranslation。 -![](https://img-blog.csdnimg.cn/8aedb97fb18c45ec9d42a967830b52f5.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -# 用技术术语命名 -![](https://img-blog.csdnimg.cn/fb4c6e2047cf4a8680fd0738b12b53cf.png) - -常见得不能再常见的代码,但却隐藏另外一个典型得不能再典型的问题:用技术术语命名。 - -这个 bookList 变量之所以叫 bookList,原因就是它声明的类型是 List。这种命名在代码中几乎是随处可见的,比如 xxxMap、xxxSet。 - -这是一种不费脑子的命名方式,但这种命名却会带来很多问题,因为它是一种基于实现细节的命名方式。 - -面向接口编程,从另外一个角度理解,就是不要面向实现编程,因为接口是稳定的,而实现易变。虽然在大多数人的理解里,这个原则是针对类型的,但在命名上,我们也应该遵循同样的原则。为什么?我举个例子你就知道了。 - -比如,如果我发现,我现在需要的是一个不重复的作品集合,也就是说,我需要把这个变量的类型从 List 改成 Set。变量类型你一定会改,但变量名你会改吗?这还真不一定,一旦出现遗忘,就会出现一个奇特的现象,一个叫 bookList 的变量,它的类型是一个 Set。这样,一个新的混淆产生了。 - -有什么更好的名字吗?我们需要一个更面向意图的名字。其实,我们在这段代码里真正要表达的是拿到了一堆书,所以,这个名字可以命名成 books。 -List books = service.getBooks(); -这个名字其实更简单,但从表意的程度上来说,它却是一个更有效的名字。 - -虽然这里我们只是以变量为例说明了以技术术语命名存在的问题,事实上,在实际的代码中,技术名词的出现,往往就代表着它缺少了一个应有的模型。 - -比如,在业务代码里如果直接出现了 Redis: -![](https://img-blog.csdnimg.cn/bbde8fc7605c4bd39e2e190ad41245d8.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_17,color_FFFFFF,t_70,g_se,x_16) - - -通常来说,这里真正需要的是一个缓存。Redis 是缓存这个模型的一个实现: -![](https://img-blog.csdnimg.cn/046069b20b594824a996afccdc2f15cd.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_14,color_FFFFFF,t_70,g_se,x_16) -再进一步,缓存这个概念其实也是一个技术术语,从某种意义上说,它也不应该出现在业务代码。 -这方面做得比较好的是 Spring。使用 Spring 框架时,如果需要缓存,我们通常是加上一个 注解: -![](https://img-blog.csdnimg.cn/84151edee9094f27a8dc9c632235779b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_14,color_FFFFFF,t_70,g_se,x_16) - -之所以喜欢用技术名词去命名,一方面是因为,这是习惯的语言,另一方面也是因为学写代码,很大程度上是参考别人代码,而行业里面优秀的代码常常是一些开源项目,而这些开源项目往往是技术类项目。在一个技术类的项目中,这些技术术语其实就是它的业务语言。但对于业务项目,这个说法就必须重新审视了。 - -如果这个部分的代码确实就是处理一些技术,使用技术术语无可厚非,但如果是在处理业务,就要尽可能把技术术语隔离开来。 - -- xxxMap这种命名表示映射关系,比如:书id与书的映射关系,不能命名为bookIdMap么? -Map 表示的是一个数据结构,而映射关系我会写成 Mapping - -# 用业务语言写代码 -无论是不精准的命名也好,技术名词也罢,归根结底,体现的是同一个问题:对业务理解不到位。 - -编写可维护的代码要使用业务语言。怎么才知道自己的命名是否用的是业务语言呢? -把这个词讲给产品经理,看他知不知道是怎么回事。 - -从团队的角度看,让每个人根据自己的理解来命名,确实就有可能出现千奇百怪的名字,所以,一个良好的团队实践是,建立团队的词汇表,让团队成员有信息可以参考。 - -团队对于业务有了共同理解,我们也许就可以发现一些更高级的坏味道,比如说下面这个函数声明: -![](https://img-blog.csdnimg.cn/8155bcdf165d46679796a990f373b25b.png) - -确认章节内容审核通过。这里有一个问题,chapterId 是审核章节的 ID,这个没问题,但 userId 是什么呢?了解了一下背景,我们才知道,之所以这里要有一个 userId,是因为这里需要记录一下审核人的信息,这个 userId 就是审核人的 userId。 - -你看,通过业务的分析,我们会发现,这个 userId 并不是一个好的命名,因为它还需要更多的解释,更好的命名是 reviewerUserId,之所以起这个名字,因为这个用户在这个场景下扮演的角色是审核人(Reviewer)。 -![](https://img-blog.csdnimg.cn/c0339ff4ad3d4e01adf2fcb049af1c46.png) - -这个坏味道也是一种不精准的命名,但它不是那种一眼可见的坏味道,而是需要在业务层面上再进行讨论,所以,它是一种更高级的坏味道。 - -能够意识到自己的命名有问题,是程序员进阶的第一步。 - - -```java -@GetMapping("getTotalSettlementInfoByYear") -@ApiOperation("公司结算信息按年求和") -public Result> getTotalSettlementInfoByYear(@RequestParam String year) { -List list = repMonthCompanyService.getTotalSettlementInfoByYear(year); -return new Result>().ok(list); -} -``` -名字长不是问题,问题是表达是否清晰,像repMonthCompanyService这个名字,是不太容易一眼看出来含义的。 - -另外,传给 service 的参数是一个字符串,这个从逻辑上是有问题的,没有进行参数的校验。后面的内容也会讲到,这个做法是一种缺乏封装的表现。 - -变量名是 list,按照这一讲的说法是用技术术语在命名。 - -再有,这个 URI 是 getTotalSettlementInfoByYear,这是不符合 REST 的命名规范的,比如,动词不应该出现在 URI 里,分词应该是“-”,byYear 实际上是一个过滤条件等等。 - -> 不管是日本人设计的 Ruby还是巴西人设计的 Lua,各种语法采用的全都是英语。所以,想要成为一个优秀的程序员,会用英语写代码是必要的。 -> 你肯定听说过,国内有一些程序员用汉语拼音写代码,这就是一种典型坏味道。而且程序设计语言已支持 -> UTF-8,用汉语拼音写代码,还不如用汉字直接写代码。这场景太低级了,不过多讨论。 - -# 违反语法规则的命名 -CR一段代码: -```java -public void completedTranslate(final List chapterIds) { - List chapters = repository.findByChapterIdIn(chapterIds); - chapters.forEach(Chapter::completedTranslate); - repository.saveAll(chapters); -} -``` -乍看写得还不错,将一些章节信息标记为翻译完成。似乎方法名也能表达这意思,但经不起推敲。 -completedTranslate 并不是一个正常的英语方法名。从这个名字你能看出,作者想表达的是“完成翻译”,因为已经翻译完了,所以用完成时的 completed,而翻译是 translate。这个函数名就成了 completedTranslate。 - -一般命名规则是: -- 类名是个名词 -表示一个对象 -- 方法名是个动词或动宾短语 -表示一个动作 - -以此为标准判断,completedTranslate 并不是一个有效的动宾结构。如果把这个名字改成动宾结构,只要把“完成”译为 complete,“翻译”用成它的名词形式 translation 就可以了。所以,这个函数名可以改成 completeTranslation: - -```java -public void completeTranslation(final List chapterIds) { - ... -} -``` -这并不是个复杂的坏味道,但却随处可见。 -比如,一个函数名是 retranslation,其表达的意图是重新翻译,但作为函数名,它应该是一个动词,所以,正确的命名应该是 retranslate。 - -只要你懂得最基本的命名要求,知道最基本的英语规则,就完全能够发现这类坏味道。 - -# 不准确的英语词汇 -有一次,我们要实现一个章节审核的功能,一个同事先定义出了审核的状态: - -```java -public enum ChapterAuditStatus { - PENDING, - APPROVED, - REJECTED; -} -``` -有问题吗?看不出来,一点都不奇怪。如果你用审核作为关键字去字典网站上搜索,确实会得到 audit 这个词。所以,审核状态写成 AuditStatus 太正常了。 - -然而,看到这个词的时候,我的第一反应就是这个词好像不太对。因为之前我实现了一个作品审核的功能,不过我写的定义是这样的: -```java -public enum BookReviewStatus { - PENDING, - APPROVED, - REJECTED; -} -``` -抛开前缀不看,同样是审核,一个用 audit,一个用 review。本着代码一致性,希望这两个定义采用同样词汇。 - -搜索引擎里查下。原来,audit 有更官方的味道,更合适的翻译应该是审计,而 review 则有更多核查的意思,二者相比,review 更适合这里的场景。于是,章节的审核状态也统一使用了 review: -```java -public enum ChapterReviewStatus { - PENDING, - APPROVED, - REJECTED; -} -``` -这个坏味道就是个高级的坏味道,英语单词用得不准确。 -但这个问题确实是国内程序员不得不面对的一个尴尬的问题,英语没那么好,体会不到不同单词之间差异。 - -很多人就是把中文扔到 Google 翻译,然后从诸多返回的结果中找一个自己看着顺眼的,而这也往往是很多问题出现的根源。这样写出来的程序看起来就像一个不熟练的外国人在说中文,虽然你知道他在说的意思,但总觉得哪里怪怪的。 - -最好的解决方案还是建立业务词汇表。一般情况下,我们都可以去和业务方谈,共同确定一个词汇表,包含业务术语的中英文表达。这样在写代码的时候,你就可以参考这个词汇表给变量和函数命名。 - -下面是一个词汇表的示例,从这个词汇表中你不难看出: -- 词汇表给出的都是业务术语,同时也给出了在特定业务场景下的含义 -- 它也给出了相应的英文,省得你费劲心思去思考 - -遇到了一个词汇表中没有的术语,就找出这个术语相应的解释,然后补充到术语表。 -![](https://img-blog.csdnimg.cn/f6eb5f883787430188db4024e9bad3f4.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -用集体智慧,而非个体智慧。你一个人的英语可能没那么好,但一群人总会找出一个合适的说法。业务词汇表也是构建通用语言的一部分成果。 - -# 英语单词的拼写错误 -我再给你看一段曾经让我迷惑不已的代码: - -```java -public class QuerySort { - private final SortBy sortBy; - private final SortFiled sortFiled; - ... -} -``` -初看这段代码时,我还想表扬代码的作者,他知道把查询的排序做一个封装,比起那些把字符串传来传去的做法要好很多。 - -但仔细看,sortFiled 是啥?排序文件吗?为啥用的还是过去式?归档? -找出这段代码的作者,向他求教,果然他把单词拼错了。 - -偶尔的拼写错误不可避免,国内的拼写错误比例是偏高的。 - -像 IntelliJ IDEA 这样的 IDE 甚至可以给你提示代码里有拼写错误(typo),只要稍微注意一下,就可以修正很多这样低级错误。 -# 总结 -两个典型的命名坏味道: - -不精准的命名; -用技术术语命名。 - -命名是软件开发中两件难事之一(另一个难事是缓存失效),不好的命名本质上是增加我们的认知成本,同样也增加了后来人(包括我们自己)维护代码的成本。 - -- 好的命名要体现出这段代码在做的事情,而无需展开代码了解其中的细节 -- 再进一步,好的命名要准确地体现意图,而不是实现细节 -- 更高的要求是,用业务语言写代码 - -**好的命名,是体现业务含义的命名。** - -几个英语使用不当造成的坏味道: -- 违反语法规则的命名 -- 不准确的英语词汇 -- 英语单词的拼写错误 - -还有一些常见的与语言相关的坏味道: -- 使用拼音进行命名 -- 使用不恰当的单词简写(比如,多个单词的首字母,或者写单词其中的一部分) - -如何从实践层面上更好地规避这些坏味道: -- 制定代码规范,比如,类名要用名词,函数名要用动词或动宾短语 -- 要建立团队的词汇表 -- 要经常进行CR - -**编写符合英语语法规则的代码。** \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\344\275\240\347\234\237\347\232\204\346\200\235\350\200\203\350\277\207\350\207\252\345\267\261\345\206\231\347\232\204\344\273\243\347\240\201\344\270\272\345\225\245\350\277\231\344\271\210\345\236\203\345\234\276\345\220\227\357\274\237.md" "b/\351\207\215\346\236\204/\344\275\240\347\234\237\347\232\204\346\200\235\350\200\203\350\277\207\350\207\252\345\267\261\345\206\231\347\232\204\344\273\243\347\240\201\344\270\272\345\225\245\350\277\231\344\271\210\345\236\203\345\234\276\345\220\227\357\274\237.md" deleted file mode 100644 index 933cee64a4..0000000000 --- "a/\351\207\215\346\236\204/\344\275\240\347\234\237\347\232\204\346\200\235\350\200\203\350\277\207\350\207\252\345\267\261\345\206\231\347\232\204\344\273\243\347\240\201\344\270\272\345\225\245\350\277\231\344\271\210\345\236\203\345\234\276\345\220\227\357\274\237.md" +++ /dev/null @@ -1,114 +0,0 @@ -# 1 前言 -不一致的代码会给团队造成理解压力,明明大家都是在一个团队做项目: -- 做同种事情,却有不同种做法 -- 起到类似作用的事物,却有不同名字 - -大部分程序员对一致性的理解都表现在较宏观方面,比如,数据库访问是叫 DAO还是 Mapper、Repository? -在一个团队内,这应该有统一标准,但编码层面,要求往往就没那么细致。所以,我们经常看到代码出现各种不一致写法。 -# 2 命名不一 -看一段代码: -![](https://img-blog.csdnimg.cn/c17dbb9786644ed689587f67270a6ace.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_10,color_FFFFFF,t_70,g_se,x_16) -可以看到,目前的分发渠道,看起来没啥问题。但我就疑惑了: -- WEBSITE 和 KINDLE_ONLY 分别表示什么? -WEBSITE 表示作品只会在我们自己的网站发布,KINDLE_ONLY 表示这部作品只会在 Kindle 的电子书商店里上架 -- 二者是不是都表示只在单独一个渠道发布? -是啊! -- 既然二者都有只在一个平台上架发布的含义,为什么不都叫 XXX 或 XXX_ONLY? -好像也是哦 - -所以问题就是这里 WEBSITE 和 KINDLE_ONLY 两个名字不一致。 - -表示类似含义的代码应该名字一致。比如,很多团队里把业务写到服务层,各种服务的命名也都叫 XXXService。 -一旦出现不一致名字,通常都表示不同含义。比如,对于那些非业务入口的业务组件,它们的名字就会不一样,会更符合其具体业务行为,像BookSender ,它表示将作品发送到翻译引擎。 - -一般枚举值表示的含义应该都有一致业务含义,一旦出现不同,就需要确定不同点到底在哪里,这也就是疑惑的原因。 - -显然,这段代码的作者给这两个枚举值命名时,只分别考虑了它应该起什么名字,却忽略了这个枚举值在整体所扮角色。 - -修正代码统一形式: -![](https://img-blog.csdnimg.cn/b430f4b47e3848b8a313dc57b8bc8ad8.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_10,color_FFFFFF,t_70,g_se,x_16) - -# 方案不一致 -![](https://img-blog.csdnimg.cn/2716a0801e3f41baa45f03e448f4b208.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -当一个系统向另外一个系统发送请求时,需要带一个时间戳过去,这里就是把这个时间戳按照一定格式转成了字符串类型,主要就是传输用,便于另外的系统进行识别,也方便在开发过程中进行调试。 - -这段代码本身的实现是没有问题的。它甚至考虑到了 SimpleDateFormat 这个类本身存在的多线程问题,所以,它每次去创建了一个新的 SimpleDateFormat 对象。 - -那我为什么还说它是有问题的呢?因为这种写法是 Java 8 之前的写法,而我们用的 Java 版本是 Java 8 之后的。 - -在很长的一段时间里,Java 的日期时间解决方案一直是一个备受争议的设计,它的问题很多,有的是概念容易让人混淆(比如:Date 和 Calendar 什么情况下该用哪个),有的是接口设计的不直观(比如:Date 的 setMonth 参数是从 0 到 11),有的是实现容易造成问题(比如:前面提到的 SimpleDateFormat 需要考虑多线程并发的问题,需要每次构建一个新的对象出来)。 - -这种乱象存在了很长时间,有很多人都在尝试解决这个问题(比如 Joda Time)。从 Java 8开始,Java 官方的 SDK 借鉴了各种程序库,引入了全新的日期时间解决方案。这套解决方案与原有的解决方案是完全独立的,也就是说,使用这套全新的解决方案完全可以应对我们的所有工作。 - -我们现在的这个项目是一个全新的项目,我们使用的版本是 Java 11,这就意味着我们完全可以使用这套从 Java 8 引入的日期时间解决方案。所以,我们在项目里的约定就是所有的日期时间类型就是使用这套新的解决方案。 - -现在你可能已经知道我说的问题在哪里了,在这个项目里,我们的要求是使用新的日期时间解决方案,而这里的 SimpleDateFormat 和 Date 是旧解决方案的一部分。所以,虽然这段代码本身的实现是没有问题的,然而,放在项目整体中,这却是一个坏味道,因为它没有和其它的部分保持一致。 - -后来使用了新的解决方案: -![](https://img-blog.csdnimg.cn/28fd48f9cfd244e0878551d2abc59d51.png) - -之所以会这样,因为一个项目中,应对同一个问题出现了多个解决方案,如果没有统一约定,项目成员会根据自己写代码时的感觉随机选择方案,导致方案不一致。 - -为什么一个项目中会出现多个解决方案? -- 时间 -时间消逝,技术发展,人们会主动意识到原方案的问题,就会提出新方案,像这里 Java 日期时间的解决方案,就是 JDK 本身随时间演化造成的。有的项目时间比较长,也会出现类似问题。 -- 因为自己的原因引入 -比如,在代码中引入做同一件事情类似的程序库。比如判断字符串是否为空或空串,就有 Guava 和 Apache Commons Lang,都能做同样事情,所以,程序员也会根据自己的熟悉程度选择其中之一来用,造成代码不一致。 - -这两个程序库是很多程序库的基础,经常因为引入了其它程序库,相应的依赖就出现在我们的代码中。所以,我们必须约定,哪种做法是我们在项目中的标准做法,以防出现各自为战的现象。比如,在我的团队中,我们就选择 Guava 作为基础库,因为相对来说,它的风格更现代,所以,团队就约定类似的操作都以 Guava 为准。 - -# 代码不一致 -![](https://img-blog.csdnimg.cn/ff8d919c9cd243e894887303de9abe93.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -在翻译引擎中创建作品的代码: -- 首先,根据要处理的作品 ID,获取其中已审核通过的作品 -- 然后,发送一个 HTTP 请求在翻译引擎中创建出这个作品 - -有什么问题? -这段代码的不一致,这些代码不是一个层次的代码! - -首先是获取审核通过的作品,这是一个业务动作,接下来的三行其实是在做一件事,也就是发送创建作品的请求,这三行代码: -- 创建请求的参数 -- 根据参数创建请求 -- 最后把请求发送出去 - -三行代码合起来完成了一个发送创建作品请求这么一件事,而这件事才是一个完整的业务动作。 - -所以,这个函数里的代码并不在一个层次上,有的是业务动作,有的是业务动作的细节。理解到这,把这些业务细节的代码提取到一个函数: -![](https://img-blog.csdnimg.cn/19a0ff7bf05346dca9de9d37e7492a73.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -结果上看,原来的函数(createBook)里都是业务动作,而提取出来的函数(createRemoteBook)则都是业务动作的细节,各自语句都在一个层次。 - -分清代码处于不同层次,基本功还是分离关注点! - -一旦分解出不同关注点,还可进一步调整代码的结构。 -像前面拆分出来的这个方法,我们已经知道它的作用是发出一个请求去创建作品,本质上并不属于这个业务类的一部分。 -所以,还可通过引入一个新模型,将这个部分调整出去: -![](https://img-blog.csdnimg.cn/fda93405b5154966ab8a6ba40223bfc8.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -一说到分层,大多数人想到的只是模型的分层,很少有人会想到在函数的语句中也要分层。各种层次的代码混在一起,许多问题也就随之而来了,最典型莫过长函数。 - -我们在做的依然是模型分层,只不过,这次的出发点是函数的语句。“分离关注点,越小越好”的意义所在。观察代码的粒度足够小,很多问题自然就会暴露出来。 - -程序员开始写测试时,有一个典型的问题:如何测试一个私有方法。有人建议用一些特殊能力(比如反射)去测试。我给这个问题的答案是,不要测私有方法。 -之所以想测试私有方法,就是分离关注点没有做好,把不同层次的代码混在一起。前面这段代码,如果要测试前面那个 createRemoteBook 方法还是有一定难度的,但调整之后,引入了 TranslationEngine 这个类,这个方法就变成了一个公开方法,就可以按照一个公开方法去测试了,所有问题迎刃而解。 - -**很多程序员纠结的技术问题,其实是一个软件设计问题,不要通过奇技淫巧去解决一个本来不应该被解决的问题。** -# 总结 -对于一个团队来说,一致是非常重要的,是降低集体认知成本的重要方式。我们分别见识了: -- 命名中的不一致 -- 方案中的不一致 -- 代码中的不一致。 - -类似含义的代码应该有类似的命名,不一致的命名表示不同含义,需要给出一个有效解释。 - -方案中的不一致: -- 由于代码长期演化造成的 -- 项目中存在完成同样功能的程序库 - -无论是哪种原因,都需要团队先统一约定,保证所有人按照同一种方式编写代码。 - -代码中的不一致常常是把不同层次的代码写在了一起,最典型的就是把业务层面的代码和实现细节的代码混在一起。解决这种问题的方式,就是通过提取方法,把不同层次的代码放到不同的函数里,而这一切的前提还是是分离关注点,这个代码问题的背后还是设计问题。 - -**务必保持代码在各个层面上的一致性。** \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\345\217\257\345\217\230\347\232\204\346\225\260\346\215\256\346\234\211\345\244\232\345\217\257\346\200\225\357\274\214\344\275\240\344\272\206\350\247\243\345\220\227\357\274\237.md" "b/\351\207\215\346\236\204/\345\217\257\345\217\230\347\232\204\346\225\260\346\215\256\346\234\211\345\244\232\345\217\257\346\200\225\357\274\214\344\275\240\344\272\206\350\247\243\345\220\227\357\274\237.md" deleted file mode 100644 index cadf3d7c1c..0000000000 --- "a/\351\207\215\346\236\204/\345\217\257\345\217\230\347\232\204\346\225\260\346\215\256\346\234\211\345\244\232\345\217\257\346\200\225\357\274\214\344\275\240\344\272\206\350\247\243\345\220\227\357\274\237.md" +++ /dev/null @@ -1,142 +0,0 @@ -程序=数据结构+算法,所以,数据几乎是软件开发最核心的一个组成部分。在一些人的认知中,所谓做软件,就是一系列的 CRUD 操作,也就是对数据进行增删改查。再具体一点,写代码就把各种数据拿来,然后改来改去。我们学习编程时,首先学会的,也是给变量赋值,类似 a = b + 1。 - -改数据,几乎已经成了很多程序员写代码的标准做法。然而,这种做法也带来了很多的问题。 - -# 满天飞的 Setter -```java -public void approve(final long bookId) { - ... - book.setReviewStatus(ReviewStatus.APPROVED); - ... -} -``` -对作品进行审核:通过 bookId,找到对应的作品,接下来,将审核状态设置成审核通过。 - -因为这里用了 setter。setter 往往是缺乏封装的一种做法。 -很多人在写代码时,写完字段就会利用 Lombok 生成 getter、setter。setter 同 getter 一样,反映的都是对细节的暴露。 - -这就意味着,你不仅可以读到一个对象的数据,还可以修改一个对象的数据。相比于读数据,修改是一个更危险操作。 - -你不知道数据会在哪里被何人以什么方式修改,造成的结果是,别人的修改会让你的代码崩溃。与之相伴的还有各种并发问题。 - -可变的数据可怕,比可变的数据更可怕的是不可控的变化,暴露 setter 就是这种不可控的变化。 -把各种实现细节完全交给对这个类不了解的使用者去修改,没有人会知道他会怎么改,所以,这种修改完全不可控。 - -缺乏封装再加上不可控变化,setter 几乎是排名第一的坏味道。 - -用一个函数替代 setter,也就是把它用行为封装起来: -```java -public void approve(final long bookId) { - ... - book.approve(); - ... -} -``` -通过在 Book 类里引入了一个 approve 函数,将审核状态封装。 -```java -class Book { - public void approve() { - this.reviewStatus = ReviewStatus.APPROVED; - } -} -``` -作为这个类的使用者,你并不需要知道这个类到底是怎么实现的。 -这里的变化也变得可控。虽然审核状态这个字段还是会修改,但你所有的修改都要通过几个函数作为入口。有任何业务上的调整,都会发生在类内部,只要保证接口行为不变,就不会影响到其它代码。 - -setter 破坏了封装,相信你对这点已经有了理解。 -有时你会说,我这 setter 只用在初始化过程,并不需要在使用的过程去调用,就像下面这样: -```java -Book book = new Book(); -book.setBookId(bookId); -book.setTitle(title); -book.setIntroduction(introduction); -``` -这种只在初始化中使用的代码,压根没必要以 setter 形式存在,真正需要的是一个有参数的构造函数: -```java -Book book = new Book(bookId, title, introduction); -``` -消除 setter ,有一种专门的重构手法,叫做移除设值函数(Remove Setting Method)。总而言之,setter 完全没有必要存在。 - -Lombok 可以在编译的过程中生成相应代码,最大的优点是不碍眼。因为它的代码是在编译阶段生成的,所以,那些生成的代码在源码级别上是不存在的。下面就是一个例子: -```java -@Getter -@Setter -class Book { - private BookId bookId; - private String title; - private String introduction; -} -``` -@Getter 表示为这个类的字段生成 getter -@Setter 表示生成 setter -因为@Setter的存在,其它代码还可以调用这个类的 setter,存在的问题并不会改变。 -所以,一个更好的做法是禁用@Setter。lombok.config 配置禁用@Setter: -```java -lombok.setter.flagUsage = error -lombok.data.flagUsage = error -``` -这里除了@Setter,还禁用了@Data,这是 Lombok 中另外一个 Annotation,表示同时生成 getter 和 setter。既然我们禁用@Setter 是为了防止生成 setter,当然也要禁用@Data了。 - -# 可变的数据 -反对 setter,一个重要原因是它暴露了数据。 -暴露数据造成的问题在于数据的修改,进而导致出现难以预料的 Bug。在上面的代码中,我们把 setter 封装成一个个的函数,实际上是把不可控的修改限制在一个有限范围内。 - -进一步,如果数据压根不让修改,犯下各种低级错误的机会就进一步降低。没错,在这种思路下,可变数据(Mutable Data)就成了一种坏味道,这是 Martin Fowler 在新版《重构》里增加的坏味道,它反映着整个行业对于编程的新理解。 - -这种想法源自函数式编程,数据建立在不改变的基础上,如果需要更新,就产生一份新的数据副本,而旧有的数据保持不变。 -随着函数式编程在软件开发领域中的地位不断提高,人们对于不变性的理解也越发深刻,不变性有效地解决了可变数据产生的各种问题。 - -> 所以,Martin Fowler 在《重构》第二版里新增了可变数据作为一种坏味道,这其实反映了行业的理解也是在逐渐推进的。不过,Martin -> Fowler 对于可变数据给出的解决方案,基本上是限制对于数据的更新,降低其风险,这与我们前面提到的对 setter 的封装如出一辙。 - -解决可变数据,还有一个解决方案 -# 编写不变类 -函数式编程的不变性,其中的关键点就是设计不变类。String 类就是一个不变类,比如,如果我们把字符串中的一个字符替换成另一个字符,String 类给出的函数签名是这样的: -```java -String replace(char oldChar, char newChar); -``` -其含义是,这里的替换并不是在原有字符串上进行修改,而是产生了一个新的字符串。 - -那么,在实际工作中,我们怎么设计不变类呢? -- 所有的字段只在构造函数中初始化 -- 所有的方法都是纯函数 -- 如果需要有改变,返回一个新的对象,而不是修改已有字段 - -回过头来看我们之前改动的“用构造函数消除 setter”的代码,其实就是朝着这个方向在迈进。如果按照这个思路改造我们前面提到的 approve 函数,同样也可以: -```java -class Book { - public void approve() { - return new Book(..., ReviewStatus.APPROVED, ...); - } -} -``` - -这里,我们创建出了一个“其它参数和原有 book 对象一模一样,只是审核状态变成了 APPROVED ”的对象。 - -在 JDK 的演化中,我们可以看到一个很明显的趋势,新增的类越来越多地采用了不变类设计,比如,用来表示时间的类。 -原来 Date 类里面还有各种 setter,而新增的 LocalDateTime 则一旦初始化就不会再修改。要操作这个对象,则会产生一个新对象: -```java -LocalDateTime twoDaysLater = now.plusDays(2); -``` -就目前的开发状态而言,想要完全消除可变数据很难做到,但可尽可能编写一些不变类。 -区分类的性质。最核心要识别的对象分成两种,实体和值对象。实体对象要限制数据变化,而值对象就要设计成不变类。 - -函数式编程的本质,是对程序中的赋值进行了约束。基于这样的理解,连赋值本身其实都会被归入到坏味道的提示,这才是真正挑战很多人编程习惯的一点。 - -越来越多的语言中开始引入值类型,也就是初始化之后便不再改变的值,比如,Java 的 Valhalla 项目。像 Rust,缺省都是值类型,而如果你需要一个可以赋值的变量,反而要去专门声明。 - -Martin Fowler 在《重构》中还提到一个与数据相关的坏味道:全局数据(Global Data)。如果你能够理解可变数据是一种坏味道,全局数据也就很容易理解了,它们处理手法基本上是类似的。 -# 总结 -可变数据最直白的体现就是各种 setter: -- 破坏了封装 -- 带来不可控的修改,给代码增添许多问题 - -解决它的一种方式就是移除设值函数(Remove Setting Method),将变化限制在一定的范围之内。 - -可变数据是《重构》第二版新增的坏味道,这其实反映了软件开发行业的一种进步,它背后的思想是函数式编程所体现的不变性。解决可变数据,一种方式是限制其变化,另一种方式是编写不变类。 - -在实践中,完全消除可变数据是很有挑战的。所以,一个实际的做法是,区分类的性质。值对象就要设计成不变类,实体类则要限制数据变化。 - -函数式编程的本质是对于赋值进行了约束,我们甚至可以把赋值作为一种坏味道的提示。很多编程语言都引入了值类型,而让变量成为次优选项。 - -**限制可变的数据。** \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\345\246\202\344\275\225\344\274\230\351\233\205\345\244\204\347\220\206\344\273\243\347\240\201\344\276\235\350\265\226\351\227\256\351\242\230\357\274\237.md" "b/\351\207\215\346\236\204/\345\246\202\344\275\225\344\274\230\351\233\205\345\244\204\347\220\206\344\273\243\347\240\201\344\276\235\350\265\226\351\227\256\351\242\230\357\274\237.md" deleted file mode 100644 index e3b09d3112..0000000000 --- "a/\351\207\215\346\236\204/\345\246\202\344\275\225\344\274\230\351\233\205\345\244\204\347\220\206\344\273\243\347\240\201\344\276\235\350\265\226\351\227\256\351\242\230\357\274\237.md" +++ /dev/null @@ -1,206 +0,0 @@ -“大类”这个坏味道,为了避免同时面对所有细节,我们需要把程序进行拆分,分解成一个个小模块。 -随之而来的问题就是,需要把这些拆分出来的模块按照一定规则重新组装,这就是依赖的源泉。 - -一个模块要依赖另外一个模块完成完整的业务功能,而到底怎么去依赖,这里就很容易产生问题。 - -# 缺少防腐层 -```java -@PostMapping("/books") -public NewBookResponse createBook(final NewBookRequest request) { - boolean result = this.service.createBook(request); - ... -} -``` -创建一部作品的入口,提供一个 REST 服务,只要对`/books` 发个 POST 请求,就可以创建一部作品。 - -按一般代码分层逻辑,一个 Resource (有人称 Controller)调用一个 Service,再正常不过了,有啥问题? - -从 Resource 调用 Service,几乎是行业里的标准做法,没有问题,问题出在传递的参数。 -这个 NewBookRequest 参数类应该属于哪层?resource or service 层? - -既然它是个请求参数,通常承载诸如参数校验和对象转换的职责,通常理解应该属 resource 层。 -如果这个理解是正确的,问题就来了,它为什么会传递给 service 层? - -按通常的架构设计原则,service 层属核心业务,resource 层属接口。 -核心业务重要度更高,所以,其稳定程度也该更高。 -同样的业务,可用 REST 方式对外提供,也可用 RPC 方式。 - -NewBookRequest 这个本该属于接口层的参数,现在成了核心业务的一部分,即便将来提供了 RPC 接口,它也要知道 REST 接口长什么样子,显然,这有问题。 - -既然 NewBookRequest 属于resource 层有问题,那假设它属 service 层? -一般请求都要承担对象校验和转化工作。若这个类属 service 层,但它用在 resource 接口上,作为 resource 的接口,它会承载一些校验和对象转换的角色,而 service 层参数是不需要关心这些的。 -如果 NewBookRequest 属 service 层,校验和对象转换谁负责? - -有时 service 层参数和 resource 层参数并非严格一一对应。 -比如,创建作品时,需要一个识别作者身份的用户 ID,而这个参数并不是通过客户端发起的请求参数带过来,而是根据用户登录信息进行识别的。 -所以,用 service 层的参数做 resource 层的参数,就存在差异的参数如何处理的问题。 - -看来我们陷入了两难境地,如此一个简单的参数,放到哪层都有问题。 -何解呢? -之所以这么纠结,在于缺少了一个模型。 - -NewBookRequest 之所以弄得如此“里外不是人”,主要就是因为它只能扮演**一个层中的模型**,所以,我们只要**再引入一个模型**就可以破解这个问题。 - -```java -class NewBookParameter { - ... -} - -class NewBookRequest { - public NewBookParameters toNewBookRequest() { - ... - } -} - -@PostMapping("/books") -public NewBookResponse createBook(final NewBookRequest request) { - boolean result = this.service.createBook(request.toNewBookParameter()); - ... -} -``` -引入一个 NewBookParameter 类,把它当作 service 层创建作品的入口,而在 resource 中,将 NewBookRequest 这个请求类的对象转换成了 NewBookParameter 对象,然后传到 service 层。 - -该结构中: -- NewBookParameter 属 service 层 -- NewBookRequest 属 resource 层 - -二者相互独立,之前纠结的问题也就不存在了。 - -增加一个模型,就破解了依赖关系上的纠结。 - -虽然它们成了两个类,但它俩长得一模一样吧,这难道不算一种重复吗? -但问题是,它们两个为什么要一样呢?有了两层不同的参数,我们就可以给不同层次上的模型以不同约定了。 - -比如,对 resource 层的请求对象,它的主要作用是传输,所以,一般约定请求对象的字段主要是基本类型。 -而 service 的参数对象,已经是核心业务一部分,就需要全部转化为业务对象。 - -比如,同样表示价格,在请求对象中,可以是个 double 类型,而在业务参数对象中,应是 Price 类型。 - -再来解决 resource 层参数和 service 层参数不一致情况,现在二者分开了,那我们就很清楚地知道,其实,就是在业务参数对象构造时,传入必需的参数即可。 -比如,若需要传入 userId,可以这么做: - -```java -class NewBookRequest { - public NewBookParameters toNewBookRequest(long userId) { - ... - } -} - -@PostMapping("/books") -public NewBookResponse createBook(final NewBookRequest request, final Authentication authentication) { - long userId = getUserIdentity(authentication); - boolean result = this.service.createBook(request.toNewBookParameter(userId)); - ... -} -``` -能注意到这个坏味道,就是从依赖关系入手发现的。 -当初注意到这段代码,因为我团队内部的约定是,所有请求对象都属 resource 层,但在这段代码里,service 层出现了 resource 层的对象,它背离了我们对依赖关系设计的约定。 - -这也是一个典型的软件设计问题:缺少防腐层,通过防腐层将外部系统和核心业务隔离开来。 - -而很多人初见这个例子,可能压根想不到它与防腐层的关系,那只不过是因为你对这种结构太熟悉了。 -其实,resource 层就是外部请求和核心业务之间的防腐层。只要理解了这一点,你就能理解这里要多构建出一个业务参数对象的意义。 - -下面这段代码,想必你也能轻易发现问题: -```java -@Entity -@Table(name = "user") -@JsonIgnoreProperties(ignoreUnknown = true) -class User { - ... -} -``` -这是个 User 类的声明,它有 **@Entity** 这个 Anntation,表示它是一个业务实体的对象,但它的上面还出现了**@JsonIgnoreProperties**。JSON 通常都是在传输中用到。业务实体和传输对象应该具备的特质在同一类出现。 -显然,这也是没有构建好防腐层,把两个职责混在一起。 - -# 业务代码里的具体实现 -```java -@Task -public void sendBook() { - try { - this.service.sendBook(); - } catch (Throwable t) { - this.feishuSender.send(new SendFailure(t))); - throw t; - } -} -``` -一旦发送过程出了问题,要通过即时通信工具发给相关人,以防系统出现问题无人发觉。只不过,这里给出的是它最初的样子,也就是通过飞书进行消息发送。 - -因为需求是通过飞书发送,所以,这里就写了飞书发送。这看上去很合理啊。 -这是一种符合直觉的做法,却不符合设计原则,违反依赖倒置原则。 - -> 高层模块不应依赖低层模块,二者应依赖于抽象。 -> High-level modules should not depend on low-level modules. Both should depend on abstractions. -> -> 抽象不应依赖于细节,细节应依赖于抽象。 -> Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions. - -能注意到这段代码,是因为在一段业务处理中出现了一个具体实现,即 feishuSender。 - -**业务代码中任何与业务无关的东西都是潜在的坏味道。** - -飞书肯定不是业务的一部分,它只是当前选择的一个具体实现。 -即是否选择飞书,与团队当前状态相关,如果哪一天团队切换IM软件,这个实现就得换掉。但团队不可能切换业务,一旦切换,那就是一个完全不同的系统。 - -**识别一个东西是业务的一部分,还是一个可以替换的实现,不妨问问自己,如果不用它,是否还有其它的选择?** - -就像这里的飞书,可被其它IM软件替换。 -常见的中间件,比如Kafka、Redis、MongoDB等,也都是一个具体实现,其它中间件都可以将其替换。所以,它们在业务代码里出现,那一定是坏味道。 - -既然这些具体的东西是一种坏味道,怎么解决? -可以引入一个模型,即这个具体实现所要扮演的角色,通过它,将业务和具体的实现隔离。 - -```java -interface FailureSender { - void send(SendFailure failure); -} - - -class FeishuFailureSenderS implements FailureSender { - ... -} -``` -引入一个 FailureSender,业务层只依赖于这个 FailureSender 接口,具体的飞书实现通过依赖注入。 - -很多程序员在写代码,由于开发习惯,常忽略掉依赖关系。 - -有一些工具,可以保证我们在写代码的时候,不会出现严重破坏依赖关系的情况,比如,像前面那种 service 层调用 resource 层的代码。 - -ArchUnit 来保证这一切。它是把这种架构层面的检查做成了单元测试,下面就是这样的一个单元测试: -```java -@Test -public void should_follow_arch_rule() { - JavaClasses clazz = new ClassFileImporter().importPackages("..."); - ArchRule rule = layeredArchitecture() - .layer("Resource").definedBy("..resource..") - .layer("Service").definedBy("..service..") - .whereLayer("Resource").mayNotBeAccessedByAnyLayer() - .whereLayer("Service").mayOnlyBeAccessedByLayers("Resource"); - - rule.check(clazz); -} -``` -定义了两个层,分别是 Resource 层和 Service 层。 -要求 Resource 层代码不能被其它层访问,而 Service 层代码只能由 Resource 层方法访问。 -这就是我们的架构规则,一旦代码里有违反这个架构规则的代码,这个测试就会失败,问题也就会暴露出来。 -# 总结 -由于代码依赖关系而产生的坏味道: -- 缺少防腐层,导致不同代码糅合在一起,一种是在业务代码中出现了具体的实现类。 -缺少防腐层,会让请求对象传导到业务代码中,造成了业务与外部接口的耦合,也就是业务依赖了一个外部通信协议。一般来说,业务的稳定性要比外部接口高,这种反向的依赖就会让业务一直无法稳定下来,继而在日后带来更多的问题。 -解决方案:引入一个防腐层,将业务和接口隔离开来。 - -业务代码中出现具体的实现类,实际上是违反了依赖倒置原则。因为违反了依赖倒置原则,业务代码也就不可避免地受到具体实现的影响,也就造成了业务代码的不稳定。识别一段代码是否属于业务,我们不妨问一下,看把它换成其它的东西,是否影响业务。解决这种坏味道就是引入一个模型,将业务与具体的实现隔离开来。 - -**代码应该向着稳定的方向依赖。** - - -# FAQ -- 对照DDD,是否可以理解为接口层接收DTO,转换为DO后传入业务层?那么缺少防腐层的问题也可能发生在业务层和持久化层之间,比如业务层直接操作持久化对象(PO)? -相比把DTO当成DO用,把PO当DO用似乎更常见。除了违反单一职责原则,实际使用似乎问题不大,因为很多系统都是从先从数据表开始设计的。 -严格说,持久化对象也应该与业务对象分开,但在实际项目中,一般持久化对象和业务对象合在一起,主要原因是,一般持久化都是一个系统在内部完成的,对于一个系统而言,是一种可控的变化。 - -- 按ddd的分层。应用层不能接收dto,应当新建一个参数类(这样工厂类的实现不依赖dto倒是很好保证了)?那么这个参数类归属什么?感觉不属于实体也不属于值对象。 -dto转pojo感觉还是不好。虽然从职责分配的角度看,dto能满足pojo创建的诉求,所以归属到dto做pojo的工厂方法。但这样dto就依赖了pojo,而这个依赖是非强必要的,所以减少依赖可能更好,借助工具单独实现一个dto转pojo的转换器。 - -参数类属于业务层,不用纠结它属于什么,它就是一个参数。具体的转换其实就是一个工厂方法,放到 Request 里面而不是单独放置,就不用多谢一个额外的类。 \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\345\246\202\344\275\225\345\210\206\346\236\220\350\275\257\344\273\266\347\232\204\346\250\241\345\236\213.md" "b/\351\207\215\346\236\204/\345\246\202\344\275\225\345\210\206\346\236\220\350\275\257\344\273\266\347\232\204\346\250\241\345\236\213.md" deleted file mode 100644 index 45b89af01b..0000000000 --- "a/\351\207\215\346\236\204/\345\246\202\344\275\225\345\210\206\346\236\220\350\275\257\344\273\266\347\232\204\346\250\241\345\236\213.md" +++ /dev/null @@ -1,115 +0,0 @@ -面对一个新项目,如何理解它的模型呢? - -要先知道项目提供了哪些模型,模型又提供了怎样的能力。若只知道这些,你只是在了解别人设计的结果,这不足以支撑你后期对模型的维护。 - -在一个项目中,常常会出现新人随意向模型中添加内容,修改实现,让模型变得难以维护。原因在于**对模型的理解不够**。 - -模型都是为解决问题,理解一个模型,需要了解在没有这个模型之前,问题是如何被解决的? -这样,你才能知道新的模型究竟提供了怎样的提升,这是理解一个模型的关键。 - -本文以Spring的IoC容器为例,来看看怎样理解软件的模型。 - -# 耦合的依赖 -Spring的根基就是IoC容器,即“ 控制反转”,也叫依赖注入。 - -> IoC容器是为了解决什么问题呢? - -组件创建和组装问题。 - -> 为什么这是个亟待解决的问题? - -软件设计需要有个分解过程,必然还面对一个组装过程,即将分解出的各组件组装到一起完成功能。 - -# 案例 -某博客服务提供:根据标题查询博客。 -![](https://img-blog.csdnimg.cn/b8de0b116dde4de7bed99396d3a95c5d.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -ArticleService处理业务过程中,需用ArticleRepository辅助完成功能,即ArticleService依赖ArticleRepository。 - -> 这时你会怎么做? - -直男做法,在 ArticleService新增一个ArticleRepository字段: -![](https://img-blog.csdnimg.cn/787ab78bbeea4e2db62a91f41ba04428.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -看起来好像还行。 -**那这个字段怎么初始化?** - -直男反应:直接new! -![](https://img-blog.csdnimg.cn/64b5600ce483444d96ae63fda603e977.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -看起好还能用,但实际上DBArticleRepository不能这样初始化。 -就如实现类的名字,这里要用到DB。但在真实的项目中,由于资源所限,一般不会在应用中任意打开DB连接,而是会共享DB连接。 -所以,DBArticleRepository需要一个DB连接(Connection)参数。 - -于是你决定通过构造器把这个参数传入: -![](https://img-blog.csdnimg.cn/c6ab873516aa4309ae16cd1785022d59.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16)看上去也还正常。既然开发完了,那么开始测试吧: -要让ArticleService跑起来,就得让ArticleRepository也跑起来;就得准备好DB连接。 - -是不是太麻烦,想放弃测试了?。但你还是决定坚持一下,去准备DB连接信息。 - -然后,真正开始写测试时,发现,要测试,还要在DB里准备数据: -- 测查询,得事先插入一些数据吧,看查出来的结果和插入的数据是否一致 -- 测更新,得先插入数据,测试跑完,再看数据更新是否正确 - -咬咬牙准备了一堆数据,你开始困惑了:我在干什么?我不是要测试服务吗?做数据准备不是测试仓库的时候该做的事吗? - -> 所以,你发现问题在哪了吗? - -在你创建对象的那刻,问题就出现了。 - -# 分离的依赖 -当我们创建一个对象时,就必须要有个实现类,即DBArticleRepository。 -虽然ArticleService写得很干净,其他部分根本不依赖DBArticleRepository,只在构造器里依赖,但依赖就是依赖。 - -而且由于要构造DBArticleRepository,还引入了Connection类,该类只与DBArticleRepository的构造有关系,与ArticleService业务逻辑毫无关系。 - -只是因为引入一个具体实现,就需要把它周边全部东西引入,而这一切都与业务类本身的业务逻辑没一毛钱关系。 -这就像是,你原本打算买套房子,现在却让你必须了解怎么和水泥、砌墙、怎么装修、户型怎么设计、各个家具怎么组装,而你想要的只是一套能住的婚房。 - -实际项目,构建一个对象可能牵扯更多内容: -- 根据不同的参数,创建不同的实现类对象,你可能需要用到工厂模式 -- 为了解方法执行时间,需要给被依赖的对象加上监控 -- 依赖的对象来自于某个框架,你自己都不知道具体的实现类咋样的 -…… - -即便是最简单的对象创建和组装,看起来也不是多简单。 - -直接构造存在这么多问题,最简单的就是把创建的过程拿出去,只留下与字段关联的过程: -![](https://img-blog.csdnimg.cn/54155a57ccaa4ac098111b28812f7d27.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -这时的ArticleService就只依赖ArticleRepository。 -测试ArticleService也很简单,只要用一个对象模拟ArticleRepository的行为。通常这种模拟对象行为的工作用一个现成的程序库就可以完成,就是那些Mock框架。 - -之前的代码里,如果我用Mock框架模拟Connection类是不是也可以? -理论上,是可以。但是想要让ArticleService的测试通过,就必须打开DBArticleRepository的实现,只有配合着其中的实现,才可能让ArticleService跑起来。显然,你跑偏了。 - -对象的创建已经分离了出去,但还是要要有一个地方完成这个工作,最简单的解决方案自然是,把所有的对象创建和组装在一个地方完成: -![](https://img-blog.csdnimg.cn/67df925a361646a189e6d0dace96fbc7.png) -相比业务逻辑,组装过程很简单,仅仅是个对象创建及传参。 -最好的解决方案就是有个框架。Java的这种组装一堆对象的东西一般被称为“容器”。 -![](https://img-blog.csdnimg.cn/001940d76418478f9240c6aa9870d89b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -至此,一个容器就此诞生。它解决的是依赖问题,把被依赖的对象注入到目标对象,所以叫“依赖注入”(Dependency Injection,简称 DI)。这个容器就叫DI容器。 - -这种创建和组装对象的方式在当年引发了很大的讨论,直到最后Martin Fowler写了一篇《反转控制容器和依赖注入模式》,才算把大家的讨论做了一个总结,行业里总算达成共识。 - -> 说了这么多,和我们要讨论的“模型”有什么关系? - -很多人习惯性把对象的创建和组装写到了一个类里,导致代码出现大量耦合。 -也导致项目很难测试,可测试性是衡量设计优劣的一个重要标准。 - -有了IoC容器后,你的代码就只剩下关联的代码,对象的创建和组装都由IoC容器完成。不经意间,还做到了面向接口编程,实现是可以替换的,且可测试。 -容器概念还能继续增强。比如,我们想给所有与数据库相关的代码加上时间监控,只要在容器构造对象时添加处理即可。这就是 AOP,而这些改动,对业务代码透明。 -但还是很多程序员即便用Spring,依然是自己构造对象,静态方法随便写。 - -# 总结 -理解模型,要知道项目提供了哪些模型,这些模型都提供了怎样的能力。 -更重要的是了解模型设计的渊源: -- 可增进对它的了解 -- 也会减少我们对模型的破坏或滥用 - -IoC容器有效地解决了对象的创建和组装的问题,让程序员们拥有了一个新的编程模型。 - -按照这个编程模型去写代码,整体的质量会得到大幅度的提升,也会规避掉之前的许多问题。这也是一个好的模型对项目起到的促进作用。像DI这种设计得非常好的模型,你甚至不觉得自己在用一个特定的模型在编程。 - -**理解模型,要了解模型设计的来龙去脉。** -![](https://img-blog.csdnimg.cn/b8a20a4382c442088e76f24996971e2d.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\345\246\202\344\275\225\345\272\224\345\257\271\347\201\253\350\275\246\344\273\243\347\240\201\345\222\214\345\237\272\346\234\254\347\261\273\345\236\213\345\201\217\346\211\247\351\227\256\351\242\230\357\274\237.md" "b/\351\207\215\346\236\204/\345\246\202\344\275\225\345\272\224\345\257\271\347\201\253\350\275\246\344\273\243\347\240\201\345\222\214\345\237\272\346\234\254\347\261\273\345\236\213\345\201\217\346\211\247\351\227\256\351\242\230\357\274\237.md" deleted file mode 100644 index 1d965d3883..0000000000 --- "a/\351\207\215\346\236\204/\345\246\202\344\275\225\345\272\224\345\257\271\347\201\253\350\275\246\344\273\243\347\240\201\345\222\214\345\237\272\346\234\254\347\261\273\345\236\213\345\201\217\346\211\247\351\227\256\351\242\230\357\274\237.md" +++ /dev/null @@ -1,161 +0,0 @@ -这一讲,我们再来讲一类代码的坏味道:缺乏封装。 - -封装,将碎片式代码封装成一个个可复用模块。但不同级别的程序员对封装的理解程度差异甚大,往往出现这么一种现象:写代码的人认为自己提供了封装,但实际上,我们还是看到许多的代码散落在那里。 - -# 火车残骸 -```java -String name = book.getAuthor().getName(); -``` -获得一部作品作者的名字。作品里有作者信息,想要获得作者的名字,通过“作者”找到“作者姓名”,这就是很多人凭借直觉写出的代码,不过这有问题! - -是不是感觉自己无法理解封装了? - -若你想写出上面这段,是不是得先了解Book、Author两个类的实现细节? -即我们得知道,作者的姓名存储在作品的作者字段。 -这就是问题了:当你必须得先了解一个类的细节,才能写代码时,这只能说明一件事,这个封装不完美。 - -翻翻你负责的项目,这种在一行代码中有连续多个方法调用的情况是不是随处可见? - -> Martin Fowler 在《重构》中给这种坏味道起的名字叫过长的消息链(Message -> Chains),而有人则给它起了一个更为夸张的名字:火车残骸(Train Wreck),形容这样的代码像火车残骸一般,断得一节一节的。 - -解决这种代码的重构方案叫隐藏委托关系(Hide Delegate),说人话,就是把这种调用封装起来: -```java -class Book { - ... - public String getAuthorName() { - return this.author.getName(); - } - ... -} - -String name = book.getAuthorName(); -``` -火车残骸这种坏味道的产生是缺乏对于封装的理解,因为封装这件事并不是很多程序员编码习惯的一部分,他们对封装的理解停留在数据结构加算法层面。 - -在学习数据结构时,我们所编写的代码都是拿到各种细节直接操作,但那是在做编程练习,并不是工程上的编码方式。遗憾的是,很多人把这种编码习惯带到工作。 - -比如说,有人编写一个新的类,第一步是写出这个类要用到的字段,然后,就是给这些字段生成各种 getXXX。很多语言或框架提供的约定就是基于这种 getter的,就像 Java 里的 JavaBean,所以相应的配套工具也很方便。这让暴露细节这种事越来越容易,封装反而成稀缺品。 - -要想成为技术专家,先从少暴露细节开始。声明完一个类的字段之后,请停下生成 getter 的手,转而思考这个类应该提供的行为。 - -迪米特法则(Law of Demeter)几乎就是针对这个坏味道的: -- 每个单元对其它单元只拥有有限的知识,而且这些单元是与当前单元有紧密联系 -- 每个单元只能与其朋友交谈,不与陌生人交谈 -- 只与自己最直接的朋友交谈。 - -这个原则需要我们思考,哪些算是直接的朋友,哪些算是陌生人。火车残骸代码显然就是没有考虑这些问题,而直接写出来的代码。 - -或许你会说,按照迪米特法则这样写代码,会不会让代码里有太多简单封装的方法? -有可能,不过,这也是单独解决这一个坏味道可能带来的结果。 -这种代码的出现,本质是缺乏对封装的理解,而一个好的封装是需要基于行为。所以,把视角再提升,应该考虑的问题是类应该提供哪些行为,而非简单地把数据换一种形式呈现出来就没了。 - -有些内部 DSL 的表现形式也是连续的方法调用,但 DSL 是声明性的,是在说做什么(What),而这里的坏味道是在说怎么做(How),二者的抽象级别是不同的,不要混在一起。 - -# 基本类型偏执 -```java -public double getEpubPrice(final boolean highQuality, - final int chapterSequence) { - ... -} -``` -根据章节信息获取 EPUB 价格。看上去非常清晰,也有坏味道? -问题在返回值类型,即价格的类型。 - -在DB存储价格时,就是用一个浮点数,用 double 可保证计算的精度,这样的设计有问题? -确实,这就是很多人使用基本类型(Primitive)作为变量类型思考的角度。但实际上,这种采用基本类型的设计缺少了一个模型。 - -虽然价格本身是用浮点数在存储,但价格和浮点数本身不是同一概念,有着不同行为需求。比如,一般要求商品价格大于 0,但 double 类型本身没这限制。 - -就以“价格大于0”这个需求为例,如果使用 double 类型你会怎么限制? -```java -if (price <= 0) { - throw new IllegalArgumentException("Price should be positive"); -} -``` -如果使用 double 作为类型,那我们要在使用的地方都保证价格的正确性,像这样的价格校验就应该是使用的地方到处写的。 - -如果补齐这里缺失的模型,我们可以引入一个 Price 类型,这样的校验就可以放在初始化时: -```java -class Price { - private long price; - - public Price(final double price) { - if (price <= 0) { - throw new IllegalArgumentException("Price should be positive"); - } - - this.price = price; - } -} -``` -这种引入一个模型封装基本类型的重构手法,叫以对象取代基本类型(Replace Primitive with Object)。 -有了这个模型,还可再进一步,比如,如果我们想要让价格在对外呈现时只有两位,在没有 Price 类的时候,这样的逻辑就会散落代码的各处,事实上,代码里很多重复的逻辑就是这样产生的。 - -现在我们可以在 Price 类里提供一个方法: -```java -public double getDisplayPrice() { - BigDecimal decimal = new BigDecimal(this.price); - return decimal.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); -} -``` -其实,使用基本类型和使用继承出现的问题异曲同工。 -大部分程序员都了解一个设计原则:组合优于继承,即不要写出这样的代码: -```java -public Books extends List { - ... -} -``` -而应该写成组合的样子,也就是: -```java -public Books { - private List books; - ... -} -``` -- 把Books写成继承,是因为在开发者眼里,Books 就是一个书的集合 -- 有人用 double 做价格的类型,因为在他看来,价格就是一个 double - -这里的误区在于,一些程序员只看到模型的相同之处,却忽略了差异。Books 可能不需要提供 List 的所有方法,价格的取值范围与 double 也有差异。 - -但 Books 的问题相对容易规避,因为产生了一个新的模型,有通用的设计原则帮助我们判断这个模型构建得是否恰当,而价格的问题却不容易规避,因为这里没有产生新的模型,也就不容易发现问题。 - -这种以基本类型为模型的坏味道称为基本类型偏执(Primitive Obsession)。 -这里的基本类型,不限于程序设计语言提供的各种基本类型,像字符串也是一个产生这种坏味道的地方。 - -很多人对于集合类型(比如数组、List、Map 等等)的使用也属于这种坏味道: -- 封装所有的基本类型和字符串 -- 使用一流的集合。 - -封装之所以有难度,主要在于它是一个构建模型的过程,而很多程序员写程序,只是用着极其粗粒度的理解写着完成功能的代码,根本没有构建模型的意识;还有一些人以为划分了模块就叫封装,所以,我们才会看到这些坏味道的滋生。 - -这里我给出的坏味道,其实也是在挑战一些人对于编程的认知:那些习以为常的代码居然成了坏味道。而这只是一个信号,一个起点,告诉你这段代码存在问题,但真正要写好代码,还是需要你对软件设计有着深入的学习。 - -# 总结 -与封装有关的坏味道: -- 过长的消息链,或者叫火车残骸 -- 基本类型偏执。 - -火车残骸的代码就是连续的函数调用,它反映的问题就是把实现细节暴露了出去,缺乏应有的封装。重构的手法是隐藏委托关系,实际就是做封装。软件行业有一个编程指导原则,叫迪米特法则,可以作为日常工作的指导,规避这种坏味道的出现。 - -基本类型偏执就是用各种基本类型作为模型到处传递,这种情况下通常是缺少了一个模型。解决它,常用的重构手法是以对象取代基本类型,也就是提供一个模型代替原来的基本类型。基本类型偏执不局限于程序设计语言提供的基本类型,字符串也是这种坏味道产生的重要原因,再延伸一点,集合类型也是。 - -这两种与封装有关的坏味道,背后体现的是对构建模型了解不足,其实,也是很多程序员在软件设计上的欠缺。想成为一个更好的程序员,学习软件设计是不可或缺的。 - -**构建模型,封装散落的代码。** - -# 怎样的封装才算是高内聚? -链式调用不一定都是火车残骸。比如builder模式,每次调用返回的都是自身,不牵涉到其他对象,不违反迪米特法则。又比如java stream操作,就是声明性操作。 - -构建模型还有一个好处是增加了一层抽象,屏蔽了外部变化,类似防腐层作用。比如写可移植的c代码用typedef定义内部使用的类型而非直接使用基本类型,或是DDD中领域内只处理本领域的对象,使用其他领域的对象要先经过转换而不会直接使用。 - -Java 里的 JavaBean,用MyBatis Genarater或者Lombok生成都会有Setter方法,这样数据库查询或者接受参数时,数据自动映射到这个对象。如果不用setter的话,应该怎么赋值? -其实,现在的数据库映射用的都是反射的方式实现,与setter关系不大。 - - -1.如果你的编码方式是置顶向下的,且当前层都只面向意图定义空类和空函数。那么写出提倡的这种风格其实很正常。 -2.结合1描述的编码方式。顶层类中不会有基础类型,每个属性的类型都会是一个面向意图的类来承接。顶层函数的实现部分只会有一个个函数,哪怕函数实现只有一行。 - - -我设计了一个客hu模型,包含客hu基本信息(证jian类型,证jian号码,名称),个人信息(有些客hu是自然人,客hu不是用户),企业信息,联xi电话List,地址List(注册地址,经营地址,身份证地址),等等;个人信息、企业信息、联xi电话等都是懒加载,需要用到的时候get才执行查询。如果按照本节的说法,可能这种设计就有问题,但是不知道怎么解决 -先要分析这些模型之间的关系,如果它们是聚合和聚合根之间的关系,那就要一次性的拿出来,没有什么懒加载的问题。如果是组合关系,也许用不同的访问入口更合适。 \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\346\234\200\345\245\275\347\232\204\347\274\226\347\250\213\350\257\255\350\250\200\346\230\257\346\200\216\346\240\267\347\232\204?.md" "b/\351\207\215\346\236\204/\346\234\200\345\245\275\347\232\204\347\274\226\347\250\213\350\257\255\350\250\200\346\230\257\346\200\216\346\240\267\347\232\204?.md" deleted file mode 100644 index ad8c2bd0c4..0000000000 --- "a/\351\207\215\346\236\204/\346\234\200\345\245\275\347\232\204\347\274\226\347\250\213\350\257\255\350\250\200\346\230\257\346\200\216\346\240\267\347\232\204?.md" +++ /dev/null @@ -1,56 +0,0 @@ -> 没有语言是完美的。 - -因语言演化,不同时期不同版本的程序员写的代码,在用同一门语言在编程。所以,我们经常看到各种不同时期风格代码并存。 - -新的语言特性都是为提高代码表达性,减少犯错几率。多用新语言特性写代码,绝对没毛病! - -> 那应该如何使用“新”语言特性,让代码写得更好? -# Optional -![](https://img-blog.csdnimg.cn/4b605b543d3e47198645f4fd71780cd2.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -暂不考虑缺乏封装问题。这段代码有问题。因为未考虑对象可能为 null。 -更好的写法: -![](https://img-blog.csdnimg.cn/a73f3114268c42c5b601fb0fff3af399.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -这种写法很稀缺,所以,新项目总是各种NPE。如果你要问程序员为什么不写对象为 null 的判断,答曰:忘了。 - -> 空指针的发明者 Tony Hoare 将其称为“自己犯下的十亿美元错误”。 - -还好Java 8有Optional,它提供了一个对象容器,你需要从中“取出(get)”你所需要对象,但取出前,你需判断该对象容器中是否真的存在一个对象。 -![](https://img-blog.csdnimg.cn/e2a57e7b800d42999c06093ddc8e468a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -你再不会忘掉判断对象是否存在,因为你需要从 Optional 取出存在里面的对象。正是这**多余的**一步,避免你“忘”了。 - -更简洁的写法: -![](https://img-blog.csdnimg.cn/d4376180d76143b6a8772ec32071d89c.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -项目中所有可能为 null 的返回值,都要返回 Optional,可大大减少各种意外惊喜。 -# 函数式编程 -![](https://img-blog.csdnimg.cn/662b603e9ed742d4a15a58cb2a7d0c8a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -准备参数的代码: -- 筛选出审核通过的章节 -- 再把章节转换成与翻译引擎通信的格式 -- 最后把所有得到的单个参数打包成一个完整的章节参数。 - -Java8后,不是不需要遍历集合,而是有了更好的遍历集合方式。函数式编程,大部分操作都可归结成列表转换,最核心的列表转换就是 map、filter 和 reduce。 - -大部分循环语句都是在对一个元素集合进行操作,而这些操作基本上都可以用列表操作进行替代。 - -再CR这段代码,有一循环语句,这循环语句在处理的是一个集合中的元素,可用列表转换: -![](https://img-blog.csdnimg.cn/be9db1cc30e04b2286dd71e5fcfd99e0.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -有人可能说这段代码还不如我原来的循环语句简单。两种写法根本差异是抽象层次不同,可读性完全不同: -- 循环语句是在描述实现细节 -必须要做一次“阅读理解”知晓其中细节才能知晓整个场景 -- 列表转换的写法是在描述做什么 -基本上和我们用语言叙述过程对应。 - -> 其实大多数人选择循环语句只是因为对列表转换不熟练,多写即可。 - -为什么我的感觉实践中,使用这种风格,为写出来的代码更难理解? -你在列表转换过程中写了太多代码!很多人直接在列表转换过程中写 lambda。lambda 本身相当于一个匿名函数,所以,很多人写成长函数了。 -lambda 是为了写短小代码提供的便利,所以,lambda 中写出大片代码,根本就是违反 lambda 设计初衷的。最好的 lambda 应只有一行代码。 - -> 那若一个转换过程中就有很多操作咋办? - -提取出一个函数!就像 toSectionParameter:完成从 Section 到 SectionParameter 转换。这样一来,列表转换的本身就完全变成了一个声明,这样的写法才是能发挥出列表转换价值的写法。 -# 总结 -代码风格逐步演化,每个程序员对语言的理解程度都有所差异,所以,我们的屎山项目中,各种代码风格并存,各具风骚,加重代码理解难度,这其实就是:不一致的坏味道。 - -编程风之所以格会过时,是因为它存在问题,新风格就是用更好方案,注意跟上时代,拥抱变化,多用新特性! \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\346\266\210\351\231\244\350\277\207\351\225\277\345\217\202\346\225\260\345\210\227\350\241\250.md" "b/\351\207\215\346\236\204/\346\266\210\351\231\244\350\277\207\351\225\277\345\217\202\346\225\260\345\210\227\350\241\250.md" deleted file mode 100644 index 045966e5f5..0000000000 --- "a/\351\207\215\346\236\204/\346\266\210\351\231\244\350\277\207\351\225\277\345\217\202\346\225\260\345\210\227\350\241\250.md" +++ /dev/null @@ -1,222 +0,0 @@ -有经验的程序员应该都见过,一个方法坐拥几十上百个参数。 - -# 方法为何要有参数? -因为不同方法间需共享信息。 - -但方法间共享信息的方式不止一种,除了参数列表,还有全局变量。但全局变量总能带来意外惊喜,所以,取消全局变量也是各大语言的趋势。 - -但方法之间还是要传递信息的,不能用全局变量,于是参数就成了唯一选择,于是,只要你想到有什么信息要传给一个方法,就会直接它加到参数列表中,参数列表也越来越长。 - -# 长参数列表的问题 -参数列表过长,你一个 crud 程序员就很难完全掌控这些逻辑了呀! - -所以症结是数量多,解决关键也就在于降低参数的数量。 -# 解决方案 -## 聚沙成塔 -一个简单的创建博客的方法: -```java -public void createActicle(final String title, - final String introduction, - final URL coverUrl, - final ActicleType type, - final ActicleColumn column, - final String protagonists, - final String tags, - final boolean completed) { - ... - Acticle acticle = Acticle.builder - .title(title) - .introduction(introduction) - .coverUrl(coverUrl) - .type(type) - .column(column) - .protagonists(protagonists) - .tags(tags) - .completed(completed) - .build(); - - this.repository.save(acticle); -} -``` -参数列表包含了一篇博客所要拥有的各种信息,比如:博客标题、博客简介、封面 URL、博客类型、博客归属的专栏、主角姓名、博客标签、博客是否完结...... - -如果只是想理解逻辑,或许你还会觉得参数列表挺合理,毕竟它把创建一篇博客所需的各种信息都传给了方法,这也是大部分人面对一段代码时理解问题的最初角度。 -虽然这样写代码容易让人理解,但这不足以让你发现问题。 - -> 现在产品要求在博客里增加一项信息,标识这部博客是否是签约博客,也就是这部博客是否可收费,咋办? - -很简单啊!我直接新增一个参数。很多屎山就这么来的,积少成多,量变引起质变! - -这里所有参数都是创建博客所必需的。所以,可以做的就是将这些参数封装成一个类,一个创建博客的参数类: -```java -public class NewActicleParamters { - private String title; - private String introduction; - private URL coverUrl; - private ActicleType type; - private ActicleColumn column; - private String protagonists; - private String tags; - private boolean completed; - ... -} -``` -这样参数列表就只剩下一个参数了: -```java -public void createActicle(final NewActicleParamters parameters) { - ... -} -``` -所以, **将参数列表封装成对象吧** ! - -只是把一个参数列表封装成一个类,然后,用到这些参数的时候,还需要把它们一个个取出来,这会不会是多此一举呢?就像这样: -```java -public void createActicle(final NewActicleParamters parameters) { - ... - Acticle acticle = Acticle.builder - .title(parameters.getTitle()) - .introduction(parameters.getIntroduction()) - .coverUrl(parameters.getCoverUrl()) - .type(parameters.getType()) - .channel(parameters.getChannel()) - .protagonists(parameters.getProtagonists()) - .tags(parameters.getTags()) - .completed(parameters.isCompleted()) - .build(); - - this.repository.save(acticle); -} -``` -若你也这样想,说明:你还没有形成对软件设计的理解。我们并非简单地把参数封装成类,站在设计角度,这里引入的是一个新模型。 -**一个模型的封装应该以【行为】为基础**。 -之前没有这个模型,所以想不到它应该有什么行为,现在模型产生了,它就该有自己配套的行为。 - -那这个模型的行为是什么?构建一个博客对象,这很清晰,则代码就能进一步重构: -```java -public class NewActicleParamters { - private String title; - private String introduction; - private URL coverUrl; - private ActicleType type; - private ActicleColumn column; - private String protagonists; - private String tags; - private boolean completed; - - public Acticle newActicle() { - return Acticle.builder - .title(title) - .introduction(introduction) - .coverUrl(coverUrl) - .type(type) - .column(column) - .protagonists(protagonists) - .tags(tags) - .completed(completed) - .build(); - } -} -``` -创建博客的方法就得到了极大简化: -```java -public void createActicle(final NewActicleParamters parameters) { - ... - Acticle acticle = parameters.newActicle(); - - this.repository.save(acticle); -} -``` - -“如何扩展需求”?如果需求扩展,需要增加创建博客所需的内容,那这个参数列表就是不变的,相对来说,它就是稳定的。 - -那这个类就会不断膨胀,变成一个大类,那该怎么办呢?如何解决大类? -## 动静分离 -不是所有情况下,参数都属于一个类: -```java -public void getChapters(final long acticleId, - final HttpClient httpClient, - final ChapterProcessor processor) { - HttpUriRequest request = createChapterRequest(acticleId); - HttpResponse response = httpClient.execute(request); - List chapters = toChapters(response); - processor.process(chapters); -} -``` -根据博客 ID 获取其对应章节信息。 -纯以参数个数论,参数数量不多。 - -如果你只是看这个方法,可能很难发现直接问题。绝对数量不是关键点,参数列表也应该是越少越好。 - -在这几个参数里面,每次传进来的 acticleId 都不一样,随请求不同而改变。但 httpClient 和 processor 两个参数一样,因为都有相同逻辑,没什么变化。 -即acticleId 的变化频率同 httpClient 和 processor 这两个参数变化频率不同。 - -不同的数据变动方向也是不同关注点。这里表现出来的就是典型的动数据(acticleId)和静数据(httpClient 和 processor),它们是不同关注点,应该分离。 - -具体到这个场景下,静态不变的数据完全可以成为这个方法所在类的一个字段,而只将每次变动的东西作为参数传递就可以了。按照这个思路,代码可以改成这个样子: -```java -public void getChapters(final long acticleId) { - HttpUriRequest request = createChapterRequest(acticleId); - HttpResponse response = this.httpClient.execute(request); - List chapters = toChapters(response); - this.processor.process(chapters); -} -``` -这个坏味道其实是一个软件设计问题,代码缺乏应有的结构,所以,原本应该属于静态结构的部分却以动态参数的方式传来传去,无形之中拉长了参数列表。 - -长参数列表固然可以用一个类进行封装,但能够封装出这个类的前提条件是:这些参数属于一个类,有相同变化原因。 - -如果方法的参数有不同的变化频率,就要视情况而定了。对于静态的部分,我们前面已经看到了,它可以成为软件结构的一篇分,而如果有多个变化频率,我们还可以封装出多个参数类。 - -## 告别标记 -```java -public void editChapter(final long chapterId, - final String title, - final String content, - final boolean apporved) { - ... -} -``` -待修改章节的ID、标题和内容,最后一个参数表示这次修改是否直接审核通过。 - -前面几个参数是修改一个章节的必要信息,重点在最后这个参数。 -从业务上说,如果是作者进行编辑,之后要经过审核,而如果编辑来编辑的,那审核就直接通过,因为编辑本身扮演了审核人的角色。所以,你发现了,这个参数实际上是一个标记,标志着接下来的处理流程会有不同。 - -使用标记参数,是程序员初学编程时常用的一种手法。正是这种手法实在太好用,导致代码里flag肆意飘荡。不仅变量里有标记,参数里也有。很多长参数列表其中就包含了各种标记参数。 - -在实际的代码中,必须小心翼翼地判断各个标记当前的值,才能做好处理。 - -解决标记参数,一种简单的方式就是,将标记参数代表的不同路径拆分出来。 - -这里的一个方法可以拆分成两个方法,一个方法负责“普通的编辑”,另一个负责“可以直接审核通过的编辑”。 - -```java -// 普通的编辑,需要审核 -public void editChapter(final long chapterId, - final String title, - final String content) { - ... -} -``` -```java -// 直接审核通过的编辑 -public void editChapterWithApproval(final long chapterId, - final String title, - final String content) { - ... -} -``` -标记参数在代码中存在的形式很多,有的是布尔值、枚举值、字符串或整数。都可以通过拆分方法的方式将它们拆开。在重构中,这种手法叫做移除标记参数(Remove Flag Argument)。 - -只有短小的代码,我们才能有更好地把握,而要写出短小的代码,需要我们能够“分离关注点”。 - -# 总结 -应对长参数列表主要的方式就是减少参数的数量,最直接的就是将参数列表封装成一个类。但并不是说所有的情况都能封装成类来解决,我们还要分析是否所有的参数都有相同的变动频率。 - -变化频率相同,则封装成一个类。 -变化频率不同的话: -- 静态不变的,可以成为软件结构的一篇分 -- 多个变化频率的,可以封装成几个类 - -此外,参数列表中经常会出现标记参数,这是参数列表变长的另一个重要原因。对于这种标记参数,一种解决方案就是根据这些标记参数,将方法拆分成多个方法。 - -**减少参数列表,越少越好。** \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\347\274\226\347\250\213\344\271\213\344\270\215\345\217\230\346\200\247.md" "b/\351\207\215\346\236\204/\347\274\226\347\250\213\344\271\213\344\270\215\345\217\230\346\200\247.md" deleted file mode 100644 index b28b40a2ce..0000000000 --- "a/\351\207\215\346\236\204/\347\274\226\347\250\213\344\271\213\344\270\215\345\217\230\346\200\247.md" +++ /dev/null @@ -1,138 +0,0 @@ -有一类Bug是很让人头疼的,就是你的代码怎么看都没问题,可是运行起来就是出问题了。 - -某程序库是其他人封装的,我只是拿来用。按理我调用这个函数逻辑也不复杂,不应该有问题。 -为快定位问题,还是打开了这个程序库源码。发现底层实现中,出现全局变量。 - -在我的代码执行过程中,有别的程序会调用另外函数,修改这个全局变量,导致程序执行失败。 -表面看,我调用的这个函数和另外那个函数没关系,但它们却通过一个底层全局变量,相互影响。 - -有人认为这是全局变量使用不当,所以在Java设计中,甚至取消了全局变量,但类似问题并未减少,只是以不同面貌展现,比如,static 变量。 - -这类问题真正原因还是在于变量可变。 -# 变量的危害 -变量不就应该是变的吗? -先上代码: -![](https://img-blog.csdnimg.cn/585a330aaf6646faba56e4311eedc7e6.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_18,color_FFFFFF,t_70,g_se,x_16) -但并发环境下有 bug。正确写法修正如下: -![](https://img-blog.csdnimg.cn/395d080991d04e04bad65aaf64f99ecd.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -区别在于,SimpleDateFormat在哪里构建: -- 被当作一个字段 -- 在方法内部 - -不同做法根本差异在于SimpleDateFormat **对象是否共享**。 - -为什么对象共享会有问题? -查看源码: -![](https://img-blog.csdnimg.cn/ddb7ceee54e1445a88ca2cee6228131a.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -calendar是SimpleDateFormat类的一个字段 -![](https://img-blog.csdnimg.cn/ad3b46e87f824e20b0e1d2a42323b66f.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -在执行format过程中修改了calendar,导致 bug。 -![](https://img-blog.csdnimg.cn/b04e706c500b4196bdda693562ef0497.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -1. A线程把变量值修改成自己需要的值 -2. 此时线程切换,B线程开始执行,将变量值修改成它需要值 -3. 线程切换回来,A继续执行,但此时变量已不是原来A自己所设值,所以,执行出错 - -对于SimpleDateFormat,calendar就是那个共享变量:一个线程刚设置的值,可能会被另一个线程修改,导致意外惊喜。 -而Test2中,每次创建一个新SDF对象,避免了线程共享,bug解决。 - -> 若就爱Test1写法,SDF该如何改写呢? - -SDF作者水平太次,换我写,就给它加synchronized或Lock锁,但你轻易地引入多线程的复杂性。多线程是另外一个关注点,能不用就不用。 - -推荐方案:将calendar变成局部变量,从根本解决线程共享变量问题。 - -这类问题在函数式编程中却几乎不可能存在,因函数式编程的不变性。 -# 不变性 -函数式编程的不变性主要体现在: -- 值 -可理解为一个初始化之后就不再改变的量,即当你使用一个值时,值不会变 -很多人开始无界:初始化后不会改变的“值”,这不就是常量吗?注意,常量一般是预先确定的,而值是在运行过程中生成的。 -- 纯函数 -对于相同的输入,给出相同的输出;没有副作用 - -二者结合: -- 值保证不会显式改变一个量 -- 纯函数保证不会隐式改变一个量 - -函数式编程中的函数源自数学的函数。当前语境下,函数就是纯函数,一个函数计算后不会产生额外改变,而函数中用到的一个个量就是值,它们是不会随着计算改变的。 -所以,在函数式编程中,计算天然不变。 - -因为不变性,所以前面的问题也就不复存在: -- 若你拿到一个量,这次的值是1,下一次它还是1,无需担心它会改变 -- 调用一个函数,传进去同样的参数,它保证给出同样的结果,行为是完全可以预期的,不会碰触到其他部分。即便是在多线程下,也无需担心同步问题 - -传统方式的基础是面向内存单元,任性的改来改去已成为大多程序员的本能。所以习惯如下代码 -```java -counter = counter + 1 -``` - -传统的编程方式占优的地方是执行效率,而如今,这优点则越来越不明显,反而因为到处都可变,带来更多问题。 -我们更该在设计中,借鉴函数式编程,把不变性更多应用在业务代码中。 - -> 怎么应用呢? - -## 值 -可以编写不变类,即对象一旦构造出来就不能改变,最常见的不变类就是String类了, - -> 如何编写一个不变类呢? - -- 所有的字段只在构造器初始化 -- 所有的方法都是纯函数 -- 若需要改变,返回一个新对象,而非修改已有字段 -如String#replace方法,会用一个新字符(newChar)替换掉这个字符串中原字符(oldChar),但并非直接修改已有字符串,而是创建一个新字符串对象返回。好处是,使用原来这个字符串的类无需担心自己引用的内容会随之变化。 - -理解这个,你就更理解 DDD 里的值对象(Value Object)是啥了。 - -## 纯函数 -写纯函数的关键: -- 不修改任何字段 -- 不调用修改字段内容的方法 - -Java并非严格函数式编程语言,不是所有量都是值。 -所以在实用性角度,可以实践: -- 若要使用变量,就使用局部变量 -- 使用语法中不变的修饰符 -多用final。无论是修饰变量、方法,都是让编译器提醒你,要多在不变性上设计 - -拥有不变性编程思维后,你会发现很多习惯都让你的代码陷入水深火热,比如你最爱的setter就是提供了一个接口,专门修改一个对象内部的值。 - -但要承认现实,纯粹函数式编程很难,只能把编程原则设定为尽可能编写不变类和纯函数。但你依然能套在大量现存业务代码上。 - -大多数涉及可变或副作用的代码,应该都是与外部系统交互。能够把大多数代码写成不变的,的确能减少许多后期维护成本。 - -正由于不变性,有些新语言默认不再是变量,而是值。比如,Rust声明的是个值,一旦初始化,就无法修改: -```rust -let result = 1; -``` -而若你想声明一个变量,必须显式告诉编译器: -```rust -let mut result = 1; -``` -Java的Valhalla 项目也在尝试将值类型引入语言。所以,不变性,真的是减少程序问题的发展趋势。 - -# 事件溯源 -事件源:不要轻易添加「状态」,取而代之的是通过事件源(通过事件的发生时间,去重建历史的对象及对应关系),我觉得这本质上是给实体模型赋予不变性,从而消除因为状态变化而引发的副作用。 - -不变性,也是诸多编程原则背后的原则。例如,基于「不变性」这样一个目标,领悟驱动设计中的「值对象」 做法(定义一个不变的对对象,用于标识实体之外的其他业务模型),以及马丁.福勒提出的「无副作用方法」(side-effect-free function,指代方法不会对对象状态产生任何改变) 等,就都显得非常恰如其分了。 - -更极端的如 Rust ,直接让不变性成为语法语汇,有人评价这是一种把道德规范引入法律的做法,觉得这种类比有一定道理。然而在语言层面,至少倒逼程序员产出不那么坏的代码。 - -对比一般的CRUD,就是没有修改,只有不断的插入值不同的同一条记录,下次修改时,在最新一条基础上修改值后再插入一条最新的。有点类似Java String 的处理方式,修改是生成另一个对象。 - - -# 总结 -函数式编程,限制使用赋值语句,它是对程序中的赋值施加了约束。一旦初始化好一个量,就不要随便给它赋值了。 - -函数式编程相关的各种说法:无副作用、无状态、引用透明等,都是在说不变性。 - -从Effect Java中学到了builder模式,实践DDD,也应多考虑不变性: -比如修改用户信息,业务逻辑提取入参数据,返回值是通过builder构造一个新对象;builder中有完整性校验;这样可保证经过业务逻辑处理后返回的对象一定是一个新的并且是符合业务完整性的领域对象。 - -变化是需求层面的不得已,不变是代码层面的努力控制。 - -最后,请**尽量编写不变类和纯函数。** - -> 参考 -> - https://www.youtube.com/watch?v=mGR0A5Jyolg \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\344\275\277\347\224\250\351\235\231\346\200\201\345\267\245\345\216\202\346\226\271\346\263\225\357\274\210\347\256\200\345\215\225\345\267\245\345\216\202\357\274\211\346\233\277\344\273\243\346\236\204\351\200\240\345\231\250.md" "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\344\275\277\347\224\250\351\235\231\346\200\201\345\267\245\345\216\202\346\226\271\346\263\225\357\274\210\347\256\200\345\215\225\345\267\245\345\216\202\357\274\211\346\233\277\344\273\243\346\236\204\351\200\240\345\231\250.md" deleted file mode 100644 index 3cfa0d3065..0000000000 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\344\275\277\347\224\250\351\235\231\346\200\201\345\267\245\345\216\202\346\226\271\346\263\225\357\274\210\347\256\200\345\215\225\345\267\245\345\216\202\357\274\211\346\233\277\344\273\243\346\236\204\351\200\240\345\231\250.md" +++ /dev/null @@ -1,195 +0,0 @@ -# 1 简单工厂 -- 定义 -由一个工厂对象决定创建出哪一种产品类的实例 - -- 类型 -创建型,但不属于GOF23种设计模式 - -- 官方定义 -Define an interface for creating an object,but let subclasses decide which class to instantiate.Factory Method lets a class defer instantiation to subclasses -定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类 - - -## 1.1 基本案例 -![](https://img-blog.csdnimg.cn/d568ab17cf6249d482ac870b33f0bcd2.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/b258dd21e3a8416587146ad443ab8d20.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/5800debd53da414aaa70cfe5e33cec82.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/883600aaeb15432c97544c5cb5d2d727.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/8684e7ff9aa64b1aad86591c53d26c44.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/dc43af8bd45d485a98c652ec71df061c.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -## 1.2 JDK应用实例 -### 日历类 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTZkMDE4OWFkMmNhMTU0YjAucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTUwZjI0OTFmZTdhMmFjNzUucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTFjZjBlNWNkNTAxNzZjMDgucG5n?x-oss-process=image/format,png) -### 迭代器 -Collection 接口就相当于 VideoFactory -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTdmMDFjMmRmMTBmN2IzMDkucG5n?x-oss-process=image/format,png) -相当于各种具体的工厂,如 JavaVideoFactory -![父类接口,子类实现](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWMxOTY5ZjUzYTllOGY5ZDIucG5n?x-oss-process=image/format,png) -Itr 就是具体产品 JavaVideo -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWE3Mzc5MDliYjg0ZTQxNjcucG5n?x-oss-process=image/format,png) -### 工厂应用 -#### 为解决 url 协议扩展使用 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWI5MjRiMDY0MjlmNTU0ZWQucG5n?x-oss-process=image/format,png) -- Launcher#Factory静态类 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTRlOWJlYjBjODEwMDI1YTgucG5n?x-oss-process=image/format,png) -#### logback 应用 -![LoggerFactory#getLogger(String name)](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTQwMzNkZjJhYjMyYTM0MWQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWE1ZGZhYTU2Yjg2YjFiMzIucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTY4NWUzY2ZiMTQ0ODAxZTkucG5n?x-oss-process=image/format,png) - -### JDBC实例 -![直接注册 MySQL 驱动](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTRhNWQ1N2FlNjQxNmE1YWYucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQzMTFiYzYzMDRlMTdlZjUucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWRlZjU2ZjZmNTM0ZmE0NzIucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQwN2JmOThkNmI1N2MyMjcucG5n?x-oss-process=image/format,png) -返回值是一个抽象类,必有一子类实现 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWIzNTgwYjdkNGQ3MTgwYjUucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWUzNmQ5MWU2OGZmMDgzNTIucG5n?x-oss-process=image/format,png) -![通过间接继承此处理器](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTRhMjdiYjljM2IzZWQ2NGIucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQyYTEwNGM0YmM1NjZiNTYucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTNlYzcyZWQwN2Y5ZjJkNGUucG5n?x-oss-process=image/format,png) -这其中URLStreamHandler就相当于各种抽象产品,而其实现类即各种具体的产品 -URLStreamHandlerFactory就相当于 VideoFactory -而如下 Factory 就相当于如 JavaVideoFactory/PythonVideoFactory -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWUwZDJlYzUyZjNlZGIwMjMucG5n?x-oss-process=image/format,png) - -### Logback实例 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTA1NmI3ZTNlNmVlYmIxYzgucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTg4OGUxMGJiMmQwNzEzMjIucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWZiNWZjZDM5MTJlNGY2NDkucG5n?x-oss-process=image/format,png) - - -客户端获得一个类实例的传统方式是调用由类提供的public构造器。但还有一种技术,一个类可以提供public的静态工厂方法,只是一个返回类实例的静态方法。 - -> 静态工厂方法与设计模式的工厂方法模式不同。在设计模式中并无直接等价的说法。 -# 2 优点 -只需要传入一个正确的参数,即可获取所需对象,无需知其创建细节。 -## 2.1 实名制 -如果构造器的参数本身并不能描述清楚返回的对象,那么具有确切名称的静态工厂则代码可读性更佳! - -例如 BigInteger 类的构造器 `BigInteger(int, int, Random)` 返回值多半是质数,那么最好使用静态工厂方法: `BigInteger.probablePrime` -![](https://img-blog.csdnimg.cn/20200708215827121.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -一个类只能有一个带给定签名的构造器。可 SE 一般还能通过提供两个构造器来解决,而构造器的参数列表就仅在参数类型的顺序上不同。dirty code!这样的 API,用户永远无法记住该用哪个构造器,并且最终会错误地调用不合适的构造器。不阅读类文档,使用者人根本不知道代码的作用。 - -而静态工厂方法有确切的名称,所以没这局限。如果一个类就是需要具有相同签名的多个构造器,那么静态工厂方法就很 nice,注意精心的命名来突出它们的区别。 - -## 2.2 无需在每次调用时创建新对象 -这使得不可变类使用事先构造好的实例,或在构造实例时缓存实例,重复分配以避免创建不必要的重复对象。`Boolean.valueOf(boolean)` 方法:它从不创建对象。 -Boolean类中该方法将 boolean 基本类型值转换为一个 Boolean 对象引用 -- 返回一个Boolean表示指定实例boolean的值。 如果指定的boolean值是true ,则此方法返回Boolean.TRUE ; 如果是false ,这个方法返回Boolean.FALSE 。 如果并不需要一个**新的**Boolean 实例,该方法一般应优于构造器中使用Boolean(boolean) ,因为此方法可能产生显著的更好的空间和时间性能 -![](https://img-blog.csdnimg.cn/20200707204138711.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -这类似于[享元模式](https://javaedge.blog.csdn.net/article/details/107218915)。如果经常请求相同对象,特别是创建对象代价高时,可以极大提高性能。 - - -静态工厂方法在重复调用下返回相同对象,这样类能严格控制存在的实例。这样的类称为实例受控的类。编写实例受控的类有几个原因。 -- 允许一个类来保证它是一个单例或不可实例化的。同时,它允许一个不可变的值类保证不存在两个相同的实例: -`a.equals(b)` 当且仅当 `a==b`。这是享元模式的基础。枚举类型提供了这种保证。 - - -## 2.3 获取返回类型的任何子类的对象 -这为选择返回对象的类提供灵活性。 - -这种灵活性的一个应用是 API 可以在public其类的情况下返回对象。以这种方式隐藏实现类会形成一个非常紧凑的 API。这适用于基于接口的框架,其中接口为静态工厂方法提供了自然的返回类型。 - -Java 8 前,接口不能有静态方法。按照惯例,一个名为 Type 的接口的静态工厂方法被放在一个名为 Types 的不可实例化的伴生类。例如,Java 的 Collections 框架有 45 个接口实用工具实现,提供了不可修改的集合、同步集合等。几乎所有这些实现都是通过一个非实例化类(`java.util.Collections`)中的静态工厂方法导出的。返回对象的类都是非public的。 - -现在的Collections 框架 API 比它导出 45 个独立的public类小得多,每个公共类对应一个方便的实现。减少的不仅仅是 API 的数量,还有概念上的减少:程序员为了使用 API 必须掌握的概念的数量和难度。程序员知道返回的对象是由相关的接口精确指定的,因此不需阅读额外的类文档。 -**使用这种静态工厂方法需要客户端通过接口而不是实现类引用返回的对象,这通常是很好的做法**。 - -Java 8 取消了接口不能包含静态方法的限制,因此通常没有理由为接口提供不可实例化的伴生类。许多公共静态成员应该放在接口本身中,而不是放在类中。但仍有必要将这些静态方法背后的大部分实现代码放到单独的包私有类中。因为 Java 8 要求接口的所有静态成员都是公共的,而 Java 9 允许接口有私有的静态方法,但是静态字段和静态成员类仍然需是public - -**A fourth advantage of static factories is that the class of the returned object can vary from call to call as a function of the input parameters.** Any subtype of the declared return type is permissible. The class of the returned object can also vary from release to release. - -## 2.4 返回对象的类可以随调用的不同而变化 -这当然取决于输入参数不同。只要是已声明的返回类型的子类型都是允许的。返回对象的类也可以因版本而异。 - -- EnumSet 类没有public构造器 -![](https://img-blog.csdnimg.cn/20200709012340394.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -只有静态工厂。在 OpenJDK 中,它们返回两种子类之一的一个实例,这取决于底层 enum 类型的大小: -- 如果它有 64 个或更少的元素,就像大多数 enum 类型一样,静态工厂返回一个 long 类型的 RegularEnumSet 实例 -- 如果 enum 类型有 65 个或更多的元素,工厂将返回一个由 `long[]` 类型的 JumboEnumSet 实例。 - -客户端看不到这两个实现类的存在。如果 RegularEnumSet 不再为小型 enum 类型提供性能优势,它可能会在未来的版本中被删除,而不会产生任何负面影响。类似地,如果证明 EnumSet 有益于性能,未来的版本可以添加第三或第四个 EnumSet 实现。客户端既不知道也不关心从工厂返回的对象的类;它们只关心它是 EnumSet 的某个子类。 - -## 2.5 当编写包含静态工厂方法的类时,返回对象的类可以不存在 -这种灵活的静态工厂方法构成了服务提供者框架(Service Provider Framework,SPF)的基础,比如 JDBC API。 - -### SPF系统 -多个提供者实现一个服务,该系统使客户端可以使用这些实现,从而将客户端与实现分离。 - -SPF有三个基本组件 -- 代表实现的服务接口 -- 提供者注册 API,提供者使用它来注册实现 -- 服务访问 API,客户端使用它来获取服务的实例。服务访问 API 允许客户端指定选择实现的条件。在没有这些条件的情况下,API 返回一个默认实现的实例,或者允许客户端循环使用所有可用的实现。服务访问 API 是灵活的静态工厂,它构成了服务提供者框架的基础。 - -SPF第四个可选组件是服务提供者接口,它描述产生服务接口实例的工厂对象。在没有服务提供者接口的情况下,必须以反射的方式实例化实现。 -#### JDBC案例 -1. Connection扮演服务接口的一部分 -2. `DriverManager.registerDriver` 是提供者注册 API -3. `DriverManager.getConnection` 是服务访问 API -4. Driver是服务提供者接口 - -SPF模式有许多变体。例如,服务访问 API 可以向客户端返回比提供者提供的更丰富的服务接口,这就是[桥接模式](https://javaedge.blog.csdn.net/article/details/107219110u) 。依赖注入(DI)框架就可以看成是强大的服务提供者。从Java 6开始,平台就提供了一个通用服务提供者框架 `Java.util.ServiceLoader`,所以你不需要,通常也不应该自己写。JDBC 不使用 ServiceLoader,因为前者比后者早! - -# 3 缺点 -- 工厂类的职责相对过重,增加新的产品 -- 需要修改工厂类的判断逻辑,违背开闭原则 -## 3.1 **仅提供静态工厂方法的主要局限是,没有public或protected构造器的类不能被继承** -例如,不可能在集合框架中子类化任何便利的实现类。这可能是一种因祸得福的做法,因为它鼓励程序员使用组合而不是继承,这对于不可变类型是必须的。 - - -## 3.2 程序员很难找到它们 -它们在 API 文档中不像构造器吸睛,因此很难弄清楚如何实例化一个只提供静态工厂方法而没有构造器的类。Javadoc 工具总有一天会关注到静态工厂方法。 -通过在类或接口文档中多关注静态工厂方法,遵守通用命名约定的方式来减少这个困扰。 -下面是一些静态工厂方法的习惯命名。 - - -> from,类型转换方法,接收单个参数并返回该类型的相应实例 -![](https://img-blog.csdnimg.cn/20200710023241706.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -> of,聚合方法,接受多个参数并返回一个实例 -![](https://img-blog.csdnimg.cn/20200710023550552.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - -> valueOf,比 from 和 of 但更繁琐的一种替代方法 -![](https://img-blog.csdnimg.cn/20200710023748541.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -> instance 或 getInstance,返回一个实例,该实例由其参数(如果有的话)描述,但不和参数具有相同的值 - -```java -StackWalker luke = StackWalker.getInstance(options); -``` - - -> create 或 newInstance,与 instance 或 getInstance 类似,只是该方法保证每个调用都返回一个新实例 -![](https://img-blog.csdnimg.cn/20200710024426667.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -> getType,类似于 getInstance,但如果工厂方法位于不同的类中,则使用此方法。其类型是工厂方法返回的对象类型,例如: - -```java -FileStore fs = Files.getFileStore(path); -``` - -> newType,与 newInstance 类似,但是如果工厂方法在不同的类中使用。类型是工厂方法返回的对象类型,例如: -![](https://img-blog.csdnimg.cn/20200710024712390.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) - -> type,一个用来替代 getType 和 newType 的比较简单的方式 - -```java -List litany = Collections.list(legacyLitany); -``` -![](https://img-blog.csdnimg.cn/20200710025342311.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -### 适用场景 -- 工厂类负责创建的对象比较少 -- 客户端(应用层)只知道传入工厂类的参数,对于如何创建对象(逻辑)不关心 -# 总结 -静态工厂方法和public构造器 各有千秋,我们需要理解它们各自的优点。通常静态工厂更可取,因此切忌不考虑静态工厂就提供public构造器。 - -> 参考 -> - effective java \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\345\267\245\345\216\202\346\226\271\346\263\225\346\250\241\345\274\217.md" "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\345\267\245\345\216\202\346\226\271\346\263\225\346\250\241\345\274\217.md" deleted file mode 100644 index f7ad2ba859..0000000000 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\345\267\245\345\216\202\346\226\271\346\263\225\346\250\241\345\274\217.md" +++ /dev/null @@ -1,308 +0,0 @@ - -- 源码 -https://github.com/Wasabi1234/design-patterns -# 2 工厂方法模式 -- 定义 -定义一个创建对象的接口。但让实现这个接口的类来决定实例化哪个类,工厂方法让类的实例化推迟到子类中进行。 -- 类型 -创建型 - -### 通用类图 -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtOGUyMDQ1N2M5Yjg2ZjcxYi5qcGc?x-oss-process=image/format,png) -在工厂方法模式中: -- 抽象产品类Product负责定义产品的共性,实现对事物最抽象的定义 -- Creator为抽象创建类,即抽象工厂,具体如何创建产品类是由具体实现工厂ConcreteCreator完成 -## 2.1 简单工厂模式的升级 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTFlMzEzMGZmNTAxNjA0ODUucG5n?x-oss-process=image/format,png) - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTJlZGRhOTJiZDA5MmQ2MDAucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTA3NTFiMjZkNGQ1ODdiNWQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTJhYTM5N2Q4NmY2OGZlOTMucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWFhNDU3MDhhNWZkM2FhMzgucG5n?x-oss-process=image/format,png) - -对造人过程进行分析,该过程涉及三个对象:女娲、八卦炉、三种不同肤色的人 -- 女娲可以使用场景类`Client`表示 -- 八卦炉类似于一个工厂,负责制造生产产品(即人类) -- 三种不同肤色的人,他们都是同一个接口下的不同实现类,对于八卦炉来说都是它生产出的产品 -![女娲造人类图](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTIzYjcyOWFiYTI1MjNmZWUucG5n?x-oss-process=image/format,png) -- 接口Human是对人类的总称,每个人种都至少具有两个方法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQxYjI0YjBjZWU3MjBlYjcucG5n?x-oss-process=image/format,png) -- 黑色人种 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTUxMjRmZTBhYWViYjIxMWYucG5n?x-oss-process=image/format,png) -- 黄色人种 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTVkYzc5NDZkNTc5ZmZiNTYucG5n?x-oss-process=image/format,png) -- 白色人种 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LThiMmQ5NTVlYmVmYzE0NzEucG5n?x-oss-process=image/format,png) - -所有人种定义完毕,下一步就是定义一个八卦炉,然后烧制。 -最可能给八卦炉下达什么样的生产命令呢? -应该是 -- `给我生产出一个黄色人种(YellowHuman类)` - -而不会是 -- `给我生产一个会走、会跑、会说话、皮肤是黄色的人种` - -因为这样的命令增加了交流的成本,作为一个生产的管理者,只要知道生产什么就可以了,无需事物具体信息 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWJiMDA2MzA4NTcyZDM0MzkucG5n?x-oss-process=image/format,png) - -通过定义泛型对createHuman的输入参数产生两层限制 -- 必须是Class类型 -- 必须是Human的实现类 -其中的`T`表示,只要实现了Human接口的类都可以作为参数 - -只有一个八卦炉,其实现生产人类的方法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWViN2E3Yjc2Njk3NzI0NTIucG5n?x-oss-process=image/format,png) -人种有了,八卦炉也有了,剩下的工作就是女娲采集黄土,然后命令八卦炉开始生产 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTE3NTZlODQwOWI2ZjU1OTgucG5n?x-oss-process=image/format,png) - -人种有了,八卦炉有了,负责生产的女娲也有了 -运行一下,结果如下所示 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTBlOTExMzgzYmE4NDE2ZDkucG5n?x-oss-process=image/format,png) -## 案例 2 -# 注册工厂 -- `Pet` 层次生成对象的问题 -每当添加一种新`Pet` 类型,必须记住将其添加到 `LiteralPetCreator.java` 的条目中。在一个定期添加更多类的系统中,这可能会成为问题。 - -你可能会考虑向每个子类添加静态初始值设定项,因此初始值设定项会将其类添加到某个列表中。但静态初始值设定项仅在首次加载类时调用:生成器的列表中没有类,因此它无法创建该类的对象,因此类不会被加载并放入列表中。 - -必须自己手工创建列表。所以最好就是把列表集中放在一个明显的地方:层次结构的基类 - -使用*工厂方法*设计模式将对象的创建推迟到类本身。 -工厂方法以多态方式调用,创建适当类型的对象。 -`java.util.function.Supplier` 用 `T get()` 描述了原型工厂方法。协变返回类型允许 `get()` 为 `Supplier` 的每个子类实现返回不同的类型。 - -在本例中,基类 `Part` 包含一个工厂对象的静态列表,列表成员类型为 `Supplier`。对于应该由 `get()` 方法生成的类型的工厂,通过将它们添加到 `prototypes` 列表向基类“注册”。奇怪的是,这些工厂本身就是对象的实例。此列表中的每个对象都是用于创建其他对象的*原型*: - -```java -// typeinfo/RegisteredFactories.java -// 注册工厂到基础类 -import java.util.*; -import java.util.function.*; -import java.util.stream.*; - -class Part implements Supplier { - @Override - public String toString() { - return getClass().getSimpleName(); - } - - static List> prototypes = - Arrays.asList( - new FuelFilter(), - new AirFilter(), - new CabinAirFilter(), - new OilFilter(), - new FanBelt(), - new PowerSteeringBelt(), - new GeneratorBelt() - ); - - private static Random rand = new Random(47); - public Part get() { - int n = rand.nextInt(prototypes.size()); - return prototypes.get(n).get(); - } -} - -class Filter extends Part {} - -class FuelFilter extends Filter { - @Override - public FuelFilter get() { - return new FuelFilter(); - } -} - -class AirFilter extends Filter { - @Override - public AirFilter get() { - return new AirFilter(); - } -} - -class CabinAirFilter extends Filter { - @Override - public CabinAirFilter get() { - return new CabinAirFilter(); - } -} - -class OilFilter extends Filter { - @Override - public OilFilter get() { - return new OilFilter(); - } -} - -class Belt extends Part {} - -class FanBelt extends Belt { - @Override - public FanBelt get() { - return new FanBelt(); - } -} - -class GeneratorBelt extends Belt { - @Override - public GeneratorBelt get() { - return new GeneratorBelt(); - } -} - -class PowerSteeringBelt extends Belt { - @Override - public PowerSteeringBelt get() { - return new PowerSteeringBelt(); - } -} - -public class RegisteredFactories { - public static void main(String[] args) { - Stream.generate(new Part()) - .limit(10) - .forEach(System.out::println); - } -} -``` -并非层次结构中的所有类都应实例化;这里的 `Filter` 和 `Belt` 只是分类器,这样你就不会创建任何一个类的实例,而是只创建它们的子类(请注意,如果尝试这样做,你将获得 `Part` 基类的行为)。 - -因为 `Part implements Supplier`,`Part` 通过其 `get()` 方法供应其他 `Part`。如果为基类 `Part` 调用 `get()`(或者如果 `generate()` 调用 `get()`),它将创建随机特定的 `Part` 子类型,每个子类型最终都从 `Part` 继承,并重写相应的 `get()` 以生成它们中的一个。 - - - -工厂方法模式变种较多,看个比较实用的通用源码。 -- 抽象产品类 -![](https://img-blog.csdnimg.cn/01f25f3d12d844ef88a8a0b38e75b589.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 具体产品类,可以有多个,都继承于抽象产品类 -![](https://img-blog.csdnimg.cn/230d4301655a4170b5f405194525f530.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/cca3ac4a844045c5bde2fb090c309518.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 抽象工厂类,定义产品对象的产生 -![](https://img-blog.csdnimg.cn/f5e746211a684cdfab67bd96aa651c0b.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -- 具体工厂类,具体如何产生一个产品的对象,由具体工厂类实现 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWI2MGI5ODY1MDUxMzA5NWEucG5n?x-oss-process=image/format,png) -- 场景类 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWI0NzliNjMyZWVlYjFjMDIucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTYwN2EyYzU2MmVkMWEyZDAucG5n?x-oss-process=image/format,png) - -该通用代码是一个比较实用、易扩展的框架,读者可以根据实际项目需要进行扩展 -# 3 应用 -## 3.1 优点 -- 用户只需要关心所需产品对应的工厂,无须关心创建细节 -- 加入新产品符合开闭原则,提高可扩展性 - -- 良好的封装性,代码结构清晰 -一个对象创建是有条件约束的,如一个调用者需要一个具体的产品对象,只要知道这个产品的类名(或约束字符串)就可以了,不用知道创建对象的艰辛过程,降低模块间的耦合 -- 工厂方法模式的扩展性非常优秀 -在增加产品类的情况下,只要适当地修改具体的工厂类或扩展一个工厂类,就可以完成“拥抱变化” -例如在我们的例子中,需要增加一个棕色人种,则只需要增加一个BrownHuman类,工厂类不用任何修改就可完成系统扩展。 -- 屏蔽产品类 -这一特点非常重要,产品类的实现如何变化,调用者都不需要关心,它只需要关心产品的接口,只要接口保持不变,系统中的上层模块就不要发生变化 -因为产品类的实例化工作是由工厂类负责的,`一个产品对象具体由哪一个产品生成是由工厂类决定的` -在数据库开发中,大家应该能够深刻体会到工厂方法模式的好处:如果使用JDBC连接数据库,数据库从MySQL切换到Oracle,需要改动的地方就是切换一下驱动名称(前提条件是SQL语句是标准语句),其他的都不需要修改,这是工厂方法模式灵活性的一个直接案例。 -- 典型的解耦框架 -高层模块值需要知道产品的抽象类,其他的实现类都不用关心 -符合迪米特法则,我不需要的就不要去交流 -也符合依赖倒置原则,只依赖产品类的抽象 -当然也符合里氏替换原则,使用产品子类替换产品父类,没问题! -## 3.2 缺点 -- 类的个数容易过多,增加复杂度 -- 增加了系统的抽象性和理解难度 -## 3.3 适用场景 -- 创建对象需要大量重复的代码 -- 客户端(应用层)不依赖于产品类实例如何被创建、实现等细节 -- 一个类通过其子类来指定创建哪个对象 -- 工厂方法模式是new一个对象的替代品 -在所有需要生成对象的地方都可以使用,但是需要慎重地考虑是否要增加一个工厂类进行管理,增加代码的复杂度。 - -### 需要灵活的、可扩展的框架时 -万物皆对象,那万物也就皆产品类。 -例如需要设计一个连接邮件服务器的框架,有三种网络协议可供选择:POP3、IMAP、HTTP。 -可以把这三种连接方法作为产品类,定义一个接口如`IConnectMail` -然后定义对邮件的操作方法 -用不同的方法实现三个具体的产品类(也就是连接方式) -再定义一个工厂方法,按照不同的传入条件,选择不同的连接方式 -如此设计,可以做到完美的扩展,如某些邮件服务器提供了WebService接口,很好,我们只要增加一个产品类就可以了 -### 异构项目 -例如通过WebService与一个非Java的项目交互,虽然WebService号称是可以做到异构系统的同构化,但是在实际的开发中,还是会碰到很多问题,如类型问题、WSDL文件的支持问题,等等。从WSDL中产生的对象都认为是一个产品,然后由一个具体的工厂类进行管理,减少与外围系统的耦合。 -### 使用在测试驱动开发的框架下 -例如,测试一个类A,就需要把与类A有关联关系的类B也同时产生出来,我们可以使用工厂方法模式把类B虚拟出来,避免类A与类B的耦合。目前由于JMock和EasyMock的诞生,该使用场景已经弱化了,读者可以在遇到此种情况时直接考虑使用JMock或EasyMock -# 4 扩展 -工厂方法模式有很多扩展,而且与其他模式结合使用威力更大,下面将介绍4种扩展。 -## 4.1 缩小为简单工厂模式 -我们这样考虑一个问题:一个模块仅需要一个工厂类,没有必要把它产生出来,使用静态的方法就可以了,根据这一要求,我们把上例中的`AbstarctHumanFactory`修改一下 -![简单工厂模式类图](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtNzJmODZiNWU5OTJjNTIzNy5qcGc?x-oss-process=image/format,png) -我们在类图中去掉了`AbstractHumanFactory`抽象类,同时把`createHuman`方法设置为静态类型,简化了类的创建过程,变更的源码仅仅是HumanFactory和NvWa类 - -- 简单工厂模式中的工厂类 -![待考证](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTRjOTc0MGVjOWJhZWM4N2EucG5n?x-oss-process=image/format,png) - -HumanFactory类仅有两个地方发生变化 -- 去掉继承抽象类 -- 在`createHuman`前增加static关键字 - -工厂类发生变化,也同时引起了调用者NvWa的变化 - ![简单工厂模式中的场景类](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTU3NjVkZGMzNmEwYmRjODQucG5n?x-oss-process=image/format,png) -运行结果没有发生变化,但是我们的类图变简单了,而且调用者也比较简单,该模式是工厂方法模式的弱化,因为简单,所以称为`简单工厂模式`(Simple Factory Pattern),也叫做`静态工厂模式` -在实际项目中,采用该方法的案例还是比较多的 -- 其缺点 -工厂类的扩展比较困难,不符合开闭原则,但它仍然是一个非常实用的设计模式。 -## 4.2 升级为多个工厂类 -当我们在做一个比较复杂的项目时,经常会遇到初始化一个对象很耗费精力的情况,所有的产品类都放到一个工厂方法中进行初始化会使代码结构不清晰 -例如,一个产品类有5个具体实现,每个实现类的初始化(不仅仅是new,初始化包括new一个对象,并对对象设置一定的初始值)方法都不相同,如果写在一个工厂方法中,势必会导致该方法巨大无比,那该怎么办? - -考虑到需要结构清晰,我们就为每个产品定义一个创造者,然后由调用者自己去选择与哪个工厂方法关联 -我们还是以女娲造人为例,每个人种都有一个固定的八卦炉,分别造出黑色人种、白色人种、黄色人种 -![多个工厂类的类图](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtNzlhZjQ3MjM0YWFjMjk5OS5qcGc?x-oss-process=image/format,png) - -- 每个人种(具体的产品类)都对应了一个创建者,每个创建者都独立负责创建对应的产品对象,非常符合单一职责原则,看看代码变化 -![多工厂模式的抽象工厂类](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTAzMDA2Y2VjYTJjOGYyNDIucG5n?x-oss-process=image/format,png) -抽象方法中已经不再需要传递相关参数了,因为每一个具体的工厂都已经非常明确自己的职责:创建自己负责的产品类对象。 - -- 黑色人种的创建工厂实现 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTUwNGQyNGFjYjRlMGIyNGQucG5n?x-oss-process=image/format,png) -- 黄色人种的创建类 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWU1MjNiNTRlMmNiYWUzYTQucG5n?x-oss-process=image/format,png) -- 白色人种的创建类 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWVmNmY2NWVjMDVjMTZjZjcucG5n?x-oss-process=image/format,png) - -三个具体的创建工厂都非常简单,但是,如果一个系统比较复杂时工厂类也会相应地变复杂。 -- 场景类NvWa修改后的代码 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWFkNTIxNTBmZDc5YzUyYzAucG5n?x-oss-process=image/format,png) - -运行结果还是相同 -每一个产品类都对应了一个创建类,好处就是创建类的职责清晰,而且结构简单,但是给可扩展性和可维护性带来了一定的影响。为什么这么说呢?如果要扩展一个产品类,就需要建立一个相应的工厂类,这样就增加了扩展的难度。因为工厂类和产品类的数量相同,维护时需要考虑两个对象之间的关系。 - -当然,在复杂的应用中一般采用多工厂的方法,然后再增加一个协调类,避免调用者与各个子工厂交流,协调类的作用是封装子工厂类,对高层模块提供统一的访问接口。 -## 4.3 替代单例模式 -单例模式的核心要求就是`在内存中只有一个对象`,通过工厂方法模式也能只在内存中生产一个对象。 -- 工厂方法模式替代单例模式类图 -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtNmVlNzUwYmM2MTJiNmQ2Zi5qcGc?x-oss-process=image/format,png) -Singleton定义了一个private的无参构造函数,目的是不允许通过new的方式创建一个对象 -![单例类](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWZjZGJjYjM1YjVlNzgwNWUucG5n?x-oss-process=image/format,png) -Singleton保证不能通过正常的渠道建立一个对象 - -那SingletonFactory如何建立一个单例对象呢? -反射! -- 负责生成单例的工厂类 -![](https://img-blog.csdnimg.cn/095f579f196847c6b9b0d1f3ed098e55.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -通过获得类构造器,然后设置private访问权限,生成一个对象,然后提供外部访问,保证内存中的对象唯一。 - -以上通过工厂方法模式创建了一个单例对象,该框架可以继续扩展,在一个项目中可以产生一个单例构造器,所有需要产生单例的类都遵循一定的规则(构造方法是private),然后通过扩展该框架,只要输入一个类型就可以获得唯一的一个实例。 -## 3.4 延迟初始化(Lazy initialization) -一个对象被消费完毕后,并不立刻释放,工厂类保持其初始状态,等待再次被使用 -延迟初始化是工厂方法模式的一个扩展应用 -- 延迟初始化的通用类图 -![](https://img-blog.csdnimg.cn/e63f7fb0b8ba47a0b91bb947b73c0874.png) - -ProductFactory负责产品类对象的创建工作,并且通过prMap变量产生一个缓存,对需要再次被重用的对象保留 - -- 延迟加载的工厂类 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWZhZWFmM2RmNjc5YTI2YzUucG5n?x-oss-process=image/format,png) -通过定义一个Map容器,容纳所有产生的对象,如果在Map容器中已经有的对象,则直接取出返回;如果没有,则根据需要的类型产生一个对象并放入到Map容器中,以方便下次调用。 - -延迟加载框架是可以扩展的,例如限制某一个产品类的最大实例化数量,可以通过判断Map中已有的对象数量来实现,这样的处理是非常有意义的,例如JDBC连接数据库,都会要求设置一个MaxConnections最大连接数量,该数量就是内存中最大实例化的数量。 - -延迟加载还可以用在对象初始化比较复杂的情况下,例如硬件访问,涉及多方面的交互,则可以通过延迟加载降低对象的产生和销毁带来的复杂性。 \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" deleted file mode 100644 index bfce4f5f76..0000000000 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" +++ /dev/null @@ -1,237 +0,0 @@ -一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。 - -- 意图 -定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 - -- 主要解决 -一些方法通用,却在每一个子类都重新写了这一方法。 - -- 关键代码 -在抽象类实现,其他步骤在子类实现。 - -# 实例 -spring 中对 Hibernate 的支持,将一些已经定好的方法封装起来,比如开启事务、获取 Session、关闭 Session 等,程序员不重复写那些已经规范好的代码,直接丢一个实体就可以保存。 - -# 优点 - 1、封装不变部分,扩展可变部分。 - 2、提取公共代码,便于维护。 - 3、行为由父类控制,子类实现。 - -# 缺点 -每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。 - -# 使用场景 -1、有多个子类共有的方法,且逻辑相同。 2、重要的、复杂的方法,可以考虑作为模板方法。 - -注意事项:为防止恶意操作,一般模板方法都加上 final 关键词。 - -# 实现 -我们将创建一个定义操作的 Game 抽象类,其中,模板方法设置为 final,这样它就不会被重写。Cricket 和 Football 是扩展了 Game 的实体类,它们重写了抽象类的方法。 - -TemplatePatternDemo,我们的演示类使用 Game 来演示模板模式的用法。 - -![](https://img-blog.csdnimg.cn/20200517194325508.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -## 步骤 1 -创建一个抽象类,它的模板方法被设置为 final。 - -```java -Game.java -public abstract class Game { - abstract void initialize(); - abstract void startPlay(); - abstract void endPlay(); - - //模板 - public final void play(){ - - //初始化游戏 - initialize(); - - //开始游戏 - startPlay(); - - //结束游戏 - endPlay(); - } -} -``` - -## 步骤 2 -创建扩展了上述类的实体类。 - -```java -Cricket.java -public class Cricket extends Game { - - @Override - void endPlay() { - System.out.println("Cricket Game Finished!"); - } - - @Override - void initialize() { - System.out.println("Cricket Game Initialized! Start playing."); - } - - @Override - void startPlay() { - System.out.println("Cricket Game Started. Enjoy the game!"); - } -} -Football.java -public class Football extends Game { - - @Override - void endPlay() { - System.out.println("Football Game Finished!"); - } - - @Override - void initialize() { - System.out.println("Football Game Initialized! Start playing."); - } - - @Override - void startPlay() { - System.out.println("Football Game Started. Enjoy the game!"); - } -} -``` - -## 步骤 3 -使用 Game 的模板方法 play() 来演示游戏的定义方式。 - -```java -TemplatePatternDemo.java -public class TemplatePatternDemo { - public static void main(String[] args) { - - Game game = new Cricket(); - game.play(); - System.out.println(); - game = new Football(); - game.play(); - } -} -``` -# 1 定义与类型 -- 模板方法模式(Template Method Pattern) -Define the skeleton of an algorithm in an operation,deferring some steps to subclasses.Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.(定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。) - -- 定义 -定义了一个算法的骨架,并允许子类为一个或多个步骤提供实现 - -模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤 - -行为型。 -![模板方法模式的通用类图](https://img-blog.csdnimg.cn/img_convert/cd217a65bf37b7874a4ae2871b42fd6c.png) -模板方法模式确实非常简单,仅仅使用了Java的继承机制,AbstractClass叫做抽象模板,它的方法分为两类: -- 基本方法 -基本操作,是由子类实现的方法,并且在模板方法被调用 -- 模板方法 -可有一个或几个,一般是一个具体方法,也就是一个框架,实现对基本方法的调度,完成固定的逻辑 - -为了防止恶意操作,一般模板方法都加上final关键字,不允许被重写。 - -在类图中还有一个角色:具体模板。ConcreteClass1和ConcreteClass2属于具体模板,实现父类所定义的一个或多个抽象方法,即父类定义的基本方法在子类中得以实现。 -AbstractClass抽象模板类 -```java -public abstract class AbstractClass { - // 基本方法 - protected abstract void doSomething(); - // 基本方法 - protected abstract void doAnything(); - // 模板方法 - public void templateMethod(){ - /* - * 调用基本方法,完成相关的逻辑 - */ - this.doAnything(); - this.doSomething(); - } -} -``` - -具体模板类 -```java -public class ConcreteClass1 extends AbstractClass { - // 实现基本方法 - protected void doAnything() { - // 业务逻辑 - } - protected void doSomething() { - // 业务逻辑处理 - } -} -public class ConcreteClass2 extends AbstractClass { - // 实现基本方法 - protected void doAnything() { - // 业务逻辑处理 - } - protected void doSomething() { - // 业务逻辑处理 - } -} -``` - -场景类 -```java -public class Client { - public static void main(String[] args) { - AbstractClass class1 = new ConcreteClass1(); - AbstractClass class2 = new ConcreteClass2(); - //调用模板方法 - class1.templateMethod(); - class2.templateMethod(); - } -} -``` -抽象模板中的基本方法尽量设计为protected类型,符合迪米特法则,不需要暴露的属性或方法尽量不要设置为protected类型。实现类若非必要,尽量不要扩大父类中的访问权限。 -# 2 适用场景 -- 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现 -- 各子类中公共的行为被提取出来并集中到一个公共父类中,从而避免代码重复 - -- 多个子类有公有的方法,并且逻辑基本相同时 -- 重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现 -- 重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通过钩子函数(见“模板方法模式的扩展”)约束其行为。 -# 3 优点 -- 提高复用性 -- 提高扩展性 -- 符合开闭原则 -- 封装不变部分,扩展可变部分 -把认为是不变部分的算法封装到父类实现,而可变部分的则可以通过继承来继续扩展 -- 提取公共部分代码,便于维护 -如果我们不抽取到父类中,任由这种散乱的代码发生,想想后果是什么样子?维护人员为了修正一个缺陷,需要到处查找类似的代码 -- 行为由父类控制,子类实现 -基本方法由子类实现,因此子类可以通过扩展的方式增加相应的功能,符合开闭原则 -# 4 缺点 -- 类数目增加 -- 增加了系统实现的复杂度 -- 继承关系自身缺点,如果父类添加新的抽象方法,所有子类都要改一遍 - -抽象类负责声明最抽象、最一般的事物属性和方法,实现类完成具体的事物属性和方法 -但是模板方法模式却颠倒了,抽象类定义了部分抽象方法,由子类实现,子类执行的结果影响了父类的结果,也就是子类对父类产生了影响,这在复杂的项目中,会带来代码阅读的难度,而且也会让新手产生不适感。 -# 相关设计模式 -模板方法模式和工厂方法模式 -模板方法模式和策略模式 - -# 案例 -![](https://img-blog.csdnimg.cn/img_convert/e11e9b9b1c4545e739c6f013a0187f13.png) -![](https://img-blog.csdnimg.cn/img_convert/211d1668926ee62aecc8d225c51dbf19.png) -![](https://img-blog.csdnimg.cn/img_convert/0b4933194b0d54bf08ed4172d5ca0d18.png) -![](https://img-blog.csdnimg.cn/img_convert/d5a0c37391d58ef0e1f746e6652e5f5a.png) - -# 最佳实践 -初级程序员在写程序的时候经常会问高手“父类怎么调用子类的方法”。这个问题很有普遍性,反正我是被问过好几回,那么父类是否可以调用子类的方法呢?我的回答是能,但强烈地、极度地不建议这么做,那该怎么做呢? - -● 把子类传递到父类的有参构造中,然后调用 -● 使用反射的方式调用 -● 父类调用子类的静态方法 - -这三种都是父类直接调用子类的方法,好用不?好用!解决问题了吗?解决了!项目中允许使用不?不允许! -我就一直没有搞懂为什么要用父类调用子类的方法。如果一定要调用子类,那为什么要继承它呢?搞不懂。其实这个问题可以换个角度去理解,父类建立框架,子类在重写了父类部分的方法后,再调用从父类继承的方法,产生不同的结果(而这正是模板方法模式)。这是不是也可以理解为父类调用了子类的方法呢?你修改了子类,影响了父类行为的结果,曲线救国的方式实现了父类依赖子类的场景,模板方法模式就是这种效果。 - -模板方法在一些开源框架中应用非常多,它提供了一个抽象类,然后开源框架写了一堆子类。 -如果你需要扩展功能,可以继承这个抽象类,然后重写protected方法,再然后就是调用一个类似execute方法,就完成你的扩展开发,非常容易扩展的一种模式。 -# 应用 -## AbstractList diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\255\226\347\225\245\346\250\241\345\274\217(Strategy-Pattern).md" "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\255\226\347\225\245\346\250\241\345\274\217(Strategy-Pattern).md" deleted file mode 100644 index ef6bcb6e23..0000000000 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\255\226\347\225\245\346\250\241\345\274\217(Strategy-Pattern).md" +++ /dev/null @@ -1,106 +0,0 @@ -[相关源码](https://github.com/Wasabi1234/Java-DesignPatterns-Tuitorial) -# 1 简介 -## 1.1 定义 -也叫做政策模式(Policy Pattern) -- wiki -对象有某个行为,但是在不同的场景中,该行为有不同的实现算法.。比如每个人都要“交个人所得税”,但是“在美国交个人所得税”和“在中国交个人所得税”就有不同的算税方法. -- 定义 -Define a family of algorithms,encapsulate each one,and make them interchangeable. -定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。 - -常见 if/else 结构。 - -## 1.2 类型 -行为型。 -在`运行时`(**非编译时**)改变软件的算法行为。 - -## 1.3 主要思想 -定义一个通用的问题,使用不同的算法来实现,然后将这些算法都封装在一个统一接口。 - -策略模式使用的就是OOP的继承和多态。 - -## 1.4 主要角色 -### 通用类图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWFkMWNhZjE4NDMyNGRlY2YucG5n?x-oss-process=image/format,png) - -- Context 封装角色 -即上下文角色,起承上启下的封装作用。屏蔽高层模块对策略&算法的直接访问,封装可能存在的变化。 - -- Strategy 抽象策略角色 -策略&算法家族的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性。 - -- ConcreteStrategy 具体策略角色 -实现抽象策略中的操作,含有具体的算法。 - -### 通用源码 -- 抽象策略角色 -一个非常普通的接口,在项目中就是一个普通接口,定义一或多个具体算法。 -# 2 适用场景 -一个对象,其行为有些固定不变,有些又容易变化。对于这些容易变化的行为,我们不希望将其实现绑定在对象中,而希望能够动态地针对不同场景产生不同应对的策略。 -这时就要用到策略模式,就是为了应对对象中复杂多变的行为而产生的: -- 系统有很多类,而他们的区别仅在于行为不同 -- 一个系统需要动态地在几种算法中选择一种 -# 3 优点 -- 符合开闭原则 -- 避免使用多重条件转移语句 -e.g. 省去大量 if/else、switch,降低代码耦合度 -- 提高算法的保密性和安全性 -只需知道策略的业务功能,而不关心内部实现 -# 4 缺点 -- 客户端必须知道所有的策略类,并决定使用哪个策略类 -- 产生很多策略类 - -# 5 相关设计模式的差异 -## 5.1 V.S 工厂模式 -- 行为型 -接收已经创建好的对象,从而实现不同的行为 -- 创造型 -接收指令,创建符合要求的具体对象 - -## 5.2 V.S 状态模式 -- 若系统中某类的某行为存在多种实现方式,客户端需知道到底使用哪个策略 -- 若系统中某对象存在多种状态,不同状态下的行为又具有差异,状态之间会自动转换,客户端不需要关心具体状态 -## 5.3 V.S 模板模式 -- 策略模式:只有选择权(由用户自己选择已有的固定算法) -- 模板模式,侧重点不是选择,你没得选择,你必须这么做。你可以参与某一部分内容自定义 -# 6 实战 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTBlYmMwOGY0MWUwN2NkY2EucG5n?x-oss-process=image/format,png) -- 促销策略接口 -![](https://img-blog.csdnimg.cn/20201104133917501.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -- 返现策略 -![](https://img-blog.csdnimg.cn/20201104134155926.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -- 立减策略 -![](https://img-blog.csdnimg.cn/2020110413472547.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -- 满减策略 -![](https://img-blog.csdnimg.cn/20201104135011162.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -- 测试类 -![](https://img-blog.csdnimg.cn/20201104135935601.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTg0NDA3NWYwMWE5ZTM0OWIucG5n?x-oss-process=image/format,png) -改造后的测试类 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTQ5OTFkMmVhYWQ5MzU3YzEucG5n?x-oss-process=image/format,png) -可见 if/else 语句过多,采取策略+工厂模式结合 -- 策略工厂 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTIzMDA4OGNhMjYwZGIyNTYucG5n?x-oss-process=image/format,png) -- 最新测试类 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWFjZjgwZGE0ZmE1ZWE5NTQucG5n?x-oss-process=image/format,png) -- 输出结果 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTdkMjYwMzNhMWIzOWJkNmEucG5n?x-oss-process=image/format,png) - -# 7 源码应用解析 -## JDK中的比较器接口 -- 策略比较器 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTMwNzY2Njg5NmMzZDE4MDAucG5n?x-oss-process=image/format,png) -![具体策略](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQ5MjhkZDE2YmVhNDRhNjAucG5n?x-oss-process=image/format,png) -比如Arrays类中的 sort 方法通过传入不同比较接口器的实现达到不同排序策略 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWY5MjA3MzcxMmUzMGNlNjYucG5n?x-oss-process=image/format,png) -## JDK中的TreeMap -类似于促销活动中有促销策略对象,在T reeMap 中也有比较器对象 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTQyNGY3ODdkYTE3ZDQ4NzYucG5n?x-oss-process=image/format,png) -compare 方法进步加工 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTMyZTAyNDU2NTQyYzFlNDgucG5n?x-oss-process=image/format,png) -## Spring 中的Resource -不同访问策略 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTY2ZDYxOTExNzdmYWFmMmEucG5n?x-oss-process=image/format,png) -## Spring 中bean 的初始化ceInstantiationStrategy -- 两种 bean 的初始化策略 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LThmYTVlNDRlNDkxYWFmZGMucG5n?x-oss-process=image/format,png) \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\243\205\351\245\260\345\231\250\346\250\241\345\274\217(Decorator Pattern).md" "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\243\205\351\245\260\345\231\250\346\250\241\345\274\217(Decorator Pattern).md" deleted file mode 100644 index 1c671ad61e..0000000000 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\243\205\351\245\260\345\231\250\346\250\241\345\274\217(Decorator Pattern).md" +++ /dev/null @@ -1,157 +0,0 @@ -# 导读 -一般有两种方式可以给一个类或对象新增行为: -- 继承 -子类在拥有自身方法同时还拥有父类方法。但这种方法是静态的,用户无法控制增加行为的方式和时机。 -- 关联 -将一个类的对象嵌入另一个对象,由另一个对象决定是否调用嵌入对象的行为以便扩展自身行为,这个嵌入的对象就叫做装饰器(Decorator)。 - -# 定义 -对象结构型模式。 - -动态地给一个对象增加额外功能,装饰器模式比生成子类实现更为灵活。 -装饰模式以对用户透明的方式**动态**给一个对象附加功能。用户不会觉得对象在装饰前、后有何不同。装饰模式可在无需创造更多子类情况下,扩展对象的功能。 - -# 角色 -- Component 接口: 抽象构件 -定义了对象的接口,可以给这些对象动态增加功能 - -- ConcreteComponent 具体类: 具体构件 -定义了具体的构件对象,实现了 在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法) - -- Decorator 抽象类: 装饰类 -抽象装饰类是抽象构件类的子类,用于给具体构件增加职责,但是具 体职责在其子类中实现; - -- ConcreteDecorator 具体类: 具体装饰类 -具体装饰类是抽象装饰类的子类,负责向构 件添加新的职责。 -![](https://img-blog.csdnimg.cn/20210531150553798.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - -# 代码实例 -窗口 接口 -```java -public interface Window { - // 绘制窗口 - public void draw(); - // 返回窗口的描述 - public String getDescription(); -} -``` -无滚动条功能的简单窗口实现 -```java -public class SimpleWindow implements Window { - public void draw() { - // 绘制窗口 - } - - public String getDescription() { - return "simple window"; - } -} -``` - -以下类包含所有Window类的decorator,以及修饰类本身。 -```java -// 抽象装饰类 注意实现Window接口 -public abstract class WindowDecorator implements Window { - // 被装饰的Window - protected Window decoratedWindow; - - public WindowDecorator (Window decoratedWindow) { - this.decoratedWindow = decoratedWindow; - } - - @Override - public void draw() { - decoratedWindow.draw(); - } - - @Override - public String getDescription() { - return decoratedWindow.getDescription(); - } -} - - -// 第一个具体装饰器 添加垂直滚动条功能 -public class VerticalScrollBar extends WindowDecorator { - public VerticalScrollBar(Window windowToBeDecorated) { - super(windowToBeDecorated); - } - - @Override - public void draw() { - super.draw(); - drawVerticalScrollBar(); - } - - private void drawVerticalScrollBar() { - // Draw the vertical scrollbar - } - - @Override - public String getDescription() { - return super.getDescription() + ", including vertical scrollbars"; - } -} - - -// 第二个具体装饰器 添加水平滚动条功能 -public class HorizontalScrollBar extends WindowDecorator { - public HorizontalScrollBar (Window windowToBeDecorated) { - super(windowToBeDecorated); - } - - @Override - public void draw() { - super.draw(); - drawHorizontalScrollBar(); - } - - private void drawHorizontalScrollBar() { - // Draw the horizontal scrollbar - } - - @Override - public String getDescription() { - return super.getDescription() + ", including horizontal scrollbars"; - } -} -``` - -# 优点 -使用装饰模式来实现扩展比继承更加灵活,它以对客户透明的方式动态地给一个对象附加更多的责任。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。 - -与继承相比,关联关系的优势在于不破坏类的封装性,而且继承是一种耦合度较大的静态关系,无法在程序运行时动态扩展。 -可通过动态方式扩展一个对象的功能,通过配置文件可以在运行时选择不同装饰器,从而实现不同行为。 - -在软件开发阶段,关联关系虽然不会比继承关系减少编码量,但到了软件维护阶段,由于关联关系使系统具有较好的松耦合性,所以更容易维护。 - -通过使用不同具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更强大的对象。 - -具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类、具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”。 -# 缺点 -产生很多小对象,这些对象区别在于它们之间相互连接的方式不同,而不是它们的类或属性值不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。 - -比继承更灵活,也意味着比继承更易出错,排查也更困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。 - -# 适用场景 -在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。 -需要动态地给一个对象增加功能,这些功能也可以动态地被撤销。 -当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。 - -不能采用继承的场景: -- 系统存在大量独立扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长 -- 类定义不能继承(如final类) - -# 扩展 -一个装饰类的接口必须与被装饰类的接口保持相同,对于客户端来说无论是装饰之前的对象还是装饰之后的对象都可以一致对待。 -尽量保持具体构件类的轻量,也就是说不要把太多的逻辑和状态放在具体构件类中,可以通过装饰类对其进行扩展。 - -装饰模式可分为: -- 透明装饰模式 -要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该声明具体构件类型和具体装饰类型,而应该全部声明为抽象构件类型 -- 半透明装饰模式 -允许用户在客户端声明具体装饰者类型的对象,调用在具体装饰者中新增的方法。 - -> 参考 -> - https://zh.wikipedia.org/wiki/%E4%BF%AE%E9%A5%B0%E6%A8%A1%E5%BC%8F#/ \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" deleted file mode 100644 index a14806d986..0000000000 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" +++ /dev/null @@ -1,39 +0,0 @@ -# 1 简介 -- 定义 -定义了对象之间的一对多依赖,让多个观察者对象同时监听某一个主题对象,当主题对象发生变化时,它的所有依赖者(观察者)都会收到通知并更新 - -- 类型 -行为型 -# 2 适用场景 -关联行为场景,建立一套触发机制 -# 3 优点 -- 观察者和被观察者之间建立一个抽象的耦合 -- 观察者模式支持广播通信 -# 4 缺点 -- 观察者之间有过多的细节依赖、提高时间消耗及程序复杂度 -- 使用要得当,要避免循环调用 -# 5 实例 -![](https://img-blog.csdnimg.cn/img_convert/e2776da5ea2bbe93bc3dba9f55317885.png) -![](https://img-blog.csdnimg.cn/img_convert/365560838866815621e5fba325024889.png) -![](https://img-blog.csdnimg.cn/img_convert/d81962748cbf8ffd7cbfbfc12c38003f.png) -![](https://img-blog.csdnimg.cn/img_convert/213d4cf8c5e9f6d539e703321c6ec3cf.png) -![](https://img-blog.csdnimg.cn/img_convert/2c2cbadeaf4306466d809255f7ebb717.png) -![](https://img-blog.csdnimg.cn/img_convert/add335433df999a5f805ea925a39a861.png) -![](https://img-blog.csdnimg.cn/img_convert/e099de4d4c019d1cf0131b39b4b48e63.png) -![](https://img-blog.csdnimg.cn/img_convert/c61ad7bf67bfb8ec91b2398025df2fd8.png) -![](https://img-blog.csdnimg.cn/img_convert/092214dcd7fe0e2f02a9d8bc389cb120.png) -![](https://img-blog.csdnimg.cn/img_convert/fa0fccf5f5d01d82f1651ec12ac01789.png) -接下来,来到观察者- `Teacher`的代码区中 -![](https://img-blog.csdnimg.cn/img_convert/0d70288282b6fb5a7070f569d882d4fe.png) -# 6 源码应用 -- JDK应用 -![](https://img-blog.csdnimg.cn/img_convert/95c6b248dd6d13bc8d32bb6b7662d401.png) -## Guava -![](https://img-blog.csdnimg.cn/img_convert/23ae47d1f0ddf8a9fbe9164e3b48afbe.png) -![](https://img-blog.csdnimg.cn/img_convert/678b3f1db4fbbafb150276461f67a53f.png) -- 注册,即添加观察者 -![](https://img-blog.csdnimg.cn/img_convert/c6f705554d52f53dd1c4f9a113726229.png) -![](https://img-blog.csdnimg.cn/img_convert/b641ddc5914ae26315bc55beedd97563.png) -![](https://img-blog.csdnimg.cn/img_convert/51752f3497f7bcd921c81d6939982cad.png) -- 移除观察者![](https://img-blog.csdnimg.cn/img_convert/58020c5aeccbfa607983c2622eccf600.png) -![](https://img-blog.csdnimg.cn/img_convert/54c962042877bd9cc2f0f9f748cd3847.png) \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\256\277\351\227\256\350\200\205\350\256\276\350\256\241\346\250\241\345\274\217(Visitor).md" "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\256\277\351\227\256\350\200\205\350\256\276\350\256\241\346\250\241\345\274\217(Visitor).md" deleted file mode 100644 index 79a79b824c..0000000000 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\256\277\351\227\256\350\200\205\350\256\276\350\256\241\346\250\241\345\274\217(Visitor).md" +++ /dev/null @@ -1,150 +0,0 @@ -# 1 简介 -## 1.1 定义 -封装某些作用于某种数据结构中各元素的操作,它可以在不改变数据结构的前提下定义作用于这些数据元素的新的操作 - -## 思想 -将数据结构和数据操作分离 - -## 目的 -稳定的数据结构和易变的操作的解耦 - -## 适用场景 -假如一个对象中存在着一些与本对象不相干(或者关系较弱)的操作,可以使用访问者模式把这些操作封装到访问者中去,这样便避免了这些不相干的操作污染这个对象。 - -假如一组对象中,存在着相似的操作,可以将这些相似的操作封装到访问者中去,这样便避免了出现大量重复的代码 - -访问者模式适用于对功能已经确定的项目进行重构的时候适用,因为功能已经确定,元素类的数据结构也基本不会变了;如果是一个新的正在开发中的项目,在访问者模式中,每一个元素类都有它对应的处理方法,每增加一个元素类都需要修改访问者类,修改起来相当麻烦。 - -# 2 示例 -如果老师教学反馈得分大于等于85分、学生成绩大于等于90分,则可以入选成绩优秀奖;如果老师论文数目大于8、学生论文数目大于2,则可以入选科研优秀奖。 - -在这个例子中,老师和学生就是Element,他们的数据结构稳定不变。从上面的描述中,我们发现,对数据结构的操作是多变的,一会儿评选成绩,一会儿评选科研,这样就适合使用访问者模式来分离数据结构和操作。 - -## 2.1 创建抽象元素 - -```java -public interface Element { - void accept(Visitor visitor); -} -``` - -## 2.2 创建具体元素 -创建两个具体元素 Student 和 Teacher,分别实现 Element 接口 - -```java -public class Student implements Element { - private String name; - private int grade; - private int paperCount; - - public Student(String name, int grade, int paperCount) { - this.name = name; - this.grade = grade; - this.paperCount = paperCount; - } - - @Override - public void accept(Visitor visitor) { - visitor.visit(this); - } - - ...... - -} -public class Teacher implements Element { - private String name; - private int score; - private int paperCount; - - public Teacher(String name, int score, int paperCount) { - this.name = name; - this.score = score; - this.paperCount = paperCount; - } - - @Override - public void accept(Visitor visitor) { - visitor.visit(this); - } - - ...... - -} -``` - -## 2.3 创建抽象访问者 - -```java -public interface Visitor { - - void visit(Student student); - - void visit(Teacher teacher); -} -``` - -## 2.4 创建具体访问者 -创建一个根据分数评比的具体访问者 GradeSelection,实现 Visitor 接口 - -```java -public class GradeSelection implements Visitor { - - @Override - public void visit(Student student) { - if (student != null && student.getGrade() >= 90) { - System.out.println(student.getName() + "的分数是" + student.getGrade() + ",荣获了成绩优秀奖。"); - } - } - - @Override - public void visit(Teacher teacher) { - if (teacher != null && teacher.getScore() >= 85) { - System.out.println(teacher.getName() + "的分数是" + teacher.getScore() + ",荣获了成绩优秀奖。"); - } - } -} -``` - -## 2.5 调用 - -```java -public class VisitorClient { - - public static void main(String[] args) { - // 抽象元素 => 具体元素 - Element element = new Student("lijiankun24", 90, 3); - // 抽象访问者 => 具体访问者 - Visitor visitor = new GradeSelection(); - // 具体元素 接收 具体访问者的访问 - element.accept(visitor); - } -} -``` - -上述代码即是一个简单的访问者模式的示例代码,输出如下所示: - - -上述代码可以分为三步: -1. 创建一个元素类的对象 -2. 创建一个访问类的对象 -3. 元素对象通过 Element#accept(Visitor visitor) 方法传入访问者对象 - -# 3 ASM 中的访问者模式 -ASM 库就是 Visitor 模式的典型应用。 - -## 3.1 ASM 中几个重要的类 -- ClassReader -将字节数组或者 class 文件读入到内存当中,并以树的数据结构表示,树中的一个节点代表着 class 文件中的某个区域 -可以将 ClassReader 看作是 Visitor 模式中的访问者的实现类 - -- ClassVisitor(抽象类) -ClassReader 对象创建之后,调用 ClassReader#accept() 方法,传入一个 ClassVisitor 对象。在 ClassReader 中遍历树结构的不同节点时会调用 ClassVisitor 对象中不同的 visit() 方法,从而实现对字节码的修改。在 ClassVisitor 中的一些访问会产生子过程,比如 visitMethod 会产生 MethodVisitor 的调用,visitField 会产生对 FieldVisitor 的调用,用户也可以对这些 Visitor 进行自己的实现,从而达到对这些子节点的字节码的访问和修改。 -在 ASM 的访问者模式中,用户还可以提供多种不同操作的 ClassVisitor 的实现,并以责任链的模式提供给 ClassReader 来使用,而 ClassReader 只需要 accept 责任链中的头节点处的 ClassVisitor。 -- ClassWriter -ClassVisitor 的实现类,它是生成字节码的工具类,它一般是责任链中的最后一个节点,其之前的每一个 ClassVisitor 都是致力于对原始字节码做修改,而 ClassWriter 的操作则是老实得把每一个节点修改后的字节码输出为字节数组。 - -## 3.2 ASM 的工作流程 -1. ClassReader 读取字节码到内存中,生成用于表示该字节码的内部表示的树,ClassReader 对应于访问者模式中的元素 -2. 组装 ClassVisitor 责任链,这一系列 ClassVisitor 完成了对字节码一系列不同的字节码修改工作,对应于访问者模式中的访问者 Visitor -3. 然后调用 ClassReader#accept() 方法,传入 ClassVisitor 对象,此 ClassVisitor 是责任链的头结点,经过责任链中每一个 ClassVisitor 的对已加载进内存的字节码的树结构上的每个节点的访问和修改 -4. 最后,在责任链的末端,调用 ClassWriter 这个 visitor 进行修改后的字节码的输出工作 \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md" "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md" deleted file mode 100644 index 97b9bec16e..0000000000 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md" +++ /dev/null @@ -1,46 +0,0 @@ -# 1 导读 -## 1.1 定义 -它包含了一些命令对象和一系列处理对象。 -每个处理对象决定它能处理哪些命令对象,它也知道如何将它不能处理的命令对象传递给该链中的下一个处理对象。 - -该模式还描述了往该处理链的末尾添加新的处理对象的方法。 - -- 精简定义 -为请求创建一个接收此次请求对象的链。 - -## 1.2 类型 -行为型 -# 2 适用场景 -一个请求的处理需要多个对象当中的一或几个协作处理 -# 3 优点 -请求的发送者和接收者(请求的处理)解耦,责任链可以动态组合。 -# 4 缺点 -- 责任链太长或者处理时间过长,影响性能 -- 责任链有可能过多 - -# 5 相关设计模式 -## V.S 状态模式 -- 各个对象并不指定下一个所要处理的对象者是谁,只有在客户端类设置链顺序及元素,直到被某个责任链处理或整条链结束 -- 每个状态知道自己下一个所要处理的对象者是谁,即编译时确定 - -# 6 实战 -![](https://img-blog.csdnimg.cn/20210717133837595.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -![](https://img-blog.csdnimg.cn/20210716225806274.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20210717140319675.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20210716230056567.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- UML -![](https://img-blog.csdnimg.cn/2021071714044678.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - - -- 测试类 -![](https://img-blog.csdnimg.cn/20210717134235324.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20210717134302416.png) -- 将博客注释掉 -![](https://img-blog.csdnimg.cn/20210717134415249.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20210717134530753.png) -# 框架应用 -## Tomcat#FilterChain -FilterChain 是一个由 Servlet 容器提供给开发人员的对象,它提供了一个对资源的过滤请求的调用链的视图。 过滤器使用 FilterChain 调用链中的下一个过滤器,或者如果调用过滤器是链中的最后一个过滤器,则调用链末尾的资源 -![](https://img-blog.csdnimg.cn/20210717140946145.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/img_convert/92c634a095e759771415bfb4d4990b03.png) \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(1)-\345\215\225\344\270\200\350\201\214\350\264\243\345\216\237\345\210\231(Single Responsibility Principle,SRP).md" "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(1)-\345\215\225\344\270\200\350\201\214\350\264\243\345\216\237\345\210\231(Single Responsibility Principle,SRP).md" deleted file mode 100644 index c2b13dbf16..0000000000 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(1)-\345\215\225\344\270\200\350\201\214\350\264\243\345\216\237\345\210\231(Single Responsibility Principle,SRP).md" +++ /dev/null @@ -1,261 +0,0 @@ -# 1 简介 -- 定义 -不要存在多于一个导致类变更的原因。 - -- 特点 -一个类/接口/方法只负责一项职责。 - -- 优点 -降低类的复杂度、提高类的可读性,提高系统的可维护性、降低变更引起的风险。 - -名字容易让人望文生义,大部分人可能理解成:一个类只干一件事,看起来似乎很合理呀。几乎所有程序员都知道“高内聚、低耦合”,应该把相关代码放一起。 - -若随便拿个模块去问作者,这个模块是不是只做了一件事,他们异口同声:对,只做了一件事。看来,这个原则很通用啊,所有人都懂,为啥还要有这样一个设计原则? - -因为一开始的理解就是错的!错在把单一职责理解成有关如何组合的原则,实际上,它是关于如何分解的。 - -Robert Martin对单一职责的定义的变化: -- 《敏捷软件开发:原则、实践与模式》 -一个模块应该有且仅有一个变化的原因 -- 《架构整洁之道》 -一个模块应该对一类且仅对一类行为者(actor)负责 - -- 单一职责原则 V.S 一个类只干一件事 -最大的差别就是,将变化纳入考量。 - -分析第一个定义:一个模块应该有且仅有一个变化的原因。 -软件设计关注长期变化,拥抱变化,我们最不愿意面对却不得不面对,只因变化会产生不确定性,可能: -- 新业务的稳定问题 -- 旧业务遭到损害而带来的问题 - -所以,一个模块最理想的状态是不改变,其次是少改变,它可成为一个模块设计好坏的衡量标准。 - -但实际开发中,一个模块频繁变化,在于能诱导它改变的原因太多! -# 2 案例 -## 2.1 鸟类案例 -- 最开始的 Bird 类 -![](https://img-blog.csdnimg.cn/20201011052517957.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -- 简单测试类 -![](https://img-blog.csdnimg.cn/img_convert/7ea1da00d7a9e0615db6170063d5d468.png) - -显然鸵鸟还用翅膀飞是错误的!于是,我们修改类实现 -![](https://img-blog.csdnimg.cn/2020101105314531.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -这种设计依旧很low,总不能一味堆砌 if/else 添加鸟类。结合该业务逻辑,考虑分别实现类职责,即根据单一原则创建两种鸟类即可: -![](https://img-blog.csdnimg.cn/20201011053445284.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -![](https://img-blog.csdnimg.cn/20201011053528342.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -![](https://img-blog.csdnimg.cn/20201011053706470.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -## 2.2 课程案例 -- 最初的课程接口有两个职责,耦合过大 -![](https://img-blog.csdnimg.cn/20201011054054803.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -- 按职责拆分 -![](https://img-blog.csdnimg.cn/20201011063546976.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -![](https://img-blog.csdnimg.cn/20201011063629925.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) - -![](https://img-blog.csdnimg.cn/img_convert/82c766b21a88c40cdc3a1946c5cf51c4.png) - -## 2.3 用户管理 -用户、机构、角色管理这些模块,基本上使用的都是RBAC模型(Role-Based Access Control,基于角色的访问控制,通过分配和取消角色来完成用户权限的授予和取消,使动作主体(用户)与资源的行为(权限)分离),这确实是一个很好的解决办法。 - -对于用户管理、修改用户的信息、增加机构(一个人属于多个机构)、增加角色等,用户有这么多的信息和行为要维护,我们就把这些写到一个接口中,都是用户管理类: -- 用户信息维护类图 -![](https://img-blog.csdnimg.cn/20210705144728253.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -很有问题,用户属性和用户行为严重耦合!这个接口确实设计得一团糟: -- 应该把用户信息抽取成一个BO(Business Object,业务对象) -- 把行为抽取成一个Biz(Business Logic,业务逻辑) - -- 职责划分后的类图 -![](https://img-blog.csdnimg.cn/20210705145341688.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -重新拆分成两个接口: -- IUserBO -负责用户的属性,职责就是收集和反馈用户的属性信息 -- IUserBiz -负责用户的行为,完成用户信息的维护和变更 - -我们是面向接口编程,所以产生了这个UserInfo对象后,当然可以把它当IUserBO接口使用,也可以当IUserBiz接口使用,这看你的使用场景。 -- 要获得用户信息,就当做IUserBO的实现类 -- 要是希望维护用户的信息,就把它当做IUserBiz的实现类 -```java -IUserInfo userInfo = new UserInfo(); -// 我要赋值了,我就认为它是一个纯粹的BO -IUserBO userBO = (IUserBO)userInfo; -userBO.setPassword("abc"); -// 我要执行动作了,我就认为是一个业务逻辑类 -IUserBiz userBiz = (IUserBiz)userInfo; -userBiz.deleteUser(); -``` - -确实这样拆分后,问题就解决了,分析一下我们的做法,为什么要把一个接口拆分成两个? -实际的使用中,更倾向于使用两个不同的类或接口:一个是IUserBO,一个是IUserBiz -- 项目中经常采用的SRP类图 -![](https://img-blog.csdnimg.cn/img_convert/ddf683f0892e2a6a08726c7642b9dd25.png) - -以上我们把一个接口拆分成两个接口的动作,就是依赖了单一职责原则,那什么是单一职责原则呢?单一职责原则的定义是:应该有且仅有一个原因引起类的变更。 - -## 2.4 电话通话 -电话通话的时候有4个过程发生:拨号、通话、回应、挂机。 -那我们写一个接口 -![电话类图](https://img-blog.csdnimg.cn/img_convert/a7fab40bd3a15f919374e59ecb03b59a.png) -- 电话过程 -![](https://img-blog.csdnimg.cn/20210705150128534.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -这个接口有问题吗?是的,这个接口接近于完美。 -单一职责原则要求一个接口或类只有一个原因引起变化,即一个接口或类只有一个职责,它就负责一件事情,看看上面的接口: -- 只负责一件事情吗? -- 是只有一个原因引起变化吗? - -好像不是!IPhone这个接口可不是只有一个职责,它包含了两个职责: -- 协议管理 -dial()和hangup()两个方法实现的是协议管理,分别负责拨号接通和挂机 -- 数据传送 -chat()实现的是数据的传送,把我们说的话转换成模拟信号或数字信号传递到对方,然后再把对方传递过来的信号还原成我们听得懂的语言。 - -协议管理的变化会引起这个接口或实现类的变化吗? -会的! -那数据传送(电话不仅可以通话,还可以上网!)的变化会引起这个接口或实现类的变化吗? -会的! - -这里有两个原因都引起了类变化。这两个职责会相互影响吗? -- 电话拨号,我只要能接通就成,不管是电信的还是联通的协议 -- 电话连接后还关心传递的是什么数据吗? - -经过分析,我们发现类图上的IPhone接口包含了两个职责,而且这两个职责的变化不相互影响,那就考虑拆分成两个接口: -- 职责分明的电话类图 -![](https://img-blog.csdnimg.cn/img_convert/a32b12f5a750fccb83178a41a30131d5.png) -完全满足了单一职责原则要求,每个接口职责分明,结构清晰,但相信你在设计时肯定不会采用这种方式。一个 Phone类要把ConnectionManager和DataTransfer组合在一块才能使用。组合是一种强耦合关系,共同生命周期,这样强耦合不如使用接口实现,而且还增加了类的复杂性,多了俩类。 - -那就再修改一下类图: -- 简洁清晰、职责分明的电话类图 -![](https://img-blog.csdnimg.cn/img_convert/6440c34103fafd869b635156ddff80cf.png) - -一个类实现了两个接口,把两个职责融合在一个类中。 -你可能会说Phone有两个原因引起变化了呀! -是的,但是别忘了我们是面向接口编程,我们对外公布的是接口而非实现类。而且,若真要实现类的单一职责,还就必须使用组合模式了,这会引起类间耦合过重、类的数量增加等问题,人为地增加了设计复杂性。 - -## 好处 -- 类的复杂性降低,实现什么职责都有清晰明确的定义 -- 可读性提高,复杂性降低,那当然可读性提高了 -- 可维护性提高,可读性提高,那当然更容易维护了 -- 变更引起的风险降低,变更是必不可少的。若接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响。这对系统的扩展性、维护性都有非常大帮助。 - -单一职责原则最难划分的就是职责。 -一个职责一个接口,但问题是“职责”没有一个量化的标准,一个类到底要负责那些职责?这些职责该怎么细化?细化后是否都要有一个接口或类? -这些都需要从实际的项目去考虑,从功能上来说,定义一个IPhone接口也没有错,实现了电话的功能,而且设计还很简单,仅仅一个接口一个实现类,实际的项目我想大家都会这么设计。项目要考虑可变因素和不可变因素,以及相关的收益成本比率,因此设计一个IPhone接口也可能是没有错的。 - -但若纯从“学究”理论上分析就有问题了,有两个可以变化的原因放到了一个接口中,这就为以后的变化带来了风险。如果以后模拟电话升级到数字电话,我们提供的接口IPhone是不是要修改了?接口修改对其他的Invoker类是不是有很大影响? - -单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。 - -## 2.5 项目管理 -开发一个项目管理工具,可能设计如下用户类: -![](https://img-blog.csdnimg.cn/ac02aaf76ba14b9ab65300e770cd750c.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_15,color_FFFFFF,t_70,g_se,x_16)类设计得看着很合理,有用户信息管理、项目管理等。 -现在新需求规定每个用户能够设置电话号码,于是你新增方法: -![](https://img-blog.csdnimg.cn/450501655028452b856e417b817f6657.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_19,color_FFFFFF,t_70,g_se,x_16) -又来新需求:查看一个用户加入了多少项目: -![](https://img-blog.csdnimg.cn/20a70e69316e47d7a8a671f8554b1524.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_19,color_FFFFFF,t_70,g_se,x_16) -就这样,几乎每个和用户沾边的需求,你都改了user类,导致: -- User类不断膨胀 -- 内部实现越来越复杂 - -这个类变动的频繁程度显然不理想,在于它诱导变动的需求太多: -- 为什么要增加电话号码? -用户管理的需求。用户管理的需求还会有很多,比如,用户实名认证、用户组织归属等 -- 为什么要查看用户加入多少项目? -项目管理的需求。项目管理的需求还会有很多,比如,团队管理、项目权限等。 - -这是两种完全不同的需求,但你都改同一个类,所以,User类无法稳定。 -最好的方案是拆分不同需求引起的变动。 -对于用户管理、项目管理两种不同需求,完全可以把User拆成两个类: -- 用户管理类需求放到User -- 项目管理类的需求放到Member - -![](https://img-blog.csdnimg.cn/92686f9f4c774c909e7aa4227c44f0aa.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_15,color_FFFFFF,t_70,g_se,x_16)这样,用户管理的需求只需调整User类,项目管理的需求只需调整Member类,二者各自变动的理由就少了。 - -### 变化的来源 -上面的做法类似分离关注点。 - -要更好地理解单一职责原则,关键就是分离不同关注点。该案例分离的是不同的业务关注点。所以,理解单一职责原则奥义在于理解分离关注点。 - -分离关注点,发现的关注点越多越好,粒度越小越好。你能看到的关注点越多,就能构建出更多的类,但每个类的规模相应越小,与之相关需求变动也越少,能稳定的几率就越大。 -代码库里稳定的类越多越好,这是我们努力的方向。 - -> 如果将这种思路推演到极致,那一个类就应该只有一个方法,这样,它受到影响最小。 - -的确如此,但实际项目,一个类通常都不只一个方法,要求所有人都做到极致,不现实。 - -> 那应该把哪些内容组织到一起? - -这就需要考虑单一职责原则定义的升级版,即第二个定义:一个模块应该对一类且仅对一类行为者负责。 - -若第一个定义将变化纳入考量,则升级版定义则将变化的来源纳入考量。 - -> 需求为什么会改变? - -因为有各种人提需求,不同人提的需求关注点不同。 -关心用户管理和关心项目管理的可能是两种不同角色的人。两件不同的事,到了代码,却混在一起,这显然不合理。 -所以,分开才是一个好选择: -- 用户管理的人,我和他们聊User -- 项目管理的人,我们来讨论Member - -> 康威定律:一个组织设计出的系统,其结构受限于其组织的沟通结构。 - -Robert Martin说,单一职责原则是基于康威定律的一个推论:一个软件系统的最佳结构高度依赖于使用这个软件的组织的内部结构。 -若我们的软件结构不能够与组织结构对应,就会带来一系列麻烦。 - -实际上,当我们更新了对于单一职责原则的理解,你会发现,它的应用范围不仅可放在类这个级别,也可放到更大级别。 - -某交易平台有个关键模型:手续费率,交易一次按xx比例收佣金。平台可以利用手续费率做不同的活动,比如,给一些人比较低的手续费率,鼓励他们来交易,不同的手续费率意味着对不同交易行为的鼓励。 -- 对运营人员 -手续费率是一个可以玩出花的东西 -- 对交易系统而言 -稳定高效是重点。显然,经常修改的手续费率和稳定的系统之间存在矛盾。 - -分析发现,这是两类不同行为者。所以,设计时,把手续费率设置放到运营子系统,而交易子系统只负责读取手续费率: -- 当运营子系统修改了手续费率,会把最新结果更新到交易子系统 -- 至于各种手续费率设置的花样,交易子系统根本无需关心 - -单一职责原则还能指导我们在不同的子系统之间进行职责分配。所以,单一职责原则这个看起来最简单的原则,实际上也蕴含着很多值得挖掘的内容。 -要想理解好单一职责原则: -- 需要理解封装,知道要把什么样的内容放到一起 -- 理解分离关注点,知道要把不同的内容拆分开来 -- 理解变化的来源,知道把不同行为者负责的代码放到不同的地方。 - - -你就可以更好地理解函数要小的含义了,每个函数承担的职责要单一,这样,它才能稳定。 - -# 4 单一且快乐 -对于: -- 接口,设计时一定要单一 -- 但对于实现类就需要多方面考虑 - -生搬硬套单一职责原则会引起类的剧增,给维护带来非常多的麻烦,而且过分细分类的职责也会人为地增加系统的复杂性。本来一个类可以实现的行为硬要拆成两个类,然后再使用聚合或组合的方式耦合在一起,人为制造了系统的复杂性。所以原则是死的,人是活的。 - -## 单一职责原则很难体现在项目 -国内的技术人员地位和话语权都是最低的,在项目中需要考虑环境、工作量、人员的技术水平、硬件的资源情况等,最终妥协经常违背单一职责原则。 - -单一职责适用于接口、类,同时也适用于方法。一个方法尽可能做一件事情,比如一个方法修改用户密码,不要把这个方法放到“修改用户信息”方法中,这个方法的颗粒度很粗. - -- 一个方法承担多个职责 -![](https://img-blog.csdnimg.cn/img_convert/5cebbf619d200508262e3ddd37e4251e.png) - -在IUserManager中定义了一个方法changeUser,根据传递的类型不同,把可变长度参数changeOptions修改到userBO这个对象上,并调用持久层的方法保存到数据库中。 - -这种代码看到,直接要求其重写即可:方法职责不清晰,不单一,不要让别人猜测这个方法可能是用来处理什么逻辑的。 - -比较好的设计如下: -- 一个方法承担一个职责 -![](https://img-blog.csdnimg.cn/img_convert/21d3889d0f1a23a242e5a24cc464dea5.png) -若要修改用户名称,就调用changeUserName方法 -要修改家庭地址,就调用changeHomeAddress方法 -要修改单位电话,就调用changeOfficeTel方法 -每个方法的职责非常清晰明确,不仅开发简单,而且日后的维护也非常容易。 -# 5 最佳实践 -类的单一职责确实受非常多因素的制约,纯理论地来讲,这个原则很好,但现实有很多难处,你必须考虑项目工期、成本、人员技术水平、硬件情况、网络情况甚至有时候还要考虑政府政策、垄断协议等因素。 - -对于单一职责原则,推荐: -- 接口一定要做到单一职责 -- 类的设计尽量做到只有一个原因引起变化 - -> 参考 -> - 《设计模式之蝉》 \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(2)-\345\274\200\351\227\255\345\216\237\345\210\231.md" "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(2)-\345\274\200\351\227\255\345\216\237\345\210\231.md" deleted file mode 100644 index f0e9f5fe36..0000000000 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(2)-\345\274\200\351\227\255\345\216\237\345\210\231.md" +++ /dev/null @@ -1,201 +0,0 @@ -# 1 定义 -作为程序员,来一个需求就改一次代码,似乎已经习惯这种节奏,甚至理所当然。反正修改也容易,只要按之前的代码再抄一段即可。 - -之所以很多人这样,是因为这样做不费脑子。但每人每次改一点点,日积月累,再来个新需求,后人的改动量就很大了。这个过程中,每个人都很无辜,因为每个人都只是循规蹈矩地修改一点点。但最终导致伤害了所有人,代码已经无法维护。 - -既然“修改”会带来这么多问题,那可以不修改吗? -开放封闭原则就是一种值得努力的方向。 - -`Software entities like classes,modules and functions should be open for extension but closed for modifications` -一个软件实体如类、模块和方法应该对扩展开放,对修改关闭。 - -这是Bertrand Meyer在其著作《面向对象软件构造》(Object-Oriented Software Construction)中提出,它给软件设计提出了一个极高要求:不修改代码。 -真让人魔怔。对扩展开放?开放什么?对修改关闭,怎么关闭? -![](https://img-blog.csdnimg.cn/20210531162533878.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -不修改代码,那我怎么实现新需求? -扩展,即新需求用新代码实现。 - -开放封闭原则向我们描述的是个结果:可不修改代码而仅靠扩展就完成新功能。 -这个结果的前提是要在软件内部留好扩展点,这就是需要设计的地方。 -每一个扩展点都是一个需设计的模型。 - -### 用抽象构建框架,用实现扩展细节 -一个软件实体应该通过扩展来实现变化,而不是通过修改已有代码来实现变化。它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。 -# 2 案例 -## 2.1 书籍 -书籍接口 -![](https://img-blog.csdnimg.cn/20210531170731356.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -Java书籍实现类 -![](https://img-blog.csdnimg.cn/20210531170902658.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -测试类 -![](https://img-blog.csdnimg.cn/20210531170942849.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -现在想添加一个折扣优惠方法:若直接修改原接口,则每个实现类都得重新添加方法实现。 - -但接口应该是稳定的,不应频繁修改! - -Java 书籍折扣类 -![](https://img-blog.csdnimg.cn/20210531171331822.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -现在 UML -![](https://img-blog.csdnimg.cn/20210531171412248.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -`接口应该是稳定且可靠的,不应该经常发生变化`,否则接口作为契约的作用就失去了效能。 -### 修改实现类 -直接在getPrice()中实现打折处理,大家应该经常这样,通过class文件替换的方式即可完成部分业务变化(或缺陷修复)。 - -该方法在项目有明确的章程(团队内约束)或优良的架构设计时,是一个非常优秀的方法,但是该方法还是有缺陷:例如采购书籍人员也要看价格,由于该方法已实现打折处理价格,因此采购人员看到的也是折后价,会`因信息不对称而出现决策失误`的情况。因此,这也不是最优方案。 -### 通过扩展实现变化 -`增加一个子类`OffNovelBook,覆写getPrice方法,高层次的模块(static静态模块区)通过OffNovelBook类产生新的对象,完成业务变化对系统的最小化开发。好办法,修改也少,风险也小。 - -开闭原则对扩展开放,对修改关闭,但并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。 - -# 变化的类型 -## 逻辑变化 -只变化一个逻辑,不涉及其它模块。比如原有的一个算法是`a*b+c`,现在需要修改为`a*b*c`,可以通过修改原有类中的方法完成,前提条件是所有依赖或关联类都按照相同的逻辑处理。 -## 子模块变化 -一个模块变化,会对其他的模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化时,高层次的模块修改是必然的。 -## 可见视图变化 -可见视图是提供给客户使用的界面,如Swing。若仅是按钮、文字的重新排布倒是简单,最司空见惯的是业务耦合变化,什么意思呢?一个展示数据的列表,按照原有的需求是6列,突然有一天要增加1列,而且这一列要跨N张表,处理M个逻辑才能展现出来,这样的变化是比较恐怖的,但还是可以通过扩展来完成变化。 - -所以放弃修改历史的想法吧,一个项目的基本路径:项目开发、重构、测试、投产、运维。 -其中的重构可对原有设计和代码进行修改,运维尽量减少对原有代码的修改,保持历史代码的纯洁性,提高系统的稳定性。 -## 会员案例 -开发一个酒店预订系统,针对不同的用户,计算出不同房价。 -比如: -- 普通用户是全价 -- 金卡是8折 -- 银卡是9折 - -代码可能如下: -![](https://img-blog.csdnimg.cn/d74b8a7ac93f45a0b259be02bffe8c75.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16)来新需求,增加白金卡会员,给出75折,一脉相承写法: -![](https://img-blog.csdnimg.cn/e35d591b809f47bdbc8788bd6766e102.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16)这就是修改代码的做法,每增加一个新类型,就修改次代码。 -但一个有各种级别用户的酒店系统肯定不只是房价不同,提供的服务也可能有区别,比如是否有早餐?预付还是现付?优惠券力度、连住优惠价格?。可以预见,每增加一个用户级别,要改的代码散布各地。 - -> 该怎么办呢? - -应该考虑如何把它设计成一个可扩展模型。 -既然每次要增加的是用户级别,而且各种服务差异都体现在用户级别,就需要一个用户级别模型。 -前面代码,用户级别只是个简单枚举,丰富一下: -![](https://img-blog.csdnimg.cn/8f1f220ec9114992a391bea76e2368bf.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_18,color_FFFFFF,t_70,g_se,x_16)原代码即可重构成: -![](https://img-blog.csdnimg.cn/7e59471ba0924cccbd4fac2503eb4f7e.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16)这样一来,再增加铂金用户,只需新写一个类: -![](https://img-blog.csdnimg.cn/34ec979369794553827bfe978fb70bc2.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_18,color_FFFFFF,t_70,g_se,x_16) -可以这么做,是因为在代码里留好了扩展点:UserLevel:把原来只支持枚举值的UserLevel,升级成了一个有行为的UserLevel。 - -改造后,HotelService的getRoomPrice方法就稳定了,无需根据用户级别不断地调整。 -一旦有稳定的构造块,就能在后续将其当做一个稳定模块复用。 -# 构建扩展点 -其实我们修改代码效果不佳,但真到自己写代码了,就晕了。 -若问你,你开发的系统有问题吗?相信大部人都会不假思索地说有。 -但又问:你会经常主动优化它吗?大部人却又开始沉默了。 -它虽然垃圾,但在线上运行得好好的,万一我一优化,优化坏了咋办,今年绩效可就 3.25 了呀。 -你看,现实就是这样 ,系统宏观层面人人都懂,而在代码落地层,却总是习惯忽视。 - -所以,写软件系统,就应该提供一个个稳定小模块,然后,将它们组合。一个经常变动的模块是不稳定的,用它去构造更大模块,必后患无穷。 - -> 为什么我们这一懂了很多大道理,却依旧写不好代码呢? - -阻碍我们构造稳定模块的,是构建模型的能力。回想产生变化的UserLevel,是如何升级成一个有行为的UserLevel的。 - -封装的要点是行为,数据只是实现细节,而很多人习惯性面向数据写法,这也是导致很多人设计缺乏扩展性。 - -构建模型的难点: -1. 分离关注点 -2. 找到共性 - -**要构建起抽象就要找到事物的共同点**,业务处理过程发现共性对大部分人就已经开始有难度了。 - -我们再来看个例子 -## 报表服务 -![](https://img-blog.csdnimg.cn/f2d1fb410ecb49ed8b2d30cf24a89d7c.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -相信很多日常写代码就是这种风格,这个代码流程肯定是特别僵化。 -只要出现新需求,基本都需要调整这段。 -现在就来了个新需求:把统计信息发给另外一个内部系统,该内部系统可将统计信息展示出来,供外部合作伙伴查阅。 -### 分析 -发给另一个系统的内容是**统计信息**。 -原代码里: -- 前两步是获取源数据,生成**统计信息** -- 后两步生成报表,将**统计信息**通过邮件发出去 - -后两步和即将添加的步骤有个共同点,都使用了统计信息。所以,就可以用一个共同模型去涵盖它们,如OrderStatisticsConsumer: -![](https://img-blog.csdnimg.cn/927eade9b0d74d9392b5bb987100577a.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -这样,新需求也只需添加一个新类,而非 if/else: -![](https://img-blog.csdnimg.cn/07d88e8615564fb2974ea0631b48b087.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -该案例中,第一步要做的还是分解:把一个个步骤分开,然后找出步骤间相似点,并构建一个新模型。 - -实际项目代码可能比这复杂,但并非一定是业务逻辑复杂,而是代码写得就复杂。 -所以,要先根据单一职责原则,将不同需求来源引起的变动拆分到不同方法,形成一个个小单元,再做这里的分析。 - -实际项目中,要达到开放封闭原则也并非一朝一夕。这里只是因为有需求变动,才提取出一个OrderStatisticsConsumer。 - -未来可能还有其它变动,如生成报表的逻辑。那时,也许还会提取一个新OrderStatisticsGenerator的接口。但不管怎样,每做一次这种模型构建,最核心的类就会朝着稳定发展。 - -好的设计都会提供足够扩展点给新功能去扩展。 -《Unix 编程艺术》就提倡“提供机制,而非策略”,这就体现了开放封闭原则。 - -很多系统有插件机制,如IDEA和VS Code,都体现开放封闭原则。去了解它们的接口,即可看到这个软件给我们提供的各种能力。 - -开放封闭原则还可帮助我们优化系统,可通过查看Git,找出那些最经常变动的文件,它们通常都没有满足开放封闭原则,这就可以成为你系统优化的起航点。 -# 为什么选择开闭原则 -## 开闭原则对测试的影响 -有变化提出时,我们就需要考虑一下,原有的健壮代码是否可以不修改,仅仅通过扩展实现变化呢? -否则,就需要把原有的测试过程回笼一遍,需要进行单元测试、功能测试、集成测试甚至是验收测试。 - -以上面提到的书店售书为例,IBook接口写完了,实现类NovelBook也写好了,我们需要写一个测试类进行测试,测试类如代码 -```java -public class NovelBookTest extends TestCase { - private String name = "平凡的世界"; - private int price = 6000; - private String author = "路遥"; - private IBook novelBook = new NovelBook(name,price,author); - - // 测试getPrice方法 - public void testGetPrice() { - //原价销售,根据输入和输出的值是否相等进行断言 - super.assertEquals(this.price, this.novelBook.getPrice()); - } -} -``` -若加个打折销售需求,直接修改getPrice,那就要修改单元测试类。而且在实际项目中,一个类一般只有一个测试类,其中可以有很多的测试方法,在一堆本来就很复杂的断言中进行大量修改,难免出现测试遗漏。 - -所以,需要通过扩展实现业务逻辑变化,而非修改。可通过增加一个子类OffNovelBook完成业务需求变化,这对测试有什么好处呢? -重新生成一个测试文件OffNovelBookTest,然后对getPrice进行测试,单元测试是孤立测试,只要保证我提供的方法正确就成,其他的不管。 -```java -public class OffNovelBookTest extends TestCase { - private IBook below40NovelBook = new OffNovelBook("平凡的世界",3000,"路遥"); - private IBook above40NovelBook = new OffNovelBook("平凡的世界",6000,"路遥"); - - // 测试低于40元的数据是否是打8折 - public void testGetPriceBelow40() { - super.assertEquals(2400, this.below40NovelBook.getPrice()); - } - - // 测试大于40的书籍是否是打9折 - public void testGetPriceAbove40(){ - super.assertEquals(5400, this.above40NovelBook.getPrice()); - } -} -``` -新增加的类,新增加的测试方法,只要保证新增加类是正确的就可以了。 -## 提高复用性 -OOP中,所有逻辑都是从原子逻辑组合而来,而非在一个类中独立实现一个业务逻辑。只有这样代码才可复用,粒度越小,被复用可能性越大。 -- 为什么要复用? -减少代码量,避免相同逻辑分散,避免后来的维护人员为修改一个小bug或加个新功能而在整个项目中到处查找相关代码,然后发出对开发人员“极度失望”的感慨。 -- 如何才能提高复用率? -缩小逻辑粒度,直到一个逻辑不可再拆分为止。 -## 提高可维护性 -一款软件投产后,维护人员的工作不仅仅是对数据进行维护,还可能要对程序进行扩展,维护人员最乐意做的事情就是扩展一个类,而非修改一个类,甭管原有代码写得好坏,让维护人员读懂原有代码,然后再修改,是炼狱!不要让他在原有代码海洋里瞎游完毕后再修改,那是对维护人员的摧残。 - -## OOP -万物皆对象,我们需要把所有的事物都抽象成对象,然后针对对象进行操作,但运动是一定的,有运动就有变化,有变化就要有策略去应对,怎么快速应对呢?这就需要在设计之初考虑到所有可能变化的因素,然后留下接口,等待“可能”转为“现实”。 - -- 优点 -提高软件系统的可复用性及可维护性 -# 总结 -若说单一职责原则主要看封装,开放封闭原则就必须有多态参与。 -要想提供扩展点,就需面向接口编程。 - -java的SPI给开发者提供了不错的扩展机制,像spring boot 和dubbo就在此基础上做了改进,各自提供了扩展点,spring boot允许用户自定义starter,dubbo可以自定义协议等 - -1、识别修改点,构建模型,将原来静态的逻辑转为动态的逻辑 -2、构建模型的难点在于分离关注点,其次就是找到共性 \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(3)-\344\276\235\350\265\226\345\200\222\347\275\256\345\216\237\345\210\231.md" "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(3)-\344\276\235\350\265\226\345\200\222\347\275\256\345\216\237\345\210\231.md" deleted file mode 100644 index da9d6780d1..0000000000 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(3)-\344\276\235\350\265\226\345\200\222\347\275\256\345\216\237\345\210\231.md" +++ /dev/null @@ -1,259 +0,0 @@ -# 1 定义 -Dependence Inversion Principle,DIP -High level modules should not depend upon low level modules.Both should depend upon abstractions.高层模块不应该依赖低层模块,二者都应该依赖其抽象 -Abstractions should not depend upon details.Details should depend upon abstractions.抽象不应该依赖细节;细节应该依赖抽象 - -针对接口编程,不要针对实现编程。 - -每个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块,原子逻辑的再组装就是高层模块 -在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的 -细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化,也就是可以加上一个关键字new产生一个对象。 -依赖倒置原则在Java语言中的表现就是: -● 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的; -● 接口或抽象类不依赖于实现类; -● 实现类依赖接口或抽象类。 - -优点:可以减少类间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险。 - -能不能把依赖弄对,要动点脑。依赖关系没处理好,就会导致一个小改动影响一大片。 -把依赖方向搞反,就是最典型错误。 - -最重要的是要理解“倒置”,而要理解什么是“倒置”,就要先理解所谓的“正常依赖”是什么样的。 -结构化编程思路是自上而下功能分解,这思路很自然地就会延续到很多人的编程习惯。按照分解结果,进行组合。所以,很自然地写出下面这种代码 -![](https://img-blog.csdnimg.cn/72ee743f28ed4577b3ac72117dd6528b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_9,color_FFFFFF,t_70,g_se,x_16) -这种结构天然的问题:高层模块依赖于低层模块: -- CriticalFeature是高层类 -- Step1和Step2就是低层模块,而且Step1和Step2通常都是具体类 - -> 很多人好奇了:step1和step2如果是接口,还有问题吗?像这种流程式的代码还挺常见的? -> 有问题,你无法确定真的是Step1和Step2,还会不会有Step3,所以这个设计依旧是不好的。如果你的设计是多个Step,这也许是个更好的设计。 - -在实际项目中,代码经常会直接耦合在具体的实现上。比如,我们用Kafka做消息传递,就在代码里直接创建了一个KafkaProducer去发送消息。我们就可能会写出这样的代码: -![](https://img-blog.csdnimg.cn/5706917b48204378aa6a4c0a6d3c8794.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -我用Kafka发消息,创建个KafkaProducer,有什么问题吗? -我们需要站在长期角度去看,什么东西是变的、什么东西是不变的。Kafka虽然很好,但它并不是系统最核心部分,未来是可能被换掉的。 - -你可能想,这可是我的关键组件,怎么可能会换掉它? -软件设计需要关注长期、放眼长期,所有那些不在自己掌控之内的东西,都有可能被替换。替换一个中间件是经常发生的。所以,依赖于一个可能会变的东西,从设计的角度看,并不是一个好的做法。 - -> 那该怎么做呢?这就轮到倒置了。 - -倒置,就是把这种习惯性的做法倒过来,让高层模块不再依赖低层模块。 -功能该如何完成? - -> 计算机科学中的所有问题都可以通过引入一个间接层得到解决。 All problems in computer science can be -> solved by another level of indirection —— David Wheeler - -引入一个间接层,即DIP里的抽象,软件设计中也叫模型。这段代码缺少了一个模型,而这个模型就是这个低层模块在这个过程中所承担角色。 - -# 2 实战 -## 重构 -既然这个模块扮演的就是消息发送者的角色,那我们就可以引入一个消息发送者(MessageSender)的模型: -![](https://img-blog.csdnimg.cn/ec275a7b4a5845d38ad27c4a361f43d9.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_12,color_FFFFFF,t_70,g_se,x_16) -有了消息发送者这个模型,又该如何把Kafka和这个模型结合呢? -实现一个Kafka的消息发送者: -![](https://img-blog.csdnimg.cn/b214d3381da04d22b4344ab9fece5322.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -这样高层模块就不像之前直接依赖低层模块,而是将依赖关系“倒置”,让低层模块去依赖由高层定义好的接口。 -好处就是解耦高层模块和低层实现。 - -若日后替换Kafka,只需重写一个MessageSender,其他部分无需修改。这就能让高层模块保持稳定,不会随低层代码改变。 - -这就是建立模型(抽象)的意义。 - -所有软件设计原则都在强调尽可能分离变的部分和不变的部分,让不变的部分保持稳定。 -模型都是相对稳定的,而实现细节是易变的。所以,构建稳定的模型层是关键。 -# 依赖于抽象 -**抽象不应依赖于细节,细节应依赖于抽象。** - -简单理解:依赖于抽象,可推出具体编码指导: -- 任何变量都不应该指向一个具体类 -如最常用的List声明![](https://img-blog.csdnimg.cn/cde6a7acfa034a779fbdd6bbf3ebf365.png) - -- 任何类都不应继承自具体类 -- 任何方法都不应该改写父类中已经实现的方法 - -当然了,如上指导并非绝对。若一个类特稳定,也可直接用,比如String 类,但这种情况很少! -因为大多数人写的代码稳定度都没人家 String 类设计的高。 - - -## 学习 -首先定义一个学习者类 -![](https://img-blog.csdnimg.cn/9d5549c4022d49a689a9855d75a31401.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -简单的测试类 -![](https://img-blog.csdnimg.cn/99e7fb4d2dab4263a0877222b926b9e5.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -假如现在又想学习Python,则面向实现编程就是直接在类添加方法 -![](https://img-blog.csdnimg.cn/24ebf1765efe4a78b6dc5a7c9ed60caf.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -此类需经常改变,扩展性太差,Test 高层模块, Learner 类为低层模块,耦合度过高! - -让我们引入抽象: -![](https://img-blog.csdnimg.cn/573f54c53f1443b58a36fafef11d79c0.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/5b6d180123974a23bf3971391b8197b3.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/4633628a03c941cb839fe5dfa23e19bb.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/fe10ce1aea6e414785543a2a2529b87a.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -现在就能将原学习者类的方法都消除 -![](https://img-blog.csdnimg.cn/8c0e7b8aa3d14d04baef534ba6813c09.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/e7b06faa36834adfb7029e1a682e5f1b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -# 3 证明 -采用依赖倒置原则可减少类间耦合,提高系统稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。 - -证明一个定理是否正确,有两种常用方法: -- 顺推证法 -根据论题,经过论证,推出和定理相同结论 -- 反证法 -先假设提出的命题是伪命题,然后推导出一个与已知条件互斥结论 - -反证法来证明依赖倒置原则的优秀! -## 论题 -依赖倒置原则可减少类间的耦合性,提高系统稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。 -## 反论题 -不使用依赖倒置原则也可减少类间的耦合性,提高系统稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。 - -通过一个例子来说明反论题不成立! -- 司机驾驶奔驰车类图 -![](https://img-blog.csdnimg.cn/img_convert/d5411d44da28d7d9af4889615357be2c.png) - -奔驰车可提供一个方法run,代表车辆运行: -![](https://img-blog.csdnimg.cn/1ecd458d153b4184a32045e088acd7d1.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_19,color_FFFFFF,t_70,g_se,x_16) - -司机通过调用奔驰车的run方法开动奔驰车 -![](https://img-blog.csdnimg.cn/4ab78125c7e44ed596512f1ff7582fdd.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -有车,有司机,在Client场景类产生相应的对象 -![](https://img-blog.csdnimg.cn/13b5f3140c76418fad3772cbe2a93849.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -现在来了新需求:张三司机不仅要开奔驰车,还要开宝马车,又该怎么实现呢? -走一步是一步,先把宝马车产生出来 -![](https://img-blog.csdnimg.cn/eb47654721bc4f7faf2cfa8a60150fd8.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -宝马车产生了,但却没有办法让张三开起来,为什么? -张三没有开动宝马车的方法呀!一个拿有C驾照的司机竟然只能开奔驰车而不能开宝马车,这也太不合理了!在现实世界都不允许存在这种情况,何况程序还是对现实世界的抽象。 - -我们的设计出现了问题:`司机类和奔驰车类紧耦合,导致系统可维护性大大降低,可读性降低`。 -两个相似的类需要阅读两个文件,你乐意吗?还有稳定性,什么是稳定性?固化的、健壮的才是稳定的,这里只是增加了一个车类就需要修改司机类,这不是稳定性,这是易变性。 -`被依赖者的变更竟然让依赖者来承担修改成本,这样的依赖关系谁肯承担?` -证明至此,反论题已经部分不成立了。 - -继续证明,`“减少并行开发引起的风险”` - -> 什么是并行开发的风险? - -并行开发最大的风险就是风险扩散,本来只是一段程序的异常,逐步波及一个功能甚至模块到整个项目。一个团队,一二十个开发人员,各人负责不同功能模块,甲负责汽车类的建造,乙负责司机类的建造,在甲没有完成的情况下,乙是不能完全地编写代码的,缺少汽车类,编译器根本就不会让你通过!在缺少Benz类的情况下,Driver类能编译吗?更不要说是单元测试了!在这种不使用依赖倒置原则的环境中,所有开发工作都是“单线程”,甲做完,乙再做,然后是丙继续……这在20世纪90年代“个人英雄主义”编程模式中还是比较适用的,一个人完成所有的代码工作。但在现在的大中型项目中已经是完全不能胜任了,一个项目是一个团队协作的结果,一个“英雄”再牛也不可能了解所有的业务和所有的技术,要协作就要并行开发,要并行开发就要解决模块之间的项目依赖关系,那然后呢? -依赖倒置原则隆重出场! - -根据以上证明,若不使用依赖倒置原则就会加重类间的耦合性,降低系统的稳定性,增加并行开发引起的风险,降低代码的可读性和可维护性。 - -引入DIP后的UML: -![](https://img-blog.csdnimg.cn/aba59e6524fb48f5bd2e69d4c8c63378.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -建立两个接口:IDriver和ICar,分别定义了司机和汽车的各个职能,司机就是驾驶汽车,必须实现drive()方法 -![](https://img-blog.csdnimg.cn/e69064246bd14ff0be1008ea94e3a2f2.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -接口只是一个抽象化的概念,是对一类事物的最抽象描述,具体的实现代码由相应的实现类来完成 -![](https://img-blog.csdnimg.cn/d15ce9e76e494223aa5be2c8b5fb567d.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -IDriver通过传入ICar接口实现了抽象之间的依赖关系,Driver实现类也传入了ICar接口,至于到底是哪个型号的Car,需要声明在高层模块。 - -ICar及其两个实现类的实现过程: -![](https://img-blog.csdnimg.cn/58f0b6e79d204818833e484cf85075f8.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -业务场景应贯彻“抽象不应依赖细节”,即抽象(ICar接口)不依赖BMW和Benz两个实现类(细节),因此在高层次的模块中应用都是抽象: -![](https://img-blog.csdnimg.cn/0ffe565e8168413297541c14fe734702.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -Client属于高层业务逻辑,它对低层模块的依赖都建立在抽象上,java的表面类型是IDriver,Benz的表面类型是ICar。 - -> 在这个高层模块中也调用到了低层模块,比如new Driver()和new Benz()等,如何解释? - -java的表面类型是IDriver,是一个接口,是抽象的、非实体化的,在其后的所有操作中,java都是以IDriver类型进行操作,屏蔽了细节对抽象的影响。当然,java如果要开宝马车,也很容易,只需修改业务场景类即可。 - -在新增加低层模块时,只修改了业务场景类,也就是高层模块,对其他低层模块如Driver类不需要做任何修改,业务就可以运行,把“变更”引起的风险扩散降到最低。 - -Java只要定义变量就必然要有类型,一个变量可以有两种类型: -- 表面类型 -在定义的时候赋予的类型 -- 实际类型 -对象的类型,如java的表面类型是IDriver,实际类型是Driver。 - -思考依赖倒置对并行开发的影响。两个类之间有依赖关系,只要制定出两者之间的接口(或抽象类)即可独立开发,而且项目之间的单元测试也可以独立地运行,而TDD(Test-Driven Development,测试驱动开发)开发模式就是依赖倒置原则的最高级应用。 - -回顾司机驾驶汽车的例子: -- 甲程序员负责IDriver开发 -- 乙程序员负责ICar的开发 - -两个开发人员只要制定好了接口就可以独立地开发了,甲开发进度比较快,完成了IDriver以及相关的实现类Driver的开发工作,而乙程序员滞后开发,那甲是否可以进行单元测试呢? -根据抽象虚拟一个对象进行测试: -![](https://img-blog.csdnimg.cn/974e7b67697144f4842d5d608666d8fb.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -只需要一个ICar接口,即可对Driver类进行单元测试。 -这点来看,两个相互依赖的对象可分别进行开发,各自独立进行单元测试,保证了并行开发的效率和质量,TDD开发的精髓不就在这? -TDD,先写好单元测试类,然后再写实现类,这对提高代码的质量有非常大的帮助,特别适合研发类项目或在项目成员整体水平较低情况下采用。 - -抽象是对实现的约束,对依赖者而言,也是一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其为保证所有细节不脱离契约范畴,确保约束双方按既定契约(抽象)共同发展,只要抽象这根基线在,细节就脱离不了这个圈圈。 -# 4 依赖 -依赖可以传递,A对象依赖B对象,B又依赖C,C又依赖D…… -`只要做到抽象依赖,即使是多层的依赖传递也无所畏惧!` - -对象的依赖关系有如下传递方式: -## 构造器传递 -在类中通过构造器声明依赖对象,`构造函数注入` -![](https://img-blog.csdnimg.cn/77920db0598846fb9914a1bb254e0b19.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_17,color_FFFFFF,t_70,g_se,x_16) -## 4.2 Setter传递 -在抽象中设置Setter方法声明依赖关系,`Setter依赖注入` -![](https://img-blog.csdnimg.cn/556f0648b1cf4d39ba350a61468efc36.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_18,color_FFFFFF,t_70,g_se,x_16) -## 4.3 接口声明依赖对象 -在接口的方法中声明依赖对象 -# 5 最佳实践 -依赖倒置原则的本质就是`通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合`。 - -DIP指导下,具体类能少用就少用。 -具体类我们还是要用的,毕竟代码要运行起来不能只依赖于接口。那具体类应该在哪用? -这些设计原则,核心关注点都是一个个业务模型。此外,还有一些代码做的工作是负责组装这些模型,这些负责组装的代码就需要用到一个个具体类。 - -做这些组装工作的就是DI容器。因为这些组装几乎是标准化且繁琐。如果你常用的语言中,没有提供DI容器,最好还是把负责组装的代码和业务模型放到不同代码。 - -DI容器另外一个说法叫IoC容器,Inversion of Control,你会看到IoC和DIP中的I都是inversion,二者意图是一致的。 - -依赖之所以可注入,是因为我们的设计遵循 DIP。而只知道DI容器不了解DIP,时常会出现让你觉得很为难的模型组装,根因在于设计没做好。 - -DIP还称为好莱坞规则:“Don’t call us, we’ll call you”,“别调用我,我会调你的”。这是框架才会有的说法,有了一个稳定抽象,各种具体实现都应由框架去调用。 - -为什么说一开始TransactionRequest是把依赖方向搞反了?因为最初的TransactionRequest是一个具体类,而TransactionHandler是业务类。 - -我们后来改进的版本里引入一个模型,把TransactionRequest变成了接口,ActualTransactionRequest 实现这个接口,TransactionHandler只依赖于接口,而原来的具体类从这个接口继承而来,相对来说,比原来的版本好一些。 - -对于任何一个项目而言,了解不同模块的依赖关系是一件很重要的事。你可以去找一些工具去生成项目的依赖关系图,然后,你就可以用DIP作为一个评判标准,去衡量一下你的项目在依赖关系上表现得到底怎么样了。很有可能,你就找到了项目改造的一些着力点。 - -理解了 DIP,再来看一些关于依赖的讨论,我们也可以看到不同的角度。 -比如,循环依赖,循环依赖就是设计没做好的结果,把依赖关系弄错,才可能循环依赖,先把设计做对,把该有的接口提出来,就不会循环了。 - -我们怎么在项目中使用这个规则呢?只要遵循以下的几个规则就可以: -- 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备 -这是依赖倒置的基本要求,接口和抽象类都是属于抽象的,有了抽象才可能依赖倒置 - -- 变量的表面类型尽量是接口或者是抽象类 -很多书上说变量的类型一定要是接口或者是抽象类,这个有点绝对化了 - - 比如一个工具类,xxxUtils一般是不需要接口或是抽象类的 - - 如果你要使用类的clone方法,就必须使用实现类,这个是JDK提供的一个规范。 - -- 任何类都不应该从具体类派生 -如果一个项目处于开发状态,确实不应该有从具体类派生出子类的情况,但这也不是绝对的,因为人都是会犯错误的,有时设计缺陷是在所难免的,因此只要不超过两层的继承都是可以忍受的 - -- 尽量不要覆写基类方法 -如果基类是一个抽象类,而且这个方法已经实现了,子类尽量不要覆写 -类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会产生一定的影响 - -- 结合里氏替换原则使用 -父类出现的地方子类就能出现, 接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。 - -## 到底什么是“倒置” -依赖正置就是类间的依赖是实实在在的实现类间的依赖,也就是面向实现编程,这也是正常人的思维方式,我要开奔驰车就依赖奔驰车,我要使用笔记本电脑就直接依赖笔记本电脑。 -而编写程序需要的是对现实世界的事物进行抽象,抽象的结果就是有了抽象类和接口,然后我们根据系统设计的需要产生了抽象间的依赖,代替了人们传统思维中的事物间的依赖,“倒置”就是从这里产生的 - - -依赖倒置原则是实现开闭原则的重要途径,依赖倒置原则没有实现,就别想实现对扩展开放,对修改关闭。 -只要记住`面向接口编程`就基本上抓住了依赖倒置原则的核心。 -![](https://img-blog.csdnimg.cn/037c5d536a184ef8b99df4f69ea9aed9.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -依赖倒置原则说的是: -1.高层模块不应依赖于低层模块,二者都应依赖于抽象 -2.抽象不应依赖于细节,细节应依赖于抽象 -总结起来就是依赖抽象(模型),具体实现抽象接口,然后把模型代码和组装代码分开,这样的设计就是分离关注点,将不变的与不变有效的区分开 - -防腐层可以解耦对外部系统的依赖。包括接口和参数。防腐层还可以贯彻接口隔离的思想,以及做一些功能增强(加缓存,异步并发取值)。 - -> 参考 -> - 设计模式之婵 \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(4)-\346\216\245\345\217\243\351\232\224\347\246\273\345\216\237\345\210\231.md" "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(4)-\346\216\245\345\217\243\351\232\224\347\246\273\345\216\237\345\210\231.md" deleted file mode 100644 index adc8d73f90..0000000000 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(4)-\346\216\245\345\217\243\351\232\224\347\246\273\345\216\237\345\210\231.md" +++ /dev/null @@ -1,7 +0,0 @@ -接口隔离原则(英语:interface-segregation principles, 缩写:ISP)指明客户(client)不应被迫使用对其而言无用的方法或功能。 - -接口隔离原则(ISP)拆分非常庞大臃肿的接口成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。这种缩小的接口也被称为角色接口(role interfaces)。 - -接口隔离原则(ISP)的目的是系统解开耦合,从而容易重构,更改和重新部署。接口隔离原则是在SOLID中五个面向对象设计(OOD)的原则之一,类似于在GRASP中的高内聚性。 - -在面向对象设计中,接口(interface)提供了便于代码在概念上解释的抽象层,并创建了避免依赖的一个屏障。 \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(5)-\350\277\252\347\261\263\347\211\271\346\263\225\345\210\231\357\274\210LOD\357\274\211.md" "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(5)-\350\277\252\347\261\263\347\211\271\346\263\225\345\210\231\357\274\210LOD\357\274\211.md" deleted file mode 100644 index 6acc76f4fd..0000000000 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\275\257\344\273\266\345\267\245\347\250\213\350\256\276\350\256\241\345\216\237\345\210\231/\350\275\257\344\273\266\350\256\276\350\256\241\345\216\237\345\210\231(5)-\350\277\252\347\261\263\347\211\271\346\263\225\345\210\231\357\274\210LOD\357\274\211.md" +++ /dev/null @@ -1,266 +0,0 @@ -尽管不像SOLID、KISS、DRY原则一样家喻户晓,但它却非常实用。 -- 啥是“高内聚、松耦合”? -- 如何利用迪米特法则来实现“高内聚、松耦合”? -- 有哪些代码设计是明显违背迪米特法则的?对此又该如何重构? - -# 何为“高内聚、松耦合”? -“高内聚、松耦合”,能有效地提高代码可读性、可维护性,缩小功能改动导致的代码改动范围。 -很多设计原则都以实现代码“高内聚、松耦合”为目的,比如: -- 单一职责原则 -- 基于接口而非实现编程 - -“高内聚、松耦合”是个较通用的设计思想,可用来指导不同粒度代码的设计与开发,比如系统、模块、类,甚至是函数,也可以应用到不同的开发场景中,比如微服务、框架、组件、类库等。 - -本文以“类”作为这个设计思想的应用对象来讲解。 - -- “高内聚”用来指导类本身的设计 -- “松耦合”用来指导类与类之间依赖关系的设计 - -这两者并非完全独立不相干。高内聚有助于松耦合,松耦合又需要高内聚的支持。 -## 什么是“高内聚”? -- 相近功能,应放到同一类 -- 不相近的功能,不要放到同一类 - -相近的功能往往会被同时修改,放到同一类中,修改会比较集中,代码容易维护。 - -单一职责原则就是实现代码高内聚非常有效的设计原则。 -## 什么是“松耦合”? -在代码中,类与类之间的依赖关系简单清晰。 - -即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。依赖注入、接口隔离、基于接口而非实现编程及迪米特法则,都是为实现代码松耦合。 - -## “内聚”和“耦合”的关系 -图中左边部分的代码结构是“高内聚、松耦合”;右边部分正好相反,是“低内聚、紧耦合”。 -![](https://img-blog.csdnimg.cn/474e839624224ad0b7b35f9f216ab43e.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -## 左半部分的代码设计 -类的粒度较小,每个类的职责都比较单一。相近的功能都放到了一个类中,不相近的功能被分割到了多个类中。 -这样类更加独立,代码的内聚性更好。 -因为职责单一,所以每个类被依赖的类就会比较少,代码低耦合。 -一个类的修改,只会影响到一个依赖类的代码改动。 -只需要测试这一个依赖类是否还能正常工作即可。 - -## 右边部分的代码设计 -类粒度比较大,低内聚,功能大而全,不相近的功能放到了一个类中。 -导致很多其他类都依赖该类。 -修改这个类的某功能代码时,会影响依赖它的多个类。 -我们需要测试这三个依赖类,是否还能正常工作。这也就是所谓的“牵一发而动全身”。 - -高内聚、低耦合的代码结构更加简单、清晰,相应地,在可维护性和可读性上确实要好很多。 - -# 迪米特法则 -Law of Demeter,LOD,这个名字看不出这个原则讲的是什么。 -它还有另外一个名字,叫作最小知识原则,The Least Knowledge Principle: -Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers. -每个模块(unit)只应该了解那些与它关系密切的模块(units: only units “closely” related to the current unit)的有限知识(knowledge)。或者说,每个模块只和自己的朋友“说话”(talk),不和陌生人“说话”(talk)。 - -结合经验,定义描述中的“模块”替换成“类”: - -> 不该有直接依赖关系的类之间,不要有依赖; -> 有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。 - -可见,迪米特法则包含前后两部分,讲的是两件事情,我用两个实战案例分别来解读一下。 -# 案例 -**不该有直接依赖关系的类之间,不要有依赖。** - -实现了简化版的搜索引擎爬取网页,包含如下主要类: -- NetworkTransporter,负责底层网络通信,根据请求获取数据 -![](https://img-blog.csdnimg.cn/461d68b211254631adedf747ee28f87d.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -- HtmlDownloader,通过URL获取网页 -![](https://img-blog.csdnimg.cn/27ed63b4a4f0439095e8f12a1785de40.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - - -- Document,表示网页文档,后续的网页内容抽取、分词、索引都是以此为处理对象 -![](https://img-blog.csdnimg.cn/7ad339dd22614416b56fe15e2d96933c.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -### 分析 -这段代码虽然“能用”,但不够“好用”,有很多设计缺陷: -- NetworkTransporter,作为一个底层网络通信类,我们希望它的功能尽可能通用,而不只是服务于下载HTML。所以,不应该直接依赖太具体的发送对象HtmlRequest。 -这点,NetworkTransporter类的设计违背迪米特法则,依赖了不该有直接依赖关系的HtmlRequest类。 - -应该如何重构? -假如你现在要去买东西,你肯定不会直接把钱包给收银员,让收银员自己从里面拿钱,而是你从钱包里把钱拿出来交给收银员。 -HtmlRequest对象相当于钱包,HtmlRequest里的address和content对象就相当于钱。 -应该把address和content交给NetworkTransporter,而非直接把HtmlRequest交给NetworkTransporter: -![](https://img-blog.csdnimg.cn/c1c2dbc6d3e64fd681373fa854734df2.png) - -HtmlDownloader类的设计没问题。不过,因为修改了NetworkTransporter#send(),而这个类用到了send(),所以要做相应修改,修改后的代码如下所示: -```java -public class HtmlDownloader { - - private NetworkTransporter transporter; - - public Html downloadHtml(String url) { - HtmlRequest htmlRequest = new HtmlRequest(url); - Byte[] rawHtml = transporter.send( - htmlRequest.getAddress(), htmlRequest.getContent().getBytes()); - return new Html(rawHtml); - } -} -``` - -Document的问题较多: -- 构造器中的`downloader.downloadHtml()`逻辑复杂,耗时长,不应放到构造器,会影响代码的可测试性 -- HtmlDownloader对象在构造器通过new来创建,违反了基于接口编程,也影响代码可测试性 -- 业务含义上说,Document网页文档没必要依赖HtmlDownloader类,违背迪米特法则 - -问题虽多,但修改简单: -```java -public class Document { - - private Html html; - private String url; - - public Document(String url, Html html) { - this.html = html; - this.url = url; - } - // ... -} - -// 工厂方法创建Document -public class DocumentFactory { - private HtmlDownloader downloader; - - public DocumentFactory(HtmlDownloader downloader) { - this.downloader = downloader; - } - - public Document createDocument(String url) { - Html html = downloader.downloadHtml(url); - return new Document(url, html); - } -} -``` -## 序列化 -**有依赖关系的类之间,尽量只依赖必要的接口。** - -Serialization类负责对象的序列化和反序列化。 -```java -public class Serialization { - - public String serialize(Object object) { - String serializedResult = ...; - //... - return serializedResult; - } - - public Object deserialize(String str) { - Object deserializedResult = ...; - //... - return deserializedResult; - } -} -``` -单看类的设计,没问题。 -但若把它放到特定应用场景,假设项目中的有些类只用到序列化操作,而另一些类只用到反序列化。 -那么,基于 **有依赖关系的类之间,尽量只依赖必要的接口**,只用到序列化操作的那部分类不应依赖反序列化接口,只用到反序列化操作的那部分类不应依赖序列化接口。 - -据此,应将Serialization类拆分为两个更小粒度的类: -- 一个只负责序列化(Serializer类) -- 一个只负责反序列化(Deserializer类) - -拆分后,使用序列化操作的类只需依赖Serializer类,使用反序列化操作的类依赖Deserializer类。 -```java -public class Serializer { - public String serialize(Object object) { - String serializedResult = ...; - ... - return serializedResult; - } -} - -public class Deserializer { - public Object deserialize(String str) { - Object deserializedResult = ...; - ... - return deserializedResult; - } -} -``` -尽管拆分后的代码更能满足迪米特法则,但却违背高内聚。高内聚要求相近功能放到同一类中,这样可以方便功能修改时,修改的地方不至于太散乱。 -针对本案例,若业务要求修改了序列化实现方式,从JSON换成XML,则反序列化实现逻辑也要一起改。 -在未拆分时,只需修改一个类。在拆分之后,却要修改两个类。这种设计思路的代码改动范围变大了! - -既不想违背高内聚,也不想违背迪米特法则,怎么办? -引入两个接口即可: -```java -public interface Serializable { - String serialize(Object object); -} - -public interface Deserializable { - Object deserialize(String text); -} - -public class Serialization implements Serializable, Deserializable { - - @Override - public String serialize(Object object) { - String serializedResult = ...; - ... - return serializedResult; - } - - @Override - public Object deserialize(String str) { - Object deserializedResult = ...; - ... - return deserializedResult; - } -} - -public class DemoClass_1 { - private Serializable serializer; - - public Demo(Serializable serializer) { - this.serializer = serializer; - } - //... -} - -public class DemoClass_2 { - private Deserializable deserializer; - - public Demo(Deserializable deserializer) { - this.deserializer = deserializer; - } - //... -} -``` - -尽管还是要往DemoClass_1的构造器传入包含序列化和反序列化的Serialization实现类,但依赖的Serializable接口只包含序列化操作,DemoClass_1无法使用Serialization类中的反序列化接口,对反序列化操作无感知,就符合了迪米特法则的“依赖有限接口”。 - -实际上,上面的的代码实现思路,也体现“面向接口编程”,结合迪米特法则,可总结出:“基于最小接口而非最大实现编程”。 - -**新的设计模式和设计原则也就是在大量的实践中,针对开发痛点总结归纳出来的套路。** - -## 多想一点 -对于案例二的重构方案,你有啥不同观点? - -整个类只包含序列化、反序列化俩操作,只用到序列化操作的使用者,即便能够感知到仅有的一个反序列化方法,问题也不大。 -为了满足迪米特法则,将一个简单的类,拆出两个接口,是否有过度设计之嫌? - -设计原则本身无对错,只有能否用对之说。不要为了应用设计原则而应用,具体问题具体分析。 - -对Serialization类,只包含两个操作,确实没太大必要拆成俩接口。 -但若我们对Serialization类添加更多功能,实现更多更好用的序列化、反序列化方法,重新考虑该问题: -```java -public class Serializer { // 参看JSON的接口定义 - public String serialize(Object object) { //... } - public String serializeMap(Map map) { //... } - public String serializeList(List list) { //... } - - public Object deserialize(String objectString) { //... } - public Map deserializeMap(String mapString) { //... } - public List deserializeList(String listString) { //... } -} -``` -这种场景下,第二种设计思路更好。因为基于之前的应用场景来说,大部分代码只用到序列化功能。对于这部分使用者,不必了解反序列化,而修改之后的Serialization类,反序列化的“知识”,从一个函数变成了三个。一旦任一反序列化操作有代码改动,我们都需要检查、测试所有依赖Serialization类的代码是否还能正常工作。为了减少耦合和测试工作量,我们应该按照迪米特法则,将反序列化和序列化的功能隔离开来。 - -# 总结 -## 高内聚、松耦合 -重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。 - -所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。所谓松耦合指的是,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。 -## 迪米特法则 - -不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。 \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\350\275\257\344\273\266\345\256\236\347\216\260\345\210\206\346\236\220\344\271\213\351\201\223.md" "b/\351\207\215\346\236\204/\350\275\257\344\273\266\345\256\236\347\216\260\345\210\206\346\236\220\344\271\213\351\201\223.md" deleted file mode 100644 index 0df1e1c7ea..0000000000 --- "a/\351\207\215\346\236\204/\350\275\257\344\273\266\345\256\236\347\216\260\345\210\206\346\236\220\344\271\213\351\201\223.md" +++ /dev/null @@ -1,152 +0,0 @@ -在一个系统中,模型和接口是相对稳定的部分。 -但同样的模型和接口,若采用不同实现,稳定性、可扩展性和性能等诸多方面相差极大。只有熟悉实现,才有改代码写新需求的基础。 - -“看实现”的确是个大难题,因有无数细节怪在等你。所以,团队的新人都需要几个月试用期去熟悉代码细节。 - -你不可能记住项目所有细节,但这不妨碍你工作。但若你心中没有一份关于项目实现的地图,你就一定会迷失。 - -新人一般用几个月熟悉代码,就是在通过代码一点点展开地图,但是,这不仅极其浪费时间,也很难形成整体认知。 - -> 推荐你应该直接把地图展开。怎么展开? - -要找到两个关键点:软件的结构和关键的技术。 - -以Kafka为例,了解一个软件设计三步走:“模型、接口、实现”。 -先看Kafka的模型和接口。 - -# MQ的模型与接口 -Kafka自我介绍是个分布式流平台,这是它现在的发展方向,但更多人觉得它是个MQ。 -MQ是Kafka这个软件的核心模型,而流平台显然是这个核心模型存在之后的扩展。所以,要先把焦点放在Kafka的核心模型——MQ。 - -MQ(Messaging Queue)是一种进程间通信方式,发消息的一方(即生产者)将消息发给MQ,收消息的一方(即消费者)将队列中的消息取出并处理。 - -看模型,MQ是很简单的,不就是生产者发消息,消费者消费消息,还有个topic,区分发给不同目标的消息。 - -基本接口也很简单: -生产者发消息: -![](https://img-blog.csdnimg.cn/201185670ef445798199f9224f4bb5c4.png) -消费者收消息: -![在这里插入图片描述](https://img-blog.csdnimg.cn/77042fe28bee460b9321cfda28384769.png) -看完模型和接口,你会感觉MQ本身并不难。 - -但MQ实现有很多,Kafka只是其中一种,为什么会有这么多不同MQ实现呢?因为每个MQ实现有所侧重,有其适用场景。 - -MQ还提供一定的消息存储能力。当 - -```java -Pro发消息速度>Con处理消息速度 -``` -MQ可起到缓冲作用。所以MQ还能“削峰填谷”:在消息量特别大时,先把消息收下来,慢慢处理,以减小系统压力。 - -Kafka之所以突出于一大堆MQ实现,关键在于它针对消息写入做优化,它的生产者写入特快,即吞吐力特强。 - -显然,接口和模型不足以将Kafka与其他MQ实现区分。所以,必须开始了解它的实现。 -看软件实现时的关键: -- 软件的结构 -- 关键的技术 - -模型是个抽象概念,被抽象的对象可以是某个聚合实体(订单中心中的订单),也可以是某个流程或功能(Java内存模型中的主存与缓存同步的规则)。 -分层对模型来说是实现层面的东西,是一种水平方向的拆分,是一个实现上的规范; -模型的细粒度拆分(父模型、子模型),应该是一种垂直维度的拆分,子模型的功能要高内聚,其复杂性不该发散到外部。 - - -# 软件结构 -软件结构也是软件模型,只不过,它不是整体上的模型,而是展开实现细节之后的模型。模型是分层的。 - -对每个软件,当你从整体去了解它时,它是完整的一块。但当你打开它的时候,就成了多模块组合,这也是“分层”意义。上一层只要使用下一层提供给它的接口。 - -所以,当打开一个层次,了解其实现时,先从大处着手。最好找到一张结构图,准确了解它的结构。 - -如果你能够找到这样一张图,你还是很幸运的。因为在真实的项目中,你可能会碰到各种可能性: -- 结构图混乱:你找到一张图,上面包含了各种内容。比如,有的是模块设计,有的是具体实现,更有甚者,还包括了一些流程 -- 结构图复杂:一个比较成熟的项目,图上画了太多的内容。确实,随着项目的发展,软件解决的问题越来越多,它必然包含了更多的模块。但对于初次接触这个项目的我们而言,它就过于复杂了 -- 无结构图 -想办法画一张 - -先了解模型和接口,因为它们永远是你的主线。 - -假设:现在你有了一张结构图,你打算做什么? -了解它的结构?是,但不够。不仅要知道一个设计的结果,最好还要推断出设计原因。 - -所以,一种更好的做法:带问题上路。 -假设自己就是这个软件设计者,问问自己要怎么做。再去对比别人的设计,你就会发现,自己的想法和别人想法的相同或不同。 - -让你来设计MQ,你会怎么做? -Kafka网上能搜到各种架构图,看个 最简单的架构图,因为最贴近MQ基础模型: -![](https://img-blog.csdnimg.cn/5161ae8c5cb24c25be134cf74a5ecfe6.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -你能看到什么? -- Kafka的生产者一端将消息发给Kafka集群 -- 消费者一端将消息取出来进行处理 - -这样的结构和你想的是不是一样? - -> 进一步设计,会干啥? - -- 生产者端封装出一个 SDK,负责消息的发送 -- 消费者端封装出一个 SDK,负责消息的接收 -- 设计一个集群系统,作为生产者和消费者之间的连接 - -可以问自己更多的问题: -生产端如果出现网络抖动,消息没有成功发送,怎么重试? -消费端处理完的消息,如何保证集群不重复发送? -为什么要设计一个集群呢?要防止出现单点的故障,而一旦有了集群,就会牵扯到下一个问题,集群内的节点如何保证消息的同步呢? -消息在集群里是怎么存储的? -生产端也好,消费端也罢,如果一个节点彻底掉线,集群该怎么处理呢? -…… - -有了更多问题,你就会在代码里更深入探索。你可根据需要,打开对应模块,进一步了解实现: -比如消息重发问题,可看生产端是如何解决的。当问题细化到具体实现时,就可以打开源码,去寻找答案。 - -结构上,Kafka不是一个特复杂系统。所以,若你的项目更复杂,层次更多,推荐把各层次逐一展开,先把整体结构放在心中,再做细节探索。 -# 核心技术 -就是能够让这个软件的“实现”与众不同的地方。 -了解关键技术可保证我们对代码的调整不会使项目出现明显劣化。 -大多数项目都愿意把自己的关键技术讲出来,所以,找到这些信息不难。 - -## Kafka -对写入做了专门优化,使其整体吞吐能力很强。 - -> 咋做到的? - -MQ实现消息存储的方式通常是把它写入磁盘,而Kafka不同之处在于,它利用磁盘顺序读写特性。 -普通机械硬盘: -- 随机写,需按机械硬盘方式寻址,然后磁头做机械运动,写入很慢 -- 顺序写,会大幅减少磁头运动 - -可以这样实现,也是充分利用MQ本身特性:有序,**技术实现与需求完美结合的产物**。还可进一步优化:利用内存映射文件减少用户空间到内核空间复制的开销。 - -若站在了解实现的角度,你会觉得这都很自然。 - -> 但要想从设计角度学到更多,还是应带着问题上路,多问自己,为什么其它MQ不这么做? - -这的确值得深思。Kafka这个实现到底是哪里不容易想到呢? -**软硬结合。** - -其它MQ实现也会把消息写入文件,但文件对于它们只是个通用接口。开发者并没有想过利用硬件的特性做开发。而Kafka开发者突破此限制,把硬件特性利用起来,取得更好结果。 - -## LMAX Disruptor -最强劲的线程通信库。经典代码片段: -![](https://img-blog.csdnimg.cn/8131301192d441f48a69d9283e325e4b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -要理解这段代码,必须理解CPU缓存行,这也是软硬结合的设计。 -Disruptor缓存行填充中的填充字段。Disruptor中的一个元素是个volatile long类型,占用8字节。 -一但一个元素被修改,则与其在同一缓存行的所有元素的缓存都会失效。这就导致变更索引位1的元素,会导致索引位0的元素缓存也失效(操作时需重新从主内存加载)。 -所以,Disruptor做了一个缓存行填充的优化,在目标元素的前后都加7个类型字段,两边都占据掉56个字节。故而保证每个元素都独占缓存行。是一种用空间换时间的思想。 -# 总结 -理解一个实现,是以对模型和接口的理解为前提。 -如果想了解一个系统的实现,应从软件结构和关键技术两个方面着手。无论是软件结构,还是关键技术,我们都需要带着自己的问题入手,而问题的出发点就是我们对模型和接口的理解。 - -了解软件的结构,其实,就是把分层的模型展开,看下一层模型: -- 要知道这个层次给你提供了怎样的模型 -- 要带着自己的问题去了解这些模型为什么要这么设计 - -Kafka的实现主要是针对机械硬盘做的优化,现在的SSD硬盘越来越多,成本越来越低,这个立意的出发点已经不像以前那样稳固了。 - -软件的结构和核心技术应该分开,kafka之所以是: -- MQ,看的是对MQ这个模型结构的实现 -就没必看存储的实现,应该看路由信息管理、消息生产、消息消费等核心实现及其旁支功能的选择(限制消息大小、故障节点延后、延迟消费) -- kafka,看的是其消息存储核心技术实现 - -如果想知道kafka为什么在 MQ如此突出,那就得了解其核心技术实现,即这里的软硬结合的存储设计。 - -**理解实现,带着自己的问题,了解软件的结构和关键的技术。** -![](https://img-blog.csdnimg.cn/0a05699fafbb4c2a8b760a8ab3e4c975.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\351\235\242\345\220\221\345\257\271\350\261\241\347\274\226\347\250\213 V.S \345\207\275\346\225\260\345\274\217\347\274\226\347\250\213.md" "b/\351\207\215\346\236\204/\351\235\242\345\220\221\345\257\271\350\261\241\347\274\226\347\250\213 V.S \345\207\275\346\225\260\345\274\217\347\274\226\347\250\213.md" deleted file mode 100644 index 27778aba37..0000000000 --- "a/\351\207\215\346\236\204/\351\235\242\345\220\221\345\257\271\350\261\241\347\274\226\347\250\213 V.S \345\207\275\346\225\260\345\274\217\347\274\226\347\250\213.md" +++ /dev/null @@ -1,81 +0,0 @@ -# 面对不断增加的需求 -假设有一组学生: -![](https://img-blog.csdnimg.cn/b82c41eefdbc4916ab8530bec8f79068.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_13,color_FFFFFF,t_70,g_se,x_16) -若按姓名找出其中一个,你的代码可能如下: -![](https://img-blog.csdnimg.cn/d159c250c9754da6a5475aafe1737564.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_17,color_FFFFFF,t_70,g_se,x_16) -突然紧急需求来了,按学号找人,代码如下: -![](https://img-blog.csdnimg.cn/012914c7599f4366b1fe52624733efac.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_15,color_FFFFFF,t_70,g_se,x_16) -又一个新需求来了,这次按照ID 找人,代码可以如法炮制: -![](https://img-blog.csdnimg.cn/59b45b54cf2e480e9371e4311465d334.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_14,color_FFFFFF,t_70,g_se,x_16) -你发现,它们除查询条件不同,其余基本一模一样,别忘了代码结构重复也是代码重复! - -> 如何消除重复呢? - -引入查询条件,这里只需要返回一个bool值,可这样定义: -![](https://img-blog.csdnimg.cn/44989bcbe1a84c02bcafa36df8e1fad8.png) -通过查询条件,改造查询方法,把条件作为参数传入: - -于是,按名字查找变成: -![](https://img-blog.csdnimg.cn/79abe26749554583adcb9e4fed93abd6.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_18,color_FFFFFF,t_70,g_se,x_16) -已经很好了,但你发现,每有一个新查询,都要做一层封装。 - -> 如何才能省去这层封装? - -可将查询条件做成一个方法: -![](https://img-blog.csdnimg.cn/9b69ac1bdf9848bb957fdc12f86a77ee.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -其它字段也可做类似封装,如此,要查询什么就由使用方自行决定: -![](https://img-blog.csdnimg.cn/a34c2a3780b74569a58f2bf77085dc92.png) - - -现在想用名字和学号同时查询,咋办? -我猜你肯定要写一个byNameAndSno方法。若是如此,岂不是每种组合你都要新写一个?。 -完全可以用已有的两个方法组合出一个新查询: -![](https://img-blog.csdnimg.cn/2621efeaeab94b4ca7321b93d1008fc1.png) - -> 这个神奇的and方法是如何实现的呢? - -按普通and逻辑写即可: -![](https://img-blog.csdnimg.cn/67fb3925fe104efdbf4f617c1efa0bf6.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -or和not同理,相信聪明如你也会实现。这样,使用方能够使用的查询条件完全可按需组合。 - -现在想找出所有指定年龄的人。写个byAge就很简单了。 -那找到所有人该怎么写? -![](https://img-blog.csdnimg.cn/29accac3556c4b2896a3bf403cda9139.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -- 要做什么动作(查询一个、所有) -- 用什么条件(名字、学号、ID、年龄等) - -就成了两个维度,使用方可按需组合。 - -同样都是常规Java代码,效果确很奇妙。这段代码: -- 作者只提供了各种基本元素(动作和条件) -- 用户可通过组合这些元素完成需求 - -这种做法完全不同于常规OO,其思想源自函数式编程。 -质变在于引入了Predicate,它就是个函数。 - -按“消除重复”这样一个简单目的,不断调整代码,就能写出这种函数式风格代码。 - -现在看看函数式编程到底是啥 -# 函数式编程 -一种编程范式,提供的编程元素就是函数。 -这个函数源于数学里的函数,因为它的起源是数学家Alonzo Church发明的Lambda演算(Lambda calculus,也写作 λ-calculus)。所以,Lambda这个词在函数式编程中经常出现,可简单理解成匿名函数。 - -和 Java的方法相比,它要规避状态和副作用,即同样输入一定会给出同样输出。 - -虽然函数式编程语言早就出现,但函数式编程概念却是John Backus在其1977 年图灵奖获奖的演讲上提出。 - -函数式编程第一个需要了解的概念就是函数。在函数式编程中,函数是一等公民(first-class citizen): -- 可按需创建 -- 可存储在数据结构中 -- 可以当作实参传给另一个函数 -- 可当作另一个函数的返回值 - -对象,是OOP语言的一等公民,它就满足上述所有条件。所以,即使语言没有这种一等公民的函数,也完全能模拟。之前就用Java对象模拟出一个函数Predicate。 - -随着函数式编程这几年蓬勃的发展,越来越多的“老”程序设计语言已经在新的版本中加入了对函数式编程的支持。所以,如果你用的是新版本,可以不必像我写得那么复杂。 - -比如,在Java里,Predicate是JDK自带的,and方法也不用自己写,加上Lambda语法简化代码: -![](https://img-blog.csdnimg.cn/cab94be419d24ede8ce83b515aa4ad27.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -按对象的理解方式,Predicate是个对象接口,但它可接受Lambda为其赋值。 -可将其理解成一个简化版匿名内部类。主要工作都是编译器帮助做了类型推演(Type Inference)。 \ No newline at end of file diff --git "a/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/kafka\351\253\230\346\200\247\350\203\275\346\200\273\347\273\223.md" "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/kafka\351\253\230\346\200\247\350\203\275\346\200\273\347\273\223.md" deleted file mode 100644 index f6a8ac9109..0000000000 --- "a/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/kafka\351\253\230\346\200\247\350\203\275\346\200\273\347\273\223.md" +++ /dev/null @@ -1,21 +0,0 @@ -kafka高性能原理 -============== -       最近翻了下kafka官方关于kafka设计的文档,面试上用,这里就总结下自己了解到的kafka设计上支持那么大吞吐量的原因 -消息传递及存储 ------------- -       从上层设计来说,kafka的生产者支持批量发送消息(可以设置发送的内容最大大小和最长等待时间)当这些批量的消息到达kafak的broker上后会通过硬盘的线性写操作将日志记录进硬盘,这种操作的速度是很快的(中间也涉及到操作系统的pagecache,kafka也可以设置这种缓存刷盘的频率比如:一秒刷一次,每条消息刷一次,按照操作系统的配置去刷),这个是说从生产者发送消息给broker很快,那么消费者消费速度呢?kafka在消费上使用pull的方式去主动向broker节点请求获取消息,而具体的offset是由消费者去指定的(这个offset其实broker上也有维护一份,但是我理解的是拉取offset的决定权是掌握在消费者手里的,只不过如果消费者挂了后,其他替代的消费者如何知道原来的offset呢,那就需要broker也存一份),Kafka底层是通过linux的sendfile函数直接将消息存储的消息内容转发到网络的socket buffer然后在copy到NIC buffer发送到网络上。这个用到的是零拷贝技术,而正常的情况是需要以下几步: - -> 1,从硬盘读取到pagecache -> 2,从pagecache读取到用户内存 -> 3,从用户内存读到socket buffer中 -> 4,从socket buffer读取到 NIC buffer中然后NIC自动硬件发送(这步是不需要耗费CPU时间的) ->>kafka使用零拷贝总共节省了从pagecache拷贝到用户内存和从用户内存拷贝到socket buffer的两次拷贝,节省了拷贝过程中用户态和心态的切换,同时因为网卡,显卡,声卡等支持了DMA也就是直接访问主内存而不需要经过CPU,那么网卡可以直接访问硬盘的pagecache而不需要在经过pagechche到socket buffer的这一步拷贝真正实现了零拷贝。 - - -      而kafka消费方式是通过消费者拉取的方式而消费者可以根据自己的消费速度批量拉取消息,消息又都是顺序读,所以kafka在发送消息给消费者的时候速度也很快。同时,kafka也支持数据的压缩,这种压缩的数据在生产者,broker,消费者都是一致的可以直接传输。 - -集群 ---------------- - -       说到大吞吐量必须也得涉及到kafka集群,现将Kafka集群我认为的重点知识记录如下: -       主要涉及两个方面吧,**一个是多boker节点,一个是主从复制**。Kafka使用多croker节点来进行负载均衡,而生产者按照topic发送消息到broker的规则可以选用轮询或者指定规则,消费者按照group进行消费,每个group中只会有一个消费者消费同一条消息,如果同一个group中有消费者挂了,那么这个消费者对应消费的broker也会分配到同一个group中的其他消费者上。但是如果broker挂了呢?这就需要用到kafka的主从节点设置了。其实broker的从节点数据同步方式跟普通的消费者没什么区别,而在同步数据的时候主节点会维护一套ISR节点群,在这个节点群的从节点,kafka认为他们的数据是比较完整的,如果主节点挂了之后,这些从节点的任意一台节点都可以替换主节点。那么怎么保证一个消息会被同步到从节点了呢,这个可以在生产者配置acks=0,1,-1来决定一条消息只有在收到多少个从节点的确认后才算真正的落地成功,当选择-1的时候那么在ISR集合中的所有节点都要收到这条消息并返回确认后,这条消息才算发送成功,这个时候延迟也会比较高,所以可以根据线上系统的特点来综合判断这个配置如何设置。 \ No newline at end of file diff --git "a/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\346\212\200\346\234\257\351\200\211\345\236\213.md" "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\346\212\200\346\234\257\351\200\211\345\236\213.md" new file mode 100644 index 0000000000..bbdc199101 --- /dev/null +++ "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\346\212\200\346\234\257\351\200\211\345\236\213.md" @@ -0,0 +1,88 @@ +## 3.1 缘何使用MQ +其实就是问问你 +- 消息队列都有哪些使用场景 +- 你项目里具体是什么场景 +- 说说你在这个场景里用消息队列搞什么 + +面试官问你这个问题,期望的一个回答是说,你有个什么业务场景,这个业务场景有个什么技术挑战,如果不用MQ可能会很麻烦,但是你现在用了MQ之后带给了你很多的好处 + +先说一下 +### 3.1.1 消息队列的常见使用场景 +其实场景有很多,但是比较核心的有3个:解耦、异步、削峰 + +#### 3.1.1.1 解耦 +![](https://img-blog.csdnimg.cn/20190516174449953.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +A系统发送个数据到BCD三个系统,接口调用发送 + +那如果E系统也要这个数据呢? +那如果C系统现在不需要了呢? +现在A系统又要发送第二种数据了呢? + +A系统负责人濒临崩溃中。。。再来点更加崩溃的事儿,A系统要时时刻刻考虑BCDE四个系统如果挂了咋办?我要不要重发?我要不要把消息存起来?头都秃了啊!!! + +##### 面试技巧 +你需要去考虑一下你负责的系统中是否有类似的场景,就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。 +但是其实这个调用是不需要直接同步调用接口的,如果用MQ给他异步化解耦,也是可以的,你就需要去考虑在你的项目里,是不是可以运用这个MQ去进行系统的解耦。在简历中体现出来这块东西,用MQ作解耦。 + +#### 3.1.1.2 异步 +![](https://img-blog.csdnimg.cn/20190516175129333.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +- 使用MQ进行异步化之后的接口性能优化 +![在这里插入图片描述](https://img-blog.csdnimg.cn/2019051617523565.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +A系统接收一个请求,需要在自己本地写库,还需要在BCD三个系统写库 +自己本地写库要3ms,BCD三个系统分别写库要300ms、450ms、200ms +最终请求总延时是3 + 300 + 450 + 200 = 953ms,接近1s,用户感觉搞个什么东西,慢死了慢死了!!!卸载! +![](https://img-blog.csdnimg.cn/20190516175419852.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +![](https://img-blog.csdnimg.cn/20190516175503573.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +#### 3.1.1.3 削峰 +![](https://img-blog.csdnimg.cn/20190516175633796.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +每天0点到11点,A系统风平浪静,每秒并发请求数量就100个。结果每次一到11点~1点,每秒并发请求数量突然会暴增到1万条。但是系统最大的处理能力就只能是每秒钟处理1000个请求啊。。。尴尬了,系统会死。。。 +![](https://img-blog.csdnimg.cn/20190516175748284.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + + +## 3.2 MQ的优缺点 + +优点上面已经说了,就是在特殊场景下有其对应的好处,解耦、异步、削峰 + +缺点呢?显而易见的 + +### 3.2.1 系统可用性降低 +![](https://img-blog.csdnimg.cn/20190516175809538.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +系统引入的外部依赖越多,越容易挂掉,本来你就是A系统调用BCD三个系统的接口就好了,人ABCD四个系统好好的,没啥问题 +你偏加个MQ进来,万一MQ挂了咋整?MQ挂了,整套系统崩溃了,你不就完了么。 + +### 3.2.2 系统复杂性提高 +硬生生加个MQ进来 +- 你怎么保证消息没有重复消费? +- 怎么处理消息丢失的情况? +- 怎么保证消息传递的顺序性? + +头大头大,问题一大堆,痛苦不已 + +一致性问题:A系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是BCD三个系统那里,BD两个系统写库成功了,结果C系统写库失败了,咋整?你这数据就不一致了。 + +所以MQ实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,最好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了10倍。但是关键时刻,用,还是得用的。。。 + +## 3.3 kafka、activemq、rabbitmq、rocketmq各自的优缺点 +常见的MQ其实就这几种,别的还有很多其他MQ,但是比较冷门的,那么就别多说了 + +作为一个码农,你起码得知道各种mq的优点和缺点吧,咱们来画个表格看看 +![](https://img-blog.csdnimg.cn/20190516180246505.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +综上所述,各种对比之后,倾向于是: + +一般的业务系统要引入MQ,最早大家都用ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了; + +后来大家开始用RabbitMQ,但是确实erlang语言阻止了大量的java工程师去深入研究和掌控他,对公司而言,几乎处于不可控的状态,但是确实人是开源的,比较稳定的支持,活跃度也高; + +不过现在确实越来越多的公司,会去用RocketMQ,确实很不错,但是我提醒一下自己想好社区万一突然黄掉的风险,对自己公司技术实力有绝对自信的,我推荐用RocketMQ,否则回去老老实实用RabbitMQ吧,人是活跃开源社区,绝对不会黄 + +所以中小型公司,技术实力较为一般,技术挑战不是特别高,用RabbitMQ是不错的选择;大型公司,基础架构研发实力较强,用RocketMQ是很好的选择 + +如果是大数据领域的实时计算、日志采集等场景,用Kafka是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范 + +- 参考 +《Java工程师面试突击》 \ No newline at end of file diff --git "a/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(12) - \346\200\273\347\273\223\346\266\210\346\201\257\351\230\237\345\210\227\347\233\270\345\205\263\351\227\256\351\242\230\347\232\204\351\235\242\350\257\225\346\212\200\345\267\247.md" "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(12) - \346\200\273\347\273\223\346\266\210\346\201\257\351\230\237\345\210\227\347\233\270\345\205\263\351\227\256\351\242\230\347\232\204\351\235\242\350\257\225\346\212\200\345\267\247.md" new file mode 100644 index 0000000000..476932a7f5 --- /dev/null +++ "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(12) - \346\200\273\347\273\223\346\266\210\346\201\257\351\230\237\345\210\227\347\233\270\345\205\263\351\227\256\351\242\230\347\232\204\351\235\242\350\257\225\346\212\200\345\267\247.md" @@ -0,0 +1,24 @@ + +一般而言,如果一个面试官水平还算不错,会沿着从浅入深的环节深入挖一个点。 + +其实按照这个思路可以一直问下去,除了这里的7个问题之外,甚至能挑着你熟悉的一个mq一直问到源码级别非常底层。 + +还可能会结合项目来仔细问,可能会先让你给我详细说说你的业务细节,然后将你的业务跟这些mq的问题场景结合起来,看看你每个细节是怎么处理的。 + +但是确实因为我们这个是面试突破型教程,不是什么kafka源码剖析课,也不是什么RocketMQ高并发架构项目实战课程,所以只能讲到这个程度。 + +所以我们这个课程只能让你从大面儿上,基本常见问题可以回答出来。基本上mq这块你能答到这个程度,你基本知识面儿是有了,但是深度是绝对没有的。所以如果一个面试官就问问这些问题,感觉你面儿上过的去了,那就恭喜你了。但是如果碰到我这种难缠的面试官,喜欢深挖底层,细扣项目细节的,那可能确实是不行的。 + +如果你碰到人家在7个问题之外还死扣着你问的,那你最好是认一下怂,就说你确实没研究那么深过,如果你面的就是个一般的职位,那可能就过去了。就我而言,如果招聘的就是个普通职位,而你能答到这个程度,那么就觉得说的过去了。毕竟说实话,相当大比例的程序员出去面java职位的时候,mq这块还回答不到这个程度呢。你能答好这些,至少比之前一无所知的你好了一些,也比很多没准备过的程序员都好了很多。 + +最后说一个技巧,要是确实碰一个面试官连这7个问题都没问满,只要他提到mq,你自己就和盘托出一整套的东西,你就说,mq你们之前遇到过什么问题,巴拉巴拉,你们的方案是什么,自己突出自己会的东西 + +# 参考 +《Java工程师面试突击第1季-中华石杉老师》 + + +# X 交流学习 +![](https://upload-images.jianshu.io/upload_images/16782311-8d7acde57fdce062.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## [Java交流群](https://jq.qq.com/?_wv=1027&k=5UB4P1T) +## [博客](https://blog.csdn.net/qq_33589510) +## [Github](https://github.com/Wasabi1234) \ No newline at end of file diff --git "a/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(13)\345\210\206\345\270\203\345\274\217\346\220\234\347\264\242\345\274\225\346\223\216\350\277\236\347\216\257\347\202\256.md" "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(13)\345\210\206\345\270\203\345\274\217\346\220\234\347\264\242\345\274\225\346\223\216\350\277\236\347\216\257\347\202\256.md" new file mode 100644 index 0000000000..3f36d55b7b --- /dev/null +++ "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(13)\345\210\206\345\270\203\345\274\217\346\220\234\347\264\242\345\274\225\346\223\216\350\277\236\347\216\257\347\202\256.md" @@ -0,0 +1,32 @@ +业内目前来说事实上的一个标准,就是分布式搜索引擎一般大家都用elasticsearch + + +# lucene +如果你确实真的不连lucene都不知道是什么?我觉得你确实不应该,lucene底层的原理是一个东西,叫做倒排索引。太基础了。 + +百度,搜索一下lucene入门,了解一下lucene是什么?倒排索引是什么?全文检索是什么?写个lucene的demo程序体验一把。 + +# elasticsearch + +百度,搜索一下:elasticsearch入门,初步至少知道es的一些基本概念,然后包括es的基本部署和基本的使用 + + +# 面试官可能会怎么问? + +(1)es的分布式架构原理能说一下么(es是如何实现分布式的啊)? + +(2)es写入数据的工作原理是什么啊?es查询数据的工作原理是什么啊? + +(3)es在数据量很大的情况下(数十亿级别)如何提高查询性能啊? + +(4)es生产集群的部署架构是什么?每个索引的数据量大概有多少?每个索引大概有多少个分片? + +# 参考 +《Java工程师面试突击第1季-中华石杉老师》 + + +# X 交流学习 +![](https://upload-images.jianshu.io/upload_images/16782311-8d7acde57fdce062.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## [Java交流群](https://jq.qq.com/?_wv=1027&k=5UB4P1T) +## [博客](https://blog.csdn.net/qq_33589510) +## [Github](https://github.com/Wasabi1234) \ No newline at end of file diff --git "a/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(23-8) - Redis\345\223\250\345\205\265\344\270\273\345\244\207\345\210\207\346\215\242\347\232\204\346\225\260\346\215\256\344\270\242\345\244\261\351\227\256\351\242\230.md" "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(23-8) - Redis\345\223\250\345\205\265\344\270\273\345\244\207\345\210\207\346\215\242\347\232\204\346\225\260\346\215\256\344\270\242\345\244\261\351\227\256\351\242\230.md" new file mode 100644 index 0000000000..e86a788c0e --- /dev/null +++ "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(23-8) - Redis\345\223\250\345\205\265\344\270\273\345\244\207\345\210\207\346\215\242\347\232\204\346\225\260\346\215\256\344\270\242\345\244\261\351\227\256\351\242\230.md" @@ -0,0 +1,60 @@ +# 1 数据丢失的两个场景 + +主备切换的过程,可能会导致数据丢失 + +## 1.1 异步复制 + +由于 `master => slave`的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机,于是这些数据就丢失了 + +![](https://ask.qcloudimg.com/http-save/1752328/93jl4vswgy.png) + +## 1.2 脑裂导致 + +脑裂,也就是说,某个master所在节点突然脱离正常的网络,无法和其他slave机器连接,但实际上master还运行着 + +此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master + +这个时候,集群里就会有两个master,也就是所谓的`脑裂` + +此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了 + +因此旧master再次恢复时,会被作为一个slave挂到新的master上去,自己的数据会被清空,重新从新的master复制数据 + +![](https://ask.qcloudimg.com/http-save/1752328/q5320luqi1.png) + +# 2 数据丢失的解决方案 + +如下配置可以减少异步复制和脑裂导致的数据丢失 + +``` +min-slaves-to-write 1 +min-slaves-max-lag 10 +``` + +配置要求至少有1个slave,数据复制和同步的延迟不能超过10秒 + +一旦所有的slave,数据复制和同步的延迟都超过了10秒钟,master就不再接收任何请求! + +## 2.1 异步复制数据丢失解决方案 + +`min-slaves-max-lag` 配置 + +即可确保,一旦slave复制数据和ack延时过长,就认为可能master宕机后损失的数据太多了,那么就拒绝写请求 + +这样就可把master宕机时由于部分数据未同步到slave导致的数据丢失降低在可控范围 + +![](https://ask.qcloudimg.com/http-save/1752328/znnqfrs21u.png) + +## 2.2 脑裂数据丢失解决方案 + +若一个master出现了脑裂,跟其他slave失去连接,那么开始的两个配置可以确保 + +若不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求 + +这样脑裂后的旧master就不会接受client的新数据,也就避免了数据丢失 + +上面的配置就确保了,如果跟任何一个slave丢了连接,在10秒后发现没有slave给自己ack,那么就拒绝新的写请求 + +因此在脑裂场景下,最多就丢失10秒的数据 + +![](https://ask.qcloudimg.com/http-save/1752328/aamxilr8we.png) \ No newline at end of file diff --git "a/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(27) - \345\246\202\344\275\225\344\277\235\350\257\201\347\274\223\345\255\230\344\270\216\346\225\260\346\215\256\345\272\223\347\232\204\346\225\260\346\215\256\344\270\200\350\207\264\346\200\247.md" "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(27) - \345\246\202\344\275\225\344\277\235\350\257\201\347\274\223\345\255\230\344\270\216\346\225\260\346\215\256\345\272\223\347\232\204\346\225\260\346\215\256\344\270\200\350\207\264\346\200\247.md" new file mode 100644 index 0000000000..6e424918d5 --- /dev/null +++ "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(27) - \345\246\202\344\275\225\344\277\235\350\257\201\347\274\223\345\255\230\344\270\216\346\225\260\346\215\256\345\272\223\347\232\204\346\225\260\346\215\256\344\270\200\350\207\264\346\200\247.md" @@ -0,0 +1,216 @@ +# 1 面试题 + +如何保证缓存与数据库的双写一致性? + +# 2 考点分析 + +你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题? + +# 3 详解 + +一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案 + +读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况 + +串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。 + +## 3.1 Cache Aside Pattern缓存+数据库读写模式的分析 + +> 最经典的缓存+数据库读写的模式 cache aside pattern + +### 3.1.1 Cache Aside Pattern + +(1)读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应 + +(2)更新的时候,先删除缓存,然后再更新数据库 + +- cache aside pattern +![](https://ask.qcloudimg.com/http-save/1752328/nlcbi9k7yy.png)3.1.2 为什么是删除缓存,而不是更新缓存呢?很多时候,复杂点的缓存的场景,因为缓存有的时候,不单是数据库中直接取出来的值 + +商品详情页的系统,修改库存,只是修改了某个表的某些字段,但是要真正把这个影响的最终的库存计算出来,可能还需要从其他表查询一些数据,然后进行一些复杂的运算,才能最终计算出 + +现在最新的库存是多少,然后才能将库存更新到缓存中去 + +比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据,并运算,才能计算出缓存最新的值的 + +> 更新缓存的代价是很高的 + +是不是说,每次修改数据库的时候,都一定要将其对应的缓存去更新一份? + +也许有的场景是这样的,但是对于比较复杂的缓存数据计算的场景,就不是这样了 + +如果你频繁修改一个缓存涉及的多个表,那么这个缓存会被频繁的更新,频繁的更新缓存 + +但是问题在于,这个缓存到底会不会被频繁访问到??? + +举个例子,一个缓存涉及的表的字段,在1分钟内就修改了20次,或者是100次,那么缓存更新20次,100次; 但是这个缓存在1分钟内就被读取了1次,有大量的冷数据 + +> 28法则,黄金法则,20%的数据,占用了80%的访问量 + +实际上,如果你只是删除缓存的话,那么1分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低 + +每次数据过来,就只是删除缓存,然后修改数据库,如果这个缓存,在1分钟内只是被访问了1次,那么只有那1次,缓存是要被重新计算的,用缓存才去算缓存 + +其实删除缓存,而不是更新缓存,就是一个惰性延迟计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算 + +mybatis,hibernate,懒加载,思想 + +查询一个部门,部门带了一个员工的list,没有必要说每次查询部门,都里面的1000个员工的数据也同时查出来啊 + +80%的情况,查这个部门,就只是要访问这个部门的信息就可以了 + +先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询1000个员工 + +更多缓存设计模式请阅读 + +[大行缓存更新之道.md](https://github.com/Wasabi1234/JavaEdge/blob/master/%25E6%2595%25B0%25E6%258D%25AE%25E5%25BA%2593/%25E5%25A4%25A7%25E8%25A1%258C%25E7%25BC%2593%25E5%25AD%2598%25E6%259B%25B4%25E6%2596%25B0%25E4%25B9%258B%25E9%2581%2593.md) + +## 3.2 高并发场景下的缓存+数据库双写不一致问题分析与解决方案设计 + +开发业务系统 + +从哪一步开始做,从比较简单的那块开始做,实时性要求比较高的那块数据的缓存去做 + +> 实时性比较高的数据缓存,就是库存的服务 + +库存可能会修改,每次修改都要去更新这个缓存数据; 每次库存的数据,在缓存中一旦过期,或者是被清理掉了,前端的nginx服务都会发送请求给库存服务,去获取相应的数据 + +库存这一块,写数据库的时候,直接更新redis缓存 + +实际上没有这么的简单,这里,其实就涉及到了一个问题 + +### 数据库与缓存双写,数据不一致的问题 + +围绕和结合实时性较高的库存服务,把数据库与缓存双写不一致问题以及其解决方案,给大家讲解一下 + +数据库与缓存双写不一致,很常见的问题,大型的缓存架构中,第一个解决方案 + +也可能说,有些方案只是适合某些场景,在某些场景下,可能需要你进行方案的优化和调整才能适用于你自己的项目 + +### 3.2.1 最初级的缓存不一致问题以及解决方案 + +Q:先修改数据库,再删除缓存,如果缓存删除失败,那么会导致数据库中是新数据,缓存中是旧数据,数据出现不一致! + +- 最初级的数据库+缓存双写不一致问题 +![](https://ask.qcloudimg.com/http-save/1752328/pin0wcbj7t.png) + +A:先删除缓存,再修改数据库,如果删除缓存成功了,如果修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致 + +因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中 + +### 3.2.2 比较复杂的数据不一致问题分析 + +数据发生了变更,先删除了缓存,然后要去修改数据库,`此时还没修改` + +一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中 + +数据变更的程序完成了数据库的修改 + +完了,数据库和缓存中的数据不一样了。。。。 + +![](https://ask.qcloudimg.com/http-save/1752328/838xpfw8gg.png) + +### 3.2.3 为什么上亿流量高并发场景下,缓存会出现这个问题? + +只有在对一个数据在并发读写时,才可能会出现这种问题 + +其实如果说你的并发量很低的话,特别是读很低,每天访问量就1万次,那么很少会出现刚才描述的那种不一致的场景 + +但问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况 + +高并发了以后,问题是很多的 + +### 3.2.4 数据库 & 缓存更新与读取 异步串行化 + +更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个JVM内部的队列中 + +读数据的时候,如果发现数据不在缓存中,那么将重读数据+更新缓存,根据唯一标识路由之后,也发送同一个JVM内部的队列中 + +`一个队列对应一个工作线程` + +每个工作线程串行拿到对应的操作,然后一条一条的执行 + +这样的话,一个数据变更的操作,先执行删除缓存,然后再更新数据库,但是还没完成更新 + +此时如果一个读请求过来,读到了空缓存,则可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成 + +> 这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可 + +待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中 + +- 如果请求还在等待时间范围内,轮询发现可以取到值了,那么就直接返回 +- 如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值 + +### 3.2.5 高并发的场景下,该解决方案要注意的问题 + +#### (1)读请求长时阻塞 + +由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回 + +该解决方案,最大的风险点在于,可能数据更新很频繁,导致队列中积压了大量更新操作,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库 + +务必通过一些模拟真实的测试,看看更新数据的频繁是怎样的 + +另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作 + +如果一个内存队列里居然会挤压100个商品的库存修改操作,每隔库存修改操作要耗费10ms区完成,那么最后一个商品的读请求,可能等待10 \* 100 = 1000ms = 1s后,才能得到数据 + +这个时候就导致读请求的长时阻塞 + +一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会hang多少时间,如果读请求在200ms返回,如果你计算过后,哪怕是最繁忙的时候,积压10个更新操作,最多等待200ms,那还可以的 + +如果一个内存队列可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少 + +其实根据之前的项目经验,一般来说数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的 + +针对读高并发,读缓存架构的项目,一般写请求相对读来说,是非常非常少的,每秒的QPS能到几百就不错了 + +一秒,500的写操作,5份,每200ms,就100个写操作 + +单机器,20个内存队列,每个内存队列,可能就积压5个写操作,每个写操作性能测试后,一般在20ms左右就完成 + +那么针对每个内存队列中的数据的读请求,也就最多hang一会儿,200ms以内肯定能返回了 + +写QPS扩大10倍,但是经过刚才的测算,就知道,单机支撑写QPS几百没问题,那么就扩容机器,扩容10倍的机器,10台机器,每个机器20个队列,200个队列 + +大部分的情况下,应该是这样的,大量的读请求过来,都是直接走缓存取到数据的 + +少量情况下,可能遇到读跟数据更新冲突的情况,如上所述,那么此时更新操作如果先入队列,之后可能会瞬间来了对这个数据大量的读请求,但是因为做了去重的优化,所以也就一个更新缓存的操作跟在它后面 + +等数据更新完了,读请求触发的缓存更新操作也完成,然后临时等待的读请求全部可以读到缓存中的数据 + +#### (2)读请求并发量过高 + +这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时hang在服务上,看服务能不能抗的住,需要多少机器才能抗住最大的极限情况的峰值 + +但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大 + +按1:99的比例计算读和写的请求,每秒5万的读QPS,可能只有500次更新操作 + +如果一秒有500的写QPS,那么要测算好,可能写操作影响的数据有500条,这500条数据在缓存中失效后,可能导致多少读请求,发送读请求到库存服务来,要求更新缓存 + +一般来说,1:1,1:2,1:3,每秒钟有1000个读请求,会hang在库存服务上,每个读请求最多hang多少时间,200ms就会返回 + +在同一时间最多hang住的可能也就是单机200个读请求,同时hang住 + +单机hang200个读请求,还是ok的 + +1:20,每秒更新500条数据,这500秒数据对应的读请求,会有20 \* 500 = 1万 + +1万个读请求全部hang在库存服务上,就死定了 + +#### (3)多服务实例部署的请求路由 + +可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过nginx服务器路由到相同的服务实例上 + +#### (4)热点商品的路由问题,导致请求的倾斜 + +万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能造成某台机器的压力过大 + +就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以更新频率不是太高的话,这个问题的影响并不是特别大 + +但是的确可能某些机器的负载会高一些 + +# 参考 + +《Java工程师面试突击第1季-中华石杉老师》 \ No newline at end of file diff --git "a/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(28) - \345\246\202\344\275\225\350\247\243\345\206\263Redis\347\232\204\345\271\266\345\217\221\347\253\236\344\272\211\351\227\256\351\242\230.md" "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(28) - \345\246\202\344\275\225\350\247\243\345\206\263Redis\347\232\204\345\271\266\345\217\221\347\253\236\344\272\211\351\227\256\351\242\230.md" new file mode 100644 index 0000000000..097a0ca853 --- /dev/null +++ "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(28) - \345\246\202\344\275\225\350\247\243\345\206\263Redis\347\232\204\345\271\266\345\217\221\347\253\236\344\272\211\351\227\256\351\242\230.md" @@ -0,0 +1,24 @@ + +# 1 面试题 +redis的并发竞争问题是什么?如何解决这个问题?了解Redis事务的CAS方案吗? + +# 2 考点分析 +这个也是线上非常常见的一个问题,就是多客户端同时并发写一个key,可能本来应该先到的数据后到了,导致数据版本错了。或者是多客户端同时获取一个key,修改值之后再写回去,只要顺序错了,数据就错了。 + +而且redis自己就有天然解决这个问题的CAS类的乐观锁方案 + +# 3 详解 +- redis并发竞争问题以及解决方案 +![](https://img-blog.csdnimg.cn/20190509175418361.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +# 参考 + +《Java工程师面试突击第1季-中华石杉老师》 + +# X 交流学习 +![](https://img-blog.csdnimg.cn/20190504005601174.jpg) +## [Java交流群](https://jq.qq.com/?_wv=1027&k=5UB4P1T) +## [博客](https://blog.csdn.net/qq_33589510) + +## [Github](https://github.com/Wasabi1234) + pFad - Phonifier reborn

    Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

    Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


    Alternative Proxies:

    Alternative Proxy

    pFad Proxy

    pFad v3 Proxy

    pFad v4 Proxy