Tuesday, March 29, 2022

SegmentFault 最新的文章

SegmentFault 最新的文章


【高并发】两种异步模型与深度解析Future接口

Posted: 25 Mar 2022 01:27 AM PDT

大家好,我是冰河~~

本文有点长,但是满满的干货,以实际案例的形式分析了两种异步模型,并从源码角度深度解析Future接口和FutureTask类,希望大家踏下心来,打开你的IDE,跟着文章看源码,相信你一定收获不小!

一、两种异步模型

在Java的并发编程中,大体上会分为两种异步编程模型,一类是直接以异步的形式来并行运行其他的任务,不需要返回任务的结果数据。一类是以异步的形式运行其他任务,需要返回结果。

1.无返回结果的异步模型

无返回结果的异步任务,可以直接将任务丢进线程或线程池中运行,此时,无法直接获得任务的执行结果数据,一种方式是可以使用回调方法来获取任务的运行结果。

具体的方案是:定义一个回调接口,并在接口中定义接收任务结果数据的方法,具体逻辑在回调接口的实现类中完成。将回调接口与任务参数一同放进线程或线程池中运行,任务运行后调用接口方法,执行回调接口实现类中的逻辑来处理结果数据。这里,给出一个简单的示例供参考。

  • 定义回调接口
package io.binghe.concurrent.lab04;  /**  * @author binghe  * @version 1.0.0  * @description 定义回调接口  */ public interface TaskCallable<T> {     T callable(T t); }

便于接口的通用型,这里为回调接口定义了泛型。

  • 定义任务结果数据的封装类
package io.binghe.concurrent.lab04;  import java.io.Serializable;  /**  * @author binghe  * @version 1.0.0  * @description 任务执行结果  */ public class TaskResult implements Serializable {     private static final long serialVersionUID = 8678277072402730062L;     /**      * 任务状态      */     private Integer taskStatus;      /**      * 任务消息      */     private String taskMessage;      /**      * 任务结果数据      */     private String taskResult;          //省略getter和setter方法     @Override     public String toString() {         return "TaskResult{" +                 "taskStatus=" + taskStatus +                 ", taskMessage='" + taskMessage + '\'' +                 ", taskResult='" + taskResult + '\'' +                 '}';     } }
  • 创建回调接口的实现类

回调接口的实现类主要用来对任务的返回结果进行相应的业务处理,这里,为了方便演示,只是将结果数据返回。大家需要根据具体的业务场景来做相应的分析和处理。

package io.binghe.concurrent.lab04;  /**  * @author binghe  * @version 1.0.0  * @description 回调函数的实现类  */ public class TaskHandler implements TaskCallable<TaskResult> {     @Override public TaskResult callable(TaskResult taskResult) { //TODO 拿到结果数据后进一步处理     System.out.println(taskResult.toString());         return taskResult;     } }
  • 创建任务的执行类

任务的执行类是具体执行任务的类,实现Runnable接口,在此类中定义一个回调接口类型的成员变量和一个String类型的任务参数(模拟任务的参数),并在构造方法中注入回调接口和任务参数。在run方法中执行任务,任务完成后将任务的结果数据封装成TaskResult对象,调用回调接口的方法将TaskResult对象传递到回调方法中。

package io.binghe.concurrent.lab04;  /**  * @author binghe  * @version 1.0.0  * @description 任务执行类  */ public class TaskExecutor implements Runnable{     private TaskCallable<TaskResult> taskCallable;     private String taskParameter;      public TaskExecutor(TaskCallable<TaskResult> taskCallable, String taskParameter){         this.taskCallable = taskCallable;         this.taskParameter = taskParameter;     }      @Override     public void run() {         //TODO 一系列业务逻辑,将结果数据封装成TaskResult对象并返回         TaskResult result = new TaskResult();         result.setTaskStatus(1);         result.setTaskMessage(this.taskParameter);         result.setTaskResult("异步回调成功");         taskCallable.callable(result);     } }

到这里,整个大的框架算是完成了,接下来,就是测试看能否获取到异步任务的结果了。

  • 异步任务测试类
package io.binghe.concurrent.lab04;  /**  * @author binghe  * @version 1.0.0  * @description 测试回调  */ public class TaskCallableTest {     public static void main(String[] args){         TaskCallable<TaskResult> taskCallable = new TaskHandler();         TaskExecutor taskExecutor = new TaskExecutor(taskCallable, "测试回调任务");         new Thread(taskExecutor).start();     } }

在测试类中,使用Thread类创建一个新的线程,并启动线程运行任务。运行程序最终的接口数据如下所示。

TaskResult{taskStatus=1, taskMessage='测试回调任务', taskResult='异步回调成功'}

大家可以细细品味下这种获取异步结果的方式。这里,只是简单的使用了Thread类来创建并启动线程,也可以使用线程池的方式实现。大家可自行实现以线程池的方式通过回调接口获取异步结果。

2.有返回结果的异步模型

尽管使用回调接口能够获取异步任务的结果,但是这种方式使用起来略显复杂。在JDK中提供了可以直接返回异步结果的处理方案。最常用的就是使用Future接口或者其实现类FutureTask来接收任务的返回结果。

  • 使用Future接口获取异步结果

使用Future接口往往配合线程池来获取异步执行结果,如下所示。

package io.binghe.concurrent.lab04;  import java.util.concurrent.*;  /**  * @author binghe  * @version 1.0.0  * @description 测试Future获取异步结果  */ public class FutureTest {      public static void main(String[] args) throws ExecutionException, InterruptedException {         ExecutorService executorService = Executors.newSingleThreadExecutor();         Future<String> future = executorService.submit(new Callable<String>() {             @Override             public String call() throws Exception {                 return "测试Future获取异步结果";             }         });         System.out.println(future.get());         executorService.shutdown();     } }

运行结果如下所示。

测试Future获取异步结果
  • 使用FutureTask类获取异步结果

FutureTask类既可以结合Thread类使用也可以结合线程池使用,接下来,就看下这两种使用方式。

结合Thread类的使用示例如下所示。

package io.binghe.concurrent.lab04;  import java.util.concurrent.*;  /**  * @author binghe  * @version 1.0.0  * @description 测试FutureTask获取异步结果  */ public class FutureTaskTest {      public static void main(String[] args)throws ExecutionException, InterruptedException{         FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {             @Override             public String call() throws Exception {                 return "测试FutureTask获取异步结果";             }         });         new Thread(futureTask).start();         System.out.println(futureTask.get());     } }

运行结果如下所示。

测试FutureTask获取异步结果

结合线程池的使用示例如下。

package io.binghe.concurrent.lab04;  import java.util.concurrent.*;  /**  * @author binghe  * @version 1.0.0  * @description 测试FutureTask获取异步结果  */ public class FutureTaskTest {      public static void main(String[] args) throws ExecutionException, InterruptedException {         ExecutorService executorService = Executors.newSingleThreadExecutor();         FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {             @Override             public String call() throws Exception {                 return "测试FutureTask获取异步结果";             }         });         executorService.execute(futureTask);         System.out.println(futureTask.get());         executorService.shutdown();     } }

运行结果如下所示。

测试FutureTask获取异步结果

可以看到使用Future接口或者FutureTask类来获取异步结果比使用回调接口获取异步结果简单多了。注意:实现异步的方式很多,这里只是用多线程举例。

接下来,就深入分析下Future接口。

二、深度解析Future接口

1.Future接口

Future是JDK1.5新增的异步编程接口,其源代码如下所示。

package java.util.concurrent;  public interface Future<V> {      boolean cancel(boolean mayInterruptIfRunning);      boolean isCancelled();      boolean isDone();      V get() throws InterruptedException, ExecutionException;      V get(long timeout, TimeUnit unit)         throws InterruptedException, ExecutionException, TimeoutException; }

可以看到,在Future接口中,总共定义了5个抽象方法。接下来,就分别介绍下这5个方法的含义。

  • cancel(boolean)

取消任务的执行,接收一个boolean类型的参数,成功取消任务,则返回true,否则返回false。当任务已经完成,已经结束或者因其他原因不能取消时,方法会返回false,表示任务取消失败。当任务未启动调用了此方法,并且结果返回true(取消成功),则当前任务不再运行。如果任务已经启动,会根据当前传递的boolean类型的参数来决定是否中断当前运行的线程来取消当前运行的任务。

  • isCancelled()

判断任务在完成之前是否被取消,如果在任务完成之前被取消,则返回true;否则,返回false。

这里需要注意一个细节:只有任务未启动,或者在完成之前被取消,才会返回true,表示任务已经被成功取消。其他情况都会返回false。

  • isDone()

判断任务是否已经完成,如果任务正常结束、抛出异常退出、被取消,都会返回true,表示任务已经完成。

  • get()

当任务完成时,直接返回任务的结果数据;当任务未完成时,等待任务完成并返回任务的结果数据。

  • get(long, TimeUnit)

当任务完成时,直接返回任务的结果数据;当任务未完成时,等待任务完成,并设置了超时等待时间。在超时时间内任务完成,则返回结果;否则,抛出TimeoutException异常。

2.RunnableFuture接口

Future接口有一个重要的子接口,那就是RunnableFuture接口,RunnableFuture接口不但继承了Future接口,而且继承了java.lang.Runnable接口,其源代码如下所示。

package java.util.concurrent;  public interface RunnableFuture<V> extends Runnable, Future<V> {     void run(); }

这里,问一下,RunnableFuture接口中有几个抽象方法?想好了再说!哈哈哈。。。

这个接口比较简单run()方法就是运行任务时调用的方法。

3.FutureTask类

FutureTask类是RunnableFuture接口的一个非常重要的实现类,它实现了RunnableFuture接口、Future接口和Runnable接口的所有方法。FutureTask类的源代码比较多,这个就不粘贴了,大家自行到java.util.concurrent下查看。

(1)FutureTask类中的变量与常量

在FutureTask类中首先定义了一个状态变量state,这个变量使用了volatile关键字修饰,这里,大家只需要知道volatile关键字通过内存屏障和禁止重排序优化来实现线程安全,后续会单独深度分析volatile关键字是如何保证线程安全的。紧接着,定义了几个任务运行时的状态常量,如下所示。

private volatile int state; private static final int NEW          = 0; private static final int COMPLETING   = 1; private static final int NORMAL       = 2; private static final int EXCEPTIONAL  = 3; private static final int CANCELLED    = 4; private static final int INTERRUPTING = 5; private static final int INTERRUPTED  = 6;

其中,代码注释中给出了几个可能的状态变更流程,如下所示。

NEW -> COMPLETING -> NORMAL NEW -> COMPLETING -> EXCEPTIONAL NEW -> CANCELLED NEW -> INTERRUPTING -> INTERRUPTED

接下来,定义了其他几个成员变量,如下所示。

private Callable<V> callable; private Object outcome;  private volatile Thread runner; private volatile WaitNode waiters;

又看到我们所熟悉的Callable接口了,Callable接口那肯定就是用来调用call()方法执行具体任务了。

  • outcome:Object类型,表示通过get()方法获取到的结果数据或者异常信息。
  • runner:运行Callable的线程,运行期间会使用CAS保证线程安全,这里大家只需要知道CAS是Java保证线程安全的一种方式,后续文章中会深度分析CAS如何保证线程安全。
  • waiters:WaitNode类型的变量,表示等待线程的堆栈,在FutureTask的实现中,会通过CAS结合此堆栈交换任务的运行状态。

看一下WaitNode类的定义,如下所示。

static final class WaitNode {     volatile Thread thread;     volatile WaitNode next;     WaitNode() { thread = Thread.currentThread(); } }

可以看到,WaitNode类是FutureTask类的静态内部类,类中定义了一个Thread成员变量和指向下一个WaitNode节点的引用。其中通过构造方法将thread变量设置为当前线程。

(2)构造方法

接下来,是FutureTask的两个构造方法,比较简单,如下所示。

public FutureTask(Callable<V> callable) {     if (callable == null)         throw new NullPointerException();     this.callable = callable;     this.state = NEW; }  public FutureTask(Runnable runnable, V result) {     this.callable = Executors.callable(runnable, result);     this.state = NEW; }

(3)是否取消与完成方法

继续向下看源码,看到一个任务是否取消的方法,和一个任务是否完成的方法,如下所示。

public boolean isCancelled() {     return state >= CANCELLED; }  public boolean isDone() {     return state != NEW; }

这两方法中,都是通过判断任务的状态来判定任务是否已取消和已完成的。为啥会这样判断呢?再次查看FutureTask类中定义的状态常量发现,其常量的定义是有规律的,并不是随意定义的。其中,大于或者等于CANCELLED的常量为CANCELLED、INTERRUPTING和INTERRUPTED,这三个状态均可以表示线程已经被取消。当状态不等于NEW时,可以表示任务已经完成。

通过这里,大家可以学到一点:以后在编码过程中,要按照规律来定义自己使用的状态,尤其是涉及到业务中有频繁的状态变更的操作,有规律的状态可使业务处理变得事半功倍,这也是通过看别人的源码设计能够学到的,这里,建议大家还是多看别人写的优秀的开源框架的源码。

(4)取消方法

我们继续向下看源码,接下来,看到的是cancel(boolean)方法,如下所示。

public boolean cancel(boolean mayInterruptIfRunning) {     if (!(state == NEW &&           UNSAFE.compareAndSwapInt(this, stateOffset, NEW,               mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))         return false;     try {    // in case call to interrupt throws exception         if (mayInterruptIfRunning) {             try {                 Thread t = runner;                 if (t != null)                     t.interrupt();             } finally { // final state                 UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);             }         }     } finally {         finishCompletion();     }     return true; }

接下来,拆解cancel(boolean)方法。在cancel(boolean)方法中,首先判断任务的状态和CAS的操作结果,如果任务的状态不等于NEW或者CAS的操作返回false,则直接返回false,表示任务取消失败。如下所示。

if (!(state == NEW &&       UNSAFE.compareAndSwapInt(this, stateOffset, NEW,           mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))     return false;

接下来,在try代码块中,首先判断是否可以中断当前任务所在的线程来取消任务的运行。如果可以中断当前任务所在的线程,则以一个Thread临时变量来指向运行任务的线程,当指向的变量不为空时,调用线程对象的interrupt()方法来中断线程的运行,最后将线程标记为被中断的状态。如下所示。

try {     if (mayInterruptIfRunning) {         try {             Thread t = runner;             if (t != null)                 t.interrupt();         } finally { // final state             UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);         }     } }

这里,发现变更任务状态使用的是UNSAFE.putOrderedInt()方法,这个方法是个什么鬼呢?点进去看一下,如下所示。

public native void putOrderedInt(Object var1, long var2, int var4);

可以看到,又是一个本地方法,嘿嘿,这里先不管它,后续文章会详解这些方法的作用。

接下来,cancel(boolean)方法会进入finally代码块,如下所示。

finally {     finishCompletion(); }

可以看到在finallly代码块中调用了finishCompletion()方法,顾名思义,finishCompletion()方法表示结束任务的运行,接下来看看它是如何实现的。点到finishCompletion()方法中看一下,如下所示。

private void finishCompletion() {     // assert state > COMPLETING;     for (WaitNode q; (q = waiters) != null;) {         if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {             for (;;) {                 Thread t = q.thread;                 if (t != null) {                     q.thread = null;                     LockSupport.unpark(t);                 }                 WaitNode next = q.next;                 if (next == null)                     break;                 q.next = null; // unlink to help gc                 q = next;             }             break;         }     }     done();     callable = null;        // to reduce footprint }

在finishCompletion()方法中,首先定义一个for循环,循环终止因子为waiters为null,在循环中,判断CAS操作是否成功,如果成功进行if条件中的逻辑。首先,定义一个for自旋循环,在自旋循环体中,唤醒WaitNode堆栈中的线程,使其运行完成。当WaitNode堆栈中的线程运行完成后,通过break退出外层for循环。接下来调用done()方法。done()方法又是个什么鬼呢?点进去看一下,如下所示。

protected void done() { }

可以看到,done()方法是一个空的方法体,交由子类来实现具体的业务逻辑。

当我们的具体业务中,需要在取消任务时,执行一些额外的业务逻辑,可以在子类中覆写done()方法的实现。

(5)get()方法

继续向下看FutureTask类的代码,FutureTask类中实现了两个get()方法,如下所示。

public V get() throws InterruptedException, ExecutionException {     int s = state;     if (s <= COMPLETING)         s = awaitDone(false, 0L);     return report(s); }  public V get(long timeout, TimeUnit unit)     throws InterruptedException, ExecutionException, TimeoutException {     if (unit == null)         throw new NullPointerException();     int s = state;     if (s <= COMPLETING &&         (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)         throw new TimeoutException();     return report(s); }

没参数的get()方法为当任务未运行完成时,会阻塞,直到返回任务结果。有参数的get()方法为当任务未运行完成,并且等待时间超出了超时时间,会TimeoutException异常。

两个get()方法的主要逻辑差不多,一个没有超时设置,一个有超时设置,这里说一下主要逻辑。判断任务的当前状态是否小于或者等于COMPLETING,也就是说,任务是NEW状态或者COMPLETING,调用awaitDone()方法,看下awaitDone()方法的实现,如下所示。

private int awaitDone(boolean timed, long nanos)     throws InterruptedException {     final long deadline = timed ? System.nanoTime() + nanos : 0L;     WaitNode q = null;     boolean queued = false;     for (;;) {         if (Thread.interrupted()) {             removeWaiter(q);             throw new InterruptedException();         }          int s = state;         if (s > COMPLETING) {             if (q != null)                 q.thread = null;             return s;         }         else if (s == COMPLETING) // cannot time out yet             Thread.yield();         else if (q == null)             q = new WaitNode();         else if (!queued)             queued = UNSAFE.compareAndSwapObject(this, waitersOffset,                                                  q.next = waiters, q);         else if (timed) {             nanos = deadline - System.nanoTime();             if (nanos <= 0L) {                 removeWaiter(q);                 return state;             }             LockSupport.parkNanos(this, nanos);         }         else             LockSupport.park(this);     } }

接下来,拆解awaitDone()方法。在awaitDone()方法中,最重要的就是for自旋循环,在循环中首先判断当前线程是否被中断,如果已经被中断,则调用removeWaiter()将当前线程从堆栈中移除,并且抛出InterruptedException异常,如下所示。

if (Thread.interrupted()) {     removeWaiter(q);     throw new InterruptedException(); }

接下来,判断任务的当前状态是否完成,如果完成,并且堆栈句柄不为空,则将堆栈中的当前线程设置为空,返回当前任务的状态,如下所示。

int s = state; if (s > COMPLETING) {     if (q != null)         q.thread = null;     return s; }

当任务的状态为COMPLETING时,使当前线程让出CPU资源,如下所示。

else if (s == COMPLETING)     Thread.yield();

如果堆栈为空,则创建堆栈对象,如下所示。

else if (q == null)     q = new WaitNode();

如果queued变量为false,通过CAS操作为queued赋值,如果awaitDone()方法传递的timed参数为true,则计算超时时间,当时间已超时,则在堆栈中移除当前线程并返回任务状态,如下所示。如果未超时,则重置超时时间,如下所示。

else if (!queued)     queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q); else if (timed) {     nanos = deadline - System.nanoTime();     if (nanos <= 0L) {         removeWaiter(q);         return state;     }     LockSupport.parkNanos(this, nanos); }

如果不满足上述的所有条件,则将当前线程设置为等待状态,如下所示。

else     LockSupport.park(this);

接下来,回到get()方法中,当awaitDone()方法返回结果,或者任务的状态不满足条件时,都会调用report()方法,并将当前任务的状态传递到report()方法中,并返回结果,如下所示。

return report(s);

看来,这里还要看下report()方法啊,点进去看下report()方法的实现,如下所示。

private V report(int s) throws ExecutionException {     Object x = outcome;     if (s == NORMAL)         return (V)x;     if (s >= CANCELLED)         throw new CancellationException();     throw new ExecutionException((Throwable)x); }

可以看到,report()方法的实现比较简单,首先,将outcome数据赋值给x变量,接下来,主要是判断接收到的任务状态,如果状态为NORMAL,则将x强转为泛型类型返回;当任务的状态大于或者等于CANCELLED,也就是任务已经取消,则抛出CancellationException异常,其他情况则抛出ExecutionException异常。

至此,get()方法分析完成。注意:一定要理解get()方法的实现,因为get()方法是我们使用Future接口和FutureTask类时,使用的比较频繁的一个方法。

(6)set()方法与setException()方法

继续看FutureTask类的代码,接下来看到的是set()方法与setException()方法,如下所示。

protected void set(V v) {     if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {         outcome = v;         UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state         finishCompletion();     } }  protected void setException(Throwable t) {     if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {         outcome = t;         UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state         finishCompletion();     } }

通过源码可以看出,set()方法与setException()方法整体逻辑几乎一样,只是在设置任务状态时一个将状态设置为NORMAL,一个将状态设置为EXCEPTIONAL。

至于finishCompletion()方法,前面已经分析过。

(7)run()方法与runAndReset()方法

接下来,就是run()方法了,run()方法的源代码如下所示。

public void run() {     if (state != NEW ||         !UNSAFE.compareAndSwapObject(this, runnerOffset,                                      null, Thread.currentThread()))         return;     try {         Callable<V> c = callable;         if (c != null && state == NEW) {             V result;             boolean ran;             try {                 result = c.call();                 ran = true;             } catch (Throwable ex) {                 result = null;                 ran = false;                 setException(ex);             }             if (ran)                 set(result);         }     } finally {         // runner must be non-null until state is settled to         // prevent concurrent calls to run()         runner = null;         // state must be re-read after nulling runner to prevent         // leaked interrupts         int s = state;         if (s >= INTERRUPTING)             handlePossibleCancellationInterrupt(s);     } }

可以这么说,只要使用了Future和FutureTask,就必然会调用run()方法来运行任务,掌握run()方法的流程是非常有必要的。在run()方法中,如果当前状态不是NEW,或者CAS操作返回的结果为false,则直接返回,不再执行后续逻辑,如下所示。

if (state != NEW ||     !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))     return;

接下来,在try代码块中,将成员变量callable赋值给一个临时变量c,判断临时变量不等于null,并且任务状态为NEW,则调用Callable接口的call()方法,并接收结果数据。并将ran变量设置为true。当程序抛出异常时,将接收结果的变量设置为null,ran变量设置为false,并且调用setException()方法将任务的状态设置为EXCEPTIONA。接下来,如果ran变量为true,则调用set()方法,如下所示。

try {     Callable<V> c = callable;     if (c != null && state == NEW) {         V result;         boolean ran;         try {             result = c.call();             ran = true;         } catch (Throwable ex) {             result = null;             ran = false;             setException(ex);         }         if (ran)             set(result);     } }

接下来,程序会进入finally代码块中,如下所示。

finally {     // runner must be non-null until state is settled to     // prevent concurrent calls to run()     runner = null;     // state must be re-read after nulling runner to prevent     // leaked interrupts     int s = state;     if (s >= INTERRUPTING)         handlePossibleCancellationInterrupt(s); }

这里,将runner设置为null,如果任务的当前状态大于或者等于INTERRUPTING,也就是线程被中断了。则调用handlePossibleCancellationInterrupt()方法,接下来,看下handlePossibleCancellationInterrupt()方法的实现。

private void handlePossibleCancellationInterrupt(int s) {     if (s == INTERRUPTING)         while (state == INTERRUPTING)             Thread.yield(); }

可以看到,handlePossibleCancellationInterrupt()方法的实现比较简单,当任务的状态为INTERRUPTING时,使用while()循环,条件为当前任务状态为INTERRUPTING,将当前线程占用的CPU资源释放,也就是说,当任务运行完成后,释放线程所占用的资源。

runAndReset()方法的逻辑与run()差不多,只是runAndReset()方法会在finally代码块中将任务状态重置为NEW。runAndReset()方法的源代码如下所示,就不重复说明了。

protected boolean runAndReset() {     if (state != NEW ||         !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))         return false;     boolean ran = false;     int s = state;     try {         Callable<V> c = callable;         if (c != null && s == NEW) {             try {                 c.call(); // don't set result                 ran = true;             } catch (Throwable ex) {                 setException(ex);             }         }     } finally {         // runner must be non-null until state is settled to         // prevent concurrent calls to run()         runner = null;         // state must be re-read after nulling runner to prevent         // leaked interrupts         s = state;         if (s >= INTERRUPTING)             handlePossibleCancellationInterrupt(s);     }     return ran && s == NEW; }

(8)removeWaiter()方法

removeWaiter()方法中主要是使用自旋循环的方式来移除WaitNode中的线程,比较简单,如下所示。

private void removeWaiter(WaitNode node) {     if (node != null) {         node.thread = null;         retry:         for (;;) {          // restart on removeWaiter race             for (WaitNode pred = null, q = waiters, s; q != null; q = s) {                 s = q.next;                 if (q.thread != null)                     pred = q;                 else if (pred != null) {                     pred.next = s;                     if (pred.thread == null) // check for race                         continue retry;                 }                 else if (!UNSAFE.compareAndSwapObject(this, waitersOffset,                                                       q, s))                     continue retry;             }             break;         }     } }

最后,在FutureTask类的最后,有如下代码。

// Unsafe mechanics private static final sun.misc.Unsafe UNSAFE; private static final long stateOffset; private static final long runnerOffset; private static final long waitersOffset; static {     try {         UNSAFE = sun.misc.Unsafe.getUnsafe();         Class<?> k = FutureTask.class;         stateOffset = UNSAFE.objectFieldOffset             (k.getDeclaredField("state"));         runnerOffset = UNSAFE.objectFieldOffset             (k.getDeclaredField("runner"));         waitersOffset = UNSAFE.objectFieldOffset             (k.getDeclaredField("waiters"));     } catch (Exception e) {         throw new Error(e);     } }

关于这些代码的作用,会在后续深度解析CAS文章中详细说明,这里就不再探讨。

至此,关于Future接口和FutureTask类的源码就分析完了。

好了,今天就到这儿吧,我是冰河,我们下期见~~

xxl-job Vs ElasticJob,谁牛?

Posted: 28 Mar 2022 08:01 AM PDT

@[toc]
前两天写了一篇文章介绍了一下 ElasticJob,有不少小伙伴强烈建议讲讲 xxl-job,其实 ElasticJob 本来就是一个引子,松哥本来就是想和大家分享 xxl-job 的(手动狗头。

1. xxl-job

松哥也在微信群里和小伙伴们讨论过各自到底用的是 xxl-job 还是 ElasticJob,讨论的结果就是,xxl-job 使用的人更多一些。

不说功能的优劣,我们单纯从数据上其实就能看出一些端倪:

这是 xxl-job 的 GitHub:

这是 ElasticJob 的 GitHub:

从这个数据比较上大概也能看出来 xxl-job 更火一些。注意我这里说的是更火一些,不是说 xxl-job 比 ElasticJob 更强。

xxl-job 出自大众点评,这是一个分布式轻量级的任务调度框架,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。

xxl-job 通过一个中心式的调度平台,调度多个执行器执行任务,调度中心通过 DB 锁保证集群分布式调度的一致性,这样扩展执行器会增大 DB 的压力,然而大部分公司的任务数,执行器并不多;xxl-job 提供了非常好用的监控页面甚至还有任务失败的邮件告警功能。不同于 ElasticJob,xxl-job 在使用时依赖 MySQL,而不需要 ZooKeeper。

ElasticJob 则出自当当,设计 ElasticJob 的初衷是为了面对高并发以及复杂的业务,即使是在业务量大,服务器多的时候也能做好任务调度,尽可能的利用服务器的资源。ElasticJob 是无中心化的,如果主服务器挂了,会自动通过 ZooKeeper 的选举机制选举出新的主服务器。因此 ElasticJob 具有良好的扩展性和可用性。

所以,你打算用哪个?

2. 运行 xxl-job

我们先把 xxl-job 跑起来,再写我们自己的代码。

首先我们先把 xxl-job 的代码搞下来,地址:

然后用 IDEA 打开项目,打开之后,有四个主要的文件夹:

  • doc:项目文档
  • xxl-job-admin:任务调度平台
  • xxl-job-core:核心代码
  • xxl-job-executor-samples:案例

由于 xxl-job 运行需要数据库,所以接下来我们就来配置数据库,先找到数据库脚本,在 doc/db/tables_xxl_job.sql 位置。

找到数据库脚本后,导入到数据库中执行一下,执行完成后,生成如下库和表:

接下来找到 xxl-job-admin/src/main/resources/application.properties 文件,修改数据库连接信息:

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=123 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

日志配置也要修改一下,在 xxl-job-admin/src/main/resources/logback.xml 文件中,有如下一行:

<property name="log.path" value="/data/applogs/xxl-job/xxl-job-admin.log"/>

如果你是 Windows 操作系统,这里肯定要改,如果你是 Mac 的话,也有可能没有这个目录的权限,因此我建议大家改一下这里的配置,改为如下这样:

<property name="log.path" value="./applogs/xxl-job/xxl-job-admin.log"/>

改为在项目运行目录下生成这个日志文件。

修改完成后,接下来我们就可以启动 xxl-job-admin 项目了,这是一个 SpringBoot 项目,找到启动类,直接运行其 main 方法即可。

项目启动成功后,浏览器输入如下地址 http://localhost:8080/xxl-job-admin/toLogin,就可以看到登录页面了:

默认的登录账号是 admin/123456

看到如下页面,就是登录成功了。

3. 开发定时任务

3.1 项目创建及配置

接下来我们来创建一个项目,跑一个定时任务看看。

首先创建一个 SpringBoot 项目,引入 Web 依赖,如下:

项目创建成功后,引入 xxl-job 的依赖:

<dependency>     <groupId>com.xuxueli</groupId>     <artifactId>xxl-job-core</artifactId>     <version>2.3.0</version> </dependency>

然后在 resources 目录下加入 logback.xml,内容如下:

<?xml version="1.0" encoding="UTF-8"?> <configuration debug="false" scan="true" scanPeriod="1 seconds">      <contextName>logback</contextName>     <property name="log.path" value="./applogs/xxl-job/xxl-job-executor-sample-springboot.log"/>      <appender name="console" class="ch.qos.logback.core.ConsoleAppender">         <encoder>             <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>         </encoder>     </appender>      <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">         <file>${log.path}</file>         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">             <fileNamePattern>${log.path}.%d{yyyy-MM-dd}.zip</fileNamePattern>         </rollingPolicy>         <encoder>             <pattern>%date %level [%thread] %logger{36} [%file : %line] %msg%n             </pattern>         </encoder>     </appender>      <root level="info">         <appender-ref ref="console"/>         <appender-ref ref="file"/>     </root>  </configuration>

修改 application.properties 文件,内容如下:

# web port server.port=8089  # 日志配置 logging.config=classpath:logback.xml  # 配置调度中心地址 xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin  # 执行器和调度中心之间的通信令牌,如果没有配置,表示关闭了通信令牌的校验。 # 在 xxl-job-admin 的配置文件中,有一个一模一样的配置项,两边都配置,就会进行校验。 xxl.job.accessToken=  # 配置执行器的名字 xxl.job.executor.appname=xxl-job-demo # 执行器地址,如果没有配置,就使用 IP:PORT 作为默认值 xxl.job.executor.address= # 执行器 ip 地址 xxl.job.executor.ip= # 执行器端口,默认即 9999 xxl.job.executor.port=9999 # 执行器日志文件位置 xxl.job.executor.logpath=./applogs/xxl-job/jobhandler # 执行器日志保存时间 xxl.job.executor.logretentiondays=30

各项配置的含义我已经加了注释了。

接下来再提供一个配置类,如下:

@Configuration public class XxlJobConfig {     private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);      @Value("${xxl.job.admin.addresses}")     private String adminAddresses;      @Value("${xxl.job.accessToken}")     private String accessToken;      @Value("${xxl.job.executor.appname}")     private String appname;      @Value("${xxl.job.executor.address}")     private String address;      @Value("${xxl.job.executor.ip}")     private String ip;      @Value("${xxl.job.executor.port}")     private int port;      @Value("${xxl.job.executor.logpath}")     private String logPath;      @Value("${xxl.job.executor.logretentiondays}")     private int logRetentionDays;       @Bean     public XxlJobSpringExecutor xxlJobExecutor() {         logger.info(">>>>>>>>>>> xxl-job config init.");         XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();         xxlJobSpringExecutor.setAdminAddresses(adminAddresses);         xxlJobSpringExecutor.setAppname(appname);         xxlJobSpringExecutor.setAddress(address);         xxlJobSpringExecutor.setIp(ip);         xxlJobSpringExecutor.setPort(port);         xxlJobSpringExecutor.setAccessToken(accessToken);         xxlJobSpringExecutor.setLogPath(logPath);         xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);         return xxlJobSpringExecutor;     }  }

其实就是把刚刚 application.properties 中的属性都配置成一个 XxlJobSpringExecutor Bean,很奇怪官方为什么没把这个搞成一个自动化配置的 Bean。

接下来我们就可以创建一个具体的定时任务了。

3.2 定时任务开发方式

对于我们 Java 工程师而言,有三种开发定时任务的方式。

3.2.1 BEAN 模式(类形式)

Bean 模式任务,支持基于类的开发方式,每个任务对应一个 Java 类。

优点:不限制项目环境,兼容性好。即使是无框架项目,如 main 方法直接启动的项目也可以提供支持。

缺点:

  • 每个任务需要占用一个Java类,造成类的浪费;
  • 不支持自动扫描任务并注入到执行器容器,需要手动注入。

开发方式:

  1. 开发一个继承自 com.xxl.job.core.handler.IJobHandler 的 JobHandler 类,实现其中任务方法。
  2. 手动通过如下方式注入到执行器容器:
XxlJobExecutor.registJobHandler("demoJobHandler", new DemoJobHandler());
  1. 在调度中心新建调度任务(后续步骤和 3.2.1 BEAN 模式(方法形式) 一致)。

这种方式用的不多,我就不给大家演示了,小伙伴们可以自行尝试。

3.2.2 BEAN模式(方法形式)

Bean 模式任务,支持基于方法的开发方式,每个任务对应一个方法,一般推荐这种方式。

优点:

  • 每个任务只需要开发一个方法,并添加 "@XxlJob" 注解即可,更加方便、快速。
  • 支持自动扫描任务并注入到执行器容器。

缺点:

  • 要求 Spring 容器环境。

基于方法开发的任务,底层会生成 JobHandler 代理,和基于类的方式一样,任务也会以 JobHandler 的形式存在于执行器任务容器中。

开发步骤:

  1. 开发 Job 方法:
@Component public class MyJob {     @XxlJob("demoJobHandler")     public ReturnT<String> demoJobHandler() throws Exception {         String param = XxlJobHelper.getJobParam();         XxlJobHelper.log("XXL-JOB, Hello World:{}",param);         return ReturnT.SUCCESS;     } }

这里的 @XxlJob 注解标记了这是一个定时任务方法,该注解还有 init 和 destroy 属性,可以分别用来配置初始化和销毁的方法。

XxlJobHelper.getJobParam() 可以用来获取任务参数。

在这个过程中,我们需要通过 XxlJobHelper.log 打印执行日志。

默认任务结果为 "成功" 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过 XxlJobHelper.handleFail/handleSuccess 自主设置任务结果。

然后启动 SpringBoot 项目。

  1. 配置调度中心,新建调度任务

接下来打开配置调度中心,找到执行器管理,点击新增,如下:

如果是自动注册的话,可以不用填机器地址。

接下来找到任务管理,并点击新增:

基础配置都没啥好说的。

调度类型选择 CRON 表达式,CRON 表达式可以自己填,也可以点击后面的编辑按钮自动生成。

运行模式就选择 BEAN,JobHandler 的值就是我们前面 @XxlJob 注解中填的值,任务参数就是定时任务的方法参数。

配置完成后,回到执行器管理,点击查看,可以查看刚刚注册的节点信息:

再次回到任务管理,选择启动,就可以开始定时任务的执行了:

开启之后,点击调度日志,就可以看到我们系统任务执行的详细信息了:

可以看到,每隔 5 秒执行一次。

点击调度备注,可以查看一些调度细节:

在右边操作按钮中选择执行日志:

可以查看执行细节:

红色框出来的,就是我们刚刚自己打印的。

当然这里还有一些其他玩法,小伙伴们可以自行点击按钮尝试,我就不赘述了。

3.2.3 GLUE 模式(Java)

任务以源码方式维护在调度中心,支持通过 Web IDE 在线更新,实时编译和生效,因此不需要指定 JobHandler。即在网页上写定时任务的代码,然后去执行。

这种方式个人感觉使用较少,小伙伴们了解一下即可。

开发流程如下:

  1. 调度中心->任务管理,新建调度任务,新建时选择 "GLUE模式(Java)":

  1. 开发任务代码:选中指定任务,点击该任务右侧 GLUE 按钮,将会前往 GLUE 任务的 Web IDE 界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)。

编辑完成后,保存即可。接下来又和前面一样了,启动任务,后查看调度日志。

这里支持 30 个版本的版本回溯,在 GLUE 任务的 Web IDE 界面,选择右上角下拉框"版本回溯",会列出该 GLUE 的更新历史,选择相应版本即可显示该版本代码,保存后 GLUE 代码即回退到对应的历史版本。

4. 小结

好啦,这就是我跟大家介绍的 xxl-job,感兴趣的小伙伴可以试试哦~

公众号后台回复 xxl-job-demo 可以下载本文案例。

【躲过裁员,成功上岸】发现小公司有不好的苗头,赶紧学习!

Posted: 28 Mar 2022 05:30 PM PDT

读者反馈:今年这波裁员有点凶,但在我们公司去年已经有了些不好的苗头,为了不让自己那么拉胯🌶,躲不过各种坑坑洼洼。跟着小傅哥的博客内容补全了自己很多的知识,包括:中间件、字节码、DDD项目、设计模式以及面试手册等,终于算是有了一点点竞争力。赶在这波裁员时上岸了!可能也有运气的存在,继续努力吧!

薪资翻倍36K,入职阿里P6

技术是长期积累沉淀的,并不是一蹴而就的,更不是成为一堆没用资料的收藏家。只有跟随还在一线编码的硬核号主,吸收实战经验才能快速成长。- 看大厂架构师写的资料,真香!

  • 资料包括:Java 面经手册、重学Java设计模式(PDF)、手撸Spring、字节码编程、从大学到毕业的资料汇总、Lottery 分布式秒杀抽奖实战项目 - 把小傅哥的好东西都拿出来了!
  • 学习地址https://bugstack.cn

有价值的干货资料介绍

1 Java 面经手册

  • 全书共计5章29节,417页11.5万字,耗时4个月完成。涵盖数据结构、算法逻辑、并发编程、JVM以及简历和互联网大厂面试等内容。

《Java 面经手册》是一本以面试题为入口讲解 Java 核心技术的 PDF 书籍,书中内容也极力的向你证实代码是对数学逻辑的具体实现为什么这么说? 当你仔细阅读书籍时,会发现这里有很多数学知识,包括:扰动函数、负载因子、拉链寻址、开放寻址、斐波那契(Fibonacci)散列法还有黄金分割点的使用等等。

编码只是在确定了研发设计后的具体实现,而设计的部分包括:数据结构、算法逻辑以及设计模式等,而这部分数据结构和算法逻辑在 Java 的核心 API 中体现的淋漓尽致。那么,也就解释了为什么这些内容成为了热点面试题,虽然可能我们都会觉得这样的面试像是造火箭。

2 重学Java设计模式 - PDF版

  • 全书共计22个真实业务场景对应59组案例工程、编写了18万字271页的PDF、开始耗时50天打造完成。

重学Java设计模式 PDF截图

本书是作者小傅哥,基于互联网真实案例编写的Java设计模式实践图书。全书以解决方案为核心,从实际开发业务中抽离出交易、营销、规则引擎、中间件、框架源码等22个真实场景,对设计模式进行全面、彻底的分析。帮助读者灵活地使用各种设计模式,从容应对复杂变化的业务需求,编写出易维护、可扩展的代码结构。

3 字节码编程

全书共计107页,11万7千字,20个章节涵盖三个字节码框架和JavaAgent使用并附带整套案例源码!

讲道理,市面上以及网络搜索中都基本很少有成体系的关于字节码编程的知识,这主要由于大部分开发人员其实很少接触这部分内容,包括;ASM、Javassist、Byte-buddy以及JavaAgent,没有很大的市场也就没有很多的资料。但大家其实已经从其他的框架或者中间件中使用到,就像你用到的;Cglib、混沌工程、非入侵的全链路监控以及你是否使用过jetbrains-agent.jar做了某项实验?

4 Spring 手撸专栏

在写了部分关于 Spring核心源码 的面经内容后,我决定要去手撸一个Spring了。为啥这么干呢?因为所有我想写的内容,都希望它是以理科思维理解为目的方式学会,而不是靠着硬背记住。而编写面经的过程中涉及到的每一篇Spring源码内容分析,在即使去掉部分非主流逻辑后,依然会显得非常庞大。

此专栏是一本以开发简化版Spring学习其原理和内核的知识内容,不仅是代码编写实现也更注重内容上的需求分析和方案设计,所以在学习的过程要结合这些内容一起来实践,并调试对应的代码。粉丝伙伴在阅读的过程中,千万不要害怕在学习的过程中遇到问题,这些都是正常的! 希望你可以一直坚持把这些内容事必躬亲、亲历亲为的学完,加油!

5 IDEA Plugin 开发手册

此开发手册,分为4章12节循序渐进的通过实践案例开发的方式,串联 IDEA Plugin 开发的各项常用技术点,为读者讲解如何开发一个 IDEA 插件。

IDEA 插件开发可以帮助研发人员提升能效,解决一些实际场景中的共性问题。但最近在折腾IDEA插件开发的时候,市面的资料确实不多,也没有成体系完整的开发指导手册,所以就遇到了很多不知道就不会的事情,需要一点点查询搜索源码、验证API接口,最终把各项功能实现,当然在这个过程中也确实踩了不少坑!接下来在这个专栏会把一些关于 IDEA 插件开发用到的各项知识做成案例输出出来,帮助有需要的研发伙伴,一起建设 IDEA Plugin。

6. Lottery 抽奖系统 - 基于领域驱动设计的四层架构实践

Lottery 抽奖系统 项目是一款互联网面向C端人群营销活动类的抽奖系统,可以提供抽奖活动玩法策略的创建、参与、记账、发奖等逻辑功能。在使用的过程中运营人员通过创建概率类奖品的抽奖玩法,对用户进行拉新、促活、留存,通常这样的系统会用在电商、外卖、出行、公众号运营等各类场景中。

  • 此系统架构为 DDD 领域驱动设计的四层架构实现方式,以重视代码实现落地的方式向读者介绍和展示如何开发这样的代码。
  • 在 Domain 领域层逐步通过拆解系统流程设计,按照职责边界的领域模块进行设计和开发,最终在应用层进行逻辑功能编排。
  • 这个系统中会体现出很多的设计模式思想和最终的实现,只有把 DDD 和设计模式结合起来,才能开发出更加易于扩展和维护的代码结构。

7. SpringBoot 中间件小册

全小册19个章节,包括16个中间件的设计和开发,包括测试案例共30个代码库提供给读者学习使用。小册实现的中间件场景涵盖:技术框架、数据服务、数据组件、分布式技术、服务治理、字节码、IDEA插件七个方面,贯穿整个互联网系统架构中常用的核心内容。非常值得了解、学习、实践到掌握。

  • 技术框架:包括 Spring、SpringBoot 配置加载、自定义注解、扫描注册Bean等,以及 ORM 框架设计原理和实现。这部分技术主要是把开发的中间件与框架结合,开发相应的组件或者包装为各类 SpringBoot Starter 的能力学习。
  • 数据服务:Mysql、Redis、Elasticsearch,都是数据服务,通常需要开发各类组件对数据服务的使用进行封装,Mysql 我们知道有 JDBC,Redis 我们知道有 Jedis,但 Elasticsearch 有 x-pack 你是否了解。
  • 数据组件:这类组件的开发就是为了简化对数据服务的使用,Mysql+JDBC+ORM,可以非常方便的使用数据库服务,那么 Elasticsearch 是否也可以做相应的组件研发,让它的查询也能像使用 MyBatis 一样呢?二折页的技术能力就需要对 MyBatis 等 ORM 框架的实现原理熟悉,同时需要了解 JDBC 的概念。
  • 分布式技术:RPC 框架、注册中心、分布式任务,都是现有互联网分布式架构中非常重要的技术,而对于如何实现一个 RPC 框架,也技术是研发人员要掌握的重点,同时如何使用注册中心、怎么下发分布式调度任务,等等,这些技术的学习能让对现有的框架使用有更深入的认识。
  • 服务治理:熔断、降级、限流、切量、黑白名单以及对现有方法的非入侵式扩展增强等,都可以成为是服务治理类组件,原本这类技术在早期是与业务逻辑代码融合的,后来逐步被拆解出来,开发成对应的组件。所以我们可以学习到,关于这类组件的包装、集成是如何做的。
  • 字节码&插件:在互联网的系统应用运维过程中,你一定会接触到各类的监控系统,而很多监控系统是非入侵的全链路监控,那么这些是如何实现的呢?其实它们是基于字节码插桩,对系统方法的增强,采集相应的运行时信息,进行监控的。再到扩展 JVMTI、IDEA 插件开发,都是为了整个研发过程的可持续交付和上线提高交付质量和降低人效的。

小傅哥所编写的这些技术资料,皆是亲自验证、体系梳理、逐步总结的技术内容,所以在学习的过程中一定要对照源码对应的案例进行学习,这样才能让你有更大的收获。

最后,我想说:能力,是你前行的最大保障。哪怕你是兢兢业业的工作者,也要拥有能留下的本事跳出去的能力,才会相对安稳度过动荡和一次次的动荡。

本文参与了 SegmentFault 思否征文「如何"反杀"面试官?」,欢迎正在阅读的你也加入。

2022招聘季|从招聘方的角度理解求职

Posted: 27 Mar 2022 07:38 PM PDT

0 起因

前些日子,有位同学找我咨询求职问题。他本科专业其实不错,但是第一份工作没找好,所以只好报了家培训班,想社招之路走得更稳一些。结业之前,他担心培训班就业辅导不够,也找了几位外部导师帮忙。其中就包括我。

熟悉我的人都知道,我比较反对报培训班,因为:

  1. 性价比太低。
  2. 培训班里真正有实战经验的很少,大部分都是纸上谈兵。

这次咨询也基本印证了我的观点。所以我想再分享一下,到底招聘方的情况是怎样的,他们有哪些需求,准备简历的时候应该注意什么,面试的时候又应该注意什么。

1 企业要招人

1.0 产生需求

某天,老板想做个产品。我们假设公司里已经有靠谱的技术团队和技术管理,那么,技术主管很快就会梳理需求,落实到岗位和个人,如果他发现,目前的人力无法应对满足需求,就会启动招聘。

1.1 产生岗位描述(JD)

岗位描述(简称 JD),即这个岗位职责是什么、需要哪些职业技能、工作地点和福利待遇等,是招聘前必须准备好的物料。

产品确定、需求确定,所以岗位需求和岗位职责基本也是确定的。 此时技术主管一般会找来以前的 JD,改一改职责和需求,完成新的 JD。然后 ta 会把 JD 发给招聘负责人(多半是 HR),上传到招聘网站、开发者社区、公司招聘库等等。

比如 Code.fun 加入我们 就是一份很典型的 JD。

1.2 筛简历

一段时间后,HR、技术主管会从各种渠道获得一大批简历。于是他们就要从这一大堆简历快速筛选出能满足岗位需要的候选人。

怎么筛呢?一般是关键词。 拿我某项工作来举例,我当时负责开发的 Showman 产品,是浏览器插件,界面部分采用 Vue,需要兼容 Puppeteer API。那么在我招聘的时候,就会很重视候选人这几方面的经历。因为匹配度越高,他能顺利接手工作、快速度过适应期的可能性就越大。

找到关键词之后,HR 可能会接受简历,转发给岗位负责人,即初筛。而岗位负责人则会结合候选人的教育经历、职业经理、项目经验等,判断这些关键词的可靠性与含金量,最终再筛选出其中最合适的一些人进入面试环节。

1.3 面试

招聘方从成百上千的简历中筛选出了若干的"看起来"比较合适的候选人,接下来,就要约过来面试聊聊,看看哪些人真正合适。

面试的过程,其实是验证简历的过程。 张三简历里说,他开发过浏览器插件,但是这个插件有多少用户、运营过多久,在开发过程中解决过多少问题,并没有写得非常详细;即使写得很细,也未必跟他有关系。所以作为面试官,我就需要再面试中弄清楚,这项职业经历对他来说,是加分还是减分,甚至是一票否决。

除了验证简历,面试还可以让招聘方初步了解候选人的非职业特性,比如沟通能力、语言概括能力、接人待物能力,等等。这些东西在招聘中占比不大,不过在诸多候选人的技术能力拉不开差距的时候,也会是必要的判断依据。

1.4 多轮面试

单场面试时间有限,通常做不到面面俱到。所以面试通常不止一轮。每个面试官关注的点不一样,比如 A 关注项目经验、B 关注代码实现能力、C 关注职业规划等。最终 ABC 的观点会汇总到一起,给每个候选人一个总评。

接下来,面试进行到一定阶段,总评过关的人数积累到一定数目,公司就会给其中比较出色的几位发 offer,然后就是入职试用转正,略过不谈。

2 我们应该怎么做

2.1 简历要有针对性

正如前文所说,企业收到的简历量很大,筛简历的压力也很大。通常来说,招聘方负责人会花在每个简历上的时间很短,基本上就扫一遍,有必要的关键词就再看仔细一点,没有就直接扔掉。

很多同学只做一套简历,投所有企业所有岗位。在这种情况下希望渺茫:A 公司招数据可视化,B 公司招小程序开发,C 公司主做移动端,三家公司的岗位要求差异很大,一套简历很难覆盖不同公司的不同需求。

有同学说那我简历里把所有技术关键词都写上可以么?很遗憾,招聘方也不是傻子,你的工作年限和项目经验、技术专长不符,也难逃直接扔掉的命运。或者简历太长,重点不突出,要费力查找关键词,招聘方多半也会直接扔掉。

当然,为每家公司单独准备一份简历成本太高,也不现实。所以,推荐的做法是:

  1. 准备一份比较通用的简历,不要太长,写上自己最擅长的东西,最能凸显自己特长的经历,用来海投,碰运气。
  2. 对自己比较中意、比较重视的公司,单独准备简历,突出该公司招聘岗位需要的知识、技能、项目经验等,专门投递。
  3. 同时精投的公司不宜过多,避免面试扎堆,影响准备时间。

2.1.1 特殊技巧

(我再想想要不要写……)

2.2 简历要尽可能真实,面试前也要做好准备

前文也提过,面试是验证简历的过程。能够走到面试这一步,说明招聘方认为候选人的履历可以满足岗位需求,接下来就是要对简历验真,以及判断候选人的发展潜力和综合排名。

所以简历里的内容可以适度美化,但一定不要做假。为什么呢?面试时间有限,面试官不会针对简历中的每一项进行审查,而是对自己关注的、擅长的领域盘问,也就是大概抽查 20% 的内容,给整份简历打分。

如果候选人简历有做假,面试时被面试官发现,ta 可不会只扣这一条的分,而是整份简历都显得不可靠,都要扣分。即使剩下的部分都是真实的,但是没有被抽检到,就不会改变面试官的判断。甚至,如果连问两条都不符合预期,可能就会直接中止面试。

面试前的准备也要尽量做好。比如你参考上一条建议,优化了简历内容,突出以前的某段项目经历以匹配特定关键词。那么这个时候最好回顾一下,审视一下当时的技术方案有何得失,哪里值得改进,自己的工作有何值得称道的地方,等等。然后搜搜看该领域目前的发展状况。不要面试官问起来,这个想不起那个不知道,好好的加分项直接被干成减分项。

2.3 项目经验要择优表现

通常来说,简历不要太长,因为筛简历的人不会有那么多时间认真仔细的读。有人说不要超过一页,我觉得不用这么极端,但是三页绝对是极限了。短简历更容易突出重点、突出优势。

所以通常来说,我们要对项目经验进行取舍,不要把每个项目都罗列出来,即使是海投简历,也要选择会增加自己竞争优势的项目,写到简历里。

比如,某位培训班同学的简历里,介绍 ta 做过的某个全栈项目,混用 MySQL + MangoDB,koa2 + express。这就很奇怪,这两组技术产品定位冲突,在实际生产中,几乎不会混用。所以这样的项目经验就只能减分。还有,在一些同学的简历里看到,他们这两年都还在用 easyui、jquery 做项目,我当然不否认这些传统框架也能完成产品需求,但是写到简历里,就不要指望它们还能帮你加分。

如果,极端情况,你的项目经验都很差,那我建议你抓紧时间参与一些能给自己加分的项目,不管是开源的、商用的、独立项目都可以,别憨憨的写一堆没价值的项目,然后抱怨拿不到面试机会。

2.4 职业经历/项目经历要能让面试官感知到你的优势

很多同学写简历写到职业经历和项目经历时会写的特别平铺直叙、没有重点、缺少关键信息。比如,做过 OA 系统,就把自己负责的模块都列举一下,或者把流程叙述一遍。这样的信息对面试官来说毫无意义:OA 系统大部分人都用过或了解,公司内的工作流程基本也大同小异。这样的简历基本难逃扫一眼直接丢掉的命运。

提升面试官感知有三个方法:

  1. 列数字。比如:"我使用了xx方法,使得首页打开速度提升了 40%",或者"我们把测试覆盖率做到 100%",等等,让面试官一下就能形成具体认知
  2. 举方案。有一些成型的优秀方案,但不是套个库就行,实施起来有一些难度,就很适合写到这里。比如:应用设计模式、分布式多线程、灰度增量,等等。如果能配合上面的"列数字"方法,效果更佳
  3. 引用其他人的评价。有时候我们想写数字,但是无奈不负责统计,拿不到数字,乱写又担心被面试官挑战,也可以写别人的评价,比如:"产品总监评价此功能至少带来 5% 的总访问量提升"

提醒大家,如果你要用这个方法吸引招聘方注意,那就要做好相应的准备,避免偷鸡不成蚀把米。比如,你要写数字,就要知道数字是怎么统计出来的;你要写方案,就要写有说服力的方案。

2.5 面试时找机会突出自己的优势

目前来看,招聘方占据优势,是买方市场。招聘方会在众多候选人当中选择最好的几个发出 offer。所以我们在面试时,不仅要证明简历上写的都是真实可靠的,还要抓住机会,展现自己的优势。不然,平平淡淡拿到一个中等分数,可能在招聘方的眼里,只是一块"鸡肋"。

别的岗位就不说了,只说技术开发岗。

通常来说,技术研发,简历外,通常需要在面试中展现的能力主要有:

  1. 沟通能力
  2. 业务理解能力
  3. 主动学习的习惯和能力
  4. 积极思考的习惯
  5. 解决问题的韧性和积极性

这些条件可能不足以形成一票通过/否决,但会影响你在同一批候选人里的排名,也要尽量好好表现。另外,不同的公司、职级、岗位,可能也有不同的权重,我就不详细解说了。


总结

大家在求职面试时,不仅考虑到自己,也要多多站在招聘方的角度想一想,对方的环境、对方的需求、对方的判断方式,你应该怎么应对。哪些东西可以不写/说,哪些东西应该强调,哪些东西会加分,哪些东西会减分。一件事情做与不做,是这么做还是那样做,一篇文章一个问答,我们是否要照做,都可以用这个方式来判断。

希望大家都能找到满意的工作。有任何想法和意见,也欢迎留言讨论。

本文参与了 SegmentFault 思否征文「如何"反杀"面试官?」,欢迎正在阅读的你也加入。

本文原载于 我的博客,两边同步沟通,欢迎访问。

MySQL 表分区?涨知识了!

Posted: 27 Mar 2022 07:10 AM PDT

@[toc]
松哥之前写过文章跟大家介绍过用 MyCat 实现 MySQL 的分库分表,不知道有没有小伙伴研究过,MySQL 其实也自带了分区功能,我们可以创建一个带有分区的表,而且不需要借助任何外部工具,今天我们就一起来看看。

1. 什么是表分区

小伙伴们知道,MySQL 数据库中的数据是以文件的形势存在磁盘上的,默认放在 /var/lib/mysql/ 目录下面,我们可以通过 show variables like '%datadir%'; 命令来查看:

我们进入到这个目录下,就可以看到我们定义的所有数据库了,一个数据库就是一个文件夹,一个库中,有其对应的表的信息,如下:

在 MySQL 中,如果存储引擎是 MyISAM,那么在 data 目录下会看到 3 类文件:.frm.myi.myd,作用如下:

  1. *.frm:这个是表定义,是描述表结构的文件。
  2. *.myd:这个是数据信息文件,是表的数据文件。
  3. *.myi:这个是索引信息文件。

如果存储引擎是 InnoDB, 那么在 data 目录下会看到两类文件:.frm.ibd,作用分别如下:

  1. *.frm:表结构文件。
  2. *.ibd:表数据和索引的文件。

无论是哪种存储引擎,只要一张表的数据量过大,就会导致 *.myd*.myi 以及 *.ibd 文件过大,数据的查找就会变的很慢。

为了解决这个问题,我们可以利用 MySQL 的分区功能,在物理上将这一张表对应的文件,分割成许多小块,如此,当我们查找一条数据时,就不用在某一个文件中进行整个遍历了,我们只需要知道这条数据位于哪一个数据块,然后在那一个数据块上查找就行了;另一方面,如果一张表的数据量太大,可能一个磁盘放不下,这个时候,通过表分区我们就可以把数据分配到不同的磁盘里面去。

MySQL 从 5.1 开始添加了对分区的支持,分区的过程是将一个表或索引分解为多个更小、更可管理的部分。对于开发者而言,分区后的表使用方式和不分区基本上还是一模一样,只不过在物理存储上,原本该表只有一个数据文件,现在变成了多个,每个分区都是独立的对象,可以独自处理,也可以作为一个更大对象的一部分进行处理。

需要注意的是,分区功能并不是在存储引擎层完成的,常见的存储引擎如 InnoDBMyISAMNDB 等都支持分区。但并不是所有的存储引擎都支持,如 CSVFEDORATEDMERGE 等就不支持分区,因此在使用此分区功能前,应该对选择的存储引擎对分区的支持有所了解。

2. 分区的两种方式

不同于 MyCat 中既可以垂直切分又可以水平切分,MySQL 数据库支持的分区类型为水平分区,它不支持垂直分区。

2.1 水平切分

先来一张简单的示意图,大家感受一下什么是水平切分:

假设我的 DB 中有 table-1、table-2 以及 table-3 三张表,水平切分就是拿着我 40 米大刀,对准黑色的线条,砍一剑或者砍 N 剑!

砍完之后,将砍掉的部分放到另外一个数据库实例中,变成下面这样:


这样,原本放在一个 DB 中的 table 现在放在两个 DB 中了,观察之后我们发现:

  1. 两个 DB 中表的个数都是完整的,就是原来 DB 中有几张表,现在还是几张。
  2. 每张表中的数据是不完整的,数据被拆分到了不同的 DB 中去了。

这就是数据库的水平切分,也可以理解为按照数据行进行切分,即按照表中某个字段的某种规则来将表数据分散到多个库之中,每个表中包含一部分数据,即水平切分不改变表结构。

2.2 垂直切分

先来一张简单的示意图,大家感受一下垂直切分:

所谓的垂直切分就是拿着我 40 米大刀,对准了黑色的线条砍。砍完之后,将不同的表放到不同的数据库实例中去,变成下面这个样子:



这个时候我们发现如下几个特点:

  1. 每一个数据库实例中的表的数量都是不完整的。
  2. 每一个数据库实例中表的数据是完整的。

这就是垂直切分。一般来说,垂直切分我们可以按照业务来划分,不同业务的表放到不同的数据库实例中。

MySQL 数据库支持的分区类型为水平分区。

此外,MySQL 数据库的分区是局部分区索引,即一个分区中既存放了数据又存放了索引,目前,MySQL数据库还不支持全局分区(数据存放在各个分区中,但是所有数据的索引放在一个对象中)。

3. 为什么需要表分区

  1. 可以让单表存储更多的数据。
  2. 分区表的数据更容易维护,可以通过清除整个分区批量删除大量数据,也可以增加新的分区来支持新插入的数据。另外,还可以对一个独立分区进行优化、检查、修复等操作。
  3. 部分查询能够从查询条件确定只落在少数分区上,查询速度会很快。
  4. 分区表的数据还可以分布在不同的物理设备上,从而高效利用多个硬件设备。
  5. 可以使用分区表来避免某些特殊瓶颈,例如 InnoDB 单个索引的互斥访问、ext3 文件系统的 inode 锁竞争。
  6. 可以备份和恢复单个分区。

分区的限制和缺点:

  1. 一个表最多只能有 1024 个分区。
  2. 如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引列都必须包含进来。
  3. 分区表无法使用外键约束。
  4. NULL 值会使分区过滤无效。
  5. 所有分区必须使用相同的存储引擎。

4. 分区实践

说了这么多,来个例子看一下。

首先我们先来查看一下当前的 MySQL 是否支持分区。

在 MySQL5.6.1 之前可以通过命令 show variables like '%have_partitioning%' 来查看 MySQL 是否支持分区。如果 have_partitioning 的值为 YES,则表示支持分区。

从 MySQL5.6.1 开始,have_partitioning 参数已经被去掉了,而是用 SHOW PLUGINS 来代替。若有 partition 行且 STATUS 列的值为 ACTIVE,则表示支持分区,如下所示:

确认我们的 MySQL 支持分区后,我们就可以开始分区啦!

接下来我们来看几种不同的分区策略。

4.1 RANGE 分区

RANGE 分区比较简单,就是根据某一个字段的值进行分区。不过这个字段有一个要求,就是必须是主键或者是联合主键中的某个字段。

例如根据 user 表的 id 进行分区:

  1. 当 id 小于 100,数据插入 p0 分区;
  2. 当 id 大于等于 100 小于 200 的时候,插入 p1 分区;
  3. 如果 id 大于等于 200 则插入 p2 分区。

上面的规则涉及到了 id 的所有范围了,如果没有第三条规则,那么插入一个 id 为 300 的记录时,就会报错。

建表 SQL 如下:

create  table  user(   id int primary key,   username varchar(255) )engine=innodb   partition by range(id)(      partition  p0  values  less  than(100),      partition  p1  values  less  than(200),      partition  p2  values  less  than maxvalue   );

表创建成功后,我们进入到 /var/lib/mysql/test08 文件夹中,来看刚刚创建的表文件:

可以看到,此时的数据文件分为好几个了。

information_schema.partitions 表中,我们可以查看分区的详细信息:

也可以自己写个 SQL 去查询:

select * from information_schema.partitions where table_schema='test08' and table_name='user'\G

每一行展示一个分区的信息,包括分区的方式、该区的范围、分区的字段、该区目前有几条记录等等。

RANGE 分区有一个比较典型的使用场景,就是按照日期对表进行分区,例如同一年注册的用户放在一个分区中,如下:

create  table  user(   id int,   username varchar(255),   password varchar(255),   createDate date,   primary key (id,createDate) )engine=innodb   partition by range(year(createDate))(      partition  p2022  values  less  than(2023),      partition  p2023  values  less  than(2024),      partition  p2024  values  less  than(2025)   );

注意,createDate 是联合主键的一员。如果 createDate 不是主键,只是一个普通字段,那么创建时就会抛出如下错误:

现在,如果我们要查询 2022 年注册的用户,系统就只会去搜索 p2022 这个分区,通过 explain 执行计划可以证实我们的想法:

如果想要删除 2022 年注册的用户,则只需要删除该分区即可:

alter table user drop partition p2022;

由上图可以看到,删除之后,数据就没了。

4.2 LIST 分区

LIST 分区和 RANGE 分区类似,区别在于 LIST 分区是基于列值匹配一个离散值集合中的某个值来进行选择,而非连续的。举个例子大家看下就明白了:

假设我有一个用户表,用户有性别,现在想按照性别将用户分开存储,男性存储在一个分区中,女性存储在一个分区中,SQL 如下:

create  table  user(   id int,   username varchar(255),   password varchar(255),   gender int,   primary key(id, gender) )engine=innodb   partition by list(gender)(      partition  man  values  in  (1),      partition  woman  values  in  (0));

这个表将来就两个分区,分别存储男性和女性,gender 的取值为 1 或者 0,gender 如果取其他值,执行就会出错,最终执行结果如下:

这样分区之后,将来查询男性或者查询女性效率都会比较高,删除某一性别的用户时删除效率也高。

4.3 HASH 分区

HASH 分区的目的是将数据均匀地分布到预先定义的各个分区中,保证各分区的数据量大致都是一样的。在 RANGE 和 LIST 分区中,必须明确指定一个给定的列值或列值集合应该保存在哪个分区中;而在 HASH 分区中,MySQL 自动完成这些工作,用户所要做的只是基于将要进行哈希分区的列指定一个表达式,并且分区的数量。

使用 HASH 分区来分割一个表,要在 CREATE TABLE 语句上添加 PARTITION BY HASH (expr),其中 expr 是一个字段或者是一个返回整数的表达式;另外通过 PARTITIONS 属性指定分区的数量,如果没有指定,那么分区的数量默认为 1,另外,HASH 分区不能删除分区,所以不能使用 DROP PARTITION 操作进行分区删除操作。

create  table  user(   id int,   username varchar(255),   password varchar(255),   gender int,   primary key(id, gender) )engine=innodb partition by hash(id) partitions 4;

4.4 KEY 分区

KEY 分区和 HASH 分区相似,但是 KEY 分区支持除 text 和 BLOB 之外的所有数据类型的分区,而 HASH 分区只支持数字分区。

KEY 分区不允许使用用户自定义的表达式进行分区,KEY 分区使用系统提供的 HASH 函数进行分区。

当表中存在主键或者唯一索引时,如果创建 KEY 分区时没有指定字段系统默认会首选主键列作为分区字段,如果不存在主键列会选择非空唯一索引列作为分区字段。

举个例子:

create  table  user(   id int,   username varchar(255),   password varchar(255),   gender int,   primary key(id, gender) )engine=innodb partition by key(id) partitions 4;

4.5 COLUMNS 分区

COLUMN 分区是 5.5 开始引入的分区功能,只有 RANGE COLUMN 和 LIST COLUMN 这两种分区;支持整形、日期、字符串;这种分区方式和 RANGE、LIST 的分区方式非常的相似。

COLUMNS Vs RANGE Vs LIST 分区:

  1. 针对日期字段的分区不需要再使用函数进行转换了。
  2. COLUMN 分区支持多个字段作为分区键但是不支持表达式作为分区键。

COLUMNS 支持的类型

  • 整形支持:tinyint、smallint、mediumint、int、bigint;不支持 decimal 和 float。
  • 时间类型支持:date、datetime。
  • 字符类型支持:char、varchar、binary、varbinary;不支持text、blob。

举个例子看下:

create  table  user(   id int,   username varchar(255),   password varchar(255),   gender int,   createDate date,   primary key(id, createDate) )engine=innodb PARTITION BY RANGE COLUMNS(createDate) (     PARTITION p0 VALUES LESS THAN ('1990-01-01'),     PARTITION p1 VALUES LESS THAN ('2000-01-01'),     PARTITION p2 VALUES LESS THAN ('2010-01-01'),     PARTITION p3 VALUES LESS THAN ('2020-01-01'),     PARTITION p4 VALUES LESS THAN MAXVALUE );

这是 RANGE COLUMNS,分区值是连续的。

再来看 LIST COLUMNS 分区,这个就类似于枚举了:

create  table  user(   id int,   username varchar(255),   password varchar(255),   gender int,   createDate date,   primary key(id, createDate) )engine=innodb PARTITION BY LIST COLUMNS(createDate) (     PARTITION p0 VALUES IN ('1990-01-01'),     PARTITION p1 VALUES IN ('2000-01-01'),     PARTITION p2 VALUES IN ('2010-01-01'),     PARTITION p3 VALUES IN ('2020-01-01') );

5. 常见分区命令

  1. 添加分区:
alter table user add partition (partition p3 values less than (4000)); -- range 分区
alter table user add partition (partition p3 values in (40));  -- lists分区
  1. 删除表分区(会删除数据):
alter table user drop partition p30;
  1. 删除表的所有分区(不会丢失数据):
alter table user remove partitioning; 
  1. 重新定义 range 分区表(不会丢失数据):
alter table user partition by range(salary)( partition p1 values less than (2000), partition p2 values less than (4000)); 
  1. 重新定义 hash 分区表(不会丢失数据):
alter table user partition by hash(salary) partitions 7; 
  1. 合并分区:把 2 个分区合并为一个,不会丢失数据:
alter table user  reorganize partition p1,p2 into (partition p1 values less than (1000));

6. 小结

不知道小伙伴们是否还记得松哥 2019 年写的 MyCat 教程(公众号江南一点雨后台回复 2019 有文章索引),这些分区策略是不是和 MyCat 中的策略非常相似呀?感兴趣的小伙伴赶紧去试一把吧~

参考资料:

https://www.cnblogs.com/dw330...

React Router v6 探索

Posted: 27 Mar 2022 10:13 AM PDT

前言

没事翻了翻 React Router 的文档,发现已推到了 v6.2.2 版本,这个版本做了很大的改动,让我们一起看看吧

为什么推出 v6

  • 推出 v6 的最大原因是 React Hooks 的出现
  • v6 写的代码要比 v5 代码更加紧凑和优雅

我们通过代码来感受下,这是 v6 写的伪代码

import { Routes, Route, useParams } from "react-router-dom";  function App() {     return (         <Routes>             <Route path="blog/:id" element={<Head />} />         </Routes>   ); }  function Head() {     let { id } = useParams();     return (         <>             <Footer />         </>     ); }  function Footer() {     let { id } = useParams(); }

这是 v5 写的伪代码

import * as React from "react"; import { Switch, Route } from "react-router-dom";  class App extends React.Component {     render() {         return (             <Switch>                 <Route                     path="head/:id"                     render={({ match }) => (                         <Head id={match.params.id} />                     )}                 />             </Switch>         );     } }  class Head extends React.Component {     render() {         return (             <>                 <Footer id={this.props.id} />             </>         );     } }  class Footer extends React.Component {     render() {         return (             <>                 <ButtonComponent id={this.props.id} />             </>         );     } }

这个例子表明

  • Hooks 消除了使用 <Route render> 访问路由器内部状态的需要
  • 手动传递 props 将该状态传播到子组件的需要
  • 应用程序包体积更小

增加了哪些特性?

  1. <Switch> 升级为 <Routes>

    • Routes 内的所有 <Route> 和 <Link> 是相对的。这使得 <Route path> 和 <Link to> 中的代码更精简、更可预测
    • 路由是根据最佳匹配,而不是按顺序遍历,这避免了由于路由不可达而导致的错误
    • 路由可以嵌套在一个地方,而不是分散在不同的组件中
  2. 新钩子 useRoutes 代替 react-router-config

之前:

import React, { lazy } from 'react'; import PrivateRoute from '@components/PrivateRoute/index';  const Dashboard = lazy(() => import('@pages/dashboard/index')); const Abount = lazy(() => import('@pages/abount/index'));  const routes = [     {         path: '/home',         component: Dashboard     },     {         path: '/about',         component: Abount     }, ];  const RouteWithSubRoutes = route => (     <PrivateRoute path={route.path} component={route.component} routes={route.routes} /> );  const routeConfig = routes.map((route, i) => <RouteWithSubRoutes key={i} {...route} />); export default routeConfig; 

现在

 function App() {     let element = useRoutes([         { path: '/', element: <Home /> },         {             path: 'users',             element: <Users />,             children: [                 { path: '/', element: <UsersIndex /> },                 { path: ':id', element: <UserProfile /> },                 { path: 'me', element: <OwnUserProfile /> },             ]         }     ]);     return element; }

就感觉更优雅一些

  1. useNavigate 代替 useHistory

之前

import { useHistory } from "react-router-dom";  function App() {     let history = useHistory();     function handleClick() {         history.push("/home");     }     return (         <div>             <button onClick={handleClick}>go home</button>         </div>     ); }

现在

import { useNavigate } from "react-router-dom";  function App() {     let navigate = useNavigate();     function handleClick() {         navigate("/home");     }     return (         <div>             <button onClick={handleClick}>go home</button>         </div>     ); }

这个变化不是很大

  1. Route 的变化
  • 4.1 <Route exact> 移除,使用 /* 代替
<Route path="/*" element={<Home />} />

`

  • 4.2 <Route children> 使用 <Route element> 代替
import Profile from './Profile';  // v5 <Route path=":userId" component={<Profile />} />  // v6 <Route path=":userId" element={<Profile />} />
  • 4.3 Outlet
    我们使用一个 <Outlet> 元素作为占位符。在 <Outlet> 这种情况下,Users 组件如何呈现其子路由。因此,将根据当前位置 <Outlet> 呈现一个 <UserProfile><OwnUserProfile> 元素
  • 4.4
import {     BrowserRouter,     Routes,     Route,     Link,     Outlet } from 'react-router-dom';  function App() {       return (         <BrowserRouter>             <Routes>                 <Route path="/" element={<Home />} />                 <Route path="users" element={<Users />}>                 <Route path="/" element={<UsersIndex />} />                 <Route path=":id" element={<UserProfile />} />                 <Route path="me" element={<OwnUserProfile />} />                 </Route>             </Routes>         </BrowserRouter>       ); }  function Users() {       return (         <div>               <nav>                 <Link to="me">My Profile</Link>             </nav>             <Outlet />         </div>       ); }

体验 v6

这里我们使用 create-react-app 来创建项目,安装好之后,进入项目,安装 react-router-dom@6 依赖

$ npx create-react-app react-app $ cd react-app $ npm install react-router-dom@6

src/index.js 在编辑器中打开,BrowserRouter 从顶部附近导入 react-router-dom 并将 <APP> 包装在 <BrowserRouter>

// src/index.js import * as React from "react"; import * as ReactDOM from "react-dom"; import { BrowserRouter } from "react-router-dom"; import "./index.css"; import App from "./App"; import * as serviceWorker from "./serviceWorker";  ReactDOM.render(     <BrowserRouter>         <App />     </BrowserRouter>,     document.getElementById("root") );

打开 src/App.js 并用一些路由替换默认标记

// App.js import * as React from "react"; import { Routes, Route, Link } from "react-router-dom"; import "./App.css";  function App() {     return (         <div className="App">             <h1>Welcome to React Router!</h1>                 <Routes>                     <Route path="/" element={<Home />} />                     <Route path="about" element={<About />} />                 </Routes>         </div>     ); }

现在,仍在 src/App.js,创建你的路由组件

// src/App.js function Home() {     return (         <>             <main>                 <h2>Home</h2>             </main>             <nav>                 <Link to="/about">About</Link>             </nav>         </>     ); }  function About() {     return (         <>             <main>                 <h2>About</h2>             </main>             <nav>                 <Link to="/">Home</Link>             </nav>         </>     ); }

运行 npm start ,您应该会看到 Home 标识

如何升级 v6

官方的迁移指南在这里:React Router v6 迁移指南

参考文章

结语

如果你正在用 Hook 重构你的应用,我的建议是可以尝试

重要的事

如果你觉得这篇内容对你挺有启发,别忘记点赞 + 关注

欢迎添加我的个人微信:Jiang9684,一起交流前端技术

我的博客地址

HMS Core视频编辑服务:AI着色, 忆往昔看今朝

Posted: 25 Mar 2022 12:29 AM PDT

近期热播的电视剧《人世间》,讲述了70年代无数普通人的故事,细腻的人物形象和真实的故事感动着我们。原来在那个年代,我们的父母和祖辈都在为新中国的美好生活而奋斗着,为国家舍弃了小家团聚的机会;原来在那个年代,拥有一张合照也不是容易的事情。

多年来,随着影像技术的迭代更新,人们的多彩生活被即时记录着。同时,给黑白图像增添色彩的技术也层出不穷,HMS Core视频编辑服务便是其一。通过其AI着色能力,可以智能填充黑白照片及视频的色彩,让黑白图像变得鲜活多彩!

当然,除了AI着色能力外,视频编辑服务还提供了专属滤镜、人物跟踪、一键染发、动态图片和人脸遮挡功能。移动应用集成这些能力之后,用户便可以随心复刻心仪图片的滤镜效果,实现百变发色,还可以锁定视频中的特定人物,让静态图片鲜活生动……

除此之外,视频编辑服务还支持多视频/图片的导入,可随时调整片段的顺序时长,实现多分辨率导出,最高支持输出4k的视频分辨率和60fps的帧率。

使用场景:随时随地剪大片

视频编辑服务的功能丰富,可以服务的场景也是各具特色,具体可以使用在以下场景:

  1. 视频剪辑:轻松完成视频裁剪、拼接、特效、音乐的处理,快速高效的制作短视频;
  2. 旅游出行:实现视频随剪随传,快速简单的制作旅行vlog,分享、记录自己的精彩生活;
  3. 社交互动:剪接、特效、滤镜的功能,用户可以随时随地制作精彩大片;
  4. 电商产品展示:商家快速剪辑,搭配字幕、特效和背景音乐等元素,让商品特性更加直观立体。

集成方式:多种接口灵活选择

目前,HMS Core为开发者提供了两种视频编辑服务的集成方式:

1.视频编辑UI SDK,提供产品级UI界面,集成简单。

2.视频编辑原子能力SDK,提供数百个底层能力接口,包含多个AI算法能力接口,可根据业务场景灵活选择。

这两种方式均提供导入、编辑、渲染、导出、媒体资源管理等一站式视频编辑能力,提供性能优异、简单易用、高兼容性的接口,帮助您轻松构建应用。您可根据使用场景选择不同的集成方式获取视频编辑能力。

了解更多详情>>

访问华为开发者联盟官网
获取开发指导文档
华为移动服务开源仓库地址:GitHubGitee

关注我们,第一时间了解 HMS Core 最新技术资讯~

No comments:

Post a Comment