开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

线程池拒绝策略的坑,不得不防 现象 问题复现 原因分析 其他

发表于 2021-11-03

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」

现象

先讲一下上周新鲜出炉的bug,业务反馈线上导出采购单功能超时,本来以为是业务导出采购单较多,让业务缩短下日期导出,结果还是导不出来,此时就怀疑是刚上线代码问题,立即进行了代码回滚,业务再重试导出成功。

本次上线代码主要是批量调用下游系统,改成了通过线程池调用,由于对下游系统不是强依赖,线程池调用设置的拒绝策略为丢弃策略(DiscardPolicy)

问题复现

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码public class DiscardTest {
public static void main(String[] args) throws Exception {

// 一个线程,队列最大为1
ThreadPoolExecutor executorService = new ThreadPoolExecutor(1, 1, 1L, TimeUnit.MINUTES,
new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.DiscardPolicy());

Future future1 = executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("start Runnable one");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

Future future2 = executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("start Runnable two");
}
});

Future future3 = executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("start Runnable three");
}
});

System.out.println("task one finish " + future1.get());// 等待任务1执行完毕
System.out.println("task two finish " + future2.get());// 等待任务2执行完毕
System.out.println("task three finish " + future3.get());// 等待任务3执行完毕

executorService.shutdown();// 关闭线程池,阻塞直到所有任务执行完毕
}
}

执行结果:一直卡着不动

image.png

原因分析

线程池线程数量到达3时,后续提交的任务执行丢弃策略(DiscardPolicy)。

我们看下DiscardPolicy实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* A handler for rejected tasks that silently discards the
* rejected task.
*/
public static class DiscardPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardPolicy}.
*/
public DiscardPolicy() { }
// 拒绝策略什么也没有做,此线程的状态依然是NEW
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}

执行丢弃策略时,丢弃策略的执行方法是什么都不做。 线程的状态依然是NEW。

要分析这个问题另外我们还需要看下线程池的submit方法里面做了什么,提交任务到线程池时,会包装成 FutureTask ,初始状态是 NEW。执行任务的是包装后的FutureTask对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}

// 初始状态为NEW
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}

// 状态小于等于COMPLETING都会一直等待
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}

我们看get()方法,FutureTask状态>COMPLETING 才会返回。因为拒绝策略没有修改FutureTask的状态,FutureTask的状态一直是NEW,所以不会返回,一直等待。

其他拒绝策略会不会导致阻塞

AbortPolicy是直接抛出异常,调用方马上可以获取结果

CallerRunsPolicy 是让主线程去执行,会更新任务状态

DiscardOldestPolicy 会poll出一个任务,但是没有任务处理,所以poll出来的任务是NEW状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
java复制代码public static class AbortPolicy implements RejectedExecutionHandler {
/**
* Creates an {@code AbortPolicy}.
*/
public AbortPolicy() { }

/**
* Always throws RejectedExecutionException.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
* @throws RejectedExecutionException always
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}

public static class CallerRunsPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code CallerRunsPolicy}.
*/
public CallerRunsPolicy() { }

/**
* Executes task r in the caller's thread, unless the executor
* has been shut down, in which case the task is discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardOldestPolicy} for the given executor.
*/
public DiscardOldestPolicy() { }

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}

总结

  1. 使用Future.get(),需要根据业务实际情况设置超时时间
  2. 能不用丢弃策略(DiscardPolicy),就不要用,如确实需要用,则需要自己实现。

延伸,线程池使用还有哪些注意事项?

  • 虽然使用CallerRunsPolicy不会造成卡死,但是还是要慎重,如果导致主线程被大量阻塞,对业务同样有影响。
  • 线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。(阿里巴巴开发手册)

说明:Executors各个方法的弊端:

1)newFixedThreadPool和newSingleThreadExecutor:

主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。

2)newCachedThreadPool和newScheduledThreadPool:

主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

  • 如果公司有全链路的trace(如阿里云的TracingAnalysis),线程中记得传递trace信息,不然trace信息会丢失。

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

spi和jar spi jar机制

发表于 2021-11-03

今天介绍两个大家每天都在用但是却很少去了解它的知识点:spi和jar运行机制,废话不多说,开始正题

spi

是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。spi机制是这样的:读取META-INF/services/目录下的元信息,然后ServiceLoader根据信息加载对应的类,你可以在自己的代码中使用这个被加载的类。要使用Java SPI,需要遵循如下约定:

当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
接口实现类所在的jar包放在主程序的classpath中;
主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
SPI的实现类必须携带一个不带参数的构造方法;
现在我们来简单的使用一下吧

spi使用示例
建一个maven项目,定义一个接口 (com.test.SpiTest),并实现该接口(com.test.SpiTestImpl);然后在 src/main/resources/ 下建立 /META-INF/services 目录, 新增一个以接口命名的文件 (com.test.SpiTest),内容是要应用的实现类(com.test.SpiTestImpl)。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public interface SpiTest {
void test();
}


public class SpiTestImpl implements SpiTest {
@Override
public void test() {
System.out.println("test");
}
}

然后在我们的应用程序中使用 ServiceLoader来加载配置文件中指定的实现。

1
2
3
4
5
6
java复制代码public static void main(String[] args) {
ServiceLoader<SpiTest> load = ServiceLoader.load(SpiTest.class);
SpiTest next = load.iterator().next();
next.test();

}

这便是spi的使用方式了,简约而不简单

spi技术的应用
那这一项技术有哪些方面的应用呢?最直接的jdbc中我们需要指定数据库驱动的全限定名,这便是spi技术。还有不少框架比如dubbo,都会预留spi扩展点比如:dubbo spi

为什么要这么做呢?在spring框架中我们注入一个bean 很容易,通过注解或者xml配置即可,然后在其他的地方就能使用这个bean。在非spring框架下,我们想要有同样的效果就可以考虑spi技术了。

写过springboot 的starter的都知道,需要在 src/main/resources/ 下建立 /META-INF/spring.factories 文件。这其实也是一种spi技术的变形。

jar机制

通常项目中我们打jar包都是通过maven来进行的,导致很多人忽略了这个东西的存在,就像很多人不知道jdb.exe 是啥玩意一样。下面我们不借助任何工具来打一个jar包并对jar文件结构进行解析。

命令行打jar包
首先我们建立一个普通的java项目,新建几个class类,然后在根目录下新建META-INF/MAINFEST.MF 这个文件包含了jar的元信息,当我们执行java -jar的时候首先会读取该文件的信息做相关的处理。我们来看看这个文件中可以配置哪些信息 :

Manifest-Version:用来定义manifest文件的版本,例如:Manifest-Version: 1.0
Main-Class:定义jar文件的入口类,该类必须是一个可执行的类,一旦定义了该属性即可通过 java -jar x.jar来运行该jar文件。
Class-Path:指定该jar包所依赖的外部jar包,以当前jar包所在的位置为相对路径,无法指定jar包内部的jar包
签名相关属性,包括Name,Digest-Algorithms,SHA-Digest等
定义好元信息之后我们就可以打jar包了,以下是打包的一些常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
arduino复制代码/* 1. 默认打包 */
// 生成的test.jar中就含test目录和jar自动生成的META-INF目录(内含MAINFEST.MF清单文件)
jar -cvf test.jar test

/* 2. 查看包内容 */
jar -tvf test.jar

/* 3. 解压jar包 */
jar -xvf test.jar

/* 4. 提取jar包部分内容 */
jar -xvf test.jar test\test.class

/* 5. 追加内容到jar包 */
//追加MAINFEST.MF清单文件以外的文件,会追加整个目录结构
jar -uvf test.jar other\ss.class

//追加清单文件,会追加整个目录结构(test.jar会包含META-INF目录)
jar -uMvf test.jar META-INF\MAINFEST.MF

/* 6. 创建自定义MAINFEST.MF的jar包 */
jar -cMvf test.jar test META-INF

// 通过-m选项配置自定义MAINFEST.MF文件时,自定义MAINFEST.MF文件必须在位于工作目录下才可以
jar -cmvf MAINFEST.MF test.jar test
jar运行的过程
jar运行过程和类加载机制有关,而类加载机制又和我们自定义的类加载器有关,现在我们先来了解一下双亲委派模式。

java中类加载器分为三个:

BootstrapClassLoader负责加载JAVAHOME/jre/lib部分jar包ExtClassLoader加载{JAVA_HOME}/jre/lib部分jar包
ExtClassLoader加载JAVAH​OME/jre/lib部分jar包ExtClassLoader加载{JAVA_HOME}/jre/lib/ext下面的jar包
AppClassLoader加载用户自定义-classpath或者Jar包的Class-Path定义的第三方包
类的生命周期为:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸载(Unloading)七个阶段。

当我们执行 java -jar的时候 jar文件以二进制流的形式被读取到内存,但不会加载到jvm中,类会在一个合适的时机加载到虚拟机中。类加载的时机:

遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先对其进行初始化。成这四条指令的最常见的Java代码场景是使用new关键字实例化对象的时候,读取或设置一个类的静态字段调用一个类的静态方法的时候。
使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
当触发类加载的时候,类加载器也不是直接加载这个类。首先交给AppClassLoader,它会查看自己有没有加载过这个类,如果有直接拿出来,无须再次加载,如果没有就将加载任务传递给ExtClassLoader,而ExtClassLoader也会先检查自己有没有加载过,没有又会将任务传递给BootstrapClassLoader,最后BootstrapClassLoader会检查自己有没有加载过这个类,如果没有就会去自己要寻找的区域去寻找这个类,如果找不到又将任务传递给ExtClassLoader,以此类推最后才是AppClassLoader加载我们的类。这样做是确保类只会被加载一次。通常我们的类加载器只识别classpath(这里的classpath指项目根路径,也就是jar包内的位置)下.class文件。jar中其他的文件包括jar包被当做了资源文件,而不会去读取里面的.class 文件。但实际上我们可以通过自定义类加载器来实现一些特别的操作

Tomcat 的类加载器
Tomcat的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类(Object,String等),各个web应用自己的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给commonClassLoader走双亲委托。

tomcat的类加载器:

Common类加载器:负责加载/common目录的类库,这儿存放的类库可被tomcat以及所有的应用使用。
Catalina类加载器:负责加载/server目录的类库,只能被tomcat使用。
Shared类加载器:负载加载/shared目录的类库,可被所有的web应用使用,但tomcat不可使用。
WebApp类加载器:负载加载单个Web应用下classes目录以及lib目录的类库,只能当前应用使用。
Jsp类加载器:负责加载Jsp,每一个Jsp文件都对应一个Jsp加载器。
我们将一堆jar包放到tomcat的项目文件夹下,tomcat 运行的时候能加载到这些jar包的class就是因为这些类加载器对读取到的二进制数据进行处理解析从中拿到了需要的类

springboot的jar包的特别之处
当我们将一个springboot项目打好包之后,不妨解压看看里面的结构是什么样子的的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码run.jar
|——org
| |——springframework
| |——boot
| |——loader
| |——JarLauncher.class
| |——Launcher.class
|——META-INF
| |——MANIFEST.MF
|——BOOT-INF
| |——class
| |——Main.class
| |——Begin.class
| |——lib
| |——commons.jar
| |——plugin.jar
| |——resource
| |——a.jpg

| |——b.jpg

classpath可加载的类只有JarLauncher.class,Launcher.class,Main.class,Begin.class。在BOOT-INF/lib和BOOT-INF/class里面的文件不属于classloader搜素对象直接访问的话会报NoClassDefDoundErr异常。Jar包里面的资源以 Stream 的形式存在(他们本就处于Jar包之中),java程序时可以访问到的。当springboot运行main方法时在main中会运行org.springframework.boot.loader.JarLauncher和Launcher.class这两个个加载器(你是否还及得前文提到过得spi技术),这个加载器去加载受stream中的jar包中的class。这样就实现了加载jar包中的jar这个功能否则正常的类加载器是无法加载jar包中的jar的class的,只会根据MAINFEST.MF来加载jar外部的jar来读取里面的class。

如何自定义类加载器
1)继承ClassLoader 重写findClass()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class MyClassLoader extends ClassLoader{

private String classpath;

public MyClassLoader(String classpath) {

this.classpath = classpath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 该方法是根据一个name加载一个类,我们可以使用一个流来读取path中的文件然后从文件中解析出class来
}

}

调用defineClass()方法加载类

1
2
3
4
5
6
7
8
9
10
java复制代码public static void main(String []args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException{
//自定义类加载器的加载路径
MyClassLoader myClassLoader=new MyClassLoader("D:\\lib");
//包名+类名
Class c=myClassLoader.loadClass("com.test.Test");

if(c!=null){
// 做点啥
}
}

总结
本文从比较基础的层面解读了我们频繁使用却大部分人不是很了解的两个知识点——spi和jar机制。希望大家看完这篇文章后能对springboot中的一些“黑魔法”有更深入的了解,而不是停留在表面。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java基础学习—day02 Java基础学习

发表于 2021-11-03

Java基础学习

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」。

7548c63f4ac26d3ebc8d4aa15945cc12.gif

1、方法的基本定义

限制条件:本次所讲解的方法指的是在主类中定义,并且由主方法由主方法直接调用。

方法是指就是一段可以被重复调用的代码块。 在java里面如果想要进行方法的定义,则可以使用如下的方法进行完成。

1
2
3
4
java复制代码public static 方法返回值 方法名称([参数类型 变量,....]){
   方法体代码 ;
    return [返回值];
}

在定义方法的时候对于方法的返回值由以下两类:void没用返回值和数据类型(基本类型、引用类型)。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class TestDemo{
public static void main(String args[]){
//如果要在主方法里面调用该方法,该方法一定要用static进行修饰
print(); //主方法里面直接调用
print(); //主方法里面直接调用
print(); //主方法里面直接调用
}
public static void print(){
System.out.println("Hello,World!");
}
}

但是有一点要特别的注意就是当返回值为void类型的时候,那么该方法当中可以直接使用return来直接结束调用。在一般情况下和if判断使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class TestDemo{
public static void main(String args[]){
//如果要在主方法里面调用该方法,该方法一定要用static进行修饰
print1(10); //主方法里面直接调用
print1(20); //主方法里面直接调用
print1(30); //主方法里面直接调用
}
public static void print(){
System.out.println("Hello,World!");
}

public static void print1(int x){
if(x == 20){ //表示方法结束的判断
return ; //此语句之后的代码不在执行
}
System.out.println("x = " + x);
}
}

image-20210726155126462

image-20210726155831793

2、方法重载

方法的重载是指:方法名称相同,参数的类型或个数不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class TestDemo2{
public static void main(String args[]){
//如果要在主方法里面调用该方法,该方法一定要用static进行修饰
System.out.println(add(10,20)); //主方法里面直接调用
System.out.println(add(10,20,30)); //主方法里面直接调用
System.out.println(add(10.1,20.1)); //主方法里面直接调用
}
public static int add(int a,int b){
return  a + b;
}

public static int add(int a,int b,int c){
return a + b + c;
}

public static double add(double a, double b){
return a + b;
}
}

image-20210726160923526
在方法重载的时候有一个重要的原则就是要求方法的返回值类型一定要相同。

通过用System.out.println()输出发现所有的类型都可以进行输出,由此我们可以发现这个方法是一个重载的方法。

3、方法的递归调用

方法的递归调用指的是一个方法调用自己的形式。如果要进行方法的递归操作往往都具备以下特点

  • 方法必须有一个递归的结束条件
  • 方法在每次递归处理的时候一定要做出一些变更
    image-20210726164358510

计算60!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class TestDemo4{
public static void main(String args[]){
​
System.out.println(mul(60));

}
public static double mul(double num){
if (num == 1){
return 1;
}
return num * mul(num - 1);
}
}
计算结果:8.320987112741392E81

其实我们在使用while的循环操作大部分都可以使用递归,而使用递归是因为主要一个方法可以执行的操作很多,而且结构简单、好。

4、面向对象的前身是面向过程

两者的区别:笼统的将最好的例子就是面向过程是解决问题,面向对象是模块化设计。对于现在的程序就像是汽车组装,不同的工厂生产不同零件,将这些零件组装在一起可以形成一个汽车,当我们零件坏了的时候还可以进行配装。

面向对象在实际上还有很多的特点

  • 封装性:内部的操作对外部而言是不可见的。
  • 继承性:在上一辈的基础上继续发展。
  • 多态性:这是我们最为重要的一个环节,利用多态性才可以得到良好设计。

三个阶段:OOA(面向对象分析)、OOD(面向对象设计)、OOP(面向对象编程) 专业化术语

5、类与对象

类和对象是面向对象核心所在,也是所有概念的基础。类属于我们的引用类型,所以类的使用会牵扯到我们的内存分配问题

所谓的类就是一个共性的概念,而对象就是一个具体可以使用的事物。

类的组成:方法(操作行为)、属性(变量,描述每一个对象的具体特点)。

类的定义一般有class进行声明

1
2
3
4
java复制代码class 类名称{
属性1; //属性可以是多个
   属性2;
}

此时的方法不在由主类进行调用,而是要通过对象进行调用。

声明实例化对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码class Person{   //定义一个类首先要将类的名称每个首字母进行大写
public void info(){
System.out.println("name = "+ name + "\nage = " + age);
}
}
​
public class TestDemo5{
public static void main(String args[]){
//实例化对象第一种方式
Person person = new Person();
person.name = "张三"; //设置对象中的属性值
person.age = 13; //设置对象中的属性值
person.info();
//实例化对象第二种方式
Person person1 = null;
person1 = new Person();
person1.info();
}
}

image-20210726175038491

image-20210727163822257

image-20210727161759502

image-20210727164325831

引用数据类型最大的特征在于内存的分配操作,只要出现关键字new那么只有一个解释:开辟新的内存(内存是不可能无限开辟的,所以这个时候所谓的性能调优调整的就是内存问题)。

内存分析

我们使用的内存空间分为两块:堆内存空间(保存真正的数据,保存对象的属性信息)和栈内存空间(保存的堆内存的地址,堆内存操作权,简单理解叫保存对象的名称),所有数据类型必须在开辟空间后才能使用。如果使用了未开辟的数据类型则会出现NullPointerException,只有引用数据类型(数组、类、接口)才会产生此类异常,以后出现了根据错误位置观察其是否进行实例化对象。

image-20210727164540802

引用传递

引用传递的本质就在于别名,而这个别名只不过是放在我们栈内存当中,一块堆内存可以被多个栈内存所指向。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码class Person{   //定义一个类首先要将类的名称每个首字母进行大写
String name;
int age;
public void info(){
System.out.println("name = "+ name + "\nage = " + age);
}
}
​
public class TestDemo5{
public static void main(String args[]){
//实例化对象第一种方式
Person per = new Person();
per.name = "张三"; //设置对象中的属性值
per.age = 13; //设置对象中的属性值
per.info();
//实例化对象第二种方式
Person per1 = null;
per1 = new Person();
per1.name = "小于子";
per1.age = 30;
per1.info();
//此步骤就是引用传递的操作
Person per2 = per1;
per2.name = "狗剩";
per1.info();

}
}
​

image-20210727170204489

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

聊一聊代码规范 代码混乱的常见问题

发表于 2021-11-03

代码混乱的常见问题

很多时候我们项目迭代到后期,项目会变得很混乱,往往只有少数人能知道某段代码是干嘛的和该如何去改,或者是干脆谁都不知道,只能靠通过注释去猜测这段代码可能的作用。原因有可能是因为团队内部的人事变动,导致原先写这段代码的人不再管理这段代码了,并且代码写的实在是屎没人捋的清。往往我们称这类代码为“祖传代码”,就像祖宗传下来的代码一样,没人懂没人敢动。祖传代码一多,这个项目就变成了屎一样,开发人员再这基础上迭代就如同屎海翻腾,恶心别人也恶心自己。这是一个很可怕的恶心循环,我们如何去避免这种事情发生呢?先让我们分析下这类代码的通病

代码又臭又长

我见过最长的方法是5000多行,那段代码没人敢动,只敢往下加 if else,每次需要改这段代码的开发都战战兢兢,生怕出现什么莫名其妙的bug。java 可是一门面向对象的语言,一个方法里面有5000多行可以说是很可恶的事情了。我想一开始代码长度可能没这么夸张,是什么导致这种结果的?一个是当初写这段代码的人本身写的是直来直去的方法,一堆if else ;后面迭代的开发,面对这么长的代码瞬间失去了从头读到尾的耐心,直接继续在后面加 if else 迭代,最后这个方法就变成了一个缝合怪一样的玩意。

好的 sql 可以很大程度上简化代码的复杂程度,但是太过复杂sql 本身就会给后来的开发人员造成阅读困难,结果又是变成一条无人敢动的祖传代码,我想这应该是不少公司极度抵制存储过程的原因之一。当然不少银行应用开发还是大量使用存储过程,存储过程有用武之地的,但是一个又臭又长的存储过程就等着变成祖传代码吧。当年我见到一个60多个join的sql,看到第一眼就惊为天人从此难以忘怀,当然那段sql也成了没人敢去动的代码了。

代码逻辑不明所以

代码逻辑不明所以是我们开发很容易去犯的毛病,是一个不致命却烦人的毛病。在代码上的体现是,逻辑判断写的比较反人类各种双重否定是肯定,不把你绕晕不罢休。或者是写起代码来东一榔头西一棒槌,让人不知道你想干嘛。导致这个的原因有可能是开发人员在需求理解上出现偏差,做到后面发现不对劲,再回去改又不大可能了,只能硬着头皮往下写,结果就是代码弯弯绕绕;还有很重要的锅是在产品经理,任意变更需求,想一出是一出,开发人员无奈只能跟着想一出写一出。还用可能是开发人员方法或者类命名太艺术了,什么四川方言拼音这种没有十年脑血栓想不出的命名咱就不说了。就说那种国产凌凌漆式的无厘头命名——这看上去是个刮胡刀实际上是个吹风机,就这种不知道让人说什么好。

规划代码的核心思想

吐槽了一堆代码规范问题,接下来我们说说如何去规范我们的代码以及如何做到就算开发人员更换了,或者项目转手给他人了,仍然可以让后面的开发可以无碍的去阅读代码修改代码。当然各个公司/团队都有自己的一套代码规范,比如项目的结构、代码命名风格、代码格式等等。不同团队有不同的风格,但核心思想是大同小异的。接下来我就我个人的开发经验来分享一下一些代码规范的思想。

花叶论

就我个人而言,这个理论是我代码规范中最浅显也是最核心的思想,只要稍微动动脑子就能想出这个思路出来。或许我们做业务开发的时候,大部分都在写crud,感觉似乎这部分代码没什么规范好说的,其实不然。对一段业务代码而言,我们可以将其分为四类:

  1. 数据校验
  2. 业务逻辑
  3. 数据转换
  4. 数据库交互(查询与持久化)

大部分时候我们最关心的是逻辑判断相关的代码,其次是数据库交互,对于远程调用的方法,我们就视其为一个普通的方法以简化模型,方法调用算业务逻辑部分的代码,对于读代码的人而言基本上不关心数据校验和数据的转换(DTO转VO等)。因此,代码应该分出一个主次,应该尽量把主逻辑给凸显出来,最好一眼看去就能让人明白这个方法或者这个类干了啥,步骤是什么样的。对于那些不重要但必要的代码我称其为叶,对于那些主要的代码我称其为花。叶是为了衬托花的,因此我们应该将那些叶子代码精简或者隐藏起来。

隐藏叶子代码,突出主干逻辑的一些手法

1)Converter(转换器)

大部分时候我们使用 bean 拷贝使用的是 BeanUtils 这个类来完成,然而一些稍微复杂的实体转换,这个类就无法胜任了,这个时候我们只能手动的 get set ,往往就是这些get set 方法掩盖了主干逻辑,让代码结构不清晰。因此我建议在你的业务逻辑代码中引入 1)Converter 这个角色来专门负责数据的传递与转换。

2)manager 层

无论我们使用的持久层框架是哪一种,jpa 或者 mybatis 我觉得我们都应该对持久层的部分方法进行简单封装一下,这也是阿里规范里面提倡的。这样做好处是明显的,我们做一个查询时往往要 set 一些查询条件或者对查询结果进行一些简单的判断,往往这类操作在业务代码可能有比较高的重复性。如果把这些代码放到业务逻辑代码里面,少量还好,多了的话就显得很臃肿了。如果把这种代码移到manager层里面去,不仅主业务逻辑代码不会被干扰,还能提高一定的代码复用率。

3)方法简单封装

假设我们一个方法要完成一端逻辑要分成三大步,而每一个步骤又分成几个小步骤,那我们就可以将这个方法拆分成三个方法,然后在这三个方法里面完成各自的步骤。这手法是很简单的,想必大家都能想到,但是我这里要介绍的是简化复杂方法封装的神器——函数式编程,我这里指的函数式编程不仅仅是 stream 流和 lambda 表达式的使用。函数式编程封装适用的场景是:整个流程比较固定,但是某几个步骤变化是不确定的。我们可以去看看 java.util.function 这个包的源码,你会发现这个包下面全是接口,这些接口被称为函数式接口。这些函数式接口总体上分为四类:

Function 类型:传入一个bean 返回另外一个bean
Consumer 类型:传入一个bean 无返回值
Predicate 类型:传入一个bean 返回布尔值
Supplier 类型:没有入参,有出参
以 Consumer 的使用为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public User getUser(Consumer<User> consumer){
User user=new User();
consumer.accept(user);
user=userMapper.getUser(user);
return user;
}

public void doSomething1(){
User user=getUser(user->{user.setId(1L)});
}

public void doSomething2(){
User user=getUser(user->{user.setName("xxx")});
}

函数式编程的想象空间很大,使用的得当必定会简化你的代码,提高代码复用率。但是在多线程中使用函数式要留意数据的可见性问题。

日志和注释的一些个人经验

1)日志

首先我们要明白日志是给人看的,你加这段日志时要考虑清楚,有没有人会去查这段日志,这段日志有没有用。然后我们查阅日志的时候,一般会通过关键词去搜索;因此我们打的日志一定要有关键词,而且这个关键词不要和其他日志重复,不要过长,便于搜索才是王道。大部分情况我们查看日志都是为了追溯bug,那么一个基本原则就是能通过日志分析出业务逻辑或者流程的走向,对此我建议打日志的地方:

数据更新:我们有必要知道写库的数据是不是正确的数据;
条件分支:便于我们分析业务走的哪一条逻辑;
批量写库:打上数据量大小的日志,便于我们分析性能瓶颈。
并不是所有的这些地方都应该打上日志,有的时候我们可能只需要通过一两条日志就能分析出整个流程的问题点在哪,这个时候其他的日志就显得多余了。还有我们打完日志之后应该在本地环境追溯一下,看看这些日志自己是否能读懂,是否有必要,是否少了重要参数。

2)注释

最基本的两个注释——类注释,方法注释相关规范阿里开发手册上就有,我这里就不复述了,我分享下我写注释的个人习惯。
方法注释上除了基本的注释,我还会将产品需求的原文贴重要的部分上去再写上日期,这样做的好处是让别人明白产品需求要求干啥这个方法该干啥,而且产品经理偷偷改需求你还能有追查的根据,有个小本本偷偷记录他的罪行。

代码注释我分享一个我偷师来的小技巧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码pulic void test(){
/** 1. 从excel 获取 vo*/
Workbook workBook = getWorkBook(wookbookStream);
//获取成员信息
Sheet userSheet = workBook.getSheetAt(3);
Map<String, UserVO> userVOMap = getUserForExcel(file, userSheet);
// 获取项目vo
Sheet projectSheet = workBook.getSheetAt(0);
ProjectVO projectVO = getProjectForExcel(file, isInsert, userVOMap);
// 获取任务vo
Sheet taskSheet = workBook.getSheetAt(1);
Map<String, TaskVO> taskVOMap = getTaskListForExcel(file, taskSheet, userVOMap);
/** 2. 插入数据 */
if (isInsert.get()){
......
}
/** 3.写入异常信息 */
if (!isInsert.get()) {
.....
}
}

如你所见,对于主干的步骤 我用 /** 1. / /*\ 2. / javadoc的注释来标注了,而普通的注释我用 // 标注,因为idea 在纯黑主题下会给 /*\ 这样的注释配上绿色,会比较显眼。我通过这种方式来强调我代码那些是花,哪些是叶子。当然这种方式实际上是不大符合代码规范的,小伙伴们理性取舍,这种手法未必好。

六大基本原则

对于面向对象的的语言,六大基本是很重要的开发准则,但似乎大部分人在写代码的时候都不大在意这个,这也是导致一个方法变得又臭又长的一个重要原因之一。对于类的复杂度我们应该遵循单一职责原则——一个类或者方法承担的职责越多,它被复用的可能性就越小,重构或者修改起来就会变得困难重重,我们应该尽量让一个方法只去做一件事情。

对于许多代码我们只要通过一些简单的手法就能很好的提高其扩展性,比如通过接口去实现类与类之间的协作就能提前解决掉许多未知隐患,而且运用得当的情况下还能满足开闭原则与里氏替换原则,其实service层的设计就有那么点味道了,而且spring的特性也支持接口注入List和map,然而许多开发多年的同学都不知道这个特性,这个特性在许多场景下可以提高代码的扩展性,众所周知,map可以减少代码的 if else 分支。

方法命名 ‘潜规则’
很多时候,好的方法命名本身就是对代码的一种注释,我这里好的方法命名是指大家约定俗成的命名规则。如果你多留心各个开源框架的代码都会发现一些特定的命名规则。阿里开发手册里面也列举不少命名前缀与后缀的规范,其实各个团队可以根据自己的实际情况规定一些命名规则,降低团队内部的代码阅读的成本。关于我的文章 设计模式杂谈

介绍过部分命名规则,感兴趣的小伙伴可以去看看。

代码提交及版本控制

正确代码提交日志格式可以帮助开发人员及时的缕清代码的修改历史,从而快速的定位问题。以git为例,我们大部分人提交日志就是几个字而已,当然你能够通过日志去定位到自己的修改历史的话,这样做也没什么大问题,但是对于团队而言,你的修改日志要让别人能看懂就得按一定的格式来写了。Git Commit message的 Angular规范中定义的 commit message 格式有3个内容:

Header Header部分有3个字段: type(必需), scope(可选), subject(必需)
Body 部分是对本次 commit 的详细描述,可以分成多行。
Footer不常用,可为空 包括不兼容变动、关闭issue。
这里由于篇幅问题不细说,感兴趣的小伙伴可以百度查查资料。我们团队不一定要按照这么严格的规则来,但是可以制定一个类似的规范来管理提交日志。

对于团队而言,gitflow 是一个很不错的开发流程。可以很大程度上管理好我们的分支代码,避免团队的人由于误操作而导致某个重要分支出现问题。下面贴出gitflow 流程图,对于其具体内容同样不会介绍太多,感兴趣小伙伴去百度吧

xxx

帮助代码规范的工具

本节主要介绍提高代码质量的idea插件和框架,当然大名鼎鼎的 阿里代码规范插件咱就不介绍了,想必大家多少了解。不过本人感觉这个插件并不适合一些团队,一是感觉这个规范太过严格,对开发人员素质要求太高,二是有的团队有自己的规范规则,而且有可能和阿里规范冲突,不适用于这个插件。下面介绍的插件可能不适合一些小伙伴。我列举出来大家自己寻思吧。

mapstruct
对于我而言是很喜欢这个东西的,这个框架解决的问题其实就是我上文提到的花叶论中的 “数据转换” 的问题。其实不少公司也有类似的概念——定义一个工具类作用是将 DO转VO 或者 VO转DTO等,一般这类类都是以 converter 结尾。而mapstruct这个框架通过编译期生成字节码来自动的生成bean的转换类。我们想将一个bean的数据赋值给另外一个bean只需要去定义接口即可。这样既减轻了开发人员的工作量还将无意义的get和set方法从逻辑代码块中剔除出去。这个框架的缺点是字节码缓存问题,用过类似自动生成字节码工具的小伙伴应该知道——mapstruct 是根据接口去自动生成类的,当我们更新了接口的时候,这个类有可能没重新生成,当然这只有用idea调试的时候才会有的问题,所以也不必太担心。

checkStyle
idea checkStyle 插件可以通过自定义配置文件来统一团队的代码风格和代码规范,降低团队的交流成本,一般配合 save actions Reborn 食用更佳。关于checkStyle的配置文件网上也不少,这里也不贴出来占篇幅了。

git flow
前文提到过git flow 给团队带来的好处,idea也有对应的插件——git Flow Integration,可以通过这个插件来规范我们的流程:

gitflow插件

开发新功能选择 start Feature 拉取分支,修复bug 选择 Start Bugfix 拉取分支,等等。此外还有 push on finish等功能,小伙伴如果感兴趣可以百度。

Git Commit Template
这个主要是用来规范git commit 的一个idea插件小工具了,github上也有类似的开源插件。团队内部也可以自己开发一个类似插件,比较简单,成本也不高。

代码规范的一些个人看法就聊到这了,喜欢的小伙伴可以分享一下哦。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JVM类加载技术的介绍 引言 类的启动 类的生命周期 类加载

发表于 2021-11-03

引言

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

一直以来,我是致力于传播正能量的,我写文章就用我自己的风格,自己的思路,不去随和、抄袭、迎合别人,我以事实为依据,在我能力范围叙述我的能力故事,我觉得能做到这一点就很好了,如果写出的文章不趁人意,那我也没有办法,努力去做就好了。

我们今天来介绍一下JVM的类加载技术,读完本篇文章你会了解类的生命周期,类的加载过程是如何从虚拟机开始的,整个流程是什么样的,至于有什么作用各位自己体会。

类的启动

关于类的启动,规范中是这样进行描述的:虚拟机初始和启动一个类是通过启动类加载器来指定初始类的启动的,(当前规范的版本是java8),然后链接和初始化该类,之后会调用公共方法main方法,也就是我们程序的入口,来拉动整个程序的运行。

image.png

对于初始类的选择呢,你要么作为参数就行提供,要么自己实现,并提供一个类加载器,使用该类加载器加载此应用,当然,其它选择也是可以的,只要你符合上述规范。
image.png

类的生命周期

类的生命周期也是老生长谈的话题:加载、验证、准备、解析、初始化、使用、卸载,下面我们分别聊一下:

  • 加载:找字节流,说白了就是找到我们的Class文件。
  • 验证:看看类文件是不是符合解析格式
  • 准备:为被加载类的静态字段分配内存
  • 解析:把符号引用,解析成实际引用。
  • 初始化:主要是初始化构造方法

类或接口的创建

这里说了类和接口的创建需要在方法区创建一个与之相匹配的内部表示,一个类或接口的创建是由另一个类或接口进行触发的,当然我们还可以通过java提供的类库触发,比如反射。

image.png

java的类库如下:

  • Reflection, such as the classes in the package java.lang.reflect and the class Class.
  • Loading and creation of a class or interface. The most obvious example is the class ClassLoader.
  • Linking and initialization of a class or interface. The example classes cited above fall into this category as well.
  • Security, such as the classes in the package java.security and other classes such as SecurityManager.
  • Multithreading, such as the class Thread.
  • Weak references, such as the classes in the package java.lang.ref.

然后,看这个类是不是数组类,如果不是数组类,那就使用类加载器进行加载它的二进制表示。
image.png

类加载器

类加载器种类

关于类加载器呢,一共有这么三种:启动类加载器,扩展类加载器,和应用类加载器。
启动类加载器加载java的核心库,比如jre下的rt.jar包,扩展类加载器呢,加载的是lib目录下ext下的包,剩下的就由我们的应用类加载器进行加载,比如我们自己定义的类。

此外,我们还可以自定义类加载器,用于加载一些特定的类。

java虚拟机支持两种类加载器:java虚拟机提供的启动类加载器和用户自定义的类加载器,每个用户自定义的类加载器都应该是抽象类Classloader的某个子类的实例。给用户提供自己定义的类加载器是为了便于扩展java虚拟机的功能,以支持动态加载并创建类。
image.png

类加载器的加载机制

关于类加载器的加载机制,无非是为了确保一个类只加载一次,看看规范是怎么说的,对于C的创建,你可以自己搞或者委托其他类加载器帮你搞,如果是你自己搞的,那就说明该类是你定义和创建的。

如果你把加载请求委托出去了,说明该类不是你直接定义的,这时可以说该类是你初始化创建的。可以看出规范说是可以委托,具体怎么委托并没有提到,那就由自己内部进行实现。

image.png

我们接着往下看:如果类是由(bootstrap 加载器)启动类加载器所定义的,那么用启动类加载器加载,如果是用户定义的,那就用户定义的类加载器加载,如果N是一个数组类,那么该数组类是由java虚拟机而不是类加载器创建的。

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Forest v1512 发布,声明式 HTTP 框架,

发表于 2021-11-03

Forest介绍

Forest 是一个开源的 Java HTTP 客户端框架,它能够将 HTTP 的所有请求信息(包括 URL、Header 以及 Body 等信息)绑定到您自定义的 Interface 方法上,能够通过调用本地接口方法的方式发送 HTTP 请求

现已超过 1600 star

stars

Forest 如何使用

Forest 不需要您编写具体的 HTTP 调用过程,只需要您定义一个接口,然后通过 Forest 注解将 HTTP 请求的信息添加到接口的方法上即可。请求发送方通过调用您定义的接口便能自动发送请求和接受请求的响应。

Forest 的工作原理

Forest 会将您定义好的接口通过动态代理的方式生成一个具体的实现类,然后组织、验证 HTTP 请求信息,绑定动态数据,转换数据形式,SSL 验证签名,调用后端 HTTP API(httpclient 等 API)执行实际请求,等待响应,失败重试,转换响应数据到 Java 类型等脏活累活都由这动态代理的实现类给包了。 请求发送方调用这个接口时,实际上就是在调用这个干脏活累活的实现类。

文档和示例

  • 项目主页
  • 中文文档
  • JavaDoc
  • Demo工程

本次更新

新增特性:

  • feat: getbody可以有key-value形式进行取值 (#I4FUSB:建议改进下getbody可以有key -value形式进行取值)

BUG FIX:

  • fix: URL参数会重复Encode (#I4FDJC:URL参数会重复Encode)
  • fix: {变量名}格式字符串模板在引用隐式变量时出错 (#I4EP04:{变量名}格式字符串模板在引用隐式变量时出错)
  • fix: 对于http://localhost/xxx:yyy这种形式的URL解析错误 (#I4GC5M:对于http://localhost/xxx:yyy这种形式的URL解析错误)
  • fix: httpclient和okhttp编码行为不一致 (#I4FRR5:httpclient和okhttp编码行为不一致)
  • fix: post请求的url为空的时候有bug (#I4F3XS:post请求的url为空的时候有bug)
  • fix: retrywhen中的异常被吃掉, 无法抛出. 且异常后仅触发一次重试 (#I4E4X7:retrywhen中的异常被吃掉, 无法抛出. 且异常后仅触发一次重试)
  • fix: Httpclient后端在连续异步发送请求后会出现I/IO报错 (#I47FD7:Httpclient后端在连续异步发送请求后会出现I/IO报错)

代码重构:

  • refactor: 重构后端代码: 表单类型Body部分
  • refactor: 重构后端: okhttp3
  • refactor: 重构后端: httpclient
  • refactor: 重构后端: 重写异步请求逻辑

代码优化:

  • optimize: Forest对于一些错误的响应处理不友好 (#I4EIDJ:Forest对于一些错误的响应处理不友好)

其它代码改动:

  • add: ForestBody类
  • add: Validations类
  • delete: OkHttp3不再使用的请求执行器类

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java中 ConcurrentHashMap 如何实现线程

发表于 2021-11-03

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」

🌊 作者主页:海拥

🌊 作者简介:🥇HDZ核心组成员、🏆全栈领域优质创作者、🥈蝉联C站周榜前十

🌊 粉丝福利:进粉丝群每周送四本书(每位都有),每月抽送各种小礼品(掘金搪瓷杯、抱枕、鼠标垫、马克杯等)

ConcurrentHashMap是一个哈希表,支持检索的全并发和更新的高预期并发。此类遵循与 Hashtable 相同的功能规范,并包含 Hashtable 的所有方法。ConcurrentHashMap 位于 java.util.Concurrent 包中。

语法:

1
2
3
java复制代码public class ConcurrentHashMap<K,V>
extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable

其中 K 指的是这个映射所维护的键的类型,V 指的是映射值的类型

ConcurrentHashmap 的需要:

  • HashMap虽然有很多优点,但不能用于多线程,因为它不是线程安全的。
  • 尽管 Hashtable 被认为是线程安全的,但它也有一些缺点。例如,Hashtable 需要锁定才能读取打开,即使它不影响对象。
  • n HashMap,如果一个线程正在迭代一个对象,另一个线程试图访问同一个对象,它会抛出 ConcurrentModificationException,而并发 hashmap 不会抛出 ConcurrentModificationException。

如何使 ConcurrentHashMap 线程安全成为可能?

  • java.util.Concurrent.ConcurrentHashMap类通过将map划分为segment来实现线程安全,不是整个对象需要锁,而是一个segment,即一个线程需要一个segment的锁。
  • 在 ConcurrenHashap 中,读操作不需要任何锁。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
java复制代码import java.util.*;
import java.util.concurrent.*;

// 扩展Thread类的主类
class GFG extends Thread {

// 创建静态 HashMap 类对象
static HashMap m = new HashMap();

public void run()
{

// try 块检查异常
try {

// 让线程休眠 3 秒
Thread.sleep(2000);
}
catch (InterruptedException e) {
}
System.out.println("子线程更新映射");
m.put(103, "C");
}
public static void main(String arg[])
throws InterruptedException
{
m.put(101, "A");
m.put(102, "B");
GFG t = new GFG();
t.start();
Set s1 = m.keySet();
Iterator itr = s1.iterator();
while (itr.hasNext()) {
Integer I1 = (Integer)itr.next();
System.out.println(
"主线程迭代映射和当前条目是:"
+ I1 + "..." + m.get(I1));
Thread.sleep(3000);
}
System.out.println(m);
}
}

输出:

1
2
3
4
5
6
java复制代码主线程迭代映射和当前条目是:101...A
子线程更新映射
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1493)
at java.base/java.util.HashMap$KeyIterator.next(HashMap.java:1516)
at Main.main(Main.java:30)

输出说明:

上述程序中使用的类扩展了 Thread 类。让我们看看控制流。所以,最初,上面的java程序包含一个线程。当我们遇到语句 Main t= new Main() 时,我们正在为扩展 Thread 类的类创建一个对象。因此,每当我们调用 t.start() 方法时,子线程都会被激活并调用 run() 方法. 现在主线程开始执行,每当子线程更新同一个地图对象时,都会抛出一个名为 ConcurrentModificationException 的异常。

现在让我们使用 ConcurrentHashMap 来修改上面的程序,以解决上述程序在执行时产生的异常。

示例 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码import java.util.*;
import java.util.concurrent.*;

class Main extends Thread {
static ConcurrentHashMap<Integer, String> m
= new ConcurrentHashMap<Integer, String>();
public void run()
{
try {
Thread.sleep(2000);
}
catch (InterruptedException e) {
}
System.out.println("子线程更新映射");
m.put(103, "C");
}
public static void main(String arg[])
throws InterruptedException
{
m.put(101, "A");
m.put(102, "B");
Main t = new Main();
t.start();
Set<Integer> s1 = m.keySet();
Iterator<Integer> itr = s1.iterator();
while (itr.hasNext()) {
Integer I1 = itr.next();
System.out.println(
"主线程迭代映射和当前条目是:"
+ I1 + "..." + m.get(I1));
Thread.sleep(3000);
}
System.out.println(m);
}
}

输出

1
2
3
4
5
java复制代码主线程迭代映射和当前条目是:101...A
子线程更新映射
主线程迭代映射和当前条目是:102...B
主线程迭代映射和当前条目是:103...C
{101=A, 102=B, 103=C}

输出说明:

上述程序中使用的 Class 扩展了Thread 类。让我们看看控制流,所以我们知道在 ConcurrentHashMap 中,当一个线程正在迭代时,剩余的线程可以以安全的方式执行任何修改。上述程序中主线程正在更新Map,同时子线程也在尝试更新Map对象。本程序不会抛出 ConcurrentModificationException。

Hashtable、Hashmap、ConcurrentHashmap的区别

Hashtable Hashmap ConcurrentHashmap
我们将通过锁定整个地图对象来获得线程安全。 它不是线程安全的。 我们将获得线程安全,而无需使用段级锁锁定 Total Map 对象。
每个读写操作都需要一个objectstotal 映射对象锁。 它不需要锁。 读操作可以不加锁执行,写操作可以用段级锁执行。
一次只允许一个线程在地图上操作(同步) 不允许同时运行多个线程。它会抛出异常 一次允许多个线程以安全的方式操作地图对象
当一个线程迭代 Map 对象时,其他线程不允许修改映射,否则我们会得到 ConcurrentModificationException 当一个线程迭代 Map 对象时,其他线程不允许修改映射,否则我们会得到 ConcurrentModificationException 当一个线程迭代 Map 对象时,其他线程被允许修改地图,我们不会得到 ConcurrentModificationException
键和值都不允许为 Null HashMap 允许一个空键和多个空值 键和值都不允许为 Null。
在 1.0 版本中引入 在 1.2 版本中引入 在 1.5 版本中引入

写在最后的

我已经写了很长一段时间的技术博客,并且主要通过掘金发表,这是我的一篇关于Java中 ConcurrentHashMap 如何实现线程安全。我喜欢通过文章分享技术与快乐。您可以访问我的博客: juejin.cn/user/204034… 以了解更多信息。希望你们会喜欢!😊

💌 欢迎大家在评论区提出意见和建议!💌

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何选择普通索引和唯一索引《死磕MySQL系列 五》

发表于 2021-11-03

系列文章

一、原来一条select语句在MySQL是这样执行的《死磕MySQL系列 一》

二、一生挚友redo log、binlog《死磕MySQL系列 二》

三、MySQL强人“锁”难《死磕MySQL系列 三》

四、S 锁与 X 锁的爱恨情仇《死磕MySQL系列 四》

看过前几期文章的伙伴会发现并没有聊过关于索引和事务的知识点,这两个大点再之前的文章中已经写过了。

这里给大家一个传送门点击直接查看哈!

揭开MySQL索引神秘面纱

上来就问MySQL事务,瑟瑟发抖…

MVCC:听说有人好奇我的底层实现

幻读:听说有人认为我是被MVCC干掉的

接下来打开普通索引和唯一索引的世界。

一、了解普通索引和唯一索引

普通索引

MySQL中基本索引类型,没有什么限制,允许在定义索引的列中插入重复值和空值,纯粹为了查询数据更快一点。

唯一索引

索引列中的值必须是唯一的,但是允许为空值。

主键索引是一种特殊的唯一索引,不允许有空值。

扩展一下其它两中索引,知识点放在一起记忆会更好

全文索引

只能在char,varchar,text类型字段上使用全文索引,介绍了要求,说说什么是全文索引,就是在一堆文字中,通过其中的某个关键字等,就能找到该字段所属的记录行,比如有“你是个靓仔,靓女。。。”通过靓仔,可能就可以找到该条记录。

空间索引

空间索引是对空间数据类型的字段建立的索引,MySQL中的空间数据类型有四种,GEOMETRY、POINT、LINESTRING、POLYGON。在创建空间索引时,使用SPATIAL关键字。要求,引擎为Myisam,创建空间索引的列,必须将其声明为not null。

索引添加方式

1、 主键索引:alter table table_name add primary key (column)

2、 唯一索引:alter table table_name add unique (column)

3、普通索引:alter table table_name add index index_name (column)

4、全文索引:alter table table_name add fulltext (column)

5、多列索引:alter table table_name add index index_name (column1,column2,column3)

二、应用场景

现在你应该知道普通索引和唯一索引的区别,接下来看看在一些场景下如何选择两个索引。

丁老师文章中提到一个业务场景是市民系统,通过身份证号来查姓名。

这里咔咔也借用这个场景来给大家通过咔咔的思路描述一下这个流程。

执行语句为select name from user where card = ‘6104301996xxxxxxxx’;

这个场景第一反应肯定是给card创建一个索引,但创建什么索引呢?主键索引肯定不建议使用。

思考:为什么不能用身份证号来作为主键索引?

三、为什么不能用太大的值作为主键

Innodb存储引擎的主键索引结构如下图

普通索引数据结构如下图

主键索引的叶子节点存储的是对应主键的整行数据。

普通索引的叶子节点存储的是对应的主键值。

如果说B+Tree读取数据的深度是三层,每个磁盘的大小为16kb。

那在B+Tree中非叶子节点可以存储多少数据呢!一般来说我们每个表都会存在一个主键。

根据三层来计算,第一层跟第二层存储的是key值,也就是主键值。

都知道int类型所占的内存时4Byte(字节),指针的存储就给个6Byte,一共就是10Tybe,那么第一层节点就可以存储16 * 1000 /10 = 1600。

同理第二层每个节点也是可以存储1600个key。

第三层是叶子节点,每个磁盘存储大小同样安装BTree的计算一样,每条数据占1kb。

在B+Tree中三层可以存储的数据就是1600 * 1600 * 16 = 40960000

结论:若主键过大会直接影响索引存储的数据量,所以非常不建议使用过大的数据作为主键索引。

四、从查询的角度分析

假设现在要查card = 5 这条记录,查询过程为,先通过B+树从树根开始,按层搜索到叶子节点,然后通过二分法来定位card = 5 的这条记录。

普通索引

对于普通索引来说当找到card = 5这条记录后,还会继续查找,直到碰到第一个不满足card = 5的记录为止。

唯一索引

对于唯一索引就非常简单的了,唯一索引的特性就是数据唯一性,所以查到card = 5这条记录后就不在查找下一条记录了。

普通索引多查询的一次对性能影响大吗?

这个影响几乎可以忽略,在之前的几期文章中咔咔给大家普及了一个名词“局部性原理”。

数据和程序都有聚集成群的倾向,在访问了一条数据之后,在之后有极大的可能再次访问这条数据和这条数据的相邻数据。

所以说MySQL的Innodb存储引擎,在读取数据时也会采取这种局部性原理,每次读取的数据是16kb,也就是一页。

在Innodb存储引擎下每页的大小默认为16kb,这个参数也可以进行调整,参数为innodb_page_size。

但有一种情况虽说几率非常低,但还是需要知道的。

当索引为普通索引时,查到的数据正好是一页的最后一个数据,此时就需要读取下一页的数据,这个操作是有点复杂,但对于现在的CPU来说可以忽略不计。

五、了解change buffer

首先,需要先了解一个新的知识点change buffer。

当需要更新card = 5这条记录时,这条数据所在的数据页在内存中就直接更新,如若不在的话就需要将更新的操作缓存在change buffer中。当下次查询需要访问这个数据页时,将这个数据页读入内存,然后执行change buffer中与这个页有关的操作。

接着,了解另一个新的知识点merge。

当把change buffer中的数据应用到数据页,得到最新结果的过程成为merge,另外数据库正常关闭的过程中,也会执行merge操作。

结论:更新操作将记录先记录到change buffer中,可以减少磁盘I/O,语句执行速度会提升。

注意

1、 数据从change buffer读入内存是需要占用buffer pool的,使用change buffer可以避免占用内存。

2、change buffer 也是可以持久化数据的,change buffer 在内存中有拷贝,也会被写入到磁盘。

六、change buffer在什么条件下使用

思考:为什么唯一索引使用不到change buffer

唯一索引肯定是用不到,对于这个答案如果你感觉有点不适,就需要在回到之前几期文章再好好看看。

唯一索引插入一行数据时都会执行一次查询操作判断表中是否已经存在这条记录,判断是否违反唯一约束,既然必须得把数据页的数据读入内存,那还用change buffer个什么劲啊!

因此,只有普通索引可以使用。

在上文中知道了将change buffer数据读入内存时是需要占用buffer pool的内存,因此在MySQL中也给了一个参数来设置change buffer的大小。跟其它的数据单位可能有点出入,若设置为30,就表示change buffer只占用buffer pool内存的30%。

思考:在什么场景下不能使用change buffer?

change buffer的作用是将更新的动作缓存下来,所以对一个数据页做merge时,change buffer记录的变更越多,收益就越大。

但也并不是所有场景都适用,咔咔目前所开发的是一款账款软件,大部分更新后都是立马查看,这种情况是不是就违背了上面说的对一个数据页做merge时,change buffer记录的越多,收益越大。

因此,只有写多读少的场景,change buffer才能发挥非常大的作用。

思考:为什么更新完立马查询change buffer就没多大用处了呢?

一条记录发起更新操作后,先记录到change buffer 中,接着,当查询的数据在这个数据页时会立即触发merge,这样随机访问的IO的次数不会减少,反而增加了change buffer的维护代价。所以说这种业务模式使用change biffer会起到反作用。

思考:如何关闭change buffer

只需要将参数innodb_change_buffer_max_size = 0 即可。

七、从更新语句性能的影响的角度分析

第一种情况这条数据要更新的数据页在内存中。

唯一索引:在内存中查找是否有这条记录,不存在时则插入这个值。

普通索引:直接更新需要更新的值即可。

结论: 当要更新的数据页在内存中时,唯一索引就比普通索引多一次判断。

第二种情况这条数据要更新的数据页不在内存中。

唯一索引:需要将这条数据所在的数据页读入内存中,查找是否存在这条记录,然后更新数据。

普通索引:将这条要更新的数据记录在change buffer即可。

结论: change buffer 当更新的数据不在数据页中时,如果你的索引是普通索引则可以很显著的提升性能。

注意: 当你把一个索引从普通索引改为唯一索引时一定要注意change buffer的影响,会直接影响内存命中率。

八、总结

回到文章主题如何选择普通索引和唯一索引,在查询方面两者是没有什么差别的,主要是在更新操作上的影响。

如果你的业务跟咔咔的场景一样,更新后立马要对这个记录查询,那么就可以选择直接关闭change buffer。

若不是这种场景,则尽量选择普通索引,使用change buffer可以非常明显的提升更新性能。

“

坚持学习、坚持写作、坚持分享是咔咔从业以来所秉持的信念。愿文章在偌大的互联网上能给你带来一点帮助,我是咔咔,下期见。

”

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

什么猫咪最受欢迎?Python爬取全网猫咪图片,哪一款是你最

发表于 2021-11-03

一起用代码吸猫!本文正在参与【喵星人征文活动】。

前言

采集目标

网页资源地址:image.baidu.com/search/inde…

QQ截图20211103141819.png

工具准备

开发工具:pycharm

开发环境:python3.7, Windows11

使用工具包:requests

项目思路解析

做爬虫案例首先需要明确自己的采集目标,白又白这里采集的是当前网页的所有图片信息,有目标后梳理自己的代码编写流程,爬虫的基本四步骤:

  • 第一步:获取到网页资源地址
  • 第二步:对地址发送网络请求
  • 第三步:提取对应数据信息
    • 提取数据的方式一般有正则、xpath、bs4、jsonpath、css选择器
  • 第四步:保存数据信息

第一步:找数据地址

数据的加载方式一般有两种,一种静态一种动态,当前网页的数据在往下刷新时不断的加载数据,可以判断出数据加载的方式为动态的,动态数据需要通过浏览器的抓包工具获取,鼠标右击点击检查,或者按f12的快捷方式,找到加载的数据地址

image.png

找到对应数据地址,点击弹出的接口后可以点击预览,预览打开的页面是展示给我们的数据,在数据多的时候通过他来进行查看,获取的数据是通过网址获取的,网址数据在请求里,对网址发送网络请求

第二步:代码发送网络请求

发送请求的工具包会非常多,入门阶段更多的是使用requests工具包,requests是第三方工具包,需要进行下载:pip install requests 发送请求时需要注意我们通过代码请求,web服务器会根据http请求报文来进行区分是浏览器还是爬虫,爬虫不受欢迎的,爬虫代码需要对自己进行伪装,发送请求时带上headers传输的数据类型为字典键值对,ua字段是非常重要的浏览器的身份证

第三步:提取数据

当前获取的数据为动态数据,动态数据动态数据一般都是json数据,json数据可以通过jsonpath直接提取,也可以直接转换成字典,通过Python提取最终的目的是提取到图片的url地址

image.png

image.png

提取出新的地址后需要再次对网址发送请求,我们需要的是图片数据,链接一般是保存在数据中,发送请求获取图片对应的进制数据

第四步: 保存数据

数据获取到之后将数据进行储存,选择自己数据储存的位置,选择写入方式,我们获取的数据是进制数据,文件访问模式用的wb,将获取到的图片进入数据写入就行,文件的后缀需要是图片结尾的后缀,可以选择用标题命名,白又白使用网址后部分进行命名。

简易源码分享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
python复制代码import requests  # 导入请求的工具包
import re # 正则匹配工具包

# 添加请求头
headers = {
# 用户代理
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
# 请求数据来源
# "Referer": "https://tupian.baidu.com/search/index",
# "Host": "tupian.baidu.com"
}

key = input("请输入要下载的图片:")
# 保存图片的地址
path = r"图片/"
# 请求数据接口
for i in range(5, 50):
url = "https://image.baidu.com/search/acjson?tn=resultjson_com&logid=12114112735054631287&ipn=rj&ct=201326592&is=&fp=result&fr=&word=%E7%8C%AB%E5%92%AA&queryWord=%E7%8C%AB%E5%92%AA&cl=2&lm=-1&ie=utf-8&oe=utf-8&adpicid=&st=-1&z=&ic=&hd=&latest=&copyright=&s=&se=&tab=&width=&height=&face=0&istype=2&qc=&nc=1&expermode=&nojc=&isAsync=&pn=120&rn=30&gsm=78&1635836468641="
# 发送请求
response = requests.get(url, headers=headers)
print(response.text)
# 正则匹配数据
url_list = re.findall('"thumbURL":"(.*?)",', response.text)

print(url_list)
# 循环取出图片url 和 name
for new_url in url_list:
# 再次对图片发送请求
result = requests.get(new_url).content
# 分割网址获取图片名字
name = new_url.split("/")[-1]
print(name)
# 写入文件
with open(path + name, "wb")as f:
f.write(result)

我是白又白i,一名喜欢分享知识的程序媛❤️

感兴趣的可以关注我的公众号:白又白学Python【非常感谢你的点赞、收藏、关注、评论,一键三连支持】

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Scrapy+爬取豆瓣电影Top250信息 Scrapy爬虫

发表于 2021-11-03

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」

Scrapy爬虫框架

scrapy是什么

它是一个快速功能强大的开源网络爬虫框架

Github地址:github.com/scrapy/scra…

官网地址:scrapy.org/

在这里插入图片描发

scrapy的安装

cmd上运行

pip install scrapy

测试: scrapy -h

一般直接pip install scrapy会出错,可参考:【转】

blog.csdn.net/qq_42543250…

安装成功后测试会(scrapy -h):
在这里插入图片描述

Scrapy爬虫框架结构

“5+2”结构
在这里插入图片描述

框架组件:

组件 作用
Scrapy Engine 引擎,处理整个框架的数据流
Scheduler 调度器,接收引擎发过来的请求,将其排至队列中,当引擎再次请求时返回
Downloader 下载器,下载所有引擎发送的请求,并将获取的源代码返回给引擎,之后由引擎交给爬虫处理
Spiders 爬虫,接收并处理所有引擎发送过来的源代码,从中分析并提取item字段所需要的数据,并将需要跟进的url提交给引擎,再次进入调度器
Item Pipeline 管道,负责处理从爬虫中获取的Item,并进行后期处理
Downloader Middlewares 下载中间件,可以理解为自定义扩展下载功能的组件
Spider Middlewares Spider中间件,自定义扩展和操作引擎与爬虫之间通信的功能组件

Scrapy爬虫的数据类型

  • Request类
  • Response类
  • Item类

Scrapy数据处理流程:

  1. 当需要打开一个域名时,爬虫开始获取第一个url,并返回给引擎
  2. 引擎把url作为一个请求交给调度器
  3. 引擎再次对调度器发出请求,并接收上一次让调度器处理的请求
  4. 引擎将请求交给下载器
  5. 下载器下载完成后,作为响应返回给引擎
  6. 引擎把响应交给爬虫,爬虫开始进一步处理,处理完成后有两个数据,一个是需要跟进的url,另一个是获取到的item数据,然后把结果返回给引擎
  7. 引擎把需要跟进的url给调度器,把获取的item数据给管道
  8. 然后从第2步开始循环,知道获取信息完毕。只有调度器中没有任何请求时,程序才会停止

Scrapy爬虫的基本使用

yield关键字的使用

  • 包含yield语句的函数是一个生成器
  • 生成器每次产生一个值(yield语句),函数被冻结,被唤醒后再产生一个值
  • 生成器是一个不断产生值的函数

Scrapy爬虫的常用命令

命令 说明 格式
startproject 创建一个新工程 scrapy startproject projectName
genspider 创建一个爬虫 scrapy genspider [options]name domain
settings 获得爬虫配置信息 scrapy settings [options]
crawl 运行一个爬虫 scrapy crawl spider
list 列出工程中所有爬虫 scrapy list
shell 启动URL调试命令行 scrapy shell [url]

Scrapy爬虫的使用步骤

  1. 新建项目 (scrapy startproject xxx):新建一个新的爬虫项目

创建工程:scrapy startproject mydemo
在这里插入图片描述

目录树:
在这里插入图片描述

工程目录下各个文件的作用

文件 作用
scrapy.cfg 配置文件
spiders 存放你Spider文件,也就是你爬取的py文件
items.py 相当于一个容器,和字典较像
middlewares.py 定义Downloader Middlewares(下载器中间件)和Spider Middlewares(蜘蛛中间件)的实现
pipelines.py 定义Item Pipeline的实现,实现数据的清洗,储存,验证。
settings.py 全局配置
  1. 明确目标 (编写items.py):明确你想要抓取的目标

items.py文件内容

在这里插入图片描述

  1. 制作爬虫 (spiders/xxspider.py):制作爬虫开始爬取网页

Spider爬虫模板:

在这里插入图片描述

  1. 存储内容 (pipelines.py):设计管道存储爬取内容

实例:豆瓣Top250信息-Scrapy爬虫

创建工程:

scrapy startproject douban
在douban目录下:
scrapy genspider douban_scrapy douban.com

明确目标

我们打算抓取movie.douban.com/top250
网站里的所有电影的序号、名称、介绍、评分、评论数、描述

1
2
3
4
5
6
7
python复制代码打开 douban 目录下的 items.py。

Item 定义结构化数据字段,用来保存爬取到的数据,有点像 Python 中的 dict,但是提供了一些额外的保护减少错误。

可以通过创建一个 scrapy.Item 类, 并且定义类型为 scrapy.Field 的类属性来定义一个 Item(可以理解成类似于 ORM 的映射关系)。

接下来,创建一个 ItcastItem 类,和构建 item 模型(model)。

items.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码import scrapy  
class DoubanItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
# 电影序号
serial_number = scrapy.Field()
# 电影名称
movie_name = scrapy.Field()
# 电影介绍
introduce = scrapy.Field()
# 电影星级
star = scrapy.Field()
# 电影评论数
evaluate = scrapy.Field()
# 电影描述
describe = scrapy.Field()

制作爬虫 (spiders/douban_scrapy.py)

在当前目录下输入命令,将在mySpider/spider目录下创建一个名为itcast的爬虫,并指定爬取域的范围:

scrapy genspider douban_scrapy movie.douban.com

打开 douban/spider目录里的 douban_scrapy.py,默认增加了下列代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
python复制代码#douban_scrapy.py
#-*- coding: utf-8 -*-
import scrapy
from douban.items import DoubanItem

class DoubanScrapySpider(scrapy.Spider):
name = 'douban_scrapy'
allowed_domains = ['movie.douban.com']
start_urls = ['https://movie.douban.com/top250']

def parse(self, response): # 解析的方法
# movie_list 的类型为<class 'scrapy.selector.unified.SelectorList'>
movie_list = response.xpath("//ol[@class ='grid_view']/li")
# 数据的查找
# self.log('movie_list 的类型为{}
for i_item in movie_list:
# item文件的导入
douban_item = DoubanItem()
# 数据的筛选
#extract():这个方法返回的是一个数组list,,里面包含了多个string,如果只有一个string,则返回['ABC']这样的形式。
#extract_first():这个方法返回的是一个string字符串,是list数组里面的第一个字符串。
douban_item['serial_number'] = i_item.xpath(".//div[@class='pic']/em/text()").extract_first()
douban_item['movie_name'] = i_item.xpath(".//div[@class='hd']/a/span[1]/text()").extract_first()
douban_item['introduce'] = i_item.xpath(".")
content = i_item.xpath(".//div[@class='bd']/p[1]/text()").extract()
for i_content in content:
contents = "".join(i_content.split())
douban_item['introduce'] = contents
douban_item['star'] = i_item.xpath(".//div[@class='star']/span[2]/text()").extract_first()
douban_item['evaluate'] = i_item.xpath(".//div[@class='star']/span[4]/text()").extract_first()
douban_item['describe'] = i_item.xpath(".//p[@class= 'quote']/span/text()").extract_first()
#将数据返回到pipeline,用生成器
yield douban_item
next_link = response.xpath("//span[@class ='next']/link/@href").extract()
# 解析下一页,规则,取后一页的xpath
if next_link:
next_link = next_link[0]
#Spider产生下一页的Request请求
yield scrapy.Request('https://movie.douban.com/top250'+next_link,callback=self.parse)

保存数据 (pipeline.py)

一.scrapy保存信息的最简单的方法主要有四种,-o 输出指定格式的文件,命令如下:

scrapy crawl douban_scrapy -o douban.json

json lines格式,默认为Unicode编码

scrapy crawl douban_scrapy -o douban.jsonl

csv 逗号表达式,可用Excel打开

scrapy crawl douban_scrapy -o douban.csv

xml格式

scrapy crawl douban_scrapy -o douban.xml

二、通过pipeline存储进mysql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
python复制代码pipeline.py
import pymysql
from twisted.enterprise import adbapi
from scrapy import log


class DoubanPipeline(object):
#使用teisted异步存储
def __init__(self, dbpool):
self.dbpool = dbpool

@classmethod
def from_settings(cls, settings):
dbparms = {
'host': "localhost",
'user': "root",
'port': 3306,
'passwd': "root",
'db': "mystudy",
'charset': 'utf8',
'cursorclass': pymysql.cursors.DictCursor,
'use_unicode': True
}

dbpool = adbapi.ConnectionPool('pymysql', **dbparms)
return cls(dbpool)

def process_item(self, item, spider):
# 使用Twisted 将MYSQL 插入变成异步执行
# runInteraction 第一个参数是一个函数
query = self.dbpool.runInteraction(self.do_insert, item)
query.addCallback(self.handle_error, item, spider) # 处理异常
return item

def handle_error(self, failure, item, spider):
# 处理异步插入的异常
print(failure)

def do_insert(self, cursor, item):
# 执行具体的插入
insert_sql = '''
insert into douban (serial_number,movie_name,introduce,star, evaluate,Mdescribe) values (%s, %s, %s, %s, %s, %s);
'''
cursor.execute(insert_sql,
(item['serial_number'], item['movie_name'], item['introduce'], item['star'], item['evaluate'],item['describe']))

一些配置

1
2
3
4
5
6
python复制代码#settings.py  
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36'
ROBOTSTXT_OBEY = False
ITEM_PIPELINES = {
'douban.pipelines.DoubanPipeline':10
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…428429430…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%